8个重大版本
0.9.0 | 2024年7月5日 |
---|---|
0.7.0 | 2024年2月17日 |
0.6.0 | 2023年11月4日 |
0.5.0 | 2023年7月11日 |
0.2.0 | 2022年11月18日 |
#224 in 游戏开发
194 每月下载量
用于 2 crates
180KB
3K SLoC
QUIC作为游戏网络协议
QUIC作为一个游戏网络协议对我来说非常吸引人,因为大部分的艰难工作都是由协议规范和实现(这里指Quinn)来完成的。在像UDP可靠性包装、加密与认证机制、拥塞控制等容易出错的主题上,无需再次重新发明轮子。
大多数大网络库提出的功能都通过QUIC默认支持。以下是在GameNetworkingSockets中展示的功能列表
- 面向连接的API(类似于TCP): -> 默认
- ...但是消息导向的(类似于UDP),而不是流导向的: -> 默认 (*)
- 支持可靠和不可靠的消息类型: -> 默认
- 消息可以大于底层MTU。协议对可靠消息执行分片、重组和重传: -> 默认(对于不可靠的数据包,协议不会执行分片和重组)
- 可靠性层 [...]。它基于DCCP(RFC 4340,第11.4节)的“ack向量”模型和Google QUIC,并由Glenn Fiedler在游戏背景下讨论 [...]: -> 默认。
- 加密 [...]。共享密钥派生和每个数据包IV的细节基于Google的QUIC协议的设计: -> 默认
- 用于模拟数据包延迟/丢失的工具,以及详细的统计数据测量: -> 默认不启用
- 首包阻塞控制和同一连接上多个消息流的带宽共享。: -> 默认启用
- IPv6 支持: -> 默认启用
- 点对点网络(通过 ICE + 信号 + 对称连接模式进行 NAT 穿越): -> 默认不启用
- 跨平台: -> 默认启用,当可用 UDP 时
-> 默认情况下大约有 11 个功能中的 9 个。
(*) 某种程度上,当共享 QUIC 流时,可靠消息需要被封装。
特性
Quinnet 具有基本功能,我主要创建它是为了满足我自己的游戏项目需求。
它目前具有以下功能
- 一个客户端插件,它可以
- 连接/断开与一个或多个服务器的连接
- 发送和接收不可靠和有序/无序可靠消息
- 一个服务器插件,它可以
- 接受客户端连接并断开它们
- 发送和接收不可靠和有序/无序可靠消息
- 客户端和服务器都接受用户定义的自定义协议结构体/枚举作为消息格式。
- 通信被加密,客户端可以 验证服务器。
尽管 Quinn 和 Quinnet 的一部分是异步的,但 Quinnet 提供给客户端和服务器的 API 是同步的。这使得表面 API 容易使用,并适应了 Bevy 的使用。实现内部使用 tokio 通道 与网络异步任务进行通信。
路线图
以下是从上到下查看的下一个可能要工作的功能/任务(不分先后顺序)
- 功能:实现客户端和服务器上大于路径 MTU 的
unreliable
消息 - 性能:在刷新有序可靠通道之前发送多个消息
- 整洁:重新设计异步后端中的错误处理
- 整洁:重新设计集合上的错误处理,以避免在第一次错误时失败
快速入门
客户端
- 将
QuinnetClientPlugin
添加到 bevy 应用程序中
App::new()
// ...
.add_plugins(QuinnetClientPlugin::default())
// ...
.run();
- 然后您可以使用
Client
资源来连接、发送和接收消息
fn start_connection(client: ResMut<QuinnetClient>) {
client
.open_connection(
ClientEndpointConfiguration::from_ips(
IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)),
6000,
IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)),
0,
),
CertificateVerificationMode::SkipVerification,
ChannelsConfiguration::default(),
);
// When trully connected, you will receive a ConnectionEvent
- 要处理服务器消息,您可以使用以下 bevy 系统等。函数
receive_message
是通用的,这里ServerMessage
是用户提供的枚举,它派生自Serialize
和Deserialize
。
fn handle_server_messages(
mut client: ResMut<QuinnetClient>,
/*...*/
) {
while let Ok(Some(message)) = client.connection_mut().receive_message::<ServerMessage>() {
match message {
// Match on your own message types ...
ServerMessage::ClientConnected { client_id, username} => {/*...*/}
ServerMessage::ClientDisconnected { client_id } => {/*...*/}
ServerMessage::ChatMessage { client_id, message } => {/*...*/}
}
}
}
服务器
- 将
QuinnetServerPlugin
添加到 bevy 应用程序中
App::new()
/*...*/
.add_plugins(QuinnetServerPlugin::default())
/*...*/
.run();
- 然后您可以使用
Server
资源来启动侦听服务器
fn start_listening(mut server: ResMut<QuinnetServer>) {
server
.start_endpoint(
ServerEndpointConfiguration::from_ip(IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)), 6000),
CertificateRetrievalMode::GenerateSelfSigned,
ChannelsConfiguration::default(),
)
.unwrap();
}
- 要处理客户端消息并发送消息,您可以使用以下 bevy 系统等。函数
receive_message
是通用的,这里ClientMessage
是用户提供的枚举,它派生自Serialize
和Deserialize
。
fn handle_client_messages(
mut server: ResMut<QuinnetServer>,
/*...*/
) {
let mut endpoint = server.endpoint_mut();
for client_id in endpoint.clients() {
while let Some(message) = endpoint.try_receive_message_from::<ClientMessage>(client_id) {
match message {
// Match on your own message types ...
ClientMessage::Join { username} => {
// Send a messsage to 1 client
endpoint.send_message(client_id, ServerMessage::InitClient {/*...*/}).unwrap();
/*...*/
}
ClientMessage::Disconnect { } => {
// Disconnect a client
endpoint.disconnect_client(client_id);
/*...*/
}
ClientMessage::ChatMessage { message } => {
// Send a message to a group of clients
endpoint.send_group_message(
client_group, // Iterator of ClientId
ServerMessage::ChatMessage {/*...*/}
)
.unwrap();
/*...*/
}
}
}
}
}
您还可以使用 endpoint.broadcast_message
,它将向所有已连接客户端发送消息。“已连接”在这里是指连接到服务器插件,这发生在您的应用程序握手/验证之前,如果您有的话。如果您想控制接收者,请使用 send_group_message
。
通道
当您发送消息时,当前有 3 种类型的通道可用
OrderedReliable
:确保发送的消息被传递,并且接收端以发送顺序处理这些消息(例如用法:聊天消息)UnorderedReliable
:确保发送的消息被传递,但可以以任何顺序传递(例如用法:动画触发器)Unreliable
:不保证接收端传递或处理消息的顺序(例如用法:每个时间步发送实体位置)
当您打开连接/端点时,根据给定的 ChannelsConfiguration
直接创建一些通道。
// Default channels configuration contains only 1 channel of the OrderedReliable type,
// akin to a TCP connection.
let channels_config = ChannelsConfiguration::default();
// Creates 2 OrderedReliable channels, and 1 unreliable channel,
// with channel ids being respectively 0, 1 and 2.
let channels_config = ChannelsConfiguration::from_types(vec![
ChannelType::OrderedReliable,
ChannelType::OrderedReliable,
ChannelType::Unreliable]);
每个通道都通过其唯一的 ChannelId
来识别。其中,有一个 default
通道,当您没有指定通道时将使用该通道。启动时,第一个打开的通道将成为默认通道。
let connection = client.connection();
// No channel specified, default channel is used
connection.send_message(message);
// Specifying the channel id
connection.send_message_on(channel_id, message);
// Changing the default channel
connection.set_default_channel(channel_id);
在某些情况下,您可能希望创建多个相同类型的通道实例。例如,使用多个 OrderedReliable
通道来避免一些 头阻塞 问题。尽管通道可以通过 ChannelsConfiguration
来定义,但它们目前也可以随时打开和关闭。您可以同时打开多达 256 个不同的通道。
// If you want to create more channels
let chat_channel = client.connection().open_channel(ChannelType::OrderedReliable).unwrap();
client.connection().send_message_on(chat_channel, chat_message);
在服务器上,通道是在端点级别创建和关闭的,并且对所有当前和未来的客户端都是存在的。
let chat_channel = server.endpoint().open_channel(ChannelType::OrderedReliable).unwrap();
server.endpoint().send_message_on(client_id, chat_channel, chat_message);
证书和服务器认证
Bevy Quinnet(通过 Quinn 和 QUIC)使用 TLS 1.3 进行身份验证,服务器需要向客户端提供一个证明其身份的证书,并且客户端必须配置为信任从服务器收到的证书。
以下是服务器和客户端插件当前可用于服务器身份验证的选项
- 客户端
- 服务器
- 生成并颁发自签名证书
- 颁发一个现有的证书(CA 或自签名)
在客户端
// To accept any certificate
client.open_connection(/*...*/, CertificateVerificationMode::SkipVerification);
// To only accept certificates issued by a Certificate Authority
client.open_connection(/*...*/, CertificateVerificationMode::SignedByCertificateAuthority);
// To use the default configuration of the Trust on first use authentication scheme
client.open_connection(/*...*/, CertificateVerificationMode::TrustOnFirstUse(TrustOnFirstUseConfig {
// You can configure TrustOnFirstUse through the TrustOnFirstUseConfig:
// Provide your own fingerprint store variable/file,
// or configure the actions to apply for each possible certificate verification status.
..Default::default()
}),
);
在服务器上
// To generate a new self-signed certificate on each startup
server.start_endpoint(/*...*/, CertificateRetrievalMode::GenerateSelfSigned {
server_hostname: "127.0.0.1".to_string(),
});
// To load a pre-existing one from files
server.start_endpoint(/*...*/, CertificateRetrievalMode::LoadFromFile {
cert_file: "./certificates.pem".into(),
key_file: "./privkey.pem".into(),
});
// To load one from files, or to generate a new self-signed one if the files do not exist.
server.start_endpoint(/*...*/, CertificateRetrievalMode::LoadFromFileOrGenerateSelfSigned {
cert_file: "./certificates.pem".into(),
key_file: "./privkey.pem".into(),
save_on_disk: true, // To persist on disk if generated
server_hostname: "127.0.0.1".to_string(),
});
有关证书的更多信息,请参阅 证书说明
示例
聊天示例
此示例包含一个无头 服务器、一个 终端客户端 和一个共享的 协议。
使用 cargo run --example chat-server
启动服务器,并使用 cargo run --example chat-client
启动所需数量的客户端。键入 quit
与客户端断开连接。
与示例相比
此示例是将经典的 Bevy 破碎 示例修改为双人对抗游戏的修改版。
它在一个客户端内部托管本地服务器,而不是像聊天演示中那样使用专用的无头服务器。您可以找到 服务器模块、一个 客户端模块、一个共享的 协议 和 bevy 应用程序调度。
它还使用了 Channels
。服务器通过 PaddleMoved
消息在 Unreliable
通道上广播每次击打的桨位,而 BrickDestroyed
和 BallCollided
事件在 UnorderedReliable
通道上发出,而游戏设置和启动使用默认的 OrderedReliable
通道。
使用 cargo run --example breakout
启动两个客户端,“主机”在一个上,“加入”在另一个上。
示例可以在 examples 目录中找到。
Replicon集成
Bevy Quinnet 可以与提供的 bevy_replicon_quinnet
一起作为 bevy_replicon
的传输。
兼容的Bevy版本
bevy_quinnet | bevy |
---|---|
0.9 | 0.14 |
0.7-0.8 | 0.13 |
0.6 | 0.12 |
0.5 | 0.11 |
0.4 | 0.10 |
0.2-0.3 | 0.9 |
0.1 | 0.8 |
其他
Cargo功能
在 cargo.toml 中查找列表和描述。
shared-client-id
[默认]:当新客户端连接到服务器时,服务器将其ClientId
发送到客户端。客户端一旦接收到此 ID,就会认为自己是Connected
。如果不启用,客户端不知道服务器上的ClientId
。
日志
有关日志配置,请参阅非官方的 bevy cheatbook。
限制
- QUIC 不能直接在浏览器中使用(在浏览器中使用,但未作为 API 公开)。目前我宁愿等待 WebTransport(Web 上的“QUIC”)而不是在 WebRTC 数据通道上黑客。
致谢
感谢 Renet crate 对高级 API 的启发。
许可协议
此软件包是免费和开源的。此存储库中的所有代码均在以下任一许可下双许可:
- MIT 许可证 (LICENSE-MIT 或 http://opensource.org/licenses/MIT)
- Apache 许可证,版本 2.0 (LICENSE-APACHE 或 http://www.apache.org/licenses/LICENSE-2.0)
任选其一。
除非您明确声明,否则任何有意提交以包含在您的工作中的贡献(根据 Apache-2.0 许可证定义),都应按上述方式双许可,不附加任何额外条款或条件。
依赖关系
~33–45MB
~829K SLoC