左瞄右喵手机版
58.11M · 2025-11-22
在上一章 主机管理 (ENetHost)中,我们建立了自己的“邮局”(Host)。但是,如果只有邮局而没有通信的对象,那这就是一个孤独的邮局。
本章我们将介绍 对等节点 (ENetPeer)。如果不把 ENetHost 比作邮局,而是比作一部手机,那么 ENetPeer 就是你正在进行的通话。
ENetPeer 代表了与特定远程计算机的一个活动连接。
在一个多人游戏中:
ENetPeer,它代表了你连接的游戏服务器。ENetPeer,每一个对象都代表一个在线的玩家。它的核心职责是充当连接的管家:
要获得一个 ENetPeer 对象,最常见的方法是主动发起连接(作为客户端)。
假设你已经创建了一个客户端 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 时发出。
连接不是瞬间完成的。我们需要在事件循环中等待服务器的“同意”。
// 等待 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,说明底层的“三次握手”已经完成了。
天下没有不散的筵席,当玩家退出游戏时,我们需要优雅地断开连接。
这是最推荐的方式。它会发送一个“再见”消息,并等待对方确认“好的,再见”。
// 发送断开请求,附带一个数字 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;
}
}
如果你不想等待,或者对方已经死机了,可以使用强制断开。这就像直接拔网线。
// 立即重置 Peer,不通知对方
enet_peer_reset(peer);
ENetPeer 是如何工作的?为什么我们在第一章说它是“预分配”的?
当我们在服务器端创建一个 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
让我们看看 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);
}
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;
}
// ...
}
Q: ENetPeer 需要我手动释放内存吗?
A: 不需要。如果你是客户端,销毁 ENetHost 时会自动清理。如果你是服务器,ENetPeer 只是大数组里的一格,调用 enet_peer_reset 或断开连接后,它只是变回“空闲状态”,内存依然归 ENetHost 管理。
Q: 我可以把 packet 发送给 NULL peer 吗?
A: 不行,程序会崩溃。在发送前永远要确保 peer 不为空且状态是 ENET_PEER_STATE_CONNECTED。
在这一章,我们学习了:
enet_host_connect 发起连接。enet_peer_disconnect 断开连接。现在连接已经建立,可以互相通话了。但是,我们具体要发送什么呢?字符串?结构体?二进制流?
下一章,我们将学习如何打包你的数据:数据包封装 (ENetPacket)