技術(shù)分析:基本 UDP 套接字編程
UDP 協(xié)議和 TCP 協(xié)議不同,它是一種面向無連接、不可靠的傳輸層協(xié)議。在基于 UDP 套接字編程中,數(shù)據(jù)傳輸可用函數(shù) sendto 和 recvfrom。以下是基本 UDP 套接字編程過程:
sendto 與 recvfrom 函數(shù)
這兩個函數(shù)的功能類似于 write 和 read 函數(shù),可用無連接的套接字編程。其定義如下:
- /* 函數(shù)功能:發(fā)送數(shù)據(jù);
- * 返回值:若成功則返回已發(fā)送的字節(jié)數(shù),若出錯則返回-1;
- * 函數(shù)原型:
- */
- #include <sys/socket.h>
- ssize_t sendto(int sockfd, void *buff, size_t nbytes, int flags,
- const struct sockaddr *destaddr, socklen_t addrlen);
- /* 說明:
- * 該函數(shù)功能類似于write函數(shù),除了有標(biāo)識符flags和目的地址信息之外,其他參數(shù)一樣;
- *
- * flags標(biāo)識符取值如下:
- * (1)MSG_DONTROUTE 勿將數(shù)據(jù)路由出本地網(wǎng)絡(luò)
- * (2)MSG_DONTWAIT 允許非阻塞操作
- * (3)MSG_EOR 如果協(xié)議支持,此為記錄結(jié)束
- * (4)MSG_OOB 如果協(xié)議支持,發(fā)送帶外數(shù)據(jù)
- *
- * 若sendto成功,則只是表示已將數(shù)據(jù)無錯誤的發(fā)送到網(wǎng)絡(luò),并不能保證正確到達(dá)對端;
- * 該函數(shù)通過指定目標(biāo)地址允許在無連接的套接字之間發(fā)送數(shù)據(jù)(例如UDP套接字);
- */
- /* 函數(shù)功能:接收數(shù)據(jù);
- * 返回值:以字節(jié)計(jì)數(shù)的消息長度,若無可用消息或?qū)Ψ揭呀?jīng)按序結(jié)束則返回0,若出錯則返回-1;
- * 函數(shù)原型:
- */
- #include <sys/socket.h>
- ssize_t recvfrom(int sockfd, void *buff, size_t nbytes, int flags,
- struct sockaddr *addr, socklen_t *addrlen);
- /* 說明:
- * 該函數(shù)功能與read類似;
- * 若addr為非空時,它將包含數(shù)據(jù)發(fā)送者的套接字地址;
- *
- * flags標(biāo)識符取值如下:
- * (1)MSG_WAITALL 等待所有數(shù)據(jù)可用
- * (2)MSG_DONTWAIT 允許非阻塞操作
- * (3)MSG_PEEK 查看已讀取的數(shù)據(jù)
- * (4)MSG_OOB 如果協(xié)議支持,發(fā)送帶外數(shù)據(jù)
- */
基于 UDP 套接字編程
下面我們使用 UDP 協(xié)議實(shí)現(xiàn)簡單的功能,客戶端從標(biāo)準(zhǔn)輸入讀取數(shù)據(jù)并把它發(fā)送給服務(wù)器,服務(wù)器接收到數(shù)據(jù)并把該數(shù)據(jù)回射給客戶端,然后客戶端收到從服務(wù)器回射的數(shù)據(jù)把它顯示到標(biāo)準(zhǔn)輸出。其功能實(shí)現(xiàn)如下圖所示:
服務(wù)器程序
- /* UDP 服務(wù)器 */
- #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;
- /* 初始化服務(wù)器地址信息 */
- bzero(&servaddr, sizeof(servaddr));
- servaddr.sin_family = AF_INET;
- servaddr.sin_port = htons(SERV_PORT);
- servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
- /* 創(chuàng)建套接字,并將服務(wù)器地址綁定到該套接字上 */
- 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");
- /* 服務(wù)器處理函數(shù):讀取套接字文本行,并把它回射給客戶端 */
- dg_echo(sockfd, (struct sockaddr*) &cliaddr, sizeof(cliaddr));
- }
處理函數(shù)
- #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");
- /* 客戶端處理函數(shù):從標(biāo)準(zhǔn)輸入讀入文本行,發(fā)送給服務(wù)器;接收來自服務(wù)器的回射文本,并把它顯示到標(biāo)準(zhǔn)輸出 */
- dg_cli(stdin, sockfd, (struct sockaddr *) &servaddr, sizeof(servaddr));
- exit(0);
- }
客戶端處理函數(shù)
- #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) {
- /* 把從標(biāo)準(zhǔn)輸入讀取的文本行發(fā)送給服務(wù)器套接字 */
- Sendto(sockfd, sendline, strlen(sendline), 0, pservaddr, servlen);
- /* 接收來自服務(wù)器回射的文本行 */
- 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..
數(shù)據(jù)報丟失
由于 UDP 是一種不可靠的傳輸協(xié)議。在上面的客戶端 / 服務(wù)器 程序中,若數(shù)據(jù)報在傳輸?shù)倪^程中丟失,那么客戶端就是阻塞于 dg_cli 處理函數(shù)中的 recvfrom 函數(shù)調(diào)用,等待一個永遠(yuǎn)都不會達(dá)到的服務(wù)器應(yīng)答。也有可能是,客戶端數(shù)據(jù)報成功到達(dá)服務(wù)器,但是服務(wù)器的應(yīng)答數(shù)據(jù)報丟失,同樣,客戶端也將永遠(yuǎn)阻塞于 recvfrom 函數(shù)調(diào)用。一般來說,會給客戶端 recvfrom 函數(shù)調(diào)用設(shè)置一個超時時鐘,但是超時時鐘并不能確定是客戶端數(shù)據(jù)報不能到達(dá)服務(wù)器還是服務(wù)器應(yīng)答不能到達(dá)客戶端。所以我們可以采用驗(yàn)證接收到的響應(yīng)。即在 recvfrom 函數(shù)調(diào)用以返回?cái)?shù)據(jù)報發(fā)送者的 IP 地址和端口號,保留來自數(shù)據(jù)報所發(fā)往服務(wù)器的應(yīng)答。
UDP 中使用 connect 函數(shù)
在沒有啟動 UDP 服務(wù)器的情況下,客戶端鍵入文本行之后,并不會回顯該文本行。此時客戶端永遠(yuǎn)阻塞于它的 recvfrom 調(diào)用,等待一個永遠(yuǎn)不會出現(xiàn)的服務(wù)器應(yīng)答。由于服務(wù)器沒有啟動,因此會響應(yīng)一個端口不可到達(dá)的 ICMP 錯誤消息(即異步錯誤),但是該 ICMP 錯誤消息并不會到達(dá)客戶端進(jìn)程,因此客戶端進(jìn)程根本不知道發(fā)生什么,一直阻塞于它的 recvfrom 調(diào)用。為了能使這個異步錯誤到達(dá)客戶端進(jìn)程,我們可以在 UDP 中調(diào)用 connect 函數(shù),使其成為一個已連接的 UDP 套接字,但是該鏈接不會像 TCP 那樣引起三次握手過程。內(nèi)核只是檢查是否存在立即可知的錯誤,并記錄對端的 IP 地址和端口號,然后立即返回到調(diào)用進(jìn)程。
下面要區(qū)分 未連接 UDP 套接字 和 已連接 UDP 套接字:
● 未連接 UDP 套接字:新創(chuàng)建 UDP 套接字默認(rèn)為該情況;
● 已連接 UDP 套接字:對 UDP 套接字調(diào)用 connect 函數(shù)的結(jié)果;
已連接 UDP 套接字 相對于 未連接 UDP 套接字 會有以下的變化:
1、不能給輸出操作指定目的 IP 地址和端口號(因?yàn)檎{(diào)用 connect 函數(shù)時已經(jīng)指定),即不能使用 sendto 函數(shù),而是使用 write 或 send 函數(shù)。寫到已連接 UDP 套接字上的內(nèi)容都會自動發(fā)送到由 connect 指定的協(xié)議地址;
2、不必使用 recvfrom 函數(shù)以獲悉數(shù)據(jù)報的發(fā)送者,而改用 read、recv 或 recvmsg 函數(shù)。在一個已連接 UDP 套接字上,由內(nèi)核為輸入操作返回的數(shù)據(jù)報只有那些來自 connect 函數(shù)所指定的協(xié)議地址的數(shù)據(jù)報。目的地為這個已連接 UDP 套接字的本地協(xié)議地址,發(fā)源地不是該套接字早先 connect 到的協(xié)議地址的數(shù)據(jù)報,不會投遞到該套接字。即只有發(fā)源地的協(xié)議地址與 connect 所指定的地址相匹配才可以把數(shù)據(jù)報傳輸?shù)皆撎捉幼帧_@樣已連接 UDP 套接字只能與一個對端交換數(shù)據(jù)報;
3、由已連接 UDP 套接字引發(fā)的異步錯誤會返回給它們所在的進(jìn)程,而未連接 UDP 套接字不會接收任何異步錯誤;
UDP 客戶端進(jìn)程或服務(wù)器進(jìn)程只在使用自己的 UDP 套接字與確定的唯一對端通信時,才可以調(diào)用 connect 函數(shù)。調(diào)用 connect 函數(shù)的通常是 UDP 客戶端。以下是調(diào)用 connect 函數(shù)的客戶端處理函數(shù):
- #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);
- }
- }
此時若不啟動服務(wù)器,只啟動客戶端,并鍵入文本行時,客戶端會接收到 異步錯誤。
- $ ./client 127.0.0.1
- message...
- read error: Connection refused