enet源码解析(3)数据包 (ENetPacket)

在 ENet 中,我们不直接发送散乱的字节流,而是将数据封装在数据包 (ENetPacket) 中。

1. 为什么需要 ENetPacket?

想象一下,你在玩一个快节奏的射击游戏:

  1. 场景 A :你开了一枪。这个信息必须到达服务器,否则你打中人都没伤害。
  2. 场景 B:你的角色在移动。如果丢失了一两个位置更新包,服务器可以用下一个包修正位置,问题不大。

如果直接使用原始的 UDP 协议,它就像寄平信,既容易丢,也不保证顺序。 ENetPacket 的出现就是为了给这些数据贴上"标签"。它允许你告诉 ENet:"这封信非常重要,必须送到(Reliable)"或者"这封信丢了也没关系(Unreliable)"。


2. 核心概念:可靠性与标志

在创建数据包时,最重要的参数是 flags(标志位)。它决定了数据包的传输行为。

2.1 可靠数据包 (Reliable)

  • 标志ENET_PACKET_FLAG_RELIABLE
  • 行为:就像 TCP 协议。如果数据包丢失,ENet 会自动重传,直到对方确认收到。且保证按发送顺序到达。
  • 适用:聊天信息、购买物品、释放技能。

2.2 不可靠数据包 (Unreliable)

  • 标志0 (无标志)
  • 行为:就像原始 UDP。发出去就不管了,丢了就丢了。但 ENet 依然会帮你进行基本的完整性校验(CRC)。
  • 适用:频繁的位置更新、语音数据。

2.3 无序数据包 (Unsequenced)

  • 标志ENET_PACKET_FLAG_UNSEQUENCED
  • 行为:数据包不仅不可靠,而且不保证顺序。后面的包可能比前面的先到。
  • 适用:极端追求低延迟且对旧数据完全不感兴趣的场景。

3. 实战:创建与发送

让我们通过代码来看看如何封装并发送一个简单的字符串消息。

第一步:准备数据与创建包

c 复制代码
#include <string.h>
// ... 初始化代码略 ...

const char * message = "Hello, ENet!";

// 创建数据包
// 参数1: 数据内容指针
// 参数2: 数据长度 (记得包含字符串结尾的 \0)
// 参数3: 标志位 (这里我们选择可靠传输)
ENetPacket * packet = enet_packet_create(message, 
                                         strlen(message) + 1, 
                                         ENET_PACKET_FLAG_RELIABLE);

解释 : 当你调用 enet_packet_create 时,ENet 会在内部申请一块新的内存,并将你的 message 复制 进去。这意味着在这个函数返回后,你可以随意修改或销毁原来的 message 变量,不会影响数据包。

第二步:发送给对等节点

c 复制代码
// 发送数据包
// 参数1: 目标节点 (peer)
// 参数2: 通道 ID (0) - 稍后在下一章详细讲解
// 参数3: 数据包对象
int result = enet_peer_send(peer, 0, packet);

if (result < 0) {
    fprintf(stderr, "发送失败!\n");
    // 如果发送失败,你需要自己销毁这个包
    enet_packet_destroy(packet); 
}

重要规则

  • 如果 enet_peer_send 成功(返回 0),ENet 接管了数据包的所有权 。你千万不要再调用 destroy,ENet 会在发完后自动销毁它。
  • 如果发送失败,你需要手动销毁它以防内存泄漏。

第三步:接收数据包

在接收端(Event Loop 中),我们解包数据。

c 复制代码
ENetEvent event;
while (enet_host_service(host, &event, 1000) > 0) {
    switch (event.type) {
        case ENET_EVENT_TYPE_RECEIVE:
            printf("收到消息: %s\n", (char*)event.packet->data);
            
            // 重要:接收到的包必须由用户负责销毁!
            enet_packet_destroy(event.packet);
            break;
    }
}

注意 :ENet 不知道你什么时候用完数据,所以在 ENET_EVENT_TYPE_RECEIVE 事件处理完毕后,你必须 调用 enet_packet_destroy,否则内存会一直被占用。


4. 内部原理解析

当你创建一个数据包时,内存里发生了什么?

sequenceDiagram participant User as 用户代码 participant API as enet_packet_create participant Mem as 堆内存 (Heap) User->>API: 请求创建包 ("Hello", 长度 6) API->>Mem: malloc(sizeof(ENetPacket)) Note right of Mem: 分配结构体头信息 API->>Mem: malloc(6 bytes) Note right of Mem: 分配数据载荷空间 API->>Mem: memcpy("Hello" -> buffer) Note right of Mem: 复制用户数据到新空间 API-->>User: 返回 Packet 指针

4.1 深入代码 (packet.c)

让我们看看 enet_packet_create 的简化版实现,理解它是如何管理内存的。

1. 分配头部 首先,分配 ENetPacket 结构体本身的内存。

c 复制代码
// file: packet.c (简化)
ENetPacket * enet_packet_create (const void * data, size_t dataLength, enet_uint32 flags)
{
    // 1. 分配结构体
    ENetPacket * packet = (ENetPacket *) enet_malloc (sizeof (ENetPacket));
    if (packet == NULL) return NULL; // 内存不足

2. 分配与复制数据 这是关键点。除非你指定了 ENET_PACKET_FLAG_NO_ALLOCATE(高级用法),否则 ENet 总是会复制数据。

c 复制代码
    // file: packet.c (简化)
    // 2. 分配数据缓冲区
    packet -> data = (enet_uint8 *) enet_malloc (dataLength);

    // 3. 复制用户数据
    if (data != NULL)
        memcpy (packet -> data, data, dataLength);

3. 设置属性 最后,初始化标志位和引用计数。

c 复制代码
    // file: packet.c (简化)
    packet -> flags = flags;
    packet -> dataLength = dataLength;
    packet -> referenceCount = 0; // 引用计数用于多播等场景
    
    return packet;
}

4.2 校验和 (CRC32)

虽然在简单的创建函数中看不到,但在发送前,ENet 还会计算数据的 CRC32 校验和。这保证了如果网络传输中数据位发生了翻转(比如比特流干扰),ENet 能够识别出来并丢弃坏包,而不会把乱码交给你的程序。


5. 高级技巧:避免内存复制

对于极其追求性能的场景,你可能希望避免 memcpy 的开销。 你可以使用 ENET_PACKET_FLAG_NO_ALLOCATE 标志。

c 复制代码
// 假设 my_data 是你自己管理的一块持久内存
ENetPacket * packet = enet_packet_create(my_data, len, 
    ENET_PACKET_FLAG_RELIABLE | ENET_PACKET_FLAG_NO_ALLOCATE);

警告 :使用此标志时,ENet 只会保存指针 my_data。你必须确保在 ENet 发送完毕并销毁 packet 之前,这块内存一直有效。这通常用于发送静态常量数据或通过自定义内存池管理的数据。


6. 总结

在本章中,我们学习了 ENet 通信的基本单元:

  1. ENetPacket 是数据的载体,包含数据内容和传输标志。
  2. 通过 Flags,我们可以轻松切换 TCP 风格的可靠传输和 UDP 风格的快速传输。
  3. 生命周期管理 至关重要:
    • 创建时:数据被复制。
    • 发送成功后:ENet 负责销毁。
    • 接收后:用户负责销毁。

现在我们已经能发包了,但如果我们需要同时处理聊天信息和高频的位置同步,如何避免聊天信息阻塞了位置更新呢?这就引入了 ENet 的另一个强大特性------通道

相关推荐
nvd111 小时前
如何为 GCE 上的 Envoy 代理启用 HTTPS (使用 Google Cloud Load Balancer)
网络协议·https
拾忆,想起1 小时前
Dubbo跨机房调用实战:从原理到架构的完美解决方案
服务器·网络·网络协议·tcp/ip·架构·dubbo
辻戋1 小时前
HTTP的血泪进化史
网络·网络协议·http
wefg12 小时前
【C++】智能指针
开发语言·c++·算法
MSTcheng.2 小时前
【C++模板进阶】C++ 模板进阶的拦路虎:模板特化和分离编译,该如何逐个突破?
开发语言·c++·模板
Demon--hx2 小时前
[c++]string的三种遍历方式
开发语言·c++·算法
拾忆,想起2 小时前
Dubbo网络延迟全链路排查指南:从微服务“快递”到光速传输
网络·网络协议·微服务·架构·php·dubbo
valan liya3 小时前
C++list
开发语言·数据结构·c++·list
小毅&Nora3 小时前
【后端】【C++】智能指针详解:从裸指针到 RAII 的优雅演进(附 5 个可运行示例)
c++·指针