TCP/IP协议栈之LwIP(六)---网络传输管理之TCP协议

一、TCP协议简介

在传输层协议中,UDP是一种没有复杂控制,提供面向无连接通信服务的一种协议,它将部分控制转移给应用程序去处理,自己却只提供作为传输层协议的最基本功能。与UDP不同,TCP则是对传输、发送、通信等进行控制的协议。

TCP(Transmission Control Protocol)与UDP(User Datagram Protocol)的区别相当大,它充分实现了数据传输时各种控制功能,可以进行丢包时的重发控制,还可以对次序乱掉的分包进行顺序控制,而这些在UDP中都没有。此外,TCP作为一种面向有连接的协议,只有在确认通信对端存在时才会发送数据,从而可以控制通信流量的浪费。根据TCP的这些机制,在IP这种无连接的网络上也能够实现高可靠的通信。

为了通过IP数据报实现可靠性传输,需要考虑很多问题,例如数据的破坏、丢包、重复以及分片顺序混乱等问题。TCP通过校验和、序列号、确认应答、重发控制、连接管理、窗口控制等机制实现可靠性传输。

1.1 正面确认与超时重传

在TCP中,当发送端的数据到达接收主机时,接收端主机会返回一个已收到消息的通知,这个消息叫做确认应答(ACK)。TCP通过肯定的确认应答实现可靠的数据传输,当发送端将数据发出之后会等待对端的确认应答,如果有确认应答说明数据已经成功到达对端,反之则说明数据丢失的可能性很大。在一定时间内没有等到确认应答,发送端就可以认为数据已经丢失并进行重发,由此即使产生了丢包仍能保证数据能够到达对端,实现可靠传输。

未收到确认应答并不意味着数据一定丢失,也有可能是数据对方已经收到,只是返回的确认应答在途中丢失,这种情况也会导致发送到因没有收到确认应答而认为数据没有到达目的地,从而进行重新发送。也有可能因为一些其他原因导致确认应答延迟到达,在源主机重发数据以后才到达的情况也屡见不鲜。此时,源发送主机只要按照机制重发数据即可,但目标主机会反复收到相同的数据,为了对上层应用提供可靠的传输必须放弃重复的数据。为此,就必须引入一种机制,它能够识别是否已经接收数据,又能够判断是否需要接收。

上述这些确认应答处理、重传控制以及重复控制等功能都可以通过序列号实现。序列号是按顺序给发送数据的每一个字节都标上号码的编号。接收端查询接收数据TCP首部中的序列号和数据的长度,将自己下一步应该接收的序号作为确认应答返送回去,这样通过序列号和确认应答号,TCP可以实现可靠传输,整个过程如下图所示:
TCP正面确认机制
前面说到发送端在一定时间内没有等到确认应答就会进行数据重发,在重发数据之前等待确认应答到来的特定时间间隔就叫重发超时。那么这个重发超时的具体时间长度又是如何确定的呢?

最理想的是,找到一个最小时间,它能保证确认应答一定能在这个时间内返回,然而这个时间长短随着数据包途经的网络环境的不同而有所变化,例如跟网络的距离、带宽、拥堵程度等都有关系。TCP要求不论处在何种网络环境下都要提供高性能通信,并且不论网络拥堵情况发生何种变化,都必须保持这一特性。为此,它在每次发包时都会计算往返时间及其偏差(往返时间RTT估计)。将这个往返时间和偏差相加,重发超时时间就是比这个总和要稍大一点的值。往返时间的计算与重发超时的时间推移过程如下图所示:

TCP往返时间与重发超时
在BSD的Unix以及Windows系统中,超时都以0.5秒为单位进行控制,因此重发超时都是0.5秒的整数倍。不过由于最初的数据包还不知道往返时间,所以其重发超时一般设置为6秒左右。

数据被重发之后若还收不到确认应答,则进行再次发送,此时等待确认应答的时间将会以2倍、4倍的指数函数延长。但数据也不会无限、反复的重发,达到一定重发次数后,如果仍没有任何确认应答返回,就会判断为网络或对端主机发生了异常,强制关闭连接,并通知应用通信异常强行终止。

1.2 连接管理与保活机制

TCP提供面向有连接的通信传输,通信双方在有效数据交互之前,必须建立稳定的连接,同时初始化与连接相关的数据交互、控制信息。UDP是一种面向无连接的通信协议,因此不检查对端是否可以通信,直接将UDP数据包发送出去。TCP与此相反,它会在数据通信之前通过TCP首部发送一个SYN包作为建立连接的请求等待确认应答。如果对端发来确认应答,则认为可以进行数据通信,如果对端的确认应答未能到达,就不会进行数据通信。在TCP中,通信双方按照客户端–服务器模型建立连接的过程称为“三次握手”过程,图示如下:
TCP建立连接的过程
TCP提供全双工的连接服务,连接的任何一方都可以关闭某个方向上的数据传输,当一个方向上的连接被终止时,另一个方向还可以继续发送数据。当发送数据的一方完成数据发送任务后,它就可以发送一个FIN标志置1的握手包来终止这个方向上的连接,当另一端收到这个FIN包时,它必须通知应用层另一端已经终止了该方向的数据传输。发送FIN通常是应用层进行关闭的结果,收到一个FIN意味着在这个方向上已经没有数据流动,但在另一个方向上仍能发送数据,此时的连接处于半关闭状态。要完全关闭一条连接,需要四次报文交互的过程,称连接断开过程为“四次握手”过程,图示如下:
TCP连接断开过程
在建立TCP连接的同时,也可以确定发送数据包的单位,也即最大报文段长度(MSS:Maximum Segment Size),最理想的情况是,MSS正好是IP中不会被分片处理的最大数据长度。

TCP在传送大量数据时,是以MSS的大小将数据进行分割传送的,进行重发时也是以MSS为单位的。MSS是在三次握手的时候,在两端主机之间被计算得出的,两端的主机在发送建立连接的请求时,会在TCP首部中写入MSS选项,告诉对方自己的接口能够适应的MSS的大小,然后会在两者之间选择一个较小的值投入使用,整个过程图示如下:
TCP通信确定MSS大小
如果一个TCP连接已处于稳定状态,而同时双方都没有数据需要发送,则在这个连接之间不会再有任何信息交互。然而在很多情况下,连接双方都希望知道对方是否仍处于活动状态,TCP提供了保活定时器来实现这种检测功能。

TCP必须为服务器应用程序提供保活功能,服务器通常希望知道客户主机的运行状况,从而可以合理分配客户占用的资源。如果某条连接在两个小时内没有任何动作,则服务器就向客户端发送一个保活探查报文,若客户主机依然正常运行且从服务器仍可达,则服务器应用程序并不能感觉到保活探查的发生,TCP负责的保活探查工作对应用程序不可见;若客户主机崩溃或从服务器不可达等情况,服务器应用程序将收到来自TCP层的差错报文(比如连接超时、连接被对方复位、路由超时等),服务器将终止该连接并释放资源。

1.3 滑动窗口与缓冲机制

TCP以1个段为单位,每发一个段进行一次确认应答处理,这种传输方式有个缺点,包的往返时间越长通信性能就越低。为解决这个问题,TCP引入了窗口的概念,即使在往返时间较长的情况下,它也能控制网络性能的下降。引入了发送接收窗口后,确认应答不再以每个分段而是以更大的单位进行确认,转发时间将会被大幅度将会被大幅度的缩短。

窗口大小就是指无需等待确认应答而可以继续发送数据的最大值,这个机制实现了使用大量的缓冲区,通过对多个段同时进行确认应答的功能。在整个窗口的确定应答没有到达之前,如果其中部分数据出现丢包,那么发送端仍然要负责重传,为此发送端主机得设置缓存保留这些待被重传的数据,直到收到它们的确认应答。滑动窗口的结构如下图示:
TCP滑动窗口
滑动窗口可以看成定义在数据缓冲上的一个窗口,缓冲中存放了从应用程序传递过来的待发送数据。在滑动窗口以外的部分包括尚未发送的数据以及已经确认对端已收到的数据。当数据发出后若如期收到确认应答就可以不用再进行重发,此时数据就可以从缓存区清除。收到确认应答的情况下,将窗口滑动到确认应答中的序列号位置,这样可以顺序的将多个段同时发送提高通信性能,这种机制被称为滑动窗口控制

滑动窗口控制可以到达很好的流量控制效果和拥塞控制效果,实际上流量控制与拥塞控制的本质在于对发送窗口的合理调节。由于每个分段都会有确认应答,而滑动窗口的已确认序列号表示该序列号之前的所有数据都已收到确认应答,即便某些确认应答丢失也无需重发。如果某个报文段确实丢失了,同一个序列号的确认应答将会被重复不断的返回(接收端在没有收到自己所期望序列号的数据时,会对之前收到的数据进行确认应答),发送端主机如果连续3次收到同一个确认应答,就会将其所对应的数据进行重发。这种机制比前面介绍的超时重传更高效,因此也被称为快速重传控制。快速重传过程如下图示:
TCP快速重传机制
接收方为了接收数据,也必须在接收缓存上维护一个接收窗口,接收方需要将数据填入缓冲区、对数据进行顺序组织(因底层的报文可能是无序到达的,需要把无序报文组织为有序数据流并删除重复报文)等操作,并向发送方通告自己的接收窗口大小,它告诉发送方:我还能接收多少字节的数据。发送方应根据这个窗口通告值适当地调整发送窗口的大小,以调整数据的发送速度。

