在上一章 主机管理 (ENetHost)中,我们建立了自己的“邮局”(Host)。但是,如果只有邮局而没有通信的对象,那这就是一个孤独的邮局。

本章我们将介绍 对等节点 (ENetPeer)。如果不把 ENetHost 比作邮局,而是比作一部手机,那么 ENetPeer 就是你正在进行的通话


1. 什么是 ENetPeer?

ENetPeer 代表了与特定远程计算机的一个活动连接。

在一个多人游戏中:

  • 对于客户端:你通常只有 1 个 ENetPeer,它代表了你连接的游戏服务器。
  • 对于服务器:你会有很多个 ENetPeer,每一个对象都代表一个在线的玩家。

它的核心职责是充当连接的管家

  1. 状态维护:记录我们是“正在连接”、“已连接”还是“正在断开”。
  2. 健康监控:计算往返时间 (RTT),也就是数据一来一回需要多久。
  3. 流量控制:如果网络太卡,它会自动降低发送速度,防止堵塞。

2. 实战:建立连接

要获得一个 ENetPeer 对象,最常见的方法是主动发起连接(作为客户端)。

2.1 发起连接

假设你已经创建了一个客户端 Host(如第一章所述),现在我们要连接到服务器。

ENetAddress address;
ENetEvent event;
ENetPeer *peer;

// 设置服务器地址 (本地回环地址)
enet_address_set_host(&address, "127.0.0.1");
address.port = 1234;

// 发起连接:2个通道,0表示不限速
peer = enet_host_connect(client, &address, 2, 0);

if (peer == NULL) {
    fprintf(stderr, "连接请求创建失败!n");
    return 0;
}

代码解读: 调用 enet_host_connect 并不会立即联网,它只是由 ENet 创建了一个“连接意向”。真正的握手数据包会在你调用 enet_host_service 时发出。

2.2 确认连接成功

连接不是瞬间完成的。我们需要在事件循环中等待服务器的“同意”。

// 等待 5 秒钟看是否能连接成功
if (enet_host_service(client, &event, 5000) > 0 &&
    event.type == ENET_EVENT_TYPE_CONNECT) {
    
    printf("连接成功!服务器响应了。n");
    // 此时,peer 对象已经完全准备好可以使用了
} else {
    // 5秒内没有收到回复,或者连接失败
    enet_peer_reset(peer);
    printf("连接失败。n");
}

注意:如果你在这里收到了 ENetPeer,说明底层的“三次握手”已经完成了。


3. 实战:断开连接

天下没有不散的筵席,当玩家退出游戏时,我们需要优雅地断开连接。

3.1 温柔的断开 (Disconnect)

这是最推荐的方式。它会发送一个“再见”消息,并等待对方确认“好的,再见”。

// 发送断开请求,附带一个数字 0 作为数据
enet_peer_disconnect(peer, 0);

// 需要继续运行服务循环,让断开消息发出去
while (enet_host_service(client, &event, 3000) > 0) {
    switch (event.type) {
    case ENET_EVENT_TYPE_RECEIVE:
        // 还有遗留数据就先销毁
        enet_packet_destroy(event.packet);
        break;
    case ENET_EVENT_TYPE_DISCONNECT:
        printf("断开连接成功。n");
        return;
    }
}

3.2 强制断开 (Disconnect Now)

如果你不想等待,或者对方已经死机了,可以使用强制断开。这就像直接拔网线。

// 立即重置 Peer,不通知对方
enet_peer_reset(peer);

4. 内部原理解析

ENetPeer 是如何工作的?为什么我们在第一章说它是“预分配”的?

4.1 节点的生命周期

当我们在服务器端创建一个 ENetHost 时,我们指定了 peerCount(比如 32)。ENet 会在内存中创建一个包含 32 个 ENetPeer 结构体的数组。

当一个新玩家连接时,ENet 不会malloc(分配)新内存,而是简单地在这个数组中找一个空闲的 Peer,把它标记为“正在使用”。

sequenceDiagram
    participant Client as 客户端
    participant ServerHost as 服务器 Host
    participant PeerPool as Peer 数组(内存)
    participant PeerObj as 激活的 Peer

    Client->>ServerHost: 发送连接请求 (Connect Packet)
    ServerHost->>PeerPool: 寻找空闲的 Peer 位置
    PeerPool-->>ServerHost: 返回索引 5 是空的
    ServerHost->>PeerObj: 初始化索引 5 (设置 IP, 端口, 状态)
    ServerHost->>Client: 发送确认 (Verify Connect)
    Note over PeerObj: 状态变为 CONNECTED

4.2 深入代码 (peer.c)

让我们看看 enet_peer_reset 函数,这个函数不仅用于断开连接,也用于初始化一个新连接的槽位。

1. 清理旧状态 无论这个 Peer 之前发生了什么,重置意味着清空所有统计数据。

// file: peer.c (简化版)
void enet_peer_reset (ENetPeer * peer) {
    // 通知 Host 减少连接计数
    enet_peer_on_disconnect (peer);

    // 重置 IP 和端口信息
    peer -> outgoingPeerID = ENET_PROTOCOL_MAXIMUM_PEER_ID;
    peer -> connectID = 0;
    
    // 状态设为断开
    peer -> state = ENET_PEER_STATE_DISCONNECTED;

2. 重置网络参数 (RTT) ENet 非常智能,它会为每个连接动态计算 RTT(网络延迟)。重置时,这些值回归默认。

    // file: peer.c
    // 默认 RTT 为 500ms
    peer -> roundTripTime = ENET_PEER_DEFAULT_ROUND_TRIP_TIME; 
    peer -> roundTripTimeVariance = 0;
    
    // 重置丢包统计
    peer -> packetLoss = 0;

3. 清空队列 最重要的一步:如果还有没发出去的包,或者没处理的包,全部丢弃,防止内存泄漏。

    // file: peer.c
    // 清理所有待发送和待接收队列
    enet_peer_reset_queues (peer);
}

4.3 智能流控 (Throttling)

ENetPeer 最强大的功能之一是自适应流控。就像水坝一样,如果下游(网络)堵塞了,上游就要关小闸门。

enet_peer_throttle 函数中:

// file: peer.c (逻辑示意)
int enet_peer_throttle (ENetPeer * peer, enet_uint32 rtt) {
    // 如果当前 RTT (延迟) 比平均值还低,说明网络很顺畅
    if (rtt <= peer -> lastRoundTripTime) {
        // 增加发送概率 (packetThrottle)
        peer -> packetThrottle += peer -> packetThrottleAcceleration;
    }
    // 如果延迟很高,说明网络堵了
    else if (rtt > peer -> lastRoundTripTime + 2 * Variance) {
        // 降低发送概率,主动丢弃一部分不可靠包
        peer -> packetThrottle -= peer -> packetThrottleDeceleration;
    }
    // ...
}
  • 解释:这保证了当网络变差时,重要的“可靠包”还能挤过去,而不太重要的“不可靠包”会被主动丢弃,防止连接彻底断开。

5. 常见问题

Q: ENetPeer 需要我手动释放内存吗? A: 不需要。如果你是客户端,销毁 ENetHost 时会自动清理。如果你是服务器,ENetPeer 只是大数组里的一格,调用 enet_peer_reset 或断开连接后,它只是变回“空闲状态”,内存依然归 ENetHost 管理。

Q: 我可以把 packet 发送给 NULL peer 吗? A: 不行,程序会崩溃。在发送前永远要确保 peer 不为空且状态是 ENET_PEER_STATE_CONNECTED


6. 总结

在这一章,我们学习了:

  • ENetPeer 是网络连接的“替身”,管理着与特定计算机的通信状态。
  • 如何使用 enet_host_connect 发起连接。
  • 如何使用 enet_peer_disconnect 断开连接。
  • 内部原理:Peer 是从 Host 的预分配数组中“借”出来的,并自带了智能的流控机制。

现在连接已经建立,可以互相通话了。但是,我们具体要发送什么呢?字符串?结构体?二进制流?

下一章,我们将学习如何打包你的数据:数据包封装 (ENetPacket)

本站提供的所有下载资源均来自互联网,仅提供学习交流使用,版权归原作者所有。如需商业使用,请联系原作者获得授权。 如您发现有涉嫌侵权的内容,请联系我们 邮箱:[email protected]