TCP chat server, implemented in Go using only the standard library, that implements a custom binary protocol.
This was developed during the Coderful 2025 Workshop led by Gabriele Santomaggio
The protocol specifications can be found in the README of the original repo, and also appended to this README, in case the original repo gets deleted.
The goal of this project is to create a simple client-server chat application using a custom binary protocol. The server is responsible for:
- Handling client connections
- Processing login and message commands
- Managing user sessions and statuses
- Storing and dispatching messages
To run the server, navigate to the server directory and run it:
go run .The server will start listening on port 5555.
This project's scope is only to learn how to write a client-server application from scratch. The code is demonstrative and focuses more on understanding how the TCP works than production code. The client applications are meant to test the server quickly.
We provide examples in the following languages:
- Server golang [Full features]
- Client golang [Full features]
- Server rust [Partial features]
- Server NodeJS [Partial features]
- Server Python [Full features]
- Server:
/server/go/run/serverand rungo run main.go localhost:5555 - Client:
/server/go/run/clientand rungo run main.go localhost:5555( yes, the client is inside theserverdirectory because they share the same codec) - You can use two different terminals with two different users
The protocol is a binary protocol with the following structure:
byte1 byteuint162 bytesuint324 bytesstring2 bytes for the length + N bytes for the string[]byte2 bytes for the length + N bytes for the stringuint648 bytes
| Name | Type |
|---|---|
version |
byte |
command |
uint16 |
| Name | Type | value(s) | reference |
|---|---|---|---|
version |
byte |
0x01 | Header::version |
key |
uint16 |
0x01 | Header::command |
correlationId |
uint32 |
||
username |
string |
| Name | Type | value(s) | reference |
|---|---|---|---|
version |
byte |
0x01 | Header::version |
key |
uint16 |
0x02 | Header::command |
correlationId |
uint32 |
||
message |
string |
||
From |
string |
||
To |
string |
||
Time |
uint64 |
All the commands will have a response with the following structure:
| Name | Type | value(s) | reference |
|---|---|---|---|
version |
byte |
0x01 | Header::version |
key |
uint16 |
0x03 | Header::command |
correlationId |
uint32 |
||
code |
uint16 |
ResponseCodes |
| Name | value(s) |
|---|---|
OK |
0x01 |
ErrorUserNotFound |
0x03 |
ErrorUserAlreadyLogged |
0x04 |
- Write the length of whole message (header + command) as a
uint32 - Write the header
- Write the command
If user1 wants to login, this will be the structure of data sent.
Header:version= 0x01 => 1 bytecommand= 0x01 => 2 bytes
CommandLogin:correlationId= 0x01 => 4 bytesusername= "user1" => length = 2 + 5 = 7
In this case the client will:
- write the length in bytes (as
uint32) of the whole message: 3 + 11 = 14- note: this
uint32is excluded from the total bytes count
- note: this
- write the
header+message:
0x00 0x00 0x00 0x0E (`uint32`)
0x01 => version (1 byte)
0x00 0x01 => command (`uint16`)
0x00 0x00 0x00 0x01 => correlationId (`uint32`)
0x00 0x05 => username length (`uint16`)
0x75 0x73 0x65 0x72 0x31 => username (user1) (5 bytes)
- Total bytes written: 14 (body) + 4 (len of body) = 18
- Send the message
- Read the
Response
- Read the length of the whole message (
header+command) as auint32 - Read the
header - Read the
command key - Read the
commandbased on thekey
For Example: CommandLogin with username user1
- Read the first 4 bytes for the length of the whole message: 14
- Ensure the socket buffer has at least 14 bytes
- Read the header:
- Read the
version: 0x01 - Read the
command: 0x01 - Read the
commandbased on thekey:CommandLogin- Read the
correlationId: 0x01 - Read the username: "user1"
- Read the
- Process the command
- Send the
Response
- Read the
The correlationId is a unique identifier for each command sent by the client.
The server will send back the correlationId to the client.
So the client can match the response with the command sent.
You must be sure to provide a unique correlationId for each command sent.
- Login (without password. It is enough to send the username)
- Send message and dispatch to the correct user
- Store in memory the users with the status (online/offline)
- Store in memory the off-line messages when the user is not online
- Send the off-line messages when the user logs in
- Check if the user is already logged in
- Check if the destination user exists
- Logout
- Send message to multiple users
- Send message to all users
- Command to get the list of users
- Persist the users and messages in a database