测来测去1-内核中的随机端口选取

375 阅读1分钟

connect和bind0

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

随机端口选取测试

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

#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调用,作作为监听端口:

#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