网络编程实践笔记

从 Socket、TCP/UDP 到并发连接和调优思路

Posted by Yvain Zhang on December 2, 2022 主题:技术

网络编程入门时,知识点看起来很多:TCP、UDP、Socket、阻塞、非阻塞、重传、拥塞控制、并发模型。
如果没有一条主线,很容易学成“术语都见过,但一写程序就还是蒙”。

这篇不打算把协议大全搬一遍,而是想先把网络编程里最常用的几条线捋顺。

1. 网络编程到底在做什么

从很朴素的角度看,网络编程做的事情就是:

  • 在不同主机之间传数据
  • 处理连接和断开
  • 处理发送、接收、超时、重试

但一旦进到真实工程里,它马上会扩展成几个问题:

  • 用 TCP 还是 UDP
  • 用阻塞还是非阻塞
  • 高并发时怎么处理连接
  • 性能和稳定性怎么平衡

2. TCP 和 UDP 先怎么区分

这个问题不只是面试题,而是决定代码结构的第一步。

TCP

更适合:

  • 需要可靠传输
  • 需要顺序
  • 需要连接状态

它会帮你处理:

  • 重传
  • 有序
  • 流量控制
  • 拥塞控制

UDP

更适合:

  • 简单快速
  • 对时延更敏感
  • 能自己接受丢包或上层处理丢包

所以选型时,重点不是“哪个更高级”,而是业务到底更怕什么:

  • 怕丢
  • 还是怕慢

3. Socket 在程序里扮演什么角色

Socket 可以先理解成操作系统提供的网络通信接口。

常见流程一般是:

  • 创建 socket
  • 绑定地址
  • 监听或主动连接
  • 发送和接收
  • 关闭连接

服务端和客户端在细节上不同,但主线差不多。

4. 阻塞和非阻塞为什么会影响整个程序结构

这不是一个小选项,而是会直接影响你怎么组织线程和事件循环。

阻塞式

优点是直观,代码好理解。缺点是一个 I/O 卡住,线程就得等。

非阻塞式

优点是更适合高并发和事件驱动;缺点是代码复杂度会明显上升,需要处理:

  • 可读 / 可写事件
  • 状态机
  • 重试和边界情况

如果连接数不多,阻塞模型可能完全够用;如果连接很多,再继续用“一连接一线程”就可能代价过高。

5. 写网络程序时最容易忽略什么

很多初学者会把 send()recv() 想得过于理想,默认:

  • 一次 send() 就发完
  • 一次 recv() 就收完整包

真实情况经常不是这样。
TCP 是字节流,不保证应用层消息边界,所以你要自己处理:

  • 粘包
  • 拆包
  • 半包

如果这层没想清楚,后面协议解析经常一塌糊涂。

6. 高并发连接为什么会把问题放大

连接一多,原本单连接下不明显的问题都会被放大,比如:

  • 每连接一个线程导致线程开销过大
  • 内存占用持续膨胀
  • 锁竞争明显
  • 超时和异常处理难以统一

这也是为什么高并发网络服务常见要在下面几类模型里做取舍:

  • 多线程
  • 多进程
  • 事件驱动
  • Reactor / Proactor

选型不是为了追时髦,而是要看连接规模、CPU 模型、业务复杂度和团队维护成本。

7. TCP 调优一般都在调什么

很多时候不是“代码错了”,而是默认参数和业务不匹配。

比较常见的调优点包括:

  • 发送 / 接收缓冲区
  • 超时重传相关参数
  • backlog
  • keepalive
  • TCP_NODELAY

其中最容易被频繁提到的就是:

Nagle 算法

它会尽量合并小包,提高带宽利用率,但有时会增加时延。

延迟确认

它和 Nagle 配合不当时,可能把小包交互的时延拉长。

所以如果业务是那种高频小包、强交互场景,经常会考虑是否关闭 Nagle。

8. 嵌入式场景下网络编程有什么额外特点

嵌入式网络编程和服务器开发相比,往往资源更紧,约束更多。

你通常还得额外考虑:

  • 内存紧不紧
  • 网络栈是不是裁剪版
  • 驱动和协议栈交界有没有瓶颈
  • 中断和收发路径是否会放大抖动

这意味着很多桌面 / 服务器环境里“理所当然”的做法,在嵌入式上不一定合适。

9. 排查网络问题时更有效的顺序

网络问题通常不要一上来就盯业务代码,先把层次拆开更稳。

先看连接有没有建立

  • 三次握手是否正常
  • 本地监听和远端地址是否正确

再看收发是否顺畅

  • 是否有重传
  • 是否有窗口受限
  • 是否有明显超时

再看应用层协议

  • 是否有粘包拆包问题
  • 包头包尾和长度字段是否正确
  • 编解码是否一致

这样排会比一开始就猜“是不是协议 bug”更有效。

10. 工程里真正值得积累的经验是什么

如果只积累“某个命令怎么用”,价值有限。更有用的是形成一些固定判断:

  • 连接问题先分层
  • TCP 稳定性问题别只看应用层
  • UDP 丢包问题别默认是代码错
  • 高并发问题通常不只是一处函数慢,而是模型不合适

这些判断一旦形成,很多问题会比单纯查资料更快进入正轨。

11. 总结

网络编程最值得先抓住的,不是 API 名字,而是下面几条主线:

  • TCP 和 UDP 的取舍
  • Socket 的基本收发模型
  • 阻塞与非阻塞的结构差异
  • 高并发连接的处理方式
  • 调优和排障时的分层思路

只要这些线理顺,后面不管你是在写普通客户端、嵌入式网络程序,还是做服务端接入,都会更容易把问题落到正确位置。