天天看點

《網絡程式設計》基本 UDP 套接字程式設計

        在前面文章中介紹了《UDP 協定》和《套接字資料傳輸》。UDP 協定和 TCP 協定不同,它是一種面向無連接配接、不可靠的傳輸層協定。在基于 UDP 套接字程式設計中,資料傳輸可用函數 sendto 和 recvfrom。以下是基本 UDP 套接字程式設計過程:

《網絡程式設計》基本 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 套接字程式設計

伺服器程式

/* 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);
	}
}
           

用戶端程式

/* 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 套接字:

  1. 未連接配接 UDP 套接字:新建立 UDP 套接字預設為該情況;
  2. 已連接配接 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
           

參考資料:

《Unix 網絡程式設計》