TCP协议中的三次握手和四次挥手

1. 准备

TCP是属于网络分层中的运输层(有的书也翻译为传输层), 分层以及每层的协议,TCP是属于运输层(有的书也翻译为传输层),如下两张图:

TCP三次握手会涉及到状态转换所以这里贴出TCP的状态转换图如下:

2.TCP三次握手简述

要想简单了解TCP三次握手,我们首先要了解TCP头部结构,如下:

TCP传递给IP层的信息单位称为报文段或段,下面都用做单位。

TCP三次握手如图:

2.1 第一次握手

客户端给服务器发送一个SYN段(在 TCP 标头中 SYN 位字段为 1 的 TCP/IP 数据包), 该段中也包含客户端的初始序列号(Sequence number = J)。

SYN是同步的缩写,SYN 段是发送到另一台计算机的 TCP 数据包,请求在它们之间建立连接

2.2 第二次握手 服务器返回客户端 SYN +ACK 段(在 TCP 标头中SYN和ACK位字段都为 1 的 TCP/IP 数据包), 该段中包含服务器的初始序列号(Sequence number = K);同时使 Acknowledgment number = J + 1来表示确认已收到客户端的 SYN段(Sequence number = J)。

ACK 是“确认”的缩写。 ACK 数据包是任何确认收到一条消息或一系列数据包的 TCP 数据包

2.3 第三次握手

客户端给服务器响应一个ACK段(在 TCP 标头中 ACK 位字段为 1 的 TCP/IP 数据包), 该段中使 Acknowledgment number = K + 1来表示确认已收到服务器的 SYN段(Sequence number = K)。

2.4 实例观察

2.4.1 tcpdump

使用tcpdump观察如下:因为都是在本机同时运行client和server所以命令为:tcpdump -i lo port 5555, 只能监听回路lo接口,结果如下

如图用红色圈起来的就是3次握手,但是为什么最后一次握手,为什么ack = 1,而不是369535922 呢, 这是因为这里的第三次握手tcpdump显示的是相对的顺序号。但是为了便于观察我们需要把tcpdump的 顺序号变为绝对的顺序号。

命令只需要加-S(大写)便可,即:tcpdump -i lo port 5555 -S

加上之后结果就正常了如下图:

从tcpdump的数据,可以明显的看到三次握手的过程是: 第一次握手:client SYN=1, Sequence number=2322326583 —> server 第二次握手:server SYN=1,Sequence number=3573692787; ACK=1, Acknowledgment number=2322326583 + 1 —> client 第三次握手:client ACK=1, Acknowledgment number=3573692787 + 1 –>server

想简单了解一下TCP三次握手的话, 看到这里就可以了.

3.TCP三次握手详细解析过程:

3.1 第一次握手

客户在socket() connect()后主动(active open)连接上服务器, 发送SYN ,这时客户端的状态是SYN_SENT 服务器在进行socket(),bind(),listen()后等待客户的连接,收到客户端的 SYN 后,

3.1.1 半连接队列(syn queue)未满

服务器将该连接的状态变为SYN_RCVD, 服务器把连接信息放到半连接队列(syn queue)里面。

3.1.2 半连接队列(syn queue)已满

