Linux網(wǎng)絡(luò)編程之綁定端口注意事項及端口復(fù)用
所謂綁定(bind)是指別人連接我只能通過我所綁定的端口,相當(dāng)于,我買了一個手機,別人要想聯(lián)系我,必須要知道我的手機號碼,這時候,我需要怎么辦呢?我需要給手機插上電話卡,固定一個電話號碼,這樣別人就能通過這個電話號碼聯(lián)系我。手機插上電話卡,固定一個電話號碼,類似于綁定(bind)的過程,綁定(bind)為了固定一個端口號,別的網(wǎng)絡(luò)程序就可以找到這個端口號,找到這個端口號就能找到這個端口號所對應(yīng)的網(wǎng)絡(luò)應(yīng)用程序。
在網(wǎng)絡(luò)編程里,通常都是在服務(wù)器里綁定(bind)端口,這并不是說客戶端里不能綁定(bind)端口,但這里需要注意的是,一個網(wǎng)絡(luò)應(yīng)用程序只能綁定一個端口( 一個套接字只能 綁定一個端口 )。
一個套接字不能同時綁定多個端口,如下:
- #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, charchar *argv[])
- {
- char server_ip[30] = "10.221.20.12";
- int sockfd;
- sockfd = socket(AF_INET, SOCK_DGRAM, 0); //創(chuàng)建UDP套接字
- if(sockfd < 0)
- {
- perror("socket");
- exit(-1);
- }
- // 初始化本地網(wǎng)絡(luò)信息
- struct sockaddr_in my_addr;
- bzero(&my_addr, sizeof(my_addr));
- my_addr.sin_family = AF_INET;
- my_addr.sin_port = htons(8000);
- my_addr.sin_addr.s_addr = htonl(INADDR_ANY);
- // ***次綁定端口8000
- int err_log;
- err_log = bind(sockfd, (struct sockaddr*)&my_addr, sizeof(my_addr));
- if(err_log != 0)
- {
- perror("bind 8000");
- close(sockfd);
- exit(-1);
- }
- // 又一次綁定別的端口9000, 會綁定失敗
- my_addr.sin_port = htons(9000);
- err_log = bind(sockfd, (struct sockaddr*)&my_addr, sizeof(my_addr));
- if(err_log != 0)
- {
- perror("bind 9000");
- close(sockfd);
- exit(-1);
- }
- close(sockfd);
- return 0;
- }
程序編譯運行后結(jié)果如下:
如果客戶端想綁定端口號,一定要調(diào)用發(fā)送信息函數(shù)之前綁定( bind )端口,因為在發(fā)送信息函數(shù)( sendto, 或 write ),系統(tǒng)會自動給當(dāng)前網(wǎng)絡(luò)程序分配一個隨機端口號,這相當(dāng)于隨機綁定了一個端口號,這里只會分配一次,以后通信就以這個隨機端口通信,我們再綁定端口號的話,就會綁定失敗。如果我們放在發(fā)送信息函數(shù)( sendto, 或 write )之前綁定,那樣程序?qū)⒁晕覀兘壎ǖ亩丝谔柊l(fā)送信息,不會再隨機分配一個端口號。
綁定失敗例子( UDP )如下:
- #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, charchar *argv[])
- {
- char server_ip[30] = "10.221.20.12";
- int sockfd;
- sockfd = socket(AF_INET, SOCK_DGRAM, 0); //創(chuàng)建UDP套接字
- if(sockfd < 0)
- {
- perror("socket");
- exit(-1);
- }
- struct sockaddr_in dest_addr;
- bzero(&dest_addr, sizeof(dest_addr));
- dest_addr.sin_family = AF_INET;
- dest_addr.sin_port = htons(8080); // 服務(wù)器的端口
- inet_pton(AF_INET, server_ip, &dest_addr.sin_addr);
- char send_buf[512] = "this is for test";
- // 如果前面沒有綁定端口,sendto()系統(tǒng)會隨機分配一個端口
- sendto(sockfd, send_buf, strlen(send_buf), 0, (struct sockaddr*)&dest_addr, sizeof(dest_addr));//發(fā)送數(shù)據(jù)
- // 初始化本地網(wǎng)絡(luò)信息
- struct sockaddr_in my_addr;
- bzero(&my_addr, sizeof(my_addr));
- my_addr.sin_family = AF_INET;
- my_addr.sin_port = htons(8000);
- my_addr.sin_addr.s_addr = htonl(INADDR_ANY);
- // sendto()后面綁定端口,綁定失敗
- int err_log;
- err_log = bind(sockfd, (struct sockaddr*)&my_addr, sizeof(my_addr));
- if(err_log != 0)
- {
- perror("bind 8000");
- close(sockfd);
- exit(-1);
- }
- close(sockfd);
- return 0;
- }
程序編譯運行后結(jié)果如下:
在上面提到:一個網(wǎng)絡(luò)應(yīng)用程序只能綁定一個端口( 一個套接字只能綁定一個端口 )。
實際上,默認的情況下,如果一個網(wǎng)絡(luò)應(yīng)用程序的一個套接字 綁定了一個端口( 占用了 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, charchar *argv[])
- {
- int sockfd_one;
- int err_log;
- sockfd_one = socket(AF_INET, SOCK_DGRAM, 0); //創(chuàng)建UDP套接字one
- if(sockfd_one < 0)
- {
- perror("sockfd_one");
- exit(-1);
- }
- // 設(shè)置本地網(wǎng)絡(luò)信息
- 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); //創(chuàng)建UDP套接字two
- if(sockfd_two < 0)
- {
- perror("sockfd_two");
- exit(-1);
- }
- // 新套接字sockfd_two,繼續(xù)綁定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;
- }
程序編譯運行后結(jié)果如下:
那如何讓sockfd_one, sockfd_two兩個套接字都能成功綁定8000端口呢?這時候就需要要到端口復(fù)用了。端口復(fù)用允許在一個應(yīng)用程序可以把 n 個套接字綁在一個端口上而不出錯。
設(shè)置socket的SO_REUSEADDR選項,即可實現(xiàn)端口復(fù)用:
- int opt = 1;
- // sockfd為需要端口復(fù)用的套接字
- setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, (const voidvoid *)&opt, sizeof(opt));
SO_REUSEADDR可以用在以下四種情況下。 (摘自《Unix網(wǎng)絡(luò)編程》卷一,即UNPv1)
1、當(dāng)有一個有相同本地地址和端口的socket1處于TIME_WAIT狀態(tài)時,而你啟動的程序的socket2要占用該地址和端口,你的程序就要用到該選項。
2、SO_REUSEADDR允許同一port上啟動同一服務(wù)器的多個實例(多個進程)。但每個實例綁定的IP地址是不能相同的。在有多塊網(wǎng)卡或用IP Alias技術(shù)的機器可以測試這種情況。
3、SO_REUSEADDR允許單個進程綁定相同的端口到多個socket上,但每個socket綁定的ip地址不同。這和2很相似,區(qū)別請看UNPv1。
4、SO_REUSEADDR允許完全相同的地址和端口的重復(fù)綁定。但這只用于UDP的多播,不用于TCP。
需要注意的是,設(shè)置端口復(fù)用函數(shù)要在綁定之前調(diào)用,而且只要綁定到同一個端口的所有套接字都得設(shè)置復(fù)用:
- // sockfd_one, sockfd_two都要設(shè)置端口復(fù)用
- // 在sockfd_one綁定bind之前,設(shè)置其端口復(fù)用
- int opt = 1;
- setsockopt( sockfd_one, SOL_SOCKET,SO_REUSEADDR, (const voidvoid *)&opt, sizeof(opt) );
- err_log = bind(sockfd_one, (struct sockaddr*)&my_addr, sizeof(my_addr));
- // 在sockfd_two綁定bind之前,設(shè)置其端口復(fù)用
- opt = 1;
- setsockopt( sockfd_two, SOL_SOCKET,SO_REUSEADDR,(const voidvoid *)&opt, sizeof(opt) );
- err_log = bind(sockfd_two, (struct sockaddr*)&my_addr, sizeof(my_addr));
端口復(fù)用完整代碼如下:
- #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, charchar *argv[])
- {
- int sockfd_one;
- int err_log;
- sockfd_one = socket(AF_INET, SOCK_DGRAM, 0); //創(chuàng)建UDP套接字one
- if(sockfd_one < 0)
- {
- perror("sockfd_one");
- exit(-1);
- }
- // 設(shè)置本地網(wǎng)絡(luò)信息
- 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之前,設(shè)置其端口復(fù)用
- int opt = 1;
- setsockopt( sockfd_one, SOL_SOCKET,SO_REUSEADDR,
- (const voidvoid *)&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); //創(chuàng)建UDP套接字two
- if(sockfd_two < 0)
- {
- perror("sockfd_two");
- exit(-1);
- }
- // 在sockfd_two綁定bind之前,設(shè)置其端口復(fù)用
- opt = 1;
- setsockopt( sockfd_two, SOL_SOCKET,SO_REUSEADDR,
- (const voidvoid *)&opt, sizeof(opt) );
- // 新套接字sockfd_two,繼續(xù)綁定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;
- }
端口復(fù)用允許在一個應(yīng)用程序可以把 n 個套接字綁在一個端口上而不出錯。同時,這 n 個套接字發(fā)送信息都正常,沒有問題。但是,這些套接字并不是所有都能讀取信息,只有***一個套接字會正常接收數(shù)據(jù)。
下面,我們在之前的代碼上,添加兩個線程,分別負責(zé)接收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的回調(diào)函數(shù)
- voidvoid *recv_one(voidvoid *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的回調(diào)函數(shù)
- voidvoid *recv_two(voidvoid *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, charchar *argv[])
- {
- int err_log;
- /////////////////////////sockfd_one
- int sockfd_one;
- sockfd_one = socket(AF_INET, SOCK_DGRAM, 0); //創(chuàng)建UDP套接字one
- if(sockfd_one < 0)
- {
- perror("sockfd_one");
- exit(-1);
- }
- // 設(shè)置本地網(wǎng)絡(luò)信息
- 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之前,設(shè)置其端口復(fù)用
- int opt = 1;
- setsockopt( sockfd_one, SOL_SOCKET,SO_REUSEADDR,
- (const voidvoid *)&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, (voidvoid *)sockfd_one);
- /////////////////////////sockfd_two
- int sockfd_two;
- sockfd_two = socket(AF_INET, SOCK_DGRAM, 0); //創(chuàng)建UDP套接字two
- if(sockfd_two < 0)
- {
- perror("sockfd_two");
- exit(-1);
- }
- // 在sockfd_two綁定bind之前,設(shè)置其端口復(fù)用
- opt = 1;
- setsockopt( sockfd_two, SOL_SOCKET,SO_REUSEADDR,
- (const voidvoid *)&opt, sizeof(opt) );
- // 新套接字sockfd_two,繼續(xù)綁定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, (voidvoid *)sockfd_two);
- while(1){ // 讓程序阻塞在這,不結(jié)束
- NULL;
- }
- close(sockfd_one);
- close(sockfd_two);
- return 0;
- }
接著,通過網(wǎng)絡(luò)調(diào)試助手給這個服務(wù)器發(fā)送數(shù)據(jù),結(jié)果顯示,只有***一個套接字sockfd_two會正常接收數(shù)據(jù):
我們上面的用法,實際上沒有太大的意義。端口復(fù)用最常用的用途應(yīng)該是防止服務(wù)器重啟時之前綁定的端口還未釋放或者程序突然退出而系統(tǒng)沒有釋放端口。這種情況下如果設(shè)定了端口復(fù)用,則新啟動的服務(wù)器進程可以直接綁定端口。如果沒有設(shè)定端口復(fù)用,綁定會失敗,提示ADDR已經(jīng)在使用中——那只好等等再重試了,麻煩!