【Linux】TCP协议


TCP协议

TCP全称为 “传输控制协议(Transmission Control Protocol”)。 人如其名,要对数据的传输进行一个详细的控制。

TCP协议段格式

在这里插入图片描述

TCP 报文中每个字段的含义如下:

  • 源端口和目的端口字段:源端口(Source Port)是源计算机上的应用程序的端口号,目的端口(Destination Port)是目标计算机的应用程序端口号,都占 16 位。
  • 序列号字段:序列号(Sequence Number)占 32 位。它表示本报文段所发送数据的第一个字节的编号。
  • 确认号字段:确认号(Acknowledgment Number,ACK Number)占 32 位。它表示接收方期望收到发送方下一个报文段的第一个字节数据的编号。
  • 数据偏移字段(4 位首部长度):数据偏移是指 TCP 报文中的有效载荷(数据)相对于 TCP 报文起始位置的字节偏移量,占 4 位。
  • 保留字段:保留(Reserved)占 6 位。为 TCP 将来的发展预留空间,目前不需要关心该字段。
  • 标志位字段:包括 URG、ACK、PSH、RST、SYN、FIN 等标志位。
    • URG:紧急指针是否有效
    • ACK:确认号是否有效
    • PSH:提示接收端应用程序立刻从 TCP 缓冲区把数据读走
    • RST:对方要求重新建立连接; 我们把携带 RST 标识的称为复位报文段
    • SYN:请求建立连接,我们把携带 SYN 标识的称为同步报文段
    • FIN:通知对方,本端要关闭了,我们称携带 FIN 标识的为结束报文段
  • 窗口大小字段:窗口大小(Window Size)占 16 位。它表示从 Ack Number 开始还可以接收多少字节的数据量,也表示当前接收端的接收窗口还有多少剩余空间。
  • TCP 校验和字段:校验位(TCP Checksum)占 16 位。它用于确认传输的数据是否有损坏。校验和由发送端填充,CRC校验,接收端校验不通过则认为数据有问题. 此处的检验和不光包含 TCP 首部,也包含 TCP 数据部分。
  • 紧急指针字段:紧急指针(Urgent Pointer)仅当前面的 URG 控制位为 1 时才有意义。它指出本数据段中为紧急数据的字节数,占 16 位。
  • 可选项字段:选项(Option)长度不定,但长度必须是 4 字节的整数倍。

通过上述我们简单了解了一下TCP协议字段,下面同样我们以有效载荷与报头如何分离这个话题切入引出对于TCP协议基本特征的讲解。

Q:如何分离TCP中有效载荷与报头?

由于UDP报头字段是固定大小的,并且报头字段中有UDP报文长度信息,所以我们能快速找到有效载荷与报头的边界,而对于TCP协议它的报头不是固定大小的,因为选项这个字段大小不确定,所以我们只要找到选项字段与有效载荷的边界即可!!对于TCP协议段我们只知道报头前20个字节是固定的,而选项这个字段的大小未知!这时候 4 位首部长度字段就起到作用了,它是用来表示 TCP 报头的长度的,4个bit位能表示最多为15,显然TCP报头固定大小都为20字节了,所以其单位必然不可能为字节,其单位为4字节根据TCP协议端格式刚好能偏移到下一个字段,也就是报头最大能表示15 * 4 = 60字节,因此TCP报头长度范围为【0,60】,前20字节为固定的,那么选项字段的范围为【20,60】,所以其最多有40个字节的选项。那么该如何找到其边界点呢?首部长度字段 * 4字节 - 20 == 0 ?报头完毕 :有几个字节的选项,我们继续端取这几个字节,那么剩下的就是有效载荷了!!
步骤:a. 提取报文前20个字节,提取出首部4位长度字段 b. 计算选项字段大小,首部长度 * 4 - 20 == 0 ?此时报头完毕,没有选项字段 :有几个字节的选项,继续读取完这几个字节 c. 剩下的就是有效载荷!

确认应答机制

我们常说TCP最大的特征就是可靠,那么出现什么现象是不可靠的表现呢?比如:丢包、乱序、重复、检验失败、发送太快/太慢、网络出现问题…这些都是不可靠的表现。

Q:出现这些问题的原因?

其实单纯就是因为通信双方距离变长了,那么在数据进行传输的过程中就加大了这些问题产生的概率!!

那么TCP是如何保持可靠性的呢?

TCP 保证可靠性的一个重要的机制就是确认应答机制。当发送端发送数据时,它会等待接收端的确认应答(ACK)。如果接收端成功接收到数据,它会返回一个确认应答给发送端。

在这里插入图片描述

确认应答机制
在这里插入图片描述
当客户端收到了服务端发来的数据(1-1000)的确认应答,则证明数据(1-1000)是一定被服务端接收到了,下一个数据从1001开始!!

序号与确认序号

客户端向服务端请求时,实际并不是只发送一个请求(注:只发送一个请求,效率过低),可能是多个请求,而服务端也可能会给客户端回复多个应答。如下面这种情况才是实际客户端与服务器的通信情况:

在这里插入图片描述

客户端一次能发送多个数据报,而服务端收到数据报之后也能发送多个确认应答,这样并行的收发数据就能大大提高效率,但是这样做就出现了很多问题,多个报文经过网络发送的顺序不一定是服务端接收的顺序,也就是报文乱序问题;同时client端收到多个确认应答,client如何知道哪个应答对应哪一个报文??所以要解决这些问题就注定了要使用编号来对这些报文加以区分!!

那么 TCP 是如何解决以上这两个问题的呢?想要知道这个,我们需要了解 TCP 报头中的序号和确认序号。

  • 序号是发送方给每个发送的报文分配的一个编号(TCP 将每个字节的数据都进行了编号),用来标识该报文在数据流中的位置。接收方可以根据序号对接收到的报文进行排序,以确保数据按顺序传输。
  • 确认序号是接收方返回给发送方的一个编号,表示该编号前的所有报文都已经接收到了,也可以用来表示接收方期望接收的下一个数据包的序号。发送方可以根据确认序号判断哪些数据包已经被接收方成功接收,哪些数据包需要重新发送。

在这里插入图片描述
通过序号和确认序号,请求和应答就可以一一对应起来了,因为确认序号的含义是该序号前的所有报文都已经接收到了。注:这里的报文指的是携带完整 TCP 报头的 TCP 报文。

Q:通过上诉分析我们发现只用序号这一个字段其实就能代表序号与确认序号,那么为什么一定要使用两个字段来完成呢?

TCP 协议是全双工的通信双方既可以收数据,也可以发数据。那么就可能会出现这种情况:服务端在给客户端应答的时候,也想给客户端发送数据。发送数据就需要有序号,那么哪个序号是数据?哪个序号是确认?此时就无法区分开来了,因此就注定了 TCP 报头中要有序号和确认序号两个字段,这样也能保证TCP进行全双工通信。(注:服务端在给客户端应答的同时也想给客户端发送数据,我们可以压缩成一个应答/请求,这样的机制叫做:捎带应答。它是一种提高效率的机制,后续我们再谈。)

超时重传

