[feat][code] proto crate, one stream per request

This commit is contained in:
2024-11-08 22:22:50 +01:00
parent 04a39790b8
commit 9d8f579a0f
18 changed files with 862 additions and 306 deletions

View File

@ -3,11 +3,23 @@ name = "lanspread-client"
version = "0.1.0"
edition = "2021"
[lints.rust]
unsafe_code = "forbid"
[lints.clippy]
pedantic = { level = "warn", priority = -1 }
todo = "warn"
unwrap_used = "warn"
[dependencies]
# local
lanspread-db = { path = "../lanspread-db" }
lanspread-proto = { path = "../lanspread-proto" }
lanspread-utils = { path = "../lanspread-utils" }
# external
eyre = { workspace = true }
s2n-quic = { workspace = true }
serde_json = { workspace = true }
tokio = { workspace = true }
tracing = { workspace = true }
tracing-subscriber = { workspace = true }

View File

@ -1,65 +1,154 @@
use std::{net::SocketAddr, sync::Arc};
use std::{net::SocketAddr, time::Duration};
use lanspread_db::{Game, GameDB};
use s2n_quic::{client::Connect, Client as QuicClient};
use tokio::{io::AsyncWriteExt as _, sync::Mutex};
use lanspread_proto::{Message as _, Request, Response};
use lanspread_utils::maybe_addr;
use s2n_quic::{client::Connect, provider::limits::Limits, Client as QuicClient};
use tokio::{
io::{AsyncBufReadExt as _, AsyncWriteExt as _},
sync::mpsc::UnboundedReceiver,
};
use tracing_subscriber::EnvFilter;
static CERT_PEM: &str = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/../../cert.pem"));
const SERVER_ADDR: &str = "127.0.0.1";
const SERVER_PORT: u16 = 13337;
struct Client {
db: Arc<Mutex<GameDB>>,
#[derive(Debug)]
enum ControlMessage {
ListGames,
GetGame(u64),
}
impl Client {
pub(crate) fn new() -> Self {
Client {
db: Arc::new(Mutex::new(GameDB::new())),
}
}
struct Client;
impl Client {
pub(crate) async fn run(
addr: SocketAddr,
mut rx_control: UnboundedReceiver<ControlMessage>,
) -> eyre::Result<()> {
tracing_subscriber::fmt()
.with_env_filter(EnvFilter::from_default_env())
.init();
let limits = Limits::default().with_max_handshake_duration(Duration::from_secs(3))?;
pub(crate) async fn run(&mut self, addr: SocketAddr) -> eyre::Result<()> {
let client = QuicClient::builder()
.with_tls(CERT_PEM)?
.with_io("0.0.0.0:0")?
.with_limits(limits)?
.start()?;
let connect1 = Connect::new(addr).with_server_name("localhost");
let mut connection1 = client.connect(connect1).await?;
connection1.keep_alive(true)?;
let conn = Connect::new(addr).with_server_name("localhost");
let mut conn = client.connect(conn).await?;
conn.keep_alive(true)?;
let stream = connection1.open_bidirectional_stream().await?;
let (mut rx, mut tx) = stream.split();
tracing::info!(
"connected: (server: {}) (client: {})",
maybe_addr!(conn.remote_addr()),
maybe_addr!(conn.local_addr())
);
let buf = b"get_games";
tx.write_all(&buf[..]).await?;
// tx
while let Some(cmd) = rx_control.recv().await {
let request = match cmd {
ControlMessage::ListGames => Request::ListGames,
ControlMessage::GetGame(id) => Request::GetGame { id },
};
while let Ok(Some(data)) = rx.receive().await {
let games: Vec<Game> = serde_json::from_slice(&data)?;
self.db = Arc::new(Mutex::new(GameDB::from(games)));
tx.close().await.unwrap();
let db = self.db.lock().await;
let data = request.encode();
tracing::trace!("encoded data: {}", String::from_utf8_lossy(&data));
eprintln!("received GameDB:");
for game in db.games.values() {
eprintln!("{:#?}", game);
let stream = conn.open_bidirectional_stream().await?;
let (mut rx, mut tx) = stream.split();
if let Err(e) = tx.write_all(&data).await {
tracing::error!(?e, "failed to send request to server");
}
if let Ok(Some(data)) = rx.receive().await {
tracing::trace!("server response (raw): {}", String::from_utf8_lossy(&data));
let response = Response::decode(&data);
tracing::trace!(
"server response (decoded): {}",
String::from_utf8_lossy(&data)
);
match response {
Response::Games(games) => {
for game in games {
tracing::debug!(?game);
}
}
Response::Game(game) => tracing::debug!(?game, "game received"),
Response::GameNotFound(id) => tracing::debug!(?id, "game not found"),
Response::InvalidRequest(request_bytes, err) => tracing::error!(
"server says our request was invalid (error: {}): {}",
err,
String::from_utf8_lossy(&request_bytes)
),
Response::EncodingError(err) => {
tracing::error!("server encoding error: {err}");
}
Response::DecodingError(data, err) => {
tracing::error!(
"response decoding error: {} (data: {})",
err,
String::from_utf8_lossy(&data)
);
}
}
if let Err(err) = tx.close().await {
tracing::error!("failed to close stream: {err}");
}
}
}
eprintln!("server closed");
tracing::info!("server closed connection");
Ok(())
}
}
#[tokio::main]
async fn main() -> eyre::Result<()> {
let mut client = Client::new();
let (tx_control, rx_control) = tokio::sync::mpsc::unbounded_channel::<ControlMessage>();
client
.run(format!("{SERVER_ADDR}:{SERVER_PORT}").parse().unwrap())
.await?;
// Spawn client in a separate task
let client_handle = tokio::spawn(async move {
#[allow(clippy::unwrap_used)]
let addr = format!("{SERVER_ADDR}:{SERVER_PORT}").parse().unwrap();
Client::run(addr, rx_control).await
});
// Handle stdin commands in the main task
let mut stdin = tokio::io::BufReader::new(tokio::io::stdin());
let mut line = String::new();
loop {
line.clear();
if stdin.read_line(&mut line).await? == 0 {
break; // EOF reached
}
// Trim whitespace and handle commands
match line.trim() {
"list" => {
tx_control.send(ControlMessage::ListGames)?;
}
cmd if cmd.starts_with("get ") => {
if let Ok(id) = cmd[4..].trim().parse::<u64>() {
tx_control.send(ControlMessage::GetGame(id))?;
} else {
println!("Invalid game ID");
}
}
"quit" | "exit" => break,
"" => continue,
_ => println!("Unknown command. Available commands: list, get <id>, quit"),
}
}
client_handle.await??;
Ok(())
}