【TCP/IP網絡編程】:06基於UDP的服務器端/客戶端


本篇文章簡單描述了UDP傳輸協議的工作原理及特點。

理解UDP

UDP和TCP一樣同屬於TCP/IP協議棧的第二層,即傳輸層。

UDP套接字的特點

UDP的工作方式類似於傳統的信件郵寄過程。寄信前應先在信封上填好寄信人和收信人的地址,之后貼上郵票放進郵筒即可。當然信件郵寄過程可能會發生丟失,我們也無法隨時知曉對方是否已收到信件。也就是說信件是一種不可靠的傳輸方式,同樣的,UDP所提供的也是一種不可靠的數據傳輸方式(以信件類比UDP只是通信形式上一致性,之前也以電話通信的方式類比了TCP的通信方式,而實際上從通信速度上來講UDP通常是要快於TCP的;每次交換的數據量越大,TCP的傳輸速率就越接近於UDP)。因此,如果僅考慮可靠性,TCP顯然由於UDP;但UDP在通信結構上較TCP更為簡潔,通常性能也要優於TCP。

區分TCP和UDP最重要的標志是流控制,流控制賦予了TCP可靠性的特點,也說TCP的生命在於流控制。

UDP內部工作原理

與TCP不同,UDP不會進行流控制,其在數據通信中的作用如下圖所示。可以看出,IP的作用就是讓離開主機B的UDP數據包准確傳遞到主機A,而UDP則是把UDP包最終交給主機A的某一UDP套接字。UDP最重要的作用就是根據端口號將傳輸到主機的數據包交付給最終的UDP套接字。

 數據包傳輸過程UDP和IP的作用

UDP的高效使用

TCP用於對可靠性要求較高的場景,比如要傳輸一個重要文件或是壓縮包,這種情況往往丟失一個數據包就會引起嚴重的問題;而對於多媒體數據來說,丟失一部分數據包並沒有太大問題,因為實時性更為重要,速度就成為了重要考慮因素。TCP慢於UDP主要在於以下兩點:

  • 收發數據前后進行的連接及清理過程
  • 收發數據過程中為保證可靠性而添加的流控制

因此,如果收發的數據量小但需要頻繁的連接時,UDP比TCP更為高效。

基於UDP的服務器端/客戶端

和TCP不同,UDP服務器端/客戶端並不需要在連接狀態下交換數據,UDP的通信只有創建套接字和數據交換的過程。TCP套接字是一對一的關系,且服務器端還需要一個額外的TCP套接字用於監聽連接請求;而UDP通信中,無論服務器端還是客戶端都只需要一個套接字即可,且可以實現一對多的通信關系。下圖展示了一個UDP套接字與兩台主機進行數據交換的過程。

 UDP套接字通信模型

基於UDP的數據I/O函數

TCP套接字建立連接之后,數據傳輸過程便無需額外添加地址信息,因為TCP套接字會保持與對端的連接狀態;而UDP則沒有這種連接狀態,因此每次數據交換過程都需要添加目標地址信息。下面是UDP套接字數據傳輸函數,與TCP傳輸函數最大的區別在於,該函數需要額外添加傳遞目標的地址信息。

#include <sys/socket.h>

ssize_t sendto(int sock, void *buff, size_t nbytes, int flags, struct sockaddr *to, socklen_t addrlen);
    -> 成功時返回傳輸的字節數,失敗時返回-1

由於UDP數據的發送端並不固定,因此,UDP套接字的數據接收函數定義了存儲發送端地址信息的數據結構。

#include <sys/socket.h>

ssize_t recvfrom(int sock, void *buff, size_t nbytes, int flags, struct sockaddr *from, socklen_t addrlen);
    -> 成功時返回接收的字節數,失敗時返回-1

基於UDP的回聲服務器端/客戶端

UDP通信函數調用流程

UDP不同於TCP,不存在請求連接和受理連接的過程,因此某種意義上並沒有明確的服務器端和客戶端之分。如下是示例源碼。

 1 #include <stdio.h>
 2 #include <stdlib.h>
 3 #include <string.h>
 4 #include <unistd.h>
 5 #include <arpa/inet.h>
 6 #include <sys/socket.h>
 7 
 8 #define BUF_SIZE 30
 9 void error_handling(char *message);