TCP 的超时重传机制是指在 TCP 协议中,当发送数据的一方在规定的时间内未收到接收方的确认信号时,就会触发超时重传机制。具体来说,TCP 将每个发送的数据包都标记一个时间戳,如果在规定的时间内未收到确认信号,就会重新发送该数据包。这个时间戳是根据之前的数据包的发送时间、传输距离以及网络拥塞程度等因素计算得出的。

超时重传的两种情况

情况一:发送方发送数据报文丢失了,此时发送方在一定的时间内无法收到对应的应答,会对该报文进行重传。

在这里插入图片描述

情况二:接收方收到了发送方的报文,但是接收方给发送方的应答报文丢失了,此时发送方也会因为一定时间内没有收到对应的应答,进而对该报文进行重传。

在这里插入图片描述

  • 所以发送方无法判别是因为发送的报文丢失了,还是因为接收方发送的应答报文丢失了,但是发送方也不关心这个,只要发送方在一定的时间内没有收到对应的应答,发送方就会对该报文进行重传。
  • 对于情况二来说,此时接收方会收到很多重复数据,这时候我们就可以利用前面提到的序列号很容易做到去重的效果。
  • 当发送缓冲区中的数据被发送出去后,操作系统并不会立即将刚发送出去的数据从缓冲区中移除或者覆盖掉。只有当接收方收到了对应的应答后,我们才能认为对方收到了该数据,否则该数据必须保留在发送缓冲区中等待超时重传。

超时重传机制可以保证数据的可靠传输,但也会影响网络的传输效率。因此,TCP 协议的实现通常会根据网络状况动态地调整超时时间,以达到最佳的传输效率和可靠性。

Q:那么超时重传的时间该如何确定?

  • 如果超时重传的时间过长,会导致丢包后发送方长时间收不到对应的应答而一直在等待,影响了整体重传的效率。
  • 如果超时重传的时间过短,会导致发送方频繁地重传数据。因为应答报文有可能正在网络中传输,并没有丢包,但是超时重传的时间过短,此时发送方就开始重传数据了,这样会导致发送方重传了大量的重复数据,浪费网络资源,降低了网络传输的效率。
  • 最理想的情况就是找到一个最小的时间,保证确认应答一定能在这个时间内返回。但是这个时间是不固定的,其应该根据网络状况和数据传输的要求进行调整。当网络状况好的时候,超时重传的时间可以设置成短一点,提高网络传输的效率;而当网络状况差时,超时重传的时间可以设置成长一点,降低重传的概率和避免网络拥塞。

TCP为了保证无论在任何环境下都能比较高性能的通信,因此会动态计算这个最大超时时间

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

六个标记位

Q:为什么TCP需要多个标记位?

  • TCP 报文的种类是多种多样的,除了正常通信的常规报文,还有建立连接时发送的建立连接报文以及断开连接时发送的断开连接报文等等。
  • 服务端接收到大量的不同类型的报文时,服务端就要根据报文的类型来进行相应的处理。例如:服务端接收到正常通信的常规报文时,会将该报文放入到接收缓冲区中等待上层应用来读取。当服务端接收到建立连接和断开连接的报文时,操作系统会在 TCP 层内进行三次握手和四次挥手的动作。
  • 正是因为不同类型的报文需要对应不同的处理逻辑,我们就需要通过某种策略来区分报文的类型,而 TCP 就是通过报头中的六个标记位来区分报文的类型,这六个标记位都只占一个比特位。其中,1 表示真,0 表示假。例如:如果 TCP 报头中的 ACK 的比特位为 1,说明该报文是一个确认报文。注:不要以为ACK只返回确认1,实际上它一定携带了TCP报头信息,这样接下来对方能知道你的报头信息,也就能采取相应的策略去保证可靠性和提高效率。

  • SYN:SYN(Synchronize Sequence Numbers)表示同步序列号,是建立连接时使用的握手信号。如果 TCP 报头中的 SYN 被设置为 1,则说明该报文是一个请求建立连接的报文。

  • ACK:ACK(Acknowledge Character)是确认应答的标记位,该标记位被设置为 1,表示对收到的报文进行确认应答。

  • RST:RST(Reset)是用于非正常关闭连接的标记位,该标记位被设置为 1 时,表示该 TCP 双方连接状态出现异常,需要关闭该连接,重新进行连接!!在 TCP 连接中,RST 数据包通常是由接收方发送的,用于表示接收方无法处理接收到的数据包或者表示接收方已经关闭了连接。当发送方收到 RST 数据包时,会立即关闭连接并且不会发送任何数据。

    • 进行三次握手时,如果客户端收到服务端发送的 SYN + ACK 数据包后,向服务端发送 ACK 数据包此时认为连接已经建立好了,但是假设此时 ACK 数据包丢失了,服务端没接收到 ACK 数据包,服务端认为连接未完成,此时双方对于连接状态的认知有误。客户端认为连接成功了就向服务端发送数据,而服务端认为双方未连接成功客户端就发送数据给我,说明客户端此时的认知出现了问题,这时服务端会向客户端发送 RST 数据包表示你需要关闭该连接并重新进行三次握手建立连接。
    • RST 攻击:攻击者可以伪造一个 TCP 数据包,将 RST 标志位设置为 1,然后发送给另一端,这样就可以终止连接。攻击者可以通过这种方式来终止正常的连接,从而使另一端无法正常工作。
  • PSH:(Push)是用来告知对方是否立即读取缓冲区的数据的标记位,如果 PSH 标记位被设置为 1,则表示发送端要求接收端立即读取缓冲区中的数据。

    • 接收端给发送端应答时会在 TCP 报头中填充窗口大小,告诉发送端自己接收缓冲区剩余空间的大小。如果窗口大小为 0,则说明接收缓冲区没有剩余空间,发送端会发送 PSH 数据包要求接收端立即从接收缓冲区中读取数据,而发送端也会不断探测接收端的缓冲区剩余空间,直至缓冲区剩余空间达到某一阀值就开始发送数据。

双方使用 TCP 协议进行网络通信时,TCP 协议是保证报文按序发送和按序到达的,即便到达接收端的接收缓冲区的顺序和发送时的顺顺序不一样,也可以根据序列号来排序,从而实现按序到达。
一般情况下,TCP 按序到达是我们所希望的,要求接收方从接收缓冲区中按序读取数据。但是有些特殊情况,发送方会给接收方发送一些紧急数据,要求接收方优先读取这些数据。那该怎么办呢?

此时就需要用到 TCP 报头中的 URG 标记位和 16 位紧急指针了。

  • URG(Urgent)是标记紧急指针是否有效的标记位,如果 URG 标记位被设置为 1,则说明该报文的紧急指针有效,该报文是紧急报文。
  • 16 位紧急指针的大小表示紧急数据在有效载荷中的偏移量,紧急指针所指向的位置的一个字节的数据就是紧急数据,需要上层优先读取该数据。

紧急数据并不参与排队,OS构建了另一条通道方便我们再特殊情况时上层优先读取该数据。

连接管理机制

如何理解连接?

