TCP/IP协议栈Lwip的设计与实现:之三

接上文:TCP/IP协议栈Lwip的设计与实现:之二_龙赤子的博客-CSDN博客

目录

10.TCP处理

10.1概述

10.2数据结构

10.3序列号计算

10.4数据入队和传输

10.5接收段数据

10.6接受新的连接

10.7快速重传

10.8计时器

10.9轮回时间评估

10.10拥塞控制


10.TCP处理

TCP是传输层协议,它为应用程序提供可靠字节流服务。TCP相比于其他协议更加复杂,TCP代码量是整个LWIP协议栈代码的50%。

10.1概述

基本的TCP处理被分为6个函数来完成(如图9所示)。函数tcp_input()、tcp_process()以及tcp_receive()与TCP输入处理相关,相应的tcp_write()、tcp_enqueue()以及tcp_output()处理输出相关。

当应用程序想要发送TCP数据时,调用tcp_write()。函数tcp_write()传输控制给tcp_enqueue(),如果需要的话,该函数将把数据分解为合适大小的TCP段,然后将这些段放到当前连接的传输队列中。函数tcp_output()之后将查看发送数据是否可能,比如,如果接收窗口有足够的空间,如果拥塞窗口足够大等等,满足的话使用ip_route()和ip_output_if()发送数据。

当ip_input()确认IP头之后并将TCP段交给tcp_input()之后,输入处理就开始了。在该函数中,进行初始的合理检查(比如校验和和TCP可选项的语法)以及确认数据段属于哪一个TCP连接。数据段之后被tcp_process()处理,它实现了TCP状态机,完成任何需要的状态传输。如果链路输入从网络接收数据的状态,tcp_receive()函数将被调用。如果是这样,tcp_receive()将把段数据传给上层的应用程序。如果段数据构成一个不可确认数据(之前已缓冲)的ACK,数据将被从缓冲区移走,其占用的内存将被回收。同样,如果数据确认响应ACK被接收到,接收者可能期望接收更多的数据,因此会调用tcp_output()。

10.2数据结构

在LWIP被设想用到的最小系统中,由于内存的限制,在TCP实现中所用到的数据结构保持较小的尺寸。在数据结构的复杂性与使用数据结构的代码的复杂性之间有个阀值,在这种情况下,代码复杂性被牺牲来保持较小尺寸的数据结构。

TCP PCB平心而论是比较大的,如图10所示。由于在LISTEN和TIME-WAIT状态的连接与其他状态的连接相比只需保持较少的状态信息,一个更小的PCB数据结构被用于这些状态。这个数据结构比起完整的PCB结构体已经过载,因此图10中PCB结构体中项目的排序显得有点尴尬(杂乱)。

所有TCP PCBs都被保持在一个链表上,通过next指针将它们连在一起。状态变量含有当前连接的TCP状态。再往下的项用来保存确定链接的IP地址和端口号。mss变量保存当前连接所允许的最大段的大小。

rcv_nxt与rcv_wnd变量在接收数据时使用。rcv_nxt域含有下一个期望从远程终端得到的字节序号,因此在发送ACKs到远程主机时使用。接收者的窗口被rcv_wnd保持,在发送TCP段中会突出这一点。tmr域被用来作为一个定时器,在特定长度时间过去之后,当前TCP连接就应当被移除,比如像处在TIME-WAIT状态的连接。链路上所允许的最大段的大小被保存在mss域中,flags域保存有链路的额外的状态信息,像连接是否属于快速恢复,或者一个延迟的ACK是否应当被发送。

rttest、rtseq、sa以及sv等域被用来进行轮回时间评估。被用来进行轮回时间评估的段的序号被保存在rtseq中,该段被发送的时间被保存在rttest中。平均轮回时间和轮回时间变量被保存在变量sa和sv中。在计算重传时间超时时这些变量就被使用,而这个基准的重传超时量保存在rto域中。

lastack和dupacks两个域在实现快速重传与快速恢复的实现中使用。lastack域保存被接收到的最后一个ACK确认的序列号,dupacks指示有多少个这样的ACKs,这些已经被接收到的ACK序列号保存在lastack中。当前连接的拥赛控制窗口被保存在cwnd域中,而慢启动的阈值保存在ssthresh中。

以下六个域snd_ack、snd_nxt、snd_wnd、snd_wl1、snd_wl2和snd_lbb在发送数据时使用。被接收者确认的最高的序列号保存在snd_ask中,下一个将要发送的序列号由snd_nxt保存。接收者的建议窗口由snd_wnd保存,snd_wl1和snd_wl2在更新snd_wnd时使用。snd_lbb含有传输队列中最后一个字节的序列号。

函数指针recv和recv_arg在将接收到的数据传送给应用层的时候使用。unsent、unacked和ooseq三个队列在发送和接收数据时使用。从应用层接收到但是还没有发送的数据被加入unsent队列,已经发送但是还没有被远程主机确认的数据保存在unacked队列,接收到的序外数据缓冲在ooseq中。

图11所示的tcp_seg数据结构是一个TCP段的内部表示,当段入队列的时候,结构体中的next指针将被用来进行链接。len域为TCP中段的长度,这就意味着,一个数据段的len域含有段中数据的长度,带有SYN和FIN标识的空段的len域会被设置为1。pbuf指针p指向实际段的缓冲区,tcphdr和data两个指针则分别指向段中的TCP头和数据。对于输出段,rtime域使用于该段的重传超时。因为输入段不需要被重传,所以,对于输入段该域就不需要,同时也不需要为它分配内存。

10.3序列号计算

TCP序列号是一个32位无符号数,用来对TCP字节流中的字节计数,其范围是0到2(32-1)次幂。因为发送到TCP链路上的字节数可能超过32位比特位所能组合表示的最大范围,因此序列号是通过模2(32)次幂来计算的。这也意味着通常的比较操作不可能用于TCP序列号中,相应的改变的操作符<和>被定义。

     

这里,s和t都是TCP序列号。>=和<=操作符也被类似定义。这些操作都作为C的宏在头文件中定义。

10.4数据入队和传输

将要发送的数据都被分为合适大小的块,并通过函数tcp_enqueue()赋予一个序列号。这儿,数据被打包到pbufs并被封装进一个tcp_seg结构体。TCP头建立在pbuf中,并用期望确认号——ackno以及建议窗口——wnd来填充。这些域中的值可以在段入队列的时候改变,因此也就能够被tcp_output()函数在进行实际段传输的时候设置。当段建立好后,就被送到PCB的未发送链上。tcp_enqueue()函数会努力为每个段填充最大可能尺寸的数据,当在未发送队列的末尾发现一个没有填充满的段时,会使用pbuf链功能函数为其附上新的数据。

在tcp_enqueue()格式化数据段并将其加入队列后,函数tcp_output()会被调用。它会检查当前的窗口是否有足够的空间来存放更多的数据。当前窗口可以通过使用最大拥塞窗口和被建议的接收者的窗口来计算。下一步它会填充没有被tcp_enqueue()填充的TCP头,并使用ip_route()和ip_output_if()来传输数据段。发送之后,该段将被放到unacked链上,直到一个数据段已经被接收的ACK收到。

当一个段在unacked链上后,它也在进行重传的计时,这在10.8节中会讲述。当一个段被重传时,原始段的TCP和IP头基本上会被保持,只需要对TCP头做一小点必需的改变。TCP头的ackno和wnd域要用当前的值进行设定,因为我们会在段的原始传输和重传这段时间内接收到数据。这只需要改变头的两个16位的字,而整个TCP校验和不需要重新计算,因为可以使用简单的算法[Rij94]来更新校验和。至于IP头,在段最开始传输的时候就被IP层添加了,这里也就没有任何理由来改变它。因此,重传不需要对IP头校验和进行重新的计算。

10.4.1愚蠢窗口避免

愚蠢窗口特征[Cla82b](SWS)是TCP所特有的,它能够导致产生非常糟糕的性能。当一个TCP接收者建议一个小的窗口而TCP发送者立即发送数据来填充这个窗口时SWS就产生了。当这个小数据段被确认,会再次打开较小量的窗口并且发送者也会再次发送一个比较小的段来填充该窗口,这会导致只由非常小的段组成的TCP流的产生。为了避免SWS,不管是发送者还是接收者都必须避免这种情形的产生。在仅有小窗口被提供时,接收者必须避免通知小窗口的更新,发送者必须避免小数据段的发送。

在LWIP中,SWS在发送端是天生免疫的,因为TCP段在构造和入队列时并不知道接收者的建议窗口大小。在一个大的传输中,输出队列将由最大尺寸的数据段构成。这就意味着,如果一个TCP接收者建议一个小的窗口,发送者将不会发送第一个数据段到队列上,因为它总会大于接收者的建议窗口。相反,它将会等待,直到窗口足够大以至于能够容纳该最大尺寸的数据段。

当作为TCP接收者时,LWIP将不会建议一个小于链路最大段尺寸的接收者的窗口。

10.5接收段数据

10.5.1多路分解

当TCP段到达tcp_input()函数时,将在TCP PCBs之间进行多路分解。多路分解的关键在于IP源和目的地址以及TCP端口号。在多路分解一个数据段时有两种类型的PCBs必须被区分,一种是与开放连接相关的,一种是与半开放连接相关的。半开放的连接是指那些处在LISTEN状态的连接并且仅仅有确定的本地TCP端口号和可选的本地IP地址,而开放的连接则是指有确定的(源和目的)IP地址和(源和目的)端口号的连接。

许多TCP的实现,像早期的BSD实现,使用这样一种技术,在其中一个带有单入口快速缓存的PCBs链被使用。这样做的基本理由是所有的TCP链路由大块传输构成,典型的表现就是本地的大数量的数据[Mog92],结果就是高cache命中率。其他的高速缓冲方案包括保持有两个单入口cache,一个用于与发送的最后一个包相关的PCB,一个用于最后接收的包的PCB[PP93]。一个相应的本地搜索方案可以通过将最近最常使用的PCB移到链头来完成。[MD92]中展现的这两种方法都胜过单入口cache方案。

在LWIP中,任何时候,在多路分解一个段时,当一个匹配的PCB被发现时,该PCB就被移到PCBs链头。尽管如此,对于处在LISTEN状态的链路的PCB,其仍然不会被移动到链头,因为这样的链路并不像接收数据的链路那样在链路建立后期望立即接收段数据。

10.5.2接收数据

对于输入数据段的真正的处理在函数tcp_receive()中完成。段的确认号会与链路的未确认队列中的段进行比较,如果该号高于未确认队列中段的序列号,该段会被移出队列,为该段分配的内存也会被释放。

如果段的序列号高于PCB结构体中的rcv_nxt变量,该输入段就是序外段。序外段将被加入PCB队列中的ooseq队列。如果输入段的序号等于rcv_nxt,该段会通过调用PCB中的recv函数分发到上层,并且rcv_nxt将被加上输入段的长度。因为一个序中段的接收可能意味着先前接收的序外段现在很可能是下一个期望接收的段,所以ooseq队列会被检查。如果它含有一个序列号等于rcv_nxt的段,该段会通过调用recv函数转发到应用层,同时更新rcv_nxt。该处理会持续直到ooseq队列为空或者在ooseq队列上的下一个段是序外段。

10.6接受新的连接

