目录
TCP连接建立
TCP建立连接的过程叫作握手,握手需要在客户和服务器之间交换三个TCP报文
现在A是客户端,B是服务器端,最初两端都处于CLOSED状态,假设A主动打开连接,B被动打开连接。
第一次握手:
一开始B服务器端先创建传输控制块TCB(调用listen)后进入LISTEN 状态,等待A客户端的连接请求。
A的客户端也是首先创建传输控制块TCB ,此时A客户端打算建立TCP连接(调用connect),向B服务器端发送了一个请求连接报文段(即SYN = 1 ),同时初始序号seq=x ,而该报文不能携带任何数据,这时,A客户端进入SYN-SENT状态。
第二次握手:
B服务器端收到A客户端发送的请求连接报文段并同意建立连接,则B向A发送确认报文段。在确认报文段中SYN和ACK都置为1,确认序号ack=x+1 ,序号seq=y ,这个报文段也不能携带任何数据。这时,B服务器端进入SYN-RCVD状态。此阶段B也可能拆分成两个报文发送给A,即先发送一个确认报文段(ACK=1,ack=x+1 ),再发送一个连接报文段(SYN=1,seq=y),结果都是一样的。
第三次握手:
A客户端收到B的确认报文段后,还要向B发送一个确认报文段,该确认报文段中ACK=1 ,确认序号ack=y+1 ,序号seq=x+1 ,TCP标准规定,此报文段是可以携带数据的。这时,TCP连接已经建立,A进入ESTABLISHED状态,B收到A的确认后也进入ESTABLISHED状态,TCP建立连接成功。
问题思考
1.为什么要三次握手?
连接的本质其实就是内核的一种数据结构类型,建立连接成功的时候,就是在内存中创建对应连接对象,在对多个连接对象进行某种数据结构的组织。
所以要明白一个道理,维护连接是需要成本的(内存+CPU)
原因一:三次握手是确认双方主机状态和收发信道的是否通畅的最小次数,从而验证了全双工
在三次握手中客户端必然会收到数据和发送数据,以此证明自己的收发信道的通畅,以及获得对方主机状态。服务端也必然会收到数据和发送数据,证明自己的收发信道的通畅,以及获得对方主机状态。
原因二:服务端可以嫁接同等的成本给客户端
(1)如果只进行一次握手
如果服务端收到来自大量SYN报文连接请求(SYN洪水),因为只需要一次握手就能建立连接,但是每次连接都需要创建对象并消耗资源,这样导致服务端的资源很快就会消耗完的,服务端因此就会挂掉,所以这肯定是不行的。
(2)如果只进行两次握手
和一次握手的情况类似,只要服务端发出ACK报文段,那么说明连接就建立起来了。如果服务端收到大量请求连接报文,并且逐一发送ACK报文段后,连接就建立成功,服务端还是要消耗大量资源。所以也不行。
(3)进行三次握手
如果服务端收到了大量请求连接报文,并逐一回复ACK+SYN报文段,此时服务端不会创建对应的连接对象,因为连接还没有建立成功。要想连接建立成功,客户端就需要发送大量ACK报文段,一旦客户端发送了ACK报文段后,会认为此时TCP连接已经建立成功,客户端就需要调用资源来维护该连接,这样服务端的成本就嫁接到客户端上了。
2.三次握手一定要保证成功吗?
不一定,因为最后一次发送的ACK报文段,客户端不能保证服务器端一定收到了该报文,所以有可能三次握手失败。
TCP连接释放
假设现在数据传输完成,A先发出连接释放报文段并停止发送数据。
第一次挥手
A发送连接释放报文段,其首部的终止控制位FIN置1,序号seq=u,u为前面已传送数据的最后一个字节的序号加1。这时,A进入FIN-WAIT-1状态。
第二次挥手
B收到A的连接释放报文段后,发送确认报文段(ACK=1 ),确认序号ack=u+1 ,序号seq=v,v为前面已传送数据的最后一个字节的序号加1。然后B就进入CLOSE-WAIT状态,这时TCP连接处于半关闭状态,即从A到B这个方向的连接已经释放了,A已经没有数据发送给B了,但是B若有数据发送给A,A仍然要接收。
A收到B的确认报文段后,就进入了FIN-WAIT-2状态。
第三次挥手
B发出连接释放报文段(FIN=1),序号seq=w,假定B又发送了一些数据给A,那么B还必须重复上次已发送的确认序号ack=u+1,这时,B进入LAST-ACK状态。
第四次挥手
A收到B的连接释放报文段后,A就必须向B发出确认报文段(ACK= 1),确认序号ack=w+1 ,自身序号seq=u+1 ,这时,A进入TIME-WAIT状态,此时TCP连接还没有释放,A需要等待2MSL(Max Segment Life, 报文最大生存时间)时间后,才会进入CLOSED状态。
B收到A的确认报文段后,就进入了CLOSED状态。
问题思考
1.理解TIME-WAIT状态
在TIME_WAIT期间仍然不能再次监听同样的端口号,因为虽然四次挥手已经完成,但是主动断开连接的一方要维持一段时间的TIME_WAIT状态,在该状态下,其地址信息,IP,端口号依旧是被占用的,所以你断开又立马连接会连接失败的。
MSL在RFC793中建议设为2分钟,但是不同的操作系统实现的不同,在Centos7上默认配置的值是60s。
可以通过cat /proc/sys/net/ipv4/tcp_fin_timeout 查看MSL的值。
为什么进入TIME-WAIT状态必须等待2MSL的时间呢?
原因一:保证在两个传输方向上的尚未被接收或迟到的报文段都已经消失。否则服务器立刻重启, 可能会收到来自上一个进程的迟到的数据, 但是这种数据很可能是错误的。
原因二 :保证最后一个报文段可靠到达。假设最后一个ACK 报文段丢失, 那么服务器会再重发FIN+ACK 报文段。这时虽然客户端的进程不在了, 但是TCP连接还在, 仍然可以重发FIN+ACK报文段。
解决TIME_WAIT状态引起的bind失败的方法
现实中服务端需要处理非常多的客户端的连接(每个连接的生存时间可能很短,但是每秒都有大量的客户端发来请求)。这个时候如果由服务端主动关闭连接(比如某些客户端不活跃,就需要被服务端主动清理掉), 就会产生大量TIME_WAIT连接,由于我们的请求量很大, 就可能导致TIME_WAIT的连接数很多, 每个连接都会占用一个通信五元组(源ip,源端口, 目的ip, 目的端口, 协议)。 其中服务器的ip、端口和协议是固定的,如果新来的客户端连接的ip、端口号和TIME_WAIT占用的连接重复了,就会出现问题。
使用setsockopt()设置socket描述符的 选项SO_REUSEADDR为1, 表示允许创建端口号相同但IP地址不同的多个socket描述符
2.理解CLOSE-WAIT状态
对于服务器上出现大量的 CLOSE_WAIT状态, 原因就是服务器没有正确的关闭 socketfd,导致四次挥手没有正确完成. 这是一个BUG. 只需要加上对应的 close 即可解决问题。
如有写的不好或错误的地方,希望能指正,谢谢。