feat: add barebones wss transport
tokio already came back...
This commit is contained in:
parent
58c3975139
commit
a0071ddc38
4 changed files with 179 additions and 0 deletions
|
@ -7,3 +7,18 @@ edition = "2021"
|
|||
|
||||
[dependencies]
|
||||
tracing = "0.1"
|
||||
thiserror = "1.0.61"
|
||||
tokio = { version = "1.38.0", features = ["full"], optional = true }
|
||||
tokio-tungstenite = { version = "0.23.1", features = ["tokio-native-tls"], optional = true }
|
||||
futures-util = "0.3.30"
|
||||
async-trait = "0.1.80"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = { version = "1.0.118", optional = true }
|
||||
|
||||
scct-model = { path = "../model/" }
|
||||
|
||||
|
||||
[features]
|
||||
default = ["websocket"]
|
||||
websocket = ["dep:tokio", "dep:tokio-tungstenite", "dep:serde_json"]
|
||||
laminar = []
|
||||
|
|
28
server/src/transport/laminar.rs
Normal file
28
server/src/transport/laminar.rs
Normal file
|
@ -0,0 +1,28 @@
|
|||
use super::{Transport, TransportResult, TransportSink, TransportStream};
|
||||
use scct_model::proto::c2s;
|
||||
|
||||
pub struct LaminarTransport;
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl Transport for LaminarTransport {
|
||||
async fn serve(self, _addr: String) -> TransportResult<tokio::sync::mpsc::Receiver<(LaminarSink, LaminarStream)>> {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct LaminarStream;
|
||||
pub struct LaminarSink;
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl TransportStream for LaminarStream {
|
||||
async fn pop(&mut self) -> TransportResult<Option<c2s::s::Packet>> {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl TransportSink for LaminarSink {
|
||||
async fn push(&mut self, msg: c2s::c::Packet) -> TransportResult<()> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
72
server/src/transport/mod.rs
Normal file
72
server/src/transport/mod.rs
Normal file
|
@ -0,0 +1,72 @@
|
|||
//! this part is mostly temporary, to get us quickly running with something web-ready (websockets)
|
||||
//!
|
||||
//! once we got most things running we can swap out websockets for laminar (or whatever we settle
|
||||
//! on) just by respecting this trait. once the swap is complete, transport and server can be
|
||||
//! coupled more, if necessary
|
||||
//!
|
||||
//! note that web app will still need a websocket, but it's probably better to make a bridge
|
||||
//! translating server's laminar/whatev into websockets, rather than having each server also bundle
|
||||
//! a websocket server just in case
|
||||
//!
|
||||
//! so basically most likely everything down here will get coupled more and merged with the rest
|
||||
|
||||
#[cfg(feature = "websocket")]
|
||||
pub mod websocket;
|
||||
|
||||
#[cfg(feature = "laminar")]
|
||||
pub mod laminar;
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum TransportError {
|
||||
|
||||
#[cfg(feature = "websocket")]
|
||||
#[error("websocket error: {0}")]
|
||||
WebSocket(#[from] tokio_tungstenite::tungstenite::Error),
|
||||
|
||||
#[cfg(feature = "websocket")]
|
||||
#[error("invalid json: {0}")]
|
||||
Json(#[from] serde_json::Error),
|
||||
|
||||
#[error("system i/o error: {0}")]
|
||||
IO(#[from] std::io::Error),
|
||||
}
|
||||
|
||||
pub type TransportResult<T> = Result<T, TransportError>;
|
||||
|
||||
|
||||
|
||||
use scct_model::proto::c2s;
|
||||
|
||||
#[async_trait::async_trait]
|
||||
pub trait Transport : Sized {
|
||||
// TODO can we get rid of tokio-specifc channel here without losing async blocking and throwing
|
||||
// in new crates?
|
||||
async fn serve(self, addr: String) -> Result<tokio::sync::mpsc::Receiver<(impl TransportSink, impl TransportStream)>, TransportError>;
|
||||
}
|
||||
|
||||
|
||||
// i hate these names: "pop" and "push" are for queues, but "send" and "recv" are used by
|
||||
// underlying transport so it gets a name clash mess and trait disambiguations are nasty
|
||||
// TODO rename these?
|
||||
|
||||
#[async_trait::async_trait]
|
||||
pub trait TransportStream: Send + Sync {
|
||||
async fn pop(&mut self) -> Result<Option<c2s::s::Packet>, TransportError>;
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
pub trait TransportSink: Send + Sync {
|
||||
async fn push(&mut self, msg: c2s::c::Packet) -> Result<(), TransportError>;
|
||||
}
|
||||
|
||||
|
||||
#[allow(unreachable_code)]
|
||||
pub fn new() -> impl Transport {
|
||||
#[cfg(feature = "laminar")]
|
||||
return laminar::LaminarTransport;
|
||||
|
||||
#[cfg(feature = "websocket")]
|
||||
return websocket::WSTransport;
|
||||
|
||||
panic!("no availabe C2S transport available in this binary!");
|
||||
}
|
64
server/src/transport/websocket.rs
Normal file
64
server/src/transport/websocket.rs
Normal file
|
@ -0,0 +1,64 @@
|
|||
use super::{Transport, TransportError, TransportResult, TransportSink, TransportStream};
|
||||
|
||||
use futures_util::{stream::{SplitSink, SplitStream, TryStreamExt}, SinkExt, StreamExt};
|
||||
|
||||
use scct_model::proto::c2s;
|
||||
use tokio::{sync::mpsc, net::{TcpListener, TcpStream}};
|
||||
use tokio_tungstenite::{tungstenite::protocol::Message, WebSocketStream};
|
||||
|
||||
pub type WSRx = SplitStream<WebSocketStream<TcpStream>>;
|
||||
pub type WSTx = SplitSink<WebSocketStream<TcpStream>, Message>;
|
||||
pub struct WSTransport;
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl Transport for WSTransport {
|
||||
async fn serve(self, addr: String) -> Result<mpsc::Receiver<(WSTx, WSRx)>, TransportError> {
|
||||
let listener = TcpListener::bind(&addr).await?;
|
||||
tracing::info!("serving websocket transport on: {addr}");
|
||||
|
||||
// basically as the k+chan_size user connects, if clients since k have not been served yet,
|
||||
// block to alleviate backpressure
|
||||
let (tx, rx) = mpsc::channel(4);
|
||||
|
||||
// spawn a background task to handle websocket listener
|
||||
tokio::spawn(async move {
|
||||
while let Ok((stream, addr)) = listener.accept().await {
|
||||
tracing::debug!("accepted connection from {addr}");
|
||||
|
||||
match tokio_tungstenite::accept_async(stream).await {
|
||||
Err(e) => tracing::error!("failed creating websocket stream for {addr}: {e}"),
|
||||
Ok(ws_stream) => {
|
||||
if let Err(e) = tx.send(ws_stream.split()).await {
|
||||
tracing::error!("could not serve {addr}: {e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Ok(rx)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl TransportStream for WSRx {
|
||||
async fn pop(&mut self) -> TransportResult<Option<c2s::s::Packet>> {
|
||||
// TODO this could forever, maybe add a timeout?
|
||||
loop {
|
||||
match self.try_next().await? {
|
||||
None => return Ok(None),
|
||||
Some(Message::Close(_frame)) => return Ok(None),
|
||||
Some(Message::Text(x)) => return Ok(Some(serde_json::from_str::<c2s::s::Packet>(&x)?)),
|
||||
x => tracing::debug!("websocket transport message: {x:?}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl TransportSink for WSTx {
|
||||
async fn push(&mut self, msg: c2s::c::Packet) -> TransportResult<()> {
|
||||
self.send(Message::Text(serde_json::to_string(&msg)?)).await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue