代码连接: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. Linux打印显示时间

    Linux打印显示时间具体使用参数如下: 1.输出当前年月日echo $(date +%F)2.输出当前时间(时分)echo $(date +%R)3.输出当前时间(时分秒)echo $(date + ...

  2. 实时音视频入门学习:开源工程WebRTC的技术原理和使用浅析

    本文由ELab技术团队分享,原题"浅谈WebRTC技术原理与应用",有修订和改动. 1.基本介绍 WebRTC(全称 Web Real-Time Communication),即网 ...

  3. 前端vue获取excell中的数据

    这个功能我觉得还是挺好的,比如要批量上传一些数据,然后不用一个一个填入直接写个excell表然后一起上传,然后我在这边记录一下 首先用到了xlsx这个插件 下载 npm i xlsx --save 使 ...

  4. 基于AI底座的数智油气田参考架构

      基于AI底座的数智油气田参考架构 Architecture for Intelligent & Digital Oilfileds Based-on AI 王权 2024.12.29   ...

  5. Report -「概率数据结构」随机化骗分?我们是专业的!

    \[\mathscr{Lorain~y~w~la~Lora~blea.} \newcommand{\DS}[0]{\displaystyle} % operators alias \newcomman ...

  6. MySQL存储引擎,锁,优化简述

    今天主要分享常见的存储引擎:MyISAM.InnoDB.MERGE.MEMORY(HEAP).BDB(BerkeleyDB)等,以及最常用的MyISAM与InnoDB两个引擎 ,文章尾部有两者的详细比 ...

  7. biancheng-Django(python)

    http://c.biancheng.net/django/ Django MTV和MVC的区别 1. MVC设计模式 我们先对 MVC 设计模式进行介绍,它是 Web 设计模式的经典之作,MTV 模 ...

  8. R语言学习数据挖掘

    1.用R计算数据基本统计量(均值) 学习机器学习和数据挖掘中的各种算法和模型,需要掌握统计学的基本概念.统计学是通过搜索.整理.分析数据等手段,以达到推断所测对象的本质,并预测对象未来走势的一门综合性 ...

  9. Redis持久化(RDB、AOF)

    为什么要持久化 Redis是内存数据库,如果不将内存中的数据库状态保存到磁盘中,那么一旦服务器进程退出,服务器的数据库状态就会消失(即断电即失).为了保证数据不丢失,我们需要将内存中的数据存储到磁盘, ...

  10. 网络通信协议:TCP(三次握手四次挥手)和UDP

    通信要素2:网络协议  网络通信协议计算机网络中实现通信必须有一些约定,即通信协议,对速率.传输代码.代码结构.传输控制步骤.出错控制等制定标准. 问题:网络协议太复杂计算机网络通信涉及内容很多, ...