前言
在学习网络通讯的时候会经常听到UDP通讯,那UDP是什么呢?如何使用UDP呢?今天这一篇文章就给大家介绍一下UDP通讯。
一、UDP通讯
1.UDP通讯概述
UDP是一种面向于无连接的用户数据报协议,在传输数据前是不需要先建立连接就可以直接进行通信的。
目标主机在接收到UDP报文后是不需要给出任何确认的。
2.UDP的特点
1.UDP相当于TCP来说速度会稍快一点。
2.简单的请求/应答程序可以使用UDP
3.广播和多播应用是使用UDP
3.UDP的应用
一般UDP使用以下领域:
1.DNS域名解析
2.NFS网络文件系统
3.RTP流媒体
4.语音和视频通话
二、UDP基本通讯
UDP通讯流程很简单,可以用下面的这张图来进行概括:
首先要分为服务端和客户端,服务端主要是负责让客户端进行连接,然后接收客户端给的数据或者指令进行分析然后发送分析结果给客户端进行一次反馈。
客户端就是发送数据的那一方。
流程非常的简单,我们现在来开始书写代码,但在书写代码前需要先了解在这个通讯过程中使用的函数。
1.socket函数
这个函数是我们在网络编程中会经常看到的一个函数,这个函数主要的功能是创建套接字,然后通过操作这个套接字实现网络通讯,函数的原型:
#include <sys/socket.h> int socket(int domain, int type, int protocol); 功能: 建立一个用于交流的端点并且返回一个描述符 参数: domain:AF_INET type:确定通信语句 SOCK_STREAM 提供有序的,可靠的,双向的,基于字节流的通讯。可能支持带外传输,一般使用TCP的时候用这个。 SOCK_DGRAM 提供数据报(不面向连接的, 不可靠的固定最大长度的信息)一般使用UDP的时候用这个。 SOCK_SEQPACKET 提供有序的,可靠的,双向的,基于固定最大长度的数据报传输路径;需要一个读取整个伴有输入系统调用的包的用户。 SOCK_RAW 提供未加工(raw)的网络协议通道。 SOCK_RDM 提供可靠的数据报层,但是不保证顺序。 SOCK_NONBLOCK 设置 O_NONBLOCK 的标志于新打开的文件描述符。 通过这个标志可以不用调用 fcntl(2) 来达到相同的结果。 SOCK_CLOEXEC 设置 close-on-exec (FD_CLOEXEC) 的标志于新打开的文件描述符。参见 open(2) 中关于 O_CLOEXEC 的描述,因为一些原因这个标志很有用。 protocol:指定一个协议用于套接字,一般为0 返回值: 成功:文件描述符 失败:-1
使用的方法很简单,比如说我要创建一个套接字用来实现UDP的通讯,那代码我可以这样写:
int sockfd = -1; // 接收套接字变量 sockfd = socket(AF_INET, SOCK_DGRAM); // 创建套接字 if (sockfd == -1){ // 错误判断 perror("socket is not ok"); return -1; }
当socket返回-1的时候我们就可以知道它创建失败了,这个时候我们可以使用perror()这个函数将它错误信息打印出来,这个函数的使用方法很简单,里面填写提示内容,如果出错就会将错误打印到提示内容后面。
但是并不是所有的函数都可以使用perror()这个函数的,使用这个函数的一个唯一条件就是看errno是否被置位,如果没有被置位,那就没办法使用
2.bind函数
在服务端中必须要这个bind
函数,这个函数是将你服务端的IP地址和端口号绑定到套接字中,如果不绑定,客户端就没办法找到你了,函数原型如下:
#include <sys/socket.h> int bind(int sockfd, const struct sockaddr* addr, socklen_t addrlen); 功能: 给套接字绑定端口和ip 参数: sockfd:套接字 addr:ipv4套接字结构体地址 addrlen:ipv4套接字结构体的大小 返回值: 成功0 失败-1
其中sockaddr结构体是最重要的,这个就是将我们需要绑定的内容放入结构体里面,相当于一个配置,这个结构体的原型如下:
struct sockaddr{ sa_family_t sa_family; /*AF_xxx*/ char sa_data[14]; /*通用的地址*/ }
但这个是一个通用的结构体,在网络中我们有ipv4和ipv6,有两个IP地址的格式,如果我们要分开这两个IP并分开配置的话就要一个函数写两遍,为了不这么浪费,就设计了一个通用的结构体来进行配置,你配置ipv4的IP和配置ipv6的IP都可以直接使用这个结构体。
但是这个结构体还是比较复杂,我们可以通过ipv4配置结构体配置完后强制类型转换成这个通用的结构体然后来使用,ipv4的配置结构体如下:
struct sockaddr_in { sa_family_t sin_family; /* 地址族: AF_INET */ u_int16_t sin_port; /* 按网络字节次序的端口 */ struct in_addr sin_addr; /* internet地址 */ }; /* Internet地址. */ struct in_addr { u_int32_t s_addr; /* 按网络字节次序的地址 */ };
如果你要配置ipv6的,也有一个结构体进行配置:
struct sockaddr_in6 { sa_family_t sin6_family; /* AF_INET6 */ in_port_t sin6_port; /* port number */ uint32_t sin6_flowinfo; /* IPv6 flow information */ struct in6_addr sin6_addr; /* IPv6 address */ uint32_t sin6_scope_id; /* Scope ID (new in 2.4) */ }; struct in6_addr { unsigned char s6_addr[16]; /* IPv6 address */ };
但我们使用ipv4
的情况还是比较多,毕竟现在还是以ipv4
的情况比较多。
使用的方法很简单,上面我们不是创建好一个套接字了吗,我现在需要将我本机的IP和端口绑定到这个结构体中,然后使用bind
函数进行绑定即可:
struct sockaddr_in serveraddr = {0}; // 创建配置ipv4的结构体 serveraddr.sin_family = AF_INET; // 配置地址族 serveraddr.sin_port = htons(9999); // 配置端口,这里的端口自己指定 serveraddr.sin_addr.s_addr = inet_addr("192.168.1.5"); // 设置服务端的IP if (bind(sockfd, (struct sockaddr*)&serveraddr, sizeof(struct sockaddr))){ // 绑定IP和端口 // 错误判断 perror("fail not bind"); return -2; }
在上面出现了两个没有见过的函数,一个是htons
和一个inet_addr
,现在给大家解释一下。
2.1 主机字节序和网络字节序
这里需要重新回忆一下C语言中的大端存储和小端存储了。其实也就是每个人的计算机的字节序存储方式不一样,有些是大端存储,而有些是小端存储,如果直接进行通讯就像中国人和英语人交流信笺一样,两边的人都看不懂对象书写的内容,这个时候如果在写信的时候将自己写的内容转换成对方看得懂的内容是不是就能很好的解决看不懂的问题了。
为了使网络程序具有可移植性,使同样的C代码在大端计算机和小端计算机上编译后都能正常运行,就可以使用以下库函数做网络字节序和主机字节序的转换
主机字节序转换为网络字节序
#include <arpa/inet.h> uint32_t htonl(uint32_t hostlong); 功能: 将无符号整数hostlong从主机字节序转换为网络字节序 参数: hostlong:需要转换的主机字节序 返回值: 转换后的网络字节序 uint16_t htons(uint16_t hostshort); 功能: 将无符号短整型hostshort从主机字节序转换为网络字节序 参数: hostshort:需要转换的主机字节序 返回值: 转换后的网络字节序
网络字节序转换成主机字节序
#include <arpa/inet.h> uint32_t ntohl(uint32_t netlong); 功能: 将无符号短整数netlong转换为主机字节序 参数: netlong:需要转换的网络字节序 返回值: 转换后的主机字节序 uint16_t ntohs(uint16_t netshort); 功能: 将无符号短整数netshort转换为主机字节序 参数: netshort:需要转换的网络字节序 返回值: 转换后的主机字节序
2.2 点分制十进制转换
当你选择已经有一个需要发送的IP:192.168.1.2,你如果直接发送,网络是没办法知道你这个是什么内容的,需要进行一次转换将192.168.1.2转换为192,168,1,2这种使用数组存放的形式。而网络传给主机的IP地址为192,168,1,2这种使用数组的IP地址,所以需要经过转换变成192.168.1.2的形式。
在C语言中提供了两个函数来处理:
#include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> in_addr_t inet_addr(const char* cp); 功能: 将点分制字符串转换为点分制十进制串 参数: cp:需要转换的分制字符串 返回值: 转换后的点分制十进制串 char* inet_ntoa(struct in_addr in); 功能: 将点分制十进制串转换为点分制字符串 参数: in:点分制十进制串 返回值: 点分制字符串
3.recvfrom接收
绑定完成后就可以进行接收了,接收使用的是recvfrom
函数,这个函数的原型如下:
#include <sys/types.h> #include <sys/socket.h> size_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen); 功能:接收数据 参数: sockfd:文件描述符,socket的返回值 buf:保存接收的数据 len:buf的长度 flags:标志位 0 阻塞 MSG_DONTWAIT 非阻塞 src_addr:源的网络信息结构体(自动填充,定义变量传参即可) addrlen:src_addr的长度 返回值: 成功:接收的字节数 失败:‐1
使用的代码如下:
#define SIZE 1024 char buf[SIZE]; int len = sizeof(struct sockaddr); while(1){ // 循环接收 recvfrom(sockfd, buf, SIZE, 0, (struct sockaddr*)&serveraddr, &len); // 打印信息 printf("[%s %d]:%s\n", inet_ntoa(serveraddr.sin_addr), ntohs(serveraddr.sin_port), buf); }
这样就可以进行一次接收了。
介绍完使用的函数后我们的服务端的UDP程序就已经写完了,代码就是上面的那些。