服务器不会将该连接的状态变为SYN_RCVD,且将该连接丢弃(SYN flood攻击就是利用这个原理, 对于SYN foold攻击,应对方法之一是使syncookies生效,将其值置1即可,路径/proc/sys/net/ipv4/tcp_syncookies, 即使是半连接队列syn queue已经满了,也可以接收正常的非恶意攻击的客户端的请求, 但是这种方法只在无计可施的情况下使用,man tcp里面的解析是这样说的,

Centos6.9默认是置为1

半连接队列(syn queue)最大值 /proc/sys/net/ipv4/tcp_max_syn_backlog

SYN flood攻击

攻击方的客户端只发送SYN分节给服务器,然后对服务器发回来的SYN+ACK什么也不做,直接忽略掉, 不发送ACK给服务器;这样就可以占据着服务器的半连接队列的资源,导致正常的客户端连接无法连接上服务器。维基百科

(SYN flood攻击的方式其实也分两种,第一种,攻击方的客户端一直发送SYN,对于服务器回应的SYN+ACK什么也不做,不回应ACK, 第二种,攻击方的客户端发送SYN时,将源IP改为一个虚假的IP, 然后服务器将SYN+ACK发送到虚假的IP, 这样当然永远也得不到ACK的回应。)

3.2 第二次握手

服务器返回SYN+ACK段给到客户端,客户端收到SYN+ACK段后,客户端的状态从SYN_SENT变为ESTABLISHED, 也即是connect()函数的返回。

3.3 第三次握手

全连接队列(accept queue)的最大值 /proc/sys/net/core/somaxconn (默认128)

全连接队列值 = min(backlog, somaxconn) 这里的backlog是listen(int sockfd, int backlog)函数里面的那个参数backlog

3.3.1 全连接队列(accept queue)未满

服务器收到客户端发来的ACK, 服务端该连接的状态从SYN_RCVD变为ESTABLISHED, 然后服务器将该连接从半连接队列(syn queue)里面移除,且将该连接的信息放到全连接队列(accept queue)里面。

3.3.2 全连接队列(accept queue)已满

服务器收到客户端发来的ACK, 不会将该连接的状态从SYN_RCVD变为ESTABLISHED。 当然全连接队列(accept queue)已满时,则根据 tcp_abort_on_overflow 的值来执行相应动作 /proc/sys/net/ipv4/tcp_abort_on_overflow 查看参数值

3.3.2.1 tcp_abort_on_overflow = 0

则服务器建立该连接的定时器,

这个定时器是一个服务器的规则是从新发送syn+ack的时间间隔成倍的增加, 比如从新了第二次握手,进行了5次,这五次的时间分别是 1s, 2s,4s,8s,16s, 这种倍数规则叫“二进制指数退让”(binary exponential backoff)

给客户端定时从新发回SYN+ACK即从新进行第二次握手,(如果客户端设定的超时时间比较短就很容易出现异常) 服务器从新进行第二次握手的次数/proc/sys/net/ipv4/tcp_synack_retries

3.3.2.2 tcp_abort_on_overflow = 1

关于tcp_abort_on_overflow的解析如下:

意思应该是,当 tcp_abort_on_overflow 等于1 时,重置连接(一般是发送RST给客户端), 至于怎么重置连接是系统的事情了。 不过我在查资料的过程发现,阿里中间件团队博客说并不是发送RST, —[阿里中间件团队博客]

这个博客跑的实例观察到的是服务器会忽略client传过来的包,然后client重传,一定次数后client认为异常,然后断开连接。 当然,我们写代码的都知道代码是第一手的注释,实践是检验真理的唯一标准, 最好还是自己以自己实践为准,因为可能你的环境跟别人的不一样。)

查看全连接队列(accept queue)的使用情况

如上图,第二列Recv-Q是,全连接队列接收到达的连接,第三列是Send-Q全连接队列的所能容纳最大值, 如果,Recv-Q 大于 Send-Q 那么大于的那部分,是要溢出的即要被丢弃overflow掉的。

希望热心的网友帮忙提改进意见时可以直接指出哪一段第几句(比如 2.4.1 tcpdump 第一段第一句, 命令tcpdump -i lo port 5555 里参数 i 用错了,应该用 I),这样比较快速找到好改正。

4 四次挥手

【注意】中断连接端可以是Client端,也可以是Server端。

假设Client端发起中断连接请求,也就是发送FIN报文。Server端接到FIN报文后,意思是说"我Client端没有数据要发给你了",但是如果你还有数据没有发送完成,则不必急着关闭Socket,可以继续发送数据。所以你先发送ACK,“告诉Client端,你的请求我收到了,但是我还没准备好,请继续你等我的消息”。这个时候Client端就进入FIN_WAIT状态,继续等待Server端的FIN报文。当Server端确定数据已发送完成,则向Client端发送FIN报文,“告诉Client端,好了,我这边数据发完了,准备好关闭连接了”。Client端收到FIN报文后,“就知道可以关闭连接了,但是他还是不相信网络,怕Server端不知道要关闭,所以发送ACK后进入TIME_WAIT状态,如果Server端没有收到ACK则可以重传。“,Server端收到ACK后,“就知道可以断开连接了”。Client端等待了2MSL后依然没有收到回复,则证明Server端已正常关闭,那好,我Client端也可以关闭连接了。Ok,TCP连接就这样关闭了!

整个过程Client端所经历的状态如下:

而Server端所经历的过程如下:

【注意】 在TIME_WAIT状态中,如果TCP client端最后一次发送的ACK丢失了,它将重新发送。TIME_WAIT状态中所需要的时间是依赖于实现方法的。典型的值为30秒、1分钟和2分钟。等待之后连接正式关闭,并且所有的资源(包括端口号)都被释放。

【问题1】为什么连接的时候是三次握手,关闭的时候却是四次握手? 答:因为当Server端收到Client端的SYN连接请求报文后,可以直接发送SYN+ACK报文。其中ACK报文是用来应答的,SYN报文是用来同步的。但是关闭连接时,当Server端收到FIN报文时,很可能并不会立即关闭SOCKET,所以只能先回复一个ACK报文,告诉Client端,“你发的FIN报文我收到了”。只有等到我Server端所有的报文都发送完了,我才能发送FIN报文,因此不能一起发送。故需要四步握手。

【问题2】为什么TIME_WAIT状态需要经过2MSL(最大报文段生存时间)才能返回到CLOSE状态?

答:虽然按道理,四个报文都发送完毕,我们可以直接进入CLOSE状态了,但是我们必须假象网络是不可靠的,有可以最后一个ACK丢失。所以TIME_WAIT状态就是用来重发可能丢失的ACK报文。

