news 2026/10/18 21:55:34

C++ UDP通信实战:从Socket编程到心跳广播的完整指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
C++ UDP通信实战:从Socket编程到心跳广播的完整指南

CSDN 和各类技术社区里搜“C++ UDP 通信”,搜出来的结果往往两极分化:一边是课堂作业级别的 demo 代码,另一边是封装到看不出协议痕迹的高级框架。写 UDP 的人其实都清楚,这个协议离业务层非常近——不需要维护连接、不需要考虑接收窗口,一个 socket 加几个函数就能打通客户端和服务端的通道。我最近在某个项目里需要做局域网内的实时数据采集,二十多台设备每隔几百毫秒上报一条状态,量不大但延迟敏感,偶尔丢一两条也不影响整体统计。这种任务用 TCP 反而别扭,UDP 几乎是为它量身定做的。

这篇就把我用 C++ 实现 UDP 客户端与服务端通信的过程完整拆开:从协议差异到 API 参数,从两份可编译的完整代码到“能跑起来”之后再补上的可靠性设计,最后还有实际调试时踩过的坑。无论你是刚接触网络编程,还是想快速给现有工程塞一块 UDP 通信模块,都能从里面拿到可以直接抄走的东西。

1. 为什么这个任务我选 UDP:先聊清楚协议再动手写代码

1.1 UDP 的“无连接”到底意味着什么

很多教程把 UDP 概括为“不可靠传输”,这个说法容易误导人。UDP 真正的基础特性是“无连接”:发送方拿一个目标 IP 和端口就能把数据报丢出去,不需要先建立连接、不需要握手、也不需要维护对端状态。至于这个数据报能不能到,那是网络层和接收方的事,协议本身不给你任何承诺。

用生活里的例子类比会更直白。TCP 像打电话:先拨号、等待接通、确认对方在听,然后你说一句我听一句,最后还要挂机。UDP 像寄自助明信片:写好收件地址往邮筒里一丢,邮局不给你回执,明信片可能中途丢了、可能顺序乱了、也可能三天后才到。但对某些场景来说,这种“丢完不管”反而优势巨大——寄明信片不需要占着一条电话线路,发 UDP 包也不需要占着一条 TCP 连接。

UDP 数据报还有一个被很多人忽略的特性:边界完整性。每个sendto调用发出的内容,接收方通过一次recvfrom拿到的就是完整的一整块,不会出现 TCP 那种“半个包先来、剩下的稍后到”的流式分割。这个特性在实现自定义协议时非常省心。

1.2 和 TCP 的关键差异:三次握手、流式与数据报的取舍

老生常谈的 TCP 三次握手我就不展开背了,但有几个和实际编码直接相关的差异,必须放在台面上对比。TCP 是流协议,接收方不知道一条完整的业务消息在哪里结束;UDP 是消息协议,一次收发对应一条完整消息。TCP 自带重传排序,UDP 把这些责任全部推给应用层。TCP 有连接状态,服务端需要维护每一条连接的缓冲区;UDP 没有连接,服务端不需要accept,谁发来消息就处理谁。

维度TCPUDP
连接状态面向连接,需要握手/挥手无连接,零状态收发
消息边界流式,无边界数据报,有边界
可靠性重传、去重、排序无保证,可能丢包/乱序
服务端模型accept 后每连接独立处理一个 socket 收所有客户端的消息
单次发送上限无限制(依赖缓冲区)受限于数据报大小

这里说一句经验之谈:项目里如果消息频率高、单条消息小、实时性要求严,UDP 几乎是必然选择。像实时对战、语音片段传输、传感器高频上报这些场景,TCP 的重传机制反而会引入延迟,而 UDP 丢个把包根本没人感知得到。

1.3 动手前的 API 预备:socket / bind / sendto / recvfrom 全家福

C++ 本身没有网络库,跨平台最通用的还是 POSIX socket API。Windows 上叫 Winsock,Linux 上就是系统库,名字和用法几乎一致。UDP 服务端的关键函数总共就四个:socket()创建套接字、bind()绑定本地地址和端口、recvfrom()接收数据报、sendto()发送数据报。客户端更简单,连bind都可以省。

有个小细节值得记在脑子里:socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP)里SOCK_DGRAM就是“数据报”的英文缩写,看到它就等于告诉内核“我接下来要走 UDP”;TCP 用的SOCK_STREAM是流式套接字。这个参数和协议栈的对接方式不同,不能在 UDP 套接字上把容量撑满来模拟 TCP。

Windows 上比 Linux 多两步初始化:WSAStartup(MAKEWORD(2, 2), &wsa)和程序退出时的WSACleanup(),Linux 上不需要。另外 Windows 的close不适用于 socket,要用closesocket。我在后面的完整代码里通过宏做了跨平台处理,可以直接在两套系统上编译。

2. 服务端第一版:socket + bind + recvfrom/sendto 的最小闭环

2.1 服务端初始化:socket() 与 bind() 的正确姿势

服务端第一个动作是创建 UDP 套接字。第二个动作是bind。bind的作用是把套接字绑定到一个确定的 IP:端口上,这样外部客户端才能找到入口。服务端不 bind 的话,内核只会给它分配一个临时端口,局域网里的客户端根本不知道往哪发。

我见过不少新手把bind的地址写成127.0.0.1,这在本地自测没问题,一搬到局域网就收不到任何设备的消息。正确姿势是用INADDR_ANY,它表示绑定本机所有网卡上的对应端口,不管数据是从哪个网卡进来都能收到。s_addr字段本身要求网络字节序,htonl(INADDR_ANY)和直接赋INADDR_ANY在这个宏等于 0 的情况下是等价的,不过再强调一次:127.0.0.1和0.0.0.0的差别是真实存在且极其致命的。

端口选择上我习惯用 4 位数的常用区间,比如 9000,避免和系统预留端口冲突。注意端口号一律要经过htons转成网络字节序,这是新手最容易漏的一步——漏掉之后 bind 绑定的实际端口会和你预期完全不同。

2.2 recvfrom 收包和 sendto 回包:参数拆解与常见误解

recvfrom的参数里有三个容易混淆的尾部参数:来源地址结构、地址长度指针、和标志位。client_addr会填上“这条消息是从哪个 IP 的哪个端口发来的”,这是后面回包的关键。有几个新手会犯的错,我在调试中见过多次:

  1. client_len必须初始化成sizeof(client_addr)。内核写入地址时会同时更新这个长度,如果你手滑写成了 0,调用直接失败返错。
  2. recv_buffer要留一个字节的余地。因为 UDP 报文字节流没有\0结尾,我却习惯把缓冲区当字符串打印,手动在末尾补\0时必须保证不越界,所以读取长度要传sizeof(recv_buffer) - 1。
  3. recvfrom默认是阻塞的。没有消息时线程会卡在调用里,后面讲多客户端和超时处理时我会专门说怎么绕开。

sendto和服务端收包是对称的:目标地址结构从recvfrom直接拿,地址结构体长度按实际拷贝。回包时如果把client_addr换成自己手写的地址,很可能因为临时端口不清楚而导致客户端收到不到响应。这块我会在坑的部分单独展开。

2.3 服务端完整代码与运行效果

下面是完整可编译的 UDP 服务端代码,同时兼容 Linux 和 Windows。用它作为起点完全够用。

// udp_server.cpp // Linux 编译运行: g++ -O2 -o udp_server udp_server.cpp && ./udp_server #include <cstdio> #include <cstring> #include <cstdlib> #ifdef _WIN32 #include <winsock2.h> #include <ws2tcpip.h> #pragma comment(lib, "ws2_32.lib") #else #include <unistd.h> #include <arpa/inet.h> #include <sys/socket.h> #include <netinet/in.h> #define SOCKET int #define INVALID_SOCKET (-1) #define SOCKET_ERROR (-1) #define closesocket close #endif int main() { #ifdef _WIN32 WSADATA wsa; WSAStartup(MAKEWORD(2, 2), &wsa); #endif SOCKET server_fd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); if (server_fd == INVALID_SOCKET) { perror("socket"); return 1; } sockaddr_in server_addr; std::memset(&server_addr, 0, sizeof(server_addr)); server_addr.sin_family = AF_INET; server_addr.sin_addr.s_addr = htonl(INADDR_ANY); server_addr.sin_port = htons(9000); if (bind(server_fd, (sockaddr*)&server_addr, sizeof(server_addr)) == SOCKET_ERROR) { perror("bind"); closesocket(server_fd); return 1; } printf("UDP server listening on 0.0.0.0:9000...\n"); char recv_buffer[2048]; while (true) { sockaddr_in client_addr; socklen_t client_len = sizeof(client_addr); int ret = recvfrom(server_fd, recv_buffer, sizeof(recv_buffer) - 1, 0, (sockaddr*)&client_addr, &client_len); if (ret == SOCKET_ERROR) { perror("recvfrom"); break; } recv_buffer[ret] = '\0'; char client_ip[INET_ADDRSTRLEN] = {0}; inet_ntop(AF_INET, &client_addr.sin_addr, client_ip, sizeof(client_ip)); printf("[recv] from %s:%d -> %s\n", client_ip, ntohs(client_addr.sin_port), recv_buffer); const char* reply = "pong"; sendto(server_fd, reply, std::strlen(reply), 0, (sockaddr*)&client_addr, sizeof(client_addr)); } closesocket(server_fd); #ifdef _WIN32 WSACleanup(); #endif return 0; }

运行后,服务端会阻塞等待,任何客户端给它发字符串,它都会打印来源并回一个pong。这已经是一个完整的“客户端-服务端”通信闭环的服务器侧。

3. 客户端配合式写法:sendto 发包和 recvfrom 收回执

3.1 客户端为什么通常不用 bind:由内核自动分配的临时端口

客户端不需要bind。当它第一次调用sendto时,内核会自动给它分配一个临时端口,这个端口号通常在 49152 以上。服务端通过recvfrom拿到这个临时端口后,回包才有地方送。

那什么时候客户端也可以bind?当你想让服务端通过固定端口识别这个客户端、或者想在同一台机器上启动多个客户端实例时,才需要显式绑定。不过多个进程绑定同一个客户端端口会直接报Address already in use,所以日常开发里客户端保持“不 bind,让内核分配”是更省心的做法。

这里再多说一句:客户端不 bind 不是“没有地址”,而是地址由内核自动生成。服务端recvfrom拿到的client_addr就是这个自动生成的地址,完整有效,可以直接用于回包。

3.2 客户端完整代码:一个 ping/pong 的最小闭环

客户端代码要做的四件事:创建 UDP 套接字、构造服务端地址结构、sendto发消息、recvfrom等回执。回执等待建议加上超时控制,不然服务端不在线时客户端会一直挂在recvfrom上,看起来像死机。

// udp_client.cpp // Linux 编译运行: g++ -O2 -o udp_client udp_client.cpp && ./udp_client 127.0.0.1 #include <cstdio> #include <cstring> #include <cstdlib> #ifdef _WIN32 #include <winsock2.h> #include <ws2tcpip.h> #pragma comment(lib, "ws2_32.lib") #else #include <unistd.h> #include <arpa/inet.h> #include <sys/socket.h> #include <netinet/in.h> #include <sys/time.h> #define SOCKET int #define INVALID_SOCKET (-1) #define SOCKET_ERROR (-1) #define closesocket close #endif int main(int argc, char* argv[]) { const char* server_ip = (argc > 1) ? argv[1] : "127.0.0.1"; const int server_port = 9000; #ifdef _WIN32 WSADATA wsa; WSAStartup(MAKEWORD(2, 2), &wsa); #endif SOCKET client_fd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); if (client_fd == INVALID_SOCKET) { perror("socket"); return 1; } sockaddr_in server_addr; std::memset(&server_addr, 0, sizeof(server_addr)); server_addr.sin_family = AF_INET; server_addr.sin_port = htons(server_port); inet_pton(AF_INET, server_ip, &server_addr.sin_addr); for (int i = 0; i < 5; i++) { const char* msg = "ping"; int sent = sendto(client_fd, msg, std::strlen(msg), 0, (sockaddr*)&server_addr, sizeof(server_addr)); if (sent == SOCKET_ERROR) { perror("sendto"); break; } // 设置 recvfrom 超时 1 秒 #ifdef _WIN32 DWORD timeout = 1000; setsockopt(client_fd, SOL_SOCKET, SO_RCVTIMEO, (char*)&timeout, sizeof(timeout)); #else timeval tv = {1, 0}; setsockopt(client_fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv)); #endif char reply[512] = {0}; sockaddr_in from_addr; socklen_t from_len = sizeof(from_addr); int ret = recvfrom(client_fd, reply, sizeof(reply) - 1, 0, (sockaddr*)&from_addr, &from_len); if (ret == SOCKET_ERROR) { #ifdef _WIN32 if (WSAGetLastError() == WSAETIMEDOUT) { #else if (errno == EAGAIN || errno == EWOULDBLOCK) { #endif printf("request %d timed out, no reply\n", i + 1); continue; } perror("recvfrom"); break; } reply[ret] = '\0'; printf("request %d reply -> %s\n", i + 1, reply); } closesocket(client_fd); #ifdef _WIN32 WSACleanup(); #endif return 0; }

这个客户端连发 5 个ping,每个都等待回执。配合上面的服务端跑一下,终端里会看到服务端打印 5 次来源和内容,客户端打印 5 次pong回复。到这里,UDP 通信的最小闭环已经完整落地了。

3.3 验证工具:ping 命令不行就上 netstat / Wireshark / tcpdump

代码跑通之后,最好再掌握几个验证手段。首先明确:ping命令走的是 ICMP 协议,和 UDP 的收发没有直接关系,项目里“能 ping 通”只能说明主机在网络层可达,代表不了 UDP 端口可用。

要看端口监听状态,Windows 用netstat -an | findstr 9000,Linux 用netstat -an | grep 9000,能看到 UDP 监听条目基本就说明服务端 bind 成功了。要抓包看具体报文交互,Windows 上用 Wireshark,Linux 上直接tcpdump -i lo udp port 9000,能清楚看到请求报文和回包报文的 IP、端口、长度,这是排查“消息到底发没发出去”的最快路径。

另外一个经验:Wireshark 抓本地回环包时,Windows 上可能会因为驱动原因抓不到,可以先抓局域网实网流量验证;Linux 上指定-i lo抓127.0.0.1完全没问题。

4. 跑通之后我踩了三个坑:绑定地址、回包端口与缓冲区撕裂

4.1 第一个坑:bind 到 127.0.0.1 后局域网设备全连不上

这个坑是团队里真实发生过的。当时代码在开发机上一切正常,本地客户端和服务端收发顺畅,部署到现场后局域网设备一个都连不上。排查了半天,最后发现服务端初始化时写的是:

server_addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK); // 127.0.0.1

回环地址只监听本机虚拟网卡,局域网物理网卡进来的包根本不会分发给这个 socket。改成INADDR_ANY或者显式的inet_addr("0.0.0.0")之后,现场设备立马上线。

这个坑的隐性成本在于:开发机上的127.0.0.1自测完全能通过,代码很容易带到现场才暴露问题。所以我的习惯是服务端构造成员函数里显式注释一句“监听所有网卡,勿改回 127.0.0.1”,防止后来接手的人改回来。

4.2 第二个坑:sendto 之后服务端回包收不到

另一个常见场景是服务端确实收到了消息,也调用了sendto,客户端却收不到回包。把服务端代码翻出来一看,问题出在回包目标地址上。有人图省事,直接复制了一份“写死客户端 IP 和端口”的地址结构,但问题在于客户端如果用临时端口,你根本不知道它这次用的端口号是多少。每次客户端启动时端口都可能不同,写死端口必然导致回包失败。

正确做法是每次recvfrom都要保住client_addr里的端口和 IP,回包原样带回。即使知道客户端 IP,端口也必须从recvfrom获取。解决办法其实已经写在前面的服务端完整代码里了,把client_addr完整传回sendto。

还有一个隐藏知识点:如果在客户端上对 UDP socket 调用了connect(server_addr),那么之后可以直接用send()和recv(),不再需要每包带地址。connect之后内核会自动过滤掉来自其他地址的报文,并且发向未监听端口时会通过SO_ERROR收到 ICMP 端口不可达错误,排障会方便不少。但要注意,UDP 的connect不是建立 TCP 连接,只是给 socket 设置了默认通信对端。

4.3 第三个坑:recvfrom 缓冲区和报文撕裂

UDP 数据报有边界,但是边界不等于“安全”。如果接收缓冲区小于实际到达的报文长度,内核只拷贝缓冲区大小的那部分数据,剩下的直接被丢弃,recvfrom不会像 TCP 那样把剩余数据留给下次读取。更麻烦的是,调用本身不报错,你根本不知道数据已经被截断了。

所以接收缓冲区的设计要结合最大报文长度来定。我常用的经验值:局域网内文本消息用 2048 字节已足够,二进制指令或包含文件片段用 4096 到 8192,要支持较大业务包就按 65536 字节申请,因为 UDP 单包的理论上限是 65507 字节(IP 头 20 字节加 UDP 头 8 字节减掉)。单包越大,IP 层分片概率越高,分片越多丢一个片整包报废的风险就越大,所以业务层尽量把单包控制在 1400 字节以内。

另外提醒一句:UTF-8 环境下中文一个字符占 3 字节,缓冲区边界卡在某个字符中间时,\0终止后打印会出乱码。如果对完整性敏感,见下一条:自定义协议头里必须带长度字段。

5. 从 demo 到能用的程序:报文设计、心跳保活与有序重组

5.1 自定义报文格式:为什么必须带类型、长度和序号

把ping/pong换成真实业务,第一件事就是设计报文格式。没有格式的话,接收方拿到一段字节流不知道该干什么:这是控制命令还是数据上报?数据有多长?包是不是重复的?这些信息全部要事先约定。

常用做法是“固定头部 + 可变负载”。头部包含魔数、消息类型、序号、长度。魔数用于快速校验是不是本协议的包,避免把其他程序的 UDP 数据误收进来;消息类型让服务端分发处理逻辑;序号用于去重和排障;长度用于确认负载实际大小。

struct MsgHeader { uint16_t magic; // 0x5A5A, 校验是否为业务包 uint16_t type; // 1=PING, 2=PONG, 3=DATA, 4=HEARTBEAT uint32_t seq; // 序号, 发送方自增 uint32_t len; // payload 长度 };

注意所有多字节字段在网络上传输时都要统一字节序。习惯做法是发送前把头部字段全部htons/htonl转成网络字节序,接收后再ntohs/ntohl转回来。如果两台机器字节序一致,这一步不做也能跑,但不可靠。跨平台程序只要出现一次大小端不一致的情况,整个协议就崩了。

5.2 心跳与超时:判断对端“死没死”不能靠感觉

UDP 无连接,服务端不知道客户端是否还在线。客户端拔网线、断电、程序崩溃,服务端完全无感知。如果业务要求“设备掉线后及时通知”,必须自己做心跳机制。

常规设计是客户端每隔 3 秒发一条HEARTBEAT消息,服务端记录每个客户端最后一次收到消息的时间戳。服务端每隔一段时间扫描一次会话表,超过 10 秒没有收到任何消息的客户端就会被标记为离线。这个“3 秒发送、10 秒判定”的比例留足了网络抖动空间,又不会让掉线检测迟钝到让人感觉不出来。

这里有个容易忽略的点:心跳包和业务包要统一走同一个recvfrom通道,服务端侧不能开一个专门的 socket 收心跳,否则会话表的活跃时间更新逻辑会分散。我的做法是所有入包先解析头部,如果是心跳就更新活跃时间并回一个确认,如果是业务包在分发的同时也更新活跃时间。

5.3 UDP 的乱序与丢包:应用层怎么补偿

UDP 不保证顺序。真实网络中,后发的包可能先到,尤其是网络拥塞或经过多个路由节点时。我在某个图像传输模拟项目中就遇到过:一帧图像拆成 20 个分片发送,接收端收到的顺序完全随机,直接拼接出的画面花得不成样子。

补偿思路有两种。最简单的一种是“重传 + 去重”:序号小于当前最大序号的包直接丢弃,序号比期望序号大的包也先丢,由发送方对这包进行重传。这种方案实现简单,适用于少量缺失、顺序偶发颠倒的场景。另一种是“接收端缓存排序”:维护一个小滑动窗口,序号落进窗口的分片暂存,凑齐后按序交给上层。这个方案更复杂,但对实时性和吞吐量的牺牲更小。

如果业务允许丢包,比如传感器瞬时值,那就更简单了:序号只用于统计丢包率,接收端直接按到达顺序处理,不做补偿。设计原则是“该丢的不要硬留,无谓的可靠性设计才是可靠性的敌人”。

6. 多客户端与广播场景:UDP 服务端的真实工作形态

6.1 不需要 accept 的服务端:怎么知道消息来自谁

UDP 服务端没有accept这个动作。一个 socket 就能收所有客户端的消息,因为每条消息自带了源 IP 和源端口。但这也带来一个问题:如果你要给某些客户端单独推送数据,必须自己维护“会话表”。

我的会话表结构一般是一个std::map<std::string, sockaddr_in>,key 用IP:端口拼出的字符串,value 存收到的地址结构。每次recvfrom拿到来源地址后,把 key 和 value 都更新一遍。需要主动推送给某个客户端时,按键查表,拿到sockaddr_in直接sendto。

这个模型天然支持大量客户端,因为服务端不需要为每个客户端维护独立的 socket 文件描述符。代价是应用层需要自己想清楚“客户端离开”的时机:超时未心跳、或收到明确的再见消息时,把对应 key 从表里删掉。

6.2 非阻塞模式与 select:让服务端别卡死在一个客户身上

默认阻塞模式下,recvfrom一进来就死等,如果只有一个客户端,问题不大;多个客户端同时发消息,所有消息也都会排队进入内核缓冲区,服务端逐个处理,看起来依然正常。真正需要非阻塞的场景是:服务端还要做定时任务(比如扫描心跳超时),或者要同时处理多个 socket。

最简单的方案是把 socket 设为非阻塞,然后配合select做超时多路复用。select的本质就是问内核:“这些 socket 里哪一个可读了?”在指定超时时间内,有事件就返回,没有事件就超时继续执行定时逻辑。

fd_set readfds; FD_ZERO(&readfds); FD_SET(server_fd, &readfds); timeval tv = {1, 0}; // 1 秒超时 int ready = select(server_fd + 1, &readfds, nullptr, nullptr, &tv); if (ready > 0 && FD_ISSET(server_fd, &readfds)) { // 此时调用 recvfrom 不会阻塞 int ret = recvfrom(server_fd, recv_buffer, sizeof(recv_buffer) - 1, 0, (sockaddr*)&client_addr, &client_len); }

Linux 上把 socket 设为非阻塞用fcntl(server_fd, F_SETFL, O_NONBLOCK),Windows 上用ioctlsocket(server_fd, FIONBIO, &mode)。配合select之后,就算没有任何客户端发消息,服务端也能每秒扫一次心跳超时,整个程序就活起来了。select 本身能处理的 socket 数有限制,但在普通局域网项目里绰绰有余。

6.3 广播地址与 SO_BROADCAST:给局域网所有设备发消息

UDP 还有一招 TCP 很难受的用法:广播。局域网内一台设备要给所有机器发消息,目标地址填255.255.255.255或者子网定向广播地址192.168.1.255,端口用约定端口。默认情况下系统不允许广播,要先开启SO_BROADCAST选项。

int broadcast = 1; setsockopt(server_fd, SOL_SOCKET, SO_BROADCAST, &broadcast, sizeof(broadcast)); sockaddr_in bcast_addr; bcast_addr.sin_family = AF_INET; bcast_addr.sin_port = htons(9000); inet_pton(AF_INET, "255.255.255.255", &bcast_addr.sin_addr); const char* msg = "discover"; sendto(server_fd, msg, std::strlen(msg), 0, (sockaddr*)&bcast_addr, sizeof(bcast_addr));

广播默认不会跨路由器,这是好事情:网络里的广播风暴会被限制在本地局域网内。常见的“设备发现”场景就是靠广播实现的——新设备上线后向局域网广播一条“我是谁”的消息,管理端就能发现它。注意广播包发送端和接收端的 socket 都必须开启SO_BROADCAST,接收端如果 bind 在0.0.0.0:9000上也能收到广播包。

广播的代价是局域网里所有主机的网卡都要把包收上来做一次应用层过滤,设备密度高时会带来额外的网络处理开销。设备数超过两位数后,建议改用组播(IPPROTO_UDP配合IP_ADD_MEMBERSHIP)来缩小关注范围。组播需要理解加入组的模型,和广播的写法差异不小,但原理是一脉相承的。

我自己在实际工程里常把 UDP 通信拆成三层看:最底层是 socket 收发,只管数据报的进和出;中间层是会话管理和心跳超时,决定对端什么时候算离线;最上层才是业务解析和分发。每一层独立迭代,换业务协议时不需要动底层通信代码。这也是我建议你把文章里的 demo 往工程方向改造时优先保持的边界。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/18 21:54:53

Vue3 + Webpack5 + Element Plus 工程模板搭建与实战指南

简介&#xff1a;这是一份基于 webpack5 搭建 Vue3 开发与生产环境的实战示例工程&#xff0c;面向希望熟悉 webpack5 新特性、掌握前端工程化配置与构建优化的初中级开发者。资源以 JavaScript 为主&#xff0c;配合 Vue 单文件组件与样式文件&#xff0c;完整呈现从开发调试到…

作者头像 李华
网站建设 2026/10/18 21:51:00

Codex实战:5分钟自动生成高效脚本,TaoToken统一Key接入全流程

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/18 21:50:38

CNN并行计算:多卡训练的数据并行与模型并行策略解析

简介&#xff1a;这是一份面向Python开发者与深度学习初学者的CNN并行计算代码包&#xff0c;聚焦卷积神经网络在多种框架下的并行训练实现&#xff0c;适合希望理解数据并行、模型并行与多GPU加速细节的读者。压缩包共25个文件&#xff0c;核心为13个Python脚本&#xff0c;分…

作者头像 李华
网站建设 2026/10/18 21:47:22

Claude Haiku 5.5、GPT-6免费与Manus回归:AI应用开发者的三重应对策略

用户上传的是一则虚构的2026年AI新闻标题&#xff0c;我无法确认其真实性&#xff0c;也不掌握任何关于这些“未来事件”的信息。我能做的是基于现有AI技术发展脉络和行业规律&#xff0c;以虚构新闻盘点的方式&#xff0c;展开一篇关于行业趋势、产品逻辑和开发者应对策略的分…

作者头像 李华
网站建设 2026/10/18 21:46:12

48小时内两份文件:白宫“拉勾承诺”刚签完,FTC调查传票就到了

摘要&#xff1a;2026年10月1日&#xff0c;FTC正式对OpenAI、Anthropic及METR展开行业性调查&#xff0c;计划强制企业提交信息并传唤高管作证。这是美国官方首次针对“失控AI代理人”的执法行动。两天前&#xff0c;特朗普刚与六大巨头签署了没有法律约束力的《前沿责任联合承…

作者头像 李华
网站建设 2026/10/18 21:45:26

皮尔逊III型分布参数估计实战:从矩法初值到MLE精修

简介&#xff1a;本资源是一套面向水文分析、统计建模及工程风险评估初学者与实践者的皮尔逊III型分布&#xff08;Pearson Type III&#xff09;拟合与可视化工具包&#xff0c;聚焦解决自然变量&#xff08;如年径流量、降雨极值&#xff09;的概率建模与频率计算问题。压缩包…

作者头像 李华