需要指出的是,TCP是全双工通信,两个方向上的数据传送是独立的,任何一方既可以作为发送端也可以作为接收端,因此任何一方都将为每个TCP连接维护两个窗口,一个用于数据接收,另一个用于数据发送,在一条完整的TCP连接上应该同时存在四个窗口。

1.4 流量控制与拥塞控制

发送端根据自己的实际情况发送数据,接收端可能因缓存耗尽或忙于处理其他任务而来不及处理到来的数据包,如果接收端将本应该接收的数据丢弃的话,就又会触发重传机制,从而导致网络流量的无端浪费。为了防止这种现象的发生,TCP提供了一种机制可以让发送端根据接收端的实际接收能力控制发送的数据量,这就是所谓的流量控制机制。

在TCP首部中,专门有一个字段用来通知接收窗口的大小,接收端主机将自己可以接收的缓存区大小放入这个字段中通知给发送端,发送端会发送不超过这个窗口限度的数据,这个字段的值越大说明网络的吞吐量越高。接收端这个缓冲区一旦面临数据溢出时,窗口大小的值也会随之被设置为一个更小的值通知给发送端,从而控制数据发送量。发送端主机根据接收端主机的指示,对发送数据的量进行控制的过程如下图示:
TCP流量控制机制
当接收端缓冲区用完后,不得不停止接收数据(此时接收窗口大小为0),在收到发送窗口更新通知后通信才能继续进行。如果这个窗口的更新通知在传送途中丢失,可能会导致无法继续通信,为避免此类问题的发生,发送端主机会定时(由坚持定时器persist timer管理该定时周期)的发送一个叫做窗口探测的数据段,次数据段仅含一个字节以获取最新的窗口大小信息。

有了TCP的窗口控制,收发主机之间即使不再以一个数据段为单位发送确认应答,也能够连续发送大量数据包。计算机网络都处于一个共享环境中,可能会因为其他主机之间的通信使得网络拥堵,如果在通信刚开始时就突然发送大量数据,可能会导致整个网络的瘫痪。TCP为了防止该问题的出现,在通信一开始时就会通过一个叫慢启动的算法得出的数值对发送数据量进行控制。

首先,为了在发送端调节所要发送数据的量,定义了一个叫做拥塞窗口的概念,在慢启动的时候将这个拥塞窗口大小设置为1个数据段(1 MSS)发送数据,之后每收到一次确认应答拥塞窗口的值就加1。在发送数据包时,将拥塞窗口的大小与接收端主机通知的窗口大小做比较,取其中较小的值作为实际发送窗口的大小。有了上述这些机制,就可以有效减少通信开始时连续发包导致的网络拥塞情况的发生。

不过,随着包的每次往返,拥塞窗口也会以1、2、4、8等指数函数增长(每收到一次确认应答拥塞窗口值加1,收到一个窗口大小数量的确认应答则拥塞窗口大小翻倍),拥堵情况激增甚至导致网络拥塞情况的发生。为了防止这些,TCP又引入了慢启动阈值的概念,只要拥塞窗口的值超过这个阈值,在每收到一次确认应答时,只允许以拥塞窗口大小的倒数为单位增加,即收到一个窗口大小数量的确认应答后拥塞窗口大小增加一个数据段,这是拥塞窗口大小是线性增长的,该变化过程如下图所示:
TCP拥塞窗口控制
TCP的通信开始时,并没有设置相应的慢启动阈值,而是在超时重传时,才会设置为当时拥塞窗口一半的大小。

由重复确认应答而触发的快速重传与普通的超时重传机制的处理多少有些不同,因为前者要求至少3次的确认应答数据段到达对方主机后才会触发,相比后者网络的拥堵要轻一些。所以由重复确认应答进行快速重传控制时,慢启动阈值的大小被设置为当时窗口大小的一半,然后将发送窗口的大小设置为该慢启动阈值 + 3个数据段的大小,相当于直接跨国慢启动阶段进入拥塞避免阶段,这种机制也称为快速恢复机制

1.5 提高网络利用率的其他机制

  • Nagle算法

TCP中为了提高网络利用率,经常使用一个叫做Nagle的算法,该算法是指发送端即使还有应该发送的数据,但如果这部分数据很少的话,则进行延迟发送的一种处理机制。具体来说就是仅在已发送的数据都已收到确认应答或可以发送最大段长度的数据时才能发送数据,如果两个条件都不满足则暂时等待一段时间后再进行数据发送。

