Linux網(wǎng)絡(luò)編程之綁定端口注意事項(xiàng)及端口復(fù)用
所謂綁定(bind)是指別人連接我只能通過(guò)我所綁定的端口,相當(dāng)于,我買(mǎi)了一個(gè)手機(jī),別人要想聯(lián)系我,必須要知道我的手機(jī)號(hào)碼,這時(shí)候,我需要怎么辦呢?我需要給手機(jī)插上電話(huà)卡,固定一個(gè)電話(huà)號(hào)碼,這樣別人就能通過(guò)這個(gè)電話(huà)號(hào)碼聯(lián)系我。手機(jī)插上電話(huà)卡,固定一個(gè)電話(huà)號(hào)碼,類(lèi)似于綁定(bind)的過(guò)程,綁定(bind)為了固定一個(gè)端口號(hào),別的網(wǎng)絡(luò)程序就可以找到這個(gè)端口號(hào),找到這個(gè)端口號(hào)就能找到這個(gè)端口號(hào)所對(duì)應(yīng)的網(wǎng)絡(luò)應(yīng)用程序。
在網(wǎng)絡(luò)編程里,通常都是在服務(wù)器里綁定(bind)端口,這并不是說(shuō)客戶(hù)端里不能綁定(bind)端口,但這里需要注意的是,一個(gè)網(wǎng)絡(luò)應(yīng)用程序只能綁定一個(gè)端口( 一個(gè)套接字只能 綁定一個(gè)端口 )。
一個(gè)套接字不能同時(shí)綁定多個(gè)端口,如下:
- #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, 會(huì)綁定失敗
- 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;
- }
程序編譯運(yùn)行后結(jié)果如下:
如果客戶(hù)端想綁定端口號(hào),一定要調(diào)用發(fā)送信息函數(shù)之前綁定( bind )端口,因?yàn)樵诎l(fā)送信息函數(shù)( sendto, 或 write ),系統(tǒng)會(huì)自動(dòng)給當(dāng)前網(wǎng)絡(luò)程序分配一個(gè)隨機(jī)端口號(hào),這相當(dāng)于隨機(jī)綁定了一個(gè)端口號(hào),這里只會(huì)分配一次,以后通信就以這個(gè)隨機(jī)端口通信,我們?cè)俳壎ǘ丝谔?hào)的話(huà),就會(huì)綁定失敗。如果我們放在發(fā)送信息函數(shù)( sendto, 或 write )之前綁定,那樣程序?qū)⒁晕覀兘壎ǖ亩丝谔?hào)發(fā)送信息,不會(huì)再隨機(jī)分配一個(gè)端口號(hào)。
綁定失敗例子( 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";
- // 如果前面沒(méi)有綁定端口,sendto()系統(tǒng)會(huì)隨機(jī)分配一個(gè)端口
- 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;
- }
程序編譯運(yùn)行后結(jié)果如下:
在上面提到:一個(gè)網(wǎng)絡(luò)應(yīng)用程序只能綁定一個(gè)端口( 一個(gè)套接字只能綁定一個(gè)端口 )。
實(shí)際上,默認(rèn)的情況下,如果一個(gè)網(wǎng)絡(luò)應(yīng)用程序的一個(gè)套接字 綁定了一個(gè)端口( 占用了 8000 ),這時(shí)候,別的套接字就無(wú)法使用這個(gè)端口( 8000 ), 驗(yàn)證例子如下:
- #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端口,綁定失敗
- // 因?yàn)?000端口已被占用,默認(rèn)情況下,端口沒(méi)有釋放,無(wú)法綁定
- 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;
- }
程序編譯運(yùn)行后結(jié)果如下:
那如何讓sockfd_one, sockfd_two兩個(gè)套接字都能成功綁定8000端口呢?這時(shí)候就需要要到端口復(fù)用了。端口復(fù)用允許在一個(gè)應(yīng)用程序可以把 n 個(gè)套接字綁在一個(gè)端口上而不出錯(cuò)。
設(shè)置socket的SO_REUSEADDR選項(xiàng),即可實(shí)現(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)有一個(gè)有相同本地地址和端口的socket1處于TIME_WAIT狀態(tài)時(shí),而你啟動(dòng)的程序的socket2要占用該地址和端口,你的程序就要用到該選項(xiàng)。
2、SO_REUSEADDR允許同一port上啟動(dòng)同一服務(wù)器的多個(gè)實(shí)例(多個(gè)進(jìn)程)。但每個(gè)實(shí)例綁定的IP地址是不能相同的。在有多塊網(wǎng)卡或用IP Alias技術(shù)的機(jī)器可以測(cè)試這種情況。
3、SO_REUSEADDR允許單個(gè)進(jìn)程綁定相同的端口到多個(gè)socket上,但每個(gè)socket綁定的ip地址不同。這和2很相似,區(qū)別請(qǐng)看UNPv1。
4、SO_REUSEADDR允許完全相同的地址和端口的重復(fù)綁定。但這只用于UDP的多播,不用于TCP。
需要注意的是,設(shè)置端口復(fù)用函數(shù)要在綁定之前調(diào)用,而且只要綁定到同一個(gè)端口的所有套接字都得設(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ù)用允許在一個(gè)應(yīng)用程序可以把 n 個(gè)套接字綁在一個(gè)端口上而不出錯(cuò)。同時(shí),這 n 個(gè)套接字發(fā)送信息都正常,沒(méi)有問(wèn)題。但是,這些套接字并不是所有都能讀取信息,只有***一個(gè)套接字會(huì)正常接收數(shù)據(jù)。
下面,我們?cè)谥暗拇a上,添加兩個(gè)線(xiàn)程,分別負(fù)責(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>
- // 線(xiàn)程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;
- }
- // 線(xiàn)程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);
- }
- //接收信息線(xiàn)程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);
- }
- //接收信息線(xiàn)程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;
- }
接著,通過(guò)網(wǎng)絡(luò)調(diào)試助手給這個(gè)服務(wù)器發(fā)送數(shù)據(jù),結(jié)果顯示,只有***一個(gè)套接字sockfd_two會(huì)正常接收數(shù)據(jù):
我們上面的用法,實(shí)際上沒(méi)有太大的意義。端口復(fù)用最常用的用途應(yīng)該是防止服務(wù)器重啟時(shí)之前綁定的端口還未釋放或者程序突然退出而系統(tǒng)沒(méi)有釋放端口。這種情況下如果設(shè)定了端口復(fù)用,則新啟動(dòng)的服務(wù)器進(jìn)程可以直接綁定端口。如果沒(méi)有設(shè)定端口復(fù)用,綁定會(huì)失敗,提示ADDR已經(jīng)在使用中——那只好等等再重試了,麻煩!