文章目录
参考来源:零声学院
kcp源码
什么是KCP?为什么使用KCP?
KCP是⼀个快速可靠协议,能以⽐ TCP浪费10%-20%的带宽的代价,换取平均延迟降低 30%-40%,且最⼤延迟降低三倍的传输效果。纯算法实现,并不负责底层协议(如UDP)的收发,需要使⽤者⾃⼰定义 下层数据包的发送⽅式,以 callback的⽅式提供给 KCP。 连时钟都需要外部传递进来,内部不会有任何⼀次系统调⽤。
TCP是为流量设计的(每秒内可以传输多少KB的数据),讲究的是充分利⽤带宽。⽽ KCP是为流速设计 的(单个数据包从⼀端发送到⼀端需要多少时间),以10%-20%带宽浪费的代价换取了⽐ TCP快 30%-40%的传输速度。
TCP信道是⼀条流速很慢,但每秒流量很⼤的⼤运河,⽽KCP是⽔流湍急的⼩激流。
KCP有正常模式和快速模式两种,
通过以下策略达到提⾼流速的结果:
- RTO翻倍vs不翻倍: TCP超时计算是RTOx2,这样连续丢三次包就变成RTOx8了,⼗分恐怖,⽽KCP启动快速模式后不x2, 只是x1.5(实验证明1.5这个值相对⽐较好),提⾼了传输速度。
- 选择性重传 vs 全部重传: TCP丢包时会全部重传从丢的那个包开始以后的数据,KCP是选择性重传,只重传真正丢失的数据包。
- 快速重传: 发送端发送了1,2,3,4,5⼏个包,然后收到远端的ACK: 1, 3, 4, 5,当收到ACK3时,KCP知道2被跳过1 次,收到ACK4时,知道2被跳过了2次,此时可以认为2号丢失,不⽤等超时,直接重传2号包,⼤⼤改善 了丢包时的传输速度。
- 延迟ACK vs ⾮延迟ACK: TCP为了充分利⽤带宽,延迟发送ACK(NODELAY都没⽤),这样超时计算会算出较⼤ RTT时间,延⻓ 了丢包时的判断过程。KCP的ACK是否延迟发送可以调节。
- UNA vs ACK+UNA: ARQ模型响应有两种,UNA(此编号前所有包已收到,如TCP)和ACK(该编号包已收到),光⽤UNA将 导致全部重传,光⽤ACK则丢失成本太⾼,以往协议都是⼆选其⼀,⽽ KCP协议中,除去单独的 ACK包 外,所有包都有UNA信息。
- 非退让流控: KCP正常模式同TCP⼀样使用公平退让法则,即发送窗口大小由:发送缓存大小、接收端剩余接收缓存大小、丢包退让及慢启动这四要素决定。但传送及时性要求很高的⼩数据时,可选择通过配置跳过后两步, 仅用前两项来控制发送频率。以牺牲部分公平性及带宽利用率之代价,换取了开着BT都能流畅传输的效果。
KCP临时头部
临时存储的头部,并非真正头部格式
struct IKCPSEG
{
struct IQUEUEHEAD node;
IUINT32 conv; // 会话编号,和TCP的con一样,确保双方需保证conv相同,相互的数据包才能被接收.conv唯一标识一个会话
IUINT32 cmd; // 区分不同的分片.IKCP_CMD_PUSH数据分片;IKCP_CMD_ACK:ack分片;IKCP_CMD_WASK:请求告知窗口大小;IKCP_CMD_WINS:告知窗口大小
IUINT32 frg; // 标识segment分片ID,用户数据可能被分成多个kcp包发送
IUINT32 wnd; // 剩余接收窗口大小(接收窗口大小-接收队列大小),发送方的发送窗口不能超过接收方给出的数值
IUINT32 ts; // 发送时刻的时间戳
IUINT32 sn; // 分片segment的序号,按1累加递增
IUINT32 una; // 待接收消息序号(接收滑动窗口左侧).对于未丢包的网络来说,una是下一个可接收的序号,如收到sn=10的包,una为11
IUINT32 len; // 数据长度
IUINT32 resendts; // 下次超时重传时间戳
IUINT32 rto; //该分片的超时等待时间,其计算方法同TCP
IUINT32 fastack; // 收到ack时计算该分片被跳过的累计次数,此字段用于快速重传,自定义需要几次确认开始快速重传
IUINT32 xmit; // 发送分片的次数,每发一次加1.发送的次数对RTO的计算有影响,但是比TCP来说,影响会小一些.
char data[1];
};
KCP头部格式
这个才是KCP真正的头部格式
KCP对象
发送接收的数据由KCP对象管理
// ts_xxx 时间戳相关
// wnd 窗口相关
// snd发送相关
// rcv接收相关
struct IKCPCB
{
IUINT32 conv; // 标识会话
IUINT32 mtu; // 最大传输单元,默认数据为1400,最小为50
IUINT32 mss; // 最大分片大小,不大于mtu
IUINT32 state; // 连接状态(0xffffffff表示断开连接)
IUINT32 snd_una; // 第一个未确认的包
IUINT32 snd_nxt; // 下一个待分配包的序号,这个值实际是用来分配序号的
IUINT32 rcv_nxt; // 待接收消息序号.为了保证包的顺序,接收方会维护一个接收窗口,接收窗口有一个起始序号rcv_nxt
// 以及尾序号rcv_nxt + rcv_wnd(接收窗口大小)
IUINT32 ts_recent;
IUINT32 ts_lastack;
IUINT32 ssthresh; // 拥塞窗口的阈值
IINT32 rx_rttval; // RTT的变化量,代表连接的抖动情况
IINT32 rx_srtt; // smoothed round trip time,平滑后的RTT;
IINT32 rx_rto; // 收ACK接收延迟计算出来的重传超时时间
IINT32 rx_minrto; // 最小重传超时时间
IUINT32 snd_wnd; // 发送窗口大小
IUINT32 rcv_wnd; // 接收窗口大小,本质上而言如果接收端一直不去读取数据则rcv_queue就会满(达到rcv_wnd)
IUINT32 rmt_wnd; // 远端接收窗口大小
IUINT32 cwnd; // 拥塞窗口大小, 动态变化
IUINT32 probe; // 探查变量, IKCP_ASK_TELL表示告知远端窗口大小。IKCP_ASK_SEND表示请求远端告知窗口大小;
IUINT32 current;
IUINT32 interval; // 内部flush刷新间隔,对系统循环效率有非常重要影响, 间隔小了cpu占用率高, 间隔大了响应慢
IUINT32 ts_flush; // 下次flush刷新的时间戳
IUINT32 xmit; // 发送segment的次数, 当segment的xmit增加时,xmit增加(重传除外)
IUINT32 nrcv_buf; // 接收缓存中的消息数量
IUINT32 nsnd_buf; // 发送缓存中的消息数量
IUINT32 nrcv_que; // 接收队列中消息数量
IUINT32 nsnd_que; // 发送队列中消息数量
IUINT32 nodelay; // 是否启动无延迟模式。无延迟模式rtomin将设置为0,拥塞控制不启动;
IUINT32 updated; //是 否调用过update函数的标识;
IUINT32 ts_probe; // 下次探查窗口的时间戳;
IUINT32 probe_wait; // 探查窗口需要等待的时间;
IUINT32 dead_link; // 最大重传次数,被认为连接中断;
IUINT32 incr; // 可发送的最大数据量;
struct IQUEUEHEAD snd_queue; //发送消息的队列
struct IQUEUEHEAD rcv_queue; //接收消息的队列, 是已经确认可以供用户读取的数据
struct IQUEUEHEAD snd_buf; //发送消息的缓存 和snd_queue有什么区别
struct IQUEUEHEAD rcv_buf; //接收消息的缓存, 还不能直接供用户读取的数据
IUINT32 *acklist; //待发送的ack的列表 当收到一个数据报文时,将其对应的 ACK 报文的 sn 号以及时间戳 ts
//同时加入到acklist 中,即形成如 [sn1, ts1, sn2, ts2 …] 的列表
IUINT32 ackcount; //是当前计数, 记录 acklist 中存放的 ACK 报文的数量
IUINT32 ackblock; //是容量, acklist 数组的可用长度,当 acklist 的容量不足时,需要进行扩容
void *user; // 指针,可以任意放置代表用户的数据,也可以设置程序中需要传递的变量;
char *buffer; //
int fastresend; // 触发快速重传的重复ACK个数;
int fastlimit;
int nocwnd; // 取消拥塞控制
int stream; // 是否采用流传输模式
int logmask; // 日志的类型,如IKCP_LOG_IN_DATA,方便调试
int (*output)(const char *buf, int len, struct IKCPCB *kcp, void *user);//发送消息的回调函数
void (*writelog)(const char *log, struct IKCPCB *kcp, void *user); // 写日志的回调函数
};
KCP接口
// 创建一个kcp对象,每个不同的会话产生不同的对象.
// 因为kcp协议本身没有提供网络部分的代码,所以需要将udp发送函数的回调设置到kcp中,在有需要的时候,调用回调函数即可.
ikcpcb* ikcp_create(IUINT32 conv, void *user);
// 释放kcp对象
void ikcp_release(ikcpcb *kcp);
// 设置输出函数,需要的时候将被kcp内部调用
void ikcp_setoutput(ikcpcb *kcp, int (*output)(const char *buf, int len,
ikcpcb *kcp, void *user));
// 接收数据
int ikcp_recv(ikcpcb *kcp, char *buffer, int len);
// 发送数据
int ikcp_send(ikcpcb *kcp, const char *buffer, int len);
// 更新状态
void ikcp_update(ikcpcb *kcp, IUINT32 current);
// 输入数据,主要是将udp协议收到的数据传给kcp进行处理
int ikcp_input(ikcpcb *kcp, const char *data, long size);
// 调用 ikcp_flush 时将数据从 snd_queue 中 移入到 snd_buf 中,然后调用 kcp->output() 发送。
void ikcp_flush(ikcpcb *kcp);
//计算一帧数据的大小,一帧即一个消息报文
int ikcp_peeksize(const ikcpcb *kcp);
ikcp_create函数
malloc一个IKCPCB对象,初始化会话标识conv,将接收窗口、发送窗口、远端接收窗口设置默认值等。成功返回IKCPCB对象的指针,失败返回NULL。
ikcp_recv函数
ikcp_recv主要做三件事:
- 读取组好包的数据
- 将接收缓存rcv_buf的分片转移到接收队列rcv_queue
- 如果有接收空间则将kcp->probe |= IKCP_ASK_TELL; 以在update的时候告知对方可以发送数据了。
ikcp_recv步骤:
- 判断rcv_queue是否为空,为空则直接返回-1
- 使用ikcp_peeksize函数查看一帧数据的大小与可接收大小len比较,未接收到完整的一帧数据返回-2;len<peeksize,返回-3;
- 判断接收队列的消息数是否大于等于接收窗口,大于等于则将recover=1,标记可以开始窗口恢复。
- 将一帧完整的数据从rcv_queue队列复制到用户的data缓冲区
- 将rcv_buf队列的递增数据未丢包的连续数据提交到rcv_queue队列。例如:rcv_queue已经接收了0-8的序号的数据,下一个待接收的数据序号为9,如果此时rcv_buf里的报文为9,10,11,13;则将9、10、11,提交到rcv_queue队列中;如果rcv_buf里的报文序号为10,11,13;则不提交,待接收到序号为9的报文,在下一次接收再提交。
- 最后根据rcv_queue队列的消息数小于rcvwnd窗口&&recover条件判断是否执行kcp->probe |= IKCP_ASK_TELL;如果为真则执行;执行这个会在下一次update时,发送IKCP_CMD_WINS(接收窗口大小告知命令给对方)。
ikcp_send函数
把用户发送的数据根据MSS(max segment size)(mss=mtu-24(KCP报文头部大小))分片成KCP的数据分片格式,插入待发送队列中。当用户的数据超过一个MSS(最大分片大小)的时候,会对发送的数据进行分片处理。通过frg进行排序区分,frg即message中的segment分片ID,在message中的索引,由大到小,0表示最后一个分片。分成4片时,frg为3,2,1,0。如用户发送2900字节的数据,MSS为1400byte。因此,该函数会把1900byte的用户数据分成两个分片,一个数据大小为1400,头frg设置为2,len设置为1400;第二个分片,头frg设置为1,len设置为1400; 第三个分片, 头frg设置为0,len设置为100。切好KCP分片之后,放入到名为snd_queue的待发送队列中。
分片方式共有两种:
- 流模式情况下,检测每个发送队列里的分片是否达到最大MSS,如果没有达到就会用新的数据填充分片。
接收端会把多片发送的数据重组为一个完整的KCP帧。 - 消息模式下,将用户数据分片,为每个分片设置sn和frag,将分片后的数据一个一个地存入发送队列,接收方通过sn和frag解析原来的包,消息方式一个分片的数据量可能不能达到MSS,也会作为一个包发送出去。
不能一下send太长的数据, 当数据长度/mss大于对方接收窗口的时候则返回错误
ikcp_send步骤:
- 判断是否是流式KCP:如果是,则判断snd_queue是否为空。不为空,则将snd_queue最后一个节点的数据填满置mss或者空间足够的话,可以将此次发送的data直接附加到最后一个节点,如果data直接附加到最后一个节点了,则已经发送完成,返回0。非流式,直接跳到第二步骤。
- 计算数据可以被最多分成多少个frag,如果分片大于对方接受窗口的大小,直接返回-2。(如果是流式,此时可能已经附加一部分数据到snd_queue队列的最后一个节点了,buf指针指向未附加的数据);
- 将数据全部新建segment插入发送队列尾部,队列计数递增, frag递减。
ikcp_input函数
ikcp_input主要功能:
接收对方的数据输入
该函数主要是处理接收到的数据
校验数据 --> 解析数据 --> 处理数据(将合法的数据分片添加到接收buf中)–> 拥塞窗口处理
- 检测una,将una之前的分片从snd_buf清除(批量)
- 检测ack,对应ack sn分片从snd_buf清除(单个)
ikcp_input步骤:
- 判断是否打印日志,判断消息大小是否小于24,KCP消息大小最小为24;小于则直接返回-1;
- 先获取头部,判断conv是否相同;判断是否符合定义的四种消息格式,更新远端的接收窗口,删除snd_buf中序号小于una的消息,更新下一个需要对方应答的序号 snd_una。
- 遍历发送buf,进行快速确认(ack);即序号小于maxACK的的消息的fastack++;在下一次update时判断是否达到快速重传的设置的值,达到则进行重传。
- 如果snd_una增加了那么就说明对端正常收到且回应了发送方发送缓冲区第一个待确认的包,此时需要更新cwnd(拥塞窗口);如果拥塞窗口小于远端窗口,且拥塞窗口小于阈值则++;如果可发送最大数据量incr小于mss,incr更新为mss;计算incr+=(mss*mss)/incr+mss/16;如果(kcp->cwnd + 1) * mss <= kcp->incr,则进行kcp->cwnd = (kcp->incr + mss - 1) / ((mss > 0)? mss : 1);此时如果拥塞窗口大于远端窗口,拥塞窗口则使用远端窗口的大小,并设置最大发送数据量。
四种消息格式
- IKCP_CMD_ACK:更新rx_srtt,rx_rttval,计算kcp->rx_rto;遍历snd_buf中(snd_una, snd_nxt),将sn相等的删除,直到大于sn;更新控制块的 snd_una;快速重传标记置一,记录最大的ACK编号。
- IKCP_CMD_PUSH:判断序号sn小于rcv_nxt+rcv_wnd;对该报文的确认 ACK 报文放入 ACK 列表中;判断接收的数据分片编号是否符合要求,即:在接收窗口(滑动窗口)范围之内; 丢弃sn > kcp->rcv_nxt + kcp->rcv_wnd的segment;逐一比较rcv_buf中的segment,若重复丢弃,非重复,新建segment加入;检查rcv_buf的包序号sn,如果是待接收的序号rcv_nxt,且可以接收(接收队列小于接收窗口);转移segment到rcv_buf,nrcv_buf减少,nrcv_que增加,rcv_nxt增加;将rcv_buf队列顺序正确的消息加入到rcv_queu队列;
- IKCP_CMD_WASK:kcp->probe |= IKCP_ASK_TELL; 在update时告诉对方自己接收窗口的大小。
- IKCP_CMD_WINS:在前面已经更新对端的接收窗口大小,所以什么也不做。
重复步骤2,直到处理完data。
if (kcp->cwnd < kcp->rmt_wnd)
{
IUINT32 mss = kcp->mss; //最大分片大小
if (kcp->cwnd < kcp->ssthresh) { //拥塞窗口小于阈值
kcp->cwnd++; // 扩大窗口?
kcp->incr += mss;
} else {
if (kcp->incr < mss) kcp->incr = mss;
kcp->incr += (mss * mss) / kcp->incr + (mss / 16);
if ((kcp->cwnd + 1) * mss <= kcp->incr) {
#if 1
kcp->cwnd = (kcp->incr + mss - 1) / ((mss > 0)? mss : 1);
#else
kcp->cwnd++;
#endif
}
}
//如果拥塞窗口大于远端窗口
if (kcp->cwnd > kcp->rmt_wnd) {
kcp->cwnd = kcp->rmt_wnd; //则使用远端窗口
kcp->incr = kcp->rmt_wnd * mss; //并设置相应数据量,该数据量以字节数
}
}
ikcp_update函数和ikcp_flsh函数
ikcp_update步骤:
判断是否调用过update,第一次调用更新ts_flush时间为当前时间;
判断间隔是否大于10ms;是则更新ts_flush;
IINT32 slap;
kcp->current = current; // 超时重传要不要?
//判断是否调用过update,第一次调用更新ts_flush时间为当前时间
if (kcp->updated == 0) {
kcp->updated = 1;
kcp->ts_flush = kcp->current;
}
slap = _itimediff(kcp->current, kcp->ts_flush);
if (slap >= 10000 || slap < -10000) { // 至少10ms的间隔触发一次
kcp->ts_flush = kcp->current;
slap = 0;
}
if (slap >= 0) {
kcp->ts_flush += kcp->interval; // 先按interval叠加下一次要刷新的时间
if (_itimediff(kcp->current, kcp->ts_flush) >= 0) { // 如果下一次要刷新的时间已经落后则需要做校正
kcp->ts_flush = kcp->current + kcp->interval; // 使用当前时间 + interval进行校正
}
ikcp_flush(kcp);
}
ikcp_flush函数主要功能:
ikcp_flush 检查 kcp->update 是否更新,未更新直接返回。kcp->update 由 ikcp_update 更新,上层应用需要每隔一段时间(10-100ms)调用 ikcp_update 来驱动 KCP 发送数据;
准备将 acklist 中记录的 ACK 报文发送出去,即从 acklist 中填充 ACK 报文的 sn 和 ts 字段;
检查当前是否需要对远端窗口进行探测。由于 KCP 流量控制依赖于远端通知其可接受窗口的大小,
一旦远端接受窗口 kcp->rmt_wnd 为0,那么本地将不会再向远端发送数据,因此就没有机会从远端接受 ACK 报文,
从而没有机会更新远端窗口大小。在这种情况下,KCP 需要发送窗口探测报文到远端,待远端回复窗口大小后,后续传输可以继续。
在发送数据之前,先设置快重传的次数和重传间隔;KCP 允许设置快重传的次数,即 fastresend 参数。
例如设置 fastresend 为2,并且发送端发送了1,2,3,4,5几个分片,收到远端的ACK: 1, 3, 4, 5,
当收到ACK3时,KCP知道2被跳过1次,收到ACK4时,知道2被“跳过”了2次,此时可以认为2号丢失,
不用等超时,直接重传2号包;每个报文的 fastack 记录了该报文被跳过了几次,由函数 ikcp_parse_fastack 更新。
于此同时,KCP 也允许设置 nodelay 参数,当激活该参数时,每个报文的超时重传时间将由 x2 变为 x1.5,即加快报文重传:
- ack确认包
- 探测远端窗口
- 发送snd_buf数据分片
- 更新拥塞窗口
步骤:
- 循环发送ACK包,ACK包带有序号、时间戳,剩余接收窗口大小、una信息等。
- kcp->rmt_wnd为0,如果探测时间为0也为则初始化探测时间,默认为7s,否则检测是否到了探测时间(即是远端窗口为0发送过探测请求,但是已经超过下次探测的时间),到了探测时间则更新probe_wait,增加为IKCP_PROBE_INIT+ probe_wait /2,但满足KCP_PROBE_LIMIT,更新下次探测时间 ts_probe与 探测变量为 IKCP_ASK_SEND,立即发送探测消息 。
远端窗口正常,则不需要探测远端窗口不等于0,更新下次探测时间与探测窗口等待时间为0,不发送窗口探测。
if (kcp->rmt_wnd == 0) {
if (kcp->probe_wait == 0) { // 初始化探测间隔和下一次探测时间
kcp->probe_wait = IKCP_PROBE_INIT; // 默认7秒探测
kcp->ts_probe = kcp->current + kcp->probe_wait; // 下一次探测时间
}
else {
//远端窗口为0,发送过探测请求,但是已经超过下次探测的时间
//更新probe_wait,增加为IKCP_PROBE_INIT+ probe_wait /2,但满足KCP_PROBE_LIMIT
//更新下次探测时间 ts_probe与 探测变量 为 IKCP_ASK_SEND,立即发送探测消息
if (_itimediff(kcp->current, kcp->ts_probe) >= 0) { // 检测是否到了探测时间
if (kcp->probe_wait < IKCP_PROBE_INIT)
kcp->probe_wait = IKCP_PROBE_INIT;
kcp->probe_wait += kcp->probe_wait / 2;
if (kcp->probe_wait > IKCP_PROBE_LIMIT)
kcp->probe_wait = IKCP_PROBE_LIMIT;
kcp->ts_probe = kcp->current + kcp->probe_wait;
kcp->probe |= IKCP_ASK_SEND;
}
}
} else {
// 远端窗口正常,则不需要探测 远端窗口不等于0,更新下次探测时间与探测窗口等待时间为0,不发送窗口探测
kcp->ts_probe = 0;
kcp->probe_wait = 0;
}
// flush window probing commands
if (kcp->probe & IKCP_ASK_SEND) {
seg.cmd = IKCP_CMD_WASK; // 窗口探测 [询问对方窗口size]
size = (int)(ptr - buffer);
if (size + (int)IKCP_OVERHEAD > (int)kcp->mtu) {
ikcp_output(kcp, buffer, size);
ptr = buffer;
}
ptr = ikcp_encode_seg(ptr, &seg);
}
- 判断是否需要回复对端自身窗口大小
if (kcp->probe & IKCP_ASK_TELL) {
seg.cmd = IKCP_CMD_WINS; // [告诉对方我方窗口size], 如果不为0,可以往我方发送数据
size = (int)(ptr - buffer);
if (size + (int)IKCP_OVERHEAD > (int)kcp->mtu) {
ikcp_output(kcp, buffer, size);
ptr = buffer;
}
//为下一次发送准备好头部
ptr = ikcp_encode_seg(ptr, &seg);
}
kcp->probe = 0; //清空标识
- 控制拥塞窗口的大小
// calculate window size 取发送窗口和远端窗口最小值得到拥塞窗口小
cwnd = _imin_(kcp->snd_wnd, kcp->rmt_wnd); // 当rmt_wnd为0的时候,
// 如果没有做流控制则取配置拥塞窗口、发送窗口和远端窗口三者最小值
if (kcp->nocwnd == 0) cwnd = _imin_(kcp->cwnd, cwnd); // 进一步控制cwnd大小
- 移动不超过对方最大接受能力的报文,从snd_queue移动到snd_buf
// 此时如果
// 发送那些符合拥塞范围的数据分片
while (_itimediff(kcp->snd_nxt, kcp->snd_una + cwnd) < 0) {
IKCPSEG *newseg;
if (iqueue_is_empty(&kcp->snd_queue)) break;
newseg = iqueue_entry(kcp->snd_queue.next, IKCPSEG, node);
iqueue_del(&newseg->node);
iqueue_add_tail(&newseg->node, &kcp->snd_buf); // 从发送队列添加到发送缓存
kcp->nsnd_que--;
kcp->nsnd_buf++;
//设置数据分片的属性
newseg->conv = kcp->conv;
newseg->cmd = IKCP_CMD_PUSH;
newseg->wnd = seg.wnd;
newseg->ts = current;
newseg->sn = kcp->snd_nxt++; // 序号
newseg->una = kcp->rcv_nxt;
newseg->resendts = current;
newseg->rto = kcp->rx_rto;
newseg->fastack = 0;
newseg->xmit = 0;
}
- 发送第一次发送的报文,应该超时重传的报文,符合快速重传的报文,如果发生超时重传或快速重传会触发拥塞控制。报文发送时,会进行拼包发送,如两个600的消息会被拼成1200的包再发送;700+800的包则不拼。
- 进行拥塞控制判断,是否发送超时重传,快速重传,发送了则进行拥塞控制处理。