1. UDP通信
UDP协议是一种无连接的,不可靠的通信协议。下面通过信件的例子说明UDP协议的特点:我们在寄信之前会在信封上填好收件人和寄件人的地址,然后投入邮箱即可,当然信件的特点是我们无法确保对方是否收到,因为在运输过程可能会发生信件丢失的情况。与之类似,UDP协议就是这样一种不可靠的协议。那么是否说明TCP协议是一种更好的协议呢?
如果从可靠性上说,TCP协议确实是一种更好的协议,但相对与TCP,UDP在结构上更加简单,不会发送类似于ACK的应答包,也不会发送类似于SEQ的流控制,因此效率上比TCP高出很多。除此之外,UDP编程也很简单,同时其丢包概率也不像我们想象的那么高,因此在更重视性能而非可靠性的情况下,UDP是一种很好的选择。
2. UDP通信原理
经过前面的学习,我们都知道UDP工作与IP层之上的传输层,那么在实际数据传输过程中UDP层与IP层分别承担什么责任呢?请看下面这张图:
在数据过程中,IP层仅负责将数据从主机B运送到主机A,当数据到达主机A时,还是依靠操作系统将数据分配给相应的套接字。
3. 客户端与服务端
类似于寄信,我们把信箱比作UDP套接字,当填上不同的收件人时,就可以将信寄给不同的收件人。UDP与此类似,通过手动指定对端,我们可以使用一个UDP套接字给不同的用户发送数据。
下面我们来介绍一下相关的API函数:
3.1 socket函数
函数原型为:
int socket(int domain, int type, int protocol); // 当type为 SOCK_DGRAM 时表示时UDP
3.2 sendto函数
函数原型为:
#include <sys/types.h>#include <sys/socket.h>/*** @brief 用于UDP发送数据* @param[in] sockfd 套接字* @param[in] buf 要发送的数据* @param[in] len 数据长度* @param[in] flags 标志,一般置为0* @param[in] dest_addr 对端地址信息* @param[in] addr_len 对端地址信息长度* @return -1:发送失败 len:发送的数据长度*/ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen);
3.3 recvfrom函数
函数原型为
#include <sys/types.h>#include <sys/socket.h>/*** @brief 用于UDP接收数据* @param[in] sockfd 套接字* @param[in] buf 要发送的数据* @param[in] len 数据长度* @param[in] flags 标志,一般置为0* @param[out] src_addr 数据发端地址信息* @param[out] addrlen 数据发端地址长度* @return -1:接收失败 len:接收的数据长度*/ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,struct sockaddr *src_addr, socklen_t *addrlen);
3.4 简单示例
server.cchar buf[BUF_SIZE];int sock_server;struct sockaddr_in addr;memset(&addr, 0, sizeof(addr));struct sockaddr_in addr_client;socklen_t addr_len;sock_server = socket(PF_INET, SOCK_DGRAM, 0);if (-1 == sock_server){printf("申请套接字错误 \n");return -1;}// 初始化网络地址addr.sin_family = AF_INET;addr.sin_addr.s_addr = htonl(INADDR_ANY);addr.sin_port = htons(atoi(argv[1]));if (-1 == bind(sock_server, (struct sockaddr *)&addr, sizeof(addr))){printf("绑定套接字到本地地址失败 \n");return -1;}while (1){int len = recvfrom(sock_server, buf, BUF_SIZE, 0, (struct sockaddr *)&addr_client, &addr_len);buf[len] = '\0';sendto(sock_server, buf, strlen(buf), 0, (struct sockaddr *)&addr_client, sizeof(addr_client));}close(sock_server);// client.cchar buf[BUF_SIZE];int sock_client;struct sockaddr_in addr;memset(&addr, 0, sizeof(addr));struct sockaddr_in addr_server;socklen_t addr_len;sock_client = socket(PF_INET, SOCK_DGRAM, 0);if (-1 == sock_client){printf("申请套接字错误 \n");return -1;}// 初始化网络地址addr.sin_family = AF_INET;addr.sin_addr.s_addr = inet_addr(argv[1]);addr.sin_port = htons(atoi(argv[2]));while (1){fputs("input message (q for quit):", stdout);fgets(buf, BUF_SIZE, stdin);if (0 == strcmp(buf, "q\n") || 0 == strcmp(buf, "Q\n")){break;}sendto(sock_client, buf, strlen(buf), 0, (struct sockaddr *)&addr, sizeof(addr));int len = recvfrom(sock_client, buf, BUF_SIZE, 0, (struct sockaddr *)&addr_server, &addr_len);buf[len] = '\0';printf("receive message from server:%s \n", buf);}
与TCP客户端程序不同的是,UDP客户端无需调用connect函数,仅通过指定对端地址信息便可直接发送数据,当然在UDP程序中也可以使用connect函数,但含义与TCP客户端并不相同,关于这一点我们下面详细解释。<br />除了`connect`函数,UDP客户端并没有为自己分配IP地址和端口,这是因为在调用`sendto函数`时,系统会自动选择本机IP地址和空闲端口号作为本地网络地址。
4. UDP数据包特性
我们都知道TCP连接是基于字节流的,其不存在”数据边界”,即当发送端多次发送数据时,只要接收端接收缓冲足够大,便可一次将这些数据接收回来,这也是”粘包”问题的由来。
UDP数据包不同于TCP,其数据包是存在”数据边界”的,也就是说:发送端发送三次数据时,接收端必须接收三次才能完成全部数据的接收,也就不存在”粘包”问题。
下面看一个例子:
// server.cfor (int i = 0; i < 3; ++i){sleep(5);int len = recvfrom(sock_server, buf, BUF_SIZE, 0, (struct sockaddr *)&addr_client, &addr_len);buf[len] = '\0';printf("第%d次接收,接收数据为:%s \n", i + 1, buf);}// client.cfor (int i = 0; i < 3; ++i){sendto(sock_client, "hello world", strlen("hello world"), 0, (struct sockaddr *)&addr, sizeof(addr));sendto(sock_client, "nice to meet you", strlen("nice to meet you"), 0, (struct sockaddr *)&addr, sizeof(addr));sendto(sock_client, "bye", strlen("bye"), 0, (struct sockaddr *)&addr, sizeof(addr));}运行结果为:第1次接收,接收数据为 hello world第2次接收,接收数据为 nice to meet you第三次接收,接收数据为 bye
5. UDP已连接套接字与未连接套接字
根据前面内容,我们知道UDP可以在发送前自由指定对端的网络地址,这种套接字被称为”未连接套接字”,在调用sendto``函数发送未连接套接字上的数据时,会经历下面三个步骤:
- 向UDP套接字注册对端IP地址和端口信息
- 发送数据
- 清除UDP套接字上的端口信息
在每次调用sendto函数时都会重复以上三个步骤,当我们只需要固定给一个对端传送数据时会极大的浪费效率,这时可以利用connect函数将对端地址信息注册到UDP套接字上(这样的UDP套接字叫做“已连接套接字”),这样每次发送时就省去了a、c两个步骤,效率提升非常多,此时我们也可以通过read、write函数进行数据的收发。
我们将前面的客户端改为“已连接套接字”模式:
// client.cchar buf[BUF_SIZE];int sock_client;struct sockaddr_in addr;memset(&addr, 0, sizeof(addr));struct sockaddr_in addr_server;socklen_t addr_len;sock_client = socket(PF_INET, SOCK_DGRAM, 0);if (-1 == sock_client){printf("申请套接字错误 \n");return -1;}// 初始化网络地址addr.sin_family = AF_INET;addr.sin_addr.s_addr = inet_addr(argv[1]);addr.sin_port = htons(atoi(argv[2]));// 将对端地址注册到套接字中if (-1 == connect(sock_client, (struct sockaddr *)&addr, sizeof(addr))){printf("注册本地地址到套接字中失败 \n");return -1;}while (1){fputs("input message (q for quit):", stdout);fgets(buf, BUF_SIZE, stdin);if (0 == strcmp(buf, "q\n") || 0 == strcmp(buf, "Q\n")){break;}write(sock_client, buf, strlen(buf));int len = read(sock_client, buf, BUF_SIZE);buf[len] = '\0';printf("receive message from server:%s \n", buf);}
