[feat][code] proto crate, one stream per request
This commit is contained in:
@ -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 }
|
||||
|
@ -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(())
|
||||
}
|
||||
|
Reference in New Issue
Block a user