跟着上章节(手把手教你手撸通讯协议(二)网络的基础)提出的问题,通过这一章节,应该能好好理解TCP是怎么解决上述问题的。 接下去我们还是通过开源的LwIP协议栈来好好了解以太网的真实工作方式,我将会在这一期的最终期,给大家实现一个基于STM32的modbusTCP主站的小demo。 第一节 初识TCP TCP中文名叫传输控制协议,它为上层提供一种面向连接的、可靠的字节流服务; 那TCP通过什么方法来提供可靠性? (1)先将应用数据分割成TCP认为最适合发送的数据块; (2)当TCP发出一个段后,它启动一个定时器,等待目的端确认收到这个报文段,如果不能及时收到一个确认,将重发这个报文段; (3)当TCP收到发自TCP连接另一端的数据,它将发送一个确认,这个确认不是立即发送,通常将推迟几分之一秒; (4)TCP将保持它首部和数据的检验和,如果收到段的检验和有差错,TCP将丢弃这个报文段并且不发送确认收,以使发送端超时并重发; (5)IP数据报的到达可能会失序,因此TCP报文段的到达也可能会失序,如果必要,TCP将对收到的数据进行重新排序,将收到的数据以正确的顺序交给应用层; (6)IP数据报会发生重复,TCP的接收端必须丢弃重复的数据; (7)TCP还能提供流量控制。 下图是TCP首部结构,若不计任选字段,其大小为20字节,与IP报首部大小相同。 源端口号和目的端口号,用于标识发送端和接收端的应用进程。这两个值加上IP首部中的源IP地址和目的IP地址就能唯一确定一个TCP连接。一个IP地址和一个端口号也称为一个插口(socket)。 在TCP首部中有6个标志比特。它们中的多个可同时被设置为1。在这里简单介绍它们的用法,在以后用到时会详加讲解:URG紧急指针(urgentpointer)有效标识;ACK确认序号有效标识;PSH接收方应该尽快将这个报文段交给应用层;RST重建连接;SYN同步序号,用来发起一个连接;FIN请求端完成发送任务。 根据上图,在LwIP中是这样描述TCP报头:structtcphdr{PACKSTRUCTFIELD(u16tsrc);源端口PACKSTRUCTFIELD(u16tdest);目的端口PACKSTRUCTFIELD(u32tseqno);序号PACKSTRUCTFIELD(u32tackno);确认序号PACKSTRUCTFIELD(u16thdrlenrsvdflags);首部长度保留位标志位PACKSTRUCTFIELD(u16twnd);窗口大小PACKSTRUCTFIELD(u16tchksum);校验和PACKSTRUCTFIELD(u16turgp);紧急指针}PACKSTRUCTSTRUCT; 第二节 TCP的断开和连接 众所周知的TCP有三次握手和四次挥手。 (图来自网上,挺常见的) 2。1 TCP连接建立 TCP要建立连接需要经历三次握手,那如何实现三次握手呢? (1)请求端发送一个SYN标志置1的TCP数据报,数据包中指明自己的端口号及将连接的服务器的端口号,同时通告自己的初始序号ISN。 (2)当服务器接收到该数据包并解析后,也发回一个SYN报文段作为应答。 (3)该回应报文包服务器自身选定的初始序号ISN,同时,将ACK置1,将确认序号设置为请求端的ISN加1以对客户的SYN报文段进行确认。 (4)这里的ISN也表示了服务器希望接收到的下一个字节的序号。由此可见,一个SYN将占用了一个序号。 (5)当请求端接收到服务器的SYN应答包后,会再次产生一个握手包,这个包中,ACK标志置位,确认序号设置为服务器发送的ISN加1,以此来实现对服务器的SYN报文段的确认。 但这边会存在一个问题,如果两端同时发起连接,即同时发送第一个SYN数据包,这时这两端都处于主动打开状态,TCP中又是如何解决的? 2。2 TCP断开 为什么断开需要四次挥手?四次挥手做了啥? (1)请求端发起中断连接请求,也就是发送FIN报文,意思是说我请求端没有数据要发给你了,但是如果你还有数据没有发送完成,则不必急着关闭Socket,可以继续发送数据。 (2)服务端接到FIN报文后,先发送ACK,告诉请求端,你的请求我收到了,但是我还没准备好,请继续你等我的消息。 (3)请求端就进入FINWAIT状态,继续等待服务端的FIN报文。 (4)当服务端确定数据已发送完成,则向请求端发送FIN报文,告诉请求端,好了,我这边数据发完了,准备好关闭连接了。 (5)请求端收到FIN报文后,就知道可以关闭连接了,但是他还是不相信网络,怕服务端不知道要关闭,所以发送ACK后进入TIMEWAIT状态,如果服务端没有收到ACK则可以重传。 (6)服务端收到ACK后,就知道可以断开连接了。 (7)请求端等待了2MSL后依然没有收到回复,则证明服务端已正常关闭,那好,请求端也可以关闭连接了。 这七个步骤可以很清晰的看到为啥要进行四次挥手,确认TCP断开。 第三节 TCP的状态转换 从上述三次握手建立连接到四次挥手断开连接过程中,其实可以总结到两张图:请求端状态切换图和服务端状态切换图。 这两个图结合起来就是TCP的状态转换图了,(图来自详解)。 第四节 TCP控制块解读 上面主要让大家对TCP这个协议有基本的认识,接下去我们要进行一些源码解读。structtcppcb{IPPCB;这是一个宏,描述了连接的IP相关信息,包括双方IP地址,TTL等信息用于连接各个TCP控制块的链表指针TCP连接的状态,即为状态图中描述的那些状态u8该控制块的优先级u16本地端口u16远程端口u8附加状态信息,如连接是快速恢复、一个被延迟的ACK是否被发送等defineTFACKDELAY(u8t)0x01U延迟发送ACK(推迟确认)defineTFACKNOW(u8t)0x02U立即发送ACKdefineTFINFR((u8t)0x04U)连接处于快重传状态defineTFTIMESTAMP((u8t)0x08U)连接的时间戳选项已使能defineTFFIN((u8t)0x20U)应用程序已关闭该连接defineTFNODELAY((u8t)0x40U)禁止Nagle算法defineTFNAGLEMEMERR((u8t)0x80U)本地缓冲区溢出接收相关字段u32期望接收的下一个字节,即它向发送端ACK的序号u16接收窗口u16通告窗口大小u32该字段记录该PCB被创建的时刻u8tpolltmr,三个定时器,后续讲解u16重传定时,该值随时间增加,当大于rto的值时则重传发生u16最大数据段大小RTT估计相关的参数u32估计得到的500ms滴答数u32用于测试RTT的包的序号s16tsa,RTT估计出的平均值及其时间差u16重发超时时间,利用前面的几个值计算出来u8重发的次数,该字段在数据包多次超时时被使用到,与设置rto的值相关快速重传恢复相关的参数u32最大的确认序号,该字段不解u8上面这个序号被重传的次数阻塞控制相关参数u16连接的当前阻塞窗口u16慢速启动阈值发送相关字段u32tsndnxt,下一个将要发送的字节序号sndmax,最高的发送字节序号sndwnd,发送窗口sndwl1,sndwl2,上次窗口更新时的数据序号和确认序号发送队列中最后一个字节的序号u16u16可用的发送缓冲字节数u8可用的发送包数发送的数据段队列发送了未收到确认的数据队列接收到序列以外的数据包队列ifLWIPCALLBACKAPI回调函数errt(sent)(voidarg,structtcppcbpcb,u16tspace)?当数据被成功发送后被调用errt(recv)(voidarg,structtcppcbpcb,structpbufp,errterr)?接收到数据后被调用errt(connected)(voidarg,structtcppcbpcb,errterr)?连接建立后被调用errt(poll)(voidarg,structtcppcbpcb)?该函数被内核周期性调用void(errf)(voidarg,errterr)?连接发生错误时调用endifLWIPCALLBACKAPIu32ifLWIPTCPKEEPALIVEu32保活定时器,用于检测空闲连接的另一端是否崩溃u32坚持定时器计数值endifLWIPTCPKEEPALIVEu32这两个字段可以使窗口大小信息保持不断流动u8坚持定时器探查报文发送的数目u8保活报文发送的次数}; 这里有一个比较重要的知识点:滑动窗口算法(这个基础算法在很多算法很实用);在这里主要用于限流和控制。这里带wnd结尾字段都与滑动窗口算法相关的参数。如果有不理解这个算法的小伙伴可以好好去了解下,后续工作中很有可能经常用到。 如上图所示:连接的双方都维持一个窗口用于数据的发送。滑动窗口把整个序列分成三部分:左边的是发送了并且被确认的分组,窗口右边是还没发送的分组,窗口内部是待确认的分组,窗口内部又分成已经发送待确认的,和未发送但将立即发送。TCP是通过正面确认和重传技术来保证可靠性的,滑动窗口可以使发送方在收到前一个分组的确认信息前发送下一个分组。 有了发送窗口,自然还有一个接收窗口维护,如下图所示:在接收方,revwnd表示了自己接收窗口的大小,它可以在给发送方的ACK包中通告自己的窗口大小值,发送方接收到该值后,就以此设子自己的发送窗口大小值sndwnd。发送方的发送窗口内包的数据发送序列是与ACK序号密切相关的,即它将ACK序号以后的sndwnd个字节序号包括在窗口内。发送方的acked字段就表示已经接收到的最高的ACK序号,sndnxt表示发送方即将发送数据的序号,acked与sndnxt之间的数据表示已经被发送但还接收到ACK,发送方也必须将他们包括在滑动窗内,以方便超时重发,sndnxt到发送窗口端表示还发送的数据。 在LWIP中实现的函数段为: 客户端:if((flagsTCPSYN)(flagsTCPFIN)){发送SYN或FIN包被认为数据长度为1}下一个要被缓冲数据的序号,注意与sndnxt不同所以,tcpenqueue函数过后,sndlbb值变为ZSL2,其他字段值不变。tcpconnect函数接下来还调用tcpoutput将数据包发送出去,后者发送一个具体的数据段是通过调用函数tcpoutputsegment实现的,这个函数主要是填充待发送数据段的TCP头部中的确认序号为rcvnxt的值为0通告窗口大小为rcvannwnd的值TCPWND最后,tcpoutput通过下面的代码来更新窗口相关的字段:pcbsndnxtntohl(segtcphdrseqno)TCPTCPLEN(seg);下一个要发送的字节序号if(TCPSEQLT(pcbsndmax,pcbsndnxt)){最大发送序号} 服务端:if((flagsTCPSYN)(flagsTCPFIN)){发送SYN或FIN包被认为数据长度为1}下一个要被缓冲数据的序号,注意与sndnxt不同所以tcpenqueue函数过后,sndlbb值变为ZSL11,其他字段值不变。接下来调用tcpoutput将数据包发送出去与客户端类似,填充待发送数据段的TCP头部中的确认序号为rcvnxt的值ZSL2,通告窗口大小为rcvannwnd的值TCPWND最后,tcpoutput还要更新窗口相关的字段:pcbsndnxtntohl(segtcphdrseqno)TCPTCPLEN(seg);下一个要发送的字节序号if(TCPSEQLT(pcbsndmax,pcbsndnxt)){最大发送序号} 今天先讲到这边;下一章主要讲解TCP协议是怎么建立,TCP状态是怎么转换的源码。后续还会讲解常用协议、modbus等