TCP 协议是一种面向连接的可靠的通信协议。使用 TCP 协议通信前,客户端和服务器之间需要建立一个连接,并维护这个连接的状态,才能进行数据的传输。
每个客户端将来都有可能连接同一个服务端,那么服务端中一定会存在大量的连接,此时操作系统就需要对这些连接进行管理。如何管理?先描述再组织!在操作系统内部里一定会有一个描述连接的结构体,结构体有着各种字段来描述着连接的各种属性,最终所有定义出来的连接结构体会以某种数据结构的方式连接起来。此时,操作系统对连接的管理就转换成对该数据结构的增删查改了。服务器维护连接管理一定是需要内存 + CPU资源的!!

在正常情况下, TCP要经过三次握手建立连接, 四次挥手断开连接:

在这里插入图片描述
我们的服务端需要先进行预备工作,创建套接字、绑定端口号+IP、成为监听套接字、调用accept阻塞等待客户端进行链接,然后再进行三次握手建立连接。

三次握手

客户端和服务端进行 TCP 通信前需要建立好连接,而建立连接的过程就称之为三次握手。

在这里插入图片描述

以客户端和服务端为例,客户端想要和服务端进行通信。客户端主动向服务端发起建立连接的请求,然后由客户端和服务端的 TCP 层自主完成三次握手建立好连接。

  • 第一次握手:客户端发送一个 SYN 标记位为 1 的数据包给服务端,其中包含了客户端随机生成的初始序列号。
  • 第二次握手:服务端收到客户端的 SYN 数据包后,向客户端发送一个 SYN 和 ACK 标记位都为 1 的数据包,表示确认收到了客户端的 SYN 数据包同意建立连接,并且还发送数据给客户端。SYN + ACK 数据包中包含了服务器随机生成的初始序列号和确认序号(客户端的初始序列号 + 1,表示之前的数据已经收到)。
  • 第三次握手:客户端收到服务端的 SYN + ACK 数据包后,会向服务端发送一个 ACK 标记位为 1 的数据包,表示确认收到了服务端的 SYN + ACK 数据包并且客户端认为建立连接成功,ACK 数据包中包含了确认序号(服务端的初始序列号 + 1)。
  • 三次握手的状态变化:
    • 客户端向服务器发送 SYN 包,客户端进入 SYN_SENT 状态。
    • 服务器收到 SYN 包后,向客户端发送SYN + ACK 包,服务器进入 SYN_RCVD 状态。
    • 客户端收到 SYN + ACK 包后,向服务器发送 ACK 包,客户端进入 ESTABLISHED 状态。
    • 服务器收到 ACK 包后,服务器进入 ESTABLISHED 状态。

三次握手完成后,客户端和服务端之间的连接就建立起来了,从此客户端和服务端就可以通过该连接进行通信,直至其中一方断开连接。三次握手的过程,由双方的OS中的TCP层自主完成!connect触发链接等待完成,accept等待建立完成,获取连接!它们并不参与握手过程!

Q:三次握手一定能保证成功吗?

三次握手不一定能够保证成功,因为在网络传输中可能会出现各种各样的问题,如:网络延迟、丢包、服务端关机等等。以第三次握手为例,如果服务端发出的 SYN + ACK 数据包超过一段的时间没有收到应答,服务端会认为该数据包丢失并进行数据包重传。重传次数根据 /proc/sys/net/ipv4/tcp_synack_retries 来指定,默认是 5 次。如果重传次数超过了这个值,服务端就会认为连接建立失败,关闭连接。

Q:为什么是三次握手呢?而不是一次、两次、四次等次数呢?

  • 如果是一次握手的话,客户端发送 SYN 包就认为双方建立连接成功这样是肯定不行的,因为客户端压根就不确定对方是否已经收到SYN包,更别谈对方是不是服务器了。并且一次握手也非常容易受到洪水攻击!!SYN 洪水攻击是攻击者向被攻击主机发送大量的伪造的 TCP 连接请求,从而使得被攻击主机服务器的资源耗尽(CPU 满负荷或内存不足)的攻击方式。
  • 如果是两次握手的话,客户端发送SYN包并且服务端发送SYN + ACK包,可以证明客户端具有收发能力,但是服务端发送SYN + ACK包并不确定客户端已经收到了,也就无法证明服务端就有发送能力!!如果是三次握手的话客户端最终向服务端发送ACK,就证明了服务端具有发送能力!!同时二次握手也非常容易受到SYN 洪水攻击。因为服务端收到客户端的 SYN 数据包后,给客户端发送 SYN + ACK 数据包,此时服务器就认为连接建立好了,但实际上客户端有没有接收到它并不知道。如果客户端向服务端发送大量的 SYN 数据包,服务端一直认为连接建立完成,进而消耗大量资源去管理连接。
  • 如果采取的是三次握手,只有当服务端接收到客户端的 ACK 数据包才会进行连接的建立,而客户端在发出 ACK 数据包时就把连接建立好了。此时,如果客户端对服务端进行 SYN 洪水攻击,那么客户端中也会存在大量的连接,它是要付出代价的。
  • 三次握手可以将连接建立异常的成本嫁接到客户端,一定程度上保证了服务端的安全。因为奇数次握手最后一定是客户端来发送ACK数据包,客户端认为已经建立了连接,而服务端不一定建立连接,这样假设客户端对服务端进行SYN洪水攻击,此时服务端连接的成本也大大减小了!!除了这个作用,三次握手还有验证全双工的作用。客户端通过发送 SYN 数据包和接收 SYN + ACK 数据包来证明自己即能收也能发,服务端通过接收SYN数据包和接收 ACK数据包来证明自己即能收又能发,而一次握手和两次握手都无法验证 TCP 是全双工的。
  • 偶数次握手会将连接建立异常的成本嫁接到服务端,因此不会采取偶数次的握手。而三次以上的奇数次握手会浪费时间和资源,因为三次握手已经能够将连接建立成功了。

从生活的角度来理解为什么TCP需要三次握手:

小明想打电话给小美,但是不确定对方是不是小美,所以会经历以下过程:

  • 小明:你好,请问是小美吗?【第一次握手】
  • 小美:是的,我是小美。【第二次握手】
  • 小明:好的,我知道你是小美了。【第三次握手】

经历三次握手后,小明可以可以很明确的确定对方就是小美,非常可靠!

  • 假如只有一次握手,那么小明问完“你好,请问是小美吗?”,小明压根不确定对方有没有收到这个问候,更不要谈对方是否是小美了。
  • 假如只有两次握手,那么小明收到小美的 “是的,我是小美。” 的回复后,小美是不知道小明是否收到她的回复,所以接下来她不确定是否要跟电话中询问她的人通信。

而三次握手刚刚好建立全双工通信!!

四次挥手

客户端和服务端结束通信时需要断开连接,断开连接的过程就是四次挥手。
在这里插入图片描述
以客户端主动断开连接为例:

  • 第一次挥手:客户端主动向服务端发送 FIN 报文,请求断开连接,表明客户端不会再向服务端发送数据了,但可以接收服务端发送过来的数据。

  • 第二次挥手:服务端接收到客户端发送的 FIN 报文后,会给客户端发送 ACK 报文,表明服务端收到了客户端的 FIN 报文。而服务端可能还有数据需要进行处理和发送,所以连接并没有真正关闭。

  • 第三次挥手:服务端处理完数据后,便向客户端发送 FIN 报文。

  • 第四次挥手:客户端收到服务端的 FIN 报文后,向服务端发送 ACK 报文,表明确认关闭连接。

  • 如果收到客户端的 FIN 报文时,服务端没有数据需要进行处理,那么 ACK 和 FIN 可以在同一个报文中同时设置为 1,此时四次挥手就变成了三次挥手。

