In this project, we built a simplified version of the classic NetCat ("nc") tool — a TCP-based chat server that allows multiple clients to connect, send messages, and interact in real time.
The goal was not just to recreate a chat system, but to deeply understand:
- TCP networking
- Go concurrency (goroutines & channels)
- State management in concurrent systems
- Client-server architecture
At its core, the system needed to:
- Accept multiple client connections
- Allow clients to send messages
- Broadcast messages to other clients
- Track when users join or leave
- Handle unexpected disconnects (like Ctrl+C)
This introduces a key challenge:
«Multiple clients interacting with shared state at the same time.»
TCP Server Basics
The server listens for incoming connections using:
listener, _ := net.Listen("tcp", ":8989")
Then continuously accepts clients:
for {
conn, _ := listener.Accept()
go handleConnection(conn)
}
Important concept:
- "Accept()" blocks until a client connects
- Each client is handled in a separate goroutine
This allows multiple users to connect simultaneously.
Goroutines and Concurrency
Each client runs in its own goroutine:
go handleConnection(conn)
This means:
- One slow client does not block others
- Each connection is handled independently
However, this introduces a problem:
«Multiple goroutines modifying shared data can cause race conditions.»
The Shared State Problem
We needed to track all connected clients:
var connections map[net.Conn]string
But multiple goroutines might:
- Add clients
- Remove clients
- Broadcast messages
At the same time.
This can cause:
- Data corruption
- Crashes ("fatal error: concurrent map writes")
Solution 1: Mutex
One approach is using a mutex:
mu.Lock()
connections[conn] = name
mu.Unlock()
But this introduces:
- Complexity
- Potential deadlocks
- Performance bottlenecks
Solution 2: Channels (The Go Way)
Instead of sharing memory, we used channels to communicate changes.
This follows Go’s philosophy:
«“Do not communicate by sharing memory; share memory by communicating.”»
ChatRoom Architecture
We designed a "ChatRoom" struct:
type ChatRoom struct {
chatters map[*Client]struct{}
history []string
Register chan *Client
Unregister chan *Client
Broadcast chan Message
}
- Only one goroutine manages "chatters" and "history"
- Other goroutines send events via channels
The Event Loop
The core of the system is the "Run()" method:
for {
select {
case client := <-Register:
case client := <-Unregister:
case msg := <-Broadcast:
}
}
This acts like a central controller.
Handling Events
1. Client Join
- Ask for name
- Add to chatters map
- Send chat history
- Broadcast join message
cr.chatters[client] = struct{}{}
2. Message Broadcast
- Append to history
- Send to all clients except sender
for c := range cr.chatters {
if c != sender {
c.receive <- message
}
}
3. Client Leave
- Remove from map
- Close channel
- Broadcast leave message
Handling Ctrl+C (Unexpected Disconnects)
When a client presses Ctrl+C:
- TCP connection closes
-
"ReadString()" returns "io.EOF"
We detect this:
if err != nil {
// client disconnected
}
And broadcast:
"%s has left the chat"
Message Flow
Here’s how a message travels:
- Client sends message
- Goroutine reads it
- Sends it to "Broadcast" channel
- "Run()" receives it
- Loops through clients
- Sends message to each client’s "receive" channel
- Client writer goroutine prints it
Main Concepts were:
1. TCP is Just a Stream
- Everything is bytes
- Messages are manually structured ("\n")
_
- Blocking is Normal_
- "Accept()" blocks waiting for connections
- "ReadString()" blocks waiting for input
But only within their goroutine.
3. Goroutines Enable Concurrency
- Lightweight threads managed by Go
- Thousands can run efficiently
4. Channels Simplify Concurrency
- Avoid shared memory issues
- Centralize state management
- Create predictable flow
Challenges Faced
- Handling empty messages
- Debugging raw string issues
- Understanding blocking behavior
- Managing client disconnects
- Avoiding race conditions
This project goes beyond just building a chat app.
It revealed:
- How real-time systems work
- How servers handle multiple users
- How to design safe concurrent programs
In many ways, this is a mini version of real-world systems like chat apps, multiplayer servers, and messaging platforms.
Building this TCP chat server helped me understand how powerful Go is for concurrent systems.
By combining:
- TCP networking
- Goroutines
- Channels
we can build scalable, real-time applications with relatively simple code.
Top comments (2)
Really enjoyed this breakdown especially the way channels were used to manage shared state instead of relying on mutexes. The ChatRoom event loop feels very close to real-world event-driven and actor-style backend systems, which makes this more than just a TCP chat project. It’s a solid way to understand how concurrent servers actually work in Go. Would be interesting to see this extended with WebSockets or message persistence. Great work!
Thank you