use std::future::Future;
use std::io;
use bytes::{Bytes, BytesMut};
use futures::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt};
use proxima_codec::{FrameCodec, FrameError, FrameLimits, LengthDelimitedCodec};
use proxima_primitives::pipe::SendPipe;
use proxima_primitives::sync::Mutex;
use crate::error::ProximaError;
const DEFAULT_READ_CHUNK: usize = 64 * 1024;
pub struct FramedClient<C> {
state: Mutex<FramedState<C>>,
}
struct FramedState<C> {
conn: C,
codec: LengthDelimitedCodec,
buf: BytesMut,
scratch: Vec<u8>,
out_frame: Vec<u8>,
}
impl<C: AsyncRead + AsyncWrite + Unpin> FramedClient<C> {
#[must_use]
pub fn new(conn: C) -> Self {
Self::with_limits(conn, FrameLimits::default())
}
#[must_use]
pub fn with_limits(conn: C, limits: FrameLimits) -> Self {
Self {
state: Mutex::new(FramedState {
conn,
codec: LengthDelimitedCodec::new(limits),
buf: BytesMut::with_capacity(DEFAULT_READ_CHUNK),
scratch: vec![0_u8; DEFAULT_READ_CHUNK],
out_frame: Vec::new(),
}),
}
}
pub async fn call(&self, payload: Bytes) -> Result<Bytes, ProximaError> {
self.round_trip(payload).await
}
async fn round_trip(&self, payload: Bytes) -> Result<Bytes, ProximaError> {
let mut state = self.state.lock().await;
state.send_frame(&payload).await?;
state.recv_frame().await
}
}
impl<C: AsyncRead + AsyncWrite + Unpin> FramedState<C> {
async fn send_frame(&mut self, payload: &[u8]) -> Result<(), ProximaError> {
self.out_frame.clear();
self.codec
.encode_frame(&payload, &mut self.out_frame)
.map_err(|err| {
ProximaError::Io(io::Error::new(io::ErrorKind::InvalidData, format!("{err}")))
})?;
self.conn
.write_all(&self.out_frame)
.await
.map_err(|err| ProximaError::Io(io::Error::other(format!("frame write: {err}"))))?;
self.conn
.flush()
.await
.map_err(|err| ProximaError::Io(io::Error::other(format!("frame flush: {err}"))))
}
async fn recv_frame(&mut self) -> Result<Bytes, ProximaError> {
loop {
match self.codec.parse_frame(&self.buf) {
Ok((_frame, consumed)) => {
let whole = self.buf.split_to(consumed).freeze();
return Ok(whole.slice(LengthDelimitedCodec::HEADER_BYTES..));
}
Err(FrameError::Incomplete) => {
let read = self.conn.read(&mut self.scratch).await.map_err(|err| {
ProximaError::Io(io::Error::other(format!("frame read: {err}")))
})?;
if read == 0 {
return Err(ProximaError::Io(io::Error::new(
io::ErrorKind::UnexpectedEof,
"connection closed mid-frame",
)));
}
self.buf.extend_from_slice(&self.scratch[..read]);
}
Err(err) => {
return Err(ProximaError::Io(io::Error::new(
io::ErrorKind::InvalidData,
format!("{err}"),
)));
}
}
}
}
}
impl<C: AsyncRead + AsyncWrite + Send + Unpin + 'static> SendPipe for FramedClient<C> {
type In = Bytes;
type Out = Bytes;
type Err = ProximaError;
fn call(&self, payload: Bytes) -> impl Future<Output = Result<Bytes, ProximaError>> + Send {
self.round_trip(payload)
}
}
#[cfg(all(feature = "tcp", feature = "tokio"))]
impl FramedClient<crate::listeners::TokioTcpConnection> {
pub async fn connect(addr: std::net::SocketAddr) -> Result<Self, ProximaError> {
let upstream = crate::upstreams::tokio_stream::TokioTcpUpstream::new(addr);
let conn = proxima_primitives::stream::StreamUpstreamExt::connect(&upstream)
.await
.map_err(ProximaError::Io)?;
Ok(Self::new(conn))
}
}
#[cfg(all(test, feature = "tcp", feature = "tokio"))]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use super::*;
use proxima_net::tokio::tokio_stream_listener::TokioTcpListener;
use proxima_primitives::stream::{StreamListener, StreamListenerExt};
use std::net::{Ipv4Addr, SocketAddr};
async fn spawn_echo_server(addr: SocketAddr) -> SocketAddr {
let listener = TokioTcpListener::bind(addr).await.expect("bind");
let local = match listener.local_addr().expect("local_addr") {
proxima_primitives::stream::BindAddr::Tcp(addr) => addr,
other => panic!("expected tcp, got {other:?}"),
};
tokio::spawn(async move {
let mut conn = listener.accept().await.expect("accept");
let mut buf = [0_u8; 4];
loop {
if conn.read_exact(&mut buf).await.is_err() {
return;
}
let len = u32::from_be_bytes(buf) as usize;
let mut payload = vec![0_u8; len];
conn.read_exact(&mut payload).await.expect("read payload");
conn.write_all(&buf).await.expect("write header");
conn.write_all(&payload).await.expect("write payload");
conn.flush().await.expect("flush");
}
});
local
}
#[proxima::test]
async fn framed_client_round_trips_against_a_raw_echo_server() {
let local = spawn_echo_server(SocketAddr::from((Ipv4Addr::LOCALHOST, 0))).await;
let client = FramedClient::connect(local).await.expect("connect");
let reply = client
.call(Bytes::from_static(b"ping"))
.await
.expect("call");
assert_eq!(reply, Bytes::from_static(b"ping"));
}
#[proxima::test]
async fn framed_client_multi_round_trips_three_sequential_calls_one_connection() {
let local = spawn_echo_server(SocketAddr::from((Ipv4Addr::LOCALHOST, 0))).await;
let client = FramedClient::connect(local).await.expect("connect");
for payload in [&b"one"[..], &b"two"[..], &b"three"[..]] {
let reply = client
.call(Bytes::copy_from_slice(payload))
.await
.expect("call");
assert_eq!(reply, Bytes::copy_from_slice(payload));
}
}
#[proxima::test]
async fn framed_client_wire_bytes_match_length_delimited_codec() {
let listener = TokioTcpListener::bind(SocketAddr::from((Ipv4Addr::LOCALHOST, 0)))
.await
.expect("bind");
let local = match listener.local_addr().expect("local_addr") {
proxima_primitives::stream::BindAddr::Tcp(addr) => addr,
other => panic!("expected tcp, got {other:?}"),
};
let capture = tokio::spawn(async move {
let mut conn = listener.accept().await.expect("accept");
let mut wire = vec![0_u8; 4 + b"hello wire".len()];
conn.read_exact(&mut wire).await.expect("read wire bytes");
wire
});
let client = FramedClient::connect(local).await.expect("connect");
tokio::spawn(async move {
let _ = client.call(Bytes::from_static(b"hello wire")).await;
});
let wire = capture.await.expect("capture task");
let mut expected = Vec::new();
LengthDelimitedCodec::default()
.encode_frame(&&b"hello wire"[..], &mut expected)
.expect("encode");
assert_eq!(wire, expected);
}
}