TCP协议笔记

 一、三次握手TCP状态。客户端:1.SYN_SENT   2.ESTABLISHED   服务端:1.SYN_RECVD  2.ESTABLISHED.

    1.首先由客户端发起TCP连接,第一个报文段,TCP头部SYN位置为1,随机生成一个ISN, MSS数值(最大报文段 <= 1460),客户端窗口大小,客户端状态变为SYN_SENT。

    2.服务端接收到SYN报文段,状态变为SYN_RECVD,然后服务端发送SYN报文段,ACK置为1,ack数值是客户端的ISN+1,MSS数值,服务端窗口大小,并且随机生成一个ISN。

    3.客户端收到服务端的SYN报文后,状态由SYN_SENT变为ESTABLISHED,然后给服务端发送确认报文,ACK置为1,ack是服务端的ISN+1。

    4.服务端接收到ACK确认报文后,状态由SYN_RECVD变为ESTABLISHED。至此三次握手完成,TCP连接建立完成。

   

 二、四次挥手TCP状态。客户端:1.FIN_WAIT1  2.FIN_WAIT2   3.TIME_WAIT(2MSL) 。服务端:1.CLOSE_WAIT  2.LAST_ACK

3.CLOSED

         1.由客户端发起TCP断开连接,报文段如下:TCP头部FIN位置为1,ACK置为1,ISN, ack。客户端状态变为FIN_WAIT1

         2.服务端收到客户端的FIN报文后,发送ack报文进行确认,状态变为CLOSE_WAIT

         3.服务端接着发送FIN报文,状态变为LAST_ACK,等待客户端的确认。

         4.客户端收到服务端的ack报文后状态变为FIN_WAIT2。

         5.客户端收到服务端的FIN报文后状态变为TIME_WAIT,然后发送ack确认报文,然后客户端会占用2*MSL(TCP报文段最大生存时间,一般是30s,1分钟,2分钟)时间的端口,防止ack丢失。原理就是如果服务端在超时重传时间内没收到客户端的ack,服务端就会重发FIN报文,这样客户端就可以收到服务端的第二个FIN报文,然后客户端会再次发送。

         6.服务端接收到客户端最后的ack报文,就会由LAST_ACK转换为CLOSED。

 

TCP半关闭:客户端发送FIN报文后,就表示客户端已经不会发送带数据的报文段了,只会接收服务端发送的数据,服务端接收到FIN报文后,先给客户端发FIN的ack,然后给客户端发送数据,客户端给服务端发送的数据发送ack。当服务端数据发送完之后,发送一个FIN报文给客户端,最后客户端给服务端发送一个FIN的ack报文给服务端。半关闭对应的socket api是shutdown函数,而不是close函数,shutdown函数的第二个参数为1,就表示半关闭状态。

 

TCP半打开: 如果一方已经关闭或异常终止连接而另一方却还不知道,我们将这样的 T C P连接称为半打开( H a l f - O p e n)的。任何一端的主机都可能出现这种情况。只要不打算在半打开的连接上传输数据,仍处于连接状态的一方就不会检测另一方已经出现异常。一般在一端出现断电的时候可能会出现这种情况。

TCP两端同时打开

    

TCP协议笔记_第1张图片

TCP两端同时关闭

TCP协议笔记_第2张图片


 

TCP交互式输入:1.客户端发送一个字符数据。2.服务端发送ack确认。3.服务端发送一个字符数据。4.客户端发送确认。然后重复以上过程。也就是说客户端发送一个报文段,必须等服务端确认之后,才能发送第二个报文段,服务端也是。也就是说不能连续发送多个报文段。这样的传输效率在广域网上很低。

TCP经受延时的确认:也就是发送数据捎带ACK,比如客户端发给服务端一个报文,服务端不会立马确认发送ack,而是等待是否有数据发送到客户端,在200ms内如果有数据发送,则会捎带ack,如果超过200ms没有数据发送,则服务端会单独发送一个ack报文。

TCP Nagle算法:该算法要求一个tcp连接上只能有一个未确认的分组,也就是如果客户端发送了一个分组,但是服务端还未发送这个分组的确认,则客户端其余的报文必须等待。TCP协议会将其余的报文进行拼装成一个报文(分组),在上一个分组确认到来后,会将此分组发送。该算法的优越之处在于它是自适应的:确认到达得越快,数据也就发送得越快。而在希望减少微小分组数目的低速广域网上,则会发送更少的分组。所以此算法发生粘包的概率更大。Nagle算法默认是开启的。socket api使用T C P _ N O D E L A Y选项关闭Nagle算法。在对实时性要求高的场景必须关闭Nagle算法,比如进程间使用TCP协议进行通信。