四次挥手的状态变化:

  • 客户端向服务端发送 FIN 报文后,进入 FIN_WAIT_1 状态。
  • 服务端向客户端响应 ACK 报文后,立马进入 CLOSE_WAIT 状态。
  • 客户端收到服务端发送的 ACK 报文后,客户端的状态会从 FIN_WAIT_1 变成 FIN_WAIT_2。
  • 服务端向客户端发送 FIN 报文后,服务端的状态会从 CLOSE_WAIT 变成 LAST_ACK。
  • 客户端收到服务端发送的 FIN 报文后,客户的状态会从 FIN_WAIT_2 变成 TIME_WAIT,并给服务端发送 ACK 报文。
  • 服务端收到客户端发送的 ACK 报文后,服务端的状态会从 LAST_ACK 变成 CLOSED。
  • 主动关闭连接的一方进行第四次挥手后,需要维持 TIME_WAIT 状态一段时间,这个时间的大小是 2 MSL(Max Segment Lifetime, 报文最大生存时间),然后才能进入 CLOSE 状态。

Q:四次挥手一定能够成功吗?

四次挥手不能够保证一定成功。四次挥手可能会失败的情况包括:客户端或服务器发送的 FIN 数据包丢失,客户端或服务器发送的 ACK 应答报文丢失。如果出现这些情况,客户端或服务器会重传丢失的数据包,直到连接断开或达到最大重传次数。

下面我们针对CLOSE_WAIT与TIME_WAIT这俩种状态来进行重点讨论。

Q:当什么情况下会出现大量CLOSE_WAIT?

出现CLOSE_WAIT状态是因为服务器接收到客户端的FIN,然后服务端发送ACK,为什么CLOSE_WAIT状态没有变为LAST_ACK呢?是因为服务端根本就没向客户端发送FIN,即未主动请求断开也就是没调用close(sock)关闭套接字!!

我们将服务端的代码稍做调整,在传输完数据之后不主动调用close(sock):

首先启动服务器:

在这里插入图片描述

使用 telnet 工具充当客户端连接服务端后再使用 netstat 来查看服务器状态

在这里插入图片描述

客户端输入 quit 断开连接后,使用 netstat 来查看 CLOSE_WAIT 状态

在这里插入图片描述

过了一段时间后,我们再次查看发现客户端此时已经退出!!

在这里插入图片描述

结论:当客户端迟迟未收到服务端发来的FIN,认为其可能没有数据要发送了,等待一段时间后会自动退出!!服务端未主动关闭连接时,会存在大量 CLOSE_WAIT 状态!!


下面我们来看看TIME_WAIT状态,主动关闭连接的一方是会进入TIME_WAIT状态的,下面以服务端主动关闭为例:

在这里插入图片描述

那么如果我们想让服务器能够立即重新启动呢,可以使用 setsocketopt 函数的 SO_REUSEADDR 选项,设置地址是可以重复使用的。SO_REUSEADDR 选项允许在同一端口上快速重新启动服务器,而不必等待 TIME_WAIT 状态的套接字释放。下面我们接着来测试一番:

在这里插入图片描述

Q:TIME_WAIT的时间为多长?

MSL在RFC1122中规定为两分钟,但是各操作系统的实现不同,在Centos7上默认配置的值是60s;可以通过 cat /proc/sys/net/ipv4/tcp_fin_timeout 查看msl的值。

Q:为什么要保持 TIME_WAIT 状态 2 MSL 的时间呢?

  • MSL 是 Max Segment Lifetime,报文最大生存时间,报文从一端到另一端所需时间的最大值,它是任何报文在网络上存在的最长时间,超过这个时间报文将被抛弃。
  • TIME_WAIT 状态保持两倍的 MSL 时间,是因为防止历史连接中的数据被相同四元组(源 IP、源端口、目的 IP、目的端口)的连接错误的接收。如果在关闭连接之前的某个报文被网络延迟了没有送到对端,可能这个报文已经被重传过了,然后客户端和服务端经过一段时间后关闭连接。但是客户端和服务端立即建立了四元组相同的连接,曾经被网络延迟的报文刚好达到对端且刚好在对端的接收窗口内,对端会正常接收这个报文,但是这个数据报文是上一个连接残留下来的,这样就会导致数据错乱等严重问题。因此,TIME_WAIT 状态需要至少保持 2 MSL 的时间,保证历史的数据完全从网络中消消失。
  • TIME_WAIT 状态保持两倍的 MSL 时间的另一个原因是保证被动关闭连接的一方能够正确地关闭连接。如果客户端(主动关闭方)最后一次 ACK 报文(第四次挥手)在网络中丢失了,那么根据 TCP 可靠性原则,服务端(被动关闭方)会重传 FIN 报文。假设客户端(主动关闭连接方)没有 TIME_WAIT 状态,而是在发完最后一次 ACK 报文就直接进入 CLOSED 状态。如果该 ACK 报文丢失了,服务端则重传 FIN 报文,而此时客户端已经进入关闭状态了,在收到服务端重传的 FIN 报文后,就会发送RST 报文给服务端。
  • 服务端收到这个 RST 报文并将其解释为一个错误(Connection Reset By Peer),这对于一个可靠的协议来说不是一种优雅的断开连接的方式。为了防止以上情况的出现,客户端必须等待足够长的时间,确保服务端能够收到 ACK 报文。如果服务端没有收到 ACK 报文,那么服务端就会重传 FIN 报文,这样一去一来刚好两个 MSL 的时间。注:客户端在收到服务端重传的 FIN 报文时,TIME_WAIT 状态的等待施加你会被重置回 2 MSL。如果服务端重传次数超过规定的次数时,服务端会自动关闭连接。

窗口大小

TCP 本身是具有接收缓冲区和发送缓冲区的:接收缓冲区用来暂时保存接收到的数据,发送缓冲区用来暂时保存还未发送的数据,这两个缓冲区都是在 TCP 传输层内部实现的。TCP 发送缓冲区当中的数据是由上层应用层程序进行写入的,当上层调用 write / send 这样的系统调用接口时,就会将应用层的数据拷贝到 TCP 的发送缓冲区中,并不是将数据直接发送到网络中;TCP 接收缓冲区当中的数据也是由上层应用层程序来进行读取的,当上层调用 read / recv 这样的系统调用接口时,就会将 TCP 接收缓冲区的数据拷贝到应用层中,并不是直接从网络中获取数据。当数据拷贝到 TCP 中的发送缓冲区中后,对应的 write / send 函数就能够返回,无须关系数据如何发送以及如何发送,这些问题都是 TCP 协议来解决的。

发送缓冲区和接收缓冲区的作用:

  • 数据在网络传输的过程中可能会遇到一些错误,导致对端无法接收到数据,这时就需要进行数据重传,因此 TCP 必须提供一个发送缓冲区来暂时保存发送出去但未收到确认应答的数据。当收到对端的确认应答后,发送缓冲区中的数据才能够被覆盖。
  • 接收端接收数据的速度是有限的,为了保证没来得及接收的数据不会被迫丢弃,因此 TCP 提供了一个接收缓冲区来保存接收到但未来得及读取的数据。因为数据的传输是非常耗时,我们不能够随意丢弃经过网络传输过来的数据。

