当前位置: 首页 > article >正文

【Linux网络】详解TCP协议(2)

图片名称
🎉博主首页: 有趣的中国人

🎉专栏首页: Linux网络

🎉其它专栏: C++初阶 | C++进阶 | 初阶数据结构

在这里插入图片描述

小伙伴们大家好,本片文章将会讲解 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 从代码看三次握手🎧


我们之前在客户端写的代码有以下几步:

  1. 创建一个套接字;
  2. 进行connect()建立链接,以下是它的接口介绍:
    • int connect (int sockfd, const struct sockaddr *addr,socklen_t addrlen);
  3. 详细代码
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
connect(sockfd, serveraddr, serveraddr_len);

在服务器中写的代码有以下几步:

  1. 分配一个监听套接字;
  2. 绑定监听套接字的源地址和目标地址:bind(),以下是它的接口介绍:
    • int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
  3. 使监听描述符成为一个监听描述符:listen(),以下是它的接口介绍:
    • int listen(int sockfd, int backlog);
  4. 进行 accept() 阻塞等待客户端链接,以下是它的接口介绍:
    • int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen, int flags);
  5. 详细代码
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,这个标志位在三次握手中起着关键的作用。

  1. 客户端给服务器发送包含 SYN 标志位的报文;
  2. 服务器接收客户端发送的报文,同意建立连接,并发送携带 ACK SYN 的报文(捎带应答);
  3. 客户端接受到报文之后,会给服务器在发送一个 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是全双工的;
    • 因为如果是两次握手只能说明客户端可以发送数据,服务器可以发送、接受数据;
  • 由于双方的地位是相同的,三次握手也可以说明他们彼此之间都想和对方通信,即达成通信共识意愿。
  • 其实三次握手本质上就是四次握手,只是在第二次握手的时候服务器将 ACKSYN 合并成了一条数据包发送而已。

在这里插入图片描述



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] 服务器收到了对 FINACK, 彻底关闭连接。

详细解释 CLOSE_WAIT 状态

  • 当服务器或者客户端处于 CLOSE_WAIT 状态,说明只是另一方要给我发送的数据发送完了,但是我还没有把数据发送完毕;
  • 但是如果主机上存在大量的 CLOSE_WAIT 状态,原因就是没有正确的关闭sockfd,导致四次挥手没有正确完成,这是一个 BUG, 只需要加上对应的 close() 即可解决问题。

详细解释 TIME_WAIT 状态(重要)

  • TIME_WAIT 状态一般是先发送退出请求的一方会处于的状态;
  • 这个状态一般有两个作用:
    • 首先就是虽然对方已经发送了FIN请求了,但是在信道中可能还存在有部分数据报并没有到达对方的接受缓冲区,所以这就是为什么要等待 2 ∗ M S L 2 * MSL 2MSL 的原因, 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 同时发送携带FINACK的报文,所以三次和四次挥手本质上没什么不同。

在这里插入图片描述


http://www.kler.cn/a/325063.html

相关文章:

  • 深入理解Redis(七)----Redis实现分布式锁
  • PostgreSQL技术内幕18:物理备份工具pg_basebackup
  • 游戏引擎学习第14天
  • ScubaGear:用于评估 Microsoft 365 配置是否存在安全漏洞的开源工具
  • 【计算机网络】水平触发与边缘触发有什么优缺点呢?
  • Uni-APP+Vue3+鸿蒙 开发菜鸟流程
  • 网站建设中常见的网站后台开发语言有哪几种,各自优缺点都是什么?
  • python和pyqt-tools安装位置
  • 【从零开始实现stm32无刷电机FOC】【实践】【7.1/7 硬件设计】
  • 【Golang】关于Go语言字符串转换strconv
  • 《牧神记》PV初体验,玄机科技再塑经典国漫
  • 学习C++的第七天!
  • 新建flask项目,配置入口文件,启动项目
  • OceanBase 一级表分区记录
  • 浅谈虚拟内存(操作系统、Redis)
  • matlab中在一个图上持续画多条曲线的方法
  • Qualitor processVariavel.php 未授权命令注入漏洞复现(CVE-2023-47253)
  • [Redis][持久化][上][RDB]详细讲解
  • 每天一个数据分析题(四百九十)- 主成分分析与因子分析
  • 7种限流算法打开新方式
  • Win32打开UWP应用
  • 数据库服务器该如何进行搭建?
  • Kali Linux上安装远程桌面服务VNC
  • 线性代数复习笔记
  • 指针(3)
  • 第18周 第1章Ajax基础知识