代码连接:https://github.com/Afeather2017/anoii/blob/master/src/udp_peer.cc

以往写了TCP的多路复用,发现它还挺难写对的。现在写UDP的,发现似乎没有容易太多。

本人所在的公司,UDP用于本机通讯时(即loopback通讯),假设了一个UDP包总是能够完整的发送到对端,且对端总是能够回复一个完整的包,所以为了处理这个问题,费了些力气。

UDP基本用法

同步的UDP的使用过程如下,忽略了头文件与错误处理过程:

// 服务端
#define PORT 12345
#define BUFFER_SIZE 1024
int main() {
int sockfd;
struct sockaddr_in server_addr, client_addr;
socklen_t client_len = sizeof(client_addr);
char buffer[BUFFER_SIZE];
// 创建 UDP 套接字
sockfd = socket(AF_INET, SOCK_DGRAM, 0);
// 设置服务器地址
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = INADDR_ANY;
server_addr.sin_port = htons(PORT);
// 绑定套接字
bind(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0);
printf("UDP 服务器已启动,等待客户端连接...\n");
while (1) {
// 接收客户端消息
ssize_t recv_len = recvfrom(sockfd, buffer, BUFFER_SIZE - 1, 0,
(struct sockaddr *)&client_addr, &client_len);
if (recv_len > 0) {
buffer[recv_len] = '\0';
printf("收到来自 %s:%d 的消息:%s\n", inet_ntoa(client_addr.sin_addr),
ntohs(client_addr.sin_port), buffer);
// 发送回复消息
const char *response = "服务器已收到消息";
sendto(sockfd, response, strlen(response), 0,
(struct sockaddr *)&client_addr, client_len);
}
}
close(sockfd);
return 0;
}
// 客户端
#define PORT 12345
#define SERVER_IP "127.0.0.1"
#define BUFFER_SIZE 1024 int main() {
int sockfd;
struct sockaddr_in server_addr;
char buffer[BUFFER_SIZE];
// 创建 UDP 套接字
sockfd = socket(AF_INET, SOCK_DGRAM, 0);
// 设置服务器地址
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = inet_addr(SERVER_IP);
server_addr.sin_port = htons(PORT);
// 发送消息
const char *message = "你好,服务器!";
sendto(sockfd, message, strlen(message), 0,
(struct sockaddr *)&server_addr, sizeof(server_addr));
printf("已发送消息:%s\n", message);
// 接收服务器回复
ssize_t recv_len = recvfrom(sockfd, buffer, BUFFER_SIZE - 1, 0, NULL, NULL);
if (recv_len > 0) {
buffer[recv_len] = '\0';
printf("收到来自服务器的消息:%s\n", buffer);
}
close(sockfd);
return 0;
}

好吧,正常流程就这么多,但是也有许多坑。

坑1: 如何才能完整发送一个包?

UDP没有传输控制,所以无法确定发送的包被接收到,这里说的完整发送就只是交给了操作系统罢了。这里不考虑操作系统拿到整个包只有由于缓冲区不够而丢弃包的问题。

Linux中,一次成功的sendto就代表发送一个包,一次成功的recvfrom就代表接收了一个包。所以不用担心sendto和recvfrom成功后,你实际发送或接收的是半个包。如果sendto发送的包大小超过了MTU该怎么办?不用担心,UDP会进行分段发送(即IP层分片),分段发送后,如果对端完整接收了所有分段,就会把它们组装起来,否则全部丢弃。

理论能发送的包的最大大小是65527。它不是65535,原因是UDP头部中的长度字段的范围是0到65535,包括了头部8字节的大小。所以如果你尝试发送一个大小是65528的UDP包,会出现包过长的问题:

但是实际上还有其他地方进行了限制,实际能够发送的包的大小会比65527小许多。我的电脑可以发送35000的包,zerotier似乎只支持1472以内的包。

sendto出错时,可能有很多的情况。其errno值中有一项是EINTR,意思是被中断了。比如说发送的时候收到了SIGPIPE之类的信号,那么这个时候你这个包就没有成功提交到操作系统中,那么这个包将不可能进入到网络中。所以写了很长一段恶心的代码:

void UdpPeer::SendTo(const char *data, int size, InetAddr &addr) {
assert(size > 0);
auto *sock_addr = addr.GetSockAddr();
for (;;) {
int sent = ::sendto(fd_, data, size, 0, sock_addr, sizeof(*sock_addr));
if (sent == size) {
if (!binded_addr_) { // 如果没有绑定,那么sendto之后会系统偷偷绑定了一个ip:port
binded_addr_ = true;
addr_ = GetLocalAddr(fd_);
}
return;
}
if (sent > 0) {
Error("Wants to send {} but sent {} actually", size, sent);
return;
}
Error(strerror(errno));
// 根据man文档可以得知以下错误。但实际上似乎没有必要关心这么多的问题。
switch (errno) {
......
case EINTR:
// 中断。再试试
continue;
......
}
}
}

