1. 通常UDP采用下面工作流程
2. recvfrom / sendto
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen); ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);
参数:
flag暂不讨论,此时总是设置为0。
返回值:
这两个函数都把读写的数据长度作为返回值
特别:
写长度为0的数据是可行的,在UDP情况下,会发送一个只包含IP首部和UDP首部而没有数据的包。
对应recvfrom返回0是可接受的。由于UDP是无连接的,所以不需要关闭连接,因此不同于TCP(TCP下,read返回0,表示收到FIN,需要close)
3. 简单的UDP回射程序
server
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)); } 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); } }
首先该程序不会退出,由于没有TCP程序中的EOF。
另外,程序实现为迭代服务器,而不是TCP程序中通过fork实现的并发服务器。这意味一个服务器需要对应多个客户端。
一般来说,大多数TCP服务器是并发的,大多数UDP服务器是迭代的。
UDP套接字层隐藏一个排队,每个UDP套接字都有一个接受缓冲区,数据到达时进入这个接受缓冲,应用调用recvfrom时按照FIFO顺序得到数据。这里有一个问题,就是接受缓冲的大小有限。
client
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); } 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); } }
需要理解的是,客户端端口的绑定,客户端通常不会调用bind,所以端口的绑定是由内核完成的,在TCP程序中绑定发生在connect,UDP程序中绑定发生在sendto。
另外,该程序有一个问题,recvfrom没有对对端信息进行检查,所以可以接收到恶意程序冒充服务器发来的数据。
另外,该程序并不可靠,原因是UDP是不可靠的,所以sendto可能没有成功发送数据给服务器,因此程序会永远阻塞在recvfrom;同样服务器发来的数据,也不能保证recvfrom一定能接收到,程序同样会阻塞在recvfrom。 解决方法可以采用设置超时,但这并不是完整解决,如果客户请求是“从账号A转一定数目钱到账户B”,请求丢失和应道丢失是完全不同的。
添加接受验证
通过比较recvfrom接受的对端地址和服务器地址即可
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); } }
使用Malloc分配套接字地址结构
为了保证本函数的协议无关,因此使用malloc分配地址结构,这样只用关心要分配的空间大小,不用关心套接字类型。
比较返回的地址
这里我们没有比较IP/port,而是直接比较套接字地址结构对象,由于知道服务端套接字地址结构的长度,所以可以使用memcmp比较。
这里有个问题
考虑下面情况:
主机1做服务器有地址:
192.168.3.1
192.168.2.22
主机2 做客户端:
运行 unpcli 192.168.2.22
即,主机2 向主机1子网发报。
运行结果:
地址比较失败, recvfrom 得到的地址是 192.168.3.1
原因是,server的套接字绑定的通配IP地址,所以client的报会发送给server,但是server回复时却不一定使用192.168.2.22,server的路由会为其选择外出接口的地址为报的源地址,所以client通过recvfrom得到的地址时192.168.3.1
解决方法是,client指定域名,并通过DNS得到对端外出接口地址。或者server为每个IP分配一个套接字,并用select监听,如果192.168.2.22收到报文,则由192.168.2.22的套接字回复。
异步错误
考虑服务器未启动,客户端就开始发包,运行结果是客户端阻塞在recvfrom
当客户端发送报后,服务器会返回icmp报表示本主机某某端口不可达,icmp报包括内容:引起错误的数据包的IP首部和UDP或TCP首部,以表示是接收方知道是哪个套接字引发的错误。
为什么客户端不报错,而是一直阻塞在recvfrom?
因为当客户端UDP服务收到icmp包后,内核只能通过errno通知进程错误,但是errno信息不能表示是哪个服务器未启动,所以只能选择不通知客户端。
但是当 客户端使用 connect后,便确定只有唯一服务器可能与之通信,errno值就有效,UDP收到icmp后,客户端会从recvfrom报错。
而 错误是由于 sendto 造成的,却在recvfrom获得报错,这就称为异步错误。
总结UDP下源IP,目的IP,源端口,目的端口 的设置与获得
对于UDP客户端而言:
目的IP和目的端口事先已知。
源IP在sendto()时,由内核通过查看路由表确定(假设没有bind()),如果客户端主机是多宿的,源IP根据走不同的链路而不同。如果调用bind()并设置了IP,而走的外出接口的IP与bind()设置的源IP不同,那么报文的源IP仍然为bind()设置的IP,并且走路由表得到的外出接口。
源端口在sendto()时,由内核分配零时端口,且不可改变。
对于UDP服务端而言:
当服务端接收报文后
要获得目的IP,只能通过IPv4设置IP_RECVDSTADDR,IPv6设置IPV6_PKTINFO套接字选项,然后调用recvmsg(不是recvfrom)获得。由于UDP是无连接的,所以每个接收到的报文的目的IP地址可能不同。
同时,UDP服务器也可以接受目的地址为服务器主机的某个广播地址或多播地址的数据报。
目的端口是自己设置,已知。
对于接收到的报文,调用recvfrom()可获得源IP,源端口。
connect
不同与TCP调用connect会进行三次握手,UDP套接字调用connect只会进行:
内核会检查是否存在立即可知的错误(如一个显然不可达的目的地址)
内核记录对端的IP和端口号
所以这里的connect更像是 setpeername,类似的bind 更好的名字是 setsockname
对于调用connect和没有调用connect的UDP有如下区别
调用connect后,不能再在发送报文时指定对端地址,否则会报错。
调用connect后,在接受报文时,内核只会将符合对端地址的报文传给套接字,UDP套接字只能与一个对端通信。
已连接的UDP套接字会返回异步错误。
对于那些目的地址不是connect设置地址的报文,如果主机没有套接字接受这些报文,会返回ICMP给对端。
对于只有单一对端时,我们调用connect,不过像TFTP会长时间与单一对端通信,也会调用connect,一个列外是DNS,DNS客户端设置一个DNS服务器时,会调用connect,如果设置多个,则不会。
通过多次调用connect可达到以下目的:
指定新的对端地址
断开一个套接字
通过清零一个地址,并设置sin_family=AF_UNSPEC,再调用connect,可断开套接字。
性能
对于未连接的UDP套接字,调用两次sendto,内核会进行下面步骤:
连接套接字(复制目的地址信息到内核)
输出第一个数据报
断开套接字
连接套接字
输出第二个数据报
断开套接字
另外一个需要考虑的是搜索路由表的次数,第一次临时连接需要为目的地址搜索路由表,并缓冲这条信息,第二次临时连接,若目的地址和这条路由信息的目的地址相同,则不必再搜索路由表。可见每次临时连接都要获取路由。
对于已连接UDP套接字,调用两次write ,内核会进行下面步骤:
连接套接字(复制目的地址信息到内核)
输出第一个数据报(仅复制数据到内核)
输出第二个数据报
可见未连接,而调用sendto,需要复制两次地址信息到内核。
临时连接未连接的UDP套接字大约会耗费每个UDP传输的三分之一的开销。
所以尽量进行connect
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); } }
调用connect后,上面程序如果发送的报文会报错,则在Read()时会报错Connecttion refused(由于write()导致对端回复ICMP)。回想TCP,TCP会在connect时报错,因为connect会进行三次握手,会发送SYN,对端回复RST。
流量控制
UDP没有流量控制,如果一个程序 不停的发送报文,且接收端主机性能差,则会导致UDP大量丢包。
可以通过设置UDP套接字接受缓冲区大小,改善这个问题,但不能真正解决。
n = 220*1024; Setsocketopt(sockfd, SOL_SOCKET, SO_RCVBUF, &n, sizeof(n));
UDP中的外出接口的确定
调用connect时,内核会根据目的地址查询路由表,找到对应的路由信息,如果没有调用bind,那么调用connect后,就能确定源IP。
使用select函数的UDP,TCP回射服务器程序
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 ( ; ; ) { FD_SET(listenfd, &rset); FD_SET(udpfd, &rset); if ( (nready = select(maxfdp1, &rset, NULL, NULL, NULL)) < 0) { if (errno == EINTR) continue; /* back to for() */ else err_sys("select error"); } if (FD_ISSET(listenfd, &rset)) { 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)) { len = sizeof(cliaddr); n = Recvfrom(udpfd, mesg, MAXLINE, 0, (SA *) &cliaddr, &len); Sendto(udpfd, mesg, n, 0, (SA *) &cliaddr, len); } } } /* end udpservselect02 */
总结:
UDP套接字不会检查丢失并重传,也不会验证消息是否来自正确对端,也没有流量控制,且异步错误只会在已经连接UDP套接字上返回。