Build a High‑Performance Go IM System with WebSocket: From Basics to Scaling

This article walks you through the fundamentals of instant‑messaging systems, explains the WebSocket protocol in depth, and provides a complete Go implementation for a scalable, high‑availability IM service with registration, authentication, single‑ and group‑chat, emoji and image support, plus optimization and architecture recommendations.

Architect's Guide
Architect's Guide
Architect's Guide
Build a High‑Performance Go IM System with WebSocket: From Basics to Scaling

1. Milestone of the era – Instant Messaging

The drama "Entrepreneurial Age" imagines a startup building an IM product called “Magic Crystal”, echoing the early vision of QQ. Instant messaging has indeed become a historic milestone: QQ created a personal‑space and entertainment ecosystem, while WeChat shifted focus to the commercial domain with payments, public accounts, and mini‑programs, eventually dominating the business‑app market.

2. Chapter Overview

The goal is to help readers deeply understand the socket protocol and quickly build a high‑availability, extensible IM system. The prototype includes basic registration, login, friend management, single‑chat, group‑chat, and support for text, emoji, and image messages. Later chapters cover WebSocket details, fast implementation techniques, architectural upgrades, and optimization ideas.

3. Deep dive into WebSocket protocol

WebSocket provides full‑duplex, bidirectional communication over a single persistent connection. After a JavaScript‑initiated HTTP request, the server upgrades the connection to the WebSocket protocol (ws:// or wss://). Because WebSocket uses a custom protocol, the URL scheme changes, and the upgrade reduces per‑message overhead compared to HTTP, making it ideal for mobile applications.

3.1 WebSocket reuses HTTP handshake channel

The client sends a standard HTTP GET request with special headers (Connection: Upgrade, Upgrade: websocket, Sec‑WebSocket‑Key, Sec‑WebSocket‑Version). The server validates the key, computes Sec‑WebSocket‑Accept, and replies, completing the handshake and establishing a persistent channel.

3.2 HTTP protocol upgrade to WebSocket

The upgrade request looks like a normal HTTP request but includes the Upgrade and Connection headers. The server concatenates the client’s Sec‑WebSocket‑Key with the GUID "258EAFA5‑E914‑47DA‑95CA‑C5AB0DC85B11", hashes it with SHA‑1, base64‑encodes the result, and returns it as Sec‑WebSocket‑Accept.

public function doHandshake($sock, $data, $key){
    if (preg_match("/Sec-WebSocket-Key: (.*)
/", $data, $match)) {
        $response = base64_encode(sha1($match[1] . '258EAFA5‑E914‑47DA‑95CA‑C5AB0DC85B11', true));
        $upgrade = "HTTP/1.1 101 Switching Protocol
".
                   "Upgrade: websocket
".
                   "Connection: Upgrade
".
                   "Sec-WebSocket-Accept: " . $response . "

";
        socket_write($sock, $upgrade, strlen($upgrade));
        $this->isHand[$key] = true;
    }
}

3.3 WebSocket frames and data fragmentation

Data is sent in frames. The FIN bit marks the final fragment; RSV1‑3 are reserved for extensions. The server reassembles fragments based on FIN, while the client can send large messages split into multiple frames, reducing per‑message overhead compared to HTTP.

3.4 WebSocket connection keep‑alive and heartbeat

Long‑living connections use ping/pong heartbeats. If no ping/pong is observed, the connection is considered broken. The following Go snippet shows a simple heartbeat implementation using the gorilla/websocket library.

package main

import (
    "net/http"
    "time"
    "github.com/gorilla/websocket"
)

var upgrade = websocket.Upgrader{CheckOrigin: func(r *http.Request) bool {return true}}

func wsHandler(w http.ResponseWriter, r *http.Request) {
    conn, err := upgrade.Upgrade(w, r, nil)
    if err != nil {return}
    go func() {
        for {
            if err = conn.WriteMessage(websocket.TextMessage, []byte("heartbeat")); err != nil {return}
            time.Sleep(1 * time.Second)
        }
    }()
    for {
        _, data, err := conn.ReadMessage()
        if err != nil {break}
        conn.WriteMessage(websocket.TextMessage, data)
    }
    conn.Close()
}

func main() {http.HandleFunc("/ws", wsHandler); http.ListenAndServe(":7777", nil)}

4. Quickly build a high‑performance, extensible IM system

4.1 System architecture and directory layout

The architecture consists of a Web app client, an access layer using WebSocket, a server layer handling authentication, routing, and message distribution, and a storage layer persisting user relationships in MySQL and media files locally.

4.2 Ten‑line template rendering

func registerView() {
    tpl, err := template.ParseGlob("./app/view/**/*")
    if err != nil {log.Fatal(err)}
    for _, v := range tpl.Templates() {
        name := v.Name()
        http.HandleFunc(name, func(w http.ResponseWriter, r *http.Request) {tpl.ExecuteTemplate(w, name, nil)})
    }
}

4.3 Registration, login and authentication

The MySQL user table stores mobile, password (MD5 salted), avatar, gender, token, etc. Registration checks for existing mobile, inserts a new record, and login validates the password, generates a token, and updates it.

type User struct {
    Id        int64     `xorm:"pk autoincr bigint(64)" json:"id"`
    Mobile    string    `xorm:"varchar(20)" json:"mobile"`
    Passwd    string    `xorm:"varchar(40)" json:"-"`
    Avatar    string    `xorm:"varchar(150)" json:"avatar"`
    Sex       string    `xorm:"varchar(2)" json:"sex"`
    Nickname  string    `xorm:"varchar(20)" json:"nickname"`
    Salt      string    `xorm:"varchar(10)" json:"-"`
    Token     string    `xorm:"varchar(40)" json:"token"`
    Createat  time.Time `xorm:"datetime" json:"createat"`
}
func init() {
    engine, _ := xorm.NewEngine("mysql", "root:root@(127.0.0.1:3306)/chat?charset=utf8")
    engine.Sync(new(User), new(Community), new(Contact))
    DbEngine = engine
}
func UserRegister(w http.ResponseWriter, r *http.Request) {
    var u User
    util.Bind(r, &u)
    user, err := UserService.UserRegister(u.Mobile, u.Passwd, u.Nickname, u.Avatar, u.Sex)
    if err != nil {util.RespFail(w, err.Error())} else {util.RespOk(w, user, "")}
}
func UserLogin(w http.ResponseWriter, r *http.Request) {
    mobile := r.PostFormValue("mobile")
    pwd := r.PostFormValue("passwd")
    user, err := UserService.Login(mobile, pwd)
    if err != nil {util.RespFail(w, err.Error())} else {util.RespOk(w, user, "")}
}

4.4 Implement single‑chat and group‑chat

Message struct defines fields such as Id, Userid, Cmd, Dstid, Media, Content, etc. Cmd constants differentiate single (10), group (11), and heartbeat (0) messages. Two goroutines per connection handle sending and receiving via channels.

type Message struct {
    Id      int64  `json:"id,omitempty"`
    Userid  int64  `json:"userid,omitempty"`
    Cmd     int    `json:"cmd,omitempty"`
    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
)
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:
        for _, n := range clientMap {
            if n.GroupSets.Has(msg.Dstid) {n.DataQueue <- data}
        }
    case CmdHeart:
        // heartbeat handling
    }
}