10 
11 int main(int argc, char *argv[])
12 {
13     int serv_sock;
14     char message[BUF_SIZE];
15     int str_len;
16     socklen_t clnt_adr_sz;
17     
18     struct sockaddr_in serv_adr, clnt_adr;
19     if(argc!=2){
20         printf("Usage : %s <port>\n", argv[0]);
21         exit(1);
22     }
23     
24     serv_sock=socket(PF_INET, SOCK_DGRAM, 0);
25     if(serv_sock==-1)
26         error_handling("UDP socket creation error");
27     
28     memset(&serv_adr, 0, sizeof(serv_adr));
29     serv_adr.sin_family=AF_INET;
30     serv_adr.sin_addr.s_addr=htonl(INADDR_ANY);
31     serv_adr.sin_port=htons(atoi(argv[1]));
32     
33     if(bind(serv_sock, (struct sockaddr*)&serv_adr, sizeof(serv_adr))==-1)
34         error_handling("bind() error");
35 
36     while(1) 
37     {
38         clnt_adr_sz=sizeof(clnt_adr);
39         str_len=recvfrom(serv_sock, message, BUF_SIZE, 0, 
40                                 (struct sockaddr*)&clnt_adr, &clnt_adr_sz);
41         sendto(serv_sock, message, str_len, 0, 
42                                 (struct sockaddr*)&clnt_adr, clnt_adr_sz);
43     }    
44     close(serv_sock);
45     return 0;
46 }
47 
48 void error_handling(char *message)
49 {
50     fputs(message, stderr);
51     fputc('\n', stderr);
52     exit(1);
53 }
uecho_server
 1 #include <stdio.h>
 2 #include <stdlib.h>
 3 #include <string.h>
 4 #include <unistd.h>
 5 #include <arpa/inet.h>
 6 #include <sys/socket.h>
 7 
 8 #define BUF_SIZE 30
 9 void error_handling(char *message);
10 
11 int main(int argc, char *argv[])
12 {
13     int sock;
14     char message[BUF_SIZE];
15     int str_len;
16     socklen_t adr_sz;
17     
18     struct sockaddr_in serv_adr, from_adr;
19     if(argc!=3){
20         printf("Usage : %s <IP> <port>\n", argv[0]);
21         exit(1);
22     }
23     
24     sock=socket(PF_INET, SOCK_DGRAM, 0);   
25     if(sock==-1)
26         error_handling("socket() error");
27     
28     memset(&serv_adr, 0, sizeof(serv_adr));
29     serv_adr.sin_family=AF_INET;
30     serv_adr.sin_addr.s_addr=inet_addr(argv[1]);
31     serv_adr.sin_port=htons(atoi(argv[2]));
32     
33     while(1)
34     {
35         fputs("Insert message(q to quit): ", stdout);
36         fgets(message, sizeof(message), stdin);     
37         if(!strcmp(message,"q\n") || !strcmp(message,"Q\n"))    
38             break;
39         
40         sendto(sock, message, strlen(message), 0, 
41                     (struct sockaddr*)&serv_adr, sizeof(serv_adr));
42         adr_sz=sizeof(from_adr);
43         str_len=recvfrom(sock, message, BUF_SIZE, 0, 
44                     (struct sockaddr*)&from_adr, &adr_sz);
45 
46         message[str_len]=0;
47         printf("Message from server: %s", message);
48     }    
49     close(sock);
50     return 0;
51 }
52 
53 void error_handling(char *message)
54 {
55     fputs(message, stderr);
56     fputc('\n', stderr);
57     exit(1);
58 }
uecho_client

示例代碼運行結果

UDP客戶端套接字的地址分配

從上述示例源碼來看,服務器端UDP套接字需要手動bind地址信息,而客戶端UDP套接字則無此過程。我們已經知道,客戶端TCP套接字是在調用connect函數的時機,由操作系統為我們自動綁定了地址信息;而客戶端UDP套接字同樣存在該過程,如果沒有手動bind地址信息,則在首次調用sendto函數時自動分配IP和端口號等地址信息。和TCP一樣,IP是主機IP,端口號則隨機分配(客戶的臨時端口是在第一次調用sendto 時一次性選定,不能改變;然而客戶的IP地址卻可以隨客戶發送的每個UDP數據報而變動(如果客戶沒有綁定一個具體的IP地址到其套接字上)。其原因在於如果客戶主機是多宿的,客戶有可能在兩個目的地之間交替選擇)。

UDP數據傳輸特性和connect函數調用

之前我們介紹了TCP傳輸數據不存在數據邊界,下面將會驗證UDP數據傳輸存在數據邊界的特性,並介紹UDP傳輸調用connect函數的作用。

存在數據邊界的UDP套接字

UDP協議具有數據邊界,這就意味着數據交換的雙方輸入函數和輸出函數必須一一對應,這樣才能保證可完整接收數據。如下是驗證UDP存在數據邊界的示例源碼。

 1 #include <stdio.h>
 2 #include <stdlib.h>
 3 #include <string.h>
 4 #include <unistd.h>
 5 #include <arpa/inet.h>
 6 #include <sys/socket.h>
 7 
 8 #define BUF_SIZE 30
 9 void error_handling(char *message);
