|
| 1 | +use tokio::io::{BufReader, BufWriter}; |
| 2 | +use tokio::net::TcpStream; |
| 3 | + |
| 4 | +use crate::protocol; |
| 5 | +use crate::types::{ClientWire, ServerEvent, ServerWire, SyncError}; |
| 6 | + |
| 7 | +/// A connected relay client. |
| 8 | +pub struct Client { |
| 9 | + reader: BufReader<tokio::net::tcp::OwnedReadHalf>, |
| 10 | + writer: BufWriter<tokio::net::tcp::OwnedWriteHalf>, |
| 11 | + max_payload: usize, |
| 12 | +} |
| 13 | + |
| 14 | +impl Client { |
| 15 | + /// Connect to a relay server. |
| 16 | + pub async fn connect(addr: &str) -> Result<Self, SyncError> { |
| 17 | + Self::builder().connect(addr).await |
| 18 | + } |
| 19 | + |
| 20 | + /// Start building a client. |
| 21 | + pub fn builder() -> ClientBuilder { |
| 22 | + ClientBuilder::new() |
| 23 | + } |
| 24 | + |
| 25 | + /// Join a room. If room does not exist, returns [`SyncError::RoomNotFound`] |
| 26 | + pub async fn join(&mut self, room_id: &str) -> Result<(), SyncError> { |
| 27 | + let msg = ClientWire::JoinRoom { |
| 28 | + room_id: room_id.into(), |
| 29 | + }; |
| 30 | + self.send(&msg).await |
| 31 | + } |
| 32 | + |
| 33 | + /// Leave the current room. |
| 34 | + pub async fn leave(&mut self) -> Result<(), SyncError> { |
| 35 | + self.send(&ClientWire::LeaveRoom).await |
| 36 | + } |
| 37 | + |
| 38 | + /// Send a ping (keep-alive). |
| 39 | + pub async fn ping(&mut self) -> Result<(), SyncError> { |
| 40 | + self.send(&ClientWire::Ping).await |
| 41 | + } |
| 42 | + |
| 43 | + /// Broadcast raw bytes to all peers in the current room. |
| 44 | + pub async fn broadcast(&mut self, data: &[u8]) -> Result<(), SyncError> { |
| 45 | + let msg = ClientWire::Broadcast { |
| 46 | + data: data.to_vec(), |
| 47 | + }; |
| 48 | + self.send(&msg).await |
| 49 | + } |
| 50 | + |
| 51 | + /// Receive the next server event. |
| 52 | + /// Ping/pong keepalive is handled internally — the user never sees it. |
| 53 | + /// Returns `Ok(None)` on clean disconnect. |
| 54 | + pub async fn recv(&mut self) -> Result<Option<ServerEvent>, SyncError> { |
| 55 | + loop { |
| 56 | + let payload = match protocol::read_frame_raw(&mut self.reader, self.max_payload).await { |
| 57 | + Ok(p) => p, |
| 58 | + Err(ref e) if e.is_connection_closed() => { |
| 59 | + return Ok(None); |
| 60 | + } |
| 61 | + Err(e) => return Err(e), |
| 62 | + }; |
| 63 | + |
| 64 | + let wire: ServerWire = wincode::deserialize(&payload) |
| 65 | + .map_err(|e| SyncError::Protocol(format!("deserialize failed: {:?}", e)))?; |
| 66 | + |
| 67 | + // Internal: auto-respond to server pings |
| 68 | + if matches!(wire, ServerWire::Ping) { |
| 69 | + self.send(&ClientWire::Pong).await?; |
| 70 | + continue; |
| 71 | + } |
| 72 | + |
| 73 | + // Internal: swallow pong responses |
| 74 | + if matches!(wire, ServerWire::Pong) { |
| 75 | + continue; |
| 76 | + } |
| 77 | + |
| 78 | + return Ok(Some(Self::wire_to_event(wire))); |
| 79 | + } |
| 80 | + } |
| 81 | + |
| 82 | + async fn send(&mut self, msg: &ClientWire) -> Result<(), SyncError> { |
| 83 | + protocol::write_frame(&mut self.writer, msg).await |
| 84 | + } |
| 85 | + |
| 86 | + fn wire_to_event(wire: ServerWire) -> ServerEvent { |
| 87 | + match wire { |
| 88 | + ServerWire::Joined { client_id, room_id } => ServerEvent::Joined { client_id, room_id }, |
| 89 | + ServerWire::PlayerJoined { client_id } => ServerEvent::PlayerJoined { client_id }, |
| 90 | + ServerWire::PlayerLeft { client_id } => ServerEvent::PlayerLeft { client_id }, |
| 91 | + ServerWire::Error(msg) => ServerEvent::Error(msg), |
| 92 | + ServerWire::Broadcast { sender_id, data } => ServerEvent::Broadcast { sender_id, data }, |
| 93 | + // Ping/Pong are handled internally before reaching here |
| 94 | + _ => unreachable!("ping/pong should be handled in recv()"), |
| 95 | + } |
| 96 | + } |
| 97 | +} |
| 98 | + |
| 99 | +/// Builder for configuring a [`Client`]. |
| 100 | +pub struct ClientBuilder { |
| 101 | + max_payload: usize, |
| 102 | +} |
| 103 | + |
| 104 | +impl ClientBuilder { |
| 105 | + pub fn new() -> Self { |
| 106 | + Self { |
| 107 | + max_payload: 64 * 1024, |
| 108 | + } |
| 109 | + } |
| 110 | +} |
| 111 | + |
| 112 | +impl Default for ClientBuilder { |
| 113 | + fn default() -> Self { |
| 114 | + Self::new() |
| 115 | + } |
| 116 | +} |
| 117 | + |
| 118 | +impl ClientBuilder { |
| 119 | + /// Set max payload size for incoming frames. |
| 120 | + pub fn max_payload(mut self, n: usize) -> Self { |
| 121 | + self.max_payload = n; |
| 122 | + self |
| 123 | + } |
| 124 | + |
| 125 | + /// Connect to the server. |
| 126 | + pub async fn connect(self, addr: &str) -> Result<Client, SyncError> { |
| 127 | + let stream = TcpStream::connect(addr).await.map_err(|e| { |
| 128 | + if e.kind() == std::io::ErrorKind::ConnectionRefused { |
| 129 | + SyncError::ConnectionRefused |
| 130 | + } else { |
| 131 | + SyncError::Io(e) |
| 132 | + } |
| 133 | + })?; |
| 134 | + let (read_half, write_half) = stream.into_split(); |
| 135 | + |
| 136 | + Ok(Client { |
| 137 | + reader: BufReader::new(read_half), |
| 138 | + writer: BufWriter::new(write_half), |
| 139 | + max_payload: self.max_payload, |
| 140 | + }) |
| 141 | + } |
| 142 | +} |
0 commit comments