logo头像

BUG本天成,妙手偶得之

从TCP的三次握手和四次挥手说起

只知道它有三次握手和四次挥手是不足以应付严格的面试官的…

传输控制协议(TCP,Transmission Control Protocol)是一种面向连接的、可靠的、基于字节流的传输层通信协议。

我们熟悉的HTTP就是基于TCP来的。

TCP连接在面试中也是一个很高频的话题,一般面试官的起手式为:请讲讲TCP的三次握手/四次挥手…

本文大纲如下,看不完可以收藏慢慢看(看完怕忘也是)

  • 三次握手和四次挥手的简单讲解
  • 三次握手和四次挥手的进阶讲解
  • 为什么是三次握手
  • 为什么是四次挥手
  • TCP报文格式
  • 慢开始与拥塞避免
  • 快重传与快恢复
  • time_wait何时出现,大量出现时怎么处理
  • close_wait何时出现,大量出现时怎么处理

入门

入门级回答,简单描述下客户端和服务端之间每次在做什么。

tcp

发起连接请求的是客户端,接收连接请求的是服务端。

对于图中的几个关键词,是TCP报文中的控制位中的标志(为1表示有对应标志)

  • SYN 表示建立连接
  • FIN 表示关闭连接
  • ACK 表示响应

三次握手(建立连接,红色部分):

  1. 客户端向服务端发送一个SYN包(建交吧)
  2. 服务端收到并发送一个ACK + SYN包(好的,建交吧)
  3. 客户端收到并发送一个ACK包(好的)

完成这三个步骤后客户端和服务端建立起深厚的友谊,开始你来我往,传递数据。当双发无话可说时,友谊的小船说翻就翻。

四次挥手(断开连接,绿色部分):

  1. 客户端向服务端发送一个FIN包(我对你言尽于此)
  2. 服务端收到并发送一个ACK包(好,很好)
  3. 服务端再发送一个FIN包(我对你也没啥好说的了)
  4. 客户端向服务端发送一个ACK包(嗯,你也不错)

断开连接可以由任意一方先提出,一般是客户端提出的。

进阶

上面的描述比较简单,我们可以更加深入探索客户端和服务端之间的种种行为。

tcp

  • SYN 表示建立连接
  • FIN 表示关闭连接
  • ACK 表示响应
  • seq sequence number,表示的是我方(发送方)这边,这个packet的数据部分的第一位应该在整个data stream中所在的位置
  • ack acknowledge number:表示的是期望的对方(接收方)的下一次sequence number是多少

三次握手(建立连接,红色部分):

服务端处于监听状态(LISTEN),随时准备接受连接。

  1. 客户端向服务端发送一个SYN包,同时用个随机数作为初始序列号seq = x,进入SYN_SENT状态
  2. 服务端收到并发送一个ACK + SYN包,也随机个数来作为初始序列号seq = y,进入SYN_RCVD状态
  3. 客户端收到并发送一个ACK包(好的),进入ESTABLISHED状态,表示连接建立
  4. 服务端收到ACK后也进入ESTABLISHED状态,表示连接建立

完成这四个步骤后(只是多了一个状态变化的描述,还是三次握手哈)客户端和服务端再次建立起深厚的友谊,开始你来我往,传递数据。

当然天下无不散之筵席,挥手再见的时刻即将到来。

四次挥手(断开连接,绿色部分):

还是客户端先提出分手。

  1. 客户端向服务端发送一个FIN包,进入FIN_WAIT1状态
  2. 服务端收到并发送一个ACK包,进入CLOSE_WAIT状态
  3. 服务端再发送一个FIN包,进入LAST_ACK状态
  4. 客户端向服务端发送一个ACK包,进入TIME_WAIT状态,并在等待2MSL(报文最大生存时间)变为CLOSED状态
  5. 服务端收到ACK后也变为CLOSED状态

扩展

对于上面描述的过程,如果你之前不太了解,那么针对某些点肯定会有些许疑问。



下面总结一些可以延伸的问题。

为什么建立连接要三次握手

为何是三,不是二,也不是四?借助经典的打电话的场景来帮助理解。

  • 第一次握手:A对B说,小B,能听到吗?(SYN)
  • 第二次握手:B对A说,听得到(ACK),你能听到吗?(SYN)
  • 第三次握手:A对B说,俺也一样!(ACK)

