1. 程式人生 > >《Linux網路程式設計》: 埠複用(多個套接字繫結同一個埠)

《Linux網路程式設計》: 埠複用(多個套接字繫結同一個埠)

在《繫結( bind )埠需要注意的問題》提到:一個網路應用程式只能繫結一個埠( 一個套接字只能繫結一個埠 )。

請檢視《Linux網路程式設計》: 繫結( bind )埠需要注意的問題

實際上,預設的情況下,如果一個網路應用程式的一個套接字 綁定了一個埠( 佔用了 8000 ),這時候,別的套接字就無法使用這個埠( 8000 ), 驗證例子如下:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
 
int main(int argc, char *argv[])
{
	int sockfd_one;
	int err_log;
	sockfd_one = socket(AF_INET, SOCK_DGRAM, 0); //建立UDP套接字one
	if(sockfd_one < 0)
	{
	perror("sockfd_one");
	exit(-1);
	}
 
	// 設定本地網路資訊
	struct sockaddr_in my_addr;
	bzero(&my_addr, sizeof(my_addr));
	my_addr.sin_family = AF_INET;
	my_addr.sin_port = htons(8000);		// 埠為8000
	my_addr.sin_addr.s_addr = htonl(INADDR_ANY);
 
	// 繫結,埠為8000
	err_log = bind(sockfd_one, (struct sockaddr*)&my_addr, sizeof(my_addr));
	if(err_log != 0)
	{
		perror("bind sockfd_one");
		close(sockfd_one);		
		exit(-1);
	}
 
	int sockfd_two;
	sockfd_two = socket(AF_INET, SOCK_DGRAM, 0);  //建立UDP套接字two
	if(sockfd_two < 0)
	{
		perror("sockfd_two");
		exit(-1);
	}
 
	// 新套接字sockfd_two,繼續繫結8000埠,繫結失敗
	// 因為8000埠已被佔用,預設情況下,埠沒有釋放,無法繫結
	err_log = bind(sockfd_two, (struct sockaddr*)&my_addr, sizeof(my_addr));
	if(err_log != 0)
	{
		perror("bind sockfd_two");
		close(sockfd_two);		
		exit(-1);
	}
 
	close(sockfd_one);
	close(sockfd_two);
 
	return 0;
}

程式編譯執行後結果如下:

那如何讓sockfd_one, sockfd_two兩個套接字都能成功繫結8000埠呢?這時候就需要要到埠複用了。埠複用允許在一個應用程式可以把 n 個套接字綁在一個埠上而不出錯。

設定socket的SO_REUSEADDR選項,即可實現埠複用:

int opt = 1;
// sockfd為需要埠複用的套接字
setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, (const void *)&opt, sizeof(opt));

SO_REUSEADDR可以用在以下四種情況下。 (摘自《Unix網路程式設計》卷一,即UNPv1)

1、當有一個有相同本地地址和埠的socket1處於TIME_WAIT狀態時,而你啟動的程式的socket2要佔用該地址和埠,你的程式就要用到該選項。

2、SO_REUSEADDR允許同一port上啟動同一伺服器的多個例項(多個程序)。但每個例項繫結的IP地址是不能相同的。在有多塊網絡卡或用IP Alias技術的機器可以測試這種情況。

3、SO_REUSEADDR允許單個程序繫結相同的埠到多個socket上,但每個socket繫結的ip地址不同。這和2很相似,區別請看UNPv1。

4、SO_REUSEADDR允許完全相同的地址和埠的重複繫結。但這隻用於UDP的多播,不用於TCP。

需要注意的是,設定埠複用函式要在繫結之前呼叫,而且只要繫結到同一個埠的所有套接字都得設定複用:

// sockfd_one, sockfd_two都要設定埠複用
// 在sockfd_one繫結bind之前,設定其埠複用
int opt = 1;
setsockopt( sockfd_one, SOL_SOCKET,SO_REUSEADDR, (const void *)&opt, sizeof(opt) );
err_log = bind(sockfd_one, (struct sockaddr*)&my_addr, sizeof(my_addr));
 
