tcp使用的进一步了解

以前对socket的了解仅仅局限于listen/connect/epoll/select/close等这些API的表面使用。其具体语义以及一些状态都没有深究。总觉得这样写代码会出问题,今天咬咬牙把《tcp协议卷1》中的Tcp部分又看了一遍。发现由于对协议和API语义的了解不足,在程序中还是犯了不少错误。

TIME_WAIT状态是tcp网络编程中最不容易理解的地方。主动关闭的一方会经历TIME_WAIT状态, 这个状态会经历2MSL的时间。其目的就是为了可靠的实现TCP全双工连接的终止和允许老的重复分组消逝在网络中(具体可以参看UNP P37)。

TIME_WAIT的持续时间一般是1分钟到4分钟,如果在这段时间内涌入大量连接,然后服务器将其断开,就会导致在服务器上残留大量处理TIME_WAIT的连接,理论上这会严重拖慢系统的性能。

因此,只要条件允许,应该尽量让客户端来主动断开连接。

由于服务器绑定的是固定端口,当重启服务器时,只要还存在有通过这个固定端口接入进来的TIME_WAIT状态的连接,就会导致bind失败。因此服务器一定要为bind的socket设置SO_REUSEADDR属性。


close的默认行为首先对描述符引用计数减一,如果引用计数为为0则执行close流程。即把该socket标记为关闭, 然后立即返回到调用进程,该socket不能再被调用进程使用。然而tcp将尝试发送已经排队等待发送到对端的任何数据,发送完闭后才会执行正常的tcp连接终止序列(即四次挥手). 这个行为可以通过SO_LINGER更改。

相比close, shutdown的行为则更为粗爆一些。

不管有多少进程在持有这个socket,一旦这个socket被shutdown, 那么所有的进程均不能再对此socket进行已经shutdown过的操作。shutdown可以分别关闭读和写。

shutdown写时会将当前发送缓冲区中的数据都发送掉才会进行连接终止序列。
shutdown读时会将当前接收缓冲区中的数据都请空。

在silly的实现中,我对于close的实现仅仅是粗爆的将所有发送缓冲区清空,然后关闭socket。对比系统的close和shutdown函数可以发现, 这样做是不对的,因为在应用层调用close时,有可能数据并没有发完, 这样就会有可能导致客户端接收到的信息不完成,而造成其他bug.

tcp的close和shutdown

今天看到”Unix网络编程”第六章中半关闭连接, 对close和shutdown的功能区别产生了疑惑. 代码运行情况如下:

情况1:C(client)与S(server)建立链接之后, 当C向S发送数据之后调用shutdown来关闭写操作(断开链接的四次挥手中的前两次)告诉S, C端已发送数据完成, 此时S依然可向C发送数据.
情况2:C(client)与S(server)建议链接之后, 当C向S发送数据之后调用close来关闭socket(同样发送断开链接的四次挥手的前两次, 后两次挥手将由S端调用close来完成), 此时S端被其他条件阻赛并不调用close函数. 然后此时S端向C端发送数据将会引起C端回应rst数据包.

那么此时,情况1与情况2的socket状态机走到相同的地方, 所作的回应却完全不一样, 行为很令人奇怪.

在网上看了众多blog多后, 比较发现, close除了相当于C和S各发了shutdown之外, 还多了一个释放socket file descriptor的操作. 因此,猜测当调用close之后函数应该是瞬间返回的, 剩余的四次挥手以及TIME_WAIT状态应该是靠kernel来实现的, 那么就容易解释上述情况的原因了.

情况1:虽然调用了shutdown, 但是C依然拥有这个socket file descriptor, 因此算是半关闭链接, 情况2在调用close之后, C所拥有的socket file desciptor即被归还与kernel, 那么剩余的四次挥手及TIME_WAIT即由kernel来完成, 当此时S向C发送数据之后, kernel发现此socket file descriptor已经被释放, 即向S返回rst数据包.

以上均为猜测, 如找到相关文档再进行修正.

tcp的端口号

虽然写了一年多的网络编程, 但是总觉得有种浮沙筑高台的感觉,于是去买了本unix网络编程卷一去读. 果不其然看到第五章使用netstat去调试第一个程序时就产生了一个疑惑.

这个例子每accept一个连接就去fork一个进程去处理, 当我开了一个server和2个client时发现, server一共会有三个进程存在, 一个是listen, 另外两个是accpet出来的, 但是使用netstat去调试时却发现这三个进程其实共享server进程的bind的端口,以前只是知道”端口是用来标识同一个IP的不同进程之间数据收发”, 却从没有深究其中原理. 而之前在windows写socket程序一般都是多线程, 多进程的方案从来没用过, 所以从未觉得这句话有什么不妥.

可能这个问题太简单了,google了半天都没找到有人解答, 最终在tcp/ip详解卷一的第18章P194上找到这样一句话”tcp使用由本机地址和远端地址组成的4元组:目的IP地址, 目的端口号, 源IP地址和源端口号来处于是传入的多个链接请求”, 于是一下豁然开朗, 找到tcp的协议头翻了一下, 果然其实每个tcp segment都是有这个四元素的. 那其实就问题就很明显了, 根本不是进程与端口的问题, 其实应该是说”端口和IP地址(源端口, 目的端口, 源IP和目的IP)是用来标识不同socket之间的数据收发”.

再仔细回忆一下server端的完整步骤:
socket //获取一个socket descriptor
bind //将刚获取socket descriptor绑定相应的地址或port(一般地址为INADDR_ANY)
listen //向kernel标识接收向该socket上的连接请求
while accept //接收client的请求


大致猜测一下kernel的方式, 当收到tcp segment时首先去查看tcp segment中的目的端口并找到与目的端口相关的socket, 然后如果发现SYN此类与连接有关的segment的时候直接转发给被listen的那个socket, 否则就根据tcp segment的源端口号和源IP地址去匹配相应的socket进行数据收发.