ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Linux 网络编程面试题:TCP 字节流、部分读写与 UDP 数据报

Linux 网络编程面试题:TCP 字节流、部分读写与 UDP 数据报

TCP 字节流、部分读写、应用层消息边界与 UDP 丢包处理

一、TCP 字节流与应用层消息边界

1. TCP 为什么会出现粘包和拆包?

答:TCP 是面向连接的可靠字节流协议,它只保证数据按照发送顺序可靠地交付给接收端,并不保留应用层的消息边界。发送端调用一次send(),接收端不一定能通过一次recv()完整接收这条消息,因此可能出现多条消息粘在一起,或者一条消息被拆成多次接收的情况。这就是常说的粘包和拆包。

2. TCP 如何解决粘包和拆包问题?

答:TCP 本身不负责应用层消息分包,需要应用层设计通信协议,明确消息边界。常见方式有三种:

  1. 固定长度:每条消息长度固定。

  2. 分隔符:使用特殊字符作为消息结束标志。

  3. 长度字段:消息头中携带消息长度,接收端先读取消息头,再根据长度读取完整消息体。

实际项目中,固定消息头加长度字段的方式比较常见。

3. 实际项目中,如何设计 TCP 应用层通信协议?

答:通常采用固定消息头加消息体的设计。消息头中可以包含消息长度、消息类型、序列号等信息,接收端先解析消息头,再根据长度读取完整消息体。例如:

struct MessageHeader { uint32_t length; // 消息体长度 uint16_t type; // 消息类型 uint16_t sequence; // 消息序列号 };

实际传输时不能简单地把结构体内存直接发送出去,还需要考虑字节序、结构体对齐和填充问题。通常会对字段进行序列化,并使用htonl()、ntohl()等函数处理网络字节序,同时限制消息最大长度,避免异常长度导致内存分配过大。

4. 如果一次 recv() 只收到半个消息头,应该怎么处理?

答:不能直接解析消息头,而是需要将已经接收到的数据暂存起来,等待后续数据到达。实际项目中通常会为每个 TCP 连接维护一个接收缓冲区,每次收到数据后先追加到缓冲区,再判断是否已经包含完整消息头。如果消息头不完整,就保留当前数据,等待下一次可读事件继续接收。

5. 如果一次 recv() 收到了三条完整消息和半条消息,应该怎么处理?

答:接收端应该循环解析缓冲区,而不是一次只处理一条消息。每解析出一条完整消息,就从缓冲区中移除对应的数据,继续解析下一条;如果剩余数据不足以组成一条完整消息,就保留在缓冲区中,等待后续数据到达。这样就能同时处理粘包和拆包问题。

6. TCP 的 send() 返回成功,是否代表对端已经收到数据?

答:不代表。send()成功通常只说明数据已经被本机内核接受,放入 TCP 发送缓冲区,并不意味着数据已经到达对端,更不代表对端应用程序已经处理。如果业务需要确认对端已经处理某条消息,需要在应用层设计 ACK 应答机制。

二、TCP 部分读写与非阻塞 I/O

7. TCP 为什么会出现部分读写?

答:TCP 是字节流协议,send()和recv()都不保证一次处理完指定长度的数据。send()可能只接受部分数据,recv()也可能只读取当前已经到达的一部分数据。因此,不能认为一次读写就能完成一条完整消息,需要根据返回值记录进度,继续处理剩余数据。

8. 如何保证 TCP 数据完整发送和接收?

答:发送时循环调用send(),根据返回值更新已经发送的字节数,直到全部数据发送完成。接收时根据应用层协议确定需要接收的长度,循环调用recv(),直到读取完整数据。还要处理recv()返回 0、系统调用被信号中断以及非阻塞 I/O 暂时无法继续读写等情况。

9. send() 为什么可能只发送部分数据?

答:send()受到本机 TCP 发送缓冲区空间、网络状态等因素影响。如果发送缓冲区只能接受一部分数据,send()就可能只返回实际接受的字节数,而不是请求发送的全部长度。因此,发送端必须记录已经发送的偏移量,下一次从剩余位置继续发送,不能重复发送已经成功提交的数据。

10. 非阻塞 socket 调用 send(),返回 EAGAIN 是什么意思?

答:这表示当前内核发送缓冲区暂时无法接受更多数据,并不是连接已经断开。应用程序应该保留尚未发送的数据,等待 socket 再次变为可写,然后继续发送。如果忽略这个返回值,直接丢弃剩余数据,就会造成应用层数据丢失。

11. 使用 epoll 处理 TCP 发送时,为什么需要维护发送缓冲区?

答:因为非阻塞 socket 发送数据时,可能一次无法发送完毕。应用程序需要维护一个发送缓冲区,记录待发送数据以及当前发送偏移量。每次收到可写事件后继续发送,直到数据全部发送完成,或者再次遇到EAGAIN。如果数据还没有发送完,就继续关注EPOLLOUT;发送完成后,应该取消不必要的可写事件监听,避免 epoll 持续通知造成 CPU 空转。

12. epoll 的 ET 模式下,为什么通常要求循环读取数据,直到 EAGAIN?

答:ET 是边缘触发模式,通常只在文件描述符状态发生变化时通知一次。如果一次可读事件到来后,只读取一部分数据就停止,剩余数据可能仍然留在接收缓冲区,但由于文件描述符一直处于可读状态,后续不一定会再次产生新的边沿通知。因此,ET 模式下通常要循环调用recv(),直到返回EAGAIN或EWOULDBLOCK,确保当前数据都被处理。

13. TCP recv() 返回 0 代表什么?

答:对于非零长度的接收请求,recv()返回 0 表示对端已经有序关闭了连接,也就是对端发送了 FIN,接收端已经读取完此前到达的数据。此时通常需要清理连接对应的资源。需要注意,返回-1才表示发生错误,例如非阻塞模式下的EAGAIN。

14. TCP 发送数据时,如何避免 SIGPIPE 信号导致进程退出?

答:如果向已经关闭连接的 socket 写数据,Linux 下可能产生SIGPIPE信号,默认处理方式是终止进程。可以通过忽略SIGPIPE信号,或者在调用send()时使用MSG_NOSIGNAL标志来避免进程被该信号终止,同时根据send()的返回值处理连接错误。

15. send() 返回 -1 时,所有错误都应该直接断开连接吗?

答:不是,需要根据errno区分处理。如果是EINTR,说明系统调用被信号中断,可以根据实际情况重试;如果是EAGAIN或EWOULDBLOCK,说明非阻塞 socket 当前无法继续发送,需要等待下一次可写事件;如果是连接重置等错误,则通常需要关闭连接并清理资源。不能把所有错误都当成连接断开处理。

三、缓冲区与环形缓冲区

16. TCP 通信为什么需要应用层缓冲区?

答:因为 TCP 是字节流协议,recv()不保证一次接收到完整消息,send()也不保证一次发送完所有数据。因此,应用层需要维护接收缓冲区,暂存尚未组成完整消息的数据;同时维护发送缓冲区,保存尚未发送完成的数据。缓冲区能够将网络 I/O 和业务消息处理解耦,方便处理部分读写、粘包和拆包。

17. 接收缓冲区和发送缓冲区分别有什么作用?

答:接收缓冲区用于保存从 socket 读取但尚未解析完成的数据,解析器根据应用层协议从中提取完整消息。发送缓冲区用于保存业务层产生但尚未全部发送的数据,非阻塞 socket 遇到EAGAIN时,剩余数据可以暂时保留,等待下一次可写事件继续发送。通常每个客户端连接都维护独立的收发缓冲区。

18. 普通线性缓冲区有什么缺点?

答:普通线性缓冲区可以使用vector或数组实现,但如果通过vector::erase()删除已经处理的数据,可能需要移动后面的剩余数据。在高频网络通信中,如果频繁删除缓冲区头部数据,就会产生额外的内存复制开销。数据量较大或者读写频率较高时,可以考虑使用环形缓冲区减少这种开销。

19. 什么是环形缓冲区?它的基本原理是什么?

答:环形缓冲区是一种循环利用固定内存空间的数据结构,通常使用数组实现,并维护读位置和写位置。写位置到达数组末尾后会回到数组开头,读位置也是如此。这样就可以重复利用已经释放的空间,不需要像普通线性缓冲区那样频繁删除头部数据和移动剩余数据。

通常需要维护三个信息:

  • readPos:下一次读取数据的位置。

  • writePos:下一次写入数据的位置。

  • size:当前缓冲区中有效数据的数量。

读写位置循环移动的计算方式:

pos = (pos + len) % capacity;

20. 环形缓冲区如何实现数据读写?

答:写入时先计算当前剩余空间,再判断数据是否需要跨越数组末尾。如果没有跨越,直接复制一段数据;如果跨越,就分成两段,先写入数组末尾,再从数组开头继续写入。读取时采用相同的方式,读取完成后移动读位置并减少有效数据长度。

例如容量为 8,写位置为 6,需要写入 4 字节:

原始数组: [ ][ ][ ][ ][ ][ ][A][B] 写入 C D E F: [C][D][ ][ ][ ][ ][A][B] ↑ ↑ 开头 末尾

实际分成两次内存复制,分别写入末尾剩余的空间和数组开头的空间。

21. 环形缓冲区如何解决 TCP 粘包和拆包?

答:环形缓冲区本身并不负责定义消息边界,真正解决粘包和拆包的仍然是应用层协议。环形缓冲区只是更高效地管理接收到的字节流。每次recv()收到数据后,先写入环形缓冲区,再根据消息头中的长度字段判断是否存在完整消息。如果消息不完整,就保留数据等待下一次接收;如果消息完整,就读取消息并移动读位置,然后继续解析下一条消息。

实际解析时通常分为三个步骤:

  1. 使用peek()查看消息头,但不移动读位置。

  2. 根据消息头中的长度判断完整消息是否已经到达。

  3. 消息完整后再使用consume()或read()消费对应数据。

这样即使消息头或消息体跨越了环形缓冲区的数组末尾,也可以正确解析。

22. 为什么解析环形缓冲区时需要 peek(),不能直接 read()?

答:因为解析消息头时,只是为了获取消息长度,还不能确定完整消息是否已经到达。如果直接read(),就会移动读位置并消费消息头;一旦发现消息体不完整,后续就很难按照原来的消息边界继续解析。因此,应该先使用peek()查看消息头,确认完整消息已经到达后,再移动读位置消费数据。

23. 环形缓冲区和普通线性缓冲区有什么区别?

答:

对比项普通线性缓冲区环形缓冲区
删除头部数据可能移动剩余数据移动读位置
空间利用需要处理头部空闲空间循环复用
内存复制可能发生额外搬移读写时可能分两段复制
实现难度简单相对复杂
适用场景普通网络收发高频数据流、音视频

环形缓冲区主要减少的是删除头部数据时产生的额外搬移,并不意味着完全没有内存拷贝。

24. 环形缓冲区满了怎么办?

答:首先要根据业务决定处理策略。如果是普通 TCP 通信,可以暂停继续读取,让 TCP 接收窗口和发送端形成背压;也可以动态扩容,但必须设置最大容量。如果是实时视频,则可以根据帧边界丢弃过期帧,不过不能随意丢弃 TCP 字节流中的任意字节,否则会破坏应用层协议边界。

25. 环形缓冲区可以直接用于多线程吗?

答:上面的普通环形缓冲区实现不是线程安全的。如果多个线程同时读写,需要使用互斥锁保护读写位置和有效数据长度。对于严格的单生产者、单消费者场景,也可以设计 SPSC 无锁环形缓冲区,使用原子变量维护读写位置,并通过合适的内存序保证数据可见性。

26. 环形缓冲区是不是完全没有内存拷贝?

答:不是。环形缓冲区避免的是删除头部数据时整体移动剩余数据,但从 socket 接收缓冲区拷贝到用户态环形缓冲区,以及从环形缓冲区读取数据时,仍然可能发生内存拷贝。它主要减少的是额外的数据搬移,并不等于零拷贝。

27. 为什么环形缓冲区适合音视频传输?

答:音视频传输通常具有持续、高频的数据读写需求,环形缓冲区可以循环复用固定内存,避免频繁申请和释放内存,也避免频繁移动剩余数据。不过,它并不能自动解决视频帧积压问题,还需要结合队列容量限制、帧时间戳和丢帧策略进行管理。

四、UDP 数据报与丢包处理

28. TCP 和 UDP 有什么区别?

答:TCP 是面向连接的可靠字节流协议,提供可靠、有序的数据交付,但不保留应用层消息边界。UDP 是无连接的数据报协议,每次发送的数据具有独立边界,但不保证数据一定到达,也不保证到达顺序。TCP 更适合需要可靠、有序传输的场景,UDP 更适合对实时性要求较高、能够容忍一定丢包的场景。

29. UDP 为什么不会出现 TCP 那种粘包问题?

答:UDP 是面向数据报的协议,每次sendto()发送的数据报在接收端通过一次recvfrom()接收,数据报边界会被保留。但如果接收缓冲区小于数据报长度,数据报可能被截断,剩余部分不会作为下一次接收的数据继续返回。因此,接收端需要准备足够大的缓冲区,并检查数据是否被截断。

30. UDP 不可靠传输具体体现在哪些方面?

答:UDP 不保证数据一定到达,也不保证数据按照发送顺序到达,更不提供自动重传和流量控制机制。例如发送端依次发送序列号为 1、2、3 的三个数据报,接收端可能只收到 1 和 3,也可能先收到 3 再收到 2。因此,如果业务需要可靠性,就需要在应用层补充相应机制。

31. UDP 如何检测丢包和乱序?

答:通常在应用层协议头中添加递增的序列号。接收端根据序列号判断数据是否连续,例如收到 100 后直接收到 102,说明 101 对应的数据报暂时没有收到;如果后续又收到 101,则说明发生了乱序,而不一定是真正丢包。对于实时业务,可以结合接收超时和抖动缓冲区判断数据是否已经过期。

32. UDP 丢包后,是否一定要重传?

答:不一定,需要根据业务场景决定。如果是文件传输、配置下发等要求数据完整性的场景,可以通过 ACK、超时重传和序列号实现可靠传输。如果是实时音视频,丢失的数据可能已经失去播放价值,重传反而会增加延迟,因此通常结合丢帧、错误隐藏、关键帧请求等机制处理。

33. UDP 发送的数据包越大越好吗?为什么?

答:不是。UDP 数据报如果超过路径 MTU,IP 层可能进行分片,而一个 IP 分片丢失就可能导致整个 UDP 数据报无法重组成功。同时,大数据报也会增加传输失败的影响范围。因此,实际网络传输中通常尽量避免 IP 分片,根据网络 MTU 控制 UDP 负载大小。以常见的 1500 字节以太网 MTU 为例,在 IPv4、无额外 IP 选项的情况下,UDP 负载通常不超过 1472 字节。

34. UDP 有校验和,为什么还不能保证可靠传输?

答:UDP 校验和主要用于检测数据报传输过程中是否发生错误,并不能保证数据一定到达接收端。即使数据报校验正确,也可能发生丢包、乱序或重复接收。校验和解决的是数据完整性检测的一部分问题,而可靠传输还需要 ACK、序列号、超时重传等机制。

35. UDP 没有 TCP 那样的流量控制和拥塞控制,会带来什么问题?

答:UDP 不会自动根据接收端处理能力或网络拥塞情况调整发送速率。如果发送端持续高速发送,可能导致网络拥塞、丢包增加,甚至影响其他网络业务。因此,实际应用需要根据反馈信息主动调整发送速率,例如根据丢包率、RTT 和接收端反馈进行码率控制。

五、结合实际项目的综合追问

36. 如果让你使用 TCP 实现一个视频流传输服务,你会怎么设计?

答:首先设计应用层协议,明确视频帧的消息边界,例如在帧头中保存帧长度、时间戳和帧序列号。网络层使用非阻塞 socket 配合 epoll 处理多客户端连接,每个客户端维护独立的接收和发送缓冲区。发送数据时处理部分写入和EAGAIN,接收数据时根据协议解析完整视频帧。另外,需要限制发送队列长度,避免慢客户端导致数据不断堆积;如果实时性优先,可以丢弃过期帧。

37. 如果一个 TCP 客户端网络很慢,导致发送缓冲区不断堆积,应该怎么处理?

答:首先要限制每个客户端的待发送队列大小,避免内存无限增长。然后根据业务需求设计过载处理策略,例如丢弃过期视频帧、降低发送帧率,或者断开长期无法跟上发送进度的客户端。对于实时视频,通常更关注最新画面,而不是把已经过期的帧全部发送出去。

38. 如果使用 UDP 实现实时视频传输,应该重点解决哪些问题?

答:主要需要处理丢包、乱序、重复包和网络抖动。可以在应用层添加序列号和时间戳,通过抖动缓冲区对乱序数据进行短暂缓存和重排;对于丢失的数据,根据业务决定是否请求重传或丢弃。如果视频帧被拆成多个 UDP 数据报,还需要增加帧编号、分片编号和分片总数,确保接收端能够正确组装完整帧。

39. TCP 已经保证可靠传输,为什么视频传输还可能出现画面延迟?

答:TCP 的可靠性意味着丢失的数据会通过重传机制恢复,但这可能引入额外延迟。例如某个 TCP 数据段丢失,即使后续数据段已经到达,接收端也需要等待缺失的数据段重传后才能按序向应用层交付,这种现象称为队头阻塞(Head-of-Line Blocking)。因此,在实时视频场景下,TCP 虽然能保证数据可靠、有序,但网络丢包可能造成画面延迟和卡顿。

40. TCP 和 UDP 分别适合什么样的视频传输场景?

答:TCP 适合对数据完整性和有序性要求较高的场景,例如视频文件传输、录像回放和 RTSP/RTP over TCP 场景。UDP 更适合对实时性要求较高、能够容忍一定丢包的场景,例如 RTP over UDP 实时音视频传输。不过,UDP 并不意味着一定比 TCP 延迟低,最终还取决于网络环境、传输协议和应用层设计。

41. 多个客户端同时发送数据,如何避免一个客户端阻塞其他客户端?

答:通常采用非阻塞 socket 配合 epoll 的方式处理多个客户端连接。每个客户端维护独立的接收缓冲区、发送缓冲区和连接状态,一个客户端暂时无法读写时,不应该阻塞整个事件循环。另外,对于耗时较长的业务处理,可以将任务投递到工作线程,避免在 epoll 线程中执行耗时操作。

42. 视频帧还没发送完,新的视频帧又来了,应该如何处理?

答:首先要区分业务是否要求完整、有序地传输每一帧。如果是实时预览,通常优先保证画面时效性,可以限制发送队列长度,丢弃尚未发送的过期帧,或者在帧边界处切换到最新帧。如果已经开始发送一帧,不能随意丢弃它的剩余字节,否则会破坏 TCP 字节流和应用层协议边界。需要设计明确的帧发送状态和队列淘汰策略。

43. 如果接收端处理视频帧的速度跟不上网络接收速度,应该怎么办?

答:需要对接收缓冲区和帧处理队列设置容量上限,避免数据持续积压导致内存不断增长。对于实时视频,可以根据时间戳和队列长度丢弃过期帧,优先处理最新数据;如果是要求完整性的录像传输,则需要考虑背压、限速或将数据可靠地存储到磁盘。具体策略取决于业务对实时性和完整性的要求。

六、重点知识总结

层次需要掌握的知识
TCP 基础字节流、粘包拆包、应用层消息边界
应用层协议固定长度、分隔符、长度字段、序列号
TCP 读写部分读写、发送偏移量、接收缓冲区
Linux 网络编程非阻塞 socket、EAGAIN、EINTR、SIGPIPE
I/O 多路复用epoll、ET/LT、EPOLLIN、EPOLLOUT
缓冲区线性缓冲区、环形缓冲区、读写指针、缓冲区满
UDP 基础数据报边界、丢包、乱序、重复包
UDP 深入序列号、ACK、重传、MTU、抖动缓冲区
实际项目多客户端、慢客户端、队列积压、视频帧丢弃

建议优先掌握

  1. TCP 为什么会粘包拆包,以及如何设计应用层协议。

  2. send()和recv()部分读写的处理方式。

  3. 非阻塞 socket 下EAGAIN的处理方式。

  4. epoll ET 模式为什么要循环读取到EAGAIN。

  5. 如何设计每个客户端独立的收发缓冲区。

  6. 环形缓冲区的读写原理,以及如何配合消息解析器使用。

  7. 环形缓冲区满了如何处理,如何保证多线程访问安全。

  8. UDP 如何处理丢包、乱序和重复包。

  9. TCP 可靠传输为什么可能导致实时视频延迟。

  10. 慢客户端和视频帧积压如何处理。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进