TCP滑动窗口协议:T F T P使用了停止等待协议。数据发送方在发送下一个数据块之前需要等待接收对已发送数据的确认。滑动窗口协议允许发送方在停止并等待确认前可以连续发送多个分组。由于发送方不必每发一个分组就停下来等待确认,因此该协议可以加速数据的传输。滑动窗口如下图所示:

TCP协议笔记_第3张图片


123....11...表示报文段发送缓冲区队列,发送从前往后进行,发送窗口为6个报文段大小,表示可以最多连续发6个TCP报文段。l我们可以用left表示窗口左边界,right表示窗口右边界,pos表示下次将要发送的报文段。

当接收方确认数据后,这个滑动窗口不时地向右移动。窗口两个边沿的相对运动增加或减少了窗口的大小。我们使用三个术语来描述窗口左右边沿的运动:

    1) 称窗口左边沿向右边沿靠近为窗口合拢。这种现象发生在数据被发送和确认时。也就是right + n
    2) 当窗口右边沿向右移动时将允许发送更多的数据,我们称之为窗口张开。这种现象发生在另一端的接收进程读取已经确认的数据并释放了 T C P的接收缓存时。left + n
   3) 当右边沿向左移动时,我们称之为窗口收缩。 Host Requirements RFC强烈建议不要使用这种方式。但T C P必须能够在某一端产生这种情况时进行处理。第 2 2 . 3节给出了这样的一个例子,一端希望向左移动右边沿来收缩窗口,但没能够这样做。
 

TCP协议笔记_第4张图片

如果左边沿到达右边沿,则称其为一个零窗口,此时发送方不能够发送任何数据。
 

 

LWIP协议栈 TCP报文接收流程源码解析

一、TCP层第一个处理函数tcp_input

     首先IP层将TCP报文段提交给tcp_input函数,tcp_input函数需要查找先查找已建立连接的tcp控制块,查找方法是遍历活跃tcp控制块链表,然后比对每个tcp控制块dst port和当前报文中包含的src port和IP报文中的src ip地址是否相等,如果相等就说明此tcp控制块是和对端连接的tcp控制块,这个流程类似于根据sessionid查找session,如果找到则调用tcp_process函数处理。如果在活跃链表未找到对应的tcp控制块。则在处于TIME_WAIT状态的pcb链表去查找,如果找到则删除此pcb,然后返回,因为处于TIME_WAIT状态已经不能再处理数据了。如果TIME_WAIT状态的pcb链表未找到,则去处于listen状态的lpcb链表进行查找,lpcb和tcp_pcb不一样,lpcb是所有处于监听状态的tcp控制块,如果找到则进入tcp_listen_input函数处理。tcp_process函数处理的是已经有过至少一次握手的tcp连接。tcp_listen_input函数处理的是还未有过握手的连接。

二、tcp_process函数处理

  1.首先判断报文是否是RST复位报文,如果是RST复位报文,再判断当前TCP连接所处的状态,如果是SYS_SENT并且发过来的报文ackno符合要求则可以复位,复位操作就是删除tcp控制块,也就是TCP的session。如果不是SYS_SENT状态,如果报文的seqno符合要求则可以复位,也就是说处于SYS_SENT状态判断ackno,因为SYS_SENT状态说明本机发送了一个SYN报文,需要等待对方的ackno,因为是第一次通信此时对方的seqno,本端肯定不知道。而非SYS_SENT状态需要判断seqno,也就是对方发的报文是否是本端想要的。如果seqno在接收窗口内,但不是想要的序列号,这里为了避免RST攻击,不会立即删除TCP控制块,而是给对方发一个ack now(也就是会立即发送,不用等超时定时器,可能是不包含数据的ackno)。如果seqno在接收窗口外,也不会删除TCP控制块,而是按照正常的ack发送数据。注RST恶意攻击如果不做防范,会让主机立即删除与对端的连接,删除TCP控制块。

  2.如果不是RST复位报文,如果是SYN报文并且当前TCP连接不是SYN_SENT和SYN_RECVD状态,说明对端重复发起了SYN连接。这种情况可能是对端突然掉电,然后重连之后发送的SYN报文,此时需要给对方发送一个ack now,然后返回,对方收到后会给本端发送一个复位报文,这样两端就会断开TCP连接。

  3.如果不是以上两种情况则进入TCP状态转换阶段,根据当前tcp连接控制块所处的状态进行判断。

     (1)当前TCP连接处于SYS_SENT:如果接收的SYN + ACK报文段,并且ackno正确,则说明是第二次握手,此时更新本端tcp控制块的发送窗口和接收窗口大小等,本端TCP连接状态转换为ESTABLISHED,然后发送acknow,并且通知应用层函数连接成功,状态: SYS_SENT ——> ESTABLISHED。  如果只是一个ACK普通的报文段,说明对方可能是半打开连接状态,此时需要给对方发送一个RST复位报文。如果重传次数小于最大SYN重传次数,则马上利用快重传再发送一个SYN报文,重新建立连接。这种情况和上面的情况2正好对应起来。状态:不变

     (2)当前TCP连接处于SYS_RECVD: 如果接收的是ACK报文段,并且ackno正确,则说明是第三次握手,此时状态由SYS_RECVD转换为ESTABLISHED,然后调用应用层的accept函数,表明三次握手已经成功。然后调用tcp_receive进行对报文进行更详细的处理。tcp_receive处理完后,如果发现报文是FIN报文,则将TCP连接状态转换为CLOSE_WAIT,然后发送一个acknow。状态转换:SYS_RECVD ——> ESTABLISHED(正常的三次握手),如果第三次是携带FIN的ACK,则状态转换:
SYS_RECVD ——> ESTABLISHED (极短时间的状态)——>CLOSE_WAIT。如果接收的ackno错误,则发送复位报文段关闭连接(有可能是恶意攻击,发送复位后,本端的tcp控制块就会被释放),然后返回。 如果接收的是SYN报文段,并且seqno和上次接收的一样,说明可能是本端刚才给对方发送的SYN报文丢包了,此时调用重传函数立即再发送一个SYN+ACK报文段,状态不变。

    (3)当前TCP连接处于CLOSE_WAIT,此时不可能接收到对方发送的数据了,所以tcp_process函数没对这个状态做处理,而在tcp_close_shutdown_fin函数对这个状态做了处理。
   (4)当前TCP连接处于 ESTABLISHED: 将报文段交给tcp_receive函数处理,处理完之后,如果报文是FIN报文段,则TCP连接状态转换为CLOSE_WAIT,然后返回。状态转换: ESTABLISHED ——> CLOSE_WAIT
    (5) 当前TCP连接处于FIN_WAIT_1: 将报文段交给tcp_receive函数处理,处理完之后,如果报文是FIN报文段并且ACK也置位了,ackno也正确,并且没有未发送的数据了,则将TCP连接状态变为TIME_WAIT,同时将tcp控制块从活跃链表,移动到TIME_WAIT链表,状态转换: FIN_WAIT_1 ——> TIME_WAIT。如果是FIN报文段但是ACK没置位,TCP状态状态转换为CLOSING,并且立即发送一个acknow,返回,这个就是两端同时关闭的情况,状态转换:FIN_WAIT_1 ——> CLOSING。如果
是一个普通的ACK报文并且ackno正确,则这个就是四次挥手中的第二次挥手,状态转换为FIN_WAIT_2,状态转换:
FIN_WAIT_1 ——> FINE_WAIT_2

  (6)当前TCP连接处于FIN_WAIT_2: 将报文段交给tcp_receive函数处理,处理完之后,如果报文是FIN报文段,状态转换为TIME_WAIT,然后发送acknow报文,然后将tcp控制块从活跃链表,移动到TIME_WAIT链表,最好返回,状态转换: FIN_WAIT_2 ——> TIME_WAIT

   (7)当前TCP连接处于CLOSING:将报文段交给tcp_receive函数处理,处理完之后,如果报文段是一个普通的ACK,并且ackno正确,并且本端没有数据发送了,则状态转换为TIME_WAIT,同时关闭中有这种情况,状态转换:CLOSING ——> TIME_WAIT。

   (8)   当前TCP连接处于LAST_ACK:将报文段交给tcp_receive函数处理,处理完之后,如果报文段是一个普通的ACK,并且ackno正确,并且本端没有数据发送了,则将接收标志位设置为TF_CLOSED, 随后就会调用tcp_input_delayed_close函数将本端的此tcp控制块移除释放掉。

