这篇文章我们将详细聊一下 tcp 的三次握手和四次挥手
TCP
传输控制协议(TCP,Transmission Control Protocol)是一种面向连接的、可靠的、基于字节流的 传输层通信协议。
TCP报文段首部最小长度是20个字节,下图是TCP报文段的首部格式
三次握手
为了方便理解我们先看一个男女生聊天的问题
男孩想找女孩聊天
上面类似 tcp 三次握手,接下来我们详细说明一下
1 | SYN: 同步位(只有在连接请求和连接请求确认才为1) |
- 第一次握手:
客户端发送连接请求报文段,无应用层数据,此时状态由 LISTEN 变为SYN_SENTSYN=1,seq=x(随机)
- 第二次握手:
服务器为该 TCP 连接分配缓存和变量,并向客户端返回确认报文段,允许连接,无应用层数据,此时状态由 LISTEN 变为SYN_RECVSYN=1,ACK=1,seq=y(随机),ack=x+1
- 第三次握手:
客户端收到后,为该 TCP 连接分配缓存和变量,并向服务器返回确认的确认,可携带数据,双方状态ESTABLISHED。ACK=1,seq=x+1,ack=y+1
注:由于第二次握手服务器会分配缓存和变量可能产生 SYN 洪宏攻击
四次挥手
咱们还是用男女聊天的问题来看一下 4 次挥手终止回话的情况
接下来我们详细说明一下
1 | FIN: 结束位 |
客户端和服务器双方都可以结束回话,本文以客户端先结束为例
- 第一次挥手
客户端发送连接释放报文段,停止发送数据,主动关闭 TCP 连接。FIN=1,seq=u
- 第二次挥手
服务器返回一个确认报文段,客户到服务器这个连接方向就释放了,此时为半关闭状态。ACK=1,seq=v,ack=u+1
- 第三次挥手
服务器发送完数据,就发送连接释放报文段,主动关闭 tcp 连接。FIN=1,ACK=1,seq=w,ack=u+1
- 第四次挥手
客户端回送一个确认报文段,再等到时间等待计时器设置的 2MSL(最长报文段寿命)后,连接彻底关闭。ACK=1,seq=u+1,ack=w+1
相关字段解释
- FIN_WAIT_1: 这个状态要好好解释一下,其实
FIN_WAIT_1
和FIN_WAIT_2
状态的真正含义都是表示等待对方的FIN
报文。
而这两种状态的区别 是:FIN_WAIT_1
状态实际上是当SOCKET
在ESTABLISHED
状态时,它想主动关闭连接,向对方发送了 FIN 报文,此时该SOCKET
即 进入到FIN_WAIT_1
状态。
而当对方回应ACK
报文后,则进入到 FIN_WAIT_2
状态,当然在实际的正常情况下,无论对方何种情况下,都应该马上回应 ACK 报文,所以FIN_WAIT_1
状态一般是比较难见到的,而FIN_WAIT_2
状态还有时常常可以用netstat
看到。 - FIN_WAIT_2:上面已经详细解释了这种状态,实际上
FIN_WAIT_2
状态下的SOCKET
,表示半连接,也即有一方要求 close 连接,但另外还告诉对方,我暂时还有点数据需要传送给你,稍后再关闭连接。
如果FIN_WAIT_1
状态下,收到了对方同时带FIN
标志和ACK
标志的报文时,可以直接进入到TIME_WAIT
状态,而无须经过FIN_WAIT_2
状态。 - CLOSING: 这种状态比较特殊,实际情况中应该是很少见,属于一种比较罕见的例外状态。正常情况下,当你发送 FIN 报文后,按理来说是应该先收到(或同时收到)对方的 ACK 报文,再收到对方的 FIN 报文。但是 CLOSING 状态表示你发送 FIN 报文后,并没有收到对方的 ACK 报文,反而却也收到了对方的 FIN 报文。
什么情况下会出现此种情况呢?其实细想一下,也不难得出结论:那就是如果双方几乎在同时 close 一个 SOCKET 的话,那么就出现了双方同时发送 FIN 报 文的情况,也即会出现 CLOSING 状态,表示双方都正在关闭 SOCKET 连接。 - CLOSE_WAIT: 这种状态的含义其实是表示在等待关闭。
怎么理解呢?当对方 close 一个 SOCKET 后发送 FIN 报文给自己,你系统毫无疑问地会回应一个 ACK 报文给对方,此时则进入到CLOSE_WAIT
状态。接下来呢,实际上你真正需要考虑的事情是察看你是否还有数据发送给对方,如果没有的话,那么你也就可以 close 这个 SOCKET,发送 FIN 报文给对方,也即关闭连接。所以你在CLOSE_WAIT
状态下,需要完成的事情是等待你去关闭连接。 - LAST_ACK: 这个状态还是比较容易好理解的,它是被动关闭一方在发送 FIN 报文后,最后等待对方的 ACK 报文。当收到
ACK
报文后,也即可以进入到CLOSED
可用状态了。
相关问题
为什么建立连接协议是三次握手,而关闭连接却是四次握手呢?
这是因为服务端的LISTEN
状态下的SOCKET
当收到SYN
报文的建连请求后,它可以把ACK
和SYN
放在一个报文里来发送。
但关闭连接时,当收到对方的FIN
报文通知时,它仅仅表示对方没有数据发送给你了;但未必你所有的数据都全部发送给对方了,所以你可以未必会马上会关闭SOCKET
,也即你可能还需要发送一些数据给对方之后,再发送 FIN 报文给对方来表示你同意现在可以关闭连接了,所以它这里的ACK
报文和FIN
报文多数情况下都是分开发送的。为什么
TIME_WAIT
状态还需要等2MSL
后才能返回到CLOSED
状态?
因为虽然双方都同意关闭连接了,而且握手的 4 个报文也都发送完毕,按理可以直接回到CLOSED
状态(就好比从SYN_SENT
状态到ESTABLISH
状态那样),但是我们必须假想网络是不可靠的,你无法保证你(客户端)最后发送的ACK
报文一定会被对方收到,就是说对方处于LAST_ACK
状态下的SOCKET
可能会因为超时未收到ACK
报文,而重发FIN
报文,所以这个TIME_WAIT
状态的作用就是用来重发可能丢失的ACK
报文。关闭 TCP 连接一定需要 4 次挥手吗?
不一定,4 次挥手关闭TCP
连接是最安全的做法。但在有些时候,我们不喜欢TIME_WAIT
状态(如当MSL
数值设置过大导致服务器端有太多TIME_WAIT
状态的 TCP 连接,减少这些条目数可以更快地关闭连接,为新连接释放更多资源),这时我们可以通过设置SOCKET
变量的SO_LINGER
标志来避免SOCKET
在close()
之后进入TIME_WAIT
状态,这时将通过发送RST
强制终止 TCP 连接(取代正常的 TCP 四次握手的终止方式)。但这并不是一个很好的主意,TIME_WAIT
对于我们来说往往是有利的。accept()
connect()
listen()
对应三次握手什么阶段- 客户端的 Connect() 函数:是一个阻塞函数,通过 TCp 三次握手与服务器建立连接,客户端主动连接服务器,建立连接方式通过 TCP 三次握手通知 Linux 内核自动完成 TCP 三次握手连接,如果连接成功为
0
,失败返回值-1
一般的情况下,客户端的connect
函数 默认是阻塞行为 直到三次握手阶段成功为止。 - 服务器端的 listen() 函数:不是一个阻塞函数,功能:将套接字 和 套接字对应队列的长度告诉
Linux
内核,他是被动连接的,一直监听来自不同客户端的请求 - 服务器端的 listen() 函数:只要作用将
socketfd
变成被动的连接监听socket
,其中参数backlog
作用是设置内核中队列的长度。 - 客户端的 accept() 函数: 阻塞,从处于
established
状态的队列中取出完成的连接,当队列中没有完成连接时候会形成阻塞,直到取出队列中已完成连接的用户连接为止。
- 客户端的 Connect() 函数:是一个阻塞函数,通过 TCp 三次握手与服务器建立连接,客户端主动连接服务器,建立连接方式通过 TCP 三次握手通知 Linux 内核自动完成 TCP 三次握手连接,如果连接成功为
服务器没有及时调用 accept 函数取走完成连接的队列怎么办?
服务器的连接队列满掉后,服务器不会对再对建立新连接的syn
进行应答,所以客户端的connect
就会返回ETIMEDOUT
。但实际上 Linux 的并不是这样的 当 TCP 连接队列满了之后 Linux 并不会书中所说的拒绝连接,只是会延时连接。TCP的6个标志位?
- SYN:同步位
- 只应用在第一、第二次握手时
- ACK:确认位
- 数值为对方的seq+1
- FIN:结束位
- 只应用在四次挥手时
- URG:紧急位
- 表示紧急指针位置有效,告诉系统此报文段中有紧急数据,应尽快传送,而不要按原来的排队顺序来传送,发送方的TCP就把紧急数据放到本报文段数据的最前面。
- URG标志位要与首部中的紧急指针字段配合使用,紧急指针指向数据段中的某个字节,(数据从第一个字节到指针所指的字节就是紧急数据)。值得注意的是即使窗口为0时也可以发送紧急数据,紧急数据不进入接收缓冲区直接交给上层进程。
- PSH:
- 客户发一个请求给服务器时希望立即能够收到对方的响应,这种情况下,客户应用程序通知TCP使用推送(push)操作,TCP就把PSH置为1,并立即创建一个报文段发送过去,类似的服务器的TCP收到一个设了PSH标志的报文段时就尽快将所有收到的数据立即提交给服务进程,而不在等到整个缓存都填满了再向上交付。
- RST:
- 这个标志表示连接复位请求。用来复位那些产生错误的连接,也被用来拒绝错误和非法的数据包。
- SYN:同步位
URG和PSH的区别?
URG:紧急标志位,表示的是此报文段中有紧急数据,将紧急数据排在普通数据的前面;当接受端收到此报文后后必须先处理紧急数据,而后再处理普通数据。
PSH: 催促标志位,当发送端将PSH置为1时,TCP会立即创建一个报文并发送。接受端收到PSH为1的报文后就立即将接受缓冲区内数据向上交付给应用程序,而不是等待缓冲区满后再交付。