深入剖析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.createServer 的 listen 来判断端口是否占用,而非真正建立一个 Socket 连接,这样性能会更优一些。
实际上目前的主流开源库也是采用 createServer 的方案来实现。
业界主流方案实现
对于查找空闲端口在 Node 生态中主要有两个用的较多的实现:
portfinder: www.npmjs.com/package/por…get-port: www.npmjs.com/package/get…
它们都是基于 createServer 监听来检测端口是否占用的,相对来说**get-port** 会更加轻量一些, vscode 的实现更加偏向于 get-port ,具体可以参考它们的 github 源代码。
小结
本文详细说明了在vscode中实现端口相关功能的方法,并解释了生成随机端口号和查找空闲端口号的功能实现。同时还介绍了端口号范围和特权端口,这些端口号被保留给特定的系统进程。
在vscode中查找空闲端口号的实现是通过findFreePort函数实现的,该函数需要四个参数:startPort、giveUpAfter、timeout和stride。函数使用一个net.Socket实例来检查指定的端口是否已经被占用。如果连接尝试成功,函数将关闭套接字并尝试下一个端口号。如果连接尝试失败,函数将检查ECONNREFUSED错误码,表示该端口可以使用,然后返回该端口号。
vscode还实现了更快的版本。它使用net.createServer来监听连接,而不是net.Socket实例。这种实现速度更快,也是目前业界主流库的实现方案。
最后,介绍了在Node.js生态系统中广泛使用的两个实现,portfinder和get-port,它们都使用createServer来检测端口是否被占用。
总体而言,查找端口是我们在日常 node 开发中可能会经常遇到的问题,背后的实现原理也较为简单,了解这些知识可以让我们更深刻地理解背后的运作原理,以便于更高效地应用在实际生产当中。