在前面文章中介绍了《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); } }
客户端程序
/* 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 套接字 会有以下的变化:
- 不能给输出操作指定目的 IP 地址和端口号(因为调用 connect 函数时已经指定),即不能使用 sendto 函数,而是使用 write 或 send 函数。写到已连接 UDP 套接字上的内容都会自动发送到由 connect 指定的协议地址;
- 不必使用 recvfrom 函数以获悉数据报的发送者,而改用 read、recv 或 recvmsg 函数。在一个已连接 UDP 套接字上,由内核为输入操作返回的数据报只有那些来自 connect 函数所指定的协议地址的数据报。目的地为这个已连接 UDP 套接字的本地协议地址,发源地不是该套接字早先 connect 到的协议地址的数据报,不会投递到该套接字。即只有发源地的协议地址与 connect 所指定的地址相匹配才可以把数据报传输到该套接字。这样已连接 UDP 套接字只能与一个对端交换数据报;
- 由已连接 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 网络编程》