LV06-02-网络基础-07-TCP协议

本文主要是网络基础——TCP协议的相关笔记,若笔记中有错误或者不合适的地方,欢迎批评指正😃。

点击查看使用工具及版本
Windows windows11
Ubuntu Ubuntu16.04的64位版本
VMware® Workstation 16 Pro 16.2.3 build-19376536
SecureCRT Version 8.7.2 (x64 build 2214) - 正式版-2020年5月14日
开发板 正点原子 i.MX6ULL Linux阿尔法开发板
uboot NXP官方提供的uboot,NXP提供的版本为uboot-imx-rel_imx_4.1.15_2.1.0_ga(使用的uboot版本为U-Boot 2016.03)
linux内核 linux-4.15(NXP官方提供)
STM32开发板 正点原子战舰V3(STM32F103ZET6)
点击查看本文参考资料
点击查看相关文件下载
--- ---

一、TCP协议简介

1. 什么是TCP

传输控制协议(Transmission Control ProtocolTCP)是一种面向连接的、可靠的、基于字节流传输层通信协议。在 TCP 协议中,通过三次握手建立连接。通信结束后,还需要断开连接。如果在发送数据包时,没有正确被发送到目的地时,将会重新发送数据包。

为什么需要TCP协议呢?IP 层(也就是网络层)是不可靠的,它不保证网络包的交付、不保证网络包的按序交付、也不保证网络包中的数据的完整性。如果需要保障网络数据包的可靠性,那么就需要由上层(传输层)的 TCP 协议来负责。因为 TCP 是一个工作在传输层可靠数据传输的服务,它能确保接收端接收的网络包是无损坏、无间隔、非冗余和按序的。

TCP 协议的数据包进行传输采用的是服务器端和客户端模式。发送TCP数据请求方为客户端,另一方则为服务器端。客户端要与服务器端进行通信,服务器端必须开启监听的端口,客户端才能通过端口连接到服务器,然后进行通信。

2. 如何确定TCP连接

用于保证可靠性和流量控制维护的某些状态信息,这些信息的组合,包括Socket、序列号和窗口大小称为连接。其中socket,由 IP 地址和端口号组成;序列号,用来解决乱序问题等;窗口大小,用来做流量控制。

那我们如何确定一个TCP连接呢?这就需要源地址源端口目的地址目的端口四个参数,这便可以唯一确定一个TCP连接。源地址和目的地址的字段(32位)是在 IP 头部中,作用是通过 IP 协议发送报文给对方主机。源端口和目的端口的字段(16位)是在 TCP 头部中,作用是告诉 TCP 协议应该把报文发给哪个进程。

3. TCP最大连接数?

服务器通常固定在某个本地端口上监听,等待客户端的连接请求。因此,客户端 IP 和 端口是可变的,其理论值计算公式如下

1
最大TCP连接数 = 客户端IP数 X 客户端的端口数

IPv4,客户端的 IP 数最多为 232 次方,客户端的端口数最多为 216 次方,也就是服务端单机最大 TCP 连接数,约为 248 次方。但是吧这只是理论上,实际服务端最大并发 TCP 连接数远不能达到理论上限,会受以下因素影响:

  • 文件描述符限制

    每个 TCP 连接都是一个文件,如果文件描述符被占满了,会发生 too many open filesLinux 对可打开的文件描述符的数量分别作了三个方面的限制:

    系统级当前系统可打开的最大数量,通过 cat /proc/sys/fs/file-max 查看
    用户级指定用户可打开的最大数量,通过 cat /etc/security/limits.conf 查看
    进程级单个进程可打开的最大数量,通过 cat /proc/sys/fs/nr_open 查看
  • 内存限制

每个 TCP 连接都要占用一定内存,操作系统的内存是有限的,如果内存资源被占满后,会发生 OOM。啥是OOM?其实前边内存管理的笔记有提到过, Linux内核有个机制叫OOM killerOut-Of-Memory killer),该机制会监控那些占用内存过大,尤其是瞬间很快消耗大量内存的进程,为了防止内存耗尽内核会把该进程杀掉。

4. TCP协议特点

TCP 协议使用的是面向连接的方法进行通信的,特点如下:

  • (1)TCP 是一个面向连接的协议

无论哪一方向另一方发送数据之前,都必须先在双方之间建立一个 TCP连接,否则将无法发送数据,通过三次握手建立连接。

  • (2)面向流的处理

TCP 以流的方式处理数据。也就是说,TCP 可以一个字节一个字节地接收数据,而不是一次接收一个预定格式的数据块。TCP 把接收到的数据组成长度不等的段,将数据按字节大小进行编号,再传递到网络层。接收端通过 ACK 来确认收到的数据编号,通过这种机制能够保证 TCP 协议的有序性和完整性,因此 TCP 能够提供可靠性传输。

  • (3)确认与重传

当数据从主机 A 发送到主机 B 时,主机 B 会返回给主机 A 一个确认应答; TCP 通过确认应答 ACK 实现可靠的数据传输。当发送端将数据发送出去之后会等待对端的确认应答。如果有确认应答,说明数据已经成功到达对端。反之,数据丢失的可能性比较大。

在一定的时间内如果没有收到确认应答,发送端就可以认为数据已经丢失,并进行重发。由此,即使产生了丢失,仍然可以保证数据能够到达对端,实现可靠传输。

  • (4)提供全双工通信,TCP 允许通信双方的应用进程在任何时候都能发送数据。TCP 连接的两端都设有发送缓存和接受缓存,用

    来临时存放双向通信的数据。

  • (5)是端到端的通信,每一条TCP连接只能有两个端点,每一条 TCP 连接只能是点对点的(一对一)。

  • (6)重新排序

TCP 协议除了确认应答与重传机制外, TCP 协议也会采用校验和的方式来检验数据的有效性,主机在接收数据的时候,会将重复的报文丢弃,将乱序的报文重组,发现某段报文丢失了会请求发送方进行重发,因此在 TCP 往上层协议递交的数据是顺序的、无差错的完整数据。

点击查看 TCP 如何保证数据的顺序化传输

(1)主机每次发送数据时,TCP就给每个数据包分配一个序列号并且在一个特定的时间内等待接收主机对分配的这个序列号进行确认;

(2)如果发送主机在一个特定时间内没有收到接收主机的确认,则发送主机会重传此数据包;

