如何找到一个可用端口?

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

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

VSCode的实现

比如VSCode,就是逐个连接,如果某个端口连接失败,并且错误不是ECONNREFUSED的话,那么就说明这个端口可用。

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,让操作系统分配端口:

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)。

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.