#packets #netcode #packet #gamedev #macro #abstraction-layer #async-context

durian

基于QUIC协议构建的客户端-服务器网络库,由quinn在Rust中实现

8个版本 (4个重大变更)

0.5.0 2023年9月6日
0.4.1 2023年7月30日
0.3.2 2023年3月2日
0.3.0 2023年2月19日
0.1.0 2022年12月8日

#1376 in 网络编程

每月46次下载
用于 2 crates

MIT 许可证

200KB
2.5K SLoC

概述

"这个库很糟糕!" ... "除非你喜欢榴莲"

durian 是一个基于QUIC协议的客户端-服务器网络库,由 QUIC 协议的 quinn 在Rust中实现。

它提供了一层薄薄的抽象层,用于连接管理、字节管理、帧结构等低级细节,以便更容易编写网络代码,并允许用户专注于消息内容。序列化和反序列化已集成到API中,因此您可以使用结构体轻松发送和接收精确的包。

durian 是一个通用库,但主要是为了让我在游戏开发中尝试而创建的。它已与 Bevy 游戏引擎进行了测试并正常工作。

完整文档可在 https://docs.rs/durian/latest/durian/ 找到

Crates.io: https://crates.io/crates/durian/

免责声明

这个库处于非常早期(但非常活跃!)的开发阶段,这意味着它的许多部分都会快速变化。
在其当前状态下,它可以用于创建快速的多玩家演示。我自己使用它来学习 游戏开发

但是,这不是生产就绪版本,并且缺少许多功能才能使其达到生产就绪状态(请参阅下面的 功能 列表)。

但是,如果您正在尝试构建某些内容,并希望避免许多低级网络代码细节的头痛,并且不需要“生产”功能,如多玩家游戏演示、局域网沙盒应用程序等,那么您可以自由尝试它!

durian 的目标是最简化网络代码的设置(请参见下面的示例)。

功能

  • 同时进行基本的客户端/服务器连接管理和操作
  • 适用于不同调用上下文的异步和同步API
  • 无阻塞头部的多路复用(QUIC特性)
    • 为每种数据包类型分配专用流和线程多路复用
  • 可靠数据包:保证所有消息的投递
  • 有序数据包:每个流上的数据包按发送顺序接收
  • 自动为您进行数据包分片和重组
  • 宏简化数据包创建和注册
  • 同时发送和接收数据包
  • 各种客户端/服务器配置
    • keep-alive-intervals
    • idle-timeout
    • ALPN协议

尚未完成

  • 客户端-服务器之间的证书认证
  • 更复杂的连接配置,例如
    • 可插拔的加密
  • 握手协议
  • 连接/流重新建立
  • 在多个PacketManager(对于连接到多个服务器的客户端或具有不同数据包协议的客户端)之间重用端点
  • 更好的错误处理/消息传递
  • 不可靠数据包
  • 无序数据包
  • 可能还有更多

Cargo.toml依赖

通过cargo add durian或手动将durian添加到您的Cargo.toml中

[dependencies]
durian = "0.3"

数据包/数据包构建器

创建一个用于与durian一起使用的Packet需要两个步骤

  1. durian允许将Packets结构化为简单的结构体。这些结构体必须实现Packet特质,该特质包含一个用于将Packet序列化为发送到客户端和服务器之间的字节的单函数Packet::as_bytes()

  2. 还需要一个实现PacketBuilder的结构体,该结构体用于将字节反序列化回您的Packet结构体,通过PacketBuilder::read()函数。

为了方便起见,durian捆绑了durian_macros,其中包含一些宏,这些宏有助于自动生成PacketPacketBuilder的结构体的Impl块。唯一的要求是该结构体必须是可序列化的,这意味着所有嵌套字段也必须是可序列化的。

#[bincode_packet]将使用bincode对您的Packet进行序列化和反序列化,并自动应用必要的 derive 宏。

use durian::bincode_packet;

// Automatically implements Packet, and generates a PositionPacketBuilder that implements 
// PacketBuilder.  You can also add other macros such as derive macros so long as they don't
// conflict with what #[bincode_packet] adds (See bincode_packet documentation).
#[bincode_packet]
#[derive(Debug)]
struct Position {
    x: i32,
    y: i32
}

// Works for Unit (empty) structs as well
#[bincode_packet]
struct Ack;

您还可以手动使用 derive 宏(BinPacketUnitPacket

use durian::serde::{Deserialize, Serialize};
use durian::{BinPacket, UnitPacket};

#[derive(Serialize, Deserialize, BinPacket)]
#[serde(crate = "durian::serde")]
struct Position { x: i32, y: i32 }

#[derive(UnitPacket)]
struct Ack;

数据包管理器

PacketManager是您将用于在客户端/服务器之间建立连接,并发送/接收Packets

在每个客户端创建一个PacketManager以连接到单个服务器,并在服务器上创建一个以连接到多个客户端。它包含同步和异步API,因此您可以从同步上下文或异步运行时(注意:同步路径将为每个PacketManager实例创建一个单独的隔离的异步运行时上下文。)调用函数。

使用PacketManager有4个基本步骤,这些步骤将在客户端和服务器端执行

  1. 通过new()或,如果从异步上下文调用,使用new_for_async()创建一个PacketManager

  2. 使用register_receive_packet()register_send_packet()注册PacketsPacketBuilders,这些包将用于PacketManager接收和发送。
    Packet注册的顺序对于每个接收和发送通道都很重要 - 客户端和服务器必须以相同的顺序注册相同的包,对于相反的通道。

    • 换句话说,客户端必须以服务器注册相同顺序的相同顺序注册接收包,反之亦然,客户端必须以服务器注册相同顺序的相同顺序注册发送包。这有助于确保客户端和服务器在要发送/接收的包上保持同步,几乎就像确保它们处于相同的“版本”一样,并用于正确识别包。
    • 为了方便起见,durian_macros附带了2个宏,用于简化发送和接收包的注册:register_receive!()register_send!()
  3. 使用 init_client() (或在客户端侧使用异步版本 async_init_client()) 初始化连接,否则在服务器端使用 init_server() (或异步版本 async_init_server))。

  4. 使用以下任意方法发送数据包:broadcast()send()send_to()

将这些放在一起

use durian::{ClientConfig, PacketManager, bincode_packet, register_receive, register_send};

#[bincode_packet]
struct Position { x: i32, y: i32 }
#[bincode_packet]
struct ServerAck;
#[bincode_packet]
struct ClientAck;
#[bincode_packet]
struct InputMovement { direction: String }

fn packet_manager_example() {
    // Create PacketManager
    let mut manager = PacketManager::new();

    // Register send and receive packets
    // Using macros
    let register_results = register_receive!(
        manager, 
        (Position, PositionPacketBuilder), 
        (ServerAck, ServerAckPacketBuilder)
    );
    let send_results = register_send!(manager, ClientAck, InputMovement);
    // Validate registrations succeeded
    assert!(receive_results.iter().all(|r| r.is_ok()));
    assert!(send_results.iter().all(|r| r.is_ok()));

    // Or equivalently with manual registrations:
    // manager.register_receive_packet::<Position>(PositionPacketBuilder).unwrap();
    // manager.register_receive_packet::<ServerAck>(ServerAckPacketBuilder).unwrap();
    // manager.register_send_packet::<ClientAck>().unwrap();
    // manager.register_send_packet::<InputMovement>().unwrap();

    // Initialize a client
    let client_config = ClientConfig::new("127.0.0.1:5001", "127.0.0.1:5000", 2, 2);
    manager.init_client(client_config).unwrap();

    // Send and receive packets
    manager.broadcast(InputMovement { direction: "North".to_string() }).unwrap();
    manager.received_all::<Position, PositionPacketBuilder>(false).unwrap();

    // The above PacketManager is for the client.  Server side is similar except the packets
    // are swapped between receive vs send channels:

    // Create PacketManager
    let mut server_manager = PacketManager::new();

    // Register send and receive packets
    let server_register_results = register_receive!(
        server_manager, 
        (ClientAck, ClientAckPacketBuilder), 
        (InputMovement, InputMovementPacketBuilder)
    );
    let server_send_results = register_send!(server_manager, Position, ServerAck);
    // Validate registrations succeeded
    assert!(server_receive_results.iter().all(|r| r.is_ok()));
    assert!(server_send_results.iter().all(|r| r.is_ok()));

    // Initialize a client
    let client_config = ClientConfig::new("127.0.0.1:5001", "127.0.0.1:5000", 2, 2);
    server_manager.init_client(client_config).unwrap();

    // Send and receive packets
    server_manager.broadcast(Position { x: 1, y: 3 }).unwrap();
    server_manager.received_all::<InputMovement, InputMovementPacketBuilder>(false).unwrap();
}

示例

对于初学者,创建客户端/服务器之间的数据包应该非常简单,上述示例应涵盖您所需的大部分内容。对于更复杂的场景,例如以自定义方式序列化/反序列化数据包,可以自行实现或通过在 multisnakegame 仓库 中进行额外配置来完成。

对于全面的示例,请参阅 example crate

我还自己使用这个库进行简单的游戏开发。请参阅 multisnakegame 仓库

调试

durian 使用日志记录和跟踪日志记录。启用调试记录以查看来自 durian 的更新日志,并启用跟踪记录以查看数据包字节传输。

依赖项

~16–27MB
~492K SLoC