最近在上手一个QT项目的时候,在windows下用C++实现了一个服务器功能,开启50000-50008端口进行TCP监听,用来和另一个工具进行交互,服务器要求能动态创建和删除。但是在实际测试的时候发现我的服务器有的时候删不掉,即使socket已经关闭了,但是端口还是处于LISTENING的状态。

最开始的时候我的排查思路一直在线程资源没有回收掉上,下断点发现线程确实已经退出了,socket也关闭掉了,处于无效值状态了。也尝试在套接字的选项上进行了一些修改,结果都无济于事。我甚至一度开始怀疑windows下socket使用上是不是有什么隐藏的机制。
由于这个问题的出现很有规律,是客户端再向我的服务器的50001-50008端口发送消息之后,才会导致所有端口关不掉,我就将关闭服务器的代码不断地切换位置,想确认一下是到哪个位置才关闭不掉了。发现是客户端向我的服务器发送消息之后,我的服务器会开启tftpd32进程来准备传输数据,而我是通过子进程的方式创建的tftpd32进程,瞬间恍然大悟了。我将创建的方式修改为了单独进程的方式,果然可以顺利关闭掉端口了。

那整个问题就变成了 "为什么我开了服务器之后,创建一个子进程,这时候咋在主进程里面就关闭不掉服务器了?"
核心根因:Windows套接字内核引用共享机制
Windows 系统中,Socket 套接字本质是内核对象,并不是单纯的进程私有资源。 当在主进程创建 TCP 监听 Socket(50000~50008)后,通过子进程方式启动 tftpd32 时:
-
Windows 默认继承父进程所有内核句柄(Socket、文件句柄、网络句柄等);
-
TCP 监听 Socket 句柄,被 tftpd32 子进程隐性继承;
-
此时,同一个监听端口的 Socket 内核对象,同时存在两个进程引用:主进程 和 tftpd32子进程
为什么关 Socket、关线程都没用?
-
在主进程中:正常关闭 Socket 句柄、销毁服务对象、退出监听线程; 仅仅只关闭了「主进程」的 Socket 引用;
-
但 tftpd32 子进程依然持有该 Socket 内核句柄;
-
Windows 端口释放机制:只有占用端口的所有进程,全部关闭 Socket 句柄,端口才会解除 LISTENING 状态; 只要还有任意一个进程持有该监听 Socket 句柄,内核就会保留端口监听,端口永久占用、无法释放。 这就是看到的现象: 主进程 socket 已关闭、线程已销毁、变量置空,但端口依然挂在 LISTENING,删不掉服务。
为什么「改为独立进程」就正常了?
我修改了进程启动方式,脱离子进程继承机制、以独立进程启动 tftpd32:
- 关闭了句柄继承特性;
- tftpd32 进程不会继承父进程的 TCP 监听 Socket 句柄;
- 主进程关闭 Socket 时,该内核对象唯一引用被释放;
- 内核正常回收端口, LISTENING 状态解除,服务可正常销毁。
补充:QT / Windows 解决方案
-
创建子进程时,禁用句柄继承 Windows 创建子进程时,设置句柄不可继承,从根源杜绝;
-
临时屏蔽网络句柄继承
-
独立进程启动第三方工具
总结
Windows 下 Socket 是内核句柄,子进程默认继承父进程所有网络句柄;tftpd32 子进程偷占了监听端口 Socket,主进程关句柄没用,端口被内核锁定,改为独立进程、关闭句柄继承即可彻底解决。