Building a High-Performance Scalable Instant Messaging System with Go and WebSocket
This article guides readers through the design and implementation of a high‑performance, scalable instant‑messaging (IM) system using Go, detailing WebSocket protocol fundamentals, server‑side architecture, authentication, message handling, code examples, and optimization strategies for production deployment.
Introduction
The article explains why instant messaging (IM) is a milestone in the Internet era and compares major products such as QQ, WeChat, DingTalk, and Enterprise WeChat. It then introduces the goal of building a lightweight yet feature‑complete IM system that supports registration, login, friend management, single‑chat, group‑chat, and media messages.
WebSocket Protocol Deep Dive
WebSocket provides a full‑duplex, persistent connection over a single TCP socket. The client initiates an HTTP request, which is upgraded to the WebSocket protocol (ws:// or wss://). The handshake reuses the HTTP TCP connection, and the protocol defines a small framing format that reduces overhead compared to traditional HTTP polling.
Key handshake steps include the Sec-WebSocket-Key header from the client and the server’s Sec-WebSocket-Accept response, which is calculated by appending the GUID 258EAFA5‑E914‑47DA‑95CA‑C5AB0DC85B11 , SHA‑1 hashing, and Base64 encoding.
public function dohandshake($sock, $data, $key) {
if (preg_match("/Sec-WebSocket-Key: (.*)\r\n/", $data, $match)) {
$response = base64_encode(sha1($match[1] . '258EAFA5-E914-47DA-95CA-C5AB0DC85B11', true));
$upgrade = "HTTP/1.1 101 Switching Protocol\r\n" .
"Upgrade: websocket\r\n" .
"Connection: Upgrade\r\n" .
"Sec-WebSocket-Accept: " . $response . "\r\n\r\n";
socket_write($sock, $upgrade, strlen($upgrade));
$this->isHand[$key] = true;
}
}System Architecture
The IM system consists of four layers: a Web app client (rendered with Vue), an access layer using WebSocket, a server‑side processing layer handling authentication, routing, and business logic, and a storage layer (MySQL) for persisting user data and relationships.
All server code is written in Go, taking advantage of its lightweight goroutine model to support thousands of concurrent connections.
Core Data Structures
type Node struct {
Conn *websocket.Conn
DataQueue chan []byte // channel for serialising messages
GroupSets set.Interface // groups the user belongs to
}
var clientMap map[int64]*Node = make(map[int64]*Node)
var rwlocker sync.RWMutexMessage Model
type Message struct {
Id int64 `json:"id,omitempty"`
Userid int64 `json:"userid,omitempty"`
Cmd int `json:"cmd,omitempty"` // 10: single, 11: group, 0: heartbeat
Dstid int64 `json:"dstid,omitempty"`
Media int `json:"media,omitempty"`
Content string `json:"content,omitempty"`
Pic string `json:"pic,omitempty"`
Url string `json:"url,omitempty"`
Memo string `json:"memo,omitempty"`
Amount int `json:"amount,omitempty"`
}
const (
CmdSingleMsg = 10
CmdRoomMsg = 11
CmdHeart = 0
)Authentication and Connection Establishment
When a client requests /chat?id=xxx&token=yyy , the server validates the token, upgrades the HTTP request to WebSocket, creates a Node , loads the user’s group IDs, and stores the node in clientMap under a write lock.
func Chat(writer http.ResponseWriter, request *http.Request) {
id := request.URL.Query().Get("id")
token := request.URL.Query().Get("token")
userId, _ := strconv.ParseInt(id, 10, 64)
isLegal := checkToken(userId, token)
conn, err := (&websocket.Upgrader{CheckOrigin: func(r *http.Request) bool { return isLegal }}).Upgrade(writer, request, nil)
if err != nil { log.Println(err); return }
node := &Node{Conn: conn, DataQueue: make(chan []byte, 50), GroupSets: set.New(set.ThreadSafe)}
// load groups …
rwlocker.Lock(); clientMap[userId] = node; rwlocker.Unlock()
go sendproc(node)
go recvproc(node)
sendMsg(userId, []byte("welcome!"))
}
func checkToken(userId int64, token string) bool {
user := UserService.Find(userId)
return user.Token == token
}Message Dispatching
Two goroutines per connection handle sending and receiving. Received messages are unmarshaled into Message structs and dispatched based on Cmd :
func sendproc(node *Node) {
for {
select {
case data := <-node.DataQueue:
if err := node.Conn.WriteMessage(websocket.TextMessage, data); err != nil { log.Println(err); return }
}
}
}
func recvproc(node *Node) {
for {
_, data, err := node.Conn.ReadMessage()
if err != nil { log.Println(err); return }
dispatch(data)
}
}
func dispatch(data []byte) {
var msg Message
if err := json.Unmarshal(data, &msg); err != nil { log.Println(err); return }
switch msg.Cmd {
case CmdSingleMsg:
sendMsg(msg.Dstid, data)
case CmdRoomMsg:
rwlocker.RLock()
for _, n := range clientMap {
if n.GroupSets.Has(msg.Dstid) { n.DataQueue <- data }
}
rwlocker.RUnlock()
case CmdHeart:
// heartbeat handling
}
}
func sendMsg(userId int64, msg []byte) {
rwlocker.RLock()
if node, ok := clientMap[userId]; ok { node.DataQueue <- msg }
rwlocker.RUnlock()
}File Upload and Media Support
Images and other media are uploaded via /attach/upload . The server stores files under ./resource/ and returns the file path, which the client later loads as an img or emoji.
func UploadLocal(writer http.ResponseWriter, request *http.Request) {
srcFile, head, err := request.FormFile("file")
if err != nil { util.RespFail(writer, err.Error()); return }
suffix := ".png"
parts := strings.Split(head.Filename, ".")
if len(parts) > 1 { suffix = "." + parts[len(parts)-1] }
filename := fmt.Sprintf("%d%s%s", time.Now().Unix(), util.GenRandomStr(32), suffix)
filepath := "./resource/" + filename
dstfile, err := os.Create(filepath)
if err != nil { util.RespFail(writer, err.Error()); return }
if _, err = io.Copy(dstfile, srcFile); err != nil { util.RespFail(writer, err.Error()); return }
util.RespOk(writer, filepath, "")
}Optimization and Architecture Upgrade Suggestions
The article proposes code refactoring with frameworks like Gin, sharding the clientMap or moving connection handles to Redis, compressing media, deduplicating uploads, using TLS (wss://), persisting messages to MongoDB, batch‑writing to reduce DB I/O, and separating business services from the chat service for better scalability.
Conclusion
By following the presented design, developers can quickly prototype a functional IM system in Go, understand WebSocket internals, and extend the platform with features such as voice/video calls, red packets, or AI‑driven chat bots, while keeping the architecture ready for production‑grade scaling.
Architecture Digest
Focusing on Java backend development, covering application architecture from top-tier internet companies (high availability, high performance, high stability), big data, machine learning, Java architecture, and other popular fields.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.