#quic #bevy #networking #bevy-plugin #client-certificate #multiplayer-game #gamedev

bevy_quinnet

Bevy插件,用于使用QUIC的客户端/服务器多人游戏

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 游戏开发

Download history 14/week @ 2024-04-29 58/week @ 2024-05-06 126/week @ 2024-05-13 18/week @ 2024-05-20 9/week @ 2024-05-27 10/week @ 2024-06-03 20/week @ 2024-06-10 13/week @ 2024-06-24 169/week @ 2024-07-01 38/week @ 2024-07-08 10/week @ 2024-07-15 9/week @ 2024-07-22 131/week @ 2024-07-29 27/week @ 2024-08-05 27/week @ 2024-08-12

194 每月下载量
用于 2 crates

MIT/Apache

180KB
3K SLoC

Bevy tracking crates.io bevy_quinnet on doc.rs

Bevy Quinnet

Bevy引擎使用的QUIC客户端/服务器游戏网络插件。

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 是用户提供的枚举,它派生自 SerializeDeserialize
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 是用户提供的枚举,它派生自 SerializeDeserialize
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 进行身份验证,服务器需要向客户端提供一个证明其身份的证书,并且客户端必须配置为信任从服务器收到的证书。

以下是服务器和客户端插件当前可用于服务器身份验证的选项

  • 客户端
    • 跳过证书验证(消息仍然加密,但服务器未经认证)
    • 接受由证书颁发机构签发的证书(在 Quinn 中实现,使用 rustls
    • 首次使用即信任 证书(在 Quinnet 中实现,使用 rustls
  • 服务器
    • 生成并颁发自签名证书
    • 颁发一个现有的证书(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 与客户端断开连接。

terminal_chat_demo

与示例相比

此示例是将经典的 Bevy 破碎 示例修改为双人对抗游戏的修改版。

它在一个客户端内部托管本地服务器,而不是像聊天演示中那样使用专用的无头服务器。您可以找到 服务器模块、一个 客户端模块、一个共享的 协议bevy 应用程序调度

它还使用了 Channels。服务器通过 PaddleMoved 消息在 Unreliable 通道上广播每次击打的桨位,而 BrickDestroyedBallCollided 事件在 UnorderedReliable 通道上发出,而游戏设置和启动使用默认的 OrderedReliable 通道。

使用 cargo run --example breakout 启动两个客户端,“主机”在一个上,“加入”在另一个上。

breakout_versus_demo_short.mp4

示例可以在 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 的启发。

许可协议

此软件包是免费和开源的。此存储库中的所有代码均在以下任一许可下双许可:

任选其一。

除非您明确声明,否则任何有意提交以包含在您的工作中的贡献(根据 Apache-2.0 许可证定义),都应按上述方式双许可,不附加任何额外条款或条件。

依赖关系

~33–45MB
~829K SLoC