aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authormetamuffin <metamuffin@disroot.org>2024-07-14 21:00:54 +0200
committermetamuffin <metamuffin@disroot.org>2024-07-14 21:00:54 +0200
commitf6cc57f29fc65a33219b1266b3508dc40536f3c2 (patch)
tree3a5ee5897f778fd1e0547a3026060e579a4c7c56
parent721b9ecb282f84839eb096de4d269176f8ab389f (diff)
downloadhurrycurry-f6cc57f29fc65a33219b1266b3508dc40536f3c2.tar
hurrycurry-f6cc57f29fc65a33219b1266b3508dc40536f3c2.tar.bz2
hurrycurry-f6cc57f29fc65a33219b1266b3508dc40536f3c2.tar.zst
add binary protocol support
-rw-r--r--Cargo.lock27
-rw-r--r--protocol.md24
-rw-r--r--server/Cargo.toml1
-rw-r--r--server/protocol/Cargo.toml2
-rw-r--r--server/protocol/src/lib.rs50
-rw-r--r--server/src/main.rs92
-rw-r--r--test-client/protocol.ts2
7 files changed, 147 insertions, 51 deletions
diff --git a/Cargo.lock b/Cargo.lock
index 80d683be..896d13dc 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -150,6 +150,25 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
[[package]]
+name = "bincode"
+version = "2.0.0-rc.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f11ea1a0346b94ef188834a65c068a03aec181c94896d481d7a0a40d85b0ce95"
+dependencies = [
+ "bincode_derive",
+ "serde",
+]
+
+[[package]]
+name = "bincode_derive"
+version = "2.0.0-rc.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7e30759b3b99a1b802a7a3aa21c85c3ded5c28e1c83170d82d70f08bbf7f3e4c"
+dependencies = [
+ "virtue",
+]
+
+[[package]]
name = "bindgen"
version = "0.69.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -561,6 +580,7 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4"
name = "hurrycurry-protocol"
version = "0.1.0"
dependencies = [
+ "bincode",
"glam",
"serde",
]
@@ -589,6 +609,7 @@ name = "hurrycurry-server"
version = "0.2.0"
dependencies = [
"anyhow",
+ "bincode",
"clap",
"env_logger",
"fake",
@@ -1365,6 +1386,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
[[package]]
+name = "virtue"
+version = "0.0.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9dcc60c0624df774c82a0ef104151231d37da4962957d691c011c852b2473314"
+
+[[package]]
name = "wasi"
version = "0.11.0+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
diff --git a/protocol.md b/protocol.md
index 28626200..956663ba 100644
--- a/protocol.md
+++ b/protocol.md
@@ -20,9 +20,10 @@
The protocol schema is defined in [`protocol.ts`](./test-client/protocol.ts)
-1. Connect to the server via ~~TCP (on port 27031) or~~ WebSocket (on
- port 27032) and send/receive json on individual lines / text messages.
-2. Wait for `init` packet.
+1. Connect to the server via WebSocket (on port 27032 for plain HTTP or 443 with
+ SSL) and send/receive json in WebSocket "Text" messages. The binary protocol
+ uses "Binary" messages and is optional for servers and clients.
+2. Wait for `init` packet and check version compatibiliy (see below).
3. Send the join packet with your username.
4. The server will send the current game state:
- `data` once for setting important look-up tables
@@ -42,6 +43,23 @@ Collisions are handled by the clients. Whenever to players collide the player
with the greater PlayerID is responsible for updating their own momentum and
sending a packet to update that of the other player.
+## Binary Protocol
+
+Servers might also support the binary protocol. It uses
+[Bincode](https://github.com/bincode-org/bincode) to encode packets. If a server
+advertises bincode support with the `init` packet, you are free to use the
+binary protocol. By default the server will send JSON. For every packet you
+send, you can choose between bincode and JSON. After a client sent the first
+Bincode packet, the server might start to reply using Bincode aswell. Bincoded
+packets are sent with WebSocket "Binary" messages.
+
+## Protocol Versioning
+
+The `init` packet sends minor and major version numbers of the protocol is use
+by the server. These are to be interpreted according to
+[SemVer](https://semver.org/) for the JSON protocol. The binary protocol can not
+be used if either minor or major version differs.
+
## Movement
Movement is handled mostly client-side. Therefore it is implemented three times:
diff --git a/server/Cargo.toml b/server/Cargo.toml
index da084855..39b9ea1c 100644
--- a/server/Cargo.toml
+++ b/server/Cargo.toml
@@ -19,5 +19,6 @@ shlex = "1.3.0"
clap = { version = "4.5.8", features = ["derive"] }
fake = "2.9.2"
pollster = "0.3.0"
+bincode = "2.0.0-rc.3"
hurrycurry-protocol = { path = "protocol" }
diff --git a/server/protocol/Cargo.toml b/server/protocol/Cargo.toml
index 8fc63121..6c51c86d 100644
--- a/server/protocol/Cargo.toml
+++ b/server/protocol/Cargo.toml
@@ -6,4 +6,4 @@ edition = "2021"
[dependencies]
serde = { version = "1.0.204", features = ["derive"] }
glam = { version = "0.28.0", features = ["serde"] }
-
+bincode = { version = "2.0.0-rc.3", features = ["serde", "derive"] }
diff --git a/server/protocol/src/lib.rs b/server/protocol/src/lib.rs
index 94bebf05..895af376 100644
--- a/server/protocol/src/lib.rs
+++ b/server/protocol/src/lib.rs
@@ -15,6 +15,10 @@
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
+use bincode::{
+ config::{standard, Configuration, Limit, LittleEndian, Varint},
+ Decode, Encode,
+};
use glam::{IVec2, Vec2};
use serde::{Deserialize, Serialize};
use std::{
@@ -26,31 +30,44 @@ pub use glam;
pub const VERSION: (u32, u32) = (1, 0);
-#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Hash)]
+pub const BINCODE_CONFIG: Configuration<LittleEndian, Varint, Limit<4096>> =
+ standard().with_limit();
+
+#[derive(
+ Debug, Clone, Copy, Serialize, Deserialize, Encode, Decode, PartialEq, Eq, PartialOrd, Ord, Hash,
+)]
#[serde(transparent)]
pub struct PlayerID(pub i64);
-#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Hash)]
+#[derive(
+ Debug, Clone, Copy, Serialize, Deserialize, Encode, Decode, PartialEq, Eq, PartialOrd, Ord, Hash,
+)]
#[serde(transparent)]
pub struct ItemIndex(pub usize);
-#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Hash)]
+#[derive(
+ Debug, Clone, Copy, Serialize, Deserialize, Encode, Decode, PartialEq, Eq, PartialOrd, Ord, Hash,
+)]
#[serde(transparent)]
pub struct TileIndex(pub usize);
-#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Hash)]
+#[derive(
+ Debug, Clone, Copy, Serialize, Deserialize, Encode, Decode, PartialEq, Eq, PartialOrd, Ord, Hash,
+)]
#[serde(transparent)]
pub struct RecipeIndex(pub usize);
-#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Hash)]
+#[derive(
+ Debug, Clone, Copy, Serialize, Deserialize, Encode, Decode, PartialEq, Eq, PartialOrd, Ord, Hash,
+)]
#[serde(transparent)]
pub struct DemandIndex(pub usize);
-#[derive(Debug, Clone, Serialize, Deserialize)]
+#[derive(Debug, Clone, Serialize, Deserialize, Encode, Decode)]
pub struct MapMetadata {
name: String,
players: usize,
difficulty: i32,
}
-#[derive(Debug, Clone, Serialize, Deserialize, Default)]
+#[derive(Debug, Clone, Serialize, Deserialize, Encode, Decode, Default)]
#[rustfmt::skip]
pub struct ClientGamedata {
pub item_names: Vec<String>,
@@ -61,7 +78,7 @@ pub struct ClientGamedata {
pub maps: HashMap<String, MapMetadata>,
}
-#[derive(Debug, Clone, Serialize, Deserialize)]
+#[derive(Debug, Clone, Serialize, Deserialize, Encode, Decode)]
#[serde(rename_all = "snake_case", tag = "type")]
pub enum PacketS {
Join {
@@ -70,15 +87,18 @@ pub enum PacketS {
},
Leave,
Position {
+ #[bincode(with_serde)]
pos: Vec2,
rot: f32,
boosting: bool,
},
Interact {
+ #[bincode(with_serde)]
pos: Option<IVec2>,
},
Collide {
player: PlayerID,
+ #[bincode(with_serde)]
force: Vec2,
},
Communicate {
@@ -87,6 +107,7 @@ pub enum PacketS {
},
#[serde(skip)]
+ #[bincode(skip)]
/// For internal use only
ReplaceHand {
item: Option<ItemIndex>,
@@ -97,7 +118,7 @@ pub enum PacketS {
},
}
-#[derive(Debug, Clone, Serialize, Deserialize)]
+#[derive(Debug, Clone, Serialize, Deserialize, Encode, Decode)]
#[serde(rename_all = "snake_case")]
pub enum Message {
Text(String),
@@ -105,12 +126,13 @@ pub enum Message {
Effect(String),
}
-#[derive(Debug, Clone, Serialize, Deserialize)]
+#[derive(Debug, Clone, Serialize, Deserialize, Encode, Decode)]
#[serde(rename_all = "snake_case", tag = "type")]
pub enum PacketC {
Version {
minor: u32,
major: u32,
+ supports_bincode: bool,
},
Init {
id: PlayerID,
@@ -120,6 +142,7 @@ pub enum PacketC {
},
AddPlayer {
id: PlayerID,
+ #[bincode(with_serde)]
position: Vec2,
character: i32,
name: String,
@@ -129,6 +152,7 @@ pub enum PacketC {
},
Position {
player: PlayerID,
+ #[bincode(with_serde)]
pos: Vec2,
rot: f32,
boosting: bool,
@@ -147,12 +171,14 @@ pub enum PacketC {
warn: bool,
},
UpdateMap {
+ #[bincode(with_serde)]
tile: IVec2,
kind: Option<TileIndex>,
neighbors: [Option<TileIndex>; 4],
},
Collide {
player: PlayerID,
+ #[bincode(with_serde)]
force: Vec2,
},
Communicate {
@@ -181,10 +207,10 @@ pub enum PacketC {
ReplayStart,
}
-#[derive(Debug, Clone, Serialize, Deserialize, Copy, PartialEq, Eq, Hash)]
+#[derive(Debug, Clone, Serialize, Deserialize, Encode, Decode, Copy, PartialEq, Eq, Hash)]
#[serde(rename_all = "snake_case")]
pub enum ItemLocation {
- Tile(IVec2),
+ Tile(#[bincode(with_serde)] IVec2),
Player(PlayerID),
}
diff --git a/server/src/main.rs b/server/src/main.rs
index 90d090d8..6f73851a 100644
--- a/server/src/main.rs
+++ b/server/src/main.rs
@@ -18,10 +18,19 @@
use anyhow::{anyhow, Result};
use clap::Parser;
use futures_util::{SinkExt, StreamExt};
-use hurrycurry_protocol::{PacketC, PacketS, PlayerID, VERSION};
+use hurrycurry_protocol::{PacketC, PacketS, PlayerID, BINCODE_CONFIG, VERSION};
use hurrycurry_server::{data::DATA_DIR, state::State};
use log::{debug, info, trace, warn, LevelFilter};
-use std::{path::PathBuf, process::exit, str::FromStr, sync::Arc, time::Duration};
+use std::{
+ path::PathBuf,
+ process::exit,
+ str::FromStr,
+ sync::{
+ atomic::{AtomicBool, Ordering},
+ Arc,
+ },
+ time::Duration,
+};
use tokio::{
net::TcpListener,
spawn,
@@ -121,9 +130,14 @@ async fn run() -> anyhow::Result<()> {
PacketC::Version {
major: VERSION.0,
minor: VERSION.1,
+ supports_bincode: true,
},
);
init.insert(1, PacketC::Init { id });
+
+ let supports_binary = Arc::new(AtomicBool::new(false));
+ let supports_binary2 = supports_binary.clone();
+
spawn(async move {
for p in init {
if let Err(e) = write
@@ -154,52 +168,62 @@ async fn run() -> anyhow::Result<()> {
info!("client outbound sender dropped. closing connection");
break;
};
- if let Err(e) = write
- .send(tokio_tungstenite::tungstenite::Message::Text(
- serde_json::to_string(&packet).unwrap(),
- ))
- .await
- {
+ let message = if supports_binary.load(Ordering::Relaxed) {
+ Message::Binary(bincode::encode_to_vec(&packet, BINCODE_CONFIG).unwrap())
+ } else {
+ Message::Text(serde_json::to_string(&packet).unwrap())
+ };
+ if let Err(e) = write.send(message).await {
warn!("ws error: {e}");
break;
}
}
});
+
spawn(async move {
info!("{id:?} joined");
while let Some(Ok(message)) = read.next().await {
- match message {
- Message::Text(line) => {
- let packet = match serde_json::from_str(&line) {
- Ok(p) => p,
- Err(e) => {
- warn!("invalid packet: {e}");
- break;
- }
- };
- if matches!(
- packet,
- PacketS::Position { .. } | PacketS::ReplayTick { .. }
- ) {
- trace!("<- {id:?} {packet:?}");
- } else {
- debug!("<- {id:?} {packet:?}");
+ let packet = match message {
+ Message::Text(line) => match serde_json::from_str(&line) {
+ Ok(p) => p,
+ Err(e) => {
+ warn!("invalid json packet: {e}");
+ break;
}
- let packet_out = match state.write().await.packet_in(id, packet).await {
- Ok(packets) => packets,
+ },
+ Message::Binary(packet) => {
+ supports_binary2.store(true, Ordering::Relaxed);
+ match bincode::decode_from_slice::<PacketS, _>(&packet, BINCODE_CONFIG) {
+ Ok((p, _size)) => p,
Err(e) => {
- warn!("client error: {e}");
- vec![PacketC::Error {
- message: format!("{e}"),
- }]
+ warn!("invalid binary packet: {e}");
+ break;
}
- };
- for packet in packet_out {
- let _ = error_tx.send(packet).await;
}
}
Message::Close(_) => break,
- _ => (),
+ _ => continue,
+ };
+
+ if matches!(
+ packet,
+ PacketS::Position { .. } | PacketS::ReplayTick { .. }
+ ) {
+ trace!("<- {id:?} {packet:?}");
+ } else {
+ debug!("<- {id:?} {packet:?}");
+ }
+ let packet_out = match state.write().await.packet_in(id, packet).await {
+ Ok(packets) => packets,
+ Err(e) => {
+ warn!("client error: {e}");
+ vec![PacketC::Error {
+ message: format!("{e}"),
+ }]
+ }
+ };
+ for packet in packet_out {
+ let _ = error_tx.send(packet).await;
}
}
info!("{id:?} left");
diff --git a/test-client/protocol.ts b/test-client/protocol.ts
index 2cd58f75..f8241854 100644
--- a/test-client/protocol.ts
+++ b/test-client/protocol.ts
@@ -44,7 +44,7 @@ export type PacketS =
| { type: "collide", player: PlayerID, force: Vec2 } // Apply force to another player as a result of a collision
export type PacketC =
- { type: "version", minor: number, major: number } // Sent once after connecting to ensure you client is compatible
+ { type: "version", minor: number, major: number, supports_bincode?: boolean } // Sent once after connecting to ensure you client is compatible
| { type: "init", id: PlayerID } // You just connected. This is your id for this session.
| { type: "data", data: Gamedata } // Game data was changed
| { type: "add_player", id: PlayerID, name: string, position: Vec2, character: number } // Somebody else joined (or was already in the game)