Building a High‑Performance, Scalable IM System with Go and WebSocket

This article explains the fundamentals of instant‑messaging, dives deep into the WebSocket protocol and its handshake, and provides a step‑by‑step guide with Go code to quickly build a high‑availability, extensible IM system that supports registration, login, single‑chat, group‑chat, file upload and optimization strategies.

IT Architects Alliance
IT Architects Alliance
IT Architects Alliance
Building a High‑Performance, Scalable IM System with Go and WebSocket

Instant messaging (IM) is described as a milestone of the digital era, with examples such as QQ, WeChat, DingTalk and others illustrating how IM products have shaped personal and enterprise communication.

The purpose of the article is to help readers understand socket protocols and quickly build a high‑availability, extensible IM system that supports registration, login, friend management, single‑chat, group‑chat, and media transmission, while also outlining future optimization directions.

WebSocket is introduced as a full‑duplex, persistent connection protocol. The article explains the HTTP‑to‑WebSocket upgrade, the handshake process, and the frame structure that enables low‑overhead, real‑time data exchange.

Handshake implementation in Go:

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; } }

WebSocket server with gorilla/websocket and heartbeat logic in Go:

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}
        // process data
        _ = data
    }
    conn.Close()
}
func main() {http.HandleFunc("/ws", wsHandler); http.ListenAndServe(":8081", nil)}

User model definition using XORM:

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:"-"`
    Online    int       `xorm:"int(10)" json:"online"`
    Token     string    `xorm:"varchar(40)" json:"token"`
    Memo      string    `xorm:"varchar(140)" json:"memo"`
    Createat  time.Time `xorm:"datetime" json:"createat"`
}

Database initialization (MySQL) with XORM:

func init() {
    driverName := "mysql"
    dsnName := "root:root@(127.0.0.1:3306)/chat?charset=utf8"
    var err error
    DbEngine, err = xorm.NewEngine(driverName, dsnName)
    if err != nil && err.Error() != "" {log.Fatal(err)}
    DbEngine.ShowSQL(true)
    DbEngine.SetMaxOpenConns(10)
    DbEngine.Sync(new(User), new(Community), new(Contact))
    fmt.Println("init database ok!")
}

Registration and login handlers (controller) and service logic (simplified):

// controller/user.go
func UserRegister(w http.ResponseWriter, r *http.Request) {
    var user model.User
    util.Bind(r, &user)
    user, err := UserService.UserRegister(user.Mobile, user.Passwd, user.Nickname, user.Avatar, user.Sex)
    if err != nil {util.RespFail(w, err.Error()); return}
    util.RespOk(w, user, "")
}
func UserLogin(w http.ResponseWriter, r *http.Request) {
    mobile := r.PostFormValue("mobile")
    pwd := r.PostFormValue("passwd")
    if len(mobile) == 0 || len(pwd) == 0 {util.RespFail(w, "用户名或密码不正确"); return}
    loginUser, err := UserService.Login(mobile, pwd)
    if err != nil {util.RespFail(w, err.Error()); return}
    util.RespOk(w, loginUser, "")
}

Chat controller handling token verification, WebSocket upgrade, and client mapping:

type Node struct {
    Conn      *websocket.Conn
    DataQueue chan []byte
    GroupSets set.Interface
}
var clientMap = make(map[int64]*Node)
var rwlocker sync.RWMutex
func Chat(w http.ResponseWriter, r *http.Request) {
    idStr := r.URL.Query().Get("id")
    token := r.URL.Query().Get("token")
    userId, _ := strconv.ParseInt(idStr, 10, 64)
    isLegal := checkToken(userId, token)
    conn, err := (&websocket.Upgrader{CheckOrigin: func(r *http.Request) bool {return isLegal}}).Upgrade(w, r, nil)
    if err != nil {log.Println(err); return}
    node := &Node{Conn: conn, DataQueue: make(chan []byte, 50), GroupSets: set.New(set.ThreadSafe)}
    // add groups, store in clientMap with lock
    rwlocker.Lock(); clientMap[userId] = node; rwlocker.Unlock()
    go sendproc(node)
    go recvproc(node)
    sendMsg(userId, []byte("welcome!"))
}

Message dispatching and command constants for single‑chat, group‑chat and heartbeat:

const (
    CmdSingleMsg = 10
    CmdRoomMsg   = 11
    CmdHeart     = 0
)
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 _, v := range clientMap {
            if v.GroupSets.Has(msg.Dstid) {v.DataQueue <- data}
        }
    case CmdHeart:
        // heartbeat handling
    }
}

File upload endpoint that stores images locally and returns the file path:

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

The article concludes with optimization suggestions: refactor using a framework like Gin, shard the clientMap or move connection handles to Redis, compress and deduplicate uploaded images, enforce secure wss connections, batch database writes, reduce JSON encoding overhead, and consider distributed deployment, service separation, and caching to achieve true high‑availability at scale.

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 SystemWebSocketMessage Queuereal-time communication
IT Architects Alliance
Written by

IT Architects Alliance

Discussion and exchange on system, internet, large‑scale distributed, high‑availability, and high‑performance architectures, as well as big data, machine learning, AI, and architecture adjustments with internet technologies. Includes real‑world large‑scale architecture case studies. Open to architects who have ideas and enjoy sharing.

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.