(3)接收主机利用序列号对接收的数据进行确认,以便检测对方发送的数据是否有丢失或者乱序等;

(4)接收主机一旦收到已经顺序化的数据,它就将这些数据按正确的顺序重组成数据流并传递到高层进行处理。

  • (7)拥塞控制

如果网络上的负载(发送到网络上的分组数)大于网络上的容量(网络同时能处理的分组数),就可能引起拥塞,判断网络拥塞的两个因素:延时和吞吐量。拥塞控制机制是:开环(预防)和闭环(消除)。

流量控制是通过接收方来控制流量的一种方式;而拥塞控制则是通过发送方来控制流量的一种方式。TCP 发送方可能因为 IP 网络的拥塞而被遏制, TCP 拥塞控制就是为了解决这个问题(注意和 TCP 流量控制的区别)。

TCP 拥塞控制的几种方法:慢启动,拥塞避免,快重传和快恢复。

  • (8)流量控制(滑动窗口协议)

TCP 流量控制主要是针对接收端的处理速度不如发送端发送速度快的问题,消除发送方使接收方缓存溢出的可能性。 TCP 流量控制主要使用滑动窗口协议,滑动窗口是接受数据端使用的窗口大小,用来告诉发送端接收端的缓存大小,以此可以控制发送端发送数据的大小,从而达到流量控制的目的。这个窗口大小就是我们一次传输几个数据。对所有数据帧按顺序赋予编号,发送方在发送过程中始终保持着一个发送窗口,只有落在发送窗口内的帧才允许被发送;同时接收方也维持着一个接收窗口,只有落在接收窗口内的帧才允许接收。这样通过调整发送方窗口和接收方窗口的大小可以实现流量控制。

5. 可靠性传输原理

5.1 什么是可靠性传输

在目前的网络栈协议族中,在需要提供可靠性数据传输的应用中,TCP 协议是首选的,有时也是唯一的选择。TCP协议工作在传输层,它主要完成的工作有:

(1)提供多路复用。

(2)实现数据基本传输功能。

(3)建立通信通道。

(4)提供流量控制。

(5)提供数据可靠性传输保证。

其中数据可靠性传输保证是其中最为重要的方面,也是TCP协议区别于其它协议的最重要特性。所谓提供数据可靠性传输不仅仅指将数据成功的由本地主机传送到远端主机,数据可靠性传输包括如下内容:

(1)能够处理数据传输过程中被破坏问题。

(2)能够处理重复数据接收问题。

(3)能够发现数据丢失以及对此进行有效解决。

(4)能够处理接收端数据乱序到达问题。

5.2 可靠性传输的基本原理

TCP可靠性传输实现的基本原理是什么呢?TCP 协议必须提供对所有这些问题的解决方案方可保证其所声称的数据可靠性传输。

TCP协议规范和当前绝大多数TCP 协议实现代码均采用数据重传和数据确认应答机制来完成TCP 协议的可靠性数据传输。数据超时重传和数据应答机制的基本前提是对每个传输的字节进行编号,即我们通常所说的序列号。数据超时重传是发送端在某个数据包发送出去,在一段固定时间后如果没有收到对该数据包的确认应答,则(假定该数据包在传输过程中丢失)重新发送该数据包。而数据确认应答是指接收端在成功接收到一个有效数据包后,发送一个确认应答数据包给发送端主机,该确认应答数据包中所包含的应答序列号即指已接收到的数据中最后一个字节的序列号加1,加1 的目的在于指出此时接收端期望接收的下一个数据包中第一个字节的序列号。数据超时重传数据确认应答以及对每个传输的字节分配序列号TCP 协议提供可靠性数据传输的核心本质

重传应答机制与序列号的结合,可以处理数据可靠性传输的四个内容。

点击查看 TCP 如何处理可靠性传输的四个问题的
  • 处理数据在传输过程中被破坏的问题

首先通过对所接收数据包的校验,确认该数据包中数据是否存在错误。如果有,则简单丢弃或者发送一个应答数据包重新对这些数据进行请求。发送端在等待一段时间后,则会重新发送这些数据。所以,数据传输错误的解决可以通过数据重传机制完成

  • 处理接收重复数据问题

首先利用序列号可以发现数据重复问题。因为每个传输的数据均被赋予一个唯一的序列号,如果到达的两份数据具有重叠的序列号(如由发送端数据包重传造成),则表示出现数据重复问题,此时只须丢弃其中一份保留另一份即可。多个数据包中数据重叠的情况解决方式类似。所以数据重复问题的解决可以通过检查序列号完成

  • 发现数据丢失以及进行有效解决

首先此处数据包丢失的概念是应该是指在一段合理时间内,理应到达的数据包没有到达,而非永远不到达。数据包丢失与数据包乱序到达有时在判断上和软件处理上很难区分,我们无法确定一个数据包在传输过程中就丢失,还是被延迟在网络中。而将二两者区分开来的一个主要依据是在合理的时间内

如若接收端只接收到序列号从1100 的数据包,之后又接收到序列号从200300 的数据包,超出一段合理的时间后,序列号从101199 的数据一直未到达,则表示包含序列号从101199 的数据包在传输过程中很可能丢失(或者有极不正常的延迟)。

影响数据包是否丢失判断的另外一个干扰因素是发送端的重传机制,如果一个序列号较前的数据包在网络中丢失,造成序列号较后的数据包提前到达接收端,也会暂时造成序列号不连续,但由于发送端在没有接收到确认应答时,会重新发送序列号较前的那个数据包,如果此后接收端接收到一个重传的数据包,则仅仅只会在接收端造成数据包乱序到达的表象。

其实无论实质如何,如果软件实现判断出数据包丢失,则接收端就会不断发送对这些丢失的数据的请求数据包(也即应答数据包)来迫使发送端重新发送这些数据。通常发送端自身会自发的重传这些未得到对方确认的数据,但由于重传机制采用指数退避算法,每次重传的间隔时间均会加倍,所以通过发送方主动重传机制恢复的时间较长,而接收端通过不断发送对这些丢失数据的请求,发送端在接收到三个这样的请求数据包后(三个请求数据包中具有同一个请求序列号也即应答序列号),会立刻触发对这些数据的重新发送,这称为快速恢复或者快速重传机制。所以对于数据丢失问题的解决是可以通过数据重传机制完成的

  • 处理接收端数据乱序到达问题

