客户端的socket不需要手动执行bind绑定地址,但这不意味着客户端socket真的不需要绑定端口,实际上是内核它帮我们做了这个操作,在执行connect时,内核发现没有绑定端口,就会自动选择一个合适的端口绑定到socket。

当然这不说明我们不能对客户端socket执行bind操作,对于客户端socket,依旧可以执行bind操作把socket绑定到一个地址。

为什么客户端的socket会自动分配端口呢?

  1. 客户端的socket很多,每产生一个连接,就会创建一个socket,我们无法准确得知有哪些端口没有被使用。
  2. 客户端的socket并不关心自己端口是多少,更多的关注是服务端的端口号,因此客户端socket可以任意指定,只要能够通信即可。

自动分配端口这一操作在内核中的体现在net/inet_connection_sock.c:inet_csk_get_port()

do {
    head = &hashinfo->bhash[inet_bhashfn(net, rover,
            hashinfo->bhash_size)];
    spin_lock(&head->lock);
    // 遍历所有的连接哈希表
    inet_bind_bucket_for_each(tb, node, &head->chain)
        // 端口已经被使用了
        if (ib_net(tb) == net && tb->port == rover) {
            // 判断是否开启了端口快速回收以及端口重用
            if (((tb->fastreuse > 0 &&
                sk->sk_reuse &&
                sk->sk_state != TCP_LISTEN) || 
                (tb->fastreuseport > 0 &&
                  sk->sk_reuseport &&
                  tb->fastuid == uid))&&
                (tb->num_owners < smallest_size || smallest_size == -1)) {
                // 处理端口重用逻辑
                smallest_size = tb->num_owners;
                smallest_rover = rover;
                if (atomic_read(&hashinfo->bsockets) > (high - low) + 1) {
                    spin_unlock(&head->lock);
                    snum = smallest_rover;
                    // 找到了一个合适的端口
                    goto have_snum;
                }
            }
            goto next;
        }
    break;
next:
    // 继续往下遍历端口号,直到找到一个可用的端口
    spin_unlock(&head->lock);
    if (++rover > high)
        rover = low;
} while (--remaining > 0);

就像上面所描述的,大多数场景,我们无法确切知道当前环境还有哪些端口可用,因此也无需绑定地址到socket。但是对于特定场景,还是需要给socket手动绑定地址。如代理程序对UDP代理时,要先在本地创建一个socket作为客户端发送数据的隧道,这个时候就要先指定好端口,然后再返回给客户端。

那么现在问题就来了,我怎么知道需要绑定哪个端口呢?上面也说了,我不知道有哪些可用的端口。同时,因为也没有执行connect,此时也没有得到一个随机的端口,这种场景应该怎么处理?可以采取的做法是:给socket绑定一个0端口,让系统随机分配一个。

代码中赋值add.sin_port = 0,就是告诉内核,我需要一个可用的随机的端口,请给我分配一个。然后内核就会分配了。

执行完后,再通过getsockname函数就可以得到系统分配的端口号了。示例代码:

int main() {
    int sock_fd;
    struct sockaddr_in addr;
    socklen_t socklen;

    sock_fd = socket(AF_INET, SOCK_STREAM, 0);

    addr.sin_family = AF_INET;
    addr.sin_port = 0; // 绑定随机端口
    addr.sin_addr.s_addr = INADDR_ANY;

    socklen = sizeof(addr);
    if (bind(sock_fd, (struct sockaddr *)&addr, socklen)) {
        perror("bind error");
        return -1;
    }

    // 获取系统随机分配的端口号
    getsockname(sock_fd, (void *)&addr, &socklen);
    printf("%u\n", ntohs(addr.sin_port));
    return 0;
}
最后修改:2020 年 05 月 07 日
如果觉得我的文章对你有用,请随意赞赏