很多场景中,我们确实需要找一个空闲端口,比如启动一个子进程监听指定端口,然后通过这个端口与之通信
然后实现方式就有很多了:
VSCode的实现 比如VSCode ,就是逐个连接,如果某个端口连接失败,并且错误不是ECONNREFUSED的话,那么就说明这个端口可用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 function doFindFreePort (startPort : number , giveUpAfter : number , clb : (port: number ) => void ): void { if (giveUpAfter === 0 ) { return clb (0 ); } const client = new net.Socket (); client.once ('connect' , () => { dispose (client); return doFindFreePort (startPort + 1 , giveUpAfter - 1 , clb); }); client.once ('data' , () => { }); client.once ('error' , (err : Error & { code?: string } ) => { dispose (client); if (err.code !== 'ECONNREFUSED' ) { return doFindFreePort (startPort + 1 , giveUpAfter - 1 , clb); } return clb (startPort); }); client.connect (startPort, '127.0.0.1' ); }
这种方式完全混淆了“端口可以被监听”和“端口不能连接”这两个语义,虽然现在Linux上述代码能够正常工作,但是保不准哪天新添加一个类似ECONNREFUSED这样的错误码呢。
vscode-mono-debug的实现 然后继续找,发现vscode-mono-debug的实现 :通过不指定端口的方式listen,让操作系统分配端口:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public static int FindFreePort (int fallback) { TcpListener l = null ; try { l = new TcpListener (IPAddress .Loopback , 0 ); l.Start (); return ((IPEndPoint )l.LocalEndpoint ).Port ; } catch (Exception ) { } finally { l.Stop (); } return fallback; }
这种方式好了一点,但是比人调用这个函数是为了拿到端口去监听,而这个函数并不会保证这个端口不被别人占用,所以这个实现还是有一点问题。
最终解决方案 目前来看,操作系统内核(至少Linux)没有提供分配并预留端口的机制,所以得到空闲端口(或者叫可用端口)是不现实的。
目前,正确的做法就是:把取端口这件事情交给使用端口的单位。
比如,开头提到的“启动一个子进程监听指定端口,然后通过这个端口与之通信”,那就应该子进程通过不指定端口listen的方式,让操作系统分配端口;并将这个端口告诉父进程(比如通过stdout)。