如果通信双方存在多条传输路径, 则有可能出现数据乱序问题,即序列号较大的数据先于序列号较小的数据到达,而发送端确实是按序列号由小到大的顺序发送的。数据乱序意思是数据都成功到达了,但到达的顺序并未按照我们预想的那样。对这个问题的解决只需对这些数据进行重新排序即可。所以对数据乱序问题的解决可以通过排序数据序列号完成

经过以上分析,我们可以知道序列号数据超时重传数据确认应答机制保证了 TCP 协议可靠性传输的要求。由于需要对所发送的数据进行编号,又需要对接收的数据进行应答,所以使用TCP 协议的通信双方必须通过某种机制了解对方的初始序列号。只有在确切知道对方的初始序列号的情况下,才能从一开始对所接收数据的合法性进行判断。另外还需要在本地维护一个对方应答的序列号,以随时跟随对方的数据请求。在最后通信通道关闭时,可以确知本地发送的数是否已被对方完全接收;此外这个对方应答序列号在控制本地数据通量方面也发挥着重要的作用:用本地发送序列号减去对方应答序列号则可以立刻得知目前发送出去的数据有多少没有得到对方的应答

【注意】实际上,TCP的可靠性传输是非常复杂的,用了很多的机制来保证数据的可靠:重传机制、滑动窗口、流量控制以及阻塞控制,这里就不一一说明了,想知道的话,可以看笔记开篇的参考资料,里边的这一节介绍的非常详细:4.2 TCP 重传、滑动窗口、流量控制、拥塞控制 | 小林coding (xiaolincoding.com)

6. 应用场景

(1)适合于对传输质量要求较高,以及传输大量数据的通信。

(2)在需要可靠数据传输的场合,通常使用TCP协议。

(3)MSN/QQ等即时通讯软件的用户登录账户管理相关的功能通常采用TCP协议。

二、TCP报文格式

1. 报文图示

image-20220623113754219

【说明】

(1)序列号用来解决网络包乱序问题。

(2)确认应答号用来解决丢包的问题。

2. 各字段说明

2.1 源端口和目的端口字段

TCP源端口Source Port源计算机上的应用程序的端口号,占 16 位。
TCP目的端口Destination Port目标计算机的应用程序端口号,占 16 位。

2.2 序列号字段

TCP序列号(Sequence Number),占 32 位,它表示本报文段所发送数据的第一个字节的编号。在 TCP 连接中,所传送的字节流的每一个字节都会按顺序编号。

SYN标记不为1时,这是当前数据分段第一个字母的序列号;如果SYN的值是1时,这个字段的值就是初始序列值(ISN),用于对序列号进行同步。这时,第一个字节的序列号比这个字段的值大1,也就是ISN1

2.3 确认应答字段

TCP 确认应答号(Acknowledgment NumberACK Number),占 32 位。它表示接收方期望收到发送方下一个报文段的第一个字节数据的编号。其值是接收计算机即将接收到的下一个序列号,也就是下一个接收到的字节的序列号加1。当ACK标志位为1时有效,表示期望收到的下一个报文段的第一个数据字节的序号。确认号为N,则表明到序号N-1为止的所有数据字节都已经被正确地接收到了。

2.4 数据偏移字段

TCP 首部长度(Header Length):数据偏移是指数据段中的数据部分起始处距离 TCP 数据段起始处的字节偏移量,占 4位。其实这里的数据偏移也是在确定 TCP 数据段头部分的长度,告诉接收端的应用程序,数据从何处开始。

2.5 保留字段

保留(Reserved),占 4 位。为 TCP 将来的发展预留空间,目前必须置为 0

2.6 标志位字段

CWRCongestion Window Reduce,拥塞窗口减少标志,用来表明它接收到了设置 ECE 标志的 TCP 包。并且,发送方收到消息之后,通过减小发送窗口的大小来降低发送速率。
ECEECN Echo,用来在 TCP 三次握手时表明一个 TCP 端是具备 ECN 功能的。在数据传输过程中,它也用来表明接收到的 TCP 包的 IP 头部的 ECN 被设置为 11,即网络线路拥堵。
URGUrgent,表示本报文段中发送的数据是否包含紧急数据。URG=1 时表示有紧急数据。当 URG=1 时,后面的紧急指针字段才有效。
ACK表示前面的确认号字段是否有效。ACK=1 时表示有效。只有当 ACK=1 时,前面的确认号字段才有效。TCP 规定,连接建立后,ACK 必须为 1。
PSHPush,告诉对方收到该报文段后是否立即把数据推送给上层。如果值为 1,表示应当立即把数据提交给上层,而不是缓存起来。
RST表示是否重置连接。如果 RST=1,说明 TCP 连接出现了严重错误(如主机崩溃),必须释放连接,然后再重新建立连接。
SYN在建立连接时使用,用来同步序号。当 SYN=1,ACK=0 时,表示这是一个请求建立连接的报文段;当 SYN=1,ACK=1 时,表示对方同意建立连接。SYN=1 时,说明这是一个请求建立连接或同意建立连接的报文。只有在前两次握手中 SYN 才为 1。
FIN标记数据是否发送完毕。如果 FIN=1,表示数据已经发送完成,可以释放连接。

2.7 窗口大小字段

窗口大小(Window Size),占 16 位。它表示从 Ack Number 开始还可以接收多少字节的数据量,也表示当前接收端的接收窗口还有多少剩余空间。该字段可以用于 TCP 的流量控制。

2.8 TCP校验和字段

校验位(TCP Checksum),占 16 位。它用于确认传输的数据是否有损坏。发送端基于数据内容校验生成一个数值,接收端根据接收的数据校验生成一个值。两个值必须相同,才能证明数据是有效的。如果两个值不同,则丢掉这个数据包。Checksum 是根据伪头 + TCP 头 + TCP 数据三部分进行计算的。

2.9 紧急指针字段

紧急指针(Urgent Pointer),仅当前面的 URG 控制位为 1 时才有意义。它指出本数据段中为紧急数据的字节数,占 16 位。当所有紧急数据处理完后,TCP 就会告诉应用程序恢复到正常操作。即使当前窗口大小为0,也是可以发送紧急数据的,因为紧急数据无须缓存。

三、TCP建立连接

1. 三次握手过程

TCP 是面向连接的协议,所以每次发出的请求都需要对方进行确认。TCP 客户端与 TCP 服务器在通信之前需要完成三次握手才能建立连接。

image-20220623120707723

