Anoii之UDP与多路复用
代码连接: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与多路复用的更多相关文章
- 运输层协议--TCP及UDP协议
TCP及UDP协议 按照网络的五层分级结构来看,TCP及UDP位于运输层,故TCP及UDP是运输层协议.TCP协议--传输控制协议UDP协议--用户数据报协议 多路复用及多路分解 图多路复用及多路分解 ...
- RTP与RTCP协议介绍(转载)
RTSP发起/终结流媒体.RTP传输流媒体数据 .RTCP对RTP进行控制,同步.RTP中没有连接的概念,本身并不能为按序传输数据包提供可靠的保证,也不提供流量控制和拥塞控制,这些都由RTCP来负责完 ...
- RTP与RTCP协议介绍
转自:http://zhangjunhd.blog.51cto.com/113473/25481/ 本文主要介绍RTP与RTCP协议. author: ZJ 06-11-17 Blog: [url ...
- [译] QUIC Wire Layout Specification - Introduction & Overview | QUIC协议标准中文翻译(1) 简介和概述
本文同步发布于: https://www.pengrl.com/p/33330/ ,转载请注明出处,谢谢. 目录 Introduction | 简介 Conventions and Definitio ...
- 盘点Linux运维常用工具(一)-web篇之httpd
#前言:想把自己学的各种服务进行分类归档起来,于是就写了盘点Linux运维常用工具,Linux方面使用到的web应用服务有httpd(apache).nginx.tomcat.lighttpd,先了解 ...
- QOE 驱动下的分布式实时网络构建:Agora SD-RTN 的演进
编者按:近日,全球软件案例研究峰会在北京召开.全球软件案例研究峰会(简称"TOP100Summit")是科技界一年一度的案例研究榜单,每年甄选年度最值得借鉴的100个好案例,旨在揭 ...
- Linux网络通信编程(套接字模型TCP\UDP与IO多路复用模型select\poll\epoll)
Linux下测试代码: http://www.linuxhowtos.org/C_C++/socket.htm TCP模型 //TCPClient.c #include<string.h> ...
- Python(七)Socket编程、IO多路复用、SocketServer
本章内容: Socket IO多路复用(select) SocketServer 模块(ThreadingTCPServer源码剖析) Socket socket通常也称作"套接字" ...
- python学习笔记-(十四)I/O多路复用 阻塞、非阻塞、同步、异步
1. 概念说明 1.1 用户空间与内核空间 现在操作系统都是采用虚拟存储器,那么对32位操作系统而言,它的寻址空间(虚拟存储空间)为4G(2的32次方).操作系统的核心是内核,独立于普通的应用程序,可 ...
- HP-SOCKET TCP/UDP通信框架库解析
项目概述: HP-SOCKET是一套通用TCP/UDP通信框架,包括服务器.客户端.Agent组件:其目标是提供高性能.通用性.简易性.可扩展.可定制: 鉴于此,其仅实现基本的通用框架通信.数据收发功 ...
随机推荐
- bug记录:Vue.use 加载 TabPane ,浏览器卡死
问题描述 Vue.use 加载 TabPane ,浏览器卡死 原因分析 参考资料:https://blog.csdn.net/ye987987/article/details/103780297 经过 ...
- Qt/C++中英输入法/嵌入式输入法/小数字面板/简繁切换/特殊字符/支持Qt456
一.前言 在嵌入式板子上由于没有系统层面的输入法支持,所以都绕不开一个问题,那就是在需要输入的UI软件中,必须提供一个输入法来进行输入,大概从Qt5.7开始官方提供了输入法的源码,作为插件的形式加入到 ...
- blast只保留一个最优结果
使用blast比对时,只保留一个最优结果 代码: blastn -db nt.blast.db -query seq.fa -out blast.nt.result -evalue 1e-5 -out ...
- docker和主机之间文件传输
docker和主机之间文件传输 1.从docker内部向主机传输文件 语法: docker cp [OPTIONS] CONTAINER:SRC_PATH DEST_PATH|- 示例:将容器96f7 ...
- 【C语言学习】——命令行编译运行 C 语言程序的完整流程
今天要学习的内容是 命令行编译运行 C 语言程序,进一步理解C语言编译运行的底层实现和编译原理相关知识,下面是命令行编译运行 C 语言程序的完整流程 一.理论讲解 1. 编译原理概述 1.1 编译过程 ...
- RESTful 架构详解-copy
1. 什么是REST REST全称是Representational State Transfer,中文意思是表述(编者注:通常译为表征)性状态转移. 它首次出现在2000年Roy Fielding的 ...
- 如何快速的开发一个完整的iOS直播app(采集篇)
作者:袁峥链接:https://www.jianshu.com/p/c71bfda055fa来源:简书著作权归作者所有.商业转载请联系作者获得授权,非商业转载请注明出处. 开发一款直播app,首先需要 ...
- ClickHouse-1介绍
https://clickhouse.com/docs/zh/ 一.clickhouse简介 ClickHouse 是俄罗斯的Yandex开源的用于在线分析处理查询(OLAP :Online Anal ...
- kafka的server.properties文件描述
版本:基于 kafka 2.4.0 http://archive.apache.org/dist/kafka/2.4.0/kafka_2.11-2.4.0.tgz # Licensed to the ...
- linux:安装php7.x
参考:链接 更新yum源 CentOS/RHEL 7.x: rpm -Uvh https://dl.fedoraproject.org/pub/epel/epel-release-latest-7.n ...