内核中的随机端口选取

本文阅读量 Posted by Kird on 2021-02-07

connect和bind0

socket通信中,客户端需要选取随机端口和服务端进行连接。通常情况下,使用相关的系统调用即可,相关函数中会实现选取随机端口的逻辑。比如TCP使用connect()来连接,UDP使用sendto()来连接。另一种方法是使用bind(0)进行随机选择端口。

随机端口选取测试

getRandomPortConnectTest.c,使用connect系统调用:

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
33
34
35
36
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>

void print_local_port() {
int sockfd;
if (sockfd = socket(AF_INET, SOCK_STREAM, 0), -1 == sockfd) {
perror("socket create error");
}
const struct sockaddr_in remote_addr = {
.sin_family = AF_INET,
.sin_port = htons(22),
.sin_addr = htonl(INADDR_ANY)
};
if (connect(sockfd, (const struct sockaddr *) &remote_addr, sizeof(remote_addr)) < 0) {
perror("connect error");
}
// 获取本地套接字地址
const struct sockaddr_in local_addr;
socklen_t local_addr_len = sizeof(local_addr);
if (getsockname(sockfd, (struct sockaddr *) &local_addr, &local_addr_len) < 0) {
perror("getsockname error");
}
printf("local port: %d\n", ntohs(local_addr.sin_port));
close(sockfd);
}

int main() {
int i;
for (i = 0; i < 20; i++) {
print_local_port();
}
return 0;
}

getRandomPortBindTest.c,使用bind,端口写0,进行随机选取端口。测试中,bind选取端口绑定后,再进行listen调用,作作为监听端口:

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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>

int create_and_bind0_and_listen() {
int sockfd;
if (sockfd = socket(AF_INET, SOCK_STREAM, 0), -1 == sockfd) {
perror("socket create error");
}
const struct sockaddr_in serv_addr = {
.sin_family = AF_INET,
.sin_port = htons(0),
.sin_addr = htonl(INADDR_ANY)
};

if (bind(sockfd, (struct sockaddr *) &serv_addr, sizeof(serv_addr)) < 0) {
perror("bind error");
}

const struct sockaddr_in server_addr;
socklen_t server_addr_len = sizeof(server_addr);
if (getsockname(sockfd, (struct sockaddr *) &server_addr, &server_addr_len) < 0) {
perror("getsockname error");
}

printf("bind server port: %d\n", ntohs(server_addr.sin_port));
if (listen(sockfd, 88) < 0) {
perror("listen failed");
exit(1);
}
return sockfd;
}

int main() {
int fd_list[10]={0};
int i;
for (i = 0; i < 10; i++) {
int sfd=create_and_bind0_and_listen();
fd_list[i]=sfd;
}
printf("socket均监听完成,10s后关闭...\n");
sleep(10);
for (i = 0; i < 10; i++) {
close(fd_list[i]);
}
return 0;
}

测试结果

image-20210207141257966

可见connect选取的随机端口为连续的偶数端口,bind0随机选取的端口则为奇数

4.2内核的相关改变

patch地址:kernel/git/torvalds/linux.git - Linux kernel source tree

image-20210207141516713

提出patch的原因可以简述为:

local port范围太小是内核中一直存在的问题,当使用connect系统调用默认使用连续的随机端口后,高并发场景下,如果再使用bind(0)进行随机端口绑定,bind可能会fail,即使success,也需要花大量时间来从前往后寻找可用的随机端口。

主要修改如下:

image-20210207141851293

其中,offset &= ~1能保证为偶数。

具体内核代码如下:

sys_bind最终调用到inet_csk_find_open_port,保证获取的源端口是奇数:

image-20210207152559343

connect最终调用__inet_hash_connect保证源端口是偶数:

image-20210207153337794



支付宝打赏 微信打赏

赞赏支持一下