在上一章 多通道机制 (Channels) 中,我们学会了如何利用多条"车道"来避免数据堵塞。我们把数据包封装好,放入了发送队列。
但是,仅仅把信扔进邮筒是不够的,还需要邮递员真正把信取走并送出去。在 ENet 中,这个"邮递员"就是事件驱动服务。
1. 为什么需要事件服务?
核心问题:你的程序(游戏或应用)和网络是两个独立的世界。
- 你的程序 :按部就班地运行逻辑(如:
while(game_running))。 - 网络世界:数据随时可能从网线那头传过来,或者你的网卡准备好发送数据了。
如果没有一个机制来协调,你的程序怎么知道:"嘿,有人发消息给我了"?或者"刚才那个重要的包对方收到了吗"?
解决方案 :enet_host_service。 它就像心脏一样,是 ENet 引擎的动力源。你必须不断地"泵"它,它才会:
- 发送你在队列里排队的数据包。
- 接收网卡上收到的原始数据,并解析成你能看懂的消息。
- 通知你发生了什么(有人连接、断开、或发消息)。
2. 核心概念:ENetEvent
当"邮递员"检查完邮箱后,如果发现有新情况,它会给你一张"报告单"。这张单子在代码中就是 ENetEvent 结构体。
它最关键的字段是 type(事件类型):
- ENET_EVENT_TYPE_NONE: 平安无事,没有任何新消息。
- ENET_EVENT_TYPE_CONNECT: 有个新朋友(Peer)连接进来了。
- ENET_EVENT_TYPE_RECEIVE: 收到了一封信(Packet)。
- ENET_EVENT_TYPE_DISCONNECT: 有个朋友断开了连接。
3. 实战:编写服务循环
要在程序中使用 ENet,你通常需要在一个循环中调用 enet_host_service。
3.1 准备工作
首先,我们需要定义一个变量来存放那张"报告单"。
c
ENetEvent event;
3.2 调用服务函数
这是 ENet 最重要的函数调用。
c
// 参数1: 你的主机 (Host)
// 参数2: 接收事件的变量指针
// 参数3: 等待时间 (毫秒)
int status = enet_host_service(host, &event, 0);
关于等待时间 (timeout):
- 设置为 0 :非阻塞模式 。函数会立即检查一下有没有事,然后马上返回。非常适合游戏开发(因为游戏每一帧都要渲染画面,不能卡在这里等网络)。
- 设置为 > 0 (如 1000):阻塞模式。如果没有事件,函数会停在这里等最多 1000 毫秒。适合服务器程序或简单的控制台应用。
3.3 处理事件
通常我们会用 while 循环来处理所有积压的事件。
c
// 只要 service 返回大于 0,说明还有事件需要处理
while (enet_host_service(host, &event, 0) > 0) {
switch (event.type) {
case ENET_EVENT_TYPE_CONNECT:
printf("新连接来自: %x\n", event.peer->address.host);
break;
case ENET_EVENT_TYPE_RECEIVE:
printf("收到数据: %s\n", event.packet->data);
// 别忘了销毁包!(参考 Chapter 3)
enet_packet_destroy(event.packet);
break;
case ENET_EVENT_TYPE_DISCONNECT:
printf("连接断开。\n");
break;
}
}
解释 :这段代码就像你去查看信箱。只要信箱里还有信(> 0),你就一直拿,每拿一封信就根据信的类型做处理,直到信箱空了为止。
4. 内部原理解析
当你调用 enet_host_service 时,底层到底发生了什么?为什么说它是一个"驱动器"?
4.1 流程图解
4.2 深入代码 (host.c)
让我们看看 host.c 中 enet_host_service 的简化逻辑,它主要做了三件大事。
1. 派发积压事件 (Dispatch) 有时一个数据包里包含多条消息,ENet 会把它们先存起来。函数一开始会先检查这个队列。
c
// file: host.c (简化)
int enet_host_service (ENetHost * host, ENetEvent * event, enet_uint32 timeout) {
// 如果有之前解析好但还没交给用户的事件,直接返回它
if (event != NULL) {
// 这是一个内部函数,检查 dispatchQueue
if (enet_protocol_dispatch_incoming_commands (host, event))
return 1;
}
2. 执行网络传输 这是真正干活的地方。它会把你的发送队列清空,并通过 Socket 发送出去。
c
// file: host.c (简化)
host -> serviceTime = enet_time_get ();
// 发送所有排队的命令 (Ack, Connect, Data...)
enet_protocol_send_outgoing_commands (host, NULL, 0);
- 这部分涉及到底层协议的打包,我们将在 协议处理逻辑 (Protocol Processing)中详细探讨。
3. 等待与接收 最后,它会等待网络响应(根据你设置的 timeout),并读取数据。
c
// file: host.c (简化)
// 等待 Socket 有动静
if (enet_socket_wait (host -> socket, & waitCondition, timeout) == 0)
return 0; // 超时或无事发生
// 从 Socket 读取数据
host -> receivedDataLength = enet_socket_receive (host -> socket,
& host -> receivedAddress,
host -> receivedData,
host -> receivedDataLength);
// ... 解析数据 ...
}
5. 常见误区
误区 1:只在需要发送时才调用 Service
有些新手只在调用了 enet_peer_send 之后才调用一次 enet_host_service。 后果 :你会收不到别人的消息,甚至连你的心跳包(Ping)都发不出去,导致连接超时断开。 正确做法:无论你有没有数据要发,都要在主循环中高频调用它。
误区 2:忘记销毁 Packet
如第 3 章所述,在 ENET_EVENT_TYPE_RECEIVE 事件中,ENet 把数据包的所有权交给了你。 后果 :内存泄漏。 正确做法 :处理完数据后,务必调用 enet_packet_destroy(event.packet)。
6. 总结
在本章中,我们学习了 ENet 的"心脏"------事件驱动服务。
- 它是谁 :
enet_host_service。 - 它做什么 :发送数据、接收数据、并将底层的网络活动转化为高层的
ENetEvent。 - 如何使用:在一个循环中不断调用它,处理它返回的 Connect、Receive 或 Disconnect 事件。
现在的你,已经能够建立主机、连接节点、封装数据包、利用通道,并让整个系统运转起来了!
但是,你是否好奇:当你把一个 Hello 字符串交给 ENet 时,它在网线上到底变成了什么样子?ENet 是如何保证"可靠传输"的?它怎么知道数据包丢了并需要重传?
下一章,我们将揭开协议底层的神秘面纱。