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/server
and rungo run main.go localhost:5555
- Client:
/server/go/run/client
and rungo run main.go localhost:5555
( yes, the client is inside theserver
directory 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:
byte
1 byteuint16
2 bytesuint32
4 bytesstring
2 bytes for the length + N bytes for the string[]byte
2 bytes for the length + N bytes for the stringuint64
8 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
uint32
is 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
command
based 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
command
based 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