0%

如何判断TCP连接是否可用?

面试遇到一个问题,如何判断一个TCP连接是断开了的,这个断开的意思就是意外中断了,而不是Client或者Server主动断开了。我们知道,TCP维持一个可靠的连接,当没有发起close请求时,默认都是连接的,而Client或者Server如何得知连接的状态就需要利用其他更多方法。

心跳机制

这个问题我觉得大多数人和我一样,第一反应可能就是常见的心跳检测,而心跳机制也有两种技术

应用层心跳机制

最常见的就是应用层HTTP的心跳检测,Server定时向Client发送一个小探测包(可以通过启动一个低级别的线程),根据Client的回复来判断Client是否在线;同样,Client在一段时间内如果没收到心跳包,则认为Server出问题了,连接不可用。

TCP的保活机制(KeepAlive)

应用层的心跳检测有一个问题就是,很多连接的情况下,Server的压力大。

TCP/IP协议内置了KeepAlive功能。不论是Server还是Client,一方开启KeepAlive功能后,就会自动在规定时间内向对方发送心跳包,而另一方在收到心跳包后就会自动回复,以告诉对方我仍然在线。因为开启KeepAlive功能需要消耗额外的宽带和流量,所以TCP协议层默认并不开启KeepAlive功能。而《TCP/CP详解》中也提到这个功能是非必须功能,可能一些协议栈根本没有实现这个功能,原因主要是两个:

  • 对于按流量收费的网络,增加费用开销
  • 不合理的时间设置可能导致短暂的网络波动导致断开了较好的健康连接

目前,部分TCP/IP协议栈实现的KeepAlive时长为2小时

不知道是不是心跳检测只是应用层回答,反正面试官觉得不是自己想要的答案。我也对这个问题来了兴趣,去网上查了一下资料,原来还有其他底层方法。

基于select机制

假设在Server使用多线程方式来处理每个Client的socket连接,Server不主动断开链路,也没有心跳机制来维护连接的状态,Client发送数据的时间也是不一定的。在Client的socket断开后,Server应该能够知道并且释放socket资源。那么Server可以利用select来进行连接是否可用的判断,具体如下:

  1. 设置接收的socket为异步的方式
  2. 使用select()测试一个socket是否可读
  3. 如果select返回值为1,说明socket就绪,使用recv函数读取数据,然后根据请求读取的长度和实际读取的字节数判断,操作如下:

    • 对于recv函数来说,传入socket描述符,缓冲区指针,请求读取的长度flag设置为阻塞型 MSG_WAITALL
    • 根据这个flag,内核在没有读取到足够请求长度的数据之前会一直阻塞,除非发生以下这些情况,会返回当前已经读取的字节数:(1)收到终止信号;(2)连接终止;(3)套接字发生错误。
    • recv的返回值为0,说明另一方是正常关闭的。
    • recv的返回值小于0(也就是-1), 说明客户端的连接可能已经断开,但是还需要判断errno是否为EINTR(中断信号),因为如果是因为接收到中断信号,那么recv函数也会返回,而此时socket可能还是正常的。也就是说当recv返回-1时,可能是socket出现问题,也可能是中断信号,需要进一步判断。

利用recv阻塞的读取返回也能够判断连接是否可用,而基于这套机制,最核心的问题就是:IO复用的过程中如何判断一个socket可读,或者说就绪,也就是select什么时候会返回socket就绪

描述符(socket)就绪(可读可写)的条件

当然,这个问题,被视为圣经之一的《UNIX 网络编程卷1》中的《第6章 I/O复用》早就给我们指明了光明大道。

可读条件

满足下列四个条件中的任何一个时,一个socket准备好读。

  • 该socket接收缓冲区中的数据字节数大于等于socket接收缓存区低水位。对于TCP和UDP而言,缓冲区低水位的值默认为1。那就意味着,默认情况下,只要缓冲区中有数据,那就是可读的。我们可以通过使用SO_RCVLOWAT选项(参见setsockopt函数)来设置该socket的低水位大小。此种描述符就绪(可读)的情况下,当我们使用read/recv等对该socket执行读操作的时候,socket不会阻塞,而是成功返回一个大于0的值(即可读数据的大小)
  • 该连接的读半部关闭(也就是接收了FIN的TCP连接)。对这样的socket的读操作,将不会阻塞,而是返回0(也就是EOF)。
  • 该socket是一个listen的监听socket,并且目前已经完成的连接数不为0。对这样的socket进行accept操作通常不会阻塞。
  • 有一个错误socket待处理。对这样的socket的读操作将不阻塞并返回-1(也就是返回了一个错误),同时把errno设置成确切的错误条件。这些待处理错误(pending error)也可以通过指定SO_ERRORsocket选项调用getsockopt获取并清除。

可写条件

满足下列四个条件中的任何一个时,一个socket准备好写。

  • 该socket发送缓冲区中的可用空间字节数大于等于socket发送缓存区低水位标记时,并且该socket已经成功连接(UDPsocket不需要连接)。对于TCP和UDP而言,这个低水位的值默认为2048,而socket默认的发送缓冲区大小是8k,这就意味着一般一个socket连接成功后,就是处于可写状态的。我们可以通过SO_SNDLOWATsocket选项(参见setsockopt函数)来设置这个低水位。此种情况下,我们设置该socket为非阻塞,对该socket进行写操作(如write,send等),将不阻塞,并返回一个正值(例如由传输层接受的字节数,即发送的数据大小)。
  • 该连接的写半部关闭。对这样的socket的写操作将会产生SIGPIPE信号。所以我们的网络程序基本都要自定义处理SIGPIPE信号。因为SIGPIPE信号的默认处理方式是程序退出。
  • 使用非阻塞的connectsocket已建立连接,或者connect已经以失败告终。即connect有结果了。
  • w有一个错误的socket待处理。对这样的socket的写操作将不阻塞并返回-1(也就是返回了一个错误),同时把errno设置成确切的错误条件。这些待处理的错误也可以通过指定SO_ERRORsocket选项调用getsockopt获取并清除。