socket通信之bind函数

bind函数的原型如下:

#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>

int bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen);

使用如下:

... ...
// bind port 
struct sockaddr_in bindaddr;
bindaddr.sin_family = AF_INET;
bindaddr.sin_addr.s_addr = htonl(INADDR_ANY);
bindaddr.sin_port = htons(3000);
if(-1 == bind(listenfd, (struct sockaddr *)&bindaddr, sizeof(bindaddr))) {
	printf("bind error");
	return -1;
}
... ...

bind函数地址

bind的地址我们使用了一个宏叫INADDR_ANY,关于这个宏的解释如下:

如果应用程序不关心bind绑定的ip地址,可以使用INADDR_ANY,这样底层的(协议栈)服务会自动选择一个合适的ip地址,这样使在一个有多个网卡机器上选择ip地址问题变得简单。

也就是说INADDR_ANY相当于地址0.0.0.0。假设我们在一台机器上开发一个服务器程序,使用bind函数时,我们有多个ip地址可以选择。首先,这台机器对外访问的ip地址是120.55.94.78,这台机器在当前局域网的地址是 192.168.1.104;同时这台机器有本地回环地址127.0.0.1

如果你指向本机上可以访问,那么你bind函数中的地址就可以使用127.0.0.1(INADDR_LOOPBACK);如果你的服务只想被局域网内部机器访问,bind函数的地址可以使用192.168.1.104;如果希望这个服务可以被公网访问,你就可以使用地址0.0.0.0或INADDR_ANY。

ip地址10.0.4.129在代码中需要写成0x0a000481,将ip地址转换为一个uint32_t类型的数字。

bindaddr.sin_addr.s_addr = htonl(0x0a000481);

bind函数端口号

网络通信程序的基本逻辑是客户端连接服务器,即从客户端的地址:端口连接到服务器地址:端口上,在上面的例子中,服务器端的端口号使用3000,那客户端连接时的端口号是多少呢?TCP通信双方中一般服务器端端口号是固定的,而客户端端口号是连接发起时由操作系统随机分配的(不会分配已经被占用的端口)。端口号是一个C short类型的值,其范围是0~65535,知道这点很重要,所以我们在编写压力测试程序时,由于端口数量的限制,在某台机器上网卡地址不变的情况下压力测试程序理论上最多只能发起六万五千多个连接。注意我说的是理论上,在实际情况下,由于当时的操作系统很多端口可能已经被占用,实际可以使用的端口比这个更少,例如,一般规定端口号在1024以下的端口是保留端口,不建议用户程序使用。

如果将bind函数中的端口号设置成0,那么操作系统会随机给程序分配一个可用的侦听端口,当然服务器程序一般不会这么做,因为服务器程序是要对外服务的,必须让客户端知道确切的ip地址和端口号。

很多人觉得只有服务器程序可以调用bind函数绑定一个端口号,其实不然,在一些特殊的应用中,我们需要客户端程序以指定的端口号去连接服务器,此时我们就可以在客户端程序中调用bind函数绑定一个具体的端口。

我们用代码来实际验证一下上面所说的,为了能看到连接状态,我们将客户端和服务器关闭socket的代码注释掉,这样连接会保持一段时间。

客户端代码不绑定端口

服务器端代码如下:

#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <string.h>
#include <stdio.h>

int main() {
    
    // create socket
    int listenfd = socket(AF_INET, SOCK_STREAM, 0);
    if(-1 == listenfd) {
        printf("create socket error");
        return -1;
    }
    
    // bind port 
    struct sockaddr_in bindaddr;
    bindaddr.sin_family = AF_INET;
    bindaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    bindaddr.sin_port = htons(3000);
    if(-1 == bind(listenfd, (struct sockaddr *)&bindaddr, sizeof(bindaddr))) {
        printf("bind error");
        return -1;
    }
    
    // start listen
    if (listen(listenfd, 2) == -1) {
        printf("listem error");
        return -1;
    }
    
    while (1) {
        struct sockaddr_in clientaddr;
        socklen_t clientaddrlen = sizeof(clientaddr);
        
        // accept connection
        int clientfd = accept(listenfd, (struct sockaddr *)&clientaddr, &clientaddrlen);
    }
    
    //close socket
    close(listenfd);
    return 0;
}

客户端代码如下:

#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <string.h>
#include <stdio.h>

int main() {
    
    // create socket
    int clientfd = socket(AF_INET, SOCK_STREAM, 0);
    if(-1 == clientfd) {
        printf("create socket error");
        return -1;
    }
    
    // connect server 
    struct sockaddr_in serveraddr;
    serveraddr.sin_family = AF_INET;
    serveraddr.sin_addr.s_addr = inet_addr("127.0.0.1");;
        serveraddr.sin_port = htons(3000);
    
    if(-1 == connect(clientfd, (struct sockaddr *)&serveraddr, sizeof(serveraddr))) {
        printf("connect error");
        return -1;
    }
    
    // send data
    int ret = send(clientfd, "hello", strlen("hello"), 0);
    if (ret != strlen("hello")){
        printf("send data error");
        return -1;
    }
    
    // receive data
    char recvBuf[32] = {0};
    ret = recv(clientfd, recvBuf, 32, 0);
    if (ret > 0) {
        printf("receive data from server: %s", recvBuf);
    } else {
        printf("receive data error: %s", recvBuf);
    }
    
    sleep(30);

    return 0;
}

先启动server,再启动三个客户端。然后通过lsof命令查看当前机器上的TCP连接信息,结果如下所示:

# lsof -i -Pn
COMMAND     PID  USER   FD   TYPE DEVICE SIZE/OFF NODE NAME
server.ou 59672  root    3u  IPv4 414148      0t0  TCP *:3000 (LISTEN)
server.ou 59672  root    4u  IPv4 414149      0t0  TCP 127.0.0.1:3000->127.0.0.1:7492 (ESTABLISHED)
server.ou 59672  root    5u  IPv4 414150      0t0  TCP 127.0.0.1:3000->127.0.0.1:7493 (ESTABLISHED)
server.ou 59672  root    6u  IPv4 414155      0t0  TCP 127.0.0.1:3000->127.0.0.1:7494 (ESTABLISHED)
client.ou 59675  root    3u  IPv4 413364      0t0  TCP 127.0.0.1:7492->127.0.0.1:3000 (ESTABLISHED)
client.ou 59683  root    3u  IPv4 414154      0t0  TCP 127.0.0.1:7493->127.0.0.1:3000 (ESTABLISHED)
client.ou 59690  root    3u  IPv4 414159      0t0  TCP 127.0.0.1:7494->127.0.0.1:3000 (ESTABLISHED)

上面的结果显示,server进程(进程ID是59672)在3000端口开启侦听,有三个client进程(进程ID分别是59675、59683、59690)分别通过端口号7492、7493、7494连到server进程上的,作为客户端的一方,端口号是系统随机分配的。

客户端绑定端口号0

服务器端代码保持不变,客户端代码在connect前添加如下代码:

struct sockaddr_in bindaddr;
bindaddr.sin_family = AF_INET;
bindaddr.sin_addr.s_addr = htonl(INADDR_ANY);
// bind 0
bindaddr.sin_port = htons(0);
if (bind(clientfd, (struct sockaddr *)&bindaddr, sizeof(bindaddr)) == -1)
{
    printf("bind error");
    return -1;
}

我们再次编译客户端程序,并启动三个client进程,然后用lsof命令查看机器上的TCP连接情况,结果如下所示:

# lsof -i -Pn
COMMAND     PID  USER   FD   TYPE DEVICE SIZE/OFF NODE NAME
server.ou 60108  root    3u  IPv4 414240      0t0  TCP *:3000 (LISTEN)
server.ou 60108  root    4u  IPv4 414241      0t0  TCP 127.0.0.1:3000->127.0.0.1:7495 (ESTABLISHED)
server.ou 60108  root    5u  IPv4 414242      0t0  TCP 127.0.0.1:3000->127.0.0.1:7496 (ESTABLISHED)
server.ou 60108  root    6u  IPv4 414246      0t0  TCP 127.0.0.1:3000->127.0.0.1:7497 (ESTABLISHED)
client.ou 60111  root    3u  IPv4 413401      0t0  TCP 127.0.0.1:7495->127.0.0.1:3000 (ESTABLISHED)
client.ou 60113  root    3u  IPv4 414245      0t0  TCP 127.0.0.1:7496->127.0.0.1:3000 (ESTABLISHED)
client.ou 60115  root    3u  IPv4 414249      0t0  TCP 127.0.0.1:7497->127.0.0.1:3000 (ESTABLISHED)

通过上面的结果,我们发现三个client进程使用的端口号仍然是系统随机分配的,也就是说绑定0号端口和没有绑定效果是一样的。

客户端绑定一个固定端口

服务器端代码保持不变,客户端代码中绑定一个固定端口20000:

// bind 20000
bindaddr.sin_port = htons(20000);

再次重新编译程序,先启动一个客户端后,我们看到此时的TCP连接状态:

# lsof -i -Pn
server.ou 60412  root    3u  IPv4 414277      0t0  TCP *:3000 (LISTEN)
server.ou 60412  root    4u  IPv4 414278      0t0  TCP 127.0.0.1:3000->127.0.0.1:20000 (ESTABLISHED)
client.ou 60415  root    3u  IPv4 414279      0t0  TCP 127.0.0.1:20000->127.0.0.1:3000 (ESTABLISHED)

通过上面的结果,我们发现client进程确实使用20000号端口连接到server进程上去了。这个时候如果我们再开启一个client进程,我们猜想由于端口号20000已经被占用,新启动的client会由于调用bind函数出错而退出,我们实际验证一下:

# ./client.out 
bind error

结果确实和我们预想的一样。

另外,Linux的nc命令有个-p选项,这个选项的作用就是nc在模拟客户端程序时,可以使用指定端口号连接到服务器程序上去,实现原理相信读者也明白了。我们还是以上面的服务器程序为例,这个我们不用我们的client程序,改用 nc命令来模拟客户端:

# nc -v -p 30000 127.0.0.1 3000
Ncat: Version 7.50 ( https://nmap.org/ncat )
Ncat: Connected to 127.0.0.1:3000.
... ...

-p选项指定客户端绑定的端口号,-v选项表示输出nc命令连接的详细信息。

我们用lsof命令来验证一下我们的 nc 命令是否确实以30000端口号连接到server进程上去了。

# lsof -i -Pn
COMMAND     PID  USER   FD   TYPE DEVICE SIZE/OFF NODE NAME
server.ou 60412  root    3u  IPv4 414277      0t0  TCP *:3000 (LISTEN)
server.ou 60412  root    5u  IPv4 414280      0t0  TCP 127.0.0.1:3000->127.0.0.1:30000 (ESTABLISHED)
nc        60590  root    3u  IPv4 413493      0t0  TCP 127.0.0.1:30000->127.0.0.1:3000 (ESTABLISHED)

结果确实如我们期望的一致。