Selecting from a variable number of tokio TcpStreams

I'm writing a small TCP game server with tokio. It accepts connections, manages them in a vec, and broadcasts game state periodically. Here's what that code looks like so far:

use std::error::Error;
use std::time::Duration;
use tokio::net::TcpListener;

#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {

    let mut tick_interval = tokio::time::interval(Duration::from_secs(1));
    let mut tcp_listener = TcpListener::bind("127.0.0.1:5000").await?;
    let mut streams = Vec::new();

    loop {
        tokio::select! {
            _ = tick_interval.tick() => println!("tick"),
            accept_result = tcp_listener.accept() => {
                match accept_result {
                    Ok((stream, sock_addr)) => streams.push(stream),
                    Err(err) => {
                        eprintln!("{}", err);
                        break
                    },
                }
            },
            // TODO How to await on a message being received from any one of our tcp streams?
        }
    }
    Ok(())
}

I'd like to add a third branch to the tokio::select! statement to read messages from each TcpStream and modify some shared state based on them, but I'm not sure of the best way to do that. My ideas are:

a) Spawn a task for each connection in the accept branch, as well as two channels to send and receive messages going between the tokio::select! loop and the task.

b) Merge the streams together and await on the result in a third branch in the tokio::select! statement.

b) seems way more elegant to me. tokio::stream::StreamExt::merge could do the trick, or if I map the streams into an iterator of futures::stream::Stream then I could use futures::stream::select_all or another combinator to do the trick.

Am I overlooking any obvious solutions, or are any of these proposals clearly better than the others?

Moving each connection to its own task is pretty much always better than trying to await them all in the same task. Generally the recommended design for network games with Tokio is to have a task that manages the game itself, and uses channels to talk to connections with the connections handling IO in a separate task. Generally you don't want to have game logic in the connection task — it would just forward messages coming from the game loop.

I have a small project lying around with this structure here, which you can check out for inspiration.

3 Likes

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.