深入剖析vscode工具函数(二)端口相关实现

648 阅读6分钟

深入剖析vscode工具函数(二)端口相关实现

端口工具函数的实现

vscode当中, base/common/port.ts 只定义了一个方法:

export function randomPort(): number {
	const min = 1025;
	const max = 65535;
	return min + Math.floor((max - min) * Math.random());
}

这是一个获取随机端口号的方法。对于这个简单的实现,我们抛两个问题:

  • 为什么最小值是1025?
  • 为什么最大值是65535?

关于特权端口

最小值定为1025的原因是在大多数操作系统中,为了确保系统的安全和稳定性,特权端口(privileged ports)的端口号小于1024,这些端口号被保留给系统管理员和操作系统内核,普通用户和应用程序无法使用。以下是一些常见的特权端口:

  • 0:保留端口,不能使用。
  • 1-1023:系统保留端口,只能由特定的系统进程使用。
  • 20、21:FTP数据传输和控制连接端口。
  • 22:SSH(Secure Shell)远程登录协议端口。
  • 23:Telnet协议端口。
  • 25:SMTP(Simple Mail Transfer Protocol)邮件传输协议端口。
  • 53:DNS(Domain Name System)域名解析协议端口。
  • 67、68:DHCP(Dynamic Host Configuration Protocol)动态主机配置协议端口。
  • 80:HTTP(HyperText Transfer Protocol)Web服务端口。
  • 110:POP3(Post Office Protocol version 3)邮局协议端口。
  • 123:NTP(Network Time Protocol)网络时间协议端口。
  • 139、445:Windows文件共享服务(SMB)端口。
  • 143:IMAP(Internet Mail Access Protocol)Internet邮件访问协议端口。
  • 161、162:SNMP(Simple Network Management Protocol)简单网络管理协议端口。
  • 443:HTTPS(HTTP Secure)安全Web服务端口。
  • 465:SMTPS(Simple Mail Transfer Protocol Secure)安全邮件传输协议端口。
  • 587:SMTP客户端端口。
  • 873:rsync文件同步协议端口。
  • 993、995:IMAPS(Internet Mail Access Protocol Secure)安全Internet邮件访问协议端口。

端口号为16位

TCP/IP 协议中,端口号被定义为2个字节进行传输,因此端口的最大值为 2^16-1=65535

以下是一个 TCP 协议的定义:

Source Port            Destination Port
      2 bytes                  2 bytes
-----------------------------------------
Sequence Number         Acknowledgment Number
      4 bytes                  4 bytes
-----------------------------------------
  Data |      |U|A|P|R|S|F|     |    Window Size     |     Checksum          | Urgent Pointer |
Offset |Reserved|S|C|S|S|Y|I|            2 bytes              |          2 bytes            |         2 bytes            |
      4 bits           6 bits

UDP 的如下:

Source Port            Destination Port
      2 bytes                  2 bytes
-----------------------------------------
  Length                   Checksum
      2 bytes                  2 bytes
-----------------------------------------
              Data (optional)

查找空闲端口号

由于端口号的范围是有限的,并且某些端口号已经被其他程序占用,因此生成的随机端口号不一定是可用的。在 vscode 中,随机端口号的方法是跟着 findFreePort 一起使用的:

const portMain = await findFreePort(randomPort(), 10, 3000);

vscode 中, findFreePort 被定义在 base/node/port.ts 中:

export function findFreePort(startPort: number, giveUpAfter: number, timeout: number, stride = 1): Promise<number> {
	let done = false;

	return new Promise(resolve => {
		const timeoutHandle = setTimeout(() => {
			if (!done) {
				done = true;
				return resolve(0);
			}
		}, timeout);

		doFindFreePort(startPort, giveUpAfter, stride, (port) => {
			if (!done) {
				done = true;
				clearTimeout(timeoutHandle);
				return resolve(port);
			}
		});
	});
}

该函数接受四个参数:

  • startPort:指定开始查找的端口号。
  • giveUpAfter:指定最多尝试查找的次数。
  • timeout:指定超时时间,单位为毫秒。
  • stride:指定每次尝试的步长,默认值为 1。

该函数返回一个 Promise,当查找到可用的端口号时,Promise 将被解析为该端口号;否则,Promise 将被解析为 0。

