当前位置: 首页 > news >正文

TCP网络编程本质

一、阻塞式网络编程的传统思路

在传统的阻塞I/O模型中,网络交互流程是“谁调用,谁等待”:

  • accept():主线程调用后阻塞,直到有客户端连接;

  • recv():线程调用后阻塞,直到有数据到达;

  • send():线程调用后阻塞,直到内核缓冲区有空间。

这种模式直观、易理解,但存在明显缺点:

  1. 每个连接需要一个独立线程;

  2. 大量连接时,线程切换和内存开销巨大;

  3. CPU 大部分时间在等待 I/O,而不是计算。

二、事件驱动的非阻塞思想

事件驱动模式(Reactor 模式)引入了一个事件循环(event loop)回调机制(callback)

  • 程序不再主动调用 recv()accept() 等系统调用;

  • 而是通过注册事件告诉系统:“当有新连接或数据到达时,请通知我”。

当内核检测到对应的 socket 可读或可写时,会通知事件循环,事件循环再调用你注册的回调函数 onMessage()onWriteComplete()

这就是“不主动拉取(pull)数据,而是等待事件推送(push)”的思维转变。

总结:思维模式的转换

阻塞模型事件驱动模型
主动调用 accept/recv/send注册回调函数
每个连接一个线程单线程+事件循环
线程被I/O阻塞事件驱动、无阻塞
简单但低效思维复杂但高性能

TCP网络编程的核心抽象模型——即“三个半事件

三个半事件的整体关系

阶段事件系统调用回调函数功能
1连接建立accept / connectonConnection创建连接对象
2消息到达readonMessage数据接收与处理
2.5消息发送完毕writeonWriteComplete输出缓冲清空
3连接断开close / read=0onClose清理资源

一、连接的建立(Connection Establishment)

1. 服务端:accept()

  • 当客户端发起 connect() 请求时,内核会完成 TCP 的三次握手。

  • 服务器监听 socket(listenfd)会变为可读(即 EPOLLIN),这意味着有新连接请求等待被 accept()

  • 调用 accept() 取出连接后,得到一个新的 socket fd(连接套接字)。

注意

  • accept() 必须在非阻塞模式下使用。

  • 新的连接 fd 才用于后续通信。

二、连接的断开(Connection Teardown)

TCP连接的关闭也有两个方向,因此分为:

