网络设计准则
服务端没接受完数据就发送,可能导致死锁
如阻塞IO下的echo服务,客户端发送数据,服务端接受数据并返回,若服务端不是全部接受完再发送,而是每接受一部分(如100字节)就发送一部分,可能导致死锁。
原因是内核中TCP的发送与接受队列是有限的(一般为几兆大小),客户端发送的数据被服务端马上发送回来,当客户一次发送大量数据时,此时客户还没发送完成,故不能接受从服务端发送来的数据,只能留在内核的接收队列中,当队列已满时,服务端发送阻塞,故其接收也阻塞,故客户端的发送也阻塞,故造成死锁。
解决方法:服务端只有全部接收完客服端的数据时,再向客户端发送。那如何确定客户端一次发送的数据量?可在协议中定义header,在header中告知;或在数据后添加消息边界(如特殊字符(串)),当服务端遇到消息边界,则停止本次接收。
TCP自连接
当客户端的TCP套接字connect之前不进行bind的话,Linux会分配临时端口号(范围为37768-60999,查看/proc/sys/net/ipv4/ip_local_port_range),当这个临时端口号与服务端监听的端口号相同时,就会发送自连接。
内在原因:TCP建立连接时处理除了一方主动SYN,另一方SYN+ACK,再SYN;还有一种方式是双方同时发送SYN,想要建立连接,双方收到SYN后分别回复SYN+ACK,之后双方连接建立
解决方法:在连接建立时,判断双方地址是否相同,相同则重连。即accept后if一下
并发模型
- thread-per-connection with blocking IO
- IO-multiplexing with no blocking IO
安全地关闭TCP连接
当TCP关闭连接时直接调用close,若发送缓冲区和接收缓冲区中还有数据的话则被丢弃,设置socket选项SO_LINGER会尝试将残留在发送缓冲区的数据发送给对方,但并不保证,接收缓冲区中还是被丢弃,故SO_LINGER并不能解决这一问题
解决方法:这时需要借助shutdown(),shutdown() 会确实发送一个FIN给对方,说明对方也即将关闭 socket,此时可以通过 recv() 返回 0 (收到 EOF)检测到接受端的关闭。
正确的关闭逻辑如下,建议用这种方式代替SO_LINGER:
-
发送方:send() → shutdown(WR) → recv() == 0(由接收方 close 导致) → close()
-
接收方:recv() == 0(由发送方 shutdown 导致) → more to send? → close()
值得注意,如果遇到恶意或错误 client,永远不 close(),则服务器 recv() 不会返回 0(阻塞且 errno == EAGAIN),因此需要加一个超时控制,若 shutdown(WR) 若干秒后 recv() 未返回 0,则直接 close() 强制关闭连接。
即使如此,shutdown() 也不能保证接收方接受到所有数据,这只是发送方能做到的最大努力。最好的办法还是像 HTTP 协议那样,附有消息的长度信息,这就需要有能力自己设计协议
参考:https://ysw1912.github.io/post/network/how_to_close_tcp_connection_correctly/