根据这个算法虽然网络利用率可以提高,但可能会发生某种程度的延迟。在某些对响应实时性要求比较高的应用场景中使用TCP时,往往会关闭对该算法的启用。

  • 延迟确认应答

接收数据的主机如果每次都立刻回复确认应答的话,可能会返回一个较小的窗口,发送端主机收到这个小窗口通知后会以它为上限发送数据,从而又降低了网络利用率。为此引入了一个方法,在收到数据后不立即返回确认应答,而是延迟一段时间(直到收到2 MSS数据时为止,最大延迟0.5秒)发送确认应答。

TCP采用滑动窗口机制,通常确认应答少一些也不无妨,TCP文件传输时,绝大多数都是每两个数据段返回一次确认应答。

  • 捎带应答

根据应用层协议,发送出去的数据到达对端,对端处理后会返回一个回执,在双方通信过程中,为提高网络利用率,TCP的确认应答和回执数据可以通过一个包发送,这种方式叫做捎带应答。

接收数据传给应用处理生成回执数据需要一段时间,如果要实现捎带应答,需要确认应答等待回执数据的生成,如果没有启用延迟确认应答就无法实现捎带应答。延迟确认应答是能够提高网络利用率从而降低计算机处理负荷的一种较优的处理机制。

二、TCP协议实现

2.1 TCP报文格式

TCP协议有着自己的数据报组织格式,这里把TCP的数据包称为报文段(Segment),TCP报文段封装在IP数据报中发送。TCP报文段由TCP首部和TCP数据区组成,首部区域包含了连接建立与断开、数据确认、窗口大小通告、数据发送相关的所有标志与控制信息,TCP报文结构如下图所示:
TCP报文结构
TCP首部相比UDP首部要复杂得多,TCP中没有表示包长度和数据长度的字段,可由IP层获知TCP的包长再由TCP的包长可知数据的长度。TCP首部的大小为20~60字节,在没有任何选项的情况下,首部大小为20字节,与不含选项字段的IP报首部大小相同,TCP数据部分可以为空(比如建立或断开连接时)。

与UDP报文相同,源端口号和目的端口号两个字段用来标识发送端和接收端应用进程分别绑定的端口号。32位序号字段标识了从TCP发送端到TCP接收端的数据字节编号,它的值为当前报文段中第一个数据的字节序号。32位确认序号只有ACK标志置1时才有效,它包含了本机所期望收到的下一个数据序号(即上次已成功收到数据字节序号加1),确认常常和反向数据一起捎带发送。序列号与确认应答号共同为TCP的正面确认、超时重传、有序重组等可靠通信提供支持。

4位首部长度指出了TCP首部的长度,以4字节为单位,若没有任何选项字段则首部长度为5(5*4 = 20字节)。接下来的6bit保留字段暂未使用,为将来保留。再接下来是6个标志比特,它们告诉了接收端应该如何解释报文的内容,比如一些报文段携带了确认信息、一些报文段携带了紧急数据、一些报文段包含建立或关闭连接的请求等,6个标志位的意义如下表示:
TCP首部标志位
在TCP发送一个报文时,可在窗口字段中填写相应值以通知对方自己的可用缓冲区大小(以字节为单位),报文接收方需要根据这个值来调整发送窗口的大小。窗口字段是实现流量控制的关键字段,当接收方向发送方通知一个大小为0的窗口时,将完全阻止发送方的数据发送。

16位校验和字段的计算和上一章中UDP校验和计算过程与原理都相同,在UDP首部中校验和的计算是可选的,但在TCP中校验和的计算是必须的、强制的。TCP中校验和包含了伪首部、TCP首部和TCP数据区三部分,伪首部的概念与UDP中完全一样,只是伪首部中的协议字段值为6,与TCP相对应。

16位的紧急指针只有当紧急标志位URG置位时才有效,此时报文中包含紧急数据,紧急数据始终放到报文段数据开始的地方,而紧急指针定义出了紧急数据在数据区中的结束处,用这个值加上序号字段值就得到了最后一个紧急数据的序号。URG位置1的报文段将告诉接收方:这里面的数据是紧急的,你可以优先直接读取,不必把它们放在接收缓冲里面(即该报文段不使用普通的数据流形式被处理)。

TCP首部可包含0个或多个选项信息,选项总长度可达40字节,用来把附加信息传递给对方。每条TCP选项由三部分组成:1字节的选项类型 + 1字节的选项总长度 + 选项数据,具有代表性的选项如下表所示:
具有代表性的TCP选项
其中类型代码为2的选项是最大报文段长度(MSS),每个连接通常都在通信的第一个报文段(包含SYN标志的连接握手报文)中指明这个选项,用来向对方指明自己所能接受的最大报文段,如果没有指明则使用默认MSS为536,前面提到的客户端与服务器协商确定MSS的功能就是通过该选项实现的。

