前言
TCP协议的连接是面向连接,可靠传输,面向字节流的,而TCP之所以能保持可靠传输是因为三次握手和四次挥手
1. 三次握手
1.1 从数据包名称和连接双方状态分析三次握手
首先我们通过数据包名和连接双方的连接状态来了解三次握手的过程
如下图所示
客户端在发送SYN数据包后客户端的状态变为SYN_SENT,当服务端接收到客户端发送的SYN数据包后,服务端的状态变为SYN_RECV,当客户端接收到服务端的SYN数据包和ACK数据包后,客户端的状态变为ESTABLISHED,当服务端接收到客户端发送的ACK数据包时,服务端的状态变为ESTABLISHED,此时客户端和服务端已完成三次握手,即建立了双向连接。
【问题一】为什么要三次握手,而两次握手不行呢?
三次握手可以保证通信双方都是有连接的,若只进行两次连接服务端回复客户端的应答并发送SYN数据报,客户端不作出回应,并不能确保客户端接收到来自服务端的数据,因此两次握手只能保证客户端到服务端的连接是有效的,并不能保证服务端到客户端的连接有效
1.2 包序管理
1.2.1 抓网络数据包
- 在windows平台下,可以使用wireshark软件来抓取网络数据包
- 在linux平台下,使用 tcpdump 来抓取网络数据,TCP和UDP的包都可以使用此命令抓取
1)万能公式:tcpdump -i any port [端口] -s 0 -w xxx.dat
2)使用root用户进行抓包
如上2)中的命令是对某个端口进行抓包,并将结果放入xxx.dat中,但我们不能直接对xxx.dat中的数据进行分析,需要借助windows平台下的wireshark软件来分析
1.2.2 分析TCP网络数据包
我们之前单进程的客户端和服务端的代码,使用tcpdump抓包分析三次握手的过程
首先在root用户下输入 tcpdump -i any port 18989 -s 0 -w 123.dat ,先进行抓包,再让客户端服务端程序跑起来,防止三次握手结束,没有抓到,让客户端服务端跑一会后终止程序,我们可以看到如下图所示:
可以看到一共抓到了64个数据包,并产生了一个123.dat文件,通过Xftp将123.dat传到windows下使用wireshark进行分析,如下图所示:
1.2.3 分析TCP包序号
为什么TCP需要包序号?
本质上是为了维护可靠传输,客户端维护了一套序号,服务端也维护了一套序号
- client–>server:消耗(seq)的是客户端维护的序号,服务端告诉客户端自己收到数据的时候,是确认(ACK)客户端的序号
- server–>client:消耗(seq)的是服务端维护的序号,客户端告诉服务端自己收到数据的时候,是确认(ACK)服务端的序号
观察如下图所示抓到的包,分析TCP包序号的变化
分析如下图所示:
- 纯ACK数据包不消耗序号
- TCP数据也消耗序号,一个字节消耗一个序号
- 确认序号ACK=消息发送方的序号+数据长度
- 确认序号的作用是:告知消息发送方,期望下次发送数据从哪一个序号开始发送
注意:TCP三次握手中,协商双方的其实序号并不一定是从0号序号开始,可以从任意位置开始,只要双方协商好就行
TCP可靠的原因就在于序号,丢了那个数据都可以知道,因为一个字节占一个序号
2. 四次挥手
通过数据包名和连接双方的状态分析四次挥手的过程如下图所示:
数据传输完毕以后,双方都可以释放连接。在最开始的时候,客户端与服务器都是处于ESTABLISHED状态,如果客户端主动关闭,则服务端被动关闭;若服务端主动关闭,则客户端被动关闭;
MSL:报文的最大生存时间,在传输中,一个报文的最大生命周期就是一个MSL时间,过了这个时间该报文就会被丢弃
【问题一】主动断开方为TIME_WAIT状态为什么要等待2MSL后才变为CLOSED状态呢?
当主动断开方为TIME_WAIT状态时,如果主动断开方发送的ACK数据包丢失,则过MSL后,被动连接方会重新发送FIN数据包,让主动断开连接方重新发送ACK数据包,若主动断开连接方MSL后将状态变为CLOSED则无法收到重发的FIN数据包,无法重新发送ACK数据包
- 2MSL = 丢失的ACK的MSL + 重传的FIN的MSL
本质就是为了让主动断开连接方能够接收到被动断开连接方重传的FIN报文
- TIME_WAIT状态存在于主动断开连接方
如果服务端位于主动断开连接方,此时一定在四次握手的过程中拥有TIME_WAIT状态;即使当服务端进程已经结束了,但是服务端之前使用TCP协议针对针对的连接还是TIME_WAIT状态,换句话说,服务端之前绑定的端口还没有被网络协议栈的TCP协议释放掉,导致服务端无法快速的重启(端口已被站用问题)
解决办法:使用setsockopt函数,让端口重用
int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen);
- sockfd:侦听套接字listen_fd
- level:SOL_SOCKET 套接字选项
- optname:SO_REUSEADDR 重用端口(只有当服务端的状态为TIME_WAIT时才能重用)
- optval:1
- optlen:opt的大小
假设客户端主动断开连接,服务端被动断开连接
1)客户端进程发出连接释放报文FIN=1,并且停止发送数据。此时,客户端进入FIN-WAIT1(终止等待1)状态。这时候客户端处于一个半关闭的状态,即客户端已经没有数据需要发送了,但是服务器若要发送数据,客户端依然需要接受。
2)服务器收到连接释放报文后,发送确认报文ACK=1。此时,服务端就进入了CLOSE-WAIT(关闭等待)状态。进入CLOSE_WAIT后说明服务器准备关闭连接。
3)客户端收到服务器的确认请求之后,此时客户端就进入了FIN-WAIT2(终止等待2)状态,等待服务器发送连接释放报文。(在这个之前还需要接受服务器发送的最后的数据)。
4)当服务器真正调用close关闭连接时, 会向客户端发送FIN=1, 此时服务器进入LAST_ACK(最后确认)状态, 等待客户端的最后一次ACK回复。
5)客户端收到服务器的链接释放报文之后,必须发出确认报文ACK=1。此时,客户端就进入了TIME-WAIT(时间等待)状态,等待用户关闭套接字。注意此时TCP链接还没有释放,必须经过2*MSL(报文最大生命周期)的时间后,当客户端撤销相应的TCP后,才进入CLOSED状态。
6)服务器只要收到客户端发出的确认,彻底关闭连接,立即就进行CLOSED状态,于是就结束了这次的TCP连接。可以看到,服务器结束TCP连接的时间要比客户端早一些。