天天看點

bind函數重難點分析

struct sockaddr_in bindaddr;
bindaddr.sin_family = AF_INET;
bindaddr.sin_addr.s_addr = htonl(INADDR_ANY);
bindaddr.sin_port = htons(3000);
if (bind(listenfd, (struct sockaddr *)&bindaddr, sizeof(bindaddr)) == -1)
{
std::cout << "bind listen socket error." << std::endl;
return -1;
}

           

其中 bind 的位址我們使用了一個宏叫 INADDR_ANY ,關于這個宏的解釋如下:

If an application does not care what local address is assigned, specify the constant value INADDR_ANY for an IPv4 local address or the constant value in6addr_any for an IPv6 local address in the sa_data member of the name parameter. This allows the underlying service provider to use any appropriate network address, potentially simplifying application programming in the presence of multihomed hosts (that is, hosts that have more than one network interface and address).


           

意譯一下:

如果應用程式不關心bind綁定的ip位址,可以使用INADDR_ANY(如果是IPv6,則對應in6addr_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; 如果你的服務隻想被區域網路内部機器通路,bind 函數的位址可以使用192.168.1.104;如果希望這個服務可以被公網通路,你就可以使用位址0.0.0.0 ** 或 **INADDR_ANY。

BIND 函數端口号問題

網絡通信程式的基本邏輯是用戶端連接配接伺服器,即從用戶端的位址:端口連接配接到伺服器位址:端口上,以 4.2 小節中的示例程式為例,伺服器端的端口号使用 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 <unistd.h>
#include <iostream>
#include <string.h>
#include <vector>

int main(int argc, char* argv[])
{
    //1.建立一個偵聽socket
    int listenfd = socket(AF_INET, SOCK_STREAM, 0);
    if (listenfd == -1)
    {
        std::cout << "create listen socket error." << std::endl;
        return -1;
    }

    //2.初始化伺服器位址
    struct sockaddr_in bindaddr;
    bindaddr.sin_family = AF_INET;
    bindaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    bindaddr.sin_port = htons(3000);
    if (bind(listenfd, (struct sockaddr *)&bindaddr, sizeof(bindaddr)) == -1)
    {
        std::cout << "bind listen socket error." << std::endl;
        return -1;
    }

	//3.啟動偵聽
    if (listen(listenfd, SOMAXCONN) == -1)
    {
        std::cout << "listen error." << std::endl;
        return -1;
    }
	
	//記錄所有用戶端連接配接的容器
	std::vector<int> clientfds;
    while (true)
    {
        struct sockaddr_in clientaddr;
        socklen_t clientaddrlen = sizeof(clientaddr);
		//4. 接受用戶端連接配接
        int clientfd = accept(listenfd, (struct sockaddr *)&clientaddr, &clientaddrlen);
        if (clientfd != -1)
        {         	
			char recvBuf[32] = {0};
			//5. 從用戶端接受資料
			int ret = recv(clientfd, recvBuf, 32, 0);
			if (ret > 0) 
			{
				std::cout << "recv data from client, data: " << recvBuf << std::endl;
				//6. 将收到的資料原封不動地發給用戶端
				ret = send(clientfd, recvBuf, strlen(recvBuf), 0);
				if (ret != strlen(recvBuf))
					std::cout << "send data error." << std::endl;
				
				std::cout << "send data to client successfully, data: " << recvBuf << std::endl;
			} 
			else 
			{
				std::cout << "recv data error." << std::endl;
			}
			
			//close(clientfd);
			clientfds.push_back(clientfd);
        }
    }
	
	//7.關閉偵聽socket
	close(listenfd);

    return 0;
}
           

修改後的用戶端代碼如下:

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

#define SERVER_ADDRESS "127.0.0.1"
#define SERVER_PORT     3000
#define SEND_DATA       "helloworld"

int main(int argc, char* argv[])
{
    //1.建立一個socket
    int clientfd = socket(AF_INET, SOCK_STREAM, 0);
    if (clientfd == -1)
    {
        std::cout << "create client socket error." << std::endl;
        return -1;
    }

    //2.連接配接伺服器
    struct sockaddr_in serveraddr;
    serveraddr.sin_family = AF_INET;
    serveraddr.sin_addr.s_addr = inet_addr(SERVER_ADDRESS);
    serveraddr.sin_port = htons(SERVER_PORT);
    if (connect(clientfd, (struct sockaddr *)&serveraddr, sizeof(serveraddr)) == -1)
    {
        std::cout << "connect socket error." << std::endl;
        return -1;
    }

	//3. 向伺服器發送資料
	int ret = send(clientfd, SEND_DATA, strlen(SEND_DATA), 0);
	if (ret != strlen(SEND_DATA))
	{
		std::cout << "send data error." << std::endl;
		return -1;
	}
	
	std::cout << "send data successfully, data: " << SEND_DATA << std::endl;
	
	//4. 從伺服器收取資料
	char recvBuf[32] = {0};
	ret = recv(clientfd, recvBuf, 32, 0);
	if (ret > 0) 
	{
		std::cout << "recv data successfully, data: " << recvBuf << std::endl;
	} 
	else 
	{
		std::cout << "recv data error, data: " << recvBuf << std::endl;
	}
	
	//5. 關閉socket
	//close(clientfd);
	//這裡僅僅是為了讓用戶端程式不退出
	while (true) 
	{
		sleep(3);
	}

    return 0;
}
           

将程式編譯好後(編譯方法和上文一樣),我們先啟動server,再啟動三個用戶端。然後通過 lsof 指令檢視目前機器上的 TCP 連接配接資訊,為了更清楚地顯示結果,已經将不相關的連接配接資訊去掉了,結果如下所示:

[[email protected] ~]# lsof -i -Pn
COMMAND   PID USER   FD   TYPE DEVICE SIZE/OFF NODE NAME
server   1445 root    3u  IPv4  21568      0t0  TCP *:3000 (LISTEN)
server   1445 root    4u  IPv4  21569      0t0  TCP 127.0.0.1:3000->127.0.0.1:40818 (ESTABLISHED)
server   1445 root    5u  IPv4  21570      0t0  TCP 127.0.0.1:3000->127.0.0.1:40820 (ESTABLISHED)
server   1445 root    6u  IPv4  21038      0t0  TCP 127.0.0.1:3000->127.0.0.1:40822 (ESTABLISHED)
client   1447 root    3u  IPv4  21037      0t0  TCP 127.0.0.1:40818->127.0.0.1:3000 (ESTABLISHED)
client   1448 root    3u  IPv4  21571      0t0  TCP 127.0.0.1:40820->127.0.0.1:3000 (ESTABLISHED)
client   1449 root    3u  IPv4  21572      0t0  TCP 127.0.0.1:40822->127.0.0.1:3000 (ESTABLISHED)

           

上面的結果顯示,server 程序(程序 ID 是 1445)在 3000 端口開啟偵聽,有三個 client 程序(程序 ID 分别是1447、1448、1449)分别通過端口号 40818、40820、40822 連到 server 程序上的,作為用戶端的一方,端口号是系統随機配置設定的。

  • 情形二:用戶端綁定端口号 0

    伺服器端代碼保持不變,我們修改下用戶端代碼:

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

#define SERVER_ADDRESS "127.0.0.1"
#define SERVER_PORT     3000
#define SEND_DATA       "helloworld"

int main(int argc, char* argv[])
{
    //1.建立一個socket
    int clientfd = socket(AF_INET, SOCK_STREAM, 0);
    if (clientfd == -1)
    {
        std::cout << "create client socket error." << std::endl;
        return -1;
    }
	
    struct sockaddr_in bindaddr;
    bindaddr.sin_family = AF_INET;
    bindaddr.sin_addr.s_addr = htonl(INADDR_ANY);
	//将socket綁定到0号端口上去
    bindaddr.sin_port = htons(0);
    if (bind(clientfd, (struct sockaddr *)&bindaddr, sizeof(bindaddr)) == -1)
    {
        std::cout << "bind socket error." << std::endl;
        return -1;
    }

    //2.連接配接伺服器
    struct sockaddr_in serveraddr;
    serveraddr.sin_family = AF_INET;
    serveraddr.sin_addr.s_addr = inet_addr(SERVER_ADDRESS);
    serveraddr.sin_port = htons(SERVER_PORT);
    if (connect(clientfd, (struct sockaddr *)&serveraddr, sizeof(serveraddr)) == -1)
    {
        std::cout << "connect socket error." << std::endl;
        return -1;
    }

	//3. 向伺服器發送資料
	int ret = send(clientfd, SEND_DATA, strlen(SEND_DATA), 0);
	if (ret != strlen(SEND_DATA))
	{
		std::cout << "send data error." << std::endl;
		return -1;
	}
	
	std::cout << "send data successfully, data: " << SEND_DATA << std::endl;
	
	//4. 從伺服器收取資料
	char recvBuf[32] = {0};
	ret = recv(clientfd, recvBuf, 32, 0);
	if (ret > 0) 
	{
		std::cout << "recv data successfully, data: " << recvBuf << std::endl;
	} 
	else 
	{
		std::cout << "recv data error, data: " << recvBuf << std::endl;
	}
	
	//5. 關閉socket
	//close(clientfd);
	//這裡僅僅是為了讓用戶端程式不退出
	while (true) 
	{
		sleep(3);
	}

    return 0;
}
           

我們再次編譯用戶端程式,并啟動三個 client 程序,然後用 lsof 指令檢視機器上的 TCP 連接配接情況,結果如下所示:

[[email protected] ~]# lsof -i -Pn
COMMAND   PID USER   FD   TYPE DEVICE SIZE/OFF NODE NAME
server   1593 root    3u  IPv4  21807      0t0  TCP *:3000 (LISTEN)
server   1593 root    4u  IPv4  21808      0t0  TCP 127.0.0.1:3000->127.0.0.1:44220 (ESTABLISHED)
server   1593 root    5u  IPv4  19311      0t0  TCP 127.0.0.1:3000->127.0.0.1:38990 (ESTABLISHED)
server   1593 root    6u  IPv4  21234      0t0  TCP 127.0.0.1:3000->127.0.0.1:42365 (ESTABLISHED)
client   1595 root    3u  IPv4  22626      0t0  TCP 127.0.0.1:44220->127.0.0.1:3000 (ESTABLISHED)
client   1611 root    3u  IPv4  21835      0t0  TCP 127.0.0.1:38990->127.0.0.1:3000 (ESTABLISHED)
client   1627 root    3u  IPv4  21239      0t0  TCP 127.0.0.1:42365->127.0.0.1:3000 (ESTABLISHED)

           

通過上面的結果,我們發現三個 client 程序使用的端口号仍然是系統随機配置設定的,也就是說綁定 0 号端口和沒有綁定效果是一樣的。

  • 情形三:用戶端綁定一個固定端口

    我們這裡使用 20000 端口,當然讀者可以根據自己的喜好選擇,隻要保證所選擇的端口号目前沒有被其他程式占用即可,伺服器代碼保持不變,用戶端綁定代碼中的端口号從 0 改成 20000。這裡為了節省篇幅,隻貼出修改處的代碼:

struct sockaddr_in bindaddr;
bindaddr.sin_family = AF_INET;
bindaddr.sin_addr.s_addr = htonl(INADDR_ANY);
//将socket綁定到20000号端口上去
bindaddr.sin_port = htons(20000);
if (bind(clientfd, (struct sockaddr *)&bindaddr, sizeof(bindaddr)) == -1)
{
    std::cout << "bind socket error." << std::endl;
    return -1;
}
           

再次重新編譯程式,先啟動一個用戶端後,我們看到此時的 TCP 連接配接狀态:

[[email protected] testsocket]# lsof -i -Pn
COMMAND   PID USER   FD   TYPE DEVICE SIZE/OFF NODE NAME
server   1676 root    3u  IPv4  21933      0t0  TCP *:3000 (LISTEN)
server   1676 root    4u  IPv4  21934      0t0  TCP 127.0.0.1:3000->127.0.0.1:20000 (ESTABLISHED)
client   1678 root    3u  IPv4  21336      0t0  TCP 127.0.0.1:20000->127.0.0.1:3000 (ESTABLISHED)

           

通過上面的結果,我們發現 client 程序确實使用 20000 号端口連接配接到 server 程序上去了。這個時候如果我們再開啟一個 client 程序,我們猜想由于端口号 20000 已經被占用,新啟動的 client 會由于調用 bind 函數出錯而退出,我們實際驗證一下:

[[email protected] testsocket]# ./client 
bind socket error.
[[email protected] testsocket]#
           

結果确實和我們預想的一樣。

在技術面試的時候,有時候面試官會問 TCP 網絡通信的用戶端程式中的 socket 是否可以調用 bind 函數,相信讀到這裡,聰明的讀者已經有答案了。

另外,Linux 的 nc 指令有個 -p 選項(字母 p 是小寫),這個選項的作用就是 nc 在模拟用戶端程式時,可以使用指定端口号連接配接到伺服器程式上去,實作原理相信讀者也明白了。我們還是以上面的伺服器程式為例,這個我們不用我們的 client 程式,改用 nc 指令來模拟用戶端。在 shell 終端輸入:

[[email protected] testsocket]# nc -v -p 9999 127.0.0.1 3000
Ncat: Version 6.40 ( http://nmap.org/ncat )
Ncat: Connected to 127.0.0.1:3000.
My name is zhangxf
My name is zhangxf
           

-v 選項表示輸出 nc 指令連接配接的詳細資訊,這裡連接配接成功以後,會輸出“Ncat: Connected to 127.0.0.1:3000.” 提示已經連接配接到伺服器的 3000 端口上去了。

-p 選項的參數值是 9999 表示,我們要求 nc 指令本地以端口号 9999 連接配接伺服器,注意不要與端口号 3000 混淆,3000 是伺服器的偵聽端口号,也就是我們的連接配接的目标端口号,9999 是我們用戶端使用的端口号。我們用 lsof 指令來驗證一下我們的 nc 指令是否确實以 9999 端口号連接配接到 server 程序上去了。