Part 1 of 1
A Server With Nobody to Talk To
Open the First TCP Connection in Rust

A multiplayer server has one awkward requirement before it can become multiplayer.
Someone has to connect.
We are going to start with two small Rust programs. The server waits on 127.0.0.1:4000. The client connects, reads one welcome message and closes. By the end, two terminals will have spoken to each other through a real TCP connection.
No rooms. No players. Barely a conversation. But it crosses the wall.
This is a sequel in skills, not source code. We are starting a separate project instead of adding a server to Rustbound Idle. If you want to compare your result after building it, the article_1 branch holds this lesson's tested checkpoint.
Create the project#
Create one Cargo package with two binaries:
cargo new one-world-many-players
cd one-world-many-players
mkdir src/bin
Remove src/main.rs, then create this shape:
one-world-many-players/
├── Cargo.toml
└── src/
└── bin/
├── client.rs
└── server.rs
Cargo treats every file in src/bin/ as a separate executable. We can now run the server and client without creating two projects.
Listen for one connection#
Put this in src/bin/server.rs:
use std::io::{self, Write};
use std::net::TcpListener;
fn main() -> io::Result<()> {
let listener = TcpListener::bind("127.0.0.1:4000")?;
println!("Server listening on 127.0.0.1:4000");
let (mut stream, address) = listener.accept()?;
println!("Connected: {address}");
writeln!(stream, "Welcome to One World")?;
Ok(())
}
TcpListener::bind() asks the operating system to reserve that address for our process. It can fail if another program already owns the port, so it returns a Result.
The ? after the call means:
If this worked, give us the listener. If it failed, return the error from
main().
That works because main() returns io::Result<()>. We are not hiding the failure. We are letting the function that can report it do exactly that.
accept() waits until a connection arrives. When one does, it gives us a TcpStream and the remote address. The stream is the connection. We write one newline-terminated message into it with writeln!.
Connect from the other terminal#
Put this in src/bin/client.rs:
use std::io::{self, BufRead, BufReader};
use std::net::TcpStream;
fn main() -> io::Result<()> {
let stream = TcpStream::connect("127.0.0.1:4000")?;
let mut reader = BufReader::new(stream);
let mut message = String::new();
reader.read_line(&mut message)?;
print!("{message}");
Ok(())
}
TcpStream::connect() reaches the address the server reserved. TCP gives us bytes, not messages, so the client wraps the stream in a BufReader and reads until the newline the server sent.
Start the server in one terminal:
cargo run --bin server
Leave it waiting. Open a second terminal in the same folder and run:
cargo run --bin client
The client prints the welcome line. The server prints the connection address, then both programs end.
What we learned#
- one Cargo package can contain several binaries
TcpListenerwaits for incoming connectionsTcpStreamrepresents one TCP connectionBufReadercan read a newline-terminated message from a byte stream?passes an error to a function that already knows how to return it
Our connection works once.
That is also exactly how many messages it can carry.
One connection. Shared world.
Comments require JavaScript.