坑2: 如何确保完整接收了一个包?

这个问题实际上是如何确保已经到达操作系统的包被完整的接收

除了上文说的EINTR以外,还有一个关键的recvfrom的缓冲区参数的问题。

recvfrom的声明是:

ssize_t recvfrom(int socket, void *restrict buffer, size_t length,
int flags, struct sockaddr *restrict address,
socklen_t *restrict address_len);

如果成功执行,那么返回值是填入缓冲区的数据量,否则返回-1。

当flags为0的时,如果这个包的大小超过了length,那么你只拿到了包的前半部分,后半部分拿不到了,直接丢弃;如果没超过,那么说明你的包被完整接收到程序里面了。

这里的超过与没超过,实际上是recvfrom的返回值size与length对比。如果length > size,表示这个包确确实实被完整接收了;如果length = size,那么这个包有可能没有被完整接收;length < size的情况不存在。

当flags为MSG_PEEK的时候,recvfrom就只是拿包出来看了一眼而已,并不会丢弃这个包。所以我们可以通过这个方式来试探一个包的大小。

所以,只有length > size的时候才可以把这个包交给回调。因此又写了一段恶心的代码:

void UdpPeer::OnMessage() {
InetAddr peer{};
auto *peer_addr = peer.GetSockAddr();
socklen_t len = sizeof(*peer.GetSockAddr());
int size;
if (!binded_addr_) {
Fatal("Tries recvfrom an unbinded UDP socket");
}
for (;;) {
size = ::recvfrom(fd_,
buffer_.data(),
buffer_.size(),
auto_buffer_size_ ? MSG_PEEK : 0,
peer_addr,
&len);
if (size > 0) {
if (!auto_buffer_size_) {
if (size < buffer_.size()) break;
Error("Package corrupted, ignore it.");
return;
}
if (size >= buffer_.size()) {
// UDP的包的长度字段包括了首部的长度,所以不是65535
// 试探一个包的大小。
if (buffer_.size() * 2 >= 65527 + 1) {
buffer_.resize(65527 + 1);
} else {
buffer_.resize(buffer_.size() * 2);
}
continue;
}
::recvfrom(fd_, nullptr, 0, 0, nullptr, nullptr);
break;
}
switch (size) {
......
case EINTR:
if (!auto_buffer_size_) return;
// 中断,由于使用的是PEEK参数,所以中断之后这个数据还有救
continue;
......
}
}
// 啧,坑真多……只有缓冲区比size大才可能表明接收的是整个包而不是半个。
// 如果没有保证尽量接收,即auto_buffer_size_=false,那么就有可能出现这种情况
assert(size >= 0);
assert(size < buffer_.size()); // Package corrupted
readable_cb_(this, peer, buffer_.data(), size);
}

坑3: 没有bind的时候进行了recvfrom

如果一个UDP socket没有进行bind,此时recvfrom,如果是阻塞IO,那么此时recvfrom会永远阻塞。非阻塞是否会有这个情况我不知道,也不想试,所以就加上了这样的判断。前文的binded_addr_就是做这个的。

在sendto调用的时候,操作系统会“隐式”地bind一个端口给socket,所以sendto的时候也会设置binded_addr_。

坑4: 与多路复用结合

poll有POLLIN, POLLOUT,而UDP中POLLOUT是没用的,因为它没有TCP那样的传输控制,没有发送缓冲区,所以我们只要设置一个POLLIN即可。其他多路复用函数操作类似。