一开始,客户端和服务端都处于 CLOSED 状态。首先客户端主动打开连接,然后服务端被动监听某个端口,处于 LISTEN 状态。接下来就可以开始客户端与服务端的连接过程了。由于前边已经安装过抓包工具wireshark了,并且此篇笔记是网络编程学习完回顾时所写,这里使用后边编写的TCP本地服务端和客户端连接实例来实际抓一下包,看一下这三次连接过程。

点击查看操作过程

ubuntu中,使用以下命令打开wireshark

1
sudo wireshark

然后进入wireshark软件,接口选择loopback:lo,额,这里吧,我暂时还不是很懂,只知道选择这个才可以捕获到本地自己编写的TCP服务端和客户端连接过程,后边再补充吧。

image-20220623141216063

然后我们另开两个终端吗,分别运行TCP服务器端和客户端,当客户端连接上服务器的时候,会有以下数据被抓取(还可能有其他的包,只是这里使用了显示过滤器,只显示TCP包):

image-20220623142905506

然后我们在客户端输入退出标记,然后断开TCP连接,会再得到几个TCP的数据包:

image-20220623142923675

可能是电脑分辨率的问题,在Ubuntu中字体实在太小,我们保存文件到Windows下,再打开分析也是一样的。

  • 第一次握手TCP客户端向服务器端发出建立连接请求的报文段

客户端会随机初始化一个序列号seq(值为X),并将此序列号置于 TCP 首部的序列号字段中,同时把同步位 SYN 标志位置为 1 ,表示 这是一个SYN 报文。接着把第一个 SYN 报文发送给服务端,表示向服务端发起连接请求,该报文不包含应用层数据,客户端发送完请求报文之后,客户端会处于 SYN-SENT (同步已发送)状态。

TCP规定,当报文段的SYN=1ACK=0(可以查看上边ACK标志取值的情况,不过这里是建立连接请求,不一定必须是1)时,表明这是一个请求建立连接的SYN报文段(SYN=1的报文段)不能携带数据,但是要消耗掉一个序列号。

我们实际抓取到的TCP的包如下图所示:

image-20220623143444725
  • 第二次握手:服务器向客户端发回确认报文段

服务端收到客户端的 SYN 报文后,首先服务端也随机初始化自己的序列号seq(值为y),并将此序列号填入 TCP 首部的序列号字段中。其次把确认号ack(值为x+1)填入 TCP 首部的确认应答号字段, 接着把 SYNACK 标志位置为 1,最后把该报文发给客户端。该报文也不包含应用层数据,服务器端发送完确认报文之后,服务端会处于 SYN-RCVD (同步收到)状态。

TCP规定,当报文段的SYN=1ACK=1时,表明这是一个同一建立连接响应报文段;这个报文段也不能携带数据,同样需要消耗掉一个序列号。

我们实际抓取到的TCP的包如下图所示:

image-20220623143622193
  • 第三次握手TCP客户端收到确认后,还要再向服务器发出确认报文段

客户端收到服务端报文后,还要向服务端回应最后一个应答报文,首先将该应答报文 TCP 首部 ACK 标志位置为 1 ,并将确认号ack(值为y+1)填入确认应答号字段 ,然后把报文发送给服务端,这次报文可以携带客户到服务器的数据,之后客户端处于 ESTABLISHED(已建立连接) 状态。

TCP规定,这个ACK报文段可以携带数据;如果不携带数据则不消耗序号,客户端的下一个数据报文段的序号仍然是seq=x+1

我们实际抓取到的TCP的包如下图所示:

image-20220623143730304

经过以上过程,客户端就和服务端建立连接了,建立连接后,两者都处于 ESTABLISHED (已建立连接)状态。同时双方都得到了彼此的窗口大小,序列号等信息,在传输 TCP 报文段的时候,每个 TCP 报文段首部的 SYN 标志都会被置 0,因为它只用于发起连接,同步序号。

其实简单来说就是这么一个过程,小明和小红打电话:

小明:“小红小红,我是小明,你能听见我说话吗?”——小明要确认小红是否可以听到自己说话

小红:“小明小明,我是小红,我能听见你说话,你能听见我说话吗?”——小红确认自己可以听到小明说话,同时需要确认小明是否可以听见自己说话。

小明:“小红小红,我能听见说话哦。”——小明告诉小红自己可以听到小红说话。

然后。。。。。然后就开始愉快的聊天啦。

在Linux中查看TCP的状态:

我们可以在终端中执行以下命令:

1
netstat -napt

【注意】

(1)经过上边的分析过程我们知道,seq应该是一个随机值才对,为什么抓包的seq是从0开始的?这是因为软件帮我们做了优化处理,要想显示真正的序列号,我们可以在数据包详情面板中这样操作:【选中Transmission Control Protocol】→【右键】→【协议首选项】→【Relative sequence numbers】,然后去掉前边的勾就可以啦。

(2)上面写的 ack 和 ACK,不是同一个概念:小写的 ack 代表的是头部的确认号 Acknowledge number, ack。大写的 ACK,则是 TCP 首部的标志位,用于标志的 TCP 包是否对上一个包进行了确认操作,如果确认了,则把 ACK 标志位设置成 1。

2. 为什么是三次握手?

为啥需要三次握手,两次不行吗?

  • 第一次握手:客户端发送网络包,服务端收到了。这样服务端就能得出结论:客户端的发送能力、服务端的接收能力是正常的。
  • 第二次握手:服务端发包,客户端收到了。这样客户端就能得出结论:服务端的接收、发送能力,客户端的接收、发送能力是正常的。不过此时服务器并不能确认客户端的接收能力是否正常。
  • 第三次握手:客户端发包,服务端收到了。这样服务端就能得出结论:客户端的接收、发送能力正常,服务器自己的发送、接收能力也正常。

假如客户端发出了连接请求,但因为网络波动导致服务器并没有收到来自客户端的请求连接,于是客户端又重发了一次连接请求,客户端和服务器经过两次握手就建立好连接。双方开始传输数据,数据传输完成以后,双方断开连接。过一段时间后,原本在网络传输中搁置的连接请求到达了服务器。服务器以为是客户端又发出来一次新的连接请求,于是就向客户端发送确认报文段,同意建立连接(两次握手只需要服务器发出确认报文段,就建立好连接)。此时服务器一直在等待客户端发送的数据,一直浪费着系统资源。因此,需要三次握手才能确认双方的接收与发送能力是否正常

