Backend Development 38 min read

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: (.*)\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; } }

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.

backendmicroservicesGoIM 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

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.