可以看到这里的实现是对超时处理的一个包裹,具体的实现还在 doFindFreePort 中:

function doFindFreePort(startPort: number, giveUpAfter: number, stride: 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 + stride, giveUpAfter - 1, stride, 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 + stride, giveUpAfter - 1, stride, clb);
		}

		// Otherwise it means the port is free to use!
		return clb(startPort);
	});

	client.connect(startPort, '127.0.0.1');
}

这段代码其实不难理解,通过创建一个 **net.Socket**实例来检查端口是否已被占用。

具体实现方案为,该函数会创建一个名为 **client**的 **net.Socket**实例,并调用 connect 方法尝试连接指定端口。如果连接成功,则表示该端口已被占用,函数会关闭 **client**并尝试查找下一个端口号。如果连接失败,函数会判断错误码是否为 ECONNREFUSED,如果是,则表示该端口未被占用,函数会关闭 **client**并返回该端口号;否则,函数会关闭 **client**并尝试查找下一个端口号。

更快的实现版本

common/node/port.ts 中, vscode 还实现了一个快速的版本:

/**
 * Uses listen instead of connect. Is faster, but if there is another listener on 0.0.0.0 then this will take 127.0.0.1 from that listener.
 */
export function findFreePortFaster(startPort: number, giveUpAfter: number, timeout: number, hostname: string = '127.0.0.1'): Promise<number> {
	let resolved: boolean = false;
	let timeoutHandle: NodeJS.Timeout | undefined = undefined;
	let countTried: number = 1;
	const server = net.createServer({ pauseOnConnect: true });
	function doResolve(port: number, resolve: (port: number) => void) {
		if (!resolved) {
			resolved = true;
			server.removeAllListeners();
			server.close();
			if (timeoutHandle) {
				clearTimeout(timeoutHandle);
			}
			resolve(port);
		}
	}
	return new Promise<number>(resolve => {
		timeoutHandle = setTimeout(() => {
			doResolve(0, resolve);
		}, timeout);

		server.on('listening', () => {
			doResolve(startPort, resolve);
		});
		server.on('error', err => {
			if (err && ((<any>err).code === 'EADDRINUSE' || (<any>err).code === 'EACCES') && (countTried < giveUpAfter)) {
				startPort++;
				countTried++;
				server.listen(startPort, hostname);
			} else {
				doResolve(0, resolve);
			}
		});
		server.on('close', () => {
			doResolve(0, resolve);
		});
		server.listen(startPort, hostname);
	});
}

这个版本和 findFreePort 的主要区别是利用 net.createServerlisten 来判断端口是否占用,而非真正建立一个 Socket 连接,这样性能会更优一些。

实际上目前的主流开源库也是采用 createServer 的方案来实现。

业界主流方案实现

对于查找空闲端口在 Node 生态中主要有两个用的较多的实现:

它们都是基于 createServer 监听来检测端口是否占用的,相对来说**get-port** 会更加轻量一些, vscode 的实现更加偏向于 get-port ,具体可以参考它们的 github 源代码。

小结

本文详细说明了在vscode中实现端口相关功能的方法,并解释了生成随机端口号和查找空闲端口号的功能实现。同时还介绍了端口号范围和特权端口,这些端口号被保留给特定的系统进程。

vscode中查找空闲端口号的实现是通过findFreePort函数实现的,该函数需要四个参数:startPortgiveUpAftertimeoutstride。函数使用一个net.Socket实例来检查指定的端口是否已经被占用。如果连接尝试成功,函数将关闭套接字并尝试下一个端口号。如果连接尝试失败,函数将检查ECONNREFUSED错误码,表示该端口可以使用,然后返回该端口号。

vscode还实现了更快的版本。它使用net.createServer来监听连接,而不是net.Socket实例。这种实现速度更快,也是目前业界主流库的实现方案。

最后,介绍了在Node.js生态系统中广泛使用的两个实现,portfinderget-port,它们都使用createServer来检测端口是否被占用。

总体而言,查找端口是我们在日常 node 开发中可能会经常遇到的问题,背后的实现原理也较为简单,了解这些知识可以让我们更深刻地理解背后的运作原理,以便于更高效地应用在实际生产当中。