网络编程实战
前方施工中
参考资料
如何保证可靠的 TCP 连接
发送比接收更难
- 按难度排序:
- 服务器建立 TCP < 客户端建立 TCP < 销毁 TCP
- 接收 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 浪费。 然而在网络编程中,若 Client 已经关闭,则 Server 同样会收到 SIGPIPE 信号。
1
gunzip -c huge.log.gz | grep ERROR | head
- 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 实现
- 阻塞式,2 threads per connection
- IO-multiplexing