๐ฆ๐ Building the Network Architecture of an Open-Source MMORPG with Rust
When building a multiplayer game, one of the first systems that has to become reliable is the network layer.
For The Last Signal Online, an open-source post-apocalyptic MMORPG I'm building, the server is written in Rust and communicates with clients through a custom TCP protocol.
The current networking layer is still under development, but it already has several important components:
- a TCP server built with Tokio
- asynchronous client handling
- a custom binary packet format
- packet parsing and validation
- packet routing
- authentication and account management
- SQLite integration
- session management
- periodic ban verification
- structured client logging
This article explains how the current architecture works and what I am working toward next.
๐๏ธ Current Architecture
The networking code is organized into five main modules:
server_rust/src/network/
โโโ packet.rs
โโโ client.rs
โโโ server.rs
โโโ handler.rs
โโโ parser.rs
Each module currently has a specific responsibility.
โโโโโโโโโโโโโโโโโโโโโโโ
โ TCP Client โ
โโโโโโโโโโโโฌโโโโโโโโโโโ
โ
โ TCP
โผ
โโโโโโโโโโโโโโโโโโโโโโโ
โ Server โ
โ TcpListener โ
โโโโโโโโโโโโฌโโโโโโโโโโโ
โ
creates Client
โ
โผ
โโโโโโโโโโโโโโโโโโโโโโโ
โ Client โ
โ session + socket โ
โโโโโโโโโโโโฌโโโโโโโโโโโ
โ
receive_packet()
โ
โผ
โโโโโโโโโโโโโโโโโโโโโโโ
โ Packet โ
โโโโโโโโโโโโฌโโโโโโโโโโโ
โ
โผ
โโโโโโโโโโโโโโโโโโโโโโโ
โ PacketHandler โ
โโโโโโโโโโโโฌโโโโโโโโโโโ
โ
โโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโ
โผ โผ โผ
Parser SQLite Response
The goal is to keep the low-level TCP handling separate from packet processing and application logic.
๐ 1. The TCP Server
The entry point for incoming network connections is the Server structure.
It contains:
pub struct Server {
listener: TcpListener,
database: DatabaseManager,
}
The server creates a Tokio TcpListener and binds it to the configured address.
Once started, it continuously waits for incoming connections:
loop {
match self.listener.accept().await {
Ok((stream, address)) => {
// ...
}
Err(e) => {
// ...
}
}
}
When a client connects, the server obtains:
- the TCP stream
- the client's socket address
It then creates a new Client instance.
โก 2. One Asynchronous Task per Client
The current implementation uses Tokio tasks to handle clients independently.
When a connection is accepted:
task::spawn(async move {
let mut client = Client::new(stream, pool);
client.run().await;
});
This means the main listener can immediately return to accepting other connections.
Conceptually:
TCP Listener
โ
โโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโ
โผ โผ โผ
Client A Client B Client C
โ โ โ
Tokio task Tokio task Tokio task
โ โ โ
run() run() run()
This is important for a multiplayer game because the server cannot block while waiting for a single player.
๐ค 3. The Client Session
Each connected player is represented by a Client.
The structure currently stores:
pub struct Client {
stream: TcpStream,
pool: SqlitePool,
session_id: Uuid,
client_id: Option<i64>,
user_id: Option<String>,
account_id: Option<i64>,
}
This gives the networking layer both the connection itself and some session state.
The session_id is generated when the Client is created:
session_id: Uuid::new_v4(),
This provides a unique identifier for the network session independently from the database user ID.
The client can also progressively acquire information during authentication:
TCP connection
โ
โผ
Client created
โ
โโโ session_id
โโโ client_id = None
โโโ user_id = None
โโโ account_id = None
โ
โ login/signup
โผ
session becomes
associated with
a user
๐ 4. The Client Event Loop
Once a client is created, Client::run() becomes responsible for the connection.
The interesting part is the use of:
tokio::select!
The client is currently waiting for two kinds of events:
- an incoming network packet
- a periodic ban-check timer
Conceptually:
Client::run()
โ
tokio::select!
/ \
/ \
Network packet Ban timer
โ โ
โผ โผ
receive_packet() get_ban_info()
โ โ
โผ โผ
PacketHandler BAN packet
โ โ
โผ โผ
Response disconnect
This allows the connection to remain responsive while the server performs periodic session checks.
๐ฆ 5. The Packet Format
The protocol currently uses a small custom binary framing format.
Every packet contains:
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Packet size โ 4 bytes โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ Packet type โ 2 bytes โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ Payload โ variable โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
The packet size is encoded as a big-endian u32.
The packet type is encoded as a big-endian u16.
The remaining bytes are the payload.
For example:
[ 4-byte size ]
[ 2-byte type ]
[ payload... ]
This framing allows the receiver to know exactly how many bytes belong to the current packet before processing it.
๐งฉ 6. Packet Types
The current protocol defines several packet types:
pub enum PacketType {
Ping = 1,
Login = 2,
Chat = 3,
Move = 4,
Log = 5,
SignUp = 6,
LoginResponse = 7,
SignUpResponse = 8,
BAN = 9,
}
The protocol therefore already covers several fundamental systems:
| Packet | Current purpose |
|---|---|
Ping |
Connection/liveness test |
Login |
Authentication |
Chat |
Chat messages |
Move |
Player movement |
Log |
Client-side logging |
SignUp |
Account creation |
LoginResponse |
Server login response |
SignUpResponse |
Server signup response |
BAN |
Server-side ban notification |
The conversion between the numeric protocol value and the Rust enum is handled by PacketType::from_u16().
Unknown packet types are rejected.
๐ก๏ธ 7. Packet Size Validation
The server currently defines:
pub const MAX_PACKET_SIZE: usize = 10 * 1024 * 1024;
Both sending and receiving enforce this maximum.
A packet smaller than the two bytes required for the packet type is also rejected.
This gives the current framing layer some basic protection against malformed or unexpectedly large frames.
The receiver follows this process:
Read 4 bytes
โ
โผ
Decode packet size
โ
โโโ size < 2 โโโโโโโโบ reject
โ
โโโ size > 10 MiB โโโบ reject
โ
โผ
Read exactly `size` bytes
โ
โผ
Read packet type
โ
โโโ unknown โโโโโโโโโบ reject
โ
โผ
Extract payload
The use of read_exact() is particularly useful here because the server explicitly requests the number of bytes required for the current frame.
๐ 8. Parsing Login and Signup Payloads
Packets provide the outer framing.
The parser then interprets the payload for specific packet types.
For login and signup, the current payload format is:
โโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Email length โ 2 bytes โ
โโโโโโโโโโโโโโโโโโโโโโโโโโค
โ Email โ variableโ
โโโโโโโโโโโโโโโโโโโโโโโโโโค
โ Password len โ 2 bytes โ
โโโโโโโโโโโโโโโโโโโโโโโโโโค
โ Password โ variableโ
โโโโโโโโโโโโโโโโโโโโโโโโโโ
Both lengths are stored as big-endian u16 values.
The parser performs several validation steps:
- checks that the length field exists
- checks that the declared number of bytes exists
- converts the data from UTF-8
- returns an error when the payload is malformed
For example, an incomplete email is rejected before the server attempts to interpret it.
This keeps payload parsing separate from packet routing.
๐ง 9. Packet Routing
Once a packet has been decoded, it is passed to:
PacketHandler::handle(...)
The handler matches on the packet type.
Conceptually:
Packet
โ
โผ
match packet.packet_type
โ
โโโ Ping
โโโ Login
โโโ SignUp
โโโ Chat
โโโ Move
โโโ Log
โโโ BAN
โโโ Responses
This is currently the main bridge between the networking layer and the rest of the server.
๐ 10. Ping / Pong
The simplest packet currently implemented is Ping.
When the server receives it, the handler creates another packet:
Packet::new(
PacketType::Ping,
b"PONG".to_vec(),
)
The response is then sent back through the same TCP connection.
So the current flow is:
Client
โ
โ PING
โผ
Server
โ
โ PacketHandler
โผ
PONG
โ
โผ
Client
This provides a simple way to test basic connectivity.
๐ 11. Authentication
The networking layer is already connected to the authentication system.
For a LOGIN packet, the handler:
- parses the payload
- searches for the user in SQLite
- checks permanent and temporary bans
- verifies the password hash
- tracks failed login attempts
- applies the temporary ban mechanism when necessary
- checks whether the account is already connected
- marks the user as connected
- associates the authenticated user with the
Client - returns a login response
The password verification itself is delegated to the project's password utility.
The handler therefore acts as the current integration point between:
Network protocol
โ
โผ
Login parser
โ
โผ
Authentication
โ
โโโ SQLite
โโโ password verification
โโโ ban system
โโโ session state
๐ซ 12. Failed Login Attempts and Temporary Bans
The current login system also keeps track of failed attempts.
After three failed attempts, the server can create or extend a temporary ban.
There is also a banssursis mechanism in the current database logic that can modify the duration of the resulting ban.
This means authentication is not simply:
password correct โ login
password wrong โ reject
There is already a larger state machine:
Login attempt
โ
โผ
Find user
โ
โโโ not found โโโโโโโโโโบ reject
โ
โผ
Check bans
โ
โโโ banned โโโโโโโโโโโโโบ reject
โ
โผ
Verify password
โ
โโโ incorrect
โ โ
โ โผ
โ increment attempts
โ โ
โ โโโ < 3 โโโโโโโโบ reject
โ โ
โ โโโ โฅ 3 โโโโโโโโบ temporary ban
โ
โผ
Check connected status
โ
โโโ already connected โโบ reject
โ
โผ
Mark CONNECTED
โ
โผ
Associate user with Client
This is one of the areas where the network layer is already interacting heavily with persistent server state.
๐ 13. Client Logging
The protocol also contains a Log packet.
The client can send structured logging information containing:
pub struct ClientLog {
pub level: LogLevel,
pub module: String,
pub file: String,
pub line: u32,
pub message: String,
}
The server deserializes this data using serde_json.
Depending on the received level, it forwards the message to the server's logging system.
Supported levels currently include:
TRACE
DEBUG
INFO
WARNING
ERROR
This gives the client a mechanism for reporting structured diagnostic information to the server.
๐ฌ 14. Chat and Movement
The protocol already defines Chat and Move.
At the current stage, their handling is intentionally simple.
For chat:
Packet::new(
PacketType::Chat,
packet.payload,
)
The server currently logs the received message and sends the payload back.
Movement currently follows a similar basic path.
This is important because these packet types are part of the current protocol, but their gameplay functionality is not yet a complete MMORPG networking system.
The architecture is being built before the full gameplay layer is implemented.
โ 15. Server-Only Responses
The handler also explicitly rejects packets that should only originate from the server.
For example:
Client โ LoginResponse
Client โ SignUpResponse
are considered invalid directions.
The server logs the situation and does not generate a response.
This establishes an important concept for the protocol:
Not every packet type is valid in both directions.
As the protocol grows, this distinction will become increasingly important.
๐จ 16. Signup
Account creation follows a similar path to login:
SIGN_UP packet
โ
โผ
parse_signup_payload()
โ
โผ
hash password
โ
โผ
generate UUID
โ
โผ
INSERT INTO users
โ
โผ
associate user with Client
โ
โผ
SIGN_UP response
The password is hashed before being stored.
The database also handles duplicate email detection.
๐ 17. Session Disconnection
When a client disconnects, the Client performs cleanup.
If the session has an authenticated user, the server updates the database:
CONNECTED
โ
โ disconnect
โผ
DISCONNECTED
The socket can then be shut down cleanly.
This means the network session and persistent user state are connected.
โฑ๏ธ 18. Periodic Ban Checking
One interesting part of the current architecture is that ban checking does not only happen during login.
The client starts a Tokio interval:
interval(Duration::from_secs(1))
Once a player is authenticated, the server periodically checks whether a ban has become active.
If a ban is detected:
Database
โ
โผ
Ban detected
โ
โผ
Create BAN packet
โ
โผ
Send to client
โ
โผ
Disconnect
This means an already-connected player can be removed from the server if their account becomes banned while their session is active.
๐ 19. What About Network Security?
The current networking code is intentionally still an evolving part of the project.
The code described in this article provides:
- packet framing
- packet size limits
- packet type validation
- payload validation
- authentication
- password hashing
- ban handling
- session tracking
However, this should not be interpreted as a finished secure production protocol.
For example, the networking files shown here implement TCP framing, but they do not themselves provide transport encryption such as TLS.
The project also has experimental cryptography work elsewhere, but that should not be confused with the packet framing layer described here.
Security is an ongoing area of development and review.
๐งช 20. What I Want to Improve
The current architecture is a foundation rather than the final networking system.
Some of the areas I want to develop further include:
Better protocol validation
More malformed and invalid packet tests are needed.
Examples include:
- invalid packet sizes
- unknown packet types
- truncated payloads
- invalid UTF-8
- invalid field lengths
- unexpected packet directions
Network integration tests
I want to test the complete path:
Client
โ
TCP
โ
Server
โ
Parser
โ
Handler
โ
Database
โ
Response
rather than testing each component only in isolation.
Better packet abstractions
As the game grows, the number of packet types will increase significantly.
The protocol therefore needs to remain easy to extend without turning the handler into an unmaintainable collection of special cases.
Gameplay networking
Movement and chat are currently basic.
The future network layer will need to support much more complex game state:
- player movement
- world state
- combat
- inventories
- NPCs
- interactions
- guilds
- territories
- trading
- persistent world events
๐บ๏ธ The Bigger Picture
The current architecture can be summarized as:
THE LAST SIGNAL
NETWORK LAYER
TCP
โ
โผ
โโโโโโโโโโโโโโโโโโโโโโ
โ Server โ
โ TcpListener โ
โโโโโโโโโโโฌโโโโโโโโโโโ
โ
โโโโโโโโโโโดโโโโโโโโโโโ
โ โ
โผ โผ
Client A Client B
โ โ
โผ โผ
receive_packet() receive_packet()
โ โ
โโโโโโโโโโโฌโโโโโโโโโโโ
โผ
PacketHandler
โ
โโโโโโโโโโโโฌโโโโโโโโโผโโโโโโโโโฌโโโโโโโโโโโ
โผ โผ โผ โผ โผ
Login Signup Chat Move Log
โ โ
โผ โผ
SQLite SQLite
โ
โผ
Session state
The architecture is deliberately being built in layers:
TCP transport
โ
Packet framing
โ
Payload parsing
โ
Packet routing
โ
Authentication / database
โ
Gameplay systems
This gives the project a foundation on which the larger MMORPG systems can eventually be built.
๐ What's Next?
The network layer is one of the areas I expect to evolve substantially as The Last Signal Online moves from prototype infrastructure toward actual multiplayer gameplay.
The next major challenges are not simply "making packets work".
They are about making the protocol:
- reliable
- testable
- extensible
- secure
- efficient
- easy for contributors to understand
And that is exactly where open-source development becomes interesting.
๐ค Want to Contribute?
The Last Signal Online is an open-source project, and there are several ways to contribute.
You don't need to work on gameplay to help.
Current areas include:
- ๐ฆ Rust server development
- ๐ network protocol development
- ๐งช network testing
- ๐ Python client development
- ๐ security and cryptography review
- โ๏ธ CI/CD
- ๐ documentation
- ๐ translation
- ๐ฎ game design
If you're interested in Rust networking, multiplayer architecture, Python clients, testing, or open-source game development, there are already areas where you can get involved.
๐ ๐ New Contributor? Start Here!
You can also explore the project's open issues and choose a problem that matches your interests and experience.
๐ฆ About The Last Signal Online
The Last Signal Online is an open-source post-apocalyptic persistent-world MMORPG built with Rust and Python.
The goal is to create a multiplayer world combining:
- exploration
- survival
- combat
- player interaction
- economy
- crafting
- factions and territories
- persistent world systems
The project is still under development, which means the architecture is evolving alongside the game itself.
That also means there are plenty of interesting problems left to solve.
๐ Explore the project on GitHub
๐ฌ Final Thoughts
Building the network layer of an MMORPG is a very different problem from building a simple client/server application.
A Ping packet is easy.
Building a protocol that can eventually carry an entire persistent multiplayer world is much harder.
That's the challenge I'm working on with The Last Signal Online.
And I'm building it openly, one system at a time.
If Rust, networking, Python, game development, or open source interests you, feel free to take a look at the project. ๐ฆ๐๐
Top comments (1)
Some comments may only be visible to logged-in visitors. Sign in to view all comments.