在这里插入图片描述
经典的生产者消费者模型:

  • 对于发送缓冲区来说,上层应用不断将数据拷贝到发送缓冲区中,下层网络层不断从发送缓冲区中拿出数据进行进一步的封装。在这个过程中,上层应用充当着生产者的角色,下层网络层充当着消费者的角色,而发送缓冲区就是交易场所。
  • 对于接收缓冲区来说,下层网络层不断将解包后的数据写入到接收缓冲区中,上层应用不断从接收缓冲区中拿出数据进行进一步的处理。在这个过程中,下层网络层充当着生产者的角色,上层应用充当着消费者的角色,而接收缓冲区就是交易场所。
  • 引入接收缓冲区和发送缓冲区就相当于引入了两个生产者消费者模型,生产者消费者模型将上层应用与底层通行细节进行了解耦,同时也支持并发来提高通信的效率。

当发送端将数据发送给对端时,其本质就是将自己发送缓冲区中的数据发送到对端的接收缓冲区中。但是缓冲区是有大小的,当接收端处理数据的速度小于发送端发送数据的数据,缓冲区就有可能会被填满。这时候,发送端再发送过来的数据就无法放入到接收缓冲区中导致数据丢失,从而引发数据重传等连锁反应。为了解决这个问题,TCP 报头中包含了 16 位窗口大小,16 位窗口大小中填充的是自己的接收缓冲区剩余空间的大小,也就是通过 16 位窗口大小告知对方自己的接收能力。
接收端在对发送端发送过来的数据进行响应时,可以通过 TCP 的报头中的 16 位窗口大小来告知发送端自己当前接收缓冲区剩余空间的大小。此时,发送端就可以根据这个窗口大小来调整自己发送数据的速度。

  • 窗口大小字段越大,说明接收端处理数据的能力越强,发送端可以适当地提高数据的发送速度。
  • 窗口大小字段越小,说明接收端处理数据的能力越弱,发送端需要适当地降低数据的发送速度。
  • 当窗口大小字段等于 0 时,说明接收端的接收缓冲区已经没有剩余空间了,发送端应该停止发送数据直到接收端处理完一些数据,接收缓冲区中有空间剩余。
  • 通过 16 位窗口大小告知对方自己的接收能力,这样就可以做到流量控制了。

理解本质:

  • 在进行套接字编程时,我们调用 write / send 函数向套接字中写入数据时,可能会因为套接字的发送缓冲区已经被写满而被阻塞住了,其本质就是 TCP 中的发送缓冲区已经被写满了,所以 write / send 函数就需要阻塞到发送缓冲区有足够的空间来存储数据。
  • 我们调用 read / recv 函数从套接字中读取数据时,可能会因为套接字中的接收缓冲区中没有数据而被阻塞住了,其本质就是 TCP 中的接收缓冲区中没有数据,所以 read / recv 就需要阻塞到接收缓冲区中有一定数量的数据。
  • 调用 write / send 和 read / recv 函数会被阻塞注,本质就是生产者消费者模型中的临界资源没有就绪,需要阻塞等待直到条件满足。

流量控制

接收端处理数据的速度是有限的,如果发送端发的太快,导致接收端的缓冲区被打满,这个时候如果发送端继续发送,就会造成丢包,继而引起丢包重传等等一系列连锁反应。因此 TCP 支持根据接收端的处理能力,来决定发送端的发送速度,这个机制就叫做流量控制(Flow Control)。

  • 接收端将自己接收缓冲区剩余空间的大小放入 TCP 报头中的窗口大小字段,通过 ACK 报文通知发送端。
  • 窗口大小字段越大,说明网络的吞吐量越高。
  • 接收端一旦发现自己的缓冲区快满了,就会将窗口大小设置成一个更小的值通知给发送端。发送端接受到这个窗口之后,就会减慢自己的发送速度。
  • 如果接收端缓冲区满了,就会将窗口大小置为 0。这时发送方不再发送数据,但是需要定期发送一个窗口探测数据段,使接收端把窗口大小告诉发送端。还有一种策略就是,接收端的缓冲区剩余空间更新后,接收端会给发送端发送窗口更新通知来告知对方接收缓冲区剩余空间的大小。
  • TCP 是全双工的,可以在两个方向上进行流量控制。
  • 三次握手时,会通过 TCP 报头中的窗口大小字段来告知对方自己的接收缓冲区剩余空间的大小。那么在正式进行网络通信时,就已经得知对方的接收能力了,可以根据对方的接收能力来发送数据。
  • 16位窗口字段最大能表示的值是 65535,那么 TCP 窗口最大就是 65535 字节么?并不是,实际上,TCP 报头 40 字节选项中还包含了一个窗口扩大因子 M,实际窗口大小是窗口字段的值左移 M 位,M 的取值范围是 0 到 14。

在这里插入图片描述

滑动窗口

我们都知道 TCP 是每发送一个数据,都要进行一次应答。当上一个数据收到了应答,再发送下一个数据。这样的模式有非常明显的缺点就是性能较差。当数据往返时间越长时,网络的吞吐量会越低。

在这里插入图片描述

既然这样一发一收的方式性能较低, 那么我们一次发送多条数据, 就可以大大的提高性能(其实是将多个段的等待时间重叠在一起了)

在这里插入图片描述
需要注意的是,虽然双方在进行通信时可以一次向对方发送多条数据,但这样也是要考虑对方的接收能力的,发送数据的总量不能超过对方接收缓冲区剩余空间的大小。

发送方给对方发送多条数据时,这么多条数据当中会有部分数据是没有收到的。我们可以将发送缓冲区中的数据分为三部分:

  • 已经发送并且已经收到 ACK 确认的数据
  • 已经发送但未收到 ACK 确认的数据
  • 未发送的数据

在这里插入图片描述
如上图所示,发送缓冲区第二部分数据所占的空间就是滑动窗口的空间。滑动窗口的大小是指无需等待 ACK 应答而可以继续发送数据的数量的最大值。

在这里插入图片描述

滑动窗口存在的最大意义就是提高通信效率:

  • 滑动窗口的大小等于对方窗口大小与自身拥塞窗口大小之间的较小值,拥塞窗口需要根网络状况有关,暂时不需要考虑拥塞窗口,可以认为滑动窗口的大小就等于对方窗口的大小。
  • 不考虑拥塞窗口的硬性,假设滑动窗口的大小固定为 4000,也就是说发送方不需要等待对方的应答而一次向其发送 4000 字节的数据。
  • 现在发送方可以直接向对方连续发送 1001 到 2000、2001 到 3000、3001 到 4000 和 4001 到 5000 这四个段的数据,不需要等待对方的应答。
  • 当收到对方发送的确认序号为 2001 的报文时,说明 1001 到 2000 这个段的数据已经被对方收到了,那么此时滑动窗口可以向右移动。因为滑动窗口的大小固定为 4000(对方窗口的大小),所以 5001 到 6000 这段数据可以直接向对方发送,不需要等待其他数据段的应答,以此类推。
  • 滑动窗口越大,则说明网络的吞吐率越高,也说明对方的接收能力越好。