处在LISTEN状态的TCP链接,比如那些积极打开的链接,已经准备好去接受来在远程主机的新的链接。对于那些连接,一个新的TCP PCB被创建,并且必须传给打开初始TCP监听链路的应用程序。在LWIP中,这可以通过让应用程序注册一个回调函数来完成,该回调函数在一个新的连接建立时将被调用。

当一个处在LISTEN状态的连接接收到一个带有置位SYN标识的TCP段后,一个新的连接会被创建,同时一个带有SYN和ACK标识的段将被发送来响应该SYN段。该连接之后进入SYN-RCVD状态,并等待一个针对当前发送的SYN段的确认。在确认到达后,连接会进入ESTABLISHED状态,并调用accept函数(图10中PCB结构体的accept域)。

10.7快速重传

快速重传和快速恢复在LWIP中通过跟踪最后收到的确认的序列号来实现。如果另外一个针对该序列号的确认被接收到,则TCP PCB中的dupadks计数会被加一。当dupacks增加到3,处在unacked队列上的第一个段会被重传,快速恢复也会被初始化。快速恢复的实现所遵循的步骤在[APS99]中列出。只要对于新数据的ACK被收到,dupacks计数就会被复位为0。

10.8计时器

就像在BSD中TCP的实现,LWIP使用两个间隔周期分别为200和500毫秒的计数器。这两个计数器之后会被用来实现更加复杂的计数逻辑,比如作为重传的计数器,TIME-WAIT的计数器以及延迟ACK的计数器

 精确计数器 (The fine gained timer),tcp_timer_fine()遍历TCP PCB来检查是否有任何延迟的ACKs应当被发送,用tcp_pcb结构体中的标识域来指示。如果延迟ACK的标识被置位,一个空TCP确认段会被发送,之后将标识清除。

次精准计数器(The coarse grained timer),由函数tcp_timer_coarse()实现,也扫描PCB链。对于每一个PCB,未确认段组成的链(由tcp_seg结构体中的unacked指针指向)被遍历,rtime变量会被增加。如果rtime增加到大于PCB中rto变量给出的当前重传超时量,这个段会被重传,并且重传超时时间会被加倍。仅仅在拥塞窗口值和接收者建议的窗口值允许的情况下一个数据段才会被重传。重传后,拥塞窗口会被设置为最大段尺寸,慢启动阈值将被置为有效窗口大小的一半,慢启动在连接中被初始化。

对于处在TIME-WAIT状态中的连接,coarse grained timer也增加PCB结构体中的tmr域。当计数器达到2*MSL阀值后连接就被移除。

Coarse grained timer同样增加全局TCP时钟——tcp_ticks,该时钟在轮回时间评估和重传超时中使用。

10.9轮回时间评估

轮回时间评估是TCP中的关键部分,因为评估后的轮回时间在决定合适的重传超时时会用到。在LWIP中,轮回时间的测量以类似于BSD实现中的传统方法实现。每轮回一次轮回时间就会被测量,在[Jac88]中描述的平稳函数会被使用来计算产生一个合适的重传超时值。

TCP PCB变量rtseq持有被进行轮回时间测量的段的序列号。PCB中的rttest变量保存有段被第一次传输时的tcp_ticks值。当一个序列号等于或大于rtseq的ACK被收到后,轮回时间通过tcp_ticks减去rttest来计算。在轮回时间测量期间重传发生,测量结果不会被采用。

10.10拥塞控制

拥塞的实现出奇的简单,并由输入输出代码中的简单几行代码组成。当新数据的ACK收到后,拥塞窗口——cwnd或被增加一个最大段的大小,或增加  ,这取决于连接是在慢启动状态还是拥塞避免状态。当发送数据的时候,接收者建议窗口和拥塞窗口的最小值被用来决定多少数据能够被发送到每一个窗口中。

TCP/IP协议栈Lwip的设计与实现:之四_龙赤子的博客-CSDN博客

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

龙赤子

你的小小鼓励助我翻山越岭

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值