那四次握手可以嘛?当然就可以啦,但是有些多余,还会浪费资源。

TCP三次握手原本应该是四次握手,但是中间的同步报文段SYN和应答报文ACK是可以合在一起的,这两个操作在时间上是同时发送的。当客户端的到达同步报文段SYN到达服务器的时候,服务器的内核就会第一时间进行应答报文段ACK, 同时也会第一时间发起同步报文段SYN,这两件事情同时触发,于是就没必要分成两次传输,直接一步到位。分成两次反而会更浪费系统资源(需要进行两次的封装和分用)。

【总结】三次握手的原因

  • 三次握手才可以阻止重复历史连接的初始化(主要原因);
  • 三次握手才可以同步双方的初始序列号;
  • 三次握手才可以避免资源浪费;

3. 两个重要的序列号

3.1 初始序列号

ISN(Initial Sequence Number),即初始序列号,在上边的图中我使用seq表示了。序列号是按顺序给发送数据的每一个字节都标上号码的编号。当一端为建立连接而发送它的SYN时,它会为连接选择一个初始的序列号ISN(客户端和服务器会分别选择一个初始序列号ISN)。初始序列号ISN并非为0,而是由随机数生成,所以每次初始的序列号可能都不一样,并不固定,但是一旦初始序列号生成,那么后面的计算则是对每一字节加一。

三次握手的其中一个重要功能是客户端和服务端交换初始序列号ISN,以便让对方知道接下来接收数据的时候如何按序列号组装数据。如果初始序列号 ISN 是固定的,攻击者很容易猜出后续的确认号,从而打断正常的TCP连接,因此 ISN 是动态生成的。

那这个ISN是如何生成的呢?起始 ISN 是基于时钟的,每 4 微秒 +1,转一圈要 4.55 个小时。RFC793 提到初始化序列号 ISN 随机生成算法:

1
ISN = M + F(localhost, localport, remotehost, remoteport)
  • M 是一个计时器,这个计时器每隔 4 微秒加 1
  • F 是一个 Hash 算法,根据源 IP、目的 IP、源端口、目的端口生成一个随机数值。要保证 Hash 算法不能被外部轻易推算得出,用 MD5 算法是一个比较好的选择。

可以看到,随机数是会基于时钟计时器递增的,基本不可能会随机成一样的初始化序列号。

3.2 应答序列号

在服务端进行回应的时候发送的数据确认应答数据包会有一个应答序列号,它有什么意义呢?

应答序列号并非其表面上所显示的意义,其实际上是指接收端希望接收的下一个字节的序列号。所以接收端在成功接收到部分数据后,其发送的应答数据包中应答序列号被设置为这些数据中最后一个字节的序列号加一。所以从其含义上来说,应答序列号称为请求序列号有时更为合适。应答序列号在TCP 首部中应答序列号字段中被设置。而TCP 首部中序列号字段表示包含该TCP 首部的数据包中所包含数据的第一个字节的序列号(令为N)。如果接收端成功接收该数据包,之前又无丢失数据包,则接收端发送的应答数据包中的应答序列号应该为:N+LEN。其中LEN 为接收的数据包的数据长度。该应答序列号也是发送端将要发送的下一个数据包中第一个字节的序列号(由此亦可看出可以将应答序列号称为请求序列号的原因所在)。

4. 握手可以携带数据吗?

第一次、第二次不可以,第三次可以。

如果第一次握手可以携带数据的话,那么将会使服务器更容易遭受攻击。如果第一次握手携带就大量的数据,那么服务器需要花很长的时间才能对数据进行解析。如果进行重复的发送,那么服务器就会因为系统资源殆尽而崩溃。

第三次握手可以携带数据。第三次握手时客户端处于ESTABLISH状态。对于客户端来说,它已经建立好了连接,并且它已经知道服务器的接收能力和自己的发送能力是正常的,所以可以进行正常的发送数据。

5. TCP握手丢失?

5.1 第一次握手丢失?

当客户端想和服务端建立 TCP 连接的时候,首先第一个发的就是 SYN 报文,然后进入到 SYN_SENT 状态。在这之后,如果客户端迟迟收不到服务端的 SYN-ACK 报文(第二次握手),就会触发超时重传机制,重传 SYN 报文。

不同版本的操作系统可能超时时间不同,有的 1 秒的,也有 3 秒的,这个超时时间是写死在内核里的,如果想要更改则需要重新编译内核,比较麻烦。

当客户端在 1 秒后没收到服务端的 SYN-ACK 报文后,客户端就会重发 SYN 报文,那到底重发几次呢?在 Linux 里,客户端的 SYN 报文最大重传次数由 tcp_syn_retries内核参数控制,这个参数是可以自定义的,默认值一般是 5

通常,第一次超时重传是在 1 秒后,第二次超时重传是在 2 秒,第三次超时重传是在 4 秒后,第四次超时重传是在 8 秒后,第五次是在超时重传 16 秒后。每次超时的时间是上一次的 2 倍。当第五次超时重传后,会继续等待 32 秒,如果服务端仍然没有回应 ACK,客户端就不再发送 SYN 包,然后断开 TCP 连接。所以,总耗时是 1+2+4+8+16+32=63 秒,大约1分钟左右。

5.2 第二次握手丢失?

当服务端收到客户端的第一次握手后,就会回 SYN-ACK 报文给客户端,这个就是第二次握手,此时服务端会进入 SYN_RCVD 状态。

第二次握手的 SYN-ACK 报文其实有两个目的 :

  • 第二次握手里的 ACK, 是对第一次握手的确认报文;
  • 第二次握手里的 SYN,是服务端发起建立 TCP 连接的报文;

所以,如果第二次握手丢了,具体会怎么样呢?

一方面,因为第二次握手报文里是包含对客户端的第一次握手的 ACK 确认报文,所以,如果客户端迟迟没有收到第二次握手,那么客户端就觉得可能自己的 SYN 报文(第一次握手)丢失了,于是客户端就会触发超时重传机制,重传 SYN 报文

另一方面,因为第二次握手中包含服务端的 SYN 报文,所以当客户端收到后,需要给服务端发送 ACK 确认报文(第三次握手),服务端才会认为该 SYN 报文被客户端收到了。如果第二次握手丢失了,服务端就收不到第三次握手,于是服务端这边会触发超时重传机制,重传 SYN-ACK 报文

Linux 下,SYN-ACK 报文的最大重传次数由 tcp_synack_retries内核参数决定,默认值是 5。因此,当第二次握手丢失了,客户端和服务端都会重传:

  • 客户端会重传 SYN 报文,也就是第一次握手,最大重传次数由 tcp_syn_retries内核参数决定;
  • 服务端会重传 SYN-ACK 报文,也就是第二次握手,最大重传次数由 tcp_synack_retries 内核参数决定。

5.3 第三次握手丢失?

客户端收到服务端的 SYN-ACK 报文后,就会给服务端回一个 ACK 报文,也就是第三次握手,此时客户端状态进入到 ESTABLISH 状态。因为这个第三次握手的 ACK 是对第二次握手的 SYN 的确认报文,所以当第三次握手丢失了,如果服务端那一方迟迟收不到这个确认报文,就会触发超时重传机制,重传 SYN-ACK 报文,直到收到第三次握手,或者达到最大重传次数。

【注意】ACK 报文是不会有重传的,当 ACK 丢失了,就由对方重传对应的报文。

四、TCP连接断开

1. 四次挥手过程

TCP 断开连接是通过四次挥手方式,需要客户端和服务端总共发送 4 个包以确认连接的断开。在 socket 编程中,这一过程由客户端或服务端任一方执行 close 来触发。

由于 TCP 连接是全双工的,因此,每个方向都必须要单独进行关闭,这一原则是当一方完成数据发送任务后,发送一个 FIN 来终止这一方向的连接,收到一个 FIN 只是意味着这一方向上没有数据流动了,即不会再收到数据了,但是在这个 TCP 连接上仍然能够发送数据,直到这一方向也发送了 FIN。首先进行关闭的一方将执行主动关闭,而另一方则执行被动关闭。

双方都可以主动断开连接,断开连接后主机中的资源将被释放,四次挥手的过程如下图:

image-20220623150318856

下边分析一下四次握手的过程,这里就不再使用TCP数据包实际分析了,图片太多的话,云存储的流量顶不住啊,不过有了上边三次握手建立连接的分析,四次握手断开连接也是很容易理解的。

  • 第一次挥手TCP客户进程向服务器端发出释放连接请求报文段

如上图所示,释放连接的报文段中FIN(终止位)需要置1,并初始化一个序列号seq,设seq=u,并将此序列号填入 TCP 首部的序列号字段中。客户端发送完释放连接的请求报文后,客户端进程进入FIN-WAIT-1(终止等待1)状态。

TCP规定,当报文段的FIN=1时,表明此报文段的发送方的数据已发生完毕,并要求释放连接;FIN报文段(FIN=1的报文段)不能携带数据,但是要消耗掉一个序列号。

  • 第二次挥手TCP服务器端向客户端发送确认报文

确认报文中,TCP服务器端也会初始化一个序列号seq,设seq=y,并将此序列号填入 TCP 首部的序列号字段中。其次把确认号ack(值为x+1)填入 TCP 首部的确认应答号字段。接着把 ACK 标志位置为 1,最后把该报文发给客户端,发送完毕之后,服务端进程进入CLOSE-WAIT(关闭等待)状态。

TCP服务器进程这时应该通知高层应用经常,客户端到服务器端这个方向的连接就要释放了,这时的TCP连接处于半关闭(half-close)状态。

客户端在收到服务器端的确认报文段之后,就进入FIN-WAIT-2(终止等待2)状态,等待服务器发出的连接释放报文段。

  • 第三次挥手:服务器端向客户端发送连接释放报文段

若服务器端已经没有要向客户端发送的数据了,那么服务器端的应用进程就通知TCP释放连接,此时会再初始化一个seq序列号,设seq=z,并将此序列号填入 TCP 首部的序列号字段中。其次把确认号ack(值为x+1)填入 TCP 首部的确认应答号字段。,接着把 ACK 标志位和FIN标志位置为 1,最后把该连接释放报文发给客户端,发送完毕之后,服务端进程进入LAST-ACK(最后确认)状态。

TCP规定,当报文段的FIN=1时,表明此报文段的发送方的数据已发生完毕,并要求释放连接;FIN报文段(FIN=1的报文段)不能携带数据,但是要消耗掉一个序列号。

  • 第四次挥手:客户端向服务器端发出确认报文段

客户端收到服务器端发送的连接释放报文段后,必须对此发出一个确认报文段。此时将seq=x+1填入 TCP 首部的序列号字段中,将ack=z+1填入 TCP 首部的确认应答号字段。接着把 ACK 标志位置为 1,最后把该报文发给服务器端,客户端发送完毕确认报文段后,进入到TIME-WAIT(时间等待)状态。服务器收到了 ACK 应答报文后,就进入了 CLOSED 状态,至此服务端已经完成连接的关闭。

随后,客户端在经过 2MSL 一段时间后,自动进入 CLOSED 状态,至此客户端也完成连接的关闭。

有上边的分析过程可知,每个方向都需要一个 FIN 和一个 ACK,因此通常被称为四次挥手。

2. 为什么是四次挥手?

原因主要有以下两点:

  • 关闭连接时,客户端向服务端发送 FIN 时,仅仅表示客户端不再发送数据了但是还能接收数据。
  • 服务器收到客户端的 FIN 报文时,先回一个 ACK 应答报文,而服务端可能还有数据需要处理和发送,等服务端不再发送数据时,才发送 FIN 报文给客户端来表示同意现在关闭连接。

由于服务端通常需要等待完成数据的发送和处理,所以服务端的 ACKFIN 一般都会分开发送,从而比三次握手导致多了一次。

3. 挥手丢失?

3.1 第一次挥手丢失?

当客户端(主动关闭方)调用 close 函数后,就会向服务端发送FIN报文,也就是请求释放连接的报文,试图与服务端断开连接,此时客户端的连接进入到 FIN_WAIT_1 状态。

正常情况下,如果能及时收到服务端(被动关闭方)的 ACK,则会很快变为 FIN_WAIT2状态。如果第一次挥手丢失了,那么客户端迟迟收不到被动方的 ACK 的话,也就会触发超时重传机制,重传 FIN 报文,重发次数由 tcp_orphan_retries 参数控制。当客户端重传 FIN 报文的次数超过 tcp_orphan_retries 后,就不再发送 FIN 报文,直接进入到 close 状态。

3.2 第二次挥手丢失?

当服务端收到客户端的第一次挥手后,就会先回一个 ACK 确认报文,此时服务端的连接进入到 CLOSE_WAIT 状态。

ACK 报文是不会重传的(这一句其实我是有疑问的,在网上看了两篇文章,都有这句话,但是我不是特别理解,应该是与TCP的重传机制有关,后边懂了再补充吧),所以如果服务端的第二次挥手丢失了,客户端就会触发超时重传机制,重传 FIN 报文,直到收到服务端的第二次挥手,或者达到最大的重传次数。

需要注意的是,当客户端收到第二次挥手,也就是收到服务端发送的 ACK 报文后,客户端就会处于 FIN_WAIT2 状态,在这个状态需要等服务端发送第三次挥手,也就是服务端的 FIN 报文。

对于 close 函数关闭的连接,由于无法再发送和接收数据,所以FIN_WAIT2 状态不可以持续太久,而 tcp_fin_timeout 控制了这个状态下连接的持续时长,默认值是 60 秒。这意味着对于调用 close 关闭的连接,如果在 60 秒后还没有收到 FIN 报文,客户端(主动关闭方)的连接就会直接关闭。

这里要注意,如果主动关闭方使用 shutdown 函数关闭连接且指定只关闭发送方向,而接收方向并没有关闭,那么意味着主动关闭方还是可以接收数据的。如果主动关闭方一直没收到第三次挥手,那么主动关闭方的连接将会一直处于 FIN_WAIT2 状态(tcp_fin_timeout 是无法控制 shutdown 关闭的连接的)。

3.3 第三次挥手丢失?

当服务端(被动关闭方)收到客户端(主动关闭方)的 FIN 报文后,内核会自动回复 ACK,同时连接处于 CLOSE_WAIT 状态,它表示等待应用进程调用 close 函数关闭连接。此时,内核是没有权利替代进程关闭连接,必须由进程主动调用 close 函数来触发服务端发送 FIN 报文。

服务端处于 CLOSE_WAIT 状态时,调用了 close 函数,内核就会发出 FIN 报文,同时连接进入 LAST_ACK 状态,等待客户端返回 ACK 来确认连接关闭。如果迟迟收不到这个 ACK,服务端就会重发 FIN 报文,重发次数仍然由 tcp_orphan_retries 参数控制,这与客户端重发 FIN 报文的重传次数控制方式是一样的。

3.4 第四次挥手丢失?

当客户端收到服务端的第三次挥手的 FIN 报文后,就会回 ACK 报文,也就是第四次挥手,此时客户端连接进入 TIME_WAIT 状态。在 Linux 系统,TIME_WAIT 状态会持续 2MSL 后才会进入关闭状态。

然后,服务端(被动关闭方)没有收到 ACK 报文前,还是处于 LAST_ACK 状态。如果第四次挥手的 ACK 报文没有到达服务端,服务端就会重发 FIN 报文,重发次数仍然由前面介绍过的 tcp_orphan_retries 参数控制。

4. 2MSL具体是什么?

上边有个疑问,就是为什么 TIME_WAIT 等待的时间是 2MSL2MSL到底是多久类?

MSLMaximum Segment Lifetime,就是报文最大生存时间,它是任何报文在网络上存在的最长时间,超过这个时间报文将被丢弃。因为 TCP 报文是基于 IP 协议的,而 IP 头中有一个 TTL 字段,是 IP 数据报可以经过的最大路由数,每经过一个处理他的路由器此值就减 1,当此值为 0 则数据报将被丢弃,同时发送 ICMP 报文通知源主机。

MSLTTL 的区别是 MSL 的单位是时间,而 TTL 是经过路由跳数。所以 MSL 应该要大于等于 TTL 消耗为 0 的时间,以确保报文已被自然消亡。TTL 的值一般是 64LinuxMSL 设置为 30 秒,意味着 Linux 认为数据报文经过 64 个路由器的时间不会超过 30 秒,如果超过了,就认为报文已经消失在网络中了。

其实客户端在发送最后一次确认报文段之后还需要等待2MSL时间,主要有两个原因:

  • (1)为了保证客户端发送的最后一个ACK报文段能够到达服务器端。因为这个ACK报文段有可能丢失,这样服务器端就无法接收到而进入CLOSED状态。于是服务器端会重传请求释放连接的报文段,客户端在这段等待时间接收到了,重传ACK报文段,这样服务器端还是会顺利接收到确认报文段,进入CLOSED状态。如果此时客户端已经关闭了,那么就无法收到服务器端的请求报文段,也不会发送ACK报文段,这样服务器端就无法进入CLOSED状态了。

  • (2)在这2MSL的等待时间内,本次连接的所有报文都已经从网络中消失,从而不会出现失效的报文出现在下次连接中而导致错误。

由此可知,TIME_WAIT 等待 2 倍的 MSL是比较合理的。 网络中可能存在来自发送方的数据包,当这些发送方的数据包被接收方处理后又会向对方发送响应,所以一来一回需要等待 2 倍的时间。比如,如果被动关闭方没有收到断开连接的最后的 ACK 报文,就会触发超时重发 FIN 报文,另一方接收到 FIN 后,会重发 ACK 给被动关闭方, 一来一去正好2MSL。所以2MSL时长, 这其实是相当于至少允许报文丢失一次。比如,若 ACK 在一个 MSL 内丢失,这样被动方重发的 FIN 会在第 2MSL 内到达,客户端还处于TIME_WAIT 状态,并未关闭,这样就可以应对这种情况,有时间重新发送一次ACK

2MSL 的时间是从客户端接收到 FIN 后发送 ACK 开始计时的。如果在 TIME-WAIT 时间内,因为客户端的 ACK 没有传输到服务端,客户端又接收到了服务端重发的 FIN 报文,那么 2MSL 时间将重新计时。

在 Linux 系统里 2MSL 默认是 60 秒,那么一个 MSL 也就是 30 秒。Linux 系统停留在 TIME_WAIT 的时间为固定的 60 秒。其定义在 Linux 内核代码里的名称为 TCP_TIMEWAIT_LEN

1
#define TCP_TIMEWAIT_LEN (60*HZ) /* how long to wait to destroy TIME-WAIT state, about 60 seconds  */

如果要修改 TIME_WAIT 的时间长度,只能修改 Linux 内核代码里 TCP_TIMEWAIT_LEN 的值,并重新编译 Linux 内核。

五、TCP状态说明

1. TCP的状态