在三次握手之后,A和B都能确定这么一件事: 我说的话,你能听到; 你说的话,我也能听到。如此这般,就可以开始愉快地交流了。

  • 如果两次,那么B无法确定B的信息A是否能收到,可能B发出的消息A都收不到。
  • 如果四次,可以,但没必要。

为什么断开连接需要四次挥手

为什么不能像建立连接那样三次?毕竟三次就能保证互相知晓了。

回顾上面的图,可以看到服务端得知客户端想要断开连接后,先给客户端发了一个ACK包,然后又发了一个FIN包,问题的关键在于这两步能否合并,如果可以那么就可以精简为三次挥手。

答案当然是不可以。因为服务端得知客户端想断开连接时,它这边可能还有些事没处理完,比如还有些消息没发完(我还有话说系列)。等它处理好后,再给客户端发送一个FIN包,表示它也可以结束了,这是客户端再发个ACK包到服务端,表示他知道了。



TCP报文格式

这个问题笔者面试时被问到过,当时自信且流畅地说完TCP的连接过程,甚至在内心默默给自己点了个赞。。。后面不说也罢。

TCP的报文构造还是有点复杂的,这里不讨论了,网上找了个图(来源见水印)。

可以看到上面曾有过出镜的FIN、ACK、SYN等东东,这些都存在报文对应位置。

TCP结构

慢开始与拥塞避免

发送方维持一个叫做拥塞窗口cwnd(congestion window)的状态变量。拥塞窗口的大小取决于网络的拥塞程度,并且动态地在变化。发送方让自己的发送窗口等于拥塞窗口,另外考虑到接受方的接收能力,发送窗口可能小于拥塞窗口。

TCP结构

快重传与快恢复

快重传

发送方只要一连收到三个重复确认就应当立即重传对方尚未收到的报文段,而不必继续等待设置的重传计时器时间到期

快恢复

  1. 当发送方连续收到三个重复确认时,就执行“乘法减小”算法,把ssthresh门限减半。但是接下去并不执行慢开始算法。
  2. 考虑到如果网络出现拥塞的话就不会收到好几个重复的确认,所以发送方现在认为网络可能没有出现拥塞。所以此时不执行慢开始算法,而是将cwnd设置为ssthresh的大小,然后执行拥塞避免算法

TCP结构

time_wait何时出现,大量出现时怎么发现和处理

time_wait是主动关闭的一方会出现的状态,当收到对方发来的FIN包并返回一个ACK后,进入time_wait。

time_wait存在的原因有两点

  1. 可靠地终止连接:若处于time_wait的client发送给server确认报文段丢失的话,server将在此又一次发送FIN报文段,那么client必须处于一个可接收的状态就是time_wait而不是close状态。
  2. 保证迟来的报文段有足够的时间被识别并丢弃:linux 中一个TCPport不能打开两次或两次以上。当client处于time_wait状态时我们将无法使用此port建立新连接,假设不存在time_wait状态,新连接可能会收到旧连接的数据。

time_wait大量出现的场景,一般是服务端,因为一般是大量客户端连接少量服务端。虽然一般是客户端主动断开连接,但某些情况也可能是客户端向服务端发送一个信息,然后服务端主动关闭。这样就可能导致服务端短时间内出现大量time_wait状态,而占用了资源致使不能创建更多的socket。

几个解决思路:

  1. 改为长链接
  2. 设计时尽量让客户端主动关闭
  3. 重用端口,即服务器设置SO_REUSEADDR套接字选项来通知内核,如果端口忙,但TCP连接位于TIME_WAIT状态时可以重用端口
  4. 增加IP

close_wait何时出现,大量出现时怎么处理

close_wait是被动关闭的一方出现的状态,出现原因时,收到要关闭的信号后,自己这边还有些事情没处理完,导致迟迟不能发送FIN包给主动断开的一方。

所以说,一般大量出现都是我们的程序有问题,建议改代码。

通过netstat命令可以查看各种状态的连接数量,举个栗子:

1
2
3
4
5
6
7
8
9
10
11
➜  ~ netstat -an|awk '/tcp/ {print $6}'|sort|uniq -c
1 CLOSED
8 CLOSE_WAIT
1 CLOSING
42 ESTABLISHED
1 FIN_WAIT_1
2 FIN_WAIT_2
2 LAST_ACK
20 LISTEN
3 SYN_SENT
1 com.apple.network.tcp_ccdebug