系列文章导航:《Unix 网络编程》笔记

#include <sys/socket.h>ssize_t recvfrom(int sockfd, void *buff, size_t nbytes, int flags, struct sockaddr * from, socklen_t * addrlen);ssize_t sendto(int sockfd, const void * buff, size_t nbytes, int flags, const struct sockaddr *to, socklen_t addrlen);参数说明
注意
udpserv01.c
int main(int argc, char **argv){ int sockfd; struct sockaddr_in servaddr, cliaddr; sockfd = Socket(AF_INET, SOCK_DGRAM, 0); bzero(&servaddr, sizeof(servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_addr.s_addr = htonl(INADDR_ANY); servaddr.sin_port = htons(SERV_PORT); Bind(sockfd, (SA *)&servaddr, sizeof(servaddr)); dg_echo(sockfd, (SA *)&cliaddr, sizeof(cliaddr));}dg_echo.c
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, &clilen); Sendto(sockfd, mesg, n, 0, pcliaddr, len); }}服务模型 :迭代服务器
因为没有连接的概念,无需维持状态,所以是来一个消费一个,有点像消费队列的感觉。
udpcli01.c
#include "unp.h"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); sockfd = Socket(AF_INET, SOCK_DGRAM, 0); dg_cli(stdin, sockfd, (SA *)&servaddr, sizeof(servaddr)); exit(0);}dg_cli.c
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; Fputs(recvline, stdout); }}对于客户端而言,什么时候指派一个套接字:
如果客户端发送的请求在去或回来的途中丢失了,那么客户端将会永远阻塞于 Recvfrom 。
简单的解决方法是为该方法设置一个超时,但是这种方法不能判断是发送的时候丢失了还是返回的途中丢失了,因此不具备可靠性。
问题
前文的代码中,由于如下语句:
n = Recvfrom(sockfd, recvline, MAXlINE, 0, NULL, NULL);后两个参数为 NULL,则任何知道客户端临时端口的进程都能向客户发送数据报,与正常的服务器应答混淆。
解决方法
创建一个变量,接收发送方的协议地址,然后和我们期望的进行对比
void dg_cli(FILE *fp, int sockfd, const SA *pservaddr, socklen_t servlen){ int n; char sendline[MAXLINE], recvline[MAXLINE + 1]; socklen_t len; struct sockaddr *preply_addr; preply_addr = Malloc(servlen); while (Fgets(sendline, MAXLINE, fp) != NULL) { Sendto(sockfd, sendline, strlen(sendline), 0, pservaddr, servlen); len = servlen; n = Recvfrom(sockfd, recvline, MAXLINE, 0, preply_addr, &len); if (len != servlen || memcmp(pservaddr, preply_addr, len) != 0) { printf("reply from %s (ignored)\n", Sock_ntop(preply_addr, len)); continue; } recvline[n] = 0; /* null terminate */ Fputs(recvline, stdout); }}仍然存在缺陷
在多宿服务器上,即一台主机有多个接口,也即有多个 IP 地址,有可能接收到的消息是从另一个接口发出的,从而被我们意外地拦截。
强端系统模型、弱端系统模型
解决方法:
从客户端的程序上来看,如果服务器没有运行,那么程序就会阻塞在 recvfrom 方法上。
通过 tcpdump 可以看到如下内容:

确实有 ICMP 的错误信息表示该协议地址 UNREACHABLE,但是这个错误不会返回给客户端
我们称这个 ICMP 错误为 异步错误 ,该错误由 sendto 引起,但是 sendto 本身却成功返回
一个基本规则是,除非它已连接,否则其引发的异步错误并不返回给它
后文将给出一个使用自己的守护进程获取未连接套接字上这些错误的简便方法
和 TCP 的 connect 相比:
UDP 调用 connect 的话:
和没有 connect 的 UDP 相比:
不能给输出操作指定目的 IP 和端口号了,即不使用 sendto 而是使用 write/send,写入内容自动发送到指定的协议地址
其实可以用 sendto,但是不能指定目的地址,必须为空指针,第六个参数应该为 0(第五个参数为 NULL 时,第六个参数不再考虑)
不必使用 recvfrom 获悉数据报的发送者,而改用 read、recv 或 recvmsg 。内核会过滤掉其他来源的数据报。
由已连接 UDP 套接字引发的异步错误会返回给他们所在的进程