10 
11 int main(int argc, char *argv[])
12 {
13     int sock;
14     char message[BUF_SIZE];
15     struct sockaddr_in my_adr, your_adr;
16     socklen_t adr_sz;
17     int str_len, i;
18 
19     if(argc!=2){
20         printf("Usage : %s <port>\n", argv[0]);
21         exit(1);
22     }
23     
24     sock=socket(PF_INET, SOCK_DGRAM, 0);
25     if(sock==-1)
26         error_handling("socket() error");
27     
28     memset(&my_adr, 0, sizeof(my_adr));
29     my_adr.sin_family=AF_INET;
30     my_adr.sin_addr.s_addr=htonl(INADDR_ANY);
31     my_adr.sin_port=htons(atoi(argv[1]));
32     
33     if(bind(sock, (struct sockaddr*)&my_adr, sizeof(my_adr))==-1)
34         error_handling("bind() error");
35     
36     for(i=0; i<3; i++)
37     {
38         sleep(5);    // delay 5 sec.
39         adr_sz=sizeof(your_adr);
40         str_len=recvfrom(sock, message, BUF_SIZE, 0, 
41                                 (struct sockaddr*)&your_adr, &adr_sz);     
42     
43         printf("Message %d: %s \n", i+1, message);
44     }
45     close(sock);    
46     return 0;
47 }
48 
49 void error_handling(char *message)
50 {
51     fputs(message, stderr);
52     fputc('\n', stderr);
53     exit(1);
54 }
55 
56 /*
57 root@my_linux:/home/swyoon/tcpip# gcc bound_host1.c -o host1
58 root@my_linux:/home/swyoon/tcpip# ./host1 
59 Usage : ./host1 <port>
60 root@my_linux:/home/swyoon/tcpip# ./host1 9190
61 Message 1: Hi! 
62 Message 2: I'm another UDP host! 
63 Message 3: Nice to meet you 
64 root@my_linux:/home/swyoon/tcpip# 
65 
66 */
bound_hostA
 1 #include <stdio.h>
 2 #include <stdlib.h>
 3 #include <string.h>
 4 #include <unistd.h>
 5 #include <arpa/inet.h>
 6 #include <sys/socket.h>
 7 
 8 #define BUF_SIZE 30
 9 void error_handling(char *message);
10 
11 int main(int argc, char *argv[])
12 {
13     int sock;
14     char message[BUF_SIZE];
15     struct sockaddr_in my_adr, your_adr;
16     socklen_t adr_sz;
17     int str_len, i;
18 
19     if(argc!=2){
20         printf("Usage : %s <port>\n", argv[0]);
21         exit(1);
22     }
23     
24     sock=socket(PF_INET, SOCK_DGRAM, 0);
25     if(sock==-1)
26         error_handling("socket() error");
27     
28     memset(&my_adr, 0, sizeof(my_adr));
29     my_adr.sin_family=AF_INET;
30     my_adr.sin_addr.s_addr=htonl(INADDR_ANY);
31     my_adr.sin_port=htons(atoi(argv[1]));
32     
33     if(bind(sock, (struct sockaddr*)&my_adr, sizeof(my_adr))==-1)
34         error_handling("bind() error");
35     
36     for(i=0; i<3; i++)
37     {
38         sleep(5);    // delay 5 sec.
39         adr_sz=sizeof(your_adr);
40         str_len=recvfrom(sock, message, BUF_SIZE, 0, 
41                                 (struct sockaddr*)&your_adr, &adr_sz);     
42     
43         printf("Message %d: %s \n", i+1, message);
44     }
45     close(sock);    
46     return 0;
47 }
48 
49 void error_handling(char *message)
50 {
51     fputs(message, stderr);
52     fputc('\n', stderr);
53     exit(1);
54 }
55 
56 /*
57 root@my_linux:/home/swyoon/tcpip# gcc bound_host1.c -o host1
58 root@my_linux:/home/swyoon/tcpip# ./host1 
59 Usage : ./host1 <port>
60 root@my_linux:/home/swyoon/tcpip# ./host1 9190
61 Message 1: Hi! 
62 Message 2: I'm another UDP host! 
63 Message 3: Nice to meet you 
64 root@my_linux:/home/swyoon/tcpip# 
65 
66 */
bound_hostB

示例代碼運行結果

調用connect函數的UDP套接字

TCP套接字需要手動注冊傳輸數據的目標IP和端口號,而UDP則是調用sendto函數時自動完成目標地址信息的注冊,該過程如下

  • 第一階段:向UDP套接字注冊目標IP和端口號
  • 第二階段:傳輸數據
  • 第三階段:刪除UDP套接字中注冊的目標地址信息

每次調用sendto函數都會重復執行以上過程,這也是為什么同一個UDP套接字可和不同目標進行數據交換的原因。像UDP這種未注冊目標地址信息的套接字稱為未連接套接字,而TCP這種注冊了目標地址信息的套接字稱為已連接connected套接字。當需要和同一目標主機進行長時間通信時,UDP的這種無連接的特點則會非常低效。通過調用connect函數使UDP變為已連接套接字則會有效改善這一點,因為上述的第一階段和第三階段會占用整個通信過程近1/3的時間。

已連接的UDP套接字不僅可以使用之前的sendto和recvfrom函數,還可以使用沒有地址信息參數的write和read函數(需要注意的是,調用connect函數的已連接UDP套接字並非真的與目標UDP套接字建立連接,僅僅是向本端UDP套接字注冊了目標IP和端口號信息而已)。修改之前的ucheo_client代碼為已連接UDP套接字如下。

 1 #include <stdio.h>
 2 #include <stdlib.h>
 3 #include <string.h>
 4 #include <unistd.h>
 5 #include <arpa/inet.h>
 6 #include <sys/socket.h>
 7 
 8 #define BUF_SIZE 30
 9 void error_handling(char *message);
10 
11 int main(int argc, char *argv[])
12 {
13     int sock;
14     char message[BUF_SIZE];
15     int str_len;
16     socklen_t adr_sz;
17     
18     struct sockaddr_in serv_adr, from_adr;
19     if(argc!=3){
20         printf("Usage : %s <IP> <port>\n", argv[0]);
21         exit(1);
22     }
23     
24     sock=socket(PF_INET, SOCK_DGRAM, 0);   
25     if(sock==-1)
26         error_handling("socket() error");
27     
28     memset(&serv_adr, 0, sizeof(serv_adr));
29     serv_adr.sin_family=AF_INET;
30     serv_adr.sin_addr.s_addr=inet_addr(argv[1]);
31     serv_adr.sin_port=htons(atoi(argv[2]));
32     
33     connect(sock, (struct sockaddr*)&serv_adr, sizeof(serv_adr));
34 
35     while(1)
36     {
37         fputs("Insert message(q to quit): ", stdout);
38         fgets(message, sizeof(message), stdin);     
39         if(!strcmp(message,"q\n") || !strcmp(message,"Q\n"))    
40             break;
41         /*
42         sendto(sock, message, strlen(message), 0, 
43                     (struct sockaddr*)&serv_adr, sizeof(serv_adr));
44         */
45         write(sock, message, strlen(message));
46 
47         /*
48         adr_sz=sizeof(from_adr);
49         str_len=recvfrom(sock, message, BUF_SIZE, 0, 
50                     (struct sockaddr*)&from_adr, &adr_sz);
51         */
52         str_len=read(sock, message, sizeof(message)-1);
53 
54         message[str_len]=0;
55         printf("Message from server: %s", message);
56     }    
57     close(sock);
58     return 0;
59 }
60 
61 void error_handling(char *message)
62 {
63     fputs(message, stderr);
64     fputc('\n', stderr);
65     exit(1);
66 }
uecho_con_client

關於recvfrom函數的思考

recvfrom是一個阻塞函數,那么該函數的返回時機是怎樣的?

顯然如果客戶端正常收到應答數據,recvfrom自然可以返回。但如果發生其他情況呢?

對端調用close函數關閉UDP套接字時是否會發送EOF信息,本端recvfrom函數又會有什么動作嗎?是否會像TCP套接字的read函數那樣收到EOF信息而返回0?

由於UDP套接字無連接的特性,即使對端調用close函數關閉套接字,本端也不會有任何感知,recvfrom自然不會返回。那如果是調用了connect函數的已連接UDP套接字呢,服務端的close函數調用是否會使客戶端的recvfrom函數退出阻塞狀態?

仍然不會。因為調用connect函數的已連接UDP套接字並非真的像TCP套接字那樣建立了連接,僅僅是為了數據交換的便利性向本端UDP套接字注冊了目標地址信息而已;而對端並不能感知到這些,close函數自然也不會向TCP那樣向本端發送文件結束標志EOF。因此,正常情況下,只有接收到發送端消息的recvfrom函數才會退出阻塞狀態而返回。

如果一個客戶端數據報丟失(譬如說,被客戶主機與服務主機之間的某個路由器丟棄),客戶端將永遠阻塞於recvfrom 調用,等待一個永遠不會到達的服務器應答。類似地,如果客戶端數據報到達服務器,但是服務器的應答丟失了,客戶端也將永遠阻塞於recvfrom 調用。防止這樣永久阻塞的一般方法是給客戶端的recvfrom 調用設置一個超時時間。

關於recvfrom函數和UDP協議的進一步內容可以參考《UNIX網絡編程》等相關書籍。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM