代码连接: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. [python]Markdown图片引用格式批处理桌面应用程序

    需求 使用python编写一个exe,实现批量修改图片引用,将修改后的文件生成为 文件名_blog.md.有一个编辑框,允许接收拖动过来md文件,拖入文件时获取文件路径,有一个编辑框编辑修改后的文件的 ...

  2. 开源产品测评之 SQL 上线能力

    背景 近期,我司准备引入一款 SQL 审核产品来供内部流程使用,解决目前 SQL 人工上线的流程管控问题,目标是对业内的开源产品进行调研,选型一款作为落地方案,后期如果内部有需求可能会进行二次开发.我 ...

  3. arch 输入法

    输入法配置 输入法采用fcitx + Sogou的组合,安装需要的包: yay -S fcitx fcitx-im fcitx-configtool fcitx-sogoupinyin 然后写一个fc ...

  4. 【转载】茅台巽风app地图详解,做任务不迷路,纯手绘

    茅台发布了新的app"巽风" 根据"巽值"的排名,发放20000个虎年茅台的资格,还是可以玩一玩的 哪些途径获取"巽值" 1.做任务,和游戏 ...

  5. MySql中的driverClassName、url

    在Java桌面开发或者Java Web开发(基于SSM框架)配置MySQL数据源时,driverClassName属性如果填错了,会导致了这一系列错误.归结其原因就是 mysql-connector- ...

  6. 阿里IM技术分享(十):深度揭密钉钉后端架构的单元化演进之路

    本文由钉钉技术专家啸台.万泓分享,为了获得更好的阅读效果,本文已对内容进行少修订和重新排版. 1.引言 钉钉后端架构的单元化工作从2018年开始到今年,已经是第五个年头了.五年的时间,钉钉单元化迭代了 ...

  7. Raspberry pi 上部署调试.Net的IoT程序

    树莓派(Raspberry pi)是一款基于ARM 架构的单板计算机(Single Board Computer),可以运行各种 Linux 操作系统,其官方推荐使用的 Raspberry Pi OS ...

  8. Transformers in Vision

    Transformers in Vision 介绍 最初引入现在著名的Attention is all you need1,Transformer 多年来一直主导着自然语言处理 (NLP) 领域.特别 ...

  9. Django_使用汇总(1)

    使用django(4.1.5) 搭建股票信息后台,显示股票信息: Stock -> models.py class Stock(models.Model): symbol = models.Ch ...

  10. 批量查找替换工具(C#)

    自己写了了个批量查找替换工具(C#),目前已知问题有查找速度不够快,假死现象等. using System; using System.Collections.Generic; using Syste ...