When building Instant Messaging (IM) systems, what causes developers the most headache is often not how to send messages, but how to manage connections.
Imagine a scenario: user Xiao Ming is chatting on their phone, and suddenly opens their computer to log in. At this moment, should the system "kick" the mobile client offline? Or should it allow both clients to be online simultaneously? If the user's network fluctuates, generating two reconnection requests in a short period of time, how does the server ensure that it won't face the awkward situation where "the old connection is not disconnected, and the new connection cannot be established"?
Today, we will talk about how to elegantly resolve multi-device connection competition using Go, through Axiom's engineering practices.
1. Modeling: From "One-to-One" to "One-to-Many"
Most beginners simply design connection management as a map[UserID]Connection. However, this is not viable in modern internet scenarios.
[!IMPORTANT]
Deep Dive: A real multi-device system needs to support a dual index of "UserID + DeviceID".
In our project, the connection manager (Manager) adopts a nested map structure:
go
type Manager struct {
// UserID -> DeviceID -> Client
Clients map[uint]map[string]*Client
Lock sync.RWMutex
}
This structure allows the same user to be online on multiple devices such as Web, iOS, and Android at the same time, laying the physical foundation for multi-device message synchronization.
2. Core Tactics: "Preempt" First, "Clean Up" Later
When a new WebSocket connection request comes in, the most elegant way to handle it is not to wait with a giant lock, but to use optimistic preemption.
2.1 Preemptive Registration
In the main loop of the Manager, our logic for handling new connections (Register) is as follows:
- Save new connection: Directly store the new
Clientinstance in the map, overwriting the old record for the same device. - Asynchronously close old connection: If it is found that the device already had an active connection previously, instead of immediately blocking to wait, we get a reference to the old connection.
- Trigger self-cleanup: Call
Close()on the old connection.
2.2 Why is this more elegant?
In Go, closing a network connection triggers its corresponding read Goroutine to exit, thereby automatically triggering the unregistration (Unregister) process. This "leveraging external forces" design avoids writing complex cleanup code inside the registration logic, keeping the main loop lightweight.
3. Devil in the Details: Preventing "Collateral Damage"
In high-concurrency scenarios, a bizarre situation may arise: connection A is disconnecting, while connection B is connecting at almost the same instant. If the logic is handled improperly, the offline logic of connection A might delete the newly connected connection B.
[!WARNING]
Deep Trap Warning: In the Unregister logic, an object pointer comparison must be performed.
go
case client := <-m.Unregister:
m.Lock.Lock()
if devices, ok := m.Clients[client.UserID]; ok {
// Key: only perform deletion if the connection object in current memory is the exact same one to be unregistered
if current, exists := devices[client.DeviceID]; exists && current == client {
delete(devices, client.DeviceID)
}
}
m.Lock.Unlock()
This "object-level" check ensures that even if millisecond-level race conditions occur, the system can accurately identify who should leave and who should stay, avoiding accidental deletions.
4. State Extension: From Memory to Redis
Managing connections in single-machine memory is perfect in a single-instance environment. However, when your IM system needs to scale horizontally (distributed deployment), connection states become "isolated islands".
[!TIP]
Deep Solution: While handling connection competition, we synchronously maintain online states in Redis.
- When going online:
setOnlineStatus(uid, device, true) - When going offline:
setOnlineStatus(uid, device, false)
This way, when a user on Server A sends a message to a user on Server B, the target connection can be quickly located on which machine it resides via Redis, achieving precise cross-node delivery.
5. Summary
Elegantly handling connection competition does not lie in advanced algorithms, but in respect for concurrency models:
- The data structure must support multi-dimensional business logic (such as dual user + device indexing).
- Resource cleanup must follow Go's concurrency philosophy, triggered automatically by Close signals.
- Logical checks must be precise to the object level (pointer comparison), rather than relying solely on IDs.
[!NOTE]
Author's Postscript: In IM systems, connections are cheap, but state consistency is expensive. I hope this article provides you with some reference when dealing with persistent connection operations.