三、tcp_receive函数处理

    tcp_receive函数都是由上面的tcp_process函数调用的,主要有功能有更新发送窗口,使用慢启动,拥塞控制,快恢复控制流量或者调用快重传函数。并且有对rtt和rto计算,最终将数据提交给应用层去处理。
   1.如果是ACK报文,计算出滑动窗口新的右边界,计算方法是tmp_right = right(现右边界) + ackno(上次发送的确认号)。如果不是重复报文(有可能丢包导致的)则根据报文段的窗口大小字段更新本端的发送窗口。

   2.判断同一个报文是否重复传了三次,判断方法必须符合以下五个条件:

      (1)  如果报文段的ackno小于等于上次传给对方的报文序号,说明对方在请求旧数据(已发送但未确认)。

         (2)   tcp报文段数据部分为0

         (3)   窗口大小没变

        (4)  重传定时器正在运行

       (5)  ackno和上次的ackno相等,说明请求的是同一个报文,然后对(重复ack变量)dupack ++

       以上5个条件必须都得满足,才能对dupack进行判断,判断如下:

       (1)如果dupack < 3,什么也不做

        (2)如果dupack ==3,调用tcp_rexmit_fast进行快重传,tcp_rexmit_fast函数先调用tcp_rexmit进行重传,然后更新

 pcb->ssthresh = LWIP_MIN(pcb->cwnd, pcb->snd_wnd) / 2;        pcb->cwnd = pcb->ssthresh + 3 * pcb->mss;

    /* Reset the retransmission timer to prevent immediate rto retransmissions */
    pcb->rtime = 0;  首先设置ssthresh慢启动阈值为滑动窗口的二分之一,cwnd是本地拥塞窗口,snd_wnd是对方告知的窗口,二者取最小值。然后设置cwnd拥塞窗口的值 cwnd 为慢启动阈值 + 3倍的最大报文段。

        (3)如果dupack大于3,说明重传后对方还没收到,此时将cwnd的值加上一个mss,相当于扩大了拥塞窗口。

3.如果ackno大于上次传过来的ackno,则说明不是重复的ack,是正常的ackno,此时就可以将窗口向右移动。如果当前tcp控制块处于快重传模式,则进行复位,取消快重传标志,pcb->cwnd = pcb->ssthresh;并且将拥塞窗口的值设置为慢启动阈值,紧接着复位重传次数,复位重传定时器,复位重复ack变量,设置lastack 为本次的ackno。如果当前tcp连接处于非 CLOSED,LISTEN 
  SYN_SENT ,SYN_RCVD的状态(因为这几种状态不是数据传输阶段),如果cwnd < ssthresh 执行慢启动,cwnd = cwnd + mms 每收到一个ack增加一个报文段,否则如果cwnd >= ssthresh 则执行拥塞控制算法,线性增加cwnd,cwnd = cwnd + mss * mss / cwnd。然后从未应答链表移除刚刚已经收到ack的报文段,如果已经没有未应答的报文则关闭超时重传定时器,否则将超时重传定时器置为0。

  4.如果不是2 3两种情况,则说明ackno是非法的,此时发送一个不带数据的ack报文。

  5.计算RTT(报文往返时间)和RTO(重传定时器定时时间)

  6.  如果tcp有数据,并且当前状态不处于CLOSE-WAIT, CLOSING,LAST-ACK and TIME-WAIT,RFC 793如果TCP连接状态处于CLOSE-WAIT, CLOSING,LAST-ACK and TIME-WAIT则会忽略数据(并不是忽略整个报文段),也就是说不会把数据提交给应用层
如果期望接收的序号在发来报文段范围之内(多发了已经确认部分的报文),则需要舍弃多余的数据,因为已经接收到了。否则如果seqno小于期望的序号,说明是一个重复的报文,则不对上层提交数据并且重发ack,获取想要的序号数据。如果发过来的报文段序列号在接收窗口内,也就是大于等于期望接收的序列号,则有以下情况:如果要接收的序列号等于发过来的序列号,就说明是最正常的接收数据,然后更新接收窗口大小,接收完发ack。如果seqno不正确也就是大于期望的序号,则发送一个不带数据的ack。

 7.如果tcp报文段没有数据,并且seqno不处于接收窗口内,则直接发送一个ack回应。

至此接收完毕。

 

 

 

 

你可能感兴趣的:(TCP/IP)