以下描述主要是针对windows平台下的TCP socket而言。
首先需要区分一下关闭socket和关闭TCP连接的区别,关闭TCP连接是指TCP协议层的东西,就是两个TCP端之间交换了一些协议包(FIN,RST等),具体的交换过程可以看TCP协议,这里不详细描述了。而关闭socket是指关闭用户应用程序中的socket句柄,释放相关资源。但是当用户关闭socket句柄时会隐含的触发TCP连接的关闭过程。
TCP连接的关闭过程有两种,一种是优雅关闭(graceful close),一种是强制关闭(hard close或abortive close)。所谓优雅关闭是指,如果发送缓存中还有数据未发出则其发出去,并且收到所有数据的ACK之后,发送FIN包,开始关闭过程。而强制关闭是指如果缓存中还有数据,则这些数据都将被丢弃,然后发送RST包,直接重置TCP连接。
下面说一下shutdown及closesocket函数。
shutdown函数的原型是:
int shutdown(
SOCKET s,
int how
);
该函数用于关闭TCP连接,但并不关闭socket句柄。其第二个参数可以取三个值:SD_RECEIVE,SD_SEND,SD_BOTH。
SD_RECEIVE表明关闭接收通道,在该socket上不能再接收数据,如果当前接收缓存中仍有未取出数据或者以后再有数据到达,则TCP会向发送端发送RST包,将连接重置。
SD_SEND表明关闭发送通道,TCP会将发送缓存中的数据都发送完毕并在收到所有数据的ACK后向对端发送FIN包,表明本端没有更多数据发送。这个是一个优雅关闭过程。
SD_BOTH则表示同时关闭接收通道和发送通道。
closesocket函数的原型是:
int closesocket(
SOCKET s
);
该函数用于关闭socket句柄,并释放相关资源。前面说过,关闭socket句柄时会隐含触发TCP连接的关闭过程,那么closesocket触发的是一个优雅关闭过程还是强制关闭过程呢?
这个与一个socket选项有关:SO_LINGER 选项,该选项的设置值决定了closesocket的行为。该选项的参数值是linger结构,其定义是:
typedef struct linger {
u_short l_onoff;
u_short l_linger;
} linger;
当setsockopt函数设置了SO_LINGER时,有下列三种情况:
1、设置 l_onoff为0,则该选项关闭,l_linger的值被忽略,等于内核缺省情况,close调用会立即返回给调用者,如果可能将会传输任何未发送的数据;
2、设置 l_onoff为非0,l_linger为0,则套接口关闭时TCP夭折连接,TCP将丢弃保留在套接口发送缓冲区中的任何数据并发送一个RST给对方,而不是通常的四分组终止序列,这避免了TIME_WAIT状态;
3、设置 l_onoff 为非0,l_linger为非0,当套接口关闭时内核将拖延一段时间(由l_linger决定)。如果套接口缓冲区中仍残留数据,进程将处于睡眠状态,直 到(a)所有数据发送完且被对方确认,之后进行正常的终止序列(描述字访问计数为0)或(b)延迟时间到。此种情况下,应用程序检查close的返回值是非常重要的,如果在数据发送完并被确认前时间到,close将返回EWOULDBLOCK错误且套接口发送缓冲区中的任何数据都丢失。close的成功返回仅告诉我们发送的数据(和FIN)已由对方TCP确认,它并不能告诉我们对方应用进程是否已读了数据。如果套接口设为非阻塞的,它将不等待close完成。
msdn中还有一条提醒:不推荐在非阻塞套接字中使用SO_LINGER
SO_DONTLINGER 选项为TRUE或FALSE表示禁用SO_LINGER或不禁用SO_LINGER,
如果禁用(即选项为TRUE)即相当于上面的情况1
注意SO_LINGER和SO_DONTLINGER选项只影响closesocket的行为,而与shutdown函数无关,shutdown总是会立即返回的。
所以建议的最好的关闭方式是这样的:
发送完了所有数据后:
(1)调用shutdown(s, SD_SEND),如果本端同时也接收数据时则执行第二步,否则跳到第4步。
(2)继续接收数据,
(3)收到FD_CLOSE事件后,调用recv函数直到recv返回0或-1(保证收到所有数据),
(4)调用closesocket,关闭socket句柄。
在实际编程中,我们经常也不调用shutdown,而是直接调用closesocket,利用closesocket隐含触发TCP连接关闭过程的特性。此时的过程就是:
当发送完所有数据后:
(1)如果本端同时也接受数据则执行第二步,否则跳到第4步。
(2)继续接收数据,
(3)收到FD_CLOSE事件后,调用recv函数直到recv返回0或-1(保证收到所有数据),
(4)调用closesocket,关闭socket句柄。
但是此时为了保证数据不丢失,则需要设置SO_DONTLINGER选项,不过windows平台下这个也是默认设置。
经过实验发现,发送端应用程序即便是异常退出或被kill掉进程,操作系统也不会丢弃发送缓冲区中的未发送数据,而是会在后台将这些数据发送出去。但是这是在socket的发送缓存不为0的前提下,当socket的发送缓存设置为0(通过SO_SNDBUF选项)时比较特殊,此时不论socket是否是阻塞的,send函数都会被阻塞直到传入的用户缓存中的数据都被发送出去并被确认,因为此时在驱动层没有分配缓存存放用户数据,而是直接使用的应用层的用户缓存,所以必须阻塞直到数据都发出,否则可能会造成系统崩溃。
另外,如果是接收端的应用程序异常退出或被kill掉进程,并且接收缓存中还有数据没有取出的话,那么接收端的TCP会向发送端发送RST包,重置连接,因为后续数据已经无法被提交应用层了。
最后这里说一个感觉是windows的bug,就是做这样的一个测试:
在一端线listen一个socket,然后在另一端connect,connect成功后,listen端会检测到网络事件触发,在listen端accept之前,将connect端kill掉,然后继续运行listen端,listen端任然会accept成功,且在accept出来的socket发送数据也能成功。发送完之后在等网络事件,此时又会等待成功,但是调用WSAEnumNetworkEvents得出的事件标识却是0。之后再也不会等到网络事件。