Linux网络TCP(中)(12)

文章目录


前言

让我们继续开始TCP的学习,在这里我将用源码的形式给大家讲解,边展示边解释的那种~


一、重看TCP报头

我下载了 Linux2.6 版本的源码,让我们来看看这个 TCP 报头吧!

你会发现跟我们上一篇的 TCP 报头协议格式相对应了

为什么会有六个不同的标志位呢?

  • TCP 报文的种类多种多样,除了正常通信时发送的普通报文,还有建立连接时发送的请求建立连接的报文,以及断开连接时发送的断开连接的报文等等
  • 收到不同种类的报文时完美需要对应执行动作,比如正常通信的报文需要放到接收缓冲区当中等待上层应用进行读取,而建立和断开连接的报文本质不是交给用户处理的,而是需要让操作系统在 TCP 层执行对应的握手和挥手动作
  • 也就是说不同种类的报文对应的是不同的处理逻辑,所以我们要能够区分报文的种类。而 TCP 就是使用报头当中的六个标志字段来进行区分的,这六个标志位都只占用一个比特位,为0表示假,为1表示真

SYN

  • 报文当中的 SYN 被设置为 1 ,表明该报文是一个连接建立的请求报文。
  • 只有在连接建立阶段, SYN 才被设置,正常通信时 SYN 不会被设置。

FIN

  • 报文当中的 FIN 被设置为 1 ,表明该报文是一个连接断开的请求报文。
  • 只有在断开连接阶段, FIN 才被设置,正常通信时 FIN 不会被设置。

URG

双方在进行网络通信的时候,由于 TCP 是保证数据按序到达的,即便发送端将要发送的数据分成了若干个 TCP 报文进行发送,最终到达接收端时这些数据也都是有序的,因为 TCP 可以通过序号来对这些 TCP 报文进行顺序重排,最终就能保证数据到达对端接收缓冲区中时是有序的。

TCP 按序到达本身也是我们的目的,此时对端上层在从接收缓冲区读取数据时也必须是按顺序读取的。但是有时候发送端可能发送了一些"紧急数据",这些数据需要让对方上层提取进行读取,此时应该怎么办呢?

也就是说,这个时候我们希望跳过有序,直接在指定位置读取我们所需要的数据,这个时候就把URG标志位记为1,然后关注报头里面的 16位紧急指针 了

但是其实这个现在越来越少用了,考虑兼容性和本身只能标记一个字节的特点,早就有更好的方案了,于是我们用DS来大致了解一下这个到底是什么东西就可以了

PSH

这是在告诉对方,尽快将你的接收缓冲区当中的数据交付给上层

我们一般认为,当使用 read / recv 从缓冲区当中读取数据时,如果缓冲区当中有数据 read / recv 函数就能够读到数据进行返回,而如果缓冲区当中没有数据,那么此时 read / recv 函数就会阻塞住,直到当缓冲区当中有数据时才会读取到数据进行返回。

但是其实你想一想就会发现这其实是不太对的,毕竟 read / recv 作为系统调用,会有一个从用户态到内核态的转换成本,假如每次有一点点数据就读出写入的话,那效率其实是不高的,是有个水位线在这里的

  • 比如我们假设 TCP 接收缓冲区的水位线是100字节,那么只有当接收缓冲区当中有100字节时才让read/recv函数读取这100字节的数据进行返回
  • 因此不是说接收缓冲区当中只要有数据,调用 read / recv 函数时就能读取到数据进行返回,而是当缓冲区当中的数据量达到一定量时才能进行读取

