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

TCP协议

TCP(Transmission Control Protocol 传输控制协议)是一种面向连接的、可靠的、基于字节流的传输层通信协议,计算机网络OSI模型中,它完成第四层传输层所指定的功能。

TCP的可靠性体现在他的确认应答机制,每当客户端发起请求,会向服务器发送一个SYN请求,当服务器接收到这个请求后会向客户端回应SYN+ACK,最后客户端向服务器回应ACK,至此三次握手完毕,成功建立连接。
TCP协议的三次握手四次挥手

三次握手

第一次握手

服务器先处于CLOSED状态,调用listen后处于监听状态,之后调用accept阻塞等待客户端连接
客户端在socket之后,调用connect发起连接请求,向服务器发送SYN请求,并进入SYN_SENT状态,服务器收到SYN请求后进入SYN_RECV状态,第一次握手完毕

第二次握手

服务器进入SYN_RECV后向客户端发送SYN+ACK数据包,
客户端接收到数据包之后,connect返回,客户端进入ESTABLISHED状态,第二次握手完毕

第三次握手

客户端发送ACK数据包
服务器接收到数据包之后,accept返回之后分配新的文件描述符,服务器进入ESTABLISHED状态,三次握手完毕,双方可以正常通信。

TCP为什么是三次握手不是两次握手?

如果采用两次握手,客户端A给服务器B发送了一条请求,但是由于网络原因造成这条请求滞留时间较长,A又发送了一条请求,B收到请求后发送响应,此时AB可以正常通信,通信完毕后AB关闭。但是这时在网络中滞留的请求报文到达服务器,服务器会认为有新的客户端请求连接,但是客户端并没有真正发起请求,造成服务器资源浪费。

客户端给服务器发送请求,服务器响应之后发送给客户端,但是由于网络问题这个响应报文丢失,客户端认为连接失败,服务器认为连接成功,等待客户端发送数据,客户端重新发送,但是客户端这时发送的并不是数据而是请求连接的请求报文,服务器接收到之后同样会响应并开辟新的资源,这就造成了一个客户端可能在服务器上有两份或者多分资源造成浪费,为了保护服务器就需要三次握手。

有些恶意客户端会使用伪造IP发送多条连接请求,响应永远到达不了客户端,服务器端开辟大量资源不进行释放,服务器在等待关闭的连接过程中消耗了资源,如果请求太多造成服务器资源被耗尽。这就是SYN泛洪攻击。

四次挥手

客户端(主动关闭方)想要断开连接,先向服务器发送FIN包,服务器接收到客户端关闭连接的请求会发送一个ACK包,之后服务器发送一个FIN包,客户端向服务器发送ACK确认包,双方断开连接。
TCP协议的三次握手四次挥手

第一次挥手

客户端(主动关闭方)关闭文件描述符,发出连接释放报文,进入FIN_WAIT_1状态

第二次挥手

服务器接收到连接释放报文,read返回0,发出确认报文,ACK=1,服务器进入CLOSE_WAIT状态,服务器通知高层应用进程,客户端向服务器方向释放,服务器发送的数据客户端仍然接收

第三次挥手

客户端收到来自服务器的ACK报文,进入FIN_WAIT_2状态,等待服务器发送连接释放报文
服务器间数据处理完毕,向客户端发送FIN=1的连接释放报文,服务器进入LAST_ACK(最后确认)状态

第四次挥手

客户端接收到连接释放报文,发出确认报文,ACK=1,客户端进入TIME_WAIT状态,此时TCP并没有完全释放,在2个MSL时间之后,客户端撤销响应数据,进入CLOSED
服务器收到客户端的确认,进入CLOSED,TCP连接断开

TCP四次挥手中为什么要有TIEM_WAIT

TIME_WAIT保证客户端发送的最后一个ACK报文能够到达服务器,如果最后一个ACK报文丢失,站在服务器角度来看,我发送了ACK+FIN报文请求断开连接,客户端没有回应,服务器会认为自己发送的FIN包客户端没有收到,于是服务器会重新发送一个请求断开连接,如果没有TIME_WAIT客户端直接断开,可能会影响下一次TCP的连接,导致刚刚建立的连接因为FIN包的出现而导致错误,所以在等待2个MSL时间,确认服务器已经接收到ACK,在这2个MSL时间中可以使本连接产生的所有报文段都从网络中消失,这样新的连接不会出现旧的连接报文。TIME_WAIT是为了保护主动关闭方。

TCP服务端

当我们在启动tcp服务端后用ctrl+c终止,当我们再次想要启动时会显示:
TCP协议的三次握手四次挥手
这里虽然server应用程序关闭,但是TCP连接并没有断开,使用netstat命令查看
TCP协议的三次握手四次挥手
TCP协议规定,主动关闭的一方要处于TIME_WAIT状态等待2个MSL时间后才能回到CLOSED状态,查看当前系统的MSL:
TCP协议的三次握手四次挥手
那么如何解决TIME_WAIT状态引起bind失败?
可以在服务器中socket()和bind()之间插入如下代码

int opt = 1;
setsockopt(listen_sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));