// 在sockfd_two繫結bind之前,設定其埠複用
opt = 1;
setsockopt( sockfd_two, SOL_SOCKET,SO_REUSEADDR,(const void *)&opt, sizeof(opt) );
err_log = bind(sockfd_two, (struct sockaddr*)&my_addr, sizeof(my_addr));

埠複用完整程式碼如下:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
 
int main(int argc, char *argv[])
{
	int sockfd_one;
	int err_log;
	sockfd_one = socket(AF_INET, SOCK_DGRAM, 0); //建立UDP套接字one
	if(sockfd_one < 0)
	{
	perror("sockfd_one");
	exit(-1);
	}
 
	// 設定本地網路資訊
	struct sockaddr_in my_addr;
	bzero(&my_addr, sizeof(my_addr));
	my_addr.sin_family = AF_INET;
	my_addr.sin_port = htons(8000);		// 埠為8000
	my_addr.sin_addr.s_addr = htonl(INADDR_ANY);
	
	// 在sockfd_one繫結bind之前,設定其埠複用
	int opt = 1;
	setsockopt( sockfd_one, SOL_SOCKET,SO_REUSEADDR, 
					(const void *)&opt, sizeof(opt) );
 
	// 繫結,埠為8000
	err_log = bind(sockfd_one, (struct sockaddr*)&my_addr, sizeof(my_addr));
	if(err_log != 0)
	{
		perror("bind sockfd_one");
		close(sockfd_one);		
		exit(-1);
	}
 
	int sockfd_two;
	sockfd_two = socket(AF_INET, SOCK_DGRAM, 0);  //建立UDP套接字two
	if(sockfd_two < 0)
	{
		perror("sockfd_two");
		exit(-1);
	}
 
	// 在sockfd_two繫結bind之前,設定其埠複用
	opt = 1;
	setsockopt( sockfd_two, SOL_SOCKET,SO_REUSEADDR, 
					(const void *)&opt, sizeof(opt) );
	
	// 新套接字sockfd_two,繼續繫結8000埠,成功
	err_log = bind(sockfd_two, (struct sockaddr*)&my_addr, sizeof(my_addr));
	if(err_log != 0)
	{
		perror("bind sockfd_two");
		close(sockfd_two);		
		exit(-1);
	}
 
	close(sockfd_one);
	close(sockfd_two);
 
	return 0;
}

埠複用允許在一個應用程式可以把 n 個套接字綁在一個埠上而不出錯。同時,這 n 個套接字傳送資訊都正常,沒有問題。但是,這些套接字並不是所有都能讀取資訊,只有最後一個套接字會正常接收資料。

下面,我們在之前的程式碼上,新增兩個執行緒,分別負責接收sockfd_one,sockfd_two的資訊:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <pthread.h>
 
// 執行緒1的回撥函式
void *recv_one(void *arg)
{
	printf("===========recv_one==============\n");
	int sockfd = (int )arg;
	while(1){
		int recv_len;
		char recv_buf[512] = "";
		struct sockaddr_in client_addr;
		char cli_ip[INET_ADDRSTRLEN] = "";//INET_ADDRSTRLEN=16
		socklen_t cliaddr_len = sizeof(client_addr);
		
		recv_len = recvfrom(sockfd, recv_buf, sizeof(recv_buf), 0, (struct sockaddr*)&client_addr, &cliaddr_len);
		inet_ntop(AF_INET, &client_addr.sin_addr, cli_ip, INET_ADDRSTRLEN);
		printf("\nip:%s ,port:%d\n",cli_ip, ntohs(client_addr.sin_port));
		printf("sockfd_one =========== data(%d):%s\n",recv_len,recv_buf);
	
	}
 
	return NULL;
}
 
// 執行緒2的回撥函式
void *recv_two(void *arg)
{
	printf("+++++++++recv_two++++++++++++++\n");
	int sockfd = (int )arg;
	while(1){
		int recv_len;
		char recv_buf[512] = "";
		struct sockaddr_in client_addr;
		char cli_ip[INET_ADDRSTRLEN] = "";//INET_ADDRSTRLEN=16
		socklen_t cliaddr_len = sizeof(client_addr);
		
		recv_len = recvfrom(sockfd, recv_buf, sizeof(recv_buf), 0, (struct sockaddr*)&client_addr, &cliaddr_len);
		inet_ntop(AF_INET, &client_addr.sin_addr, cli_ip, INET_ADDRSTRLEN);
		printf("\nip:%s ,port:%d\n",cli_ip, ntohs(client_addr.sin_port));
		printf("sockfd_two @@@@@@@@@@@@@@@ data(%d):%s\n",recv_len,recv_buf);
	
	}
 
	return NULL;
}
 
int main(int argc, char *argv[])
{
	int err_log;
	
	/////////////////////////sockfd_one
	int sockfd_one;
	sockfd_one = socket(AF_INET, SOCK_DGRAM, 0); //建立UDP套接字one
	if(sockfd_one < 0)
	{
	perror("sockfd_one");
	exit(-1);
	}
 
	// 設定本地網路資訊
	struct sockaddr_in my_addr;
	bzero(&my_addr, sizeof(my_addr));
	my_addr.sin_family = AF_INET;
	my_addr.sin_port = htons(8000);		// 埠為8000
	my_addr.sin_addr.s_addr = htonl(INADDR_ANY);
	
	// 在sockfd_one繫結bind之前,設定其埠複用
	int opt = 1;
	setsockopt( sockfd_one, SOL_SOCKET,SO_REUSEADDR, 
					(const void *)&opt, sizeof(opt) );
 
	// 繫結,埠為8000
	err_log = bind(sockfd_one, (struct sockaddr*)&my_addr, sizeof(my_addr));
	if(err_log != 0)
	{
		perror("bind sockfd_one");
		close(sockfd_one);		
		exit(-1);
	}
	
	//接收資訊執行緒1
	pthread_t tid_one;
	pthread_create(&tid_one, NULL, recv_one, (void *)sockfd_one);
	
	/////////////////////////sockfd_two
	int sockfd_two;
	sockfd_two = socket(AF_INET, SOCK_DGRAM, 0);  //建立UDP套接字two
	if(sockfd_two < 0)
	{
		perror("sockfd_two");
		exit(-1);
	}
 
	// 在sockfd_two繫結bind之前,設定其埠複用
	opt = 1;
	setsockopt( sockfd_two, SOL_SOCKET,SO_REUSEADDR, 
					(const void *)&opt, sizeof(opt) );
	
	// 新套接字sockfd_two,繼續繫結8000埠,成功
	err_log = bind(sockfd_two, (struct sockaddr*)&my_addr, sizeof(my_addr));
	if(err_log != 0)
	{
		perror("bind sockfd_two");
		close(sockfd_two);		
		exit(-1);
	}
	//接收資訊執行緒2
	pthread_t tid_two;
	pthread_create(&tid_two, NULL, recv_two, (void *)sockfd_two);
	
	
	while(1){	// 讓程式阻塞在這,不結束
		NULL;
	}
 
	close(sockfd_one);
	close(sockfd_two);
 
	return 0;
}

接著,通過網路除錯助手給這個伺服器傳送資料,結果顯示,只有最後一個套接字sockfd_two會正常接收資料:

我們上面的用法,實際上沒有太大的意義。埠複用最常用的用途應該是防止伺服器重啟時之前繫結的埠還未釋放或者程式突然退出而系統沒有釋放埠。這種情況下如果設定了埠複用,則新啟動的伺服器程序可以直接繫結埠。如果沒有設定埠複用,繫結會失敗,提示ADDR已經在使用中——那隻好等等再重試了,麻煩!