当报文当中的 PSH 被设置为 1 时,实际就是在告知对方操作系统,尽快将接收缓冲区当中的数据交付给上层,尽管接收缓冲区当中的数据还没到达所指定的水位线。这也就是为什么我们使用 read / recv 函数读取数据时,期望读取的字节数和实际读取的字节数是不一定吻合的(这其实也是TCP字节流特性的一种实际体现

RST

报文当中的 RST 被设置为 1 ,表示需要让对方重新建立连接

在通信双方在连接未建立好的情况下,一方向另一方发数据,此时另一方发送的响应报文当中的 RST 标志位就会被置 1 ,表示要求对方重新建立连接

在双方建立好连接进行正常通信时,如果通信中途发现之前建立好的连接出现了异常也会要求重新建立连接

二、确认应答机制

我认为这应该是TCP可靠性理解最基础也是最重要的一部分了

确认应答机制就是由 TCP 报头当中的, 32位序号 和 32位确认序号 来保证的。需要再次强调的是,确认应答机制不是保证双方通信的全部消息的可靠性,而是通过收到对方的应答消息,来保证自己曾经发送给对方的某一条消息被对方可靠的收到了

TCP是面向字节流的,我们可以将TCP的发送缓冲区和接收缓冲区都想象成一个字符数组(其实应该是环形字符数组,不过问题不大,知道个意思就好)

  • 此时上层应用拷贝到 TCP 发送缓冲区当中的每一个字节数据天然有了一个序号 ,这个序号就是字符数组的下标,只不过这个下标不是从 0 开始的,而是从 1 开始往后递增的(简化理解,其实初始序列号是随机值
  • 而双方在通信时,本质就是将自己发送缓冲区当中的数据拷贝到对方的接收缓冲区当中
  • 发送方发送数据时报头当中所填的序号,实际就是发送的若干字节数据当中,首个字节数据在发送缓冲区当中对应的下标
  • 接收方接收到数据进行响应时,响应报头当中的确认序号实际就是,接收缓冲区中接收到的最后一个有效数据的下一个位置所对应的下标
  • 当发送方收到接收方的响应后,就可以从下标为确认序号的位置继续进行发送了

三、超时重传机制

双方在进行网络通信时,发送方发出去的数据在一个特定的事件间隔内如果得不到对方的应答,此时发送方就会进行数据重发,这就是 TCP 的超时重传机制

需要注意的是, TCP 保证双方通信的可靠性,一部分是通过 TCP 的协议报头体现出来的,还有一部分是通过实现 TCP 的代码逻辑体现出来的

比如超时重传机制实际就是发送方在发送数据后开启了一个定时器,若是在这个时间内没有收到刚才发送数据的确认应答报文,则会对该报文进行重传,这就是通过 TCP 的代码逻辑实现的,而在 TCP 报头当中是体现不出来的(解耦出去了,你不用管)

丢包的两种情况

丢包分为两种情况,一种是发送的数据报文丢失了,此时发送端在一定时间内收不到对应的响应报文,就会进行超时重传

丢包的另一种情况其实不是发送端发送的数据丢包了,而是对方发来的响应报文丢包了,此时发送端也会因为收不到对应的响应报文,而进行超时重传

当出现丢包时,发送方是无法辨别是发送的数据报文丢失了,还是对方发来的响应报文丢失了,因为这两种情况下发送方都收不到对方发来的响应报文,此时发送方就只能进行超时重传

如果是对方的响应报文丢失而导致发送方进行超时重传,此时接收方就会再次收到一个重复的报文数据,但此时也不用担心,接收方可以根据报头当中的32位序号来判断曾经是否收到过这个报文,从而达到报文去重的目的

需要注意的是,当发送缓冲区当中的数据被发送出去后,操作系统不会立即将该数据从发送缓冲区当中删除或覆盖,而会让其保留在发送缓冲区当中,以免需要进行超时重传 ,直到收到该数据的响应报文后,发送缓冲区中的这部分数据才可以被删除或覆盖

(其实我上面说的就是滑动窗口的逻辑,后面会结合序号机制来细细讲解)

超时重传的等待时间

超时重传的时间不能设置的太长也不能设置的太短。

  • 超时重传的时间设置的太长,会导致丢包后对方长时间收不到对应的数据,进而影响整体重传的效率
  • 超时重传的时间设置的太短,会导致对方收到大量的重复报文,可能对方发送的响应报文还在网络中传输而并没有丢包,但此时发送方就开始进行数据重传了,并且发送大量重复报文会也是对网络资源的浪费
  • 当然肯定也不能固定,因为网络环境是会变的,我们的超时重传的时间是要根据实际环境而变的

(很多时候还是一个度的问题,达到一个平衡的效果,就像红黑树取到了一个高度差和删除旋转的一个平衡点一样)

  • Linux 中(BSD Unix 和 Windows 也是如此), 超时以 500ms 为一个单位进行控制, 每次判定超时重发的超时时间都是 500ms 的整数倍.(当然这个在如今也不一定准确了,可能会更小一点)
  • 如果重发一次之后, 仍然得不到应答, 等待 2 * 500ms 后再进行重传.
  • 如果仍然得不到应答, 等待 4 * 500ms 进行重传. 依次类推, 以指数形式递增.
  • 累计到一定的重传次数, TCP 认为网络或者对端主机出现异常, 强制关闭连接.

我的电脑上,基本时间单位是4ms


四、连接管理机制

TCP是面向连接的

TCP 的各种可靠性机制实际都不是从 主机 到 主机 的,而是基于连接的,与连接是强相关的。比如一台服务器启动后可能有多个客户端前来访问,如果 TCP 不是基于连接的,也就意味着服务器端只有一个接收缓冲区,此时各个客户端发来的数据都会拷贝到这个接收缓冲区当中,此时这些数据就可能会互相干扰。

而我们在进行 TCP 通信之前需要先建立连接,就是因为 TCP 的各种可靠性保证都是基于连接的,要保证传输数据的可靠性的前提就是先建立好连接

操作系统对连接的管理

面向连接是 TCP 可靠性的一种,只有在通信建立好连接才会有各种可靠性的保证,而一台机器上可能会存在大量的连接,此时操作系统就不得不对这些连接进行管理

操作系统在管理这些连接时需要先描述,再组织,在操作系统中一定有一个描述连接的结构体,该结构体当中包含了连接的各种属性字段,所有定义出来的连接结构体最终都会以某种数据结构组织起来,此时操作系统对连接的管理就变成了对该数据结构的增删查改。

建立连接,实际就是在操作系统中用该结构体定义一个结构体变量,然后填充连接的各种属性字段,最后将其插入到管理连接的数据结构当中即可。

断开连接,实际就是将某个连接从管理连接的数据结构当中删除,释放该连接曾经占用的各种资源。

因此连接的管理也是有成本的,这个成本就是管理连接结构体的时间成本,以及存储连接结构体的空间成本。

三次握手

双方在进行 TCP 通信之前需要先建立连接,建立连接的这个过程我们称之为三次握手

以服务器和客户端为例,当客户端想要与服务器进行通信时,需要先与服务器建立连接,此时客户端作为主动方会先向服务器发送连接建立请求,然后双方 TCP 在底层会自动进行三次握手。

  • 第一次握手:客户端向服务器发送的报文当中的 SYN 位被设置为1,表示请求与服务器建立连接
  • 第二次握手:服务器收到客户端发来的连接请求报文后,紧接着向客户端发起连接建立请求并对客户端发来的连接请求进行响应,此时服务器向客户端发送的报文当中的 SYN位 和 ACK位 均被设置为1
  • 第三次握手:客户端收到服务器发来的报文后,得知服务器收到了自己发送的连接建立请求,并请求和自己建立连接,最后客户端再向服务器发来的报文进行响应

需要注意的是,客户端向服务器发起的连接建立请求,是请求建立从客户端到服务器方向的通信连接,而TCP是全双工通信,因此服务器在收到客户端发来的连接建立请求后,服务器也需要向客户端发起连接建立请求,请求建立从服务器到客户端方法的通信连接

双方地位等同,虽然我们一般都说是客户端向服务端发起连接,但是其实理论上服务端向客户端发起连接也是可以的
  为什么是三次握手,一次为什么不行,两次为什么不行?

首先我们需要知道,连接建立不是百分之百能成功的,通信双方在进行三次握手时,其中前两次握手能够保证被对方收到,因为前两次握手都有对应的下一次握手对其进行响应,但第三次握手是没有对应的响应报文的,如果第三次握手时客户端发送的 ACK报文 丢失了,那么连接建立就会失败。

虽然客户端发起第三次握手后就完成了三次握手,但服务器却没有收到客户端发来的第三次握手,此时服务器端就不会建立对应的连接。所以建立连接时不管采用几次握手,最后一次握手的可靠性都是不能保证的。

既然连接的建立都不是百分之百成功的,因此建立连接时具体采用几次握手的依据,实际是看几次握手时的优点更多。

也就是说,最新一条的握手永远无法保证,于是我们就要来看看到底至少需要握手多少次

因为TCP是全双工通信的,因此连接建立的核心要务实际是,验证双方的通信信道是否是连通的,也就是说,双方都要确认自己和对端都是能发能收的,而三次握手恰好是验证双方通信信道的最小次数,通过三次握手后双方就都能知道自己和对方是否都能够正常发送和接收数据

另外其实还有个原因就是三次握手能保证连接建立时的异常连接挂在客户端

总结一下采用三次握手的原因就是:

  • 三次握手是验证双方通信信道的最小次数,能够让能建立的连接尽快建立起来
  • 三次握手能够保证连接建立时的异常连接挂在客户端(风险转移)

三次握手的状态变化

三次握手时的状态变化如下:

  • 最开始时客户端和服务器都处于 CLOSED 状态。
  • 服务器为了能够接收客户端发来的连接请求,需要由 CLOSED 状态变为 LISTEN 状态。
  • 此时客户端就可以向服务器发起三次握手了,当客户端发起第一次握手后,状态变为 SYN_SENT 状态。
  • 处于 LISTEN 状态的服务器收到客户端的连接请求后,将该连接放入内核等待队列中,并向客户端发起第二次握手,此时服务器的状态变为 SYN_RCVD
  • 当客户端收到服务器发来的第二次握手后,紧接着向服务器发送最后一次握手,此时客户端的连接已经建立,状态变为 ESTABLISHED
  • 而服务器收到客户端发来的最后一次握手后,连接也建立成功,此时服务器的状态也变成 ESTABLISHED

至此三次握手结束,通信双方可以开始进行数据交互了

而且其实你会发现它其实是能跟我们套接字函数相对应的:

  • 在客户端发起连接建立请求之前,服务器需要先进入 LISTEN状态 ,此时就需要服务器调用对应 listen函数
  • 当服务器进入 LISTEN状态 后,客户端就可以向服务器发起三次握手了,此时客户端对应调用的就是 connect函数
  • 需要注意的是,connect函数 不参与底层的三次握手,connect函数 的作用只是发起三次握手。当 connect函数 返回时,要么是底层已经成功完成了三次握手连接建立成功,要么是底层三次握手失败
  • 如果服务器端与客户端成功完成了三次握手,此时在服务器端就会建立一个连接,但这个连接在内核的等待队列当中,服务器端需要通过调用 accept函数 将这个建立好的连接获取上来
  • 当服务器端将建立好的连接获取上来后,双方就可以通过调用 read / recv 函数和 write / send 函数进行数据交互了

四次挥手

由于双方维护连接都是需要成本的,因此当双方 TCP 通信结束之后就需要断开连接,断开连接的这个过程我们称之为四次挥手

  • 第一次挥手:客户端向服务器发送的报文当中的 FIN位 被设置为 1 ,表示请求与服务器断开连接
  • 第二次挥手:服务器收到客户端发来的断开连接请求后对其进行响应
  • 第三次挥手:服务器收到客户端断开连接的请求,且已经没有数据需要发送给客户端的时候,服务器就会向客户端发起断开连接请求
  • 第四次挥手:客户端收到服务器发来的断开连接请求后对其进行响应

为什么是四次挥手?

由于TCP是全双工的,建立连接的时候需要建立双方的连接,断开连接时也同样如此。在断开连接时不仅要断开从客户端到服务器方向的通信信道,也要断开从服务器到客户端的通信信道,其中每两次挥手对应就是关闭一个方向的通信信道,因此断开连接时需要进行四次挥手。

需要注意的是,四次挥手当中的第二次和第三次挥手不能合并在一起,因为第三次握手是服务器端想要与客户端断开连接时发给客户端的请求,而当服务器收到客户端断开连接的请求并响应后,服务器不一定会马上发起第三次挥手,因为服务器可能还有某些数据要发送给客户端,只有当服务器端将这些数据发送完后才会向客户端发起第三次挥手。

四次挥手时的状态变化

  • 在挥手前客户端和服务器都处于连接建立后的 ESTABLISHED 状态。
  • 客户端为了与服务器断开连接主动向服务器发起连接断开请求,此时客户端的状态变为 FIN_WAIT_1
  • 服务器收到客户端发来的连接断开请求后对其进行响应,此时服务器的状态变为 CLOSE_WAIT
  • 当服务器没有数据需要发送给客户端的时,服务器会向客户端发起断开连接请求,等待最后一个 ACK 到来,此时服务器的状态变为 LASE_ACK
  • 客户端收到服务器发来的第三次挥手后,会向服务器发送最后一个响应报文,此时客户端进入 TIME_WAIT 状态。
  • 当服务器收到客户端发来的最后一个响应报文时,服务器会彻底关闭连接,变为 CLOSED 状态。
  • 而客户端则会等待一个 2MSL(Maximum Segment Lifetime,报文最大生存时间 )才会进入 CLOSED 状态。

套接字和四次挥手之间的关系

  • 客户端发起断开连接请求,对应就是客户端主动调用 close函数 。
  • 服务器发起断开连接请求,对应就是服务器主动调用 close函数 。
  • 一个 close对应 的就是两次挥手,双方都要调用 close ,因此就是四次挥手。

CLOSE_WAIT

  • 双方在进行四次挥手时,如果只有客户端调用了 close函数 ,而服务器不调用 close函数 ,此时服务器就会进入 CLOSE_WAIT状态 ,而客户端则会进入到 FIN_WAIT_2 状态。
  • 但只有完成四次挥手后连接才算真正断开,此时双方才会释放对应的连接资源。如果服务器没有主动关闭不需要的文件描述符,此时在服务器端就会存在大量处于 CLOSE_WAIT状态 的连接,而每个连接都会占用服务器的资源,最终就会导致服务器可用资源越来越少。
  • 因此如果不及时关闭不用的文件描述符,除了会造成文件描述符泄漏以外,可能也会导致连接资源没有完全释放,这其实也是一种内存泄漏的问题。
  • 因此在编写网络套接字代码时,如果发现服务器端存在大量处于 CLOSE_WAIT状态 的连接,此时就可以检查一下是不是服务器没有及时调用 close函数 关闭对应的文件描述符。

TIME_WAIT

四次挥手中前三次挥手丢包时的解决方法:

  • 第一次挥手丢包:客户端收不到服务器的应答,进而进行超时重传
  • 第二次挥手丢包:客户端收不到服务器的应答,进而进行超时重传
  • 第三次挥手丢包:服务器收不到客户端的应答,进而进行超时重传
  • 第四次挥手丢包:服务器收不到客户端的应答,进而进行超时重传

如果客户端在发出第四次挥手后立即进入 CLOSED状态 ,此时服务器虽然进行了超时重传,但已经得不到客户端的响应了,因为客户端已经将连接关闭了

服务器在经过若干次超时重发后得不到响应,最终也一定会将对应的连接关闭,但在服务器不断进行超时重传期间还需要维护这条废弃的连接,这样对服务器是非常不友好的。(这样关闭的很不优雅)

为了避免这种情况,因此客户端在四次挥手后没有立即进入 CLOSED状态 ,而是进入到了 TIME_WAIT状态 进行等待,此时要是第四次挥手的报文丢包了,客户端也能收到服务器重发的报文然后进行响应。

TIME_WAIT状态 存在的必要性:

  • 客户端在进行四次挥手后进入 TIME_WAIT 状态,如果第四次挥手的报文丢包了,客户端在一段时间内仍然能够接收服务器重发的 FIN报文 并对其进行响应,能够较大概率保证最后一个 ACK 被服务器收到
  • 客户端发出最后一次挥手时,双方历史通信的数据可能还没有发送到对方。因此客户端四次挥手后进入 TIME_WAIT 状态,还可以保证双方通信信道上的数据在网络中尽可能的消散

实际上这个 TIME_WAIT 状态维持的 2MSL 是从客户端发送最后一次 ACK 握手的时候开始计时的,确保了:

  • 有足够的时间来应对最后一个 ACK 的丢失,通过重传机制实现连接的可靠终止
  • 有足够的时间让本次连接的所有报文从网络中自然消亡,防止它们干扰未来的新连接

以上是为什么要 TIME_WAIT 维持 两个MSL 的原因 DS大人 给出的解答,其实 2MSL 是一个工程上的一个平衡


总结

麻了,还没讲完TCP,那就先停一下吧,后面还有很多东西呢!

相关推荐
qq_12498707533 小时前
基于springboot的建筑业数据管理系统的设计与实现(源码+论文+部署+安装)
java·spring boot·后端·毕业设计
IT_陈寒4 小时前
Vite 5.0实战:10个你可能不知道的性能优化技巧与插件生态深度解析
前端·人工智能·后端
z***3354 小时前
SQL Server2022版+SSMS安装教程(保姆级)
后端·python·flask
特种加菲猫4 小时前
用户数据报协议(UDP)详解
网络·网络协议·udp
码上上班4 小时前
ubuntu 安装ragflow
linux·运维·ubuntu
HIT_Weston4 小时前
38、【Ubuntu】【远程开发】拉出内网 Web 服务:构建静态网页(一)
linux·前端·ubuntu
XH-hui4 小时前
【打靶日记】HackMyVm 之 hunter
linux·网络安全·hackmyvm·hmv
苏小瀚4 小时前
[JavaSE] 网络编程
网络
zxguan5 小时前
Springboot 学习 之 下载接口 HttpMessageNotWritableException
spring boot·后端·学习