当发送方发送的数据陆陆续续地收到对应的应答时,此时可以将应答所对应的数据段移出滑动窗口,置于滑动窗口的左侧。而窗口是否会向右移动则取决于对方的接收能力(窗口大小)。如果对方可以接收更多的数据,那么窗口可以向右移动将位于窗口右侧的数据包含进窗口中,进行数据的发送。

TCP 超时重传机制要求发送缓存区暂时保存发送未被对方确认的数据,而这部分数据就是在滑动窗口当中。位于滑动窗口左侧的数据都是已经被对方确认收到的数据,这些数据能够被操作系统删除或者覆盖。因此,滑动窗口不仅能够保证能向对方一次性发送多条数据,而且还保证了数据的超时重传。
滑动窗口主要是解决数据传输的效率问题,顺带保证超时重传的可靠性。

滑动窗口的本质就是指针或者下标,而滑动窗口的移动就是指针或者下标增加。

在这里插入图片描述

Q:滑动窗口一定能够向右移动吗?滑动窗口如果一直向右移动会造成越界问题吗?

  • 滑动窗口不一定能够向右移动,因为滑动窗口的大小是受对方窗口大小的限制的,如果对方应用层没有从接收缓冲区中拿出数据,这样对方的窗口会越来越小,因此滑动窗口会越来越小,无妨向右移动。
  • 滑动窗口并不一定是整体向右移动的,可能是窗口的左边界进行移动而右边界不移动,因为对方的窗口大小是不固定的,随时在变化,所以滑动窗口的大小也不是固定的,也是随时都在变化。
  • 滑动窗口一直向右移动并不会造成越界问题。因为发送缓存区是被看成环形队列的,当滑动窗口向右移动时,如果超出了缓冲区的范围,那么就会进行模除运算,重新回到缓冲区的起始位置进行向右移动。

Q:滑动窗口的大小可以为零吗?

滑动窗口的大小可以为零。当对方一直不从接收缓冲区中拿取数据,会导致发送方的滑动窗口越来越小直至为零。


下面我们分析一下丢包问题:

情况一:数据已经被接收方接收,但是 ACK 丢失了
在这里插入图片描述
这种情况下,部分 ACK 丢了并不要紧,因为可以通过后续的 ACK 进行确认。

情况二:数据直接丢失了,未被接收方接收到
在这里插入图片描述

  • 当某一段报文段丢失之后, 发送端会一直收到 1001 这样的ACK, 就像是在提醒发送端 "我想要的是 1001"一样;
  • 如果发送端主机连续三次收到了同样一个 “1001” 这样的应答, 就会将对应的数据 1001 - 2000 重新发送;
  • 这个时候接收端收到了 1001 之后, 再次返回的ACK就是7001了(因为2001 - 7000)接收端其实之前就已经收到了, 被放到了接收端操作系统内核的接收缓冲区中

这种重传机制就是快重传机制。快速重传机制的实际方式是:当发送方收到三个相同的 ACK 时,就认为是前一个数据包已经丢了,并立即重传。这个机制提高重传的效率。

快重传 VS 超时重传

  • 快重传能够进行数据的快熟重传,当发送方连续收到三个相同的 ACK 时,就会触发快重传机制。而快重传机制不需要向超时重传那样通过设置重传定时器,在一定的时间后进行重传。
  • 虽然快重传机制能够实现数据的快速重传,但是触发这个机制是有条件的,就是连续收到三个相同的 ACK。这也是快重传无法取代超时重传的原因,超时重传也是一种兜底的策略。如果接收方发送的 ACK 报文一直丢失的话,就无法触发快重传了,这时候就需要超时重传。

拥塞控制

双方通过 TCP 进行通信时,出现丢包问题是非常正常的,此时就通过超时重传或者快重传进行数据的重传。但是如果双方进行通信时出现了大量的丢包,此时就不能认为这是正常现象了。TCP 协议不仅考虑到了两个主机端到端的可靠性问题,还考虑了网络状态的问题。

当出现大量丢包问题时,就可能出现网络拥塞问题。当出现网络拥塞问题,就不能立即将这些数据进行重传。因为一个网络是多个主机进行共用的,并且这样主机使用的都是 TCP / IP 协议,你重传了,那别的主机要不要重传呢。所以当网络出现问题时,不应该再向网络中发送大量数据。网络拥塞影响的不只是一台主机,影响的是该网络下的所有主机,此时所有使用 TCP 协议的主机都需要执行拥塞避免算法来缓解网络拥塞的状态,使得网络状态慢慢得以恢复。

虽然 TCP 协议有了滑动窗口这个大杀器,能够高效可靠的发送大量的数据。但是如果在刚开始阶段就发送大量的数据,仍然可能引发问题。因为网络上有很多的计算机,可能当前的网络状态就已经比较拥堵。在不清楚当前网络状态下,贸然地发送大量的数据是很有可能引起雪上加霜的。

TCP 协议引入了慢启动机制,先发少量的数据探探路,摸清当前的网络拥堵状态,再决定按照多大的速度传输数据。

在这里插入图片描述

为了解决网络拥塞问题,就引入了一个概念拥塞窗口。

  • 拥塞窗口是可能引起网络拥塞的上限值,如果一次性发送的数据量超过了拥塞窗口的大小,就有可能引起网络拥塞。
  • 刚开始发送数据时,拥塞窗口的大小被定义成 1,。每收到一个 ACK 应答,拥塞窗口的大小就增加一。
  • 每次发送数据的视乎,会将拥塞窗口的大小和接收方主机返回的窗口大小进行比较,取较小值作为滑动窗口的大小。

每收到一个 ACK 应答,拥塞窗口的大小就增加一,那么拥塞窗口的大小是以指数的形式进行增长的。指数形式增长只是初始时增长较慢,也就是所谓的慢启动,但是越往后增长就后越快,这时候就有可能短时间内又造成了网络拥塞的问题。

  • 为了避免短时间内有造成网络拥塞的问题,那么就不能让拥塞窗口大小一直以指数的形式增长下去。
  • 此时就引入了一个慢启动的阈值,当拥塞窗口的大小超过这个阈值时,拥塞窗口大小就不在意指数的形式进行增长了,而是以线性的形式进行增长。
  • 当 TCP 刚开始启动的时候,慢启动的阈值被设置为对方窗口大小的最大值。
  • 在每次超时重传的时候,慢启动的阈值会变成当前拥塞窗口大小的一般,同时拥塞窗口的大小变成一,重新开始增长。

在这里插入图片描述

上图说明:

  • 指数增长:刚开始发送数据时,拥塞窗口大小被设置为 1,并且以指数形式进行增长。因为指数前期增长较慢,可以避免网络拥塞问题。而中后期时指数增长快,此时网络也恢复了,可以尽快恢复双方的通信效率。
  • 线性增长:慢启动的阈值初始时被设置为对方窗口大小的最大值,上图中的慢启动阈值为 16。当拥塞窗口的大小增长到慢启动阈值时就不再以指数形式进行增长,而采用线性增长的方式。注意:线性增长阶段时,还未发生网络拥塞问题。
  • 乘法减小:当拥塞窗口大小增长到 24 时,发生了网络拥塞问题,此时慢启动阈值就会变成当前拥塞窗口大小的一半,同时拥塞窗口大小变成一。然后拥塞窗口再次以指数的形式进行增长,周而复始。

