I'm trying to write a simple TCP based server using tokio where the server holds a list of Clients and the Clients have access to the Server. This is necessary so that the Server can check the Clients for something (for instance if a Client with a specific name exists) and so that Clients can call to server for these checks
After trying various approaches to make it work I eventually arrived at using Arc<Mutex<T>>
for storing the instances between each other which seems to be working fine though I'm not sure if that's the right choice here and I'd like to know if there's a better suited mechanism for it instead.
Furthermore this makes it impossible to send the Client task to tokio::spawn
as Mutex
cannot be "safely sent between threads". As an alternative, I tried using tokio::sync::Mutex
instead of std::sync::Mutex
which technically should work as it provides the Send trait, however it's inherently async meaning that I'd have to mark every function that accesses it as async even when the functions aren't necessarily async, which makes it a bit cumbersome.
I also realized that if I'm going to pass the mutex to tokio::spawn
, lock it, and call Client task on it, it will essentially hold the lock indefinitely so that makes me convinced my approach here is maybe inherently flawed and I should try something else.
Here's a relevant minimal example of what I'm trying to achieve (the troubling piece of code is commented out):
use std::sync::{Arc, Mutex};
use std::vec::Vec;
use tokio::io::{AsyncBufReadExt, BufReader};
use tokio::net::TcpListener;
use tokio::net::TcpStream;
struct Client {
pub name: String,
server: Arc<Mutex<Server>>,
}
impl Client {
pub fn new(name: String, server: Arc<Mutex<Server>>) -> Client {
Client {
name: name,
server: server,
}
}
pub async fn task(&mut self, mut stream: TcpStream) {
let (reader, _writer) = stream.split();
let mut line = String::new();
let mut buf_reader = BufReader::new(reader);
loop {
let size = buf_reader.read_line(&mut line).await.unwrap();
if size == 0 {
break;
} else {
if self.name.len() == 0 {
let server = self.server.lock().unwrap();
if server.is_name_used(line.clone()) {
println!("Name already in use.");
} else {
self.name = line.clone();
println!("Set name to: {}", self.name);
}
} else {
println!("Message: {}", line);
}
}
line.clear();
}
}
}
struct Server {
pub name: String,
clients: Vec<Arc<Mutex<Client>>>,
}
impl Server {
pub fn new() -> Server {
Server {
name: "Test".to_string(),
clients: Vec::new(),
}
}
pub async fn accept(self) -> Result<(), Box<dyn std::error::Error>> {
let server = Arc::new(Mutex::new(self));
let mut acceptor = TcpListener::bind("127.0.0.1:1234").await?;
loop {
let (stream, _) = acceptor.accept().await?;
let client = Arc::new(Mutex::new(Client::new("Test".to_string(), server.clone())));
/*let c = client.clone();
tokio::spawn(async move {
client.lock().unwrap().task(stream).await;
});*/
server.lock().unwrap().clients.push(client);
}
}
pub fn is_name_used(&self, name: String) -> bool {
self.clients
.iter()
.position(|c| c.lock().unwrap().name == name)
.is_some()
}
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
return Server::new().accept().await;
}
Any pointers what I could use here instead to achieve my goal?