TCP/IP协议族中的TCP(三):解析其关键特性与机制

发布于:2024-04-28 ⋅ 阅读:(27) ⋅ 点赞:(0)

小白苦学IT的博客主页⭐


 

初学者必看:Linux操作系统入门⭐


 

代码仓库:Linux代码仓库⭐


 

❤关注我一起讨论和学习Linux系统

前言

TCP(Transmission Control Protocol,传输控制协议)是互联网协议族中至关重要的组成部分,它为应用程序提供了一种可靠的、面向连接的、字节流的数据传输服务。在复杂多变的网络环境中,TCP以其独特的机制确保了数据的完整性和有序性。然而,正如任何技术都有其局限性一样,TCP协议在使用过程中也面临着一些挑战,如面向字节流的处理方式、粘包问题以及异常情况的处理等。

本文将深入剖析TCP协议的工作原理,重点关注其面向字节流的特性、粘包问题的成因及解决方案,以及TCP在异常情况下的表现和处理方式。通过对这些问题的探讨,我们将更好地理解TCP协议在实际应用中的优势和局限,从而更有效地利用它进行数据传输。

在接下来的章节中,我们将首先介绍TCP协议的基本概念和工作原理,然后详细分析面向字节流的特点及其对数据传输的影响。接着,我们将探讨粘包问题的产生原因,并介绍几种常见的解决方案。最后,我们将讨论TCP协议在异常情况下的行为,包括丢包、超时、重传等问题,以及相应的处理策略。

通过本文的学习,我们将能够更全面地了解TCP协议,掌握其在实际应用中的关键技术和方法。无论是对于网络工程师、系统开发者,还是对于对网络技术感兴趣的读者,本文都将提供有益的参考和启示。让我们一同走进TCP协议的世界,探索其背后的奥秘和挑战。

面向字节流

TCP的面向字节流是指TCP协议将数据看作是一个连续的字节流,而不是独立的数据包。在TCP中,发送方将待发送的数据分割成一个个的数据段(segment),每个数据段都有一个序号(sequence number)和一个确认号(acknowledgement number)。接收方通过确认号来确认已经接收到的数据段,并向发送方发送确认消息。如果发送方没有收到确认消息,它会重新发送数据段,直到接收方确认为止。

TCP的面向字节流特性为数据的传输提供了很大的灵活性。无论发送方发送的数据块大小如何,TCP都能以字节流的形式进行处理。这意味着接收方不必等待整个数据块完全接收完毕后再进行处理,而是可以在接收到一定数量的字节后就开始处理,实现了数据的流式传输。

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

  • 调用write时, 数据会先写入发送缓冲区中;
  • 如果发送的字节数太长, 会被拆分成多个TCP的数据包发出;
  • 如果发送的字节数太短, 就会先在缓冲区里等待, 等到缓冲区长度差不多了, 或者其他合适的时机发送出去;
  • 接收数据的时候, 数据也是从网卡驱动程序到达内核的接收缓冲区;
  • 然后应用程序可以调用read从接收缓冲区拿数据;
  • 另一方面, TCP的一个连接, 既有发送缓冲区, 也有接收缓冲区, 那么对于这一个连接, 既可以读数据, 也可以写数据. 这个概念叫做 全双工

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

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

用一个例子来理解

假设你正在使用TCP协议向你的朋友发送一条消息,这条消息是:“你好,朋友!今天天气真好。” 在TCP的视角下,这条消息会被视为一个连续的字节流。

现在,当你开始发送这条消息时,TCP并不关心这条消息是如何被分割成多个数据包的。它只关注每个数据包中的字节以及这些字节的顺序。因此,你的消息可能会被TCP分割成几个数据包进行发送,每个数据包包含消息的一部分字节。

在接收端,你的朋友的TCP栈会将这些数据包重新组合成一个连续的字节流。无论数据包是如何被分割和传输的,TCP都会确保这些字节按照正确的顺序重新组合起来,从而恢复出原始的消息:“你好,朋友!今天天气真好。”

这个过程就是TCP面向字节流的核心思想。TCP并不关心应用程序发送或接收的数据块的大小和边界,它只关注字节的顺序和完整性。这种设计使得TCP能够灵活地处理不同大小的数据块,同时也为数据的可靠传输提供了保障。

粘包问题

由于TCP是面向字节流的,因此在发送和接收数据时可能会出现粘包问题。

猪八戒吃馒头

想象一下,猪八戒正在参加一个盛大的宴会,宴会上摆满了各种美食,其中就包括了他最喜欢的馒头。由于猪八戒非常贪吃,他看到馒头就忍不住一个接一个地往嘴里塞。