类型代码为3的选项是窗口扩大因子选项,可以让通信双方声明更大的窗口,首部中的窗口字段长度16bit,即接收窗口最大值为65535字节,在许多高速场合下,这样的窗口还是太小,会影响发送端的发送速度。使用该选项可以向对方通告更大的窗口,此时通告窗口大小值(假设为N)为首部中窗口大小字段值(假设为W)乘以2的窗口扩大因子值(假设为A)次幂(即N = W * 2^A)。

2.2 TCP数据报描述

TCP数据报首部比UDP复杂些,描述TCP的数据结构自然更复杂,在LwIP中用于描述TCP首部的数据结构如下:

// rt-thread\components\net\lwip-1.4.1\src\include\lwip\tcp_impl.h

/* Fields are (of course) in network byte order.
 * Some fields are converted to host byte order in tcp_input().
 */
PACK_STRUCT_BEGIN
struct tcp_hdr {
   
  PACK_STRUCT_FIELD(u16_t src);
  PACK_STRUCT_FIELD(u16_t dest);
  PACK_STRUCT_FIELD(u32_t seqno);
  PACK_STRUCT_FIELD(u32_t ackno);
  PACK_STRUCT_FIELD(u16_t _hdrlen_rsvd_flags);
  PACK_STRUCT_FIELD(u16_t wnd);
  PACK_STRUCT_FIELD(u16_t chksum);
  PACK_STRUCT_FIELD(u16_t urgp);
} PACK_STRUCT_STRUCT;
PACK_STRUCT_END

#define TCP_FIN 0x01U
#define TCP_SYN 0x02U
#define TCP_RST 0x04U
#define TCP_PSH 0x08U
#define TCP_ACK 0x10U
#define TCP_URG 0x20U
#define TCP_ECE 0x40U
#define TCP_CWR 0x80U

#define TCPH_HDRLEN(phdr) (ntohs((phdr)->_hdrlen_rsvd_flags) >> 12)
#define TCPH_FLAGS(phdr)  (ntohs((phdr)->_hdrlen_rsvd_flags) & TCP_FLAGS)

#define TCPH_HDRLEN_SET(phdr, len) (phdr)->_hdrlen_rsvd_flags = htons(((len) << 12) | TCPH_FLAGS(phdr))
#define TCPH_FLAGS_SET(phdr, flags) (phdr)->_hdrlen_rsvd_flags = (((phdr)->_hdrlen_rsvd_flags & PP_HTONS((u16_t)(~(u16_t)(TCP_FLAGS)))) | htons(flags))
#define TCPH_HDRLEN_FLAGS_SET(phdr, len, flags) (phdr)->_hdrlen_rsvd_flags = htons(((len) << 12) | (flags))

#define TCPH_SET_FLAG(phdr, flags ) (phdr)->_hdrlen_rsvd_flags = ((phdr)->_hdrlen_rsvd_flags | htons(flags))
#define TCPH_UNSET_FLAG(phdr, flags) (phdr)->_hdrlen_rsvd_flags = htons(ntohs((phdr)->_hdrlen_rsvd_flags) | (TCPH_FLAGS(phdr) & ~(flags)) )

#define TCP_TCPLEN(seg) ((seg)->len + ((TCPH_FLAGS((seg)->tcphdr) & (TCP_FIN | TCP_SYN)) != 0))

TCP首部中的各个标志位以宏定义的形式表示,同时定义了操作TCP首部各字段的宏定义。

与UDP的内容相同,在TCP实现中也专门使用一个数据结构来描述一个连接,把这个数据结构称为TCP控制块或传输控制块。TCP控制块中包含了双方实现基本通信所需要的信息,如发送窗口、接收窗口、数据缓冲区等,也包含了所有与该连接性能保障相关的字段,如定时器、拥塞控制、滑动窗口控制等。TCP协议实现的本质就是对TCP控制块中各个字段的操作:在接收到TCP报文段时,在所有控制块中查找,以得到和报文目的地相匹配的控制块,并调用控制块上注册的各个函数对报文进行处理;TCP内核维护了一些周期性的定时事件,在定时处理函数中会对所有控制块进行处理,例如把某些控制块中的超时报文段进行重传,把某些控制块中的失序报文段删除。TCP控制块是整个TCP协议的核心,也是整个内核中最大的数据结构,在LwIP中用于描述TCP控制块的数据结构如下:

// rt-thread\components\net\lwip-1.4.1\src\include\lwip\tcp.h

