八、基本UDP套接字编程
1.典型的UDP客户/服务器程序函数调用图
2. recvfrom和sendto函数
#include <sys/socket.h> ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen); ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen);
说明:recvfrom和sendto函数前面三个参数等同于read和write的三个参数。flags暂时不讨论。两个函数最后两个参数为NULL时可用于TCP。
3. UDP回射服务端程序示例
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <sys/socket.h> #include <arpa/inet.h> #define SRV_PORT 7777 #define MAXLINE 4096 void dg_echo(int sockfd, struct sockaddr *cliAddr, socklen_t addrlen); int main(int argc, char **argv) { int sockfd = socket(AF_INET, SOCK_DGRAM, 0); if(sockfd < 0) { perror("create socket failed"); exit(-1); } struct sockaddr_in srvaddr; bzero(&srvaddr, sizeof(srvaddr)); srvaddr.sin_family = AF_INET; srvaddr.sin_addr.s_addr = htonl(INADDR_ANY); srvaddr.sin_port = htons(SRV_PORT); int bret = bind(sockfd, (struct sockaddr *)&srvaddr, sizeof(srvaddr)); if( bret < 0) { perror("bind failed"); close(sockfd); exit(-1); } struct sockaddr_in cliaddr; bzero(&cliaddr, sizeof(cliaddr)); dg_echo(sockfd, (struct sockaddr *)&cliaddr, sizeof(cliaddr)); } void dg_echo(int sockfd, struct sockaddr *cliAddr, socklen_t addrlen) { char msg[MAXLINE]; for( ;; ) { socklen_t len = addrlen; int bCount = recvfrom(sockfd, msg, MAXLINE, 0, cliAddr, &len); if(bCount <= 0) { perror("recvfrom error"); return; } int sCount = sendto(sockfd, msg, bCount, 0, cliAddr, len); if(sCount < 0) { perror("sendto error"); return; } } }
说明:大多数TCP服务器是并发的,而UDP服务器是迭代的。
4. UDP回射客户端程序示例
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <sys/socket.h> #include <arpa/inet.h> #include <sys/types.h> #define SRV_PORT 7777 #define MAXLINE 4096 void dg_cli(FILE *fp, int sockfd, struct sockaddr *cliAddr, socklen_t addrlen); int main(int argc, char **argv) { if(argc < 2) { printf("usage:udpcli <ip address>\n"); exit(-1); } struct sockaddr_in srvaddr; bzero(&srvaddr, sizeof(srvaddr)); srvaddr.sin_family = AF_INET; srvaddr.sin_port = htons(SRV_PORT); //srvaddr.sin_port = htons(7); int ret = inet_pton(AF_INET, argv[1], &srvaddr.sin_addr); if(ret <= 0) { printf("inet_pton address error %s\n",argv[1]); exit(-1); } int sockfd = socket(AF_INET, SOCK_DGRAM, 0); dg_cli(stdin, sockfd, (struct sockaddr*)&srvaddr, sizeof(srvaddr)); exit(0); } void dg_cli(FILE *fp, int sockfd, struct sockaddr *cliAddr, socklen_t addrlen) { char sndmsg[MAXLINE]; char rcvmsg[MAXLINE]; while(fgets(sndmsg, MAXLINE, fp)) { int sret = sendto(sockfd, sndmsg, strlen(sndmsg), 0, cliAddr, addrlen); if(sret < 0) { perror("sendto failed"); return; } int rcount = recvfrom(sockfd, rcvmsg, MAXLINE, 0, NULL, NULL); if(rcount < 0) { perror("recvfrom error"); return; } rcvmsg[rcount] = 0; fputs(rcvmsg, stdout); } }
说明:recvfrom函数最后面两个参数为NULL,说明客户端没有关注消息应答者。这样可能造成任何进程像本客户端IP+端口上发数据都会被认为是消息应答。
5. 数据报的丢失
上面例子中UDP客户、服务器的传输是不可靠的,如果客户端发送的数据丢失,或是服务器的应答丢失,客户端recvfrom将永远阻塞。在后面将继续讨论如果增加UDP的可靠性。
6. 验证收到的响应
更改的程序
void dg_cli(FILE *fp, int sockfd, struct sockaddr *cliAddr, socklen_t addrlen) { char sndmsg[MAXLINE]; char rcvmsg[MAXLINE + 1]; struct sockaddr *replyAddr = malloc(addrlen); while(fgets(sndmsg, MAXLINE, fp)) { int sret = sendto(sockfd, sndmsg, strlen(sndmsg), 0, cliAddr, addrlen); if(sret < 0) { perror("sendto failed"); return; } socklen_t len = addrlen; int rcount = recvfrom(sockfd, rcvmsg, MAXLINE, 0, replyAddr, &len); if(rcount < 0) { perror("recvfrom error"); return; } if(len != addrlen || memcmp(cliAddr, replyAddr, len) != 0 ) { char addr[56] = {0}; inet_ntop(AF_INET, replyAddr, addr, len); printf("reply from %s(ignored)\n", addr); continue; } rcvmsg[rcount] = 0; fputs(rcvmsg, stdout); } }
说明:如果服务器是多宿的主机,客户端发送数据给服务器,如果服务器没有把套接字绑定在一个IP上,其应答外出接口的IP地址将是服务器的主IP地址,可能不是客户端发送的目的IP地址。所以说上面测程序有可能失败。
解决办法:
- 客户通过在DNS中查找服务器主机的名字来验证该主机的域名而不是IP;
- 服务器给每个IP创建一个套接字,用bind绑定到各自的套接字,然后用select再从可读的套接字给出应答。
7. 服务器未运行
- sendto将产生port unreachable错误,但是sendto却返回成功;
- recvfrom将收不到数据,永远阻塞;
- 规则:对于UDP套接字,它引发的异步错误不返回给它本身,除非它已经连接。所以只有在进程已经连接到对端后,错误才会返回。后面将说到UDP的connect函数。
8. UDP的connect函数
1)udp的connect没有三次握手过程,完全是本地行为;
2)既然有connect,我们就要区分未连接UDP套接字(默认)和已连接UDP套接字(调用connect);
3)已连接套接字和未连接套接字的区别:
-
- 不用给输出指定目的IP和端口了,即不使用sendto而是改用wirte或send;
- 不必使用recvfrom,而是使用read、recv或recvmsg;
- UDP套接字引发的异步错误将返回给它们的进程。
4)UDP套接字多次调用connect可能出于以下目的:
-
- 指定新地址的IP地址和端口号(对于TCP,connect只能调用一次)
- 断开套接字:需要再次调用connect,把地址结构上的地址族成员(sin_family或sin6_family)设置成AF_UNSPEC
5)性能
-
- 未连接套接字两次sendto执行步骤:连接套接字 -> 输出第一个数据报 -> 断开套接字连接 -> 连接套接字 -> 输出第二个数据报 ->断开套接字连接;
- 已连接套接字两次write的步骤:连接套接字 -> 输出第一个数据报 -> 输出第二个数据报
- 从上面可看出未连接的两次发送数据比已经连接的两次发送数据复杂的多,效率自然低得多。
9. 6中例子函数的修订
//使用connect void dg_cli_2(FILE *fp, int sockfd, struct sockaddr *cliAddr, socklen_t addrlen) { char sndmsg[MAXLINE]; char rcvmsg[MAXLINE + 1]; struct sockaddr *replyAddr = malloc(addrlen); while(fgets(sndmsg, MAXLINE, fp)) { if( connect(sockfd, cliAddr, addrlen) < 0 ) { perror("connect error"); return; } if( write( sockfd, sndmsg, strlen(sndmsg) ) != strlen(sndmsg)) { perror("write error"); return; } socklen_t len = addrlen; int rcount = read(sockfd, rcvmsg, MAXLINE); if(rcount < 0) { perror("recvfrom error"); return; } rcvmsg[rcount] = 0; fputs(rcvmsg, stdout); } }
示例中返回的错误码为:ECONNREFUSED,映射成消息串是“Connection refused”
10. UDP的流量控制
UDP发送端发送大量数据给接收端,消息淹没接收端是比较容易的,这会造成大量的数据被接收端丢弃。当然,通过改变接收端缓冲区的大小可以缓解这个情况,但是没根本解决问题。
11. 使用select的TCP和UDP回射服务器程序