少量的丢包,我们仅仅是触发超时重传或快重传,大量的丢包,我们就认为可能是网络拥塞了。当 TCP 通信开始后,网络吞吐量会逐渐上升;随着网络发生拥堵,吞吐量会立刻下降。拥塞控制,归根结底是 TCP 协议想尽可能快的把数据传输给对方,但是又要避免给网络造成太大压力的折中方案。

延迟应答

如果接收数据的主机接收到数据立刻返回 ACK 应答,此时返回的窗口可能比较小。

  • 假设接收端缓冲区为 1M,一次收到了 500K 的数据。如果立刻进行应答,那么返回的窗口大小就是 500K。
  • 但实际上接收方处理数据的速度可能会很快,10ms 之内就可以把 500K 数据从缓冲区消费掉了。
  • 在这种情况下,接收端处理还远没有达到自己的极限,即使窗口再放大一些,也能处理过来。
  • 如果接收端稍微等一会再应答,比如等待 200ms 再应答,那么这个时候接收方返回的窗口大小就是 1M。
  • 接收方在接收到数据后,并不会立即发送 ACK 应答,而且是等候一段时间(一般是200ms),等接收方上层处理完数据后再给发送方发送一个更大的窗口大小,这种机制就是 TCP 协议的延迟应答机制。延迟应答机制并不是为了保证可靠性的,而是为了提高效率的一种策略。

有了延迟应答机制,发送给对方的窗口大小就会越大,网络吞吐量就越大,传输效率就越高。延迟应答机制的目标是在保证网络不拥塞的情况下尽量提高传输效率。
在这里插入图片描述
需要注意的是,并不是所有的数据包都可以延迟应答。

  • 数量限制:每隔 N 个包就应答一次。
  • 时间限制:超过最大延迟时间就应答一次(这个时间不会导致超时重传)。

具体的数量和超时时间,依操作系统不同也有差异。一般 N 取2,超时时间取 200ms。

捎带应答

捎带应答是双方进行 TCP 通信时最常见的一种方式。TCP 协议的捎带应答机制就是发送的同一个 TCP 数据包中即包含数据又包含 ACK 应答的一种机制。

就好比,主机 A 给主机 B 发送一条消息,主机 B 收到该消息后就回复给出 ACK 应答。但如果主机 B 也有消息需要给主机 A 发送,那么这个 ACK 应答就可以和该消息放在同一个报文中,而不需要再单独发送一个 ACK 应答了。此时,主机 B 发送的这个既完成了对收到的数据的应答,又完成了自己数据的发送。

在这里插入图片描述
捎带应答机制很明显能够减小网络通信的开销,因为通信双方不再单独地发送 ACK 应答了。

面向字节流

创建一个TCP的socket, 同时在内核中创建一个发送缓冲区和一个接收缓冲区;

  • 调用 write 或 send 函数时,数据会先考到到发送缓冲区中。
  • 如果要发送的数据的字节数太多,会被拆分成多个 TCP 的数据包发出。
  • 如果要发送的数据的字节数太少,就会先发送缓冲区中保存着。等到缓冲区长度差不多了,或者其他合适的时机再发送出去。
  • 接收数据的时候,数据也是从网卡驱动程序到达内核的接收缓冲区,然后应用程序可以调用 read 或 recv 函数从接收缓冲区拿取任意字节长度的数据。
  • TCP 的一个连接既有发送缓冲区,也有接收缓冲区。那么对于这一个连接,既可以读数据,也可以写数据,这个概念叫做全双工。

由于缓冲区的存在,TCP 程序的读和写不需要一一匹配。

  • 写 100 个字节数据时,可以调用一次 write 函数写 100 个字节,也可以调用 100 次 write 函数,每次写一个字节。
  • 读 100 个字节数据时,也完全不需要考虑写的时候是怎么写的,既可以一次 read 100 个字节,也可以一次 read 一个字节, 重复 100 次。

对应 TCP 来说,根本就不关心发送缓冲区中存储的是什么数据,在它看来就是一个个字节的数据,它只需要将这些数据可靠地发送到对方的接收缓冲区中就行了。而对方的上层也不会关心接收缓冲区的数据是什么数据,只需要将其一个字节一个字节地读取上来就行了,像这种不关心数据格式的数据通信流程,就被称为面向字节流。

粘包

Q:什么是粘包?

首先要明确的是,粘包问题中的 “包”,是指的应用层的数据包。在 TCP 的协议报头中,没有向 UDP 协议一样的 “报文长度” 这样的字段,但是有一个序号这样的字段。站在传输层的角度,TCP 协议是一个一个报文传输过来的,并且按照序号排好序放在缓冲区中。但是站在应用层的角度,看到的只是一串连续的字节数据。那么应用程序看到了这么一连串的字节数据,并不知道从哪个部分开始到哪个部分是一个完整的应用层数据包,这就是所谓的粘包。

Q:如何解决粘包问题呢?

解决粘包问题的本质就是明确两个包之间的边界,只要知道包与包之间的边界,就能够正确读取一个数据包了。

  • 对于定长的包,保证每次都按固定大小读取即可。
  • 对于变长的包,可以在包头的位置,约定一个包总长度的字段,从而就知道了包的结束位置。比如 HTTP 报头使用 Content-Length 字段来表示正文的长度。
  • 对于变长的包,还可以在包和包之间使用明确的分隔符。应用层协议可以由程序员自己来定制的,只要保证分隔符不和正文冲突即可。

UDP 协议是否存在粘包问题呢?

  • 对于 UDP协议,如果还没有向上层交付数据,UDP 的报文长度仍然存在。同时 UDP 协议是一个一个把数据交付给应用层,就有很明确的数据边界。
  • 站在应用层的角度,使用 UDP 协议进行通信的时,要么是收到完整的 UDP 报文,要么就是没有收到 UDP 报文,不会出现读取到半个 UDP 报文的情况。
  • 因此,UDP 协议是不存在粘包问题的,根本原因就是 UDP 报头中有 16 位 UDP 长度字段来明确数据之间的边界。而 TCP 是基于字节流,没有明确的数据边界,需要应用层定制协议来明确数据与数据之间的边界。

TCP异常情况

1. 进程崩溃 / 进程退出

当客户端和服务端正常通信时,客户端进程突然崩溃了,那么建立好的连接会怎么办呢?

TCP 的连接信息是有内核维护的,所以当客户端进程崩溃后,内核需要挥手该进程的 TCP 连接资源。于是内核会发送第一次挥手 FIN 报文,后续的挥手过程也都是在内核中完成的,并不需要进程的参与。所以即使客户端进程退出了,还是能与服务器完成 TCP 四次挥手释放连接的。

2. 机器重启

当客户端和服务端正常通信时,客户端机器重启,那么建立好的连接会怎么办呢?

