如何非阻塞地检查TCP Socket是否仍处于打开状态?
我完全懂你在usocket上遇到的这个麻烦——读取socket时要么阻塞到天荒地老,要么碰到EOF才知道连接断了,试了listen、usocket::state还有sb-bsd-sockets:socket-open-p都不管用,确实闹心。你用wait-for-input的思路其实方向是对的,只是可以优化得更严谨,另外还有几个更可靠的方案,结合SBCL的特性来解决:
1. 优化wait-for-input+listen的组合(更准确判断关闭状态)
你之前的方法不算粗糙,只是缺了一步EOF确认。当wait-for-input返回就绪但listen返回nil时,大概率是连接已经被对方关闭了,这时候可以尝试读取一个字符来确认是否触发EOF:
(defun socket-ready-or-closed-p (socket &optional (timeout 0.1)) (let ((ready-sockets (usocket:wait-for-input socket :timeout timeout :ready-only t))) (cond ((null ready-sockets) nil) ; 超时,无数据且连接正常 ((listen (usocket:socket-stream socket)) t) ; 有数据可读 (t ; 无数据但就绪,检查是否是EOF (eq (read-char (usocket:socket-stream socket) nil :eof) :eof)))))
这个方法的逻辑是:wait-for-input返回就绪意味着socket上有事件(要么有数据,要么连接关闭),如果listen没检测到数据,那读取一个字符看是不是EOF,就能准确判断连接是否关闭了。
2. 用SBCL底层接口检查SO_ERROR(最可靠的关闭检测)
sb-bsd-sockets:socket-open-p不靠谱是因为它只检查socket句柄是否存在,不管对方是否关闭连接。而底层的SO_ERROR选项可以直接获取socket的错误状态,当对方关闭连接后,这个值会被设置为非零(比如ECONNRESET或EPIPE):
(defun socket-closed-by-peer-p (socket) (let ((fd (usocket:socket-file-descriptor socket))) (handler-case (let ((error-code (sb-bsd-sockets:socket-option fd sb-bsd-sockets:so-error))) (not (zerop error-code))) (sb-bsd-sockets:socket-error () t)))) ; 获取选项出错,说明socket已无效
这个方法直接绕过上层的流抽象,查底层socket的状态,是判断连接是否被对方异常关闭最可靠的方式。
3. 设置socket为非阻塞模式(从根源避免阻塞)
如果你从一开始就不想让读取操作阻塞,可以把socket设置为非阻塞模式,这样所有读取操作都会立即返回,不会等待数据:
; 开启非阻塞模式 (usocket:set-fd-nonblocking (usocket:socket-file-descriptor socket) t) ; 非阻塞读取的示例函数 (defun read-non-blocking (stream buffer) (handler-case (read-sequence buffer stream) (sb-bsd-sockets:non-blocking-error () 0) ; 无数据可读,返回0字节 (end-of-file () :eof))) ; 连接已关闭
这种方式适合需要持续轮询socket的场景,不过要注意处理SBCL特有的sb-bsd-sockets:non-blocking-error,不同CL实现的非阻塞错误类型可能有差异,但SBCL里是这个。
4. 用usocket:with-timeout包装读取操作(简单粗暴的防阻塞)
如果你只是想避免无限阻塞,不想搞复杂的状态检查,可以直接用usocket自带的超时包装器,给读取操作设置一个超时时间,超时就抛出异常,你捕获后处理即可:
(handler-case (usocket:with-timeout (5) ; 设置5秒超时 (read-line (usocket:socket-stream socket))) (usocket:timeout-error () nil) ; 超时,无数据 (end-of-file () :eof)) ; 连接关闭
这个方法最简单,适合对精度要求不高的场景,缺点是每次读取都要设置超时,而且超时时间是固定的。
内容的提问来源于stack exchange,提问作者Andrei