根据第三版《UNIX网络编程 卷1》2.7节,TIME_WAIT状态的主要目的有两个:

  • 优雅的关闭TCP连接,也就是尽量保证被动关闭的一端收到它自己发出去的FIN报文的ACK确认报文;
  • 处理延迟的重复报文,这主要是为了避免前后两个使用相同四元组的连接中的前一个连接的报文干扰后一个连接。

  很明显,要实现上述两个目标,TIME_WAIT状态需要持续一段时间,但这段时间应该是多长呢?

  如果只考虑上述第一个目标,则TIME_WAIT状态需要持续的时间应该参考对端的RTO(重传超时时间)以及MSL(报文在网络中的最大生存时间)来计算而不是仅仅按MSL来计算,因为只要对端没有收到针对FIN报文的ACK,就会一直持续重传FIN报文直到重传超时,所以最能实现完美关闭连接的时长计算方式应该是从对端发送第一个FIN报文开始计时到它最后一次重传FIN报文这段时长加上MSL,但这个计算方式过于保守,只有在所有的ACK报文都丢失的情况下才需要这么长的时间;另外,第一个目标虽然重要,但并不十分关键,因为既然已经到了关闭连接的最后一步,说明在这个TCP连接上的所有用户数据已经完成可靠传输,所以要不要完美的关闭这个连接其实已经不是那么关键了。因此,(我猜)RFC标准的制定者才决定以网络丢包不太严重为前提条件,然后根据第二个目标来计算TIME_WAIT状态应该持续的时长。

对于刚才说的第二点,如何理解TIME_WAIT状态持续2MSL的时间就可以避免前后两个使用相同四元组的连接中的前一个连接的报文干扰后一个连接呢?

首先我们需要了解如下要点:

  1. TCP连接中的一端发送了FIN报文之后如果收不到对端针对该FIN的ACK,则会反复多次重传FIN报文,大约持续几分钟;
  2. 被动关闭处于LAST_ACK状态的一端在收到最后一个ACK之后不会发送任何报文,立即进入CLOSED状态;
  3. 主动关闭的一端在收到被动关闭端发送过来的FIN报文并回复ACK之后进入TIME_WAIT状态;
  4. 之所以TIME_WAIT状态需要维持一段时间而不是进入CLOSED状态,是因为需要处理对端可能重传的FIN报文或其它一些因网络原因而延迟的数据报文,不处理这些报文可能导致前后两个使用相同四元组的连接中的后一个连接出现异常(详见UNIX网络编程卷1的2.7节 第三版);
  5. 处于TIME_WAIT状态的一端在收到重传的FIN时会重新计时(rfc793 以及 linux kernel源代码tcp_timewait_state_process函数)。

  下面我们开始分析为什么在发送了最后一个ACK报文之后需要等待2MSL时长来确保没有任何属于当前连接的报文还存活于网络之中(前提是在这2MSL时间内不再收到对方的FIN报文,但即使收到了对端的FIN报文也并不影响我们的讨论,因为如果收到FIN则会回复ACK并重新计时)。

  为了便于描述,我们设想有一个处于拆链过程中的TCP连接,这个连接的两端分别是A和B,其中A是主动关闭连接的一端,因为刚刚向对端发送了针对对端发送过来的FIN报文的ACK,此时正处于TIME_WAIT状态;而B是被动关闭的一端,此时正处于LAST_ACK状态,在收到最后一个ACK之前它会一直重传FIN报文直至超时。随着时间的流逝,A发送给B的ACK报文将会有两种结局:

  1. ACK报文在网络中丢失;如前所述,这种情况我们不需要考虑,因为除非多次重传失败,否则AB两端的状态不会发生变化直至某一个ACK不再丢失。
  2. ACK报文被B接收到。我们假设A发送了ACK报文后过了一段时间t之后B才收到该ACK,则有 0 < t <= MSL。因为A并不知道它发送出去的ACK要多久对方才能收到,所以A至少要维持MSL时长的TIME_WAIT状态才能保证它的ACK从网络中消失。同时处于LAST_ACK状态的B因为收到了ACK,所以它直接就进入了CLOSED状态,而不会向网络发送任何报文。所以晃眼一看,A只需要等待1个MSL就够了,但仔细想一下其实1个MSL是不行的,因为在B收到ACK前的一刹那,B可能因为没收到ACK而重传了一个FIN报文,这个FIN报文要从网络中消失最多还需要一个MSL时长,所以A还需要多等一个MSL。

  综上所述,TIME_WAIT至少需要持续2MSL时长,这2个MSL中的第一个MSL是为了等自己发出去的最后一个ACK从网络中消失,而第二MSL是为了等在对端收到ACK之前的一刹那可能重传的FIN报文从网络中消失。另外,虽然说维持TIME_WAIT状态一段时间有2个目的,但这段时间具体应该多长主要是为了达成上述第二个目的而设计的。

Licensed under CC BY-NC-SA 4.0
Built with Hugo
主题 StackJimmy 设计