在前面文章中介绍了《UDP 协议》和《套接字数据传输》。UDP 协议和 TCP 协议不同,它是一种面向无连接、不可靠的传输层协议。在基于 UDP 套接字编程中,数据传输可用函数 sendto 和 recvfrom。以下是基本 UDP 套接字编程过程:
这两个函数的功能类似于 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 服务器 */ #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 服务器的情况下,客户端键入文本行之后,并不会回显该文本行。此时客户端永远阻塞于它的 recvfrom 调用,等待一个永远不会出现的服务器应答。由于服务器没有启动,因此会响应一个端口不可到达的 ICMP 错误消息(即异步错误),但是该 ICMP 错误消息并不会到达客户端进程,因此客户端进程根本不知道发生什么,一直阻塞于它的 recvfrom 调用。为了能使这个异步错误到达客户端进程,我们可以在 UDP 中调用 connect 函数,使其成为一个已连接的 UDP 套接字,但是该链接不会像 TCP 那样引起三次握手过程。内核只是检查是否存在立即可知的错误,并记录对端的 IP 地址和端口号,然后立即返回到调用进程。
下面要区分 未连接 UDP 套接字 和 已连接 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 网络编程》