TCP的建立——三次握手
1.服务器必须准备好接受外来的连接。通常通过调用socket,bind,listen这三个函数来完成,我们称之为被动打开(passive open)。
2. 客户端通过调用connect函数发起主动的打开(active open)。这导致客户TCP发送一个SYN(同步)分节,它告诉服务器客户将在(待建立的)连接中发送的数据的初始序列号。通常SYN分节不携带任何数据,其所在IP数据报只含有一个IP首部,一个TCP首部以及可能有的TCP选项。
3. 服务器必须确认(ACK)客户的SYN,同时自己也发送一个SYN分节,它含有服务器将在同一连接中发送的数据的初始序列号。服务器单个分节中发送SYN和客户SYN的ACK(确认)。(客户端的connect函数返回)
4. 客户必须确认服务器发来的SYN,发送一个ACK,然后服务器的accept函数返回。
这种交换至少需要三个分组,因此称之为TCP的三次握手。上图展示了所交换的三个分节。
SYN、ACK的序列号
上图给出的客户初始序列号为J,服务器的初始序列号为K。ACK中的确认号发送这个ACK一端所期待的下一个序列号。因为SYN占据一个字节的序列号空间,所以每一个SYN的ACK确认号就是SYN的初始序列号加1。类似的下文中断开连接的FIN的ACK也是FIN的序列号加1。
当然三次握手最主要的目的是为了知道双方的初始序列号,因为TCP是全双工通信。这样后续的信息的收发才能确保准确安全
TCP断开——四次挥手
TCP终止一个连接需要四个分节。
1.某个应用进程首先调用close,我们称之为该端执行主动关闭(active close)。该端的TCP于是发送一个FIN分节,表示数据发送完毕。
2.接收到这个FIN的对端执行被动关闭(passive close)。这个FIN由TCP确认(发送ACK进行确认)。它的接收也作为一个文件结束符(end-of-file)传递给接收端应用进程(放在已排队等候应用进程接收的任何其它数据之后),因为FIN的接收意味着接收端应用进程在相应连接上再无额外数据可接收。
3.一段时间后,接收到这个文件结束符的应用进程将调用close关闭它的套接字。这导致它的TCP也发送一个FIN。
4.接收这个最终FIN的原发送端TCP(即主动关闭的那一端)确认这个FIN(发送ACK进行确认)。
既然每个方向都需要一个FIN和一个ACK,因此通常需要四个分节,所以我们称他为四次挥手。
在步骤2与步骤3之间,执行被动关闭的一端到执行主动关闭一端数据流动是可能的。这称为半关闭。
面试经典问题,tcp四次挥手可以变成三次吗?
答案是可以的。我们先抓个包来验证一下:
大家可以看到,这里我将客户端关闭并抓包进行查看,首先是客户端(port = 44722)给服务端(port = 8848)发送了一个FIN请求关闭,然后服务端将FIN和ack合并成一条发给了客户端,然后客户端在发送ack给服务端,关闭完成。
这里四次挥手->三次挥手就是服务端将FIN和ack合并成一条进行发送。为什么会进行合并呢?是因为在关闭的时候,服务端没有数据发送给客户端,然后优化后就会将FIN和ack合并在一起发送给客户端。
time_wait
1. 首先什么是time_wait
如上图,在服务器端发送一个FIN时,客户端会处于time_wait状态。当处于time_wait状态时,我们无法创建新的连接,因为端口被占用。
2. time_wait有什么作用
(1)可靠的终止TCP连接。
若处于time_wait的客户端发送给服务器确认报文段丢失的话,服务器将在此重新发送FIN报文段,那么客户端必须处于一个可接收的状态就是time_wait状态而不是close状态。
(2)保证让迟来的TCP报文段有足够的时间识别并丢弃
linux中一个TCP端口不能被打开两次或两次以上,当客户端处于time_wait状态时我们将无法使用此端口建立新连接,如果不存在time_wait状态,新连接可能会收到旧连接的数据。
为什么在time_wait中就可以保证旧数据完全被销毁?
因为网络中数据存在时间最大为MSL(maxinum segment lifetime),而time_wait 持续时间为2MSL所以保证网络中的数据可以丢弃。
3一定是客户端才有time_wait状态吗?
不一定。
当客户端进行主动关闭时,time_wait存在于客户端,但是当服务器执行主动关闭或者发生异常时,会产生在服务器,所以当服务器异常断开时,你可能需要等待一会才能重启服务器,如果你立即重启服务器,终端会提醒你端口被占用。
上图是我们启动服务器正常运转,当我的服务器发生异常时,time_wait就存在于服务器,然后我立即重启服务器,则bind函数会返回,如下图所示,端口被占用。
如何避免time_wait状态占用资源
如果是客户端,我们一般不需要担心,因为客户端一般选择的都是临时端口,再次创建新的连接会分配未被占用的端口。除非客户端指定使用某一个端口,但是不需要这么做。
如果在服务器主动关闭后异常终止,因为服务器使用的是指定的服务器端口,所以time_wait状态将导致它不能重启,需要等待一段时间,但在大型服务器中,会造成巨额的损失。
如何避免这种情况的发生呢?
1.我们可以使用socket的选项SO_REUSEADDR来强制进程立即使用time_wait状态的连接占用端口。通过setsockopt设置后,即使服务器处于time_wait状态,与之绑定的socket地址也可以立即被重启使用。
setsockopt的用法
当我在我的服务器中设置好之后,我异常关闭服务器再立即重启就不会显示端口被占用,而是立即可以重启。
2、设置SO_LINGER套接字选项也可以避免TCP的time_wait状态。
#include<sys/socket.h>
struct linger
{
int l_onoff;
int l_linger;
};
如果将l_onoff设置为非0值,而l_linger为0。那么当close某个连接时,TCP将中止该连接。这就是说TCP将丢弃保留在套接字发送缓冲区的任何数据,并发送RST给一个对端,而没有通常的四次挥手连接终止序列。这么一来就避免了TIME_WAIT状态。
但是会存在一个问题,在2MSL秒内我们创建该连接的一个化身,导致来自刚被终止的连接上的旧的重复分节被不正确地递送到新的化身上。
下面我们用实际例子进行讲解
我们在服务器端给监听套接字listenfd设置了SO_LINGER选项
//设置SO_LINGER选项
struct linger ser_lin;
ser_lin.l_onoff = 1;
ser_lin.l_linger = 0;
setsockopt(listenfd, SOL_SOCKET, SO_LINGER, &ser_lin, sizeof(ser_lin));
然后我们用客户端进行连接,并断开,我用tcpdump抓到如下的包
可以看到当客户端(port = 39624)给服务端(port = 6666)发送了一个FIN分节,告知服务器。因为服务端设置了如上的SO_LINGER选项,导致服务端给客户端发送了RST分节,当客户收到这个分节后就立即断开。因为没有了正常的四次挥手断开连接,所以避免了TIME_WAIT状态。