欢迎来到 ENet 教程的第一章!在这里,我们将从最基础的概念开始:主机 (Host)。
1. 什么是 ENetHost?
想象一下,如果你想寄信,首先需要什么?你需要一个邮局。
在 ENet 的世界里,ENetHost 就扮演着“邮局”的角色。它是所有网络通信的核心枢纽。无论你的程序是服务器(负责接收很多人的连接)还是客户端(负责连接别人),你都必须首先创建一个 ENetHost。
它的主要工作包括:
- 管理大门:它在底层打开一个网络套接字(Socket),这是数据进出的物理通道。
- 维护名册:它保存着所有与你连接的人(称为“对等节点”或 Peers)的列表。
- 控制流量:它像交通警察一样,监控带宽使用情况,防止网络拥堵。
核心应用场景:
你要开发一个多人游戏。第一步就是在玩家电脑上创建一个 ENetHost 作为客户端,并在你的云端服务器上创建一个 ENetHost 作为服务端。只有建立了这两个“邮局”,数据包才能开始在互联网上飞翔。
2. 核心概念拆解
在深入代码之前,我们先了解一下 ENetHost 内部管理的几个关键概念:
2.1 网络地址 (ENetAddress)
就像邮局需要一个具体的地址(街道和门牌号)一样,主机也需要一个 IP 地址和端口号。
- 服务器通常需要绑定一个固定的地址和端口,以便别人能找到它。
- 客户端通常不需要指定地址,系统会自动分配一个临时的。
2.2 对等节点列表 (Peers Array)
ENetHost 不仅是一个单一的对象,它还预先分配了一组空位。每一个空位都可以用来存放一个连接进来的用户。这就像餐厅里的桌子,你创建主机时决定了这间餐厅有多少张桌子(最大连接数)。
- 关于节点的详细内容,我们将在 对等节点 (ENetPeer) 中详细介绍。
2.3 通道 (Channels)
ENet 允许你在同一个连接中通过不同的“车道”发送数据。比如,聊天信息走 0 号通道,位置更新走 1 号通道。ENetHost 决定了最多支持多少条这样的车道。
- 更多细节请参考 多通道机制 (Channels)。
3. 如何使用 ENetHost
让我们通过代码来看看如何创建和销毁一个主机。
第一步:初始化 ENet
在使用任何 ENet 功能之前,必须先初始化库。
#include <enet/enet.h>
#include <stdio.h>
int main() {
// 初始化 ENet 库
if (enet_initialize() != 0) {
fprintf(stderr, "初始化 ENet 失败!\n");
return 1;
}
// ... 程序代码 ...
enet_deinitialize(); // 程序结束时清理
}
第二步:创建一个服务器 Host
服务器需要监听特定的端口,所以我们要先设置地址。
ENetAddress address;
ENetHost * server;
// 绑定到本机所有 IP 地址
address.host = ENET_HOST_ANY;
// 监听 1234 端口
address.port = 1234;
接下来,创建 Host 对象:
// 创建主机
// 参数含义:
// 1. 地址指针
// 2. 32: 允许最多 32 个客户端连接
// 3. 2: 允许 2 个通道
// 4. 0: 入站带宽无限制
// 5. 0: 出站带宽无限制
server = enet_host_create(&address, 32, 2, 0, 0);
if (server == NULL) {
fprintf(stderr, "创建服务器 Host 失败!\n");
}
第三步:创建一个客户端 Host
客户端通常不需要别人主动连接它,所以第一个参数传 NULL。
ENetHost * client;
// 客户端只连接 1 个服务器,所以 peerCount 设为 1
client = enet_host_create(NULL, 1, 2, 0, 0);
if (client == NULL) {
fprintf(stderr, "创建客户端 Host 失败!\n");
}
第四步:处理网络事件 (服务循环)
ENetHost 需要你不断地驱动它,它才会工作。这是通过 enet_host_service 函数完成的。它就像是一个心跳,每一次跳动都会检查收件箱、发送发件箱里的信件。
ENetEvent event;
// 等待最多 1000 毫秒来处理网络事件
while (enet_host_service(server, &event, 1000) > 0) {
switch (event.type) {
case ENET_EVENT_TYPE_CONNECT:
printf("有新朋友连接了!\n");
break;
// ... 处理其他事件
}
}
- 关于事件处理的细节,请参考 事件驱动服务 (Event Service)。
第五步:销毁 Host
当你不再需要通信时,记得拆除这间“邮局”以释放内存。
if (server != NULL) {
enet_host_destroy(server);
}
4. 内部原理解析
当你调用 enet_host_create 时,底层到底发生了什么?我们来看看这个过程的示意图。
sequenceDiagram
participant User as 你的代码
participant Host as ENetHost
participant Memory as 内存管理
participant OS as 操作系统 (Socket)
User->>Host: enet_host_create(地址, 32人, ...)
Host->>Memory: 分配 Host 结构体内存
Host->>Memory: 分配 32 个 Peer 的数组内存
Note right of Memory: 这里预留了所有连接所需的空间
Host->>OS: 创建 UDP Socket
Host->>OS: 绑定地址和端口 (如果是服务器)
Host->>OS: 设置 Socket 选项 (非阻塞, 广播等)
Host-->>User: 返回 Host 指针
4.1 内存分配策略
ENet 的一个设计特点是预分配。当你创建一个允许 32 人连接的 Host 时,它会立即申请这 32 个 ENetPeer 结构体的内存。
这意味着在程序运行过程中,新用户连接时不需要临时申请内存,这保证了高性能和低延迟,非常适合游戏开发。
4.2 深入代码 (host.c)
让我们看看 host.c 中的 enet_host_create 函数是如何实现的(简化版):
1. 基础内存分配 首先,它为 Host 对象本身分配内存。
// file: host.c (简化)
ENetHost * enet_host_create (/* 参数 */) {
ENetHost * host;
// ...
host = (ENetHost *) enet_malloc (sizeof (ENetHost));
memset (host, 0, sizeof (ENetHost));
// ...
2. 预分配对等节点 (Peers)
这里就是我们要强调的预分配机制。peerCount 决定了数组的大小。
// file: host.c (简化)
host -> peers = (ENetPeer *) enet_malloc (peerCount * sizeof (ENetPeer));
memset (host -> peers, 0, peerCount * sizeof (ENetPeer));
- 如果你想了解
ENetPeer结构体里有什么,请阅读 对等节点 (ENetPeer)。
3. 创建底层套接字 ENet 会根据你的平台(Windows/Linux 等)调用底层的 Socket API。
// file: host.c (简化)
// 创建一个 UDP 套接字 (Datagram)
host -> socket = enet_socket_create (ENET_SOCKET_TYPE_DATAGRAM);
// 如果提供了地址 (服务器模式),则绑定它
if (address != NULL)
enet_socket_bind (host -> socket, address);
- 这部分的跨平台细节封装在 跨平台套接字抽象 (Platform Socket Abstraction) 中。
4. 设置 Socket 选项 为了保证实时性,ENet 将 Socket 设置为非阻塞 (Non-blocking) 模式。这意味着如果现在没有数据可读,函数会立即返回,而不会卡住你的程序。
// file: host.c (简化)
enet_socket_set_option (host -> socket, ENET_SOCKOPT_NONBLOCK, 1);
enet_socket_set_option (host -> socket, ENET_SOCKOPT_BROADCAST, 1);
5. 初始化所有 Peer 最后,它会遍历刚刚分配的那个 Peer 数组,把它们都重置为初始状态,准备好迎接连接。
// file: host.c (简化)
for (currentPeer = host -> peers;
currentPeer < & host -> peers [host -> peerCount];
++ currentPeer)
{
currentPeer -> host = host;
enet_peer_reset (currentPeer);
}
return host; // 完成!
}
5. 总结
在这一章,我们学习了 ENet 的基石——主机 (ENetHost)。
- 它是什么:网络通信的“邮局”和总指挥。
- 关键作用:持有 Socket,管理 Peer 列表,控制带宽。
- 如何使用:通过
enet_host_create创建,通过enet_host_service驱动,最后通过enet_host_destroy销毁。 - 内部机制:一次性预分配所有 Peer 的内存,确保运行时的高效。
现在你的“邮局”已经建好了,但是它目前还是空的。在下一章,我们将学习如何管理那些连接到你的用户。