Backend Development 39 min read

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.

Architecture Digest
Architecture Digest
Architecture Digest
Building a High-Performance Scalable Instant Messaging System with Go and WebSocket

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.RWMutex

Message 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.

Backend Developmentgoscalable architectureWebSocketreal-time communicationInstant Messaging
Architecture Digest
Written by

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.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.