当客户端选择机器重启时,操作系统会把正在运行的所有进程杀掉再进行机器重启,那么机器重启的情况就和进程崩溃、进程退出的情况一样了。操作系统自动帮客户端与服务器进行四次挥手,正确释放连接。

3. 机器掉电 / 网线断开

当客户端和服务端正常通信时,客户端机器掉电或网线断开,那么建立好的连接会怎么办呢?

当客户端机器掉电或网线断开时,客户端的连接会自动关闭掉,但是服务端是无法感觉客户端已经关闭连接了,因此服务端还会保持连接。但是这个连接并不会一直保持,因为 TCP 协议具有保活机制。

  • 保活机制:定义一个时间段,在这个时间段内,如果没有任何连接相关的活动,TCP 保活机制会开始作用。每隔一个时间间隔,发送一个探测报文,该探测报文包含的数据非常少。如果连续发送几个探测报文都没有得到应答,则认为当前连接已经死亡,系统内核会将错误信息通知上层应用程序,进而关闭连接。
  • 客户端长期掉线,服务端会发送探测报文来检查客户端的状态。如果发送多个探测报文都没有收到应答,那么服务端会将该连接进行关闭。
  • TCP 的保活机制是基于定时器实现的,如果对方能够正常应答,那么定时器将会被重置。

应用层的某些协议,也有一些这样的检测机制。例如 HTTP 长连接中,也会定期检测对方的状态。例如 QQ,在 QQ断线之后,也会定期尝试重新连接。

TCP小结

TCP 协议之所以设计得这么复杂,是因为既要保证可靠性,同时也要尽可能地提高性能。

可靠性:

  • 校验和
  • 序列号
  • 确认应答
  • 超时重发
  • 连接管理
  • 流量控制
  • 拥塞控制

提高性能:

  • 滑动窗口
  • 快速重传
  • 延迟应答
  • 捎带应答

其他:

  • 超时重传定时器:用于实现可靠传输。当一个 TCP 数据包被发送出去后,如果在一定时间内没有收到对方的确认应答,则会触发超时重传机制,即重传该数据包。而超时时间则是通过定时器来控制的。
  • 保活定时器:TCP 协议中的保活定时器用于检测 TCP 连接是否已经失效,如果失效则断开连接。
  • TIME_WAIT 定时器:主动断开连接的一方需要保持 TIME_WAIT 状态 2MSL 的时间,确保被动关闭连接放能够正确关闭连接。

基于TCP应用层协议

基于 TCP 协议的应用层协议有很多,常见的协议如下:

  • HTTP:超文本传输协议,用于万维网上的数据传输。
  • HTTPS:超文本传输安全协议,基于 HTTP 协议之上的安全协议,用于万维网上的数据传输。
  • SSH:安全外壳协议,用于远程登录主机和文件传输
  • Telnet:远程终端协议,用于远程登录主机。
  • FTP:文件传输协议,用于在网络上进行文件传输。
  • SMTP:简单邮件传输协议,用于电子邮件的发送。
  • DNS:域名系统,用于将域名转换为 IP 地址。

当然,也包括我们自己写 TCP 程序时自定义的应用层协议。

TCP与UDP的对比

TCP 协议和 UDP 协议的区别主要有一下几个方面:

  • 连接:TCP 是面向连接的协议,而 UDP 是无连接的协议。
  • 可靠性:TCP 协议提供可靠的数据传输,即数据包在传输过程中会进行确认、重传和流量控制等机制,而 UDP 协议则不提供可靠性保证。
  • 传输效率:由于 TCP 协议提供了许多可靠性保证机制,因此其传输效率相对较低;而 UDP 协议则没有这些机制,因此传输效率相对较高。

TCP 协议和 UDP 协议没有明显的好坏之分,我们需要根据具体的应用场景来选择合适的协议。TCP 协议和 UDP 协议的具体应用场景:

  • TCP 协议适用于需要可靠传输保障的应用场景,如:文件传输、电子邮件和网页访问等等。
  • UDP 协议适用于要求传输效率高、实时性高的应用场景,如:实时音视频传输、游戏直播等等。

用UDP实现可靠传输

可以参考 TCP 协议保证可靠性的做法,在应用层实现类似的功能。例如:

  • 引入序列号:为每个数据包分配一个唯一的序列号,接收方可以使用序列号来检测数据包的丢失和顺序错误,并进行必要的处理。
  • 引入确认应答:发送方在发送数据包后等待一段时间,如果没有收到确认响应,就会认为数据包丢失,然后重新发送。接收方在收到数据包后发送确认响应。
  • 引入超时重传:发送方在发送数据包后启动一个计时器。如果在一定时间内未收到确认响应,就认为数据包丢失,然后进行重传。

理解listen的第二个参数

在讲三次握手的过程中我们就提到了accept函数不参与三次握手的过程,三次握手的过程是由双方OS的TCP层自主完成的,也就是说上层不调用accept函数我们也能建立连接!!但此时是双方肯定是不能通信的,因为服务端还没调用accept函数对这个连接做任何处理呢!!下面我们来看看listen的第二个参数,它到底是干啥的呢?接下来我们来测试一番:

在这里插入图片描述

通过上述测试客户端状态正常,但是服务器端出现了 SYN_RECV 状态, 而不是 ESTABLISHED 状态
这是因为,Linux内核协议栈为一个tcp连接管理使用两个队列:

  • 半连接队列(用来保存处于SYN_SENT和SYN_RECV状态的请求),这个时间很短是临时的!
  • 全连接队列(accpetd队列)(用来保存处于established状态,但是应用层没有调用accept取走的请求)

而全连接队列的长度会受到 listen 第二个参数的影响,全连接队列满了的时候, 就无法继续让当前连接的状态进入ESTABLISHED 状态了,这个队列的长度通过上述实验可知是 listen 的第二个参数 + 1。

Q:为何TCP协议需要在底层维护全连接队列?

如果上层来不及调用 accept 函数,并且对端还来了大量的连接请求,说明服务器现在已经很繁忙了,但是也不能随意的拒绝连接请求,因此我们就需要在底层维护一个全连接队列,当上层能继续调用accept获取连接的话,此时直接可以从全连接队列里面拿到连接,这样就使得服务器资源得到了充分发挥!!如果没有队列的话,服务器闲下来的话,没有连接可以获取,这样就无法使服务器的资源充分发挥处理。

但是这个队列不能太长,太长的话client端等的时间就会变长;太长的话会过多消耗OS资源去维护队列,还不如直接将这些资源直接划分给服务器,这样服务器就能处理更多的连接请求更能提高效率!

我们通过生活中的一个小故事来理解为何需要队列的问题:我们在外面吃饭,是不是有时会看到餐馆座位满了的情况,这时候服务员看到了就问我们要不要排号在休息区等待,餐馆里面有座位了就立马叫你,此时排号就叫做全连接队列;假设没有排号服务,那么餐馆的顾客用餐结束之后,可能会过一会儿才有顾客来用餐,那么此时就相当于服务器处于休闲状态,无法充分利用资源!!而我们的排号不能太长,太长了顾客不愿意等待,而且花大量资源就修建休息区供顾客等待,还不如直接扩大店面这样就能直接提高用餐体验和效率!!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

malloc不出对象

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值