Anoii之UDP与多路复用的更多相关文章

  1. 运输层协议--TCP及UDP协议

    TCP及UDP协议 按照网络的五层分级结构来看,TCP及UDP位于运输层,故TCP及UDP是运输层协议.TCP协议--传输控制协议UDP协议--用户数据报协议 多路复用及多路分解 图多路复用及多路分解 ...

  2. RTP与RTCP协议介绍(转载)

    RTSP发起/终结流媒体.RTP传输流媒体数据 .RTCP对RTP进行控制,同步.RTP中没有连接的概念,本身并不能为按序传输数据包提供可靠的保证,也不提供流量控制和拥塞控制,这些都由RTCP来负责完 ...

  3. RTP与RTCP协议介绍

    转自:http://zhangjunhd.blog.51cto.com/113473/25481/ 本文主要介绍RTP与RTCP协议. author: ZJ   06-11-17 Blog: [url ...

  4. [译] QUIC Wire Layout Specification - Introduction & Overview | QUIC协议标准中文翻译(1) 简介和概述

    本文同步发布于: https://www.pengrl.com/p/33330/ ,转载请注明出处,谢谢. 目录 Introduction | 简介 Conventions and Definitio ...

  5. 盘点Linux运维常用工具(一)-web篇之httpd

    #前言:想把自己学的各种服务进行分类归档起来,于是就写了盘点Linux运维常用工具,Linux方面使用到的web应用服务有httpd(apache).nginx.tomcat.lighttpd,先了解 ...

  6. QOE 驱动下的分布式实时网络构建:Agora SD-RTN 的演进

    编者按:近日,全球软件案例研究峰会在北京召开.全球软件案例研究峰会(简称"TOP100Summit")是科技界一年一度的案例研究榜单,每年甄选年度最值得借鉴的100个好案例,旨在揭 ...

  7. Linux网络通信编程(套接字模型TCP\UDP与IO多路复用模型select\poll\epoll)

    Linux下测试代码: http://www.linuxhowtos.org/C_C++/socket.htm TCP模型 //TCPClient.c #include<string.h> ...

  8. Python(七)Socket编程、IO多路复用、SocketServer

    本章内容: Socket IO多路复用(select) SocketServer 模块(ThreadingTCPServer源码剖析) Socket socket通常也称作"套接字" ...

  9. python学习笔记-(十四)I/O多路复用 阻塞、非阻塞、同步、异步

    1. 概念说明 1.1 用户空间与内核空间 现在操作系统都是采用虚拟存储器,那么对32位操作系统而言,它的寻址空间(虚拟存储空间)为4G(2的32次方).操作系统的核心是内核,独立于普通的应用程序,可 ...

  10. HP-SOCKET TCP/UDP通信框架库解析

    项目概述: HP-SOCKET是一套通用TCP/UDP通信框架,包括服务器.客户端.Agent组件:其目标是提供高性能.通用性.简易性.可扩展.可定制: 鉴于此,其仅实现基本的通用框架通信.数据收发功 ...

随机推荐

  1. Mybatis plus 多表联查字段名重复报错 Column ‘id‘ in where clause is ambiguous

    一.报错信息 Caused by: Column 'xxxx' in where clause is ambiguous 二.报错原因 表 person 和 表 class 都有字段 id 和 nam ...

  2. Centos yum命令提示failed to set locale, defaulting to C

    目录 问题 locale提示 解决方案 问题 在输入yum命令是出现 Failed to set locale, defaulting to C 1 出现这个问题是由于系统没有正确设置locale环境 ...

  3. Ubuntu安装宝塔服务

    Linux面板7.9.4安装脚本 查看详细安装教程 使用 SSH 连接工具,如 堡塔SSH终端 连接到您的 Linux 服务器后, 挂载磁盘 ,根据系统执行相应命令开始安装(大约2分钟完成面板安装): ...

  4. 单片机的主程序中为什么都要加一个while(1)?

    *** * C51 为什么都要加一个while(1)?****** while(1)的作用: while(1) 是一个死循环 为了不让代码继续向下执行. 单片机中使用while(1),大部分:为了防止 ...

  5. 「工具分享」Checker Script for Linux

      以前整的一个 Linux 下对拍程序 qwq.   建一个文件夹, 假设叫 dir, 然后把 checker.sh 扔进去, 顺便 chmod +x checker.sh. 你需要自己设置一下代码 ...

  6. kafka介绍和使用

    1 Kafka简介 ​Kafka是最初由Linkedin公司开发,它是一个分布式.可分区.多副本,基于zookeeper协调的分布式日志系统:常见可以用于web/nginx日志.访问日志,消息服务等等 ...

  7. 第四五章 (Nginx+Lua)Lua模块开发

    在实际开发中,不可能把所有代码写到一个大而全的lua文件中,需要进行分模块开发:而且模块化是高性能Lua应用的关键.使用require第一次导入模块后,所有Nginx 进程全局共享模块的数据和代码,每 ...

  8. biancheng-算法教程

    目录http://c.biancheng.net/algorithm/ 1算法是什么2时间复杂度和空间复杂度3递归算法4斐波那契数列5分治算法6找数组的最大值和最小值7汉诺塔问题8贪心算法9部分背包问 ...

  9. Exfiltrated pg walkthrough Easy

    80端口弱口令admin admin 发现cms 搜索exp 发现漏洞 https://www.exploit-db.com/exploits/49876 找到敏感数据库密码和用户 ╔════════ ...

  10. 《SpringBoot》EasyExcel实现百万数据的导入导出

    24年11月6日消息,阿里巴巴旗下的Java Excel工具库EasyExcel近日宣布,将停止更新,未来将逐步进入维护模式,将继续修复Bug,但不再主动新增功能. EasyExcel 是一款知名的 ...