├── example ├── README.md ├── Cargo.toml └── src │ └── main.rs ├── .gitignore ├── Cargo.toml ├── LICENSE └── src └── lib.rs /example/README.md: -------------------------------------------------------------------------------- 1 | Run `cargo build` to build the example, then spin up a server with `./target/debug/chat --serve` and add a new client with `./target/debug/chat`. 2 | -------------------------------------------------------------------------------- /example/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "chat" 3 | version = "0.0.1" 4 | authors = ["Damien Radtke "] 5 | 6 | [dependencies.lobby] 7 | path = ".." 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled files 2 | *.o 3 | *.so 4 | *.rlib 5 | *.dll 6 | 7 | # Executables 8 | *.exe 9 | 10 | # Generated by Cargo 11 | /target/ 12 | 13 | Cargo.lock 14 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "lobby" 3 | version = "0.0.1" 4 | authors = ["Damien Radtke "] 5 | 6 | description = "TCP server implementation suitable for chat rooms and games." 7 | repository = "https://github.com/dradtke/lobby" 8 | documentation = "http://www.rustdox.com/dradtke/lobby/lobby/" 9 | license = "MIT" 10 | 11 | keywords = ["tcp", "server", "lobby", "game", "networking"] 12 | 13 | [dependencies] 14 | vec_map = "0.6.0" 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Damien Radtke 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /example/src/main.rs: -------------------------------------------------------------------------------- 1 | extern crate lobby; 2 | 3 | use lobby::{Lobby, ScanResult}; 4 | use std::env::args; 5 | use std::io::{self, BufReader, BufRead, Write}; 6 | use std::net::TcpStream; 7 | use std::thread; 8 | 9 | const ADDR: &'static str = "127.0.0.1:8080"; 10 | 11 | /// Run the server. 12 | fn server_main() { 13 | let lobby = match Lobby::new(ADDR) { 14 | Ok(lobby) => lobby, 15 | Err(e) => { println!("{}", e); return; }, 16 | }; 17 | 18 | println!("Chatroom started, waiting on client connections."); 19 | 20 | loop { 21 | // Scan connected clients for input or updates. 22 | lobby.scan(|id, result| match result { 23 | ScanResult::Connected => println!("{} has connected.", lobby.name(id).unwrap()), 24 | ScanResult::Data(data) => { 25 | let msg = String::from_utf8(data).unwrap(); 26 | let name = lobby.name(id).unwrap(); 27 | lobby.message_rest(id, format!("{}: {}\n", name, msg).as_bytes()); 28 | }, 29 | ScanResult::IoError(err) => println!("io error: {}", err), 30 | ScanResult::Disconnected => println!("{} has disconnected.", lobby.name(id).unwrap()), 31 | }); 32 | } 33 | } 34 | 35 | /// Connect to the server. 36 | fn client_main() { 37 | println!("Connecting..."); 38 | let mut stdout = io::stdout(); 39 | 40 | match TcpStream::connect(ADDR) { 41 | Ok(mut stream) => { 42 | stdout.write("Name: ".as_bytes()).unwrap(); 43 | stdout.flush().unwrap(); 44 | 45 | let mut lines = BufReader::new(io::stdin()).lines(); 46 | 47 | match lines.next() { 48 | Some(line) => if let Ok(line) = line { 49 | stream.write_all(line.trim_right().as_bytes()).unwrap(); 50 | stream.write(&[0]).unwrap(); 51 | stream.flush().unwrap(); 52 | }, 53 | None => return, 54 | } 55 | 56 | { 57 | // Read messages from other clients and print them out. 58 | let stream = stream.try_clone().unwrap(); 59 | thread::spawn(move || { 60 | for line in BufReader::new(stream).lines() { 61 | println!("{}", line.unwrap()); 62 | } 63 | }); 64 | } 65 | 66 | for line in lines { 67 | if let Ok(line) = line { 68 | if let Err(e) = stream.write_all(line.trim_right().as_bytes()) { 69 | println!("{}", e); 70 | break; 71 | } 72 | } 73 | } 74 | }, 75 | Err(e) => println!("{}", e), 76 | } 77 | } 78 | 79 | fn main() { 80 | match args().skip(1).next() { 81 | None => client_main(), 82 | Some(ref arg) if arg == "--serve" => server_main(), 83 | Some(arg) => panic!("unexpected argument: {}", arg), 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! TCP server implementation suitable for chat rooms and games. 2 | //! 3 | //! The `lobby` crate is designed to provide a convenient way to spin up a server 4 | //! which automatically keeps track of connected clients and provides facilities 5 | //! for messaging them and polling them for data. 6 | //! 7 | //! Each client is represented using a unique integer id, and id's are recycled 8 | //! as clients disconnect from the server. 9 | //! 10 | //! Here's how you spin up a `lobby` server and poll the clients for data: 11 | //! 12 | //! ``` 13 | //! extern crate lobby; 14 | //! use lobby::{Lobby, ScanResult}; 15 | //! 16 | //! let server = Lobby::new("127.0.0.1:8080").unwrap(); 17 | //! 18 | //! loop { 19 | //! server.scan(|id, result| { 20 | //! let name = server.name(id).unwrap(); 21 | //! match result { 22 | //! ScanResult::Connected => println!("{} has connected.", name), 23 | //! ScanResult::Data(data) => println!("{} sent {} bytes of data.", name, data.len()), 24 | //! ScanResult::IoError(err) => println!("{} ran into an IO error: {}", name, err), 25 | //! ScanResult::Disconnected => println!("{} has disconnected.", name), 26 | //! } 27 | //! }); 28 | //! } 29 | //! ``` 30 | //! 31 | //! Clients can then connect to the server using `TcpStream::connect()`. The first 32 | //! thing a client should do after establishing the connection is send a UTF-8 encoded 33 | //! name followed by a 0 byte to indicate its termination. After that, all further 34 | //! data sent will be queued up to be scanned by the server. 35 | #![allow(dead_code)] 36 | 37 | extern crate vec_map; 38 | 39 | use std::collections::VecDeque; 40 | use std::io::{self, BufRead, Write, BufReader}; 41 | use std::net::{TcpListener, TcpStream, ToSocketAddrs}; 42 | use std::thread::{self, JoinHandle}; 43 | use std::sync::{Arc, Mutex}; 44 | use std::sync::mpsc::{channel, Receiver, TryRecvError}; 45 | 46 | use vec_map::VecMap; 47 | 48 | type ClientConn = (Receiver>>, TcpStream); 49 | 50 | /// A Lobby server instance. 51 | pub struct Lobby { 52 | listener: TcpListener, 53 | connections: Arc>>, 54 | names: Arc>>, 55 | new_r: Receiver, 56 | thread: JoinHandle<()>, 57 | } 58 | 59 | impl Lobby { 60 | /// Create a new Lobby server at the specified address. 61 | /// 62 | /// Creating a Lobby will spawn a new thread listening for incoming connections, 63 | /// plus an additional thread for each connection. The first thing any new 64 | /// client should send is a UTF-8 encoded string followed by a 0 byte to indicate 65 | /// its termination, which will serve as the name associated with this connection. 66 | /// Note that this is not necessarily a unique identifier. 67 | /// 68 | /// Any additional data sent by the client will need to be processed via the `scan()` 69 | /// method. 70 | pub fn new(addr: A) -> io::Result where A: ToSocketAddrs { 71 | let listener = try!(TcpListener::bind(&addr)); 72 | let connections = Arc::new(Mutex::new(VecMap::new())); 73 | let names = Arc::new(Mutex::new(VecMap::new())); 74 | let (new_s, new_r) = channel(); 75 | 76 | let thread = { 77 | let listener = listener.try_clone().unwrap(); 78 | let connections = connections.clone(); 79 | let names = names.clone(); 80 | 81 | thread::spawn(move || { 82 | let mut id = 0; 83 | let free_ids = Arc::new(Mutex::new(VecDeque::new())); 84 | for conn in listener.incoming() { 85 | if let Ok(conn) = conn { 86 | let free_ids = free_ids.clone(); 87 | let new_id = match free_ids.lock().unwrap().pop_front() { 88 | Some(id) => id, 89 | None => { id += 1; id }, 90 | }; 91 | 92 | let conn_reader = conn.try_clone().unwrap(); 93 | let (ds, dr) = channel(); 94 | let new_s = new_s.clone(); 95 | let names = names.clone(); 96 | 97 | thread::spawn(move || { 98 | let mut reader = BufReader::new(conn_reader); 99 | let mut name_buf = Vec::new(); 100 | let my_id = new_id; 101 | 102 | match reader.read_until(0, &mut name_buf) { 103 | Ok(_) => { 104 | name_buf.pop(); // remove the delimiting 0 105 | names.lock().unwrap().insert(my_id, String::from_utf8(name_buf).unwrap()); 106 | new_s.send(new_id).unwrap(); 107 | }, 108 | Err(_) => { 109 | drop(ds); 110 | free_ids.lock().unwrap().push_back(my_id); 111 | return; 112 | }, 113 | } 114 | 115 | loop { 116 | let result = match reader.fill_buf() { 117 | Ok(data) if data.len() == 0 => Some(0), 118 | Ok(data) => { ds.send(Ok(data.to_vec())).unwrap(); Some(data.len()) }, 119 | Err(e) => { ds.send(Err(e)).unwrap(); None }, 120 | }; 121 | 122 | if let Some(read) = result { 123 | if read > 0 { 124 | reader.consume(read); 125 | } else { 126 | drop(ds); 127 | free_ids.lock().unwrap().push_back(my_id); 128 | break; 129 | } 130 | } 131 | } 132 | }); 133 | 134 | connections.lock().unwrap().insert(new_id, (dr, conn)); 135 | } 136 | } 137 | }) 138 | }; 139 | 140 | Ok(Lobby{ 141 | listener: listener, 142 | connections: connections, 143 | names: names, 144 | new_r: new_r, 145 | thread: thread, 146 | }) 147 | } 148 | 149 | /// Send a message to all connected clients. 150 | /// 151 | /// Returns a list of tuples pairing the id of each client that ran into an IO error with 152 | /// the error itself. 153 | pub fn message_all(&self, data: &[u8]) -> Vec<(usize, io::Error)> { 154 | self.message(|_| true, data) 155 | } 156 | 157 | /// Send a message to a single client. 158 | /// 159 | /// Returns a list of tuples pairing the id of each client that ran into an IO error with 160 | /// the error itself. 161 | pub fn message_client(&self, client: usize, data: &[u8]) -> Vec<(usize, io::Error)> { 162 | self.message(|id| id == client, data) 163 | } 164 | 165 | /// Send a message to every client but one. Useful for, e.g., one client messaging the others. 166 | /// 167 | /// Returns a list of tuples pairing the id of each client that ran into an IO error with 168 | /// the error itself. 169 | pub fn message_rest(&self, client: usize, data: &[u8]) -> Vec<(usize, io::Error)> { 170 | self.message(|id| id != client, data) 171 | } 172 | 173 | /// Send a message to every connected client for which `predicate` returns true. 174 | /// 175 | /// Returns a list of tuples pairing the id of each client that ran into an IO error with 176 | /// the error itself. 177 | pub fn message

(&self, predicate: P, data: &[u8]) -> Vec<(usize, io::Error)> where P: Fn(usize) -> bool { 178 | let mut failed = Vec::new(); 179 | for (id, &mut (_, ref mut conn)) in self.connections.lock().unwrap().iter_mut().filter(|&(id, _)| predicate(id)) { 180 | if let Err(e) = conn.write_all(data) { 181 | failed.push((id, e)); 182 | } 183 | } 184 | failed 185 | } 186 | 187 | /// Scan the clients' message queues for data. 188 | /// 189 | /// Note that the callback is only invoked if there is something to report, and that 190 | /// this method does not block. Most applications will want to wrap this call up 191 | /// in their main loop in order to continuously process data. 192 | pub fn scan ()>(&self, callback: F) { 193 | loop { 194 | match self.new_r.try_recv() { 195 | Ok(id) => callback(id, ScanResult::Connected), 196 | Err(e) if e == TryRecvError::Empty => break, 197 | Err(e) if e == TryRecvError::Disconnected => { 198 | panic!("tried to check for new clients on disconnected channel!"); 199 | }, 200 | Err(_) => unimplemented!(), 201 | } 202 | } 203 | 204 | let mut results = Vec::with_capacity(self.connections.lock().unwrap().len()); 205 | 206 | for (id, &mut (ref mut dr, _)) in self.connections.lock().unwrap().iter_mut() { 207 | match dr.try_recv() { 208 | Ok(Ok(data)) => results.push((id, ScanResult::Data(data))), 209 | Ok(Err(err)) => results.push((id, ScanResult::IoError(err))), 210 | Err(TryRecvError::Empty) => {}, // do nothing 211 | Err(TryRecvError::Disconnected) => results.push((id, ScanResult::Disconnected)), 212 | } 213 | } 214 | 215 | for (id, result) in results.into_iter() { 216 | if let ScanResult::Disconnected = result { 217 | self.connections.lock().unwrap().remove(id); 218 | } 219 | callback(id, result); 220 | } 221 | } 222 | 223 | /// Get the registered name for a given client. 224 | pub fn name(&self, client: usize) -> Option { 225 | self.names.lock().unwrap().get(client).map(|s| s.clone()) 226 | } 227 | } 228 | 229 | /// The result of a client scan. 230 | pub enum ScanResult { 231 | /// The client sent data. 232 | Data(Vec), 233 | /// An IO error occurred while scanning. 234 | IoError(io::Error), 235 | /// A new client has connected. 236 | Connected, 237 | /// A client has disconnected. 238 | Disconnected, 239 | } 240 | --------------------------------------------------------------------------------