网络编程实战

前方施工中

参考资料

如何保证可靠的 TCP 连接

发送比接收更难

  • 按难度排序:
    1. 服务器建立 TCP < 客户端建立 TCP < 销毁 TCP
    2. 接收 TCP 数据 < 发送 TCP 数据 (尤其在非阻塞 IO 中)
  • 常见错误:send()+close() 导致丢数据
    • 如果接收缓冲区里有数据,close() 会导致 RST 段(而不是 FIN 段)强行断连,不论发送缓冲区是否为空,导致对方丢数据。
    • SO_LINGER 不能解决这个问题。
    • 正确发送端做法:send()+shutdown_write()+read_until_eof()+close()
    • 正确接收端做法:read() -> 0 + nothing to send + close()
    • 再考虑服务器防御:shutdown_write() 后设置超时,无论如何都关闭连接。
    • 更好的做法是在协议中包含数据长度,使 App 能够判断数据是否完整。
    • 还有一点漏洞:sender 无法保证 receiver 已经正确处理数据,例如 receiver 崩溃时,sender 也会读到 eof。

启用 SO_REUSEADDR

  • 允许重复监听同一个端口
    • 以便 server 崩溃之后可以立即重启
    • 以便多进程 server 监听同一个端口

在 Server 中处理 SIGPIPE 信号

  • 在 Linux I/O 中,若输出管道已经关闭,则 writer 会收到 SIGPIPE 信号。默认的反应行为是中止进程。
  • 默认行为在多数场景下十分好用,可以提前结束工作流,减少 CPU 浪费。
    1
    gunzip -c huge.log.gz | grep ERROR | head
    然而在网络编程中,若 Client 已经关闭,则 Server 同样会收到 SIGPIPE 信号。
  • Server 应当小心处理 SIGPIPE 信号,以防 Client 出错或者恶意响应。
  • 若直接忽略 SIGPIPE 信号,则应当检查printf()的返回值,在出错时exit()

Nagle 算法, TCP_NODELAY

  • 影响「请求-响应」型协议。
  • 如果有报文段未收到 ack,write() 就不会发送数据,避免应用层太拉发太多小数据。
  • 对于「写-写-读」模式,第二次写会被延迟一个 RTT(Round-Trip Time, 往返延迟)。
    • 解决办法:应用层缓冲,改成「写-读」模式
    • 然而难以解决并发请求问题
  • 应当考虑关闭 Nagle 算法
    • Go 语言就是这么干的

起线程还是 IO 复用

服务器应该选用「thread-per-connect」还是「IO-multiplexing」模式?

开销的根源在于切换。起线程会有切换开销,IO 复用也有系统调用的开销。

  • 若客户端不超过硬件并发数,就(几乎)没有线程切换开销。
  • 若客户端很多,则 IO 复用的系统调用开销小于线程切换开销。

netcat 实现

  1. 阻塞式,2 threads per connection
  2. IO-multiplexing