TCP/IP网络编程_第6章基于UDP的服务器端/客户端
6.1 理解 DUP
我們在第4章學習TCP的過程中, 還同時了解了 TCP/IP 協議. 在4層TCP/IP模型中, 上數第二層傳輸(Transport)層分為TCP和UDP這兩種. 數據交換過程可以分為通過TCP套接字完成的TCP方式和通過UDP套接字完成的UDP方式.
UDP 套接字的特點
下面通過信件說明 UDP 的工作原理, 這是講解UDP 時使用的傳統示例, 它與 UDP 特性完全相符. 寄信前應在信上填好寄信人和收信人的地址, 之后貼上郵票即可. 當然, 信件的特點使我們無法確認對方是否收到. 另外, 郵寄過程中也可能發生信件丟失的情況. 也就是說, 信件是一種不可靠的傳輸方式. 與之類似, UDP 提供的同樣是不可靠的數據傳輸服務.
如果只考慮可靠性, TCP 應該的確比 UDP 好. 但UDP 在結構上比 TCP 更簡潔. UDP 不會發送類似 ACK 的應答消息, 也不會像SEQ那樣給數據包分配序號. 因此, UDP的性能有時比 TCP 高出很多. 編程中實現 UDP 也比 TCP 簡單. 另外, UDP 的可靠性雖然比不上 TCP , 但也不會像想象中那么 頻繁地數據損毀. 因此, 在更重視性能而非可靠情況下, DUP 是一種很好的選擇.
既然如此, UDP 的作用到底是什么呢? 為了提供可靠的數據傳輸服務, TCP 在不可靠的IP層進行流控制, 而UDP 就缺少這種流控制機制.
是的, 流控制是區分UDP 和TCP 的最重要的標志. 但若從 TCP 中除去流控制, 所剩內容也屈指可數. 也就是說, TCP 的生命在于流控制. 第5章講過的"與對方套接字連接及斷開連接過程"也是流控制的一部分.
UDP 內部工作原理
與TCP 不同, UDP 不會進行流控制. 接下來具體討論UDP的作用, 如圖 6-1 所示.
從圖6-1中可以看出, IP的作用就是讓離開主機B的UDP數據包準確傳達到主機A. 但把UDP包最終交給主機A的某一UDP套機字的過程則是由UDP完成的. UDP最重要的作用就是根據端口號傳到主機的數據包交付給最終的UDP套接字.
UDP 的高效使用
雖然貌似大部分網絡編程都基于TCP實現, 但也有一些是基于UDP實現的. 接下來考慮何時使用UDP 更有效. 講解前希望各位明白, UDP也具有一定的可靠性. 網絡傳輸特性導致信息丟失頻繁, 可若要傳遞壓縮文件(發送1萬個數據包時, 只要丟失一個就會產生問題), 則必須使用TCP, 因為壓縮文件只要丟失一部分就很難解壓. 但通過網絡實時傳輸視頻或音頻時的情況有所不同. 對于多媒體數據而言, 丟失一部分也沒有太大問題, 這只會引起短暫的畫面抖動的情況, 對于多媒體數據而言, 丟失一部分沒有太大問題, 這只會引起短暫的畫面抖動, 或出細微的雜音. 但因為需要提供實時服務, 速度就成為非常重要的因素. 因此, 第5章的流控制就顯得有些多余, 此時需要考慮使用 UDP. 但UDP并非每次都快于 TCP, TCP比 UDP 慢的原因有一下兩點.
如果收發的數據量小但需要頻繁連接時, UDP比TCP更高效. 有機會的話, 希望各位深入學習TCP/IP 協議的內容構造. C語言程序員懂計算機結構和操作系統知識就能寫出更好的程序, 同樣, 網絡程序員若能深入理解TCP/IP協議則可大幅度 提高自身實力.
6.2 實現基于 UDP 的服務器端/客戶端
接下來通過之前介紹的UDP理論實現真正的程序. 對于UDP 而言, 只要理解之前的內容, 實現并非難事.
UDP 中的服務器端和客戶端沒有連接
UDP 服務器端/客戶端不像TCP那樣在連接狀態下交換數據, 因此與TCP不同, 無需經過連接過程. 也就是說, 不必調用TCP 連接過程中調用的listen函數和accept函數. UDP 中只有創建套接字過程和數據交換過程.
UDP 服務器端和客戶端均只需1個套接字
TCP 中, 套接字之間應該是一對一的關系. 若要向10個客戶端提供服務, 則除了守門的服務器套接字外, 還需要10個服務器套接字. 但在UDP 中, 不管是服務器端還是客戶端都只需要一個套接字. 之前解析UDP原理是舉例了信件的例子, 收發信件時使用的郵箱可以比如為UDP套接字. 只要附近有一個郵箱, 就可以通過它向任意地址寄出信件. 同樣, 只需1個UDP套接字就 可以向任意主機傳輸數據, 如圖6-2所示.
圖6-2展示了一個UDP套接字與2個不同主機交換數據的過程. 也就是說, 只需1個UDP 套接字就能和多臺主機通信.
基于 UDP 的數據 I/O 函數
創建好TCP套接字后, 傳輸數據時無需再添加地址信息. 因為TCP 套接字將保持與對方套接字的連接. 換言之, TCP 套機字知道目標地址信息. 但UDP套接字不會保持連接狀態(UDP 套機字只有簡單的郵箱功能), 因此每次傳輸數據都要添加目標地址信息. 這相當于寄信中填寫地址. 接下來介紹填寫地址并傳輸數據時調用的UDP相關函數.
上述函數與之前的TCP 輸出函數最大區別在于, 此函數需要向它傳遞目標地址信息. 接下來介紹接收UDP數據的函數. UDP數據的發送端并不固定, 因此該函數定義為可接收發送端信息的形式, 也就是將同時返回UDP數據包中的發送信息.
編寫UDP程序是最核心的部分就在于上述兩個函數, 這也說明二者在UDP 數據傳輸中的地位.
基于 UDP 的回聲服務器端/客戶端
下面結合之前的內容實現回聲服務器. 需要注意的是, UDP 不同于 TCP, 不存在請求連接和受理過程, 因此在某種意義上無法明確區分服務器端和客戶端. 只是因其提供服務而稱為服務器端, 希望各位不要誤解.
服務器端:
客戶端:
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <sys/socket.h> #include <arpa/inet.h> #include <string.h>#define BUF_SIZE 30void error_handlin(char *message);int main(int argc, char *argv[]) {int sock;char message[BUF_SIZE];int str_len;socklen_t adr_sz;struct sockaddr_in serv_adr, from_adr;if (argc != 3){printf("Usage ; %s <IP> <port>\n", argv[0]);exit(1);}sock = socket(PF_INET, SOCK_DGRAM, 0);if (sock == -1){error_handlin("socket() error");}memset(&serv_adr, 0, sizeof(serv_adr));serv_adr.sin_family = AF_INET;serv_adr.sin_addr.s_addr = inet_addr(argv[1]);serv_adr.sin_port = htons(atoi(argv[2]));while(1){fputs("Insert message(q to quit): ", stdout);fgets(message, sizeof(message), stdin);if (!strcmp(message, "q\n") || !strcmp(message, "Q\n")){break;}sendto(sock, message, strlen(message), 0, (struct sockaddr*)&serv_adr, sizeof(serv_adr));adr_sz = sizeof(from_adr);str_len = recvfrom(sock, message, BUF_SIZE, 0, (struct sockaddr*)&from_adr, &adr_sz);message[str_len] = 0;printf("Message from server: %s", message);}close(sock);return 0; }void error_handlin(char *message) {fputs(message, stderr);fputc('\n', stderr);exit(1); }客戶端;’
服務器端:
運行過程中的順序并不重要. 只需保證在調用sendto函數前, sendto函數的目標主機程序已經開始運行.
UDP 客戶端套接字的地址分配
前面講解了 UDP 服務器/客戶端的實現方法. 但如果仔細觀察 UDP 客戶端會發現, 它缺少把IP和端口分配的過程給套接字的過程. TCP 客戶端調用connect 函數自動完成此過程, 而UDP 中連能承擔相同的函數語句都沒有. 究竟在何處分配IP 和 端口號呢?
UDP 程序中, 調用sendto函數傳輸數據強應完成對套接字的地址分配工作, 因此調用bind函數. 當然, bind函數在TCP 程序中出現過, 但bind函數不區分和 UDP , 也就是說, 在UDP 程序中同樣可以調用. 另外, 如果調用sendto 函數時尚未分配地址信息, 則在首次調用sendto函數是給出相應的套接字自動分配IP和端口. 而且此時分配的地址一直保留到程序結束為止, 因此也可用來與其他UDP 套接字進行數據交換. 當然, IP用主機IP, 端口號尚未使用的任意端口號.
綜上所述, 調用sendto 函數是自動分配IP和端口號, 因此, UDP 客戶端中通常無需額外的地址分配過程. 所以之前實例中省略了該過程, 這也是普遍的實現方式.
6.3 UDP 的數據傳輸特性和調用connect 函數
我們之前通過實例驗證了TCP傳輸的數據不存在數據邊界, 本節講驗證UDP 數據傳輸中存在數據邊界. 最后討論UDP 中connect 函數的調用, 以此結束UDP 相關討論.
存在數據邊界的 UDP 套接字
前面說過TCP 數據傳輸中不存在邊界, 這表示"數據傳輸過程中調用I/O函數的次數不具有任何意義."
相反, UDP 是具有數據邊界的協議, 傳輸中調用I/O函數的次數非常重要. 因此, 輸入函數的調用次數和輸出函數的調用次數完全一致, 這樣才能保證接收全部已發數據. 例如, 調用3次輸出函數發送的數據必須通過調用3次輸入函數才能接收完. 下面通過簡單實例進行驗證.
#include <stdlib.h> #include <stdio.h> #include <string.h> #include <arpa/inet.h> #include <sys/socket.h> #include <unistd.h>#define BUF_SIZE 30void error_handling(char *message);int main(int argc, char *argv[]) {int sock;char message[BUF_SIZE];struct sockaddr_in my_adr, your_adr;socklen_t adr_sz;int str_len, i;if (argc != 2){printf("Usage : %s <port> \n", argv[0]);exit(1);}sock = socket(PF_INET, SOCK_DGRAM, 0);if (sock == -1){error_handling("socket() error");}memset(&my_adr, 0, sizeof(my_adr));my_adr.sin_family = AF_INET;my_adr.sin_addr.s_addr = htonl(INADDR_ANY);my_adr.sin_port = htons(atoi(argv[1]));if (bind(sock, (struct sockaddr*)&my_adr, sizeof(my_adr)) == -1){error_handling("bind() error");}for (i=0; i<3; i++){sleep(5);adr_sz = sizeof(your_adr);str_len = recvfrom(sock, message, BUF_SIZE, 0, (struct sockaddr*)&your_adr, &adr_sz);printf("Message %d : %s \n", i+1, message);}close(sock);return 0; }void error_handling(char *message) {fputs(message, stderr);fputc('\n', stderr);exit(1); }(請到https://www.jiumodiary.com/)
下面本書
上述實例中需要各位特別留意的是第30行中的for語句. 首先在第32行中調用sleep函數, 使程序停頓時間等于傳遞的時間(以秒為單位)參數. 也就是說, 第30行的for循環中每隔5秒調用1次recvfrom 函數. 另外還添加了驗證函數調用次數的語句. 稍后再講解延遲執行程序的原因.
接下來的實例向之前的bound_host1.c 傳輸數據, 該實例共調用sendto函數3次以傳輸字符串數據.
客戶端:
運行結果:
客戶端:
服務器端:
證明必須在UDP通信過程是I/O函數調用次數保存一致.
已連接(connected) UDP 套機字與未連接(unconnected) UDP 套接字
TCP 套接字中需注冊待傳輸的目標IP和端口號, 而UDP中則無需注冊. 因此, 通過sendto 函數傳輸數據的過程大致可分以下3階段.
每次調用sendto函數時重復上述過程. 每次都變更目標地址. 因此可以重復利用統一UDP套接字向不同目標傳輸數據. 這種未注冊目標地址信息的套機字稱為未連接套機字, 反之, 注冊了目標地址的套機字稱為連接connected 套接字. 顯然, UDP 套機字默認屬于未連接套機字, 但UDP套接字在下述情況顯得不太合理:
此時需要重復3次上述三階段. 因此, 要與同一主機進行長時間通信時, 將UDP套機字變成已連接套接字會提高效率. 上述三個階段中, 第一個和第三個階段占整個通信過程接近1/3的時間, 縮短這部分時間將大大提高整體性能.
創建已連接UDP套接字
創建已連接UDP套接字的過程格外簡單, 只需針對UDP套接字調用connect函數.
上述代碼看似與TCP套接字創建過程一致, 但socket函數的第二個參數分明是SOCK_DGRAM. 也就是說, 創建的的確是UDP套接字. 當然, 針對UDP 套機字調用connect 函數并不意味著要與對方UDP套機字連接, 這只是向UDP 套機字注冊目標IP和端口信息.
之后就與TCP 套機字一樣, 每次調用sendto 函數時只需傳遞數據. 因為已經指定了收發對象, 所以可以使用sendto, recvfrom函數, 還可以使用write, read 函數進行通信.
下列實例將之前的uecho_client.c 程序改成基于已連接UDP 套機字的程序, 因此可以結合uecho_server.c 程序運行. 另外, 為便于說明, 未直接刪除uecho_client.c 的I/O 函數, 而是添加了注釋.
#include <stdio.h> #include <stdlib.h> #include <sys/socket.h> #include <arpa/inet.h> #include <string.h> #include <unistd.h>#define BUF_SIZE 30 void error_handling(char *message);int main(int argc, char *argv[]) {int sock;char message[BUF_SIZE];int str_len;socklen_t adr_sz; /* 多余變量 */struct sockaddr_in serv_adr, from_adr; /* 不再需要from_adr! */if (argc != 3){printf("Usage : %s <IP> <port> \n", argv[0]);exit(1);}sock = socket(PF_INET, SOCK_DGRAM, 0);if (sock == -1){error_handling("socket() error");}memset(&serv_adr, 0, sizeof(serv_adr));serv_adr.sin_family = AF_INET;serv_adr.sin_addr.s_addr = inet_addr(argv[1]);serv_adr.sin_port = htons(atoi(argv[2]));connect(sock, (struct sockaddr*)&serv_adr, sizeof(serv_adr));while(1){fputs("Insert message(q to Q): ", stdout);fgets(message, sizeof(message), stdin);if (!strcmp(message, "q\n") || !strcmp(message, "Q\n")){break;}write(sock, message, strlen(message));str_len = read(sock, message, sizeof(message)-1);message[str_len] = 0;printf("Message from server: %s", message);}close(sock);return 0; }void error_handling(char *message) {fputs(message, stderr);fputc('\n', stderr);exit(1); }沒有運行結果和代碼說明, 代碼中用write read 函數代替sendto, recvfrom函數.
6.4 基于Windows 的實現
首先介紹Windows 平臺下的sendto 函數好readfrom 函數, 實際上與 Linux 的函數沒有太大區別, 但為了各位親自確認這一點, 這里給出定義.
以上兩個函數與 Linux 下的sendto, recvfrom 函數相比, 其參數個數, 順序及含義完全相同, 故省略具體說明, 接下來實現Windows平臺下的UDP 回聲服務器端/客戶端. 其中, 回聲服務器端是利用已連接UDP 套接字實現的.
服務器端
客戶端:
#include <stdio.h> #include <stdlib.h> #include <WinSock2.h> #include <string.h>#define BUF_SIZE 30void ErrorHandling(const char* message);int main(int argc, char* argv[]) {WSADATA wsaData;SOCKET sock;char message[BUF_SIZE];int strLen;SOCKADDR_IN servAdr;if (argc != 3){printf("Usage : %s <IP> <port> \n", argv[0]);exit(1);}if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0){ErrorHandling("WSAStartup() error");}sock = socket(PF_INET, SOCK_DGRAM, 0);if (sock == INVALID_SOCKET){ErrorHandling("socket() error");}memset(&servAdr, 0, sizeof(servAdr));servAdr.sin_family = AF_INET;servAdr.sin_addr.s_addr = inet_addr(argv[1]);servAdr.sin_port = htons(atoi(argv[2]));connect(sock, (SOCKADDR*)&servAdr, sizeof(servAdr));while (1){fputs("Insert message(q to quit): ", stdout);fgets(message, sizeof(message), stdin);if (!strcmp(message, "q\n") || !strcmp(message, "Q\n")){break;}send(sock, message, strlen(message), 0);strLen = recv(sock, message, sizeof(message) - 1, 0);message[strLen] = 0;printf("Message from server: %s", message);}closesocket(sock);WSACleanup();return 0; }void ErrorHandling(const char* message) {fputs(message, stderr);fputc('\n', stderr);exit(1); }運行結果:
客戶端:
上述客戶端實例利用已連接UDP套接字進行輸入輸出, 因此用send, recv 函數替代sendto , recvfrom 函數. 此外也如實反映了已連接UDP 套接字的好處.
結語:
你可以在下面這個網站, 下載這本書
https://www.jiumodiary.com
時間: 2020-05-30
總結
以上是生活随笔為你收集整理的TCP/IP网络编程_第6章基于UDP的服务器端/客户端的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: [集训队作业2018]uoj 449 喂
- 下一篇: 我的编程之路点滴记录(三)