1. 主动关闭(主动调用 close()shutdown()

  • 应用层调用 shutdown(fd, SHUT_WR) 表示不再发送数据;

  • 之后若对方也关闭其写端,read() 会返回 0。

2. 被动关闭(检测到对方关闭)

  • 当本端调用 read() 返回 0,表示对方关闭了连接;

  • 此时应关闭该 fd 并清理资源。

三、消息到达(Message Arrived)

这是最核心的事件

  • 对应文件描述符可读事件(EPOLLIN

  • 本质上是:socket 缓冲区中有数据可读

  • 触发时需要:

    • 从内核缓冲区读取数据

    • 处理“粘包/拆包”问题

    • 投递给上层协议解析或业务逻辑

设计要点:

  1. 非阻塞 I/O + 应用层缓冲区

    • 避免阻塞 read;

    • 设计环形缓冲或动态 buffer;

  2. 消息边界处理

    • 对于流式协议(TCP),需要在应用层解决分包、粘包;

  3. 异步处理模型

    • 可以通过线程池、任务队列解耦读写与计算。

消息发送完毕(Write Complete)

之所以称为“半个事件”,是因为它并非所有应用都需要关心。

  • 对应文件描述符可写事件EPOLLOUT);

  • 意味着:

    • 内核发送缓冲区有空间;

    • 应用层之前未写完的数据现在可以继续写;

    • 或者此前的异步发送任务已完成。

注意:

  • “写完”只表示数据进入内核发送缓冲区

  • 真正送达对方需要 TCP 协议层保证(由内核自动重传);

  • 高流量服务必须关注写事件(防止 write EAGAIN / 发送阻塞);

  • 低流量服务可以直接假设一次写完,无需复杂逻辑。

细节注意

1. 主动关闭如何确保数据已发完?

目标:先把应用层输出缓冲(output buffer)完全写入内核,再“半关闭”写端,等待对端确认关闭或超时回收。

要点

  • 绝不在输出缓冲未清空时直接 close(fd)

  • 采用**“写完再关写端”**:当 outputBuffer.empty() 时执行 shutdown(fd, SHUT_WR)

  • 可选:在进入半关闭后设置定时器(如 30–120s)防止对端不发 FIN 导致长时间占用。

  • 若业务需要“对端确收”语义,需应用层 ACK 协议配合,TCP 本身只能保证“已入对端内核/重传可靠”,无法保证“对端应用已处理”。

2. 主动发起连接被拒绝,如何带退避重试?

要点

  • 非阻塞 connect() 之后以可写事件判定完成;失败错误码如 ECONNREFUSEDETIMEDOUT

  • 使用指数退避 + 抖动(jitter)delay = min(base * 2^k + rand(0, δ), maxDelay),避免雪崩同步。

  • 通过事件循环的定时器重试;失败次数过多时进入熔断窗口。

3. EPOLL 的 LT vs ET:如何避免忙轮询或漏读饥饿?

电平触发(LT, level-triggered)

  • 适合大多数场景,简单稳妥

  • EPOLLOUT 订阅策略:仅当 outputBuffer 非空时启用;写空后立刻关闭 EPOLLOUT。否则会busy-loop

  • 读回调:循环 read() 直到 EAGAIN,虽是 LT,但这样能减少唤醒与系统调用次数。

边沿触发(ET, edge-triggered)

  • 每次就绪仅通知一次;必须在回调里把能读的全部读完能写的尽量写完,直到返回 EAGAIN

  • 若未“榨干”内核缓冲,本次边沿已消费,下次无事件→漏读饥饿

  • ET 更高效但更容易出错,建议只有在压测证明必要时采用。

4. epoll 一定比 poll 快吗?

  • 大规模 fd(上万)epoll 通常优于 poll/select(就绪集合输出、避免每次传大数组、O(ready) 语义)。

  • 小规模 fd(几十/几百):两者差距可能不明显,性能由内存局部性、锁、回调开销等决定。

  • 结论:在高并发长连接服务中优先 epoll;在低并发/短生命周期工具程序中差异可忽略。

5. 为什么需要应用层发送缓冲区(output buffer)?

场景:一次要发 40 KB,但 OS 发送缓冲还剩 25 KB。

  • 直接 write() 可能只写 25 KB,剩余 15 KB必须在应用层缓存,等待 EPOLLOUT 再续写。

  • 若上层接着又发 50 KB,而 output 中仍有残留数据,必须先排队再写,保证应用层顺序与 TCP 字节流顺序一致。

  • 不得在 output 未清空时“抢写”(另起一次 write()),否则乱序(应用层语义层面的乱序)风险上升。

关键规则

  • 只要 output 非空,就不直写用户新数据,先 append 再由写事件一次性排空;

  • 只在 output 从空变非空时打开 EPOLLOUT,从非空变空时关闭 EPOLLOUT

6. 为什么需要应用层接收缓冲区(input buffer)?

原因

  • TCP 是字节流,无消息边界:一次 read() 既可能读到 0.5 个包,也可能 1.5 个包。

  • 必须把零散数据存进 input buffer,由增量解析器(state machine / frame decoder)从中提取完整帧

  • 处理“每字节触发”极端情况:哪怕每 10 ms 到一个字节,也应靠 input buffer 聚合、靠解析器状态机拼包。

实践

  • 固定头部(含长度)、分隔符(如 \r\n\r\n)、TLV、变长 varint……都应有鲁棒的增量解析读超时

  • 历史教训:对分隔符处理不当会触发安全漏洞(过度读、注入、阻塞等)。严格做边界检查超时丢弃

7. 缓冲区如何设计,既减少系统调用又节省内存?

目标:高吞吐、低内存、低分配次数。

  • 读路径readv() + 双缓冲策略。先把数据读入 input buffer 剩余可写区;若不足,再把多余数据暂存到栈上临时大块(如 64 KB)iovec,读完后一次性 append 到 input(避免二次 read() 与频繁扩容)。

  • 延迟分配:连接建立时只给极小缓冲;根据实际数据量按需扩容,并在空闲时收缩

  • 上限控制:对 input/output 设置软/硬上限,防御异常客户端。

  • 对象池/arena:高频分配的 buffer、连接对象可池化(注意碎片与生命周期管理)。

8. 防止发送缓冲失控(背压 & 流量控制)

问题:对端处理慢,output 越积越多,导致服务端内存暴涨。
措施

  1. 高水位线回调(high-water mark):当 output.size() 超过阈值(如 512 KB/连接),触发回调:

    • 暂停上游生产(应用或业务管道);

    • 或对该连接停读disable EPOLLIN)以触发 TCP 端到端背压(对端滑窗耗尽)。

  2. 硬限制:超过硬上限(如 2–8 MB/连接)直接断开或丢弃低优先级数据,避免拖垮进程。

  3. 队列分级:区分控制/心跳/关键业务与大体量数据的优先级,必要时丢弃次要流量

  4. 全局保护:总输出缓冲达到进程级阈值时,进入限流/降级/拒绝新连接。

9. 定时器设计并与网络 I/O 共线程

原则单线程事件循环同时处理 I/O 与计时任务,避免锁。
实现选型

  • 时间轮(hierarchical timing wheel):海量短时定时器,近似 O(1) Tick;

  • 小根堆 / 红黑树:支持多样期限与取消操作,操作 O(log N);

  • Linux timerfd:把定时器变成一个可读 fd,直接并入 epoll;

  • 事件循环每次在 epoll_wait() 前设置超时为最近到期定时器的剩余时间。

典型用途

  • 连接空闲超时(idle timeout);

  • 读/写超时(防粘死);

  • 重连退避;

  • 优雅关闭守护;

  • 心跳与健康检查。

推荐的事件与状态管理“黄金法则”

  • 只在需要时订阅 EPOLLOUT;读事件长期打开但要循环读到 EAGAIN

  • ET 模式必须读/写到 EAGAIN;LT 模式也建议尽量“榨干”。

  • 所有系统调用都要处理 EAGAIN/EINTR,并在错误码上做明确分支。

  • 连接状态机Connecting → Connected → HalfClosing(Write) → Closed,明确可转移边与回调触发点。

  • 统一的 Buffer 与 Parser 抽象:复用代码路径,降低边界错误。

  • 可观测性:为每条连接暴露关键指标(in/out buffer 长度、读写频率、阻塞时长、重连次数、退避级别、定时器活跃数)。

http://www.dtcms.com/a/520930.html

相关文章:

  • 内蒙古建设厅官网站凡科建站公司
  • MySQL专题Day(3)————索引
  • 开源项目分享:Gitee热榜项目 2025年10月第四周 周榜
  • Linux常用命令与KVM基础
  • 全链路智能运维中的跨域数据联邦学习与隐私增强技术
  • 海曙网站制作wordpress模板使用
  • PD快充协议芯片XSP18 支持诱骗5V9V12V15V20V电压档位
  • AMD KFD的SDMA Packet 类型和定义解析
  • Python-模块和包
  • 网站首页代码怎么做写网站教程
  • 网站建设岗位能力评估表老鹰主机 wordpress
  • 什么网站好看用h5做天津免费建设网站
  • 从零起步学习MySQL || 第八章:索引深入理解及高级运用(结合常见优化问题讲解)
  • ASP.NET酒店管理系统源码
  • 汕头企业网站公司高端大气上档次网站
  • 昆明企业建网站多少钱河源网站设计怎么做
  • JavaEE知识点梳理与整合
  • 充值网站制作购物网站制作公司
  • 在线免费网站模板机械外贸有哪些平台
  • 基于MATLAB的Relief-F算法实现
  • 滥用 CDN 缓存功能(传播恶意内容)
  • 正点原子RK3568学习日志18-一个驱动兼容不同设备
  • SpringBoot-Web开发之静态资源管理
  • 广州做网站代理商建立网站的目录结构应注意哪些问题
  • 初识C语言12. 结构体(自定义类型的核心工具)
  • webrtc源码走读(二)-QOS-RTT
  • 【Java】线程安全问题
  • 数据结构算法学习:LeetCode热题100-链表篇(下)(随机链表的复制、排序链表、合并 K 个升序链表、LRU 缓存)
  • 网络营销网站 功能南京微信小程序开发制作
  • 域名打不开原来的网站手机免费网站制作