如何找到一个可用端口?

很多场景中,我们确实需要找一个空闲端口,比如启动一个子进程监听指定端口,然后通过这个端口与之通信

然后实现方式就有很多了:

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();

// If we can connect to the port it means the port is already taken so we continue searching
client.once('connect', () => {
dispose(client);

return doFindFreePort(startPort + 1, giveUpAfter - 1, clb);
});

client.once('data', () => {
// this listener is required since node.js 8.x
});

client.once('error', (err: Error & { code?: string }) => {
dispose(client);

// If we receive any non ECONNREFUSED error, it means the port is used but we cannot connect
if (err.code !== 'ECONNREFUSED') {
return doFindFreePort(startPort + 1, giveUpAfter - 1, clb);
}

// Otherwise it means the port is free to use!
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) {
// ignore
}
finally {
l.Stop();
}
return fallback;
}

这种方式好了一点,但是比人调用这个函数是为了拿到端口去监听,而这个函数并不会保证这个端口不被别人占用,所以这个实现还是有一点问题。

最终解决方案

目前来看,操作系统内核(至少Linux)没有提供分配并预留端口的机制,所以得到空闲端口(或者叫可用端口)是不现实的。

目前,正确的做法就是:把取端口这件事情交给使用端口的单位。

比如,开头提到的“启动一个子进程监听指定端口,然后通过这个端口与之通信”,那就应该子进程通过不指定端口listen的方式,让操作系统分配端口;并将这个端口告诉父进程(比如通过stdout)。

作者

Robert Lu

发布于

2019-05-04

许可协议

评论