已设置SO_REUSEADDR并关闭Socket,为何仍出现Bind地址占用错误?
设置SO_REUSEADDR后仍遇EADDRINUSE:线程中select持有Socket句柄的问题
问题重现
你的代码逻辑是创建带SO_REUSEADDR的Socket监听端口,关闭后尝试重新绑定同端口,却触发以下错误:
`bind': Address already in use - bind(2) for 0.0.0.0:9000 (Errno::EADDRINUSE)
核心原因
问题出在run方法里的子线程IO.select调用:
- 第一个
ServerWrapper实例启动的子线程中,IO.select [@sock]会持续阻塞并持有Socket对象的引用。 - 主线程调用
@a.close关闭Socket后,子线程的select仍在阻塞,Socket对象的引用并未完全释放(子线程还在占用它)。在类UNIX系统中,Socket作为文件描述符,只有当所有引用它的IO对象都被销毁后,内核才会彻底回收端口资源。 SO_REUSEADDR的作用是允许复用处于TIME_WAIT状态的端口,或者允许多个进程/线程绑定同端口(用于多进程监听),但它无法解决“仍被活跃线程引用的Socket”占用端口的问题。
解决方案
要解决这个问题,必须让子线程在Socket关闭后及时退出,释放Socket引用。以下两种方法都可行:
方法1:捕获Socket关闭时的异常退出线程
修改run方法中的子线程代码,捕获EBADF异常(Socket关闭后select会触发此错误):
class ServerWrapper def run @sock = Socket.new(Socket::AF_INET, Socket::SOCK_STREAM) @sock.setsockopt(Socket::SOL_SOCKET, Socket::SO_REUSEADDR, 1) @sock.bind(Socket.sockaddr_in(9000, '0.0.0.0')) @sock.listen(1024) Thread.new do begin IO.select [@sock], nil, nil, nil rescue Errno::EBADF # Socket已关闭,线程正常退出 end end.join end def close @sock.close end end
调用close后,select会触发异常,子线程退出,Socket的所有引用被释放,内核回收端口,第二次绑定即可成功。
方法2:添加停止标志实现优雅退出
给ServerWrapper添加停止标志,让select通过超时定期检查标志:
class ServerWrapper def initialize @stop = false end def run @sock = Socket.new(Socket::AF_INET, Socket::SOCK_STREAM) @sock.setsockopt(Socket::SOL_SOCKET, Socket::SO_REUSEADDR, 1) @sock.bind(Socket.sockaddr_in(9000, '0.0.0.0')) @sock.listen(1024) Thread.new do until @stop # 每1秒检查一次停止状态 ready = IO.select([@sock], nil, nil, 1) break if @stop # 此处可添加连接处理逻辑 end end.join end def close @stop = true @sock.close end end
这种方式更灵活,适合需要优雅关闭服务的场景,线程会在下一次检查标志时退出,释放Socket资源。
内容的提问来源于stack exchange,提问作者eskaes
相关产品推荐
相关产品推荐

