jason--liu/Blog

TCP三次握手,四次挥手

Opened this issue · 5 comments

TCP数据头结构

首先我们看看TCP头的结构,如图所示
image
在TCP连接过程中,我们重点关注16位校验值中的ACK和FIN位。
可以使用wireshark方便抓包,其中9001是服务器端口,51034是操作系统位客户端(这里我们用telnet)分配的端口。
image

第一次握手

当客户端发起connect请求时,客户端发发送一个SYN=1,ACK=0的数据包。

第二次握手

当服务器收到收到客户端发来的第一个数据包后会回一个SYN=1,ACK=1的数据包。

第三次握手

当客户端收到服务器回的的数据包,会再发送一个数据包,其中ACK=1。
具体时序图如下
image

为什么TCP是三次握手而不是两次呢

网上有很多说法,总的来来说都是围绕数据安全和数据可靠方面来说的。

TCP四次挥手

整个连接时序图
image
和状态转换图一起放上来方便理解
image
实际抓包抓到的四次挥手过程
image
从wireshark抓到数据包来看,
1.客户端先发送ACK
2.服务器收到后回一个FIN,ACK包
3.客户端收到服务器的回应后再发一个FIN,ACK包
4.最后服务器在发送一个ACK置位的包

TIME_WAIT状态

具有TIME_WAIT状态的TCP连接,就好像一种残留的信息一样;当这种状态存在的时候,服务器程序退出并重新执行会失败,会提示:

bind返回的值为-1,错误码为:98,错误信息为:Address already in use

连接处于TIME_WAIT状态是有时间限制的(1-4分钟之间) = 2 MSL【最长数据包生命周期】;
TCP引入TIME_WAIT状态的原因:

如果服务器最后发送的ACK【应答】包因为某种原因丢失了,那么客户端一定 会重新发送FIN,这样
因为服务器端有TIME_WAIT的存在,服务器会重新发送ACK包给客户端,但是如果没有TIME_WAIT这个状态,那么无论客户端收到ACK包,服务器都已经关闭连接了,此时客户端重新发送FIN,服务器给回的就不是ACK包,而是RST【连接复位】包,从而使客户端没有完成正常的4次挥手,不友好,而且有可能造成数据包丢失;也就是说,TIME_WAIT有助于可靠的实现TCP全双工连接的终止;

RST标志
对于每一个TCP连接,操作系统是要开辟出来一个收缓冲区,和一个发送缓冲区 来处理数据的收和发;
当我们close一个TCP连接时,如果我们这个发送缓冲区有数据,那么操作系统会很优雅的把发送缓冲区里的数据发送完毕,然后再发fin包表示连接关闭;
FIN【四次挥手】,是个优雅的关闭标志,表示正常的TCP连接关闭;

反观RST标志:出现这个标志的包一般都表示 异常关闭;如果发生了异常,一般都会导致丢失一些数据包;如果将来用setsockopt(SO_LINGER)选项要是开启;发送的就是RST包,此时发送缓冲区的数据会被丢弃;RST是异常关闭,是粗暴关闭,不是正常的四次挥手关闭,所以如果你这么关闭tcp连接,那么主动关闭一方也不会进入TIME_WAIT;

netstat命令介绍

介绍命令netstat:显示网络相关信息
-a:显示所有选项
-n:能显示成数字的内容全部显示成数字
-p:显示段落这对应程序名

netstat -anp | grep -E 'State|9001'

解决TIME_WAIT状态导致bind()失败的问题

setsockopt(SO_REUSEADDR)用在服务器端,socket()创建之后,bind()之前

//参数2:是表示级别,和参数3配套使用,也就是说,参数3如果确定了,参数2就确定了;
//参数3:允许重用本地地址
int  reuseaddr=1; //开启
if(setsockopt(listenfd,SOL_SOCKET,SO_REUSEADDR, (const void *) &reuseaddr,sizeof(reuseaddr)) == -1)
{
     char *perrorinfo = strerror(errno); 
     printf("setsockopt(SO_REUSEADDR)返回值为%d,错误码为:%d,错误信息为:%s;\n",-1,errno,perrorinfo);
}

SO_REUSEADDR的能力:

  • SO_REUSEADDR允许启动一个监听服务器并捆绑其端口,即使以前建立的将端口用作他们的本地端口的连接仍旧存在;【即便TIME_WAIT状态存在,服务器bind()也能成功】
  • 允许同一个端口上启动同一个服务器的多个实例,只要每个实例捆绑一个不同的本地IP地址即可;
  • SO_REUSEADDR允许单个进程捆绑同一个端口到多个套接字,只要每次捆绑指定不同的本地IP地址即可;
  • SO_REUSEADDR允许完全重复的绑定:当一端口已经绑定到个IP地址和某个套接字上时,如果传输协议支持,同样的IP地址和端口还可以绑定到另一个套接字上;一般来说本特性仅支持UDP套接字[TCP不行];

老哥,实际抓包抓到的四次挥手过程,为什么和理论上不一样呀

哪里不一样呢,理论上挥手就是4次。

上面分析的:

从wireshark抓到数据包来看,
1.客户端先发送ACK
2.服务器收到后回一个FIN,ACK包
3.客户端收到服务器的回应后再发一个FIN,ACK包
4.最后服务器在发送一个ACK置位的包

四次挥手,理论上不是一方先发送FIN吗

emm,之前都没注意到,如果您知道答案,可以告知下^^

上面分析的:

从wireshark抓到数据包来看,
1.客户端先发送ACK
2.服务器收到后回一个FIN,ACK包
3.客户端收到服务器的回应后再发一个FIN,ACK包
4.最后服务器在发送一个ACK置位的包

四次挥手,理论上不是一方先发送FIN吗

截图是四个包,其实这次挥手是从2开始的,这次挥手只需要三次。

It is also possible to terminate the connection by a 3-way handshake, when host A sends a FIN and host B replies with a FIN & ACK (merely combines 2 steps into one) and host A replies with an ACK.[17]
https://en.wikipedia.org/wiki/Transmission_Control_Protocol

也就是说,有些机制可以使得三次挥手成为可能。

在这里我的理解是:
这是一个curl的请求,client发起FIN的时候,server已经知道没有后续请求的包了,所以可以直接关闭连接,因此就把ACK和FIN+ACK合并为一步了,这种情况下挥手只需要三次。