两个作用
AF_UNSPEC当要给同一目的地址发送多个数据报时,显式连接套接字效率更高
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); }}此时当我们建立连接时并不会报错,在我们用 Write 发送数据时会发现网络不可达的错误
如果发送方的能力很强,且不断发数据报,而接收方处理的速度比较慢,则有可能将接收方的缓冲区冲垮
可以通过如下命令查看 UDP 数据的统计信息
netstat -s -p -u可以通过如下方式增加缓冲区的大小:
n = 220 * 1024;Setsockopt(sockfd, SOL_SOCKET, SO_RCVBUF, &n, sizeof(n));通过 connect 应用到 UDP 的一个副作用来获取:
int main(int argc, char **argv){ int sockfd; socklen_t len; struct sockaddr_in cliaddr, servaddr; if (argc != 2) err_quit("usage: udpcli <IPaddress>"); sockfd = Socket(AF_INET, SOCK_DGRAM, 0); bzero(&servaddr, sizeof(servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_port = htons(SERV_PORT); Inet_pton(AF_INET, argv[1], &servaddr.sin_addr); Connect(sockfd, (SA *)&servaddr, sizeof(servaddr)); len = sizeof(cliaddr); Getsockname(sockfd, (SA *)&cliaddr, &len); printf("local address %s\n", Sock_ntop((SA *)&cliaddr, len)); exit(0);}我们想做一个既能接收 TCP 请求,又能接收 UDP 请求的服务器。
思路是:把之前的并发 TCP 服务器和本章中的迭代 UDP 服务器结合成使用 select 来复用 TCP 和 UDP 套接字的程序。
具体来说,就是:
int main(int argc, char **argv){ int listenfd, connfd, udpfd, nready, maxfdp1; char mesg[MAXLINE]; pid_t childpid; fd_set rset; ssize_t n; socklen_t len; const int on = 1; struct sockaddr_in cliaddr, servaddr; void sig_chld(int); /* 4create listening TCP socket */ listenfd = Socket(AF_INET, SOCK_STREAM, 0); bzero(&servaddr, sizeof(servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_addr.s_addr = htonl(INADDR_ANY); servaddr.sin_port = htons(SERV_PORT); Setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)); Bind(listenfd, (SA *)&servaddr, sizeof(servaddr)); Listen(listenfd, LISTENQ); /* 4create UDP socket */ udpfd = Socket(AF_INET, SOCK_DGRAM, 0); bzero(&servaddr, sizeof(servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_addr.s_addr = htonl(INADDR_ANY); servaddr.sin_port = htons(SERV_PORT); Bind(udpfd, (SA *)&servaddr, sizeof(servaddr)); /* end udpservselect01 */ /* include udpservselect02 */ Signal(SIGCHLD, sig_chld); /* must call waitpid() */ FD_ZERO(&rset); maxfdp1 = max(listenfd, udpfd) + 1; for (;;) { printf("Hello UDP!\n"); FD_SET(listenfd, &rset); FD_SET(udpfd, &rset); if ((nready = select(maxfdp1, &rset, NULL, NULL, NULL)) < 0) { printf("NREADY, restart!\n"); if (errno == EINTR) continue; /* back to for() */ else err_sys("select error\n"); } if (FD_ISSET(listenfd, &rset)) { printf("listenfd selected!\n"); len = sizeof(cliaddr); connfd = Accept(listenfd, (SA *)&cliaddr, &len); if ((childpid = Fork()) == 0) { /* child process */ Close(listenfd); /* close listening socket */ str_echo(connfd); /* process the request */ exit(0); } Close(connfd); /* parent closes connected socket */ } if (FD_ISSET(udpfd, &rset)) { printf("udpfd selected!\n"); len = sizeof(cliaddr); n = Recvfrom(udpfd, mesg, MAXLINE, 0, (SA *)&cliaddr, &len); Sendto(udpfd, mesg, n, 0, (SA *)&cliaddr, len); } }}/* end udpservselect02 */