UDP编程的基本流程
UDP是无连接、不可靠的数据报协议,不同于TCP的面向连接的可靠数据流。UDP编程常见的应用包括:DNS域名服务器、NFS网络文件系统、SNMP简单网络管理协议。
客户端不必与服务端建立连接,只需调用sendto 函数向服务端发送数据。服务端不接受客户端的连接,只需在指定端口调用recvfrom 函数接收客户端的数据即可。服务端调用recvfrom 获取到客户端的地址,因此服务端可以把响应数据准确地发回给客户端。
UDP编程的基本流程如下所示:
服务端:socket()创建套接字 --> bind()绑定监听的地址、端口 --> recvfrom()等待接收客户端数据 --> sendto()发送应答数据给客户端
客户端:socket()创建套接字 --> sendto()向服务端发送数据 --> recvfrom()接收服务端应答数据 --> close()关闭套接字
recvfrom和sendto函数
#include <sys/socket.h>
ssize_t recvfrom(int sockfd, void *buff, size_t nbytes, int flags, struct sockaddr *from, socklen_t *addrlen);
- 返回值:接收数据的大小
- sockfd:socket描述符
- buff:接收数据的缓存
- nbytes:缓存大小
- flags:默认0
- from:客户端地址通用结构
- addrlen:客户端地址长度
#include <sys/socket.h>
ssize_t sendto(int sockfd, const void *buff, size_t nbytes, int flags, const struct sockaddr *to, socklen_t addrlen);
- 返回值:发送数据的大小
- sockfd:socket描述符
- buff:发送数据的缓存
- nbytes:缓存大小
- flags:默认0
- to:服务端地址通用结构
- addrlen:服务端地址长度
UDP允许发送长度为0的数据,数据包只包含20字节IP头、8字节UDP头,recvfrom返回0,这是可以接收的,在TCP编程中read返回0意味着客户端发了FIN包。
每个UDP套接字都有一个接收缓冲区,收到的数据加入到缓冲区的尾部,调用recvfrom 时,从缓冲区头部获取数据返回给应用程序。这里注意到一个和TCP的区别:TCP服务端每收到一个客户端的请求,就会从accept函数中返回一个新的fd,这个fd有自己的缓冲区,因此与每个客户端的连接都有自己独立的缓冲区,UDP不一样,所有的客户端数据共享一个服务端缓冲区。
UDP的connect函数
connect函数的作用
UDP调用connect 函数有如下几个作用:
- 告诉内核这个UDP套接字的数据包都发往
connect 绑定的目的地址,此时套接字可以调用send 函数发送数据。也可以调用sendto 函数,但是要将地址和长度都置为NULL。 - 内核会过滤数据包,进程调用
read 、recv 、recvmsg 时,仅源地址为connect 绑定的地址的数据包才返回给进程。 - 进程可以多次调用connect以解绑并绑定一个不同的IP,这点和TCP套接字不同。
- UDP套接字引发的异步错误会返回给进程。
connect解决UDP异步错误
UDP在给目标主机不存在的端口发送数据包时,目标主机内核会返回端口不可达的ICMP差错报文,通常情况下,这个报文仅达到发送主机的内核,并不会返回给应用程序。如果UDP调用connect ,该ICMP差错报文会返回给调用程序。编写如下代码验证这个逻辑:
#include "unp.h"
int
main(int argc, char **argv)
{
struct sockaddr_in servaddr;
socklen_t sock_len;
int sockfd, i;
char buff[MAX_LINE] = "hello world";
bzero(&servaddr, sizeof(servaddr));
sockfd = socket(AF_INET, SOCK_DGRAM, 0);
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(13);
inet_pton(AF_INET, "192.168.157.144", &servaddr.sin_addr);
sock_len = sizeof(struct sockaddr_in);
i = connect(sockfd, (struct sockaddr *)&servaddr, sock_len);
if (i < 0) {
perror("connect fail \n");
exit(0);
}
while (1) {
i = send(sockfd, buff, strlen(buff), 0);
if ( i < 0 ) {
perror("send error\n");
exit(0);
}
}
return 0;
}
运行的结果如下所示:
[root@localhost sendto]
send error
: Connection refused
抓包可以看到:
21:17:07.345779 IP 192.168.157.144.43712 > 192.168.157.144.13: UDP, length 11
21:17:07.345789 IP 192.168.157.144 > 192.168.157.144: ICMP 192.168.157.144 udp port 13 unreachable, length 47
我的192.168.157.144是一个真实存在的主机,但是13端口并未开启。可以看到客户端收到ICMP的差错报文,send返回对应的错误码,但是这里有个地方要注意,ICMP报文返回需要时间,因此第一次send的时候没报错,后面再send,才会探测到错误。
修改代码,将目的地址改成不存在的主机IP,再次运行程序,虽然抓包可以看到IP不可达的ICMP差错报文,但是应用程序并不能感知到,从而可以推断出,仅目的主机IP可达,端口不可用的情况下,应用程序才能感知到。
通过抓包也可以看到,UDP调用connect并不会发送任何数据包,只是记录了目的地址。
connect对性能的提升
调用sendto函数发送UDP报文需要经过这样几个步骤:连接套接字–>发送一个报文–>断开套接字连接,发送第二个报文需要经过同样的步骤。调用connect后,无论发送几个报文,都不用重新连接和断开连接,提升了性能。
|