小伙伴们大家好,本片文章将会讲解 TCP协议的三次握手和四次挥手 的相关内容。
如果看到最后您觉得这篇文章写得不错,有所收获,麻烦点赞👍、收藏🌟、留下评论📝。您的支持是我最大的动力,让我们一起努力,共同成长!
文章目录
- `1. 三次握手`
- ==<font color = blue><b>🎧1.1 从代码看三次握手🎧==
- ==<font color = blue><b>🎧1.2 三次握手的中间过程🎧==
- ==<font color = blue><b>🎧1.3 三次握手的中间状态🎧==
- ==<font color = blue><b>🎧1.4 三次握手一定要三次吗🎧==
- `2. 四次挥手`
- ==<font color = blue><b>🎧2.1 从代码看四次挥手🎧==
- ==<font color = blue><b>🎧2.2 四次挥手的中间过程🎧==
- ==<font color = blue><b>🎧2.3 四次挥手的中间状态🎧==
- ==<font color = blue><b>🎧2.4 四次挥手的原因🎧==
上一篇文章中,博主介绍了 :
- TCP 的确认应答机制;
- TCP 的捎带应答机制;
- TCP 的超时重传机制;
- TCP 的报头
建议将上一篇文章看完之后再来看这篇文章,链接如下:
【Linux网络】详解TCP协议(1)那么接下来正片开始:
1. 三次握手
🎧1.1 从代码看三次握手🎧
我们之前在客户端写的代码有以下几步:
- 创建一个套接字;
- 进行
connect()
建立链接,以下是它的接口介绍:int connect (int sockfd, const struct sockaddr *addr,socklen_t addrlen);
- 详细代码
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
connect(sockfd, serveraddr, serveraddr_len);
在服务器中写的代码有以下几步:
- 分配一个监听套接字;
- 绑定监听套接字的源地址和目标地址:
bind()
,以下是它的接口介绍:int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
- 使监听描述符成为一个监听描述符:
listen()
,以下是它的接口介绍:int listen(int sockfd, int backlog);
- 进行
accept()
阻塞等待客户端链接,以下是它的接口介绍:int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen, int flags);
- 详细代码
int listenfd = socket(AF_INET, SOCK_STREAM, 0);
bind(listenfd, localaddr, localaddr_len);
listen(listenfd, backlog);
int connfd = accept(listenfd, cilentaddr, clientaddr_len, 0);
详细解释 connect() 和 listen()
- 其实在
connect()
的时候就是 客户端向服务器发送三次握手的开始时间点; - 服务器会首先处于
listen()
状态
(监听状态),准备接受客户端的连接请求; - 在
listen()
的底层其实会维护两个队列,一个是已完成连接的队列 ,另一个是还在建立连接的队列。- 当客户端与服务器的三次握手完成后,连接将被移入已完成连接的队列,这个队列的大小实际上就是由 l i s t e n ( ) listen() listen() 的第二个参数 b a c k l o g backlog backlog 决定的,如果队列满了,服务器就会直接拒绝请求。
- 未完成连接队列存放的是那些尚未完成三次握手的连接请求。这些连接请求在被处理之前,会暂时存放在这个队列中。
- 所以
accept()
是在干什么呢?其实就是从已经完成连接建立的队列中取出一个连接,然后再分配一个文件描述符。
🎧1.2 三次握手的中间过程🎧
三次握手过程图解
在 TCP
报头中有一个标志位是 SYN
,这个标志位在三次握手中起着关键的作用。
- 客户端给服务器发送包含
SYN
标志位的报文; - 服务器接收客户端发送的报文,同意建立连接,并发送携带
ACK
和SYN
的报文(捎带应答); - 客户端接受到报文之后,会给服务器在发送一个
ACK
,客户端一旦发送了这个报文,就表示建立连接成功了。- 我们知道当接收方接收到发送方的信息的时候会给发送方发送
ACK
数据包,这样发送方就可以确定接收方收到了消息; - 那么在三次握手中,最后一次客户端发送的
ACK
,客户端本身是不能确定服务器一定收到了消息; - 所以其实客户端其实是在赌,服务器收到了消息。
- 我们知道当接收方接收到发送方的信息的时候会给发送方发送
详细解释最后一次 ACK 丢包
- 刚才说了,客户端最后一次的
ACK
服务器可能并没有收到; - 但是在客户端发送最后一次
ACK
之后,客户端立即把自己的状态变成了ESTABLISHED
; - 那么就说明客户端此时就可以立即给服务器发送携带正文的数据;
- 但是如果服务器接收到了消息,但是很明显,服务器的连接状态并没有变成
ESTABLISHED
; - 所以此时,服务器就会给客户端发送一个携带
RST
标志位的报文;
RST 标志位
- 当服务器给客户端发送这个报文的时候,服务器就会提醒客户端重新建立连接;
- 因此这样就可以完美解决最后一次
ACK
服务器可能没收到的情况。
🎧1.3 三次握手的中间状态🎧
客户端的状态变化:
[CLOSED -> SYN_SENT]
客户端调用connect
, 发送同步报文段;[SYN_SENT -> ESTABLISHED]
成功调用connect
, 则进入ESTABLISHED
状态, 开始读写数据;(成功接受服务器端的ACK + SYN
)
服务器的状态变化:
[CLOSED -> LISTEN]
服务器端调用listen
后进入 LISTEN 状态, 等待客户端连接;[LISTEN -> SYN_RCVD]
一旦监听到连接请求(同步报文段), 就将该连接放入内核等待队列中, 并向客户端发送 SYN 确认报文;[SYN_RCVD -> ESTABLISHED]
服务端一旦收到客户端的确认报文, 就进入ESTABLISHED
状态, 可以进行读写数据了。
🎧1.4 三次握手一定要三次吗🎧
三次握手可以是一次吗?
- 如果三次握手变成一次,就是说只要客户端给服务器发送一次
SYN
请求,服务器就会立马在内核中维护这个连接说明连接建立成功; - 这个时候如果是某些不法分子利用这个特点,在一台甚至多台主机上向某个服务器发送多个
SYN
请求,就会造成服务器崩溃; - 这个被称为
SYN 洪水
,SYN 洪水
具体的内容博主也不是很清楚,只知道这个在网络安全中被称之为DoS
攻击,如果各位有兴趣可以去了解一下。
三次握手可以是两次吗?
- 如果是两次,其实也不行,因为如果客户端对服务器发送的
ACK + SYN
不做处理,只是单纯的让服务器在他的操作系统内部维护连接队列,就依然会引发SYN 洪水
问题。 - 所以两次也是不行的,但是其实三次也会有类似的问题,但是这个时候服务器和客户端消耗的资源是类似的,所以少数的主机就很难将服务器挂掉。
为什么一定是三次呢?
- 首先因为三次握手可以验证网络的连通性,同时验证
TCP
是全双工的;- 因为如果是两次握手只能说明客户端可以发送数据,服务器可以发送、接受数据;
- 由于双方的地位是相同的,三次握手也可以说明他们彼此之间都想和对方通信,即达成通信共识意愿。
- 其实三次握手本质上就是四次握手,只是在第二次握手的时候服务器将
ACK
和SYN
合并成了一条数据包发送而已。
2. 四次挥手
🎧2.1 从代码看四次挥手🎧
- 在双方通信完毕之后,就会关闭掉对应的文件描述符,调用
close()
接口,下面是它的详细接口:int close(int fd);
- 其实在
close()
的时候就会发生两次次挥手,表示要和对方断开连接。 - 双方都调用
close()
就是四次挥手了。
🎧2.2 四次挥手的中间过程🎧
四次挥手过程图解
在 TCP
报头中有一个标志位是 FIN
,这个标志位在三次握手中起着关键的作用。
- 当服务器或者客户端中的一方已经把要发送给对方的数据发送完了,那么这时就会给对方发送一个带有
FIN
标志位的数据报;- 这里断开连接的一方没有特定的规定必须是客户端或者服务器,只要给对方发送的数据发送完了就行。
- 当对方接受到报文之后就会给对方发送
ACK
; - 如果过了一段时间我要发送的数据也给对方发完了,那么我也要给对方发送带有
FIN
标志位的报文,并且另一端也要给我发送ACK
表示收到了我要断开连接的请求。
两次挥手可能不会同时发生
- 刚才说了如果一方已经把消息发送完了,就会给对方发送带有
FIN
标志位的数据报,但是这个时候另一方可能还要给我发送数据,所以说这个时候另一方还不想和我断开连接; - 那么这个时候另一方还是可以和我发送数据的;
- 但是我已经把我的文件描述符
fd
关掉了,也就是这个文件描述符对应的发送和接收缓冲区也就关掉了,但是我还是要接受另一方发送的数据的,所以这里就不能单纯的调用close()
系统调用; - 这里再介绍一个系统调用
shutdown()
,下面是这个接口的用法:int shutdown(int sockfd, int how);
这个接口表示我要以何种方式关闭我的文件描述符,何种方式就是how
对应的参数,有以下几个选项:SHUT_RD
表示只关闭读端;SHUT_WR
表示只关闭写端;SHUT_RDWR
表示同时关闭读写端,这个时候就和close()
类似了。
🎧2.3 四次挥手的中间状态🎧
客户端的状态变化:
[ESTABLISHED -> FIN_WAIT_1]
客户端主动调用close
时, 向服务器发送结束报文段,同时进入FIN_WAIT_1
;[FIN_WAIT_1 -> FIN_WAIT_2]
客户端收到服务器对结束报文段的确认, 则进入FIN_WAIT_2
,开始等待服务器的结束报文段;[FIN_WAIT_2 -> TIME_WAIT]
客户端收到服务器发来的结束报文段, 进入TIME_WAIT
, 并发出LAST_ACK
;[TIME_WAIT -> CLOSED]
客户端要等待一个 2MSL(Max Segment Life, 报文
最大生存时间)的时间,才会进入CLOSED
状态。(这里待会细说)
服务器的状态变化:
[ESTABLISHED -> CLOSE_WAIT]
当客户端主动关闭连接(调用close
), 服务器会收到结束报文段, 服务器返回确认报文段并进入CLOSE_WAIT
;[CLOSE_WAIT -> LAST_ACK]
进入CLOSE_WAIT
后说明服务器准备关闭连接(需要处理完之前的数据); 当服务器真正调用close
关闭连接时, 会向客户端发送FIN
, 此时服务器进入LAST_ACK
状态,等待最后一个ACK
到来(这个ACK
是客户端确认收到了FIN
);[LAST_ACK -> CLOSED]
服务器收到了对FIN
的ACK
, 彻底关闭连接。
详细解释 CLOSE_WAIT 状态
- 当服务器或者客户端处于
CLOSE_WAIT
状态,说明只是另一方要给我发送的数据发送完了,但是我还没有把数据发送完毕; - 但是如果主机上存在大量的
CLOSE_WAIT
状态,原因就是没有正确的关闭sockfd
,导致四次挥手没有正确完成,这是一个BUG
, 只需要加上对应的close()
即可解决问题。
详细解释 TIME_WAIT 状态(重要)
TIME_WAIT
状态一般是先发送退出请求的一方会处于的状态;- 这个状态一般有两个作用:
- 首先就是虽然对方已经发送了
FIN
请求了,但是在信道中可能还存在有部分数据报并没有到达对方的接受缓冲区,所以这就是为什么要等待 2 ∗ M S L 2 * MSL 2∗MSL 的原因,MSL
就是MAX SEGMENT LIFETIME
,最大段生存时间; - 如果说不等待这个时间,可能会对下一次连接的主机产生影响,会收到来自上一个进程的迟到的数据,会有预想不到的错误;
- 第二个作用就是对方处于
LAST_ACK
状态发送了FIN
数据报给我,我收到之后会给对方发送ACK
,我要保证对方收到了我的ACK
,以保证链接正确关闭。 - 假设最后一个
ACK
丢失,那么服务器会再重发一个FIN
, 这时虽然客户端的进程不在了, 但是TCP
连接还在, 仍然可以重发LAST_ACK
。
- 首先就是虽然对方已经发送了
- 这也就是说为什么我们写一个服务器第一次绑定一个端口,如果关闭服务器之后,第二次立即重新绑定这个端口是不可能的,一般要等待一段时间。
- 当然我们以可以用
setsockopt()
这个系统调用解决这个问题,下面是他的一般用法:-
接口API:
int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen);
-
第一步:
int opt = 1;
-
第二步:
setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
-
- 当然我们以可以用
查看TIME_WAIT状态
- 博主这里用了本地的浏览器去访问了在云服务器写的
http
的服务器; - 然后关闭掉服务器;
- 在采用
netstat -natp
命令查看。 - 这里为什么本地地址是
12.0.12.12
我们之后在讲网络层IP
协议的时候会讲,这里其实是公网路由器的IP,这个路由器是公网和内网交互的路由器。
🎧2.4 四次挥手的原因🎧
- 如果只是 主机A 给 主机B 发送完了数据,那么 主机A 会给 主机B 首先发送
FIN
报文; - 但是 主机B 还没有发送完数据,因此我还要继续发送数据;
- 等到发送完了数据,主机B 才会给 主机A 发送
FIN
数据报; - 所以总的来说双方地位平等,都要给对方发送断开连接的请求(FIN)才能完美的断开连接,所以这就是为什么是四次的原因。
- 但是如果 主机A 发送
FIN
数据报的时候,主机B 接收到了请求,主机B这个时候也发送完了数据,那么就会给 主机A 同时发送携带FIN
和ACK
的报文,所以三次和四次挥手本质上没什么不同。