enet源码解析(1)主机管理 (ENetHost)

60 阅读6分钟

欢迎来到 ENet 教程的第一章!在这里,我们将从最基础的概念开始:主机 (Host)

1. 什么是 ENetHost?

想象一下,如果你想寄信,首先需要什么?你需要一个邮局

在 ENet 的世界里,ENetHost 就扮演着“邮局”的角色。它是所有网络通信的核心枢纽。无论你的程序是服务器(负责接收很多人的连接)还是客户端(负责连接别人),你都必须首先创建一个 ENetHost

它的主要工作包括:

  1. 管理大门:它在底层打开一个网络套接字(Socket),这是数据进出的物理通道。
  2. 维护名册:它保存着所有与你连接的人(称为“对等节点”或 Peers)的列表。
  3. 控制流量:它像交通警察一样,监控带宽使用情况,防止网络拥堵。

核心应用场景: 你要开发一个多人游戏。第一步就是在玩家电脑上创建一个 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 的内存,确保运行时的高效。

现在你的“邮局”已经建好了,但是它目前还是空的。在下一章,我们将学习如何管理那些连接到你的用户。