4.5 Send emojis and images

Emojis are small images referenced by a URL. Image upload stores the file under ./resource and returns the path; the client loads the image via that URL.

func FileUpload(w http.ResponseWriter, r *http.Request) {
    src, header, err := r.FormFile("file")
    if err != nil {util.RespFail(w, err.Error()); return}
    suffix := ".png"
    parts := strings.Split(header.Filename, ".")
    if len(parts) > 1 {suffix = "." + parts[len(parts)-1]}
    filename := fmt.Sprintf("%d%s%s", time.Now().Unix(), util.GenRandomStr(32), suffix)
    dstPath := "./resource/" + filename
    dst, err := os.Create(dstPath)
    if err != nil {util.RespFail(w, err.Error()); return}
    io.Copy(dst, src)
    util.RespOk(w, dstPath, "")
}

5. Program optimization and architecture upgrade

Recommendations include adopting a framework like Gin, sharding the clientMap or moving connection handles to Redis, compressing and deduplicating images, switching to wss://, encrypting payloads, batching DB writes, using a CDN for static resources, separating user‑service from chat‑service, and scaling out with distributed nodes and caching layers.

6. Conclusion

Go makes building an IM system straightforward and performant. By mastering WebSocket fundamentals and the provided code, developers can launch a functional chat service in a short time and extend it to richer features such as voice, video, and AI‑driven interactions.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

GoIM SystemWebSocket
Architect's Guide
Written by

Architect's Guide

Dedicated to sharing programmer-architect skills—Java backend, system, microservice, and distributed architectures—to help you become a senior architect.

0 followers
Reader feedback

How this landed with the community

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.