TCP三次握手和四次挥手
TCP(Transmission Control Protocol)报文结构
-
**源端口和目的端口:**各占2个字节,分别写入源端口号和目的端口号。
-
**序号:**占4个字节。序号使用mod运算。TCP是面向字节流的,在一个TCP连接中传送的字节流中的每一个字节都按顺序编号。故该字段也叫做“报文段序号”。
-
**确认序号:**占4个字节,是期望收到对方下一个报文段的第一个数据字节的序号。若确认序号=N,则表明:到序号N-1为止的所有数据都已正确收到。
-
**数据偏移:**占4位,表示TCP报文段的首部长度。注意,“数据偏移”的单位是32位字(即以4字节长的字为计算单位)。故TCP首部的最大长度为60字节。
-
**保留:**占6位,保留为今后使用,目前置为0;
-
**紧急URG:**当URG=1,表明紧急指针字段有效。这时发送方TCP就把紧急数据插入到本报文段数据的最前面,而在紧急数据后面的数据仍是普通数据。
-
**确认ACK:**当ACK=1时,确认字段才有效。当ACK=0时,确认号无效。TCP规定,在连接建立后所有传送的报文段都必须把ACK置1。
-
**推送PSH:**接收方TCP收到PSH=1的报文段,就尽快地交付给接收应用进程,而不再等到整个缓存都填满了后再向上交付。
-
**复位RST:**当RST=1时,表明TCP连接中出现严重差错,必须释放连接,然后再重新建立运输连接。
-
**同步SYN:**在连接建立时用来同步序号。当SYN=1而ACK=0时,表明这是一个连接请求报文段。对方若同意建立连接,则应在响应的报文段中使SYN=1和ACK=1。故SYN置为1,就表示这是一个连接请求和连接接收报文。
-
**终止FIN:**用来释放连接。当FIN=1时,表明此报文段的发送方的数据已发送完毕,并要求释放运输连接。
-
**窗口:**占2个字节。窗口值作为接收方让发送方设置其发送窗口的依据。
-
**检验和:**占2字节。检验和字段检验的范围包括首部和数据这两部分。和UDP数据报一样,在计算检验和时,也要在TCP报文段的前面加上12字节的伪首部。伪首部的格式与UDP用户数据报的伪首部一样,但要将伪首部第四个字段中的17 改为6(协议号),把第5字段中的UDP长度改为TCP长度。
-
**紧急指针:**占2字节。紧急指针仅在URG=1时才有意义,它指出本报文段中的紧急数据的字节数。
TCP三次握手过程
整个流程为:
-
客户端主动打开,发送连接请求报文段,将SYN标识位置为1,Sequence Number置为x(TCP规定SYN=1时不能携带数据,x为随机产生的一个值),然后进入SYN_SEND状态
-
服务器收到SYN报文段进行确认,将SYN标识位置为1,ACK置为1,Sequence Number置为y,Acknowledgment Number置为x+1,然后进入SYN_RECV状态,这个状态被称为半连接状态
-
客户端再进行一次确认,将ACK置为1(此时不用SYN),Sequence Number置为x+1,Acknowledgment Number置为y+1发向服务器,最后客户端与服务器都进入ESTABLISHED状态
wireshark抓包三次握手
序列号为当前端成功发送的数据位数,确认号为当前端成功接收的数据位数
某大佬的纠错
1、序列号是“随机”的;
2、ACK +1的问题;
序列号应该不是随机的,而是由双方协商的,协商之前最开始的值由TCP运输层生成。 它可能是上一个该端口的序列号+1 ,原因是为了避免上次已断开的连接,还存在网络中延迟到达的TCP报文段的序号与当前连接中等待报文段的序号相同,以至于认为是这一次连接的包,发生错误。
关于ACK+1,在文中“包2”中如下所述:“需要注意的是,尽管客户端没有发送任何有效数据,确认号还是被加1,这是因为接收的包中包含SYN或FIN标志位(并不会对有效数据的计数产生影响,因为含有SYN或FIN标志位的包并不携带有效数据)”这实际上是误解,因此误导了很多人。在停-等协议中,ACK确认的是当前包的序列号Seq=1 ,接收端会传回 ACK=1 。标识接收到了Seq=1的包,这是我们认为理所当然的。但是在TCP协议中不是这样, 当发生Seq=1的包,接受端会传回ACK=2, 也就是将接受到的Seq+1 , 代表的是期望接受到的下一个包是 Seq=2的包。 (换句话说,和头部的标志位并没有任何关系)
为什么要这样呢?… 这么做比较违反我们的直觉。因为TCP是以流水线发出的,比如发送端顺序的发出 Seq=1、Seq=2、Seq=3。 那么如果ACK确认的序号和收到的包的序号一致的话,那么需要发回 ACK=1、ACK=2、ACK=3 共三个包。但是TCP协议对此进行了优化,只需要发送一个ACK包就能代表说自己已经收到了前面三个包, 那就是发送ACK=4 (期望收到Seq为4的包)。这样节省了ACK确认的数量。
另外TCP是的序号是根据数据流编码的, 假设最开始Seq=0 Len=3, 那么 ACK=4的时候:第一个意思是想表明期待收到下一个Seq为4的包。第二个意思实际上是说,想收到的包开始的那个比特位于数据流中的第四个比特。(下次从数据流中的第四个Byte开始发送)(实际上上面两个意思是一样的 = = ,同时也再次说明这个ACK+1和头部的标志位无关)
以上问题在 计算机网络:自顶向下 中都有说到, 因为书中和文字有不一致的地方,然后我提出来纠正一下。
为什么在第3步中客户端还要再进行一次确认呢?
这主要是为了防止已经失效的连接请求报文段突然又传回到服务端而产生错误的场景:所谓"已失效的连接请求报文段"是这样产生的。正常来说,客户端发出连接请求,但因为连接请求报文丢失而未收到确认。于是客户端再次发出一次连接请求,后来收到了确认,建立了连接。数据传输完毕后,释放了连接,客户端一共发送了两个连接请求报文段,其中第一个丢失,第二个到达了服务端,没有"已失效的连接请求报文段"。
现在假定一种异常情况,即客户端发出的第一个连接请求报文段并没有丢失,只是在某些网络节点长时间滞留了,以至于延误到连接释放以后的某个时间点才到达服务端。本来这个连接请求已经失效了,但是服务端收到此失效的连接请求报文段后,就误认为这是客户端又发出了一次新的连接请求。于是服务端又向客户端发出请求报文段,同意建立连接。假定不采用三次握手,那么只要服务端发出确认,连接就建立了。
由于现在客户端并没有发出连接建立的请求,因此不会理会服务端的确认,也不会向服务端发送数据,但是服务端却以为新的传输连接已经建立了,并一直等待客户端发来数据,这样服务端的许多资源就这样白白浪费了。
采用三次握手的办法可以防止上述现象的发生。比如在上述的场景下,客户端不向服务端的发出确认请求,服务端由于收不到确认,就知道客户端并没有要求建立连接。
TCP四次挥手
TCP三次握手是TCP连接建立的过程,TCP四次挥手则是TCP连接释放的过程**。**下面是TCP四次挥手的流程图:
当客户端没有数据再需要发送给服务端时,就需要释放客户端的连接,这整个过程为:
- 客户端发送一个报文给服务端(没有数据),其中FIN设置为1,Sequence Number置为u,客户端进入FIN_WAIT_1状态
- 服务端收到来自客户端的请求,发送一个ACK给客户端,Acknowledge置为u+1,同时发送Sequence Number为v,服务端年进入CLOSE_WAIT状态
- 服务端发送一个FIN给客户端,ACK置为1,Sequence置为w,Acknowledge置为u+1,用来关闭服务端到客户端的数据传送,服务端进入LAST_ACK状态
- 客户端收到FIN后,进入TIME_WAIT状态,接着发送一个ACK给服务端,Acknowledge置为w+1,Sequence Number置为u+1,最后客户端和服务端都进入CLOSED状态
为什么建链接要3次握手,断链接需要4次挥手?
-
**对于建链接的3次握手,**主要是要初始化Sequence Number 的初始值。通信的双方要互相通知对方自己的初始化的Sequence Number(缩写为ISN:Inital Sequence Number)——所以叫SYN,全称Synchronize Sequence Numbers。也就上图中的 x 和 y。这个号要作为以后的数据通信的序号,以保证应用层接收到的数据不会因为网络上的传输的问题而乱序(TCP会用这个序号来拼接数据)。
-
**对于4次挥手,**其实你仔细看是2次,因为TCP是全双工的,所以,发送方和接收方都需要Fin和Ack。只不过,有一方是被动的,所以看上去就成了所谓的4次挥手。如果两边同时断连接,那就会就进入到CLOSING状态,然后到达TIME_WAIT状态。下图是双方同时断连接的示意图(你同样可以对照着TCP状态机看):
wireshark抓包四次挥手
深入理解TCP握手过程中建连接的流程和队列
(图片来源:http://www.cnxct.com/something-about-phpfpm-s-backlog/)
如上图所示,这里有两个队列:syns queue(半连接队列);accept queue(全连接队列)。
三次握手中,在第一步server收到client的syn后,把这个连接信息放到半连接队列中,同时回复syn+ack给client(第二步);
第三步的时候server收到client的ack,如果这时全连接队列没满,那么从半连接队列拿出这个连接的信息放入到全连接队列中,否则按tcp_abort_on_overflow指示的执行。
这时如果全连接队列满了并且tcp_abort_on_overflow是0的话,server过一段时间再次发送syn+ack给client(也就是重新走握手的第二步),如果client超时等待比较短,client就很容易异常了。
在我们的os中retry 第二步的默认次数是2(centos默认是5次):
如果TCP连接队列溢出,有哪些指标可以看呢?
上述解决过程有点绕,听起来懵,那么下次再出现类似问题有什么更快更明确的手段来确认这个问题呢?(通过具体的、感性的东西来强化我们对知识点的理解和吸收。)
netstat -s
比如上面看到的 667399 times ,表示全连接队列溢出的次数,隔几秒钟执行下,如果这个数字一直在增加的话肯定全连接队列偶尔满了。
ss 命令
上面看到的第二列Send-Q 值是50,表示第三列的listen端口上的全连接队列最大为50,第一列Recv-Q为全连接队列当前使用了多少。
全连接队列的大小取决于:min(backlog, somaxconn) . backlog是在socket创建的时候传入的,somaxconn是一个os级别的系统参数。
这个时候可以跟我们的代码建立联系了,比如Java创建ServerSocket的时候会让你传入backlog的值:
(来自JDK帮助文档:https://docs.oracle.com/javase/7/docs/api/java/net/ServerSocket.html)
半连接队列的大小取决于:max(64, /proc/sys/net/ipv4/tcp_max_syn_backlog),不同版本的os会有些差异。
我们写代码的时候从来没有想过这个backlog或者说大多时候就没给他值(那么默认就是50),直接忽视了他,首先这是一个知识点的盲点;其次也许哪天你在哪篇文章中看到了这个参数,当时有点印象,但是过一阵子就忘了,这是知识之间没有建立连接,不是体系化的。但是如果你跟我一样首先经历了这个问题的痛苦,然后在压力和痛苦的驱动自己去找为什么,同时能够把为什么从代码层推理理解到OS层,那么这个知识点你才算是比较好地掌握了,也会成为你的知识体系在TCP或者性能方面成长自我生长的一个有力抓手。
netstat 命令
netstat跟ss命令一样也能看到Send-Q、Recv-Q这些状态信息,不过如果这个连接不是Listen状态的话,Recv-Q就是指收到的数据还在缓存中,还没被进程读取,这个值就是还没被进程读取的 bytes;而 Send 则是发送队列中没有被远程主机确认的 bytes 数。
netstat -tn 看到的 Recv-Q 跟全连接半连接没有关系,这里特意拿出来说一下是因为容易跟 ss -lnt 的 Recv-Q 搞混淆,顺便建立知识体系,巩固相关知识点 。
比如如下netstat -t 看到的Recv-Q有大量数据堆积,那么一般是CPU处理不过来导致的:
上面是通过一些具体的工具、指标来认识全连接队列(工程效率的手段)。
实践验证一下上面的理解
把java中backlog改成10(越小越容易溢出),继续跑压力,这个时候client又开始报异常了,然后在server上通过 ss 命令观察到:
按照前面的理解,这个时候我们能看到3306这个端口上的服务全连接队列最大是10,但是现在有11个在队列中和等待进队列的,肯定有一个连接进不去队列要overflow掉,同时也确实能看到overflow的值在不断地增大。
总结
全连接队列、半连接队列溢出这种问题很容易被忽视,但是又很关键,特别是对于一些短连接应用(比如Nginx、PHP,当然他们也是支持长连接的)更容易爆发。 一旦溢出,从cpu、线程状态看起来都比较正常,但是压力上不去,在client看来rt也比较高(rt=网络+排队+真正服务时间),但是从server日志记录的真正服务时间来看rt又很短。
jdk、netty等一些框架默认backlog比较小,可能有些情况下导致性能上不去。
参考:https://www.cnblogs.com/kaleidoscope/p/9701117.html
https://mp.weixin.qq.com/s?__biz=MzIzOTU0NTQ0MA==&mid=2247487779&idx=1&sn=0980243dcec05c5df8e2e60937c2c5ed&chksm=e9292c2cde5ea53a7b309e3575c78940a378b358292a3badb828d6c9bb6e8d3997800564b1d5&mpshare=1&scene=23&srcid=0705cNzStWAzvxd5GzatiiNZ#rd