[关闭]
@FunC 2018-05-15T11:26:13.000000Z 字数 3652 阅读 1893

UNP CH08 基本UDP套接字编程

unix


不同于TCP提供的面向连接的可靠字节流,UDP是无连接不可靠的数据包协议。然而相比TCP,有些场合确实更适合使用UDP,例如DNS、NFS和SNMP。

下面是典型的UDP客户/服务器程序的函数调用:

本章将用UDP重写先前的回射客户/服务器程序。

recvfrom和sendto函数

  1. #include <sys/socket.h>
  2. ssize_t recvfrom(int sockfd, void *buff, size_t nbytes, int flags, struct sockaddr *from, socklen_t *addrlen);
  3. ssize_t sendto(int sockfd, void *buff, size_t nbytes, int flags, struct sockaddr *to, socklen_t addrlen);
  4. /* 返回:若成功则返回读或写的字节数,出错返回-1 */

前三个参数与 read 和 write 函数的三个参数类似:描述符、指向读入/写出缓冲区的指针和读/写字节数

flags参数在之后14章讨论recv, send, recvmsg, sendmsg 等函数再介绍,当前先设为0.

sendto 函数的 to 参数指向含有数据报接受者的协议地址(IP+port)的套接字地址结构,大小由addrlen参数指定(所以协议无关)。

recvfrom 的from参数指向返回者的套接字地址结构,地址结构中填写的字节数放在addrlen参数中。注意recvfrom的addrlen参数为 值-结果 参数,填写时是地址结构的buffer大小,返回时是实际写入的大小。

recvfrom 的 from 参数和addrlen参数均为空指针时,说明我们不关心数据发送者的协议地址。

UDP回射服务器程序:dg_echo函数

  1. #include "unp.h"
  2. void dg_echo(int sockfd, SA *pcliaddr, socklen_t clilen)
  3. {
  4. int n;
  5. socklen_t len;
  6. char mesg[MAXLINE];
  7. for ( ; ; ) {
  8. len = clilen;
  9. n = Recvfrom(sockfd, mesg, MAXLINE, 0, pcliaddr, &len);
  10. Sendto(sockfd, mesg, n, 0, pcliaddr, len);
  11. }
  12. }

虽然这个函数很简单,但也有一些细节需要考虑:
* 因为UDP是无连接的协议,没有类似EOF之类的东西,所以该函数永不终止
* 这是一个迭代服务器,单个UDP服务器处理所有客户

对于本套接字,UDP层其实是隐含有排队发生的,每个UDP套接字都有一个接受缓冲区,数据包排队进入队列,先进先出。

UDP回射客户程序:dg_cli 函数

  1. #include "unp.h"
  2. void dg_cli(FILE *fp, int sockfd, const SA *pservaddr, socklen_t servlen)
  3. {
  4. int n;
  5. char sendline[MAXLINE], recvline[MAXLINE + 1];
  6. while (Fgets(sendline, MAXLINE, fp) != NULL) {
  7. Sendto(sockfd, sendline, strlen(sendline), 0, pservaddr, servlen);
  8. // 后两个参数为 NULL,说明不关心应答数据由谁发送
  9. n = Recvfrom(sockfd, recvline, MAXLINE, 0, NULL, NULL);
  10. recvline[n] = 0; /* null terminate */
  11. Fputs(recvline, stdout);
  12. }
  13. }

数据包的丢失

显然上述例子是不可靠的,如果一个客户数据报丢失,那么客户将永远阻塞于dg_cli 函数中的 recvfrom 调用。一般的解决方法是设置一个超时。

然而仅仅设置超时是不够的,我们无从判断超时的原因(我们的数据报没到达服务器还是服务器的应答没有回到客户),关于增加UDP程序的可靠性将在之后22章讨论。

验收接收到的响应

