最近因為工作需要,在Linux下需要讓多個UDP socket使用同一個port。本文將透過實驗來觀察多個UDP socket使用同一個port時會發生什麼事。
本文的程式原始碼可在以下網址取得
https://github.com/huangtw/UDPReuseAddrTest
SO_REUSEADDR
在Linux下如果多個socket要bind同一個port,可以用setsocket設定SO_REUSERADDR來達成,而在TCP socket與UDP socket上各有不同的效果。
TCP
在尚未設定SO_REUSERADDR的情況下,當一個TCP socket bind了0.0.0.0:PortA後,則其他Tsocket將無法在bind PortA,因為0.0.0.0代表任何一個interface的IP,因此socket不管綁定哪個IP的PortA都會造成衝突。
此時如果一個socket設定了SO_REUSERADDR,則可以成功bind指定IP的PortA,但是還是不能bind 0.0.0.0:PortA,也就是說在設定了SO_REUSERADDR之後,只有IP一模一樣才會視為衝突,0.0.0.0與特定IP不再視為衝突。
不過有一個例外,當一個TCP socket bind了0.0.0.0:PortA,並且開始listen之後,此時其他TCP socket即使設定了SO_REUSERADDR也無法bind PortA,就算指定特定的IP也一樣。
除了共用port之外,另外一個常見使用情境是,當一個bind了某個port的TCP socket close之後,其他socket並無法馬上bind同一個port,因為TCP socket還處於TIME_WAIE的狀態,尚未真的關掉。此時新的socket如果設定SO_REUSERADDR,則可以成功bind同一個port。
UDP
在UDP與TCP不太一樣,只要每個UDP socket都有設定SO_REUSERADDR,則可以綁定一模一樣的IP跟port。在boardcast或者multicast模式下,如果多個UDP綁定同一個IP與port,當有UDP封包送到該IP:port時,每一個socket都會收到一份相同的封包。
如果是unicast呢?網路上似乎很少提到這個狀況,因此我做了一個實驗來確定這件事。
實驗
本實驗需要兩個程式,一邊為傳送端(udp_send),一邊為接收端(udp_receive),傳送端每隔固定時間會從IP_A:Port_A向IP_B:Port_B發送UDP封包,而接收端會產生多個UDP socket bind同時bind IP_B:Port_B,觀察哪一個socket能收到封包。
除此之外,接收端的部分UDP socket為connected(指定destination為IP_A:Port_A,只會收到來自IP_A:Port_A的封包,封包也只能傳送給IP_A:Port_A),來看這種情況下一般UDP socket與connected UDP socket的差異。
udp_send.c#include <unistd.h> #include <stdio.h> #include <stdlib.h> #include <netdb.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <string.h> int main(int argc, char* argv[]) { int fd; int localPort, peerPort; char *peerIP; struct sockaddr_in local_addr, peer_addr; char buffer[1024]; if (4 != argc) { exit(1); } localPort = atoi(argv[1]); peerIP = argv[2]; peerPort = atoi(argv[3]); if ((fd = socket(AF_INET, SOCK_DGRAM, 0)) < 0) { printf("socket open failed!\n"); exit(1); } bzero((char *) &local_addr, sizeof(local_addr)); local_addr.sin_family = AF_INET; local_addr.sin_addr.s_addr = ntohl(INADDR_ANY); local_addr.sin_port = htons(localPort); if (bind(fd, (struct sockaddr *)&local_addr, sizeof(local_addr)) < 0) { printf("bind failed\n"); } bzero((char *) &peer_addr, sizeof(peer_addr)); peer_addr.sin_family = AF_INET; peer_addr.sin_addr.s_addr = inet_addr(peerIP); peer_addr.sin_port = htons(peerPort); if (connect(fd , (struct sockaddr *)&peer_addr , sizeof(peer_addr)) < 0) { printf("udp connect failed!\n"); exit(1); } int i = 0; while(1) { int size,ss; int ssize = 0; size = sprintf(buffer, "%d\n", i++); sleep(2); while (ssize < size) { if ((ss = send(fd, buffer + ssize, size - ssize, 0)) < 0) { //cout << "send error" << endl; printf("send error\n"); exit(1); } ssize += ss; } } return 0; }
#include <stdio.h> #include <unistd.h> #include <stdlib.h> #include <string.h> #include <netdb.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> int createSocket(int port) { int fd; struct sockaddr_in local_addr; if ((fd = socket(AF_INET, SOCK_DGRAM, 0)) < 0) { printf("socket open failed!\n"); exit(1); } int sock_opt = 1; if (setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, (void*)&sock_opt, sizeof(sock_opt)) < 0) { printf("setsockopt failed!\n"); exit(1); } bzero((char *) &local_addr, sizeof(local_addr)); local_addr.sin_family = AF_INET; local_addr.sin_addr.s_addr = ntohl(INADDR_ANY); local_addr.sin_port = htons(port); if (bind(fd, (struct sockaddr *)&local_addr, sizeof(local_addr)) < 0) { printf("bind failed\n"); } return fd; } void* worker(void* pfd) { int fd = *(int*)pfd; char buffer[1024]; while (1) { if (recv(fd, buffer, sizeof(buffer) - 1, 0) < 0) { printf("udp recv failed!\n"); exit(1); } printf("socket %d received: %s\n", fd, buffer); //cout << "socket " << fd << " received:" << buffer << endl; } } int main(int argc, char* argv[]) { int *pfd, localPort, peerPort; char *peerIP; char buffer[1024]; struct sockaddr_in local_addr, peer_addr; if (4 != argc) { exit(1); } localPort = atoi(argv[1]); peerIP = argv[2]; peerPort = atoi(argv[3]); int i = 0; while (1) { pfd = malloc(sizeof(int)); *pfd = createSocket(localPort); printf("socket %d opened\n", *pfd); if ((i >= 3) && (i < 6)) { bzero((char *) &peer_addr, sizeof(peer_addr)); peer_addr.sin_family = AF_INET; peer_addr.sin_addr.s_addr = inet_addr(peerIP); peer_addr.sin_port = htons(peerPort); if (connect(*pfd , (struct sockaddr *)&peer_addr , sizeof(peer_addr)) < 0) { printf("udp connect failed!\n"); exit(1); } } pthread_t workerThread; pthread_create(&workerThread, NULL, worker, pfd); pthread_detach(workerThread); getchar(); i++; } return 0; }
在udp_receive中,每按一次按鍵,就會產生bind同一組IP與port的socket,而第4~6個socket還會connect到傳送端的IP跟port,引此能比較出一般UDP socket與connetec UDP的差別
- 執行結果
上圖為接收端的執行畫面,接收端的UDP socket會bind 127.0.0.1:5555,傳送端則綁定127.0.0.1:6666。
可以看到前六個socket依序產生的過程中,只有最後一個bind的socket才會收到封包,但是第七個socket(socket 9),最後產生
卻收不到封包,仍然是第六個socket(socket 8)收到封包。
從這個結果可以知道,如果多個socket綁定同樣的IP跟port,connect到封包來源IP:port的connected UDP socket可以優先接收,如果有多個socket滿足這個條件,則最晚bind的connected UDP socket可以收到封包。如果沒有connected UDP socket,則最晚bind的一般UDP socket可以收到封包。
