| |
|
开发:
C++知识库
Java知识库
JavaScript
Python
PHP知识库
人工智能
区块链
大数据
移动开发
嵌入式
开发工具
数据结构与算法
开发测试
游戏开发
网络协议
系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程 数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁 |
-> 网络协议 -> Lwip之TCP协议实现(二) -> 正文阅读 |
|
[网络协议]Lwip之TCP协议实现(二) |
接上文:Lwip之TCP协议实现(一)_龙赤子的博客-CSDN博客 第二部分:数据输入处理 Tcp数据的输入处理主要在文件tcp_in.c中实现。输入的数据包在IP层进行分发处理。如果输入的数据包为TCP包,则调用tcp_input进行TCP包的输入处理。因此,tcp_input为tcp输入处理的入口。在tcp_input中会判断当前输入的包所在连接的状态,对处于LISTEN和TIME_WAIT状态连接上到达的包,使用tcp_listen_input和tcp_timewait_input两个接口来专门处理。 所有的TCP连接都是由tcp_pcb这样一个数据结构来描述和控制的。该数据结构保存了与当前连接相关的许多细节信息。在Lwip中将tcp_pcb分为三类,一种是处于active的pcb,会根据协议状态机进行处理,一种是处于listen状态的pcb,还有一种是处于time_wait状态的pcb。如果输入的包属于后两种类型的pcb,则会由单独的函数进行处理,分别是tcp_listen_input()和tcp_timewait_input()。Tcp_process接口完成tcp状态机的处理,进一步的处理由tcp_receive完成(这个函数很庞大)。Tcp_parseopt完成tcp头部选项数据的解析工作。 在正式的介绍tcp的输入处理之前,先介绍几个常用到的重要的全局变量。 Inseg:tcp_seg数据结构,输入的数据段由该结构保存 Tcphdr:tcp_hdr数据结构类型的指针,指向输入数据的tcp头部 Iphdr:ip_hdr数据类型的指针,指向输入数据的ip头部 Seqno:输入段数据的开始序号 Ackno:输入数据段的确认号,也就是期望从该端得到数据包的序号 Recv_flags:接收处理中用于保存当前处理状态的标识 目录 一:输入处理总体框架Tcp_input为tcp输入处理的入口。它对包的头部进行检查,将包在各个pcb之间多路分解,并将它们传给tcp_process处理,tcp_process实现了tcp的有限状态机。 Tcp_input函数的基本处理流程如下: 首先将输入报文段的ip和tcp头部保存到全局变量iphdr和tcphdr中。 1. 有效行检查包括两部分:包的长度和包的目的地址。 首先验证包的长度。如果调用pbuf_header将pbuf中数据指针从以太网头部移动到ip头部出错,或者pbuf中数据的总长度还小于tcp头部的长度的话,就认为输入的数据包太多,丢弃,并调用pbuf_free释放其占用的内存。 其次进行目的地址验证。如果输入的数据包是广播包或者组播包,同样将其直接丢弃,并释放内存。 2. 校验和检查这一步调用特定的校验和计算函数(这些计算函数很有讲究)进行校验和的计算。如果计算出校验和错了,处理同上,丢弃包,释放内存。 3. 字节序号转换与标识保存首先将pbuf中的数据指针移动到tcp头部。将其中的各个域的值转换为本地字节序后保存。将其中的seqno和ackno同时保存到全局变量中。全局变量flags保存tcp头部所带的标识,tcplen保存tcp的段长。这里,如果输入段是一个SYN或者FIN包,根据这两个标识也消耗一个序号,tcplen对应的也加1。 4. 多路分解输入段到这里已经完成了基本的准备工作,开始输入段和其所属连接的匹配工作。在这部分的开始提到,tcp将其连接分为三类:active,time_wait以及listen。匹配工作也是按照这三种连接依次进行:首先检查active连接,其次time_wait连接,最后listen状态连接。 上述三类连接分别由三个全局变量保存指向:tcp_active_pcbs,tcp_tw_pcbs以及tcp_listen_pcbs。 首先查看tcp的活动pcb(active pcbs),匹配条件是端口号与ip地址均匹配(包括本地与远端)。整个查找工作在一个for循环中进行。 如果找到了,则将该pcb移到活动pcb的链头,退出查找。否则,进行下一次查找,直到找到或者不再满足循环条件(就是循环查找完所有active状态的pcb)。这里有一个小细节,就是如果找到输入数据段所在的pcb,我们都将它放到active状态pcb链的头部。这样做,是基于如下的这样一个假设:针对同一连接的数据可能在短时间内连续到达。因此,那样做,可以使得下次查找更加快速。 如果在active状态的pcb链表上没有找到匹配的pcb,则接着查找time_wait和listen状态的pcb链。首先查找time_wati状态的pcb链,匹配条件仍然为端口号与ip地址均匹配,整个查找工作同样在for循环中进行。如果找到了,则调用tcp_timewait_input进行相应处理,最后释放包缓存,并返回。 最后,如果还没有找到匹配的pcb,就查找listen状态的pcb链。因为在listen状态还没有确定远程端口和远程ip,所以此时的匹配条件就是本地端口和本地ip。如果匹配了,就把对应的pcb移到头部(原因同之前),并调用tcp_listen_input进行输入处理,最后释放存储,返回 如果没有找到,则转入最后的处理:if(pcb!= NULL){输入段属于active连接}else{没有找到匹配的连接} 5.处理不匹配的包(if-else之else部分)程序执行到这里,说明没有找到匹配的pcb。此时的处理就是发送一个tcp_rst包给发送者,复位当前的连接。程序中具体处理时,判断了一下当前的连接是否已经带了RST标识,如果没有带,则调用tcp_rst带上当前的ackno、seqno以及tcplen发送RST报文到对端。之后释放存储资源,返回。 6.处理active状态连接上的输入(if部分)如果输入报文段属于某个active状态的连接,则程序跳转到这里继续处理。(对于listen和time_wait状态的处理在后面介绍) 在这部分,首先用inseg全局变量保存输入段的信息便于后续处理。之后,全局变量tcp_input_pcb保存当前连接的pcb;调用tcp_process进行状态机的处理;然后将tcp_input_pcb重新置空。之所以使用tcp_input_pcb进行两步额外操作,是为了避免在tcp_process中调用tcp_output进行输出处理,在必要的输入处理完成之后会再次调用tcp_output,此时,会将所有需要发送的数据一起输出。
在tcp_process处理输入数据段过程中,全局变量recv_flags保存了接收处理中tcp所处的状态,返回后的处理依赖于recv_flags保存的状态:(前提是连接没有被ABORT,否则,表明tcp_abort已经被调用,连接也已经被释放,不需要再做其他什么事。) 如果连接被复位(标识TF_RESET被设置),说明对端已经复位了当前连接,我们需要调用注册的错误处理回调函数来通知应用程序连接在被我们释放之前已经被杀死了。随后调用tcp_pcb_remove释放该连接占用的内存,将其从链表上移除,并释放连接数据结构本身。 否则,如果TF_CLOSED标识被设置,表明连接已经被关闭了,我们需要释放存储资源,包括连接附带的和连接本身。 如果上述两个条件不满足,说明连接既没有被复位,也没有被关闭。此时还在状态机的正常状态处理中。此时,如果连接注册了一个发送函数,并且发送空间可用,则调用发送宏(TCP_EVENT_SEND)尝试发送数据,判断条件为pcb的acked域大于零。如果全局变量recv_data不为空,表明有数据收到,调用接收宏TCP_EVENT_RECV进行接收处理。如果TF_GOT_FIN标识被设置,则表明一个FIN数据段收到,同样调用接收宏TCP_EVENT_RECV但是将缓冲指针设置为空来通知应用接收完成EOF。 如果没有其他错误,则调用tcp_output试图发送一些东西,这可能就包括之前在tcp_process中调用tcp_output而没有实际完成的工作。 如果所有的工作都完成了,最后就释放全局变量inseg保存的输入段。如果缓存是应用在使用的,那么释放操作只是将pbuf的参考计数减一,而并不进行真正的释放操作。 二:处理listen状态的连接上的输入因为连接在listen状态和time_wait状态下的处理比较简单,就将它们单独列出来处理,使实现简单,整体逻辑结构清晰。 根据tcp的状态机,处在listen状态的连接是主动打开连接的一方,处在服务器端。此时只需接收对端的带TCP_SYN标识的tcp包。Lwip中的处理如下: 如果输入数据包带有ack标识,则直接响应一个rst报文来复位连接。 如果输入数据包有syn标识,则需要和对端建立一个新的连接。此时,调用tcp_alloc创建一个新的pcb(listen状态的连接和新的连接不是同一个连接),对新的pcb进行完整的设置(包括本地ip端口,远程ip端口,pcb状态设置为SYN_RCVD,下一个期望从对端接收的序号设置为接收包头部的seqno序号加1即可(因为TCP_SYN标识的包是不带数据的,所以数据长度len为零),发送窗口设置为tcp头部接收者提供的窗口,拥塞阀值设置为发送窗口大小,窗口最后更新的序号设置为seqno减1,以便在需要时来强制进行窗口更新。Callback_arg与accept使用已有pcb中保存的值),并将其注册添加到active链上。 调用tcp_parseopt解析输入数据段中tcp头部的选项,使用其中的MSS值来设置新连接的MSS。 根据之前保存的MSS值建立一个新的tcp头部选项数据,调用tcp_enqueue将一个带选项数据的、标识设置为SYN/ACK的tcp建立连接的响应数据段加入到该连接的发送队列。 建立一个mss选项,连同一个syn/ack调用tcp_enqueue加入到输出队列 调用tcp_output以便尽快(立即)发送该响应包。 对于该状态连接到达的其他的数据包,则静静的丢弃。LISTEN状态不处理其他包。这里的处理也不发送RST报文。 三:处理time_wait状态的连接上的输入Tcp的timewait状态也叫做2MSL等待时间,tcp规定一个数据包在网络中的最大生存时间为MSL,考虑来回,总的时间就是2MSL。这个时间是任何报文段被丢弃前在网络中的最长时间。在该段时间内,连接等待接收仍然属于自己的报文段,直到超时关闭连接。在这段时间内,连接只接收包,发送确认,但从不(主动的)发送任何数据。 如果该pcb接收到的包的序号范围超过了连接中保存的期望从对端接收的序号,则用新收到的报文段的序号来更新连接中rcv_nxt值。判断条件是数据段的起始序号和数据长度之值大于rcv_nxt。(如果仅仅是一个FIN包,根据前面对tcplen的赋值可以看出,也是需要更新的。) 如果数据段的长度大于零,表示该报文段带有数据,立即发送针对该报文段的ack报文。 调用tcp_output看该连接上是否有数据需要发送,有的话就发送它。 四:tcp状态机的处理Tcp_process实现tcp的状态机处理。这里不包括以下几个状态:首先是closed,也就是开始状态,这种状态会有一些事件进行驱动,比如上层应用程序的主动调用;其次是listen状态,这会有专门的pcb链进行处理;最后,就是time_wait状态,这与listen类似。 在正式的根据连接的当前状态进行处理之前,先单独处理含有RST标识的报文段。此时,先检查该reset是不是可以接收的,如果该pcb的状态为已发送syn(SYN_SENT),并且对端发送来的期望接收的下一个数据段的序号就是连接保存的下一个将要发送的数据包的序号,则表明其是可以接受的;否则,对于非该状态的连接pcb,如果接收到的段的开始序号在数据接收空间内(本地接收窗口内),同样视为可以接受的。对于可以接受的reset,设置全局变量recv_flags为IF_RESET,取消连接(pcb)的TF_ACK_DELAY标识,返回err_rst。对于不能接受的reset,不进行处理,静静的丢弃,然后返回ERR_OK。 下面真正进入tcp的状态机处理。对于输入的报文段,根据当前连接所处的状态的不同进行不同的处理,整个模块通过一个switch---case语句实现(case条件是pcb的状态)。另外,在真正处理之前,更新一下连接的保活计时。因为一旦有包到达,连接就脱离了空闲状态,同时,连接的keep_cnt(用于计数发送保活探测报文的数量)也清零,因为连接已处于active状态。 Syn_sent状态: 如果收到的数据包含有ack和syn(同时)标识,并且确认号也正确,则连接即将建立。此时,设置该pcb的各个项(snd_buf++:释放SYN报文段占用的一个字节空间;rcv_nxt设置为期望从对端接收的下一个报文段的开始序号,即接收到的报文段的发送序号加1,因为SYN不带数据,只消耗一个序号;lastack设置为输入报文段的确认号;snd_wnd设置为输入段的窗口值,因为这是对端告诉我们的接收窗口值;snd_wl1设置为输入报文段发送序号减1,这样可以强制进行窗口更新;state设置为ESTABLISHED状态,表明连接建立;cwnd设置为连接的MSS,因为处于慢启动状态,每收到一个包,就将拥塞窗口增加一个报文段的大小;snd_queuelen减1,表明未确认队列中的一个段已经被确认;unacked指针下移,理由同上。)。调用tcp_seg_free释放当前已被确认的段。调用tcp_parseopt解析该数据段头部中所带的选项数据,这里主要是MSS选项,因为可能会使用解析的值来作为当前连接的MSS值。调用宏TCP_EVENT_CONNECTED执行注册的连接函数,用户可在连接成功建立时注册该函数进行一些操作。最后调用tcp_ack_now立即发送一个ACK响应给对端。 否则,如果收到的数据包仅含有ack标识,则连接很可能就处于半开放状态。此时,发送RST报文给对端,使其不要处在不同步的状态。 对于该状态接收到的其他数据段,则静静的丢弃。 Syn_rcvd状态: 如果收到的数据包含有ack并且不含rst(其实根据这部分最初的处理,所有含有RST标识的包都已经被过滤了),此时,如果确认号符合要求(就是对端期望接收的下一个数据包的序号在我们最后确认的序号和即将发送数据段的序号之间),就将连接状态设置为established,表明连接已经建立,同时,调用宏TCP_EVENT_ACCEPT进行accept连接处理(此时,本地是处在服务器端)。如果出现错误,调用tcp_abort中断该连接,返回ERR_ABORT,否则调用tcp_receive进行数据接收处理(因为数据有可能搭载这个ACK到达),最后将拥塞窗口设置为最大数据包大小,连接进入慢启动状态。 如果确认号不正确,则调用tcp_rst发送reset给对端。 对于该状态接收到的其他数据段,同样静静的丢弃。 CLOSE_WAIT 或者 ESTABLISHED状态 CLOSE_WAIT是被动关闭进入的一种状态。此时,连接等待应用主动关闭,并接收可能到达的数据。因为该状态和ESTABLISHED状态是互斥的,所以可以将它们放在一块处理。 首先调用tcp_receive进行数据接收处理。如果当然输入段含有tcp_fin标识,并且可以接受输入段,则进入被动关闭,调用tcp_ack_now立即进行响应,并将状态改为CLOSE_WAIT。 Fin_wait_1状态 调用tcp_receive尝试进行数据接收处理。 如果输入包含有FIN标识,并且含有ACK标识,并且对端期望接收的下一个字节的数据就是连接将要发送的数据,则调用tcp_ack_now立即响应一个ACK,并调用tcp_pcb_purge释放该连接占用的存储。最后,在active pcb上移除该pcb,将其加入到time_wait pcb上,连接的状态相应的改为TIME_WAIT。 如果不满足后两个条件,只是有FIN标识,则直接调用tcp_ack_now进行响应,此时连接的状态改为CLOSING,进入同时关闭状态。 但是如果只含有ACK,并不含FIN,并且对端期望接收的下一个字节的序号就是当前将要发送数据的序号,言外之意就是对端已经确认了所有的数据,则将连接状态改为fin_wait_2,连接进入FIN_WAIT_2状态。 Fin_wait_2状态 同样,先调用tcp_receive进行数据输入处理 如果输入包含有FIN标识,操作类似与FIN_WAIT_1的状态转入TIME_WAIT的操作。 对于其他的数据包,lwip本来提供了相应的超时处理,大概20秒,但为了更快的结束这种状态,我们在这里直接调用tcp_abort终止当前的连接。这种情况出现在连接处于这种状态时,对端还在发送数据过来。 Closing状态 调用tcp_receive进行数据输入处理 如果输入包含有ack标识,并且已经确认了所有我们发送的数据,则连接进入TIME_WAIT状态,处理同上。 Last_ack状态 调用tcp_receive进行数据输入处理 如果输入包含有ack标识,并且对端已经确认了所有已发送的数据,则将状态改为CLOSED,将表示接收处理状态的全局变量recv_flags设置为TF_CLOSED,表明我们已经关闭了连接。 Default 不进行任何处理。 到这里,数据段在tcp状态机中的处理就完成了,我们返回ERR_OK,进行后续处理。 五:tcp_receive函数Tcp_receive函数应该是lwip中最长的一个函数了。该函数在需要进行数据接收处理时被调用。 /*************************************************************************/ 如果输入段带ack标识,进入下面的处理: 内部变量right_wnd_edge保存发送窗口的右边界。(连接保存的发送窗口的大小与连接保存的最后一次窗口更新时的发送序号) 更新窗口(这里更新窗口包括本地的接收窗口和本地的发送窗口):更新条件 窗口最后一次更新时收到对端发送数据的序号 小于 当前收到数据所带的序号(这里似乎将窗口最后一次更新时保存的对端发送数据带的序号作为了窗口的左边边沿。这样的话,这个判断条件就表明有新的数据到达。) 上述小于变为等于,但是窗口最后一次更新时保存对端数据确认的序号 小于 当前数据所带的确认序号(窗口最后一次更新时保存的确认序号就是本地接收窗口的左边边沿,这个条件表明有新的数据被确认,接收需扩大) 上述小于变等于,但是对端当前发送数据通告的窗口值大于连接保存的发送窗口值(表明对端的接收窗口更新了,可以接收更多的数据,本地的发送窗口也需要进行相应的更新) 上述三个条件是逻辑或的关系,表明根据收到的数据段,如果需要更新接收窗口或者发送窗口就进行更新操作,但是更新操作是同时针对接收窗口和发送窗口的,不管根据条件是否真正更新了某个窗口,也许还是之前的设置。 更新操作是用收到的数据段的通告窗口更新连接的发送窗口,数据段的发送序号为连接最后一次更新时保存的发送序号,数据段的确认序号为窗口最后一次更新时保存的确认序号。 如果连接保存的被确认的最高字节的序号lastack等于当前收到数据段带的确认序号(表明没有向对端发送数据,但同时有可能收到了一个重复的ack),连接的acked域设置为零,也就是没有数据被确认。如果此时更新后的发送窗口相比之前的窗口没有变化,也就是窗口右边沿没有移动(判断条件就是连接最后更新的保存的发送数据序号 和 连接保存的窗口大小之和 等于 之前保存的右边沿),则增加连接对重复ack的计数。如果重复ack计数增加到大于等于3次并且连接的unacked队列(已发送,还未确认的队列)不空,在上述条件都满足的情况下,如果连接的快速重传/恢复标识位TF_INFR没有被设置,则进入快速重传状态{ 调用tcp_rexmit重传unacked队列上的第一个未被确认的数据段。 设置拥塞阀值为当前拥塞窗口和接收者建议窗口(就是发送窗口)之间小值的一半。 拥塞窗口设置为更新后的拥塞阀值加上3倍的报文段大小。 设置连接的TF_INFR,表明连接进入了快速重传和快速恢复状态。 } 否则,表明连接已经在进行快速重传了。此时执行拥塞避免算法,也就是快速恢复算法。因此,每当收到另一个重复的ack时,拥塞窗口cwnd就增加一个报文段大小。这里增加过程持续进行,直到到达最大值,也就是变量所能表示的最大值(16位数65535)。 (上面这部分处理实现快速重传和恢复,但是如果收到重复ack,本地窗口有扩大,则不增加重复ack计数,???) 否则,继续进行其他处理 新数据被确认。 如果确认序号在本地发送窗口范围内(判断条件:ackno在连接被确认的最高序号,也就是左边沿和连接的最大已发送的数据序号,也就是窗口的右边沿之间),说明有新的数据被确认,此时,如果连接的TF_INFR已经被设置了,则复位连接的TF_INFR状态,因为我们已经不在快速重传状态,并且复位拥塞窗口为慢启动阀值。 复位连接的重传计数nrtx为零。 复位连接的超时重传计数器。 更新连接的发送缓冲空间。 复位快速重传的相关变量,包括重复ack计数值清为零,最高确认序号lastack设置为当前输入段的确认序号。 更新拥塞控制的相关变量,包括拥塞窗口和拥塞阀值:这只在当前连接的状态在建立连接及其后续状态时才进行。如果拥塞窗口小于拥塞阀值(表明处在慢启动阶段),此时如果拥塞窗口还没有达到上限值,则将其增加一个报文段的大小。否则,说明在拥塞避免阶段,按照对应的算法来增加拥塞窗口值。 对于被确认的数据,将它们从unacked队列上移除。移除过程是一个循环过程,循环条件为(unacked队列不为空;unacked数据段的确认号和其长度之和小于等于全局变量保存的acnno,也就是说unacked段占用的序号小于等于确认号,说明该报文段已经被确认了。)将unacked队列头的报文移除,释放其占用的存储,更新连接的发送窗口中的数据报文数snd_queuelen为减去当前移除报文所消耗的pbuf数,因为一个报文段可能有几个pbuf构成(比如报文段的合并),并且snd_queuelen增加时也是根据pbuf的数量来增加的。 检查完unacked队列后我们还要检查unsent的队列,看看有没有被ack确认的数据段。这听起来似乎不可思议,因为unsent队列上的数据是还有发送的,怎么会被确认呢?理由是lwip在重传后(不管是超时重传还是快速重传)会将这些带外数据放到unsent队列上,所以,实际上unsent队列上的有些报文段可能是已经发送过的。类似于unacked队列的处理,整个过程也是在一个循环中进行的,循环条件为(unsent队列不空,并且收到报文段的确认号ackno在当前判断的unsent报文段所占用的序号范围内)如果存在这样的报文段,就将其从unsent队列上移除,释放其占用的存储,调整连接的snd_queuelen值(snd_queuelen保存了一个虚拟的已发送过但未被确认过的所有数据段的队列)。此时如果unsent队列中还有数据,则将连接的下一个将要发送数据的序号snd_nxt调整为队列中当前队列头部报文段的序号。 下面进行RTT评估的相关处理。 如果连接的RTT评估计时器在计时(我们在重传报文段时是不进行RTT评估计时的。因为此时进行评估很明显应该是不合理的,链路发生拥塞,是一个不正常的状态),并且我们发送的用于进行RTT评估的报文段的序号在确认序号ackno所确认的范围内,就重新进行RTT评估。评估按照具体的算法进行,结果保存在连接的重传超时变量RTO中。最后复位评估计数器,它会在下次发送需要评估的报文段时重新启动。 输入数据段与ack标识相关的处理到此就处理完了。 /*************************************************************************/ /*************************************************************************/ 如果输入的数据段带数据,就进一步的处理数据。 这部分代码主要做三件事情: 首先,如果输入段包含的数据是序号内的,则数据将被传送给应用。连接的下一个将要接收的数据的序号rcv_nxt和发送方通告的接收窗口将被调整; 其次,如果到达的是序号外的数据,则将数据放到连接的ooseq队列上,针对该数据段的ack也被立即发送以指示我们收到了一个序外段。 最后,我们会检查当前ooseq队列上的数据段是否有在序号内的,如果有,在更新连接的rcv_nxt前我们将修剪该数据段。其上在序号内的数据将会被连接到当前的输入段中,所以我们只需要将数据交给应用一次。 首先我们检查是否必须分割输入段。如果下一个期望接收的序号在当前输入段占用的序号“中间”,则说明输入段的前半部分已经正确接收了,只需要后半部分,所以必须将段剪断(即使之前只是接收了该报文段的第一个字节)。如果需要剪断,则对输入包的pbuf做一下特殊处理。具体的处理这里不再详细说明。剪断处理后,全局变量seqno会有个调整。 否则,如果当前接收包的序号小于期望接收的序号,则说明这是一个重复的报文段,对于重复数据段,不需要特殊处理,只需调用tcp_ack_now立即发送一个ack(实际上ack并不会立即发送,它必须在tcp的状态机处理完成之后再发送)。 数据的开头到这里已经被修改好了,如果此时序号在接收窗口内则表明数据可能有效。 如果连接期望接收的下一个字节的序号就是seqno,则说明这是一个序号内数据,变量accepted_inseg设置为1,表明接收到了一个序号内数据段。此时我们检查确定是否需要裁剪数据段的尾部(如果数据段太长超过了接收窗口?不,是该数据段是否和ooseq队列中保存的序号外数据段有重复)并更新rcv_nxt,将数据交给应用。 如果连接的ooseq队列不空,并且队列上第一个报文段的序号在新到达数据段的序号范围内(说明两个报文的数据有重复),则我们必须还要裁剪输入数据段的尾部。Pbuf的缩减调用pbuf_realloc完成。 此时输入报文段的裁剪工作完成,我们使用裁剪后的段长度来更新最初设置的全局变量tcplen的长度。 如果连接不再CLOSE_WAIT状态,则更新连接期望接收的下一个字节的序号为当前序号与当前输入段的长度(调整后的)之和。(在CLOSE_WAIT状态,我们不这样做是因为在进入该状态之前,我们已经将ack加1了。此时进入的是被动关闭状态,对端主动关闭,说明对端不应该在发送数据过来,所以这样处理是可以的。其他状态,则可能是主动关闭状态,有可能还继续接收对端发送的数据。) 更新接收窗口,如果接收窗口小于tcplen,则说明接收窗口被占用完了,将rcv_wnd设置为零,否则,接收窗口rcv_wnd更新为减去当前输入段长度tcplen的值。 如果输入段含有数据,则全局变量recv_data(用来保存将传送给应用的数据)指向数据所在的pbuf,并将inseg保存的pbuf指针清为空,避免删除是造成错误。同时,如果该数据段是一个FIN段,则设置全局变量recv_flags的TF_GOT_FIN标识。 到这里,输入段已经处理完成,并且rcv_nxt和rcv_wnd变量都已经更新,此时我们再判断是否有ooseq队列上的包在接收序号内了,也就是进入了接收窗口。处理过程是循环进行的,循环条件为:ooseq队列中还有数据段,并且数据段的序号就是当前接收窗口的左边沿,也就是新的下一个期望接收的数据。 如果条件满足,则将队列中的段当作新的输入段处理。Seqno更新的该段的序号,rcv_nxt更新为加上段的长度,更新接收窗口大小,同之前,如果当前窗口小于段的大小则设置为零,否则更新为减去段的长度。此操作完成后,接收窗口左边沿也就移动了段的长度。如果段含有数据,并且recv_data不为空,说明已经有数据了,则将新的数据通过调用pbuf_cat并到已接收的数据上,否则recv_data指向序外数据。同之前,仍然需要将段的pbuf指针复位为空,避免删除时出错。 如果数据段带有FIN标识,则将recv_flags设置为TF_GOT_FIN。并且如果当前连接是ESTABLISHED状态,则需要将连接的状态设置为CLOSE_WAIT,因为不会有新的包到达来驱动状态机使连接进入CLOSE_WAIT状态。 删除当前段从ooseq队列上,释放段占用的存储。Ooseq队列头指针指向下一个段,进入下一次循环处理。这里有个问题,就是ooseq队列上段的处理并没有像输入段,如果连接变为了CLOSE_WAIT状态,就不再根据段的长度来更新连接的rcv_nxt下一个期望从对端接收数据的序号。 到这里序号内数据的接收都处理完了,调用tcp_ack发送延迟的ack。 到这里,说明收到的是一个序号外数据 对于序号外数据,首先调用tcp_ack_now立即发送一个ack到对端,表明我们已经收到了数据,然后将数据放入ooseq队列。 如果此时ooseq队列为空,则调用tcp_seg_copy将输入段拷贝到队列,否则遍历ooseq队列将该数据段放到合适的位置。这里所谓的合适的位置是指输入段的序号要在其放入位置之前之后段的序号之间,并且,在需要的时候需要修剪之前的段的尾部和新入队段的尾部来使得序号匹配(连续)。如果输入段和队列中的某个段有相同的序号,就丢弃含数据更少的那个段。 否则,序号不在窗口内,不进行处理。 /*************************************************************************/ 否则,输入段可能仅仅是一个纯ACK,不进行特殊操作,调用tcp_ack_now发送一个ACK响应给对端。 最后,返回变量accepted_inseg,如果从对端收到了一个序号内数据段,则该变量为真。 六:头部选项解析输入数据段tcp头部选项的解析由接口tcp_parseopt来完成。首先将内部变量opts调整到tcp选项处,如果tcp头部长度大于正常的20字节,就说明含有选项数据,进行选项的解析处理 { 选项的处理通过for循环从第一个字节处理到最后一个字节 因为数据零表示选项结束,所以循环中首先判断读到的字节是否为零,如果是,则直接break跳出循环。 数据1标识是NOP操作,循环处理会跳过该字节,循环中这是第二个判断条件。 如果上述两个条件都不满足,则首先处理MSS选项。判断表达式是第一个字节为2,第二个字节为4。如果这个条件满足,说明连接中带有MSS选项,则将后两个字节读出组合出MSS值。但是这个MSS值还不能直接拿来用,如果这个值大于我们设定的连接的最大MSS值,则用配置的值来代替。因为目前只解析MSS选项,如果成功解析的话就直接break跳出。 如果上述条件都不满足,则要么是一个不符合格式选项,要么是其他选项。对于其他选项,目前跳过,不进行处理。后续添加了窗口扩大因子选项后,这部分代码需要修改。 } 七:问题列表tcp_process? 498?? 当tcp pcb处于syn_sent状态时,如果收到syn数据段,则会出现同时打开情景,程序中没有处理该状态下收到syn数据段? |
|
网络协议 最新文章 |
使用Easyswoole 搭建简单的Websoket服务 |
常见的数据通信方式有哪些? |
Openssl 1024bit RSA算法---公私钥获取和处 |
HTTPS协议的密钥交换流程 |
《小白WEB安全入门》03. 漏洞篇 |
HttpRunner4.x 安装与使用 |
2021-07-04 |
手写RPC学习笔记 |
K8S高可用版本部署 |
mySQL计算IP地址范围 |
|
上一篇文章 下一篇文章 查看所有文章 |
|
开发:
C++知识库
Java知识库
JavaScript
Python
PHP知识库
人工智能
区块链
大数据
移动开发
嵌入式
开发工具
数据结构与算法
开发测试
游戏开发
网络协议
系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程 数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁 |
360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 | -2024/11/25 22:49:07- |
|
网站联系: qq:121756557 email:121756557@qq.com IT数码 |