技術分析:基本 UDP 套接字編程
UDP 協議和 TCP 協議不同,它是一種面向無連接、不可靠的傳輸層協議。在基于 UDP 套接字編程中,數據傳輸可用函數 sendto 和 recvfrom。以下是基本 UDP 套接字編程過程:
sendto 與 recvfrom 函數
這兩個函數的功能類似于 write 和 read 函數,可用無連接的套接字編程。其定義如下:
- /* 函數功能:發送數據;
- * 返回值:若成功則返回已發送的字節數,若出錯則返回-1;
- * 函數原型:
- */
- #include <sys/socket.h>
- ssize_t sendto(int sockfd, void *buff, size_t nbytes, int flags,
- const struct sockaddr *destaddr, socklen_t addrlen);
- /* 說明:
- * 該函數功能類似于write函數,除了有標識符flags和目的地址信息之外,其他參數一樣;
- *
- * flags標識符取值如下:
- * (1)MSG_DONTROUTE 勿將數據路由出本地網絡
- * (2)MSG_DONTWAIT 允許非阻塞操作
- * (3)MSG_EOR 如果協議支持,此為記錄結束
- * (4)MSG_OOB 如果協議支持,發送帶外數據
- *
- * 若sendto成功,則只是表示已將數據無錯誤的發送到網絡,并不能保證正確到達對端;
- * 該函數通過指定目標地址允許在無連接的套接字之間發送數據(例如UDP套接字);
- */
- /* 函數功能:接收數據;
- * 返回值:以字節計數的消息長度,若無可用消息或對方已經按序結束則返回0,若出錯則返回-1;
- * 函數原型:
- */
- #include <sys/socket.h>
- ssize_t recvfrom(int sockfd, void *buff, size_t nbytes, int flags,
- struct sockaddr *addr, socklen_t *addrlen);
- /* 說明:
- * 該函數功能與read類似;
- * 若addr為非空時,它將包含數據發送者的套接字地址;
- *
- * flags標識符取值如下:
- * (1)MSG_WAITALL 等待所有數據可用
- * (2)MSG_DONTWAIT 允許非阻塞操作
- * (3)MSG_PEEK 查看已讀取的數據
- * (4)MSG_OOB 如果協議支持,發送帶外數據
- */
基于 UDP 套接字編程
下面我們使用 UDP 協議實現簡單的功能,客戶端從標準輸入讀取數據并把它發送給服務器,服務器接收到數據并把該數據回射給客戶端,然后客戶端收到從服務器回射的數據把它顯示到標準輸出。其功能實現如下圖所示:
服務器程序
- /* UDP 服務器 */
- #include <string.h>
- #include <stdio.h>
- #include <unistd.h>
- #include <stdlib.h>
- #include <sys/socket.h>
- #include <netinet/in.h>
- #define SERV_PORT 9877 /* 通用端口號 */
- extern void err_sys(const char *, ...);
- extern void dg_echo(int sockfd, struct sockaddr *addr, socklen_t addrlen);
- int main(int argc, char **argv)
- {
- int sockfd;
- int err;
- struct sockaddr_in servaddr, cliaddr;
- /* 初始化服務器地址信息 */
- bzero(&servaddr, sizeof(servaddr));
- servaddr.sin_family = AF_INET;
- servaddr.sin_port = htons(SERV_PORT);
- servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
- /* 創建套接字,并將服務器地址綁定到該套接字上 */
- if( (sockfd = socket(AF_INET, SOCK_DGRAM, 0)) < 0)
- err_sys("socket error");
- err =bind(sockfd, (struct sockaddr*)&servaddr, sizeof(servaddr));
- if(err < 0)
- err_sys("bind error");
- /* 服務器處理函數:讀取套接字文本行,并把它回射給客戶端 */
- dg_echo(sockfd, (struct sockaddr*) &cliaddr, sizeof(cliaddr));
- }
處理函數
- #include "unp.h"
- void
- dg_echo(int sockfd, SA *pcliaddr, socklen_t clilen)
- {
- int n;
- socklen_t len;
- char mesg[MAXLINE];
- for ( ; ; ) {
- len = clilen;
- n = Recvfrom(sockfd, mesg, MAXLINE, 0, pcliaddr, &len);
- Sendto(sockfd, mesg, n, 0, pcliaddr, len);
- }
- }
#p#客戶端程序
- /* UDP 客戶端 */
- #include <string.h>
- #include <stdio.h>
- #include <unistd.h>
- #include <stdlib.h>
- #include <sys/socket.h>
- #include <netinet/in.h>
- #include <arpa/inet.h>
- #define SERV_PORT 9877 /* 通用端口號 */
- extern void err_sys(const char *, ...);
- extern void err_quit(const char *, ...);
- extern void dg_cli(FILE *fd, int sockfd, struct sockaddr *addr, socklen_t addrlen);
- int main(int argc, char **argv)
- {
- int sockfd;
- struct sockaddr_in servaddr;
- if (argc != 2)
- err_quit("usage: udpcli <IPaddress>");
- bzero(&servaddr, sizeof(servaddr));
- servaddr.sin_family = AF_INET;
- servaddr.sin_port = htons(SERV_PORT);
- inet_pton(AF_INET, argv[1], &servaddr.sin_addr);
- if( (sockfd = socket(AF_INET, SOCK_DGRAM, 0)) < 0)
- err_sys("socket err");
- /* 客戶端處理函數:從標準輸入讀入文本行,發送給服務器;接收來自服務器的回射文本,并把它顯示到標準輸出 */
- dg_cli(stdin, sockfd, (struct sockaddr *) &servaddr, sizeof(servaddr));
- exit(0);
- }
客戶端處理函數
- #include "unp.h"
- void
- dg_cli(FILE *fp, int sockfd, const SA *pservaddr, socklen_t servlen)
- {
- int n;
- char sendline[MAXLINE], recvline[MAXLINE + 1];
- while (Fgets(sendline, MAXLINE, fp) != NULL) {
- /* 把從標準輸入讀取的文本行發送給服務器套接字 */
- Sendto(sockfd, sendline, strlen(sendline), 0, pservaddr, servlen);
- /* 接收來自服務器回射的文本行 */
- n = Recvfrom(sockfd, recvline, MAXLINE, 0, NULL, NULL);
- recvline[n] = 0; /* null terminate */
- Fputs(recvline, stdout);
- }
- }
- $./serv &
- [1] 17911
- $ ./client 127.0.0.1
- sending text based on UDP
- sending text based on UDP
- goodbyte..
- goodbyte..
數據報丟失
由于 UDP 是一種不可靠的傳輸協議。在上面的客戶端 / 服務器 程序中,若數據報在傳輸的過程中丟失,那么客戶端就是阻塞于 dg_cli 處理函數中的 recvfrom 函數調用,等待一個永遠都不會達到的服務器應答。也有可能是,客戶端數據報成功到達服務器,但是服務器的應答數據報丟失,同樣,客戶端也將永遠阻塞于 recvfrom 函數調用。一般來說,會給客戶端 recvfrom 函數調用設置一個超時時鐘,但是超時時鐘并不能確定是客戶端數據報不能到達服務器還是服務器應答不能到達客戶端。所以我們可以采用驗證接收到的響應。即在 recvfrom 函數調用以返回數據報發送者的 IP 地址和端口號,保留來自數據報所發往服務器的應答。
UDP 中使用 connect 函數
在沒有啟動 UDP 服務器的情況下,客戶端鍵入文本行之后,并不會回顯該文本行。此時客戶端永遠阻塞于它的 recvfrom 調用,等待一個永遠不會出現的服務器應答。由于服務器沒有啟動,因此會響應一個端口不可到達的 ICMP 錯誤消息(即異步錯誤),但是該 ICMP 錯誤消息并不會到達客戶端進程,因此客戶端進程根本不知道發生什么,一直阻塞于它的 recvfrom 調用。為了能使這個異步錯誤到達客戶端進程,我們可以在 UDP 中調用 connect 函數,使其成為一個已連接的 UDP 套接字,但是該鏈接不會像 TCP 那樣引起三次握手過程。內核只是檢查是否存在立即可知的錯誤,并記錄對端的 IP 地址和端口號,然后立即返回到調用進程。
下面要區分 未連接 UDP 套接字 和 已連接 UDP 套接字:
● 未連接 UDP 套接字:新創建 UDP 套接字默認為該情況;
● 已連接 UDP 套接字:對 UDP 套接字調用 connect 函數的結果;
已連接 UDP 套接字 相對于 未連接 UDP 套接字 會有以下的變化:
1、不能給輸出操作指定目的 IP 地址和端口號(因為調用 connect 函數時已經指定),即不能使用 sendto 函數,而是使用 write 或 send 函數。寫到已連接 UDP 套接字上的內容都會自動發送到由 connect 指定的協議地址;
2、不必使用 recvfrom 函數以獲悉數據報的發送者,而改用 read、recv 或 recvmsg 函數。在一個已連接 UDP 套接字上,由內核為輸入操作返回的數據報只有那些來自 connect 函數所指定的協議地址的數據報。目的地為這個已連接 UDP 套接字的本地協議地址,發源地不是該套接字早先 connect 到的協議地址的數據報,不會投遞到該套接字。即只有發源地的協議地址與 connect 所指定的地址相匹配才可以把數據報傳輸到該套接字。這樣已連接 UDP 套接字只能與一個對端交換數據報;
3、由已連接 UDP 套接字引發的異步錯誤會返回給它們所在的進程,而未連接 UDP 套接字不會接收任何異步錯誤;
UDP 客戶端進程或服務器進程只在使用自己的 UDP 套接字與確定的唯一對端通信時,才可以調用 connect 函數。調用 connect 函數的通常是 UDP 客戶端。以下是調用 connect 函數的客戶端處理函數:
- #include "unp.h"
- void
- dg_cli(FILE *fp, int sockfd, const SA *pservaddr, socklen_t servlen)
- {
- int n;
- char sendline[MAXLINE], recvline[MAXLINE + 1];
- Connect(sockfd, (SA *) pservaddr, servlen);
- while (Fgets(sendline, MAXLINE, fp) != NULL) {
- Write(sockfd, sendline, strlen(sendline));
- n = Read(sockfd, recvline, MAXLINE);
- recvline[n] = 0; /* null terminate */
- Fputs(recvline, stdout);
- }
- }
此時若不啟動服務器,只啟動客戶端,并鍵入文本行時,客戶端會接收到 異步錯誤。
- $ ./client 127.0.0.1
- message...
- read error: Connection refused