在这个场景中,我们可以将猪八戒吃馒头的行为类比为TCP接收数据的过程。每个馒头就相当于TCP接收到的一个数据包,而猪八戒的嘴巴则代表了TCP的接收缓冲区。

现在,假设宴会上的馒头是连续摆放的,并且猪八戒吃馒头的速度非常快。由于他的嘴巴容量有限,他可能会一口气吃下好几个馒头,而这些馒头在他嘴里就形成了一个“馒头堆”。这就好比TCP接收缓冲区中连续接收到的多个数据包,它们在没有被应用程序及时处理之前,会暂时存储在接收缓冲区中。

然而,问题在于猪八戒并不能准确记住他每次吃了多少个馒头,特别是当他吃得很快的时候。这就可能导致一个问题:如果猪八戒想要告诉他的师傅唐僧他吃了多少个馒头,他可能会因为吃得太快而记不清,从而给出一个错误的数量。这就好比TCP在接收数据时,由于数据包的连续到达和接收缓冲区的存在,接收方可能无法准确区分出每个数据包的边界,从而导致粘包问题。

粘包问题在TCP通信中是很常见的,尤其是在发送方发送数据的速度较快,而接收方处理数据的速度较慢时。为了解决这个问题,实际应用中通常会采取一些策略,比如发送方在发送数据前添加一些特殊的标识符来区分数据包的边界,或者在接收方实现一些逻辑来检测和处理粘包的情况。

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

那么如何避免粘包问题呢? 归根结底就是一句话, 明确两个包之间的边界.

解决TCP粘包问题的方法主要有以下几种:

  1. 使用消息定界符:在每个消息的结尾添加一个特定的字符或者字符序列作为消息的定界符。接收方可以根据定界符判断消息的结束位置,从而正确地解析消息。

  2. 使用消息长度:在每个消息的开头添加一个指定长度的字段,用于表示消息的总长度。接收方首先读取该字段,然后根据长度读取对应的消息,从而正确地解析消息。

  3. 使用固定长度的消息:如果每个消息的长度都是固定的,那么接收方可以按照固定长度读取数据,从而正确地解析消息。

  4. 使用分隔符:可以在每个消息之间添加一个分隔符,例如换行符、制表符等。接收方可以根据分隔符判断消息的结束位置,从而正确地解析消息。

  5. 应用层协议设计:在设计应用层协议时,可以将每个消息分为消息头和消息体两部分。消息头中包含消息的长度或者其他相关信息,以便接收方正确解析消息。

  6. 优化发送和接收逻辑:发送方可以在发送数据后等待接收方的确认再发送下一包数据,接收方在收到数据后也及时发送确认消息,这样可以降低粘包的可能性。

  7. 使用更高级别的协议:例如,使用UDP协议而不是TCP协议。UDP协议是非面向连接的,它不会使用块的合并优化算法,因此不会出现粘包问题。但请注意,UDP协议并不保证数据的顺序性和可靠性,所以在选择UDP时需要权衡这些因素

对于定长的包, 保证每次都按固定大小读取即可; 例如上面的Request结构, 是固定大小的, 那么就从缓冲区从头开始按sizeof(Request)依次读取即可;
对于变长的包, 可以在包头的位置, 约定一个包总长度的字段, 从而就知道了包的结束位置;
对于变长的包, 还可以在包和包之间使用明确的分隔符(应用层协议, 是程序猿自己来定的, 只要保证分隔符不和正文冲突即可);

思考: 对于UDP协议来说, 是否也存在 "粘包问题" 呢?

对于UDP, 如果还没有上层交付数据, UDP的报文长度仍然在. 同时, UDP是一个一个把数据交付给应用层. 就有很明确的数据边界.
站在应用层的站在应用层的角度, 使用UDP的时候, 要么收到完整的UDP报文, 要么不收. 不会出现"半个"的情况.