TCP 协议在建立连接、断开连接以及数据传输过程中都会呈现出现不同的状态,不同的状态采取的动作也是不同的,需要处理各个状态之间的关系。这些 TCP 状态的说明如下所示:

  • CLOSED 状态: 表示一个初始状态。

  • LISTENING 状态: 这是一个非常容易理解的状态,表示服务器端的某个 SOCKET 处于监听状态,监听客户端的连接请求,可以接受连接了。例如服务器能够提供某种服务,它会监听客户端 TCP端口的连接请求,处于 LISTENING 状态,端口是开放的,等待被客户端连接。

  • **SYN_SENT 状态(客户端状态)**: 当客户端调用 connect()函数连接时,它首先会发送 SYN 报文给服务器请求建立连接,因此也随即它会进入到了 SYN_SENT 状态,并等待服务器的发送三次握手中的第 2 个报文。 SYN_SENT 状态表示客户端已发送 SYN 报文。

  • **SYN_REVD 状态(服务端状态)**: 这个状态表示服务器接受到了 SYN 报文,在正常情况下,这个状态是服务器端的 SOCKET 在建立 TCP 连接时的三次握手过程中的一个中间状态, 很短暂,基本上用 netstat 是很难看到这种状态的,除非特意写了一个客户端测试程序,故意将三次 TCP 握手过程中最后一个 ACK 报文不予发送。因此这种状态时,当收到客户端的 ACK 报文后,它会进入到 ESTABLISHED 状态。

  • ESTABLISHED 状态: 这个容易理解了,表示连接已经建立了。

  • FIN_WAIT_1 和 FIN_WAIT_2 状态: 其实 FIN_WAIT_1 和 FIN_WAIT_2 状态的真正含义都是表示等待对方的 FIN 报文。而这两种状态的区别是: FIN\_WAIT_1 状态实际上是当 SOCKET 在ESTABLISHED 状态时,它想主动关闭连接,向对方发送了 FIN 报文,此时该SOCKET 即进入到FIN_WAIT_1 状态。而当对方回应 ACK 报文后,则进入到 FIN_WAIT_2 状态,当然在实际的正常情况下,无论对方何种情况下,都应该马上回应 ACK 报文,所以 FIN_WAIT_1 状态一般是比较难见到的,而 FIN_WAIT_2 状态还有时常常可以用 netstat 看到。

  • TIME_WAIT 状态: 表示收到了对方的 FIN 报文,并发送出了 ACK 报文,就等 2MSL 后即可回到 CLOSED 可用状态了。如果 FIN_WAIT_1 状态下,收到了对方同时带 FIN 标志和 ACK 标志的报文时,可以直接进入到 TIME_WAIT 状态,而无须经过 FIN_WAIT_2 状态。

  • CLOSE_WAIT 状态: 这种状态的含义其实是表示在等待关闭。怎么理解呢?当对方 close 一个SOCKET 后发送 FIN 报文给自己,我们的系统毫无疑问地会回应一个 ACK 报文给对方,此时则进入到CLOSE_WAIT状态。接下来呢,实际上我们真正需要考虑的事情是察看我们是否还有数据发送给对方,如果没有的话,我们也就可以 close 这个 SOCKET,发送 FIN 报文给对方,也即关闭连接。所以我们在 CLOSE_WAIT 状态下,需要完成的事情是等待我们去关闭连接。

  • LAST_ACK 状态: 它是被动关闭一方在发送 FIN 报文后,最后等待对方的 ACK 报文。当收到 ACK报文后,也即可以进入到 CLOSED 状态了。

2. 结合图文分析一下

tcpip007
  • 虚线:表示服务器的状态转移。
  • 实线:表示客户端的状态转移。
  • 图中所有“关闭”、“打开”都是应用程序主动处理。
  • 图中所有的“超时”都是内核超时处理。

三次握手过程:

  • 图中(7):服务器的应用程序主动使服务器进入监听状态,等待客户端的连接请求。
  • 图中(1):首先客户端的应用程序会主动发起连接,发送SNY报文段给服务器,在发送之后就进入SYN_SENT状态等待服务器的SNY ACK报文段进行确认,如果在指定超时时间内服务器不进行应答确认,那么客户端将关闭连接。
  • 图中(8):处于监听状态的服务器收到客户端的连接请求(SNY报文段),那么服务器就返回一个SNY ACK报文段应答客户端的响应,并且服务器进入SYN_RCVD状态。
  • 图中(1):如果客户端收到了服务器的SNY ACK报文段,那么就进入ESTABLISHED稳定连接状态,并向服务器发送一个ACK报文段。
  • 图中(9):同时,服务器收到来自客户端的ACK报文段,表示连接成功,进入ESTABLISHED稳定连接状态,这正是我们建立连接的三次握手过程。

四次挥手过程:

  • 图中(3):一般来说,都是客户端主动发送一个FIN报文段来终止连接,此时客户端从ESTABLISHED稳定连接状态转移为FIN_WAIT_1状态,并且等待来自服务器的应答确认。
  • 图中(10):服务器收到FIN报文段,知道客户端请求终止连接,那么将返回一个ACK报文段到客户端确认终止连接,并且服务器状态由稳定状态转移为CLOSE_WAIT等待终止连接状态。
  • 图中(4):客户端收到确认报文段后,进入FIN_WAIT_2状态,等待来自服务器的主动请求终止连接,此时 {客户端->服务器} 方向上的连接已经断开。
  • 图中(11):一般来说,当客户端终止了连接之后,服务器也会终止 {客户端->服务器} 方向上的连接,因此服务器的原因程序会主动关闭该方向上的连接,发送一个FIN报文段给客户端。
  • 图中(5):处于FIN_WAIT_2的客户端收到FIN报文段后,发送一个ACK报文段给服务器。
  • 图中(12):服务器收到ACK报文段,就直接关闭,此时 {客户端->服务器} 方向上的连接已经终止,进入CLOSED状态。
  • 图中(6):客户端还会等待2MSL,以防ACK报文段没被服务器收到,这就是四次挥手的全部过程。 注意:对于图中(13)(14)(15)的这些状态都是一些比较特殊的状态,我们暂时就不详细去说了,总的来说都是一样的。

六、总结

主要是几张图:

  • 建立连接的三次握手
image-20241027133727320
  • 断开连接的四次挥手
image-20241027133759867
  • 整个通信过程
image-20241027133840905