1个不稳定版本
0.1.0 | 2023年12月5日 |
---|
#10 在 #netcode
175KB
3.5K SLoC
Pickleback
一种在不可靠的数据报流上复用和合并消息的方法,用于游戏网络代码。
预计这将连接到UDP套接字;此crate没有内置网络。
当前状态
开发中。如果你想要讨论,请在bevy的Discord上联系我,我是RJ。
存在一个用于在PiclebackServer和PicklebackClient之间建立会话的非加密握手协议。在握手之后,大部分有用的操作发生在交换Message
数据包时。请参阅测试工具包以获取示例用法。
设计目标
支持大多数更新以不可靠方式发送的多玩家游戏,偶尔需要可靠有序消息,例如游戏内聊天和加入状态同步。
预计双向交换数据包。在实践中,这意味着即使没有要传输的明确消息,每秒也会在每个方向发送至少10个数据包。
特性
- 将多个小消息合并成数据包
- 透明地分割和重组超过一个数据包大小的消息
- 多个虚拟通道用于发送/接收消息
- 可选的可靠通道,具有可配置的重传行为
- 发送消息时,您将获得一个句柄,可用于检查确认(即使在不可靠通道上)
- 内部缓冲池用于消息和数据包,以最小化分配
- 无异步:设计用于集成到现有的游戏循环中。每次tick时调用它。
- 单元测试和与坏链路模拟器的集成/浸泡测试,该模拟器丢弃、复制和重新排序数据包。
- 计算rtt(需要在测试工具包外部验证)
- 计算数据包丢失估计(需要在测试工具包外部验证)
- 在有序通道上强制执行顺序(目前仅支持可靠性)
- 带宽跟踪和预算
- 允许配置通道和通道设置(目前只有1个可靠的,1个不可靠的)
- 在选择要发送的消息时优先考虑通道。低量可靠的优先吗?
待办事项
- 将一些函数代理到ConnectedClient以整理服务器端API
- 基准测试,包括有和没有池化缓冲区的情况。
- 使用bevy和不可靠传输机制的示例。
示例
use pickleback::prelude::*;
let config = PicklebackConfig::default();
let time = 0.0;
let mut server = PicklebackServer::new(time, &config);
let mut client = PicklebackClient::new(time, &config);
// Address must be valid, but isn't used in this example:
client.connect("127.0.0.1:0");
// in lieu of sending over a network, deliver directly:
fn transmit_packets(server: &mut PicklebackServer, client: &mut PicklebackClient) {
// Server --> Client
{
server.update(0.1);
let mut send_to_client = |address, packet: BufHandle| {client.receive(packet.as_slice(), address); };
server.visit_packets_to_send(&mut send_to_client);
}
// Client --> Server
{
client.update(0.1);
let mut send_to_server = |address, packet: BufHandle| {server.receive(packet.as_slice(), address); };
client.visit_packets_to_send(&mut send_to_server);
}
}
// need to have some back-and-forth here to finish handshaking
// this would usually happen each tick of your game event loop.
// client sends ConnectionRequest
transmit_packets(&mut server, &mut client);
// server responds with ConnectionChallenge
transmit_packets(&mut server, &mut client);
// client responds with ConnectionChallengeResponse
transmit_packets(&mut server, &mut client);
// server sends a keep-alive, denoting fully connected
transmit_packets(&mut server, &mut client);
assert_eq!(client.state(), &ClientState::Connected);
let channel: u8 = 0;
// this can return an error if throttled due to backpressure and unable to send.
// we unwrap here, since it will not fail at this point.
let msg_id = {
let mut connected_client = server.connected_clients_mut().next().unwrap();
connected_client.pickleback.send_message(channel, b"hello").unwrap()
};
// server sends a Message packet containing a single message.
// (when sending multiple messages, they are coalesced into as few packets as possible)
transmit_packets(&mut server, &mut client);
// client will have received a message:
let received = client.drain_received_messages(channel).collect::<Vec<_>>();
assert_eq!(received.len(), 1);
// normally you'd use the .payload() to get a Reader, rather than .payload_to_owned()
// which reads it into a Vec. But a vec here makes it easier to test.
let recv_payload = received[0].payload_to_owned();
assert_eq!(b"hello".to_vec(), recv_payload);
// if the client doesn't have a message payload to send, it will still send
// a packet here just to transmit acks
transmit_packets(&mut server, &mut client);
// now the client has sent packets to the server, the server will have received an ack
let mut connected_client = server.connected_clients_mut().next().unwrap();
// TODO: don't expose pickleback via connected_client, proxy a couple of fns and document..
assert_eq!(vec![msg_id], connected_client.pickleback.drain_message_acks(channel).collect::<Vec<_>>());
协议概述
数据包由数据包类型、序列号、确认头和有效载荷组成。
ack头部确认已收到远程端点发送的最后一个N个数据包。
消息数据包负载由一个或多个消息组成。消息可以是任何大小,大消息会被分割成1024字节的片段消息,并为您重新组装。
调用send_message
后,您会得到一个MessageId
。一旦您的消息所在的数据包被确认,您将收到对您的MessageId
的确认。不可靠的通道也会收到确认(除非数据包丢失)。
发送大于1024字节的消息时,您会在所有片段都成功交付并且消息被成功重新组装后收到确认。
可靠的通道会在可配置的时间内重传尚未确认的消息。
消息会被重传,数据包则不会。也就是说,尚未确认的可靠消息将包含在新的未来数据包中,直到它们被确认。
消息大小
任意限制为1024个1024B的片段,因此最大消息大小为1MB。
记住,随着片段数量的增加,数据包丢失的影响会被放大。
在1%的数据包丢失率下,10个片段的丢失概率为1 - (0.99^10) = 9.6%。
来源
- Gaffer文章(构建网络协议、数据包确认、序列缓冲区等)
- netcode.io rust代码(Gaffer概念的实现)
依赖项
~4–5.5MB
~100K SLoC