TCP异常情况

  1. 进程终止:当一个进程终止时,它会释放与该进程相关的所有资源,包括文件描述符。在TCP连接中,即使进程终止,如果连接还处于打开状态,那么操作系统通常会尝试发送一个FIN包来正常关闭连接。这是因为TCP连接是双向的,即使一端进程终止,另一端仍然需要知道连接已经关闭。因此,从TCP协议的角度来看,进程终止并不会立即导致连接中断,而是会触发正常的关闭流程。
  2. 机器重启:机器重启时,所有正在运行的进程都会被终止,包括那些维护TCP连接的进程。然而,与进程终止类似,由于TCP连接是双向的,重启机器并不会立即中断连接。操作系统会在重启过程中尝试关闭所有打开的连接,或者在网络栈恢复后发送RST包来通知对端连接已经不可用。
  3. 机器掉电/网线断开:当机器掉电或网线断开时,TCP连接实际上已经失去了物理层的支持,但TCP协议本身并不会立即知道这一点。接收端在一段时间内仍然会认为连接是有效的,直到它尝试写入数据或收到TCP的保活定时器发出的探测报文并发现连接已经中断。此时,接收端会发送RST包来关闭连接。TCP的保活定时器是确保连接有效性的重要机制,它定期发送探测报文来检查对端是否仍然在线。如果对方不在,定时器会触发连接释放流程。
  4. 应用层协议检测机制:除了TCP本身的保活定时器外,许多应用层协议也实现了自己的连接状态检测机制。例如,HTTP长连接会定期发送心跳包来检查连接是否仍然有效;QQ等应用也会在断线后尝试重新连接,以确保用户能够持续通信。这些机制增强了通信的可靠性和稳定性,使得即使在网络不稳定的情况下,应用也能尽量保持连接状态。

TCP小结

为什么TCP这么复杂? 因为要保证可靠性, 同时又尽可能的提高性能.
可靠性:

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

提高性能:

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

其他:

  • 定时器(超时重传定时器, 保活定时器, TIME_WAIT定时器等)

TCP/UDP对比

TCP和UDP是两种完全不同的通信协议,它们在网络通信中各自扮演着重要的角色。以下是它们之间的一些主要区别:

  1. 连接性:TCP是面向连接的协议,意味着在数据传输之前,通信双方必须先建立连接。这个连接过程通过三次握手完成,确保了数据的可靠传输。而UDP则是无连接的协议,发送数据前无需建立连接,因此具有更小的开销和更快的发送速度。
  2. 可靠性:TCP提供可靠的数据传输服务,通过序列号、确认应答、超时重传等机制保证数据的完整性和顺序性。相比之下,UDP则不保证可靠交付,发送端发送数据后,不会进行任何错误检查或重传,因此可能会出现数据丢失或乱序的情况。
  3. 速度:由于TCP需要建立连接和进行各种可靠性保障操作,其传输速度通常比UDP慢一些。UDP直接发送数据,无需额外的控制信息,因此传输速度更快。
  4. 数据包大小:TCP在传输数据时,会将数据分割成较小的数据块,并根据网络状况调整数据块的大小。而UDP的数据包大小没有限制,它允许发送方发送任意长度的数据包。
  5. 应用场景:TCP因其可靠性和有序性,常被用于需要稳定数据传输的场景,如文件传输、电子邮件等。而UDP因其轻量级和快速性,更适用于实时性要求高、对可靠性要求不那么严格的场景,如视频流、实时游戏等。

用UDP实现可靠传输

1.序列号与确认应答

  • 为每个UDP数据包分配一个唯一的序列号,这样接收端就可以按照正确的顺序重组数据。
  • 接收端在成功接收每个数据包后,发送一个确认应答(ACK)给发送端。ACK中应包含已成功接收的数据包的序列号。

2.超时重传

  • 发送端维护一个发送缓冲区,用于存储待发送的数据包和它们的序列号。
  • 如果发送端在一定时间内未收到某个数据包的确认应答,它将重传该数据包。
  • 需要合理设置超时时间,以避免因网络延迟导致的误判。

3.流量控制

  • 接收端维护一个接收窗口,告诉发送端当前可以接收的数据包范围。
  • 发送端根据接收窗口的大小来控制发送速率,避免数据包丢失和拥塞。

4.数据包的拆分与组装

  • 如果应用层数据较大,需要将其拆分成多个UDP数据包进行传输。
  • 接收端在收到所有数据包后,按照序列号将它们组装成原始的应用层数据。

5.错误检测与纠正

  • 可以使用校验和等方式来检测数据包的完整性。
  • 如果发现数据包损坏或丢失,发送端应负责重传。

6.拥塞控制

  • 虽然UDP本身不提供拥塞控制机制,但可靠UDP传输的实现可以借鉴TCP的拥塞控制算法,如慢开始、拥塞避免等。
  • 通过观察丢包率和往返时间(RTT)等网络参数来调整发送速率。

7.连接管理

  • 虽然UDP是无连接的协议,但可靠UDP传输的实现可以加入连接建立和终止的机制。
  • 例如,在数据传输前发送一个握手包来确认双方的可用性,并在数据传输完成后发送一个终止包来释放资源。


网站公告

今日签到

点亮在社区的每一天
去签到