/* the TCP protocol control block */
struct tcp_pcb {
   
/** common PCB members */
  IP_PCB;
/** protocol specific PCB members */
  TCP_PCB_COMMON(struct tcp_pcb);

  /* ports are in host byte order */
  u16_t remote_port;
  
  u8_t flags;
#define TF_ACK_DELAY   ((u8_t)0x01U)   /* Delayed ACK. */
#define TF_ACK_NOW     ((u8_t)0x02U)   /* Immediate ACK. */
#define TF_INFR        ((u8_t)0x04U)   /* In fast recovery. */
#define TF_TIMESTAMP   ((u8_t)0x08U)   /* Timestamp option enabled */
#define TF_RXCLOSED    ((u8_t)0x10U)   /* rx closed by tcp_shutdown */
#define TF_FIN         ((u8_t)0x20U)   /* Connection was closed locally (FIN segment enqueued). */
#define TF_NODELAY     ((u8_t)0x40U)   /* Disable Nagle algorithm */
#define TF_NAGLEMEMERR ((u8_t)0x80U)   /* nagle enabled, memerr, try to output to prevent delayed ACK to happen */

  /* the rest of the fields are in host byte order
     as we have to do some math with them */

  /* Timers */
  u8_t polltmr, pollinterval;
  u8_t last_timer;
  u32_t tmr;

  /* receiver variables */
  u32_t rcv_nxt;   /* next seqno expected */
  u16_t rcv_wnd;   /* receiver window available */
  u16_t rcv_ann_wnd; /* receiver window to announce */
  u32_t rcv_ann_right_edge; /* announced right edge of window */

  /* Retransmission timer. */
  s16_t rtime;

  u16_t mss;   /* maximum segment size */

  /* RTT (round trip time) estimation variables */
  u32_t rttest; /* RTT estimate in 500ms ticks */
  u32_t rtseq;  /* sequence number being timed */
  s16_t sa, sv; /* @todo document this */

  s16_t rto;    /* retransmission time-out */
  u8_t nrtx;    /* number of retransmissions */

  /* fast retransmit/recovery */
  u8_t dupacks;
  u32_t lastack; /* Highest acknowledged seqno. */

  /* congestion avoidance/control variables */
  u16_t cwnd;
  u16_t ssthresh;

  /* sender variables */
  u32_t snd_nxt;   /* next new seqno to be sent */
  u32_t snd_wl1, snd_wl2; /* Sequence and acknowledgement numbers of last
                             window update. */
  u32_t snd_lbb;       /* Sequence number of next byte to be buffered. */
  u16_t snd_wnd;   /* sender window */
  u16_t snd_wnd_max; /* the maximum sender window announced by the remote host */

  u16_t acked;

  u16_t snd_buf;   /* Available buffer space for sending (in bytes). */
#define TCP_SNDQUEUELEN_OVERFLOW (0xffffU-3)
  u16_t snd_queuelen; /* Available buffer space for sending (in tcp_segs). */

  /* These are ordered by sequence number: */
  struct tcp_seg *unsent;   /* Unsent (queued) segments. */
  struct tcp_seg *unacked;  /* Sent but unacknowledged segments. */
  struct tcp_seg *ooseq;    /* Received out of sequence segments. */
  struct pbuf *refused_data; /* Data previously received but not yet taken by upper layer */

  /* Function to be called when more send buffer space is available. */
  tcp_sent_fn sent;
  /* Function to be called when (in-sequence) data has arrived. */
  tcp_recv_fn recv;
  /* Function to be called when a connection has been set up. */
  tcp_connected_fn connected;
  /* Function which is called periodically. */
  tcp_poll_fn poll;
  /* Function to be called whenever a fatal error occurs. */
  tcp_err_fn errf;

  /* idle time before KEEPALIVE is sent */
  u32_t keep_idle;
  /* Persist timer counter */
  u8_t persist_cnt;
  /* Persist timer back-off */
  u8_t persist_backoff;
  /* KEEPALIVE counter */
  u8_t keep_cnt_sent;
};

struct tcp_pcb_listen {
     
/* Common members of all PCB types */
  IP_PCB;
/* Protocol specific PCB members */
  TCP_PCB_COMMON(struct tcp_pcb_listen);
};

/**
 * members common to struct tcp_pcb and struct tcp_listen_pcb
 */
#define TCP_PCB_COMMON(type) \
  type *next; /* for the linked list */ \
  void *callback_arg; \
  /* the accept callback for listen- and normal pcbs, if LWIP_CALLBACK_API */ \
  DEF_ACCEPT_CALLBACK \
  enum tcp_state state; /* TCP state */ \
  u8_t prio; \
  /* ports are in host byte order */ \
  u16_t local_port

#define DEF_ACCEPT_CALLBACK  tcp_accept_fn accept;

enum tcp_state {
   
  CLOSED      = 0,
  LISTEN      = 1,
  SYN_SENT    = 2,
  SYN_RCVD    = 3,
  ESTABLISHED = 4,
  FIN_WAIT_1  = 5,
  FIN_WAIT_2  = 6,
  CLOSE_WAIT  = 7,
  CLOSING     = 8,
  LAST_ACK    = 9,
  TIME_WAIT   = 10
};

/* This structure represents a TCP segment on the unsent, unacked and ooseq queues */
struct tcp_seg {
   
  struct tcp_seg *next;    /* used when putting segements on a queue */
  struct pbuf *p;          /* buffer containing data + TCP header */
  u16_t len;               /* the TCP length of this segment */
  u8_t  flags;
#define TF_SEG_OPTS_MSS         (u8_t)0x01U /* Include MSS option. */
#define TF_SEG_OPTS_TS          (u8_t)0x02U /* Include timestamp option. */
#define TF_SEG_DATA_CHECKSUMMED (u8_t)0x04U /* ALL data (not the header) is
                                               checksummed into 'chksum' */
  struct tcp_hdr *tcphdr;  /* the TCP header */
};

/** Function prototype for tcp accept callback functions. Called when a new
 * connection can be accepted on a listening pcb.
 * @param arg Additional argument to pass to the callback function (@see tcp_arg())
 * @param newpcb The new connection pcb
 * @param err An error code if there has been an error accepting.
 *            Only return ERR_ABRT if you have called tcp_abort from within the
 *            callback function!
 */
typedef err_t (*tcp_accept_fn)(void *arg, struct tcp_pcb *newpcb, err_t err);

/** Function prototype for tcp receive callback functions. Called when data has
 * been received.
 * @param arg Additional argument to pass to the callback function (@see tcp_arg())
 * @param tpcb The connection pcb which received data
 * @param p The received data (or NULL when the connection has been closed!)
 * @param err An error code if there has been an error receiving
 *            Only return ERR_ABRT if you have called tcp_abort from within the
 *            callback function!
 */
typedef err_t (*tcp_recv_fn)(void *arg, struct tcp_pcb *tpcb,
                             struct pbuf *p, err_t err);

/** Function prototype for tcp sent callback functions. Called when sent data has
 * been acknowledged by the remote side. Use it to free corresponding resources.
 * This also means that the pcb has now space available to send new data.
 * @param arg Additional argument to pass to the callback function (@see tcp_arg())
 * @param tpcb The connection pcb for which data has been acknowledged
 * @param len The amount of bytes acknowledged
 * @return ERR_OK: try to send some data by calling tcp_output
 *            Only return ERR_ABRT if you have called tcp_abort from within the
 *            callback function!
 */
typedef err_t (*tcp_sent_fn)(void *arg, struct tcp_pcb *tpcb,
                              u16_t len);

/** Function prototype for tcp poll callback functions. Called periodically as
 * specified by @see tcp_poll.
 * @param arg Additional argument to pass to the callback function (@see tcp_arg())
 * @param tpcb tcp pcb
 * @return ERR_OK: try to send some data by calling tcp_output
 *            Only return ERR_ABRT if you have called tcp_abort from within the
 *            callback function!
 */
typedef err_t (*tcp_poll_fn)(void *arg, struct tcp_pcb *tpcb);

/** Function prototype for tcp error callback functions. Called when the pcb
 * receives a RST or is unexpectedly closed for any other reason.
 * @note The corresponding pcb is already freed when this callback is called!
 * @param arg Additional argument to pass to the callback function (@see tcp_arg())
 * @param err Error code to indicate why the pcb has been closed
 *            ERR_ABRT: aborted through tcp_abort or by a TCP timer
 *            ERR_RST: the connection was reset by the remote host
 */
typedef void  (*tcp_err_fn)(void *arg, err_t err);

/** Function prototype for tcp connected callback functions. Called when a pcb
 * is connected to the remote side after initiating a connection attempt by
 * calling tcp_connect().
 * @param arg Additional argument to pass to the callback function (@see tcp_arg())
 * @param tpcb The connection pcb which is connected
 * @param err An unused error code, always ERR_OK currently ;-) TODO!
 *            Only return ERR_ABRT if you have called tcp_abort from within the
 *            callback function!
 * @note When a connection attempt fails, the error callback is currently called!
 */
typedef err_t (*tcp_connected_fn)(void *arg, struct tcp_pcb *tpcb, err_t err);


/* The TCP PCB lists. */
/** List of all TCP PCBs bound but not yet (connected || listening) */
struct tcp_pcb *tcp_bound_pcbs;
/** List of all TCP PCBs in LISTEN state */
union tcp_listen_pcbs_t tcp_listen_pcbs;
/** List of all TCP PCBs that are in a state in which
 * they accept or send data. */
struct tcp_pcb *tcp_active_pcbs;
/** List of all TCP PCBs in TIME-WAIT state */
struct tcp_pcb *tcp_tw_pcbs;

上面的TCP控制块tcp_pcb看起来很大,可以把成员变量分组,每种TCP相关机制的实现只涉及到其中的某几个字段,这几个字段可以按一组去理解和操作。除了定义tcp_pcb,还定义了tcp_pcb_listen,后者主要是用来描述处于LISTEN状态的连接,处于LISTEN状态的连接只记录本地端口信息,不记录任何远程端口信息,一般只用于在服务器端打开某个端口为客户端服务。处于LISTEN状态的控制块不会对应于任何一条有效连接,它会进行数据发送、连接握手之类的工作,因此描述LISTEN状态的控制块结构体比tcp_pcb相比更小,使用它可以节省内存空间。

对于描述一个连接的通用字段(比如远程端口、本地端口、远程IP地址、本地IP地址、控制块优先级等)就不再赘述了。重点说下flags字段,它描述了当前控制块的特性,例如是否允许立即发送ACK、是否使能Nagle算法等,这些标志位是提高TCP传输性能的关键。

TCP控制块中维护了三个缓冲队列,unsent、unacked、ooseq三个字段分别为队列的首指针,unsent用于连接还未被发送出去的报文段,unacked用于连接已经发送出去但还未被确认的报文段,ooseq用于连接接收到的无序报文段,这三个缓冲队列简单的实现了对连接的所有报文段的管理。每个报文段用结构体tcp_seg来描述,并以链表形式组织成队列,tcp_seg报文段不仅包含指向装载报文段的指针pbuf,还包含指向报文段中的TCP首部的指针tcp_hdr,报文段缓冲队列的组织关系如下图所示:
TCP报文段缓冲队列
为了组织和描述系统内的所有TCP控制块,内核定义了四条链表来连接处于不同状态下的控制块,TCP操作过程通常都包括对链表上控制块的查找。定义四条链表的代码在上面已给出:tcp_bound_pcbs链表用来连接新创建的且绑定了本地端口的控制块,可以认为此时的控制块处于CLOSED状态;tcp_listen_pcbs链表用来连接处于LISTEN状态的控制块,该状态下用结构体tcp_pcb_listen来描述一个本地连接;tcp_tw_pcbs链表用来连接处于TIME_WAIT状态的控制块;tcp_active_pcbs用于连接处于TCP转换图中其它所有状态的控制块,上图展示的就是该链表上的控制块。

2.3 TCP状态机

TCP状态字段state表示一个连接在整个通信过程中的状态变迁。那么TCP连接的状态是如何变迁的呢?

  • 2
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
嵌入式Linux网络体系结构设计与TCP/IP协议紧密相关。嵌入式Linux操作系统是为嵌入式设备设计的一种轻量级操作系统,而TCP/IP协议网络通信中最基础的协议。在嵌入式Linux中,网络体系结构设计的目标是实现高效稳定的网络通信功能。 嵌入式Linux网络体系结构设计通常由两部分组成:网络驱动程序和协议网络驱动程序负责与物理网络接口进行通信,处理硬件设备的输入输出以及网络数据包的传输。而协议则负责解析网络数据包,进行协议的处理和数据包的封装转发。 TCP/IP协议网络通信中最常用的协议,它包含了网络层的IP协议传输层TCP和UDP协议以及应用层的各种协议。在嵌入式Linux中,TCP/IP协议的设计需考虑性能、资源占用和可靠性等因素。 嵌入式Linux网络体系结构设计需要根据具体的应用场景进行调整。对于资源有限的嵌入式设备,可以选择裁剪协议中的某些功能来降低资源占用。同时,还需要考虑网络通信的性能和延迟要求,合理分配处理器和内存资源,以确保网络通信的稳定和高效。 在实际开发中,可以选择成熟的开源TCP/IP协议,如Linux内核自带的协议或者lwIP协议等。这些协议经过长期的实践和优化,已具备较高的稳定性和可靠性,并且可以根据需要进行灵活的配置和扩展。 总之,嵌入式Linux网络体系结构设计与TCP/IP协议紧密相关,需要综合考虑资源占用、性能和可靠性等因素,选择合适的协议,并根据具体应用场景进行调整,以实现高效稳定的网络通信功能。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

流云IoT

你的鼓励将是我创作的最大动力

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

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

打赏作者

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

抵扣说明:

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

余额充值