为了确保应答的数据来自于我们的目标地址,我们可以根据recvfrom调用的返回结果,忽略掉其他的数据报:

  1. #include "unp.h"
  2. void dg_cli(FILE *fp, int sockfd, const SA *pservaddr, socklen_t servlen)
  3. {
  4. int n;
  5. char sendline[MAXLINE], recvline[MAXLINE + 1];
  6. socklen_t len;
  7. struct sockaddr *preply_addr;
  8. // 分配对应的内存空间
  9. preply_addr = Malloc(servlen);
  10. while (Fgets(sendline, MAXLINE, fp) != NULL) {
  11. Sendto(sockfd, sendline, strlen(sendline), 0, pservaddr, servlen);
  12. len = servlen;
  13. n = Recvfrom(sockfd, recvline, MAXLINE, 0, preply_addr, &len);
  14. // 验证长度,比较地址结构本身
  15. if (len != servlen || memcmp(pservaddr, preply_addr, len) != 0) {
  16. printf("reply from %s (ignored)\n",
  17. Sock_ntop(preply_addr, len));
  18. continue;
  19. }
  20. recvline[n] = 0; /* null terminate */
  21. Fputs(recvline, stdout);
  22. }
  23. }

上面的方案还有一些问题:
因为没有指定发送地址,内核将临时分配地址和端口。对于多宿的服务器主机可能有问题(即有多个IP地址)

解决方案有二:
1. 通过DNS来验证主机域名
2. UDP服务器给配置的每个IP地址创建一个套接字,用bind绑定IP,然后通过select来监听,使用可读的套接字来响应

服务器进程未执行

同样因为UDP是无连接的协议,我们可能在服务器未启动的情况下启动客户,这样客户将永远阻塞于recvfrom调用。那我们是否有办法识别出服务器进程未执行,从而去处理这个错误呢?

查看tcpdump输出能发现,在发出UDP数据报之前,需要一次ARP请求和应答的交换(因为在同一子网下需要知道对方MAC地址,非同一子网至少要知道网关的MAC地址)。如果ARP请求失败,会响应一个ICMP错误信息(但是不会返回给客户进程)

这个ICMP错误称为异步错误。这是因为虽然错误由sendto引起,但sendto却成功返回,而该错误在一段时间后才返回。(sendto成功返回其实只能说明接口输出队列中具有存放所形成IP数据报的空间

异步错误不返回给客户进程,我们可以考虑单个UDP套接字上接连发送3个数据报给3个不同的服务器。出错时,客户需要知道到底是哪个服务器的应答出错了。但是recvfrom可以返回的错误信息仅有errno值,不能返回出错数据报的目的IP地址和目的UDP端口号。所以仅在进程已将其UDP套接字连接到恰恰一个对端后,这些异步错误才返回给进程。

UDP 的 connect 函数

可以给UDP套接字调用connect,但是结果于TCP不同,内核只会检查是否存在立即可知的错误(如目的地显然不可达),记录对端的IP和port后立即返回到调用进程。

于是我们需要区分:
* 未连接UDP套接字
* 已连接UDP套接字:对UDP套接字调用connect的结果

connect后的UDP套接字有3点不同:
1. 不再需要制定目的IP和port。所以我们不使用sendto,而改用write或send
2. 不必使用recvfrom来获悉响应的发送者,而改用read,recv或recvmsg。只有来自connect所制定协议地址的数据报才会被写入套接字
3. 已连接UDP套接字引发的异步错误会返回给她所在的进程

给一个UDP套接字多次调用connect

不同于TCP套接字,可以对UDP套接字多次调用connect,目的有两种:
1. 指定新的IP地址和端口号
2. 断开套接字

性能

在未连接UDP套接字上调用sendto时,内核暂时连接该套接字,发送数据,然后就断开连接。因此要给同一目的地址发送多个数据报时,使用已连接UDP套接字效率更高

UDP 缺乏流量控制

不同于TCP,UDP没有流量控制。如果接收方的UDP套接字缓冲区满了,后续的数据报将被直接丢弃。同应用进程也不知道这些数据报已丢失。

UDP 中的外出接口的确定

已连接UDP套接字还有一个功能,就是可以用于确定特定目的地的外出接口。这是因为调用connect函数时,内核选择本地IP地址,这个本地IP地址通过为目的IP地址搜索路由表得到外出接口,然后选用该接口的主IP地址而选定:
[image:D4A3463B-9538-46DF-9247-5207A1FC8036-355-000005358E606553/13B235DA-0093-4E08-833D-D50FB2743DA8.png]

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注