Expand description
“This library stinks!” … “Unless you like durian”
durian
is a client-server networking library built on top of the QUIC protocol which is
implemented in Rust by quinn.
It provides a thin abstraction layer above the lower-level details of connection management, byte management, framing, and more, to make writing netcode easier and allow the user to focus on the messaging contents instead. Serialization and deserialization are built into the APIs so you can send and receive exact Packets as structs for ease of development.
§Disclaimer
This library is in very early (but very active!) development, meaning a LOT of it will change rapidly.
In its current state, it’s usable to create a quick multiplayer demo. I use it myself to learn game
development.
This is not production ready, and is missing a lot of features to make it production ready.
However, if you are trying to build something and want to avoid a lot of the headache of lower-level netcode details, and don’t need the “production” features, such as a multiplayer game demo, LAN sandbox applications, etc., then feel free to try it out!
durian
’s goal is to make it as simple as possible to setup netcode (See the examples below).
§Cargo.toml Dependency
Add durian
to your Cargo.toml via cargo add durian
or manually:
[dependencies]
durian = "0.3"
§Packet/PacketBuilder
There are 2 steps needed to create a Packet
to be used with durian
:
-
durian
allows for structuringPackets
as simple structs. The structs must implement TraitPacket
, which has a single functionPacket::as_bytes()
which will be called for serializing thePacket
into bytes to be sent over the wire between client and server. -
There also needs to be a struct that implements
PacketBuilder
which is used to deserialize from bytes back into yourPacket
struct via thePacketBuilder::read()
function.
For your convenience, durian
is bundled with durian_macros
which contains a few macros that
help autogenerate Impl blocks for a struct for both Packet
and PacketBuilder
. The only
requirement is the struct must be de/serializable, meaning all nested fields also need to be
de/serializable.
bincode_packet
will de/serialize your Packet using bincode
and applies necessary derive
macros automatically for you.
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;
You can also use the derive macros (BinPacket
and UnitPacket
) manually:
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
PacketManager
is what you will use to initiate connections between clients/servers, and
send/receive Packets
.
A PacketManager
would be created on each client to connect to a
single server, and one created on the server to connect to multiple clients. It contains both
synchronous and asynchronous APIs, so you can call the functions both from a synchronous
context, or within an async runtime (Note: the synchronous path will create a separate
isolated async runtime context per PacketManager
instance.)
There are 4 basic steps to using the PacketManager
, which would be done on both the client
and server side:
-
Create a
PacketManager
vianew()
or, if calling from an async context,new_for_async()
-
Register the
Packets
andPacketBuilders
that thePacketManager
will receive and send usingregister_receive_packet()
andregister_send_packet()
.
The ordering ofPacket
registration matters for thereceive
channel andsend
channel each - the client and server must register the same packets in the same order, for the opposite channels.- In other words, the client must register
receive
packets in the same order the server registers the same assend
packets, and vice versa, the client must registersend
packets in the same order the server registers the same asreceive
packets. This helps to ensure the client and servers are in sync on what Packets to send/receive, almost like ensuring they are on the same “version” so to speak, and is used to properly identify Packets. - For your convenience,
durian_macros
comes bundled with 2 macros that simplify registration of send and receive packets:register_receive!()
, andregister_send!()
- In other words, the client must register
-
Initiate connection(s) with
init_client()
(or the async variantasync_init_client()
if on the client side, else useinit_server()
(or the async variantasync_init_server)
if on the server side. -
Send packets using any of
broadcast()
,send()
,send_to()
or the respectiveasync
variants if calling from an async context already. Receive packets using any ofreceived_all()
,received()
, or the respectiveasync
variants.
Putting these together:
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 receive_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_receive_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();
}
§durian_macros
Rust docs don’t properly link documentation of re-exported crates. If you want to learn more about how the
bincode_packet
and other macros used above work, see the durian_macros
docs.
§Examples
For beginners, creating packets to be sent between clients/server should be extremely straight-forward
and the above examples should covers most of what you’d need. For more complex scenarios, such as
serializing/deserializing packets in a custom way, can be done by implementing the various Traits
yourself, or through extra configurations in the PacketManager
.
For a comprehensive minimal example, see the example crate
.
I also use this library myself for simple game development. See the multisnakegame repo
.
§Debugging
durian
uses the log
API with debug and trace logs. Enable debug logging to see update logs
from durian
, and enable trace logging to see packet byte transmissions.
Re-exports§
Modules§
- durian_
macros - Macros for the durian crate
Macros§
Structs§
- Client
Config - Client configuration for initiating a connection from a client to a server via
PacketManager::init_client()
- Close
Error - Error when calling
PacketManager::close_connection()
- Connection
Error - Error when calling
PacketManager::init_client()
,PacketManager::async_init_client()
orPacketManager::init_server()
,PacketManager::async_init_server()
- Endpoint
- For
PacketManager::get_source()
A QUIC endpoint. - Packet
Manager - The core of
durian
that is the central struct containing all the necessary APIs for initiating and managing connections, creating streams, sendingPackets
, receiving, broadcasting, etc. - Receive
Error - Error when calling
PacketManager::register_receive_packet()
,PacketManager::received_all()
,PacketManager::async_received_all()
,PacketManager::received()
,PacketManager::async_received()
- Send
Error - Error when calling
PacketManager::register_send_packet()
,PacketManager::broadcast()
,PacketManager::async_broadcast()
,PacketManager::send()
,PacketManager::async_send()
,PacketManager::send_to()
,PacketManager::async_send_to()
- Server
Config - Server configuration for spinning up a server on a socket address via
PacketManager::init_server()
Enums§
- Error
Type - Error types when receiving an error on one of the
PacketManager
APIs
Traits§
- Packet
- Packet trait that allows a struct to be sent through
PacketManager
, for serializing - Packet
Builder - PacketBuilder is the deserializer for
Packet
and used whenPacketManager
receives bytes
Attribute Macros§
- bincode_
packet - Macros for easy creation of
Packets
andPacketBuilders
Derive Macros§
- BinPacket
- derive macro to automatically implement
Packet
andPacketBuilder
for a struct. - Deref
Packet Manager - Convenience derive macro that implements [
Deref
] and [DerefMut
] for a struct that contains amanager: PacketManager
field. - Error
Only Message - Implements a
new()
function for a struct that contains only amessage: String
field - Unit
Packet - Same as
BinPacket
but for empty or Unit structs