WebSocket Real-Time Communication
Unlike HTTP, WebSocket allows the server and client to freely exchange messages in both directions once a connection is established. It is used for chat, real-time notifications, stock streaming, online games, and more. Go's gorilla/websocket library is the de facto standard for WebSocket implementation.
WebSocket Conceptsβ
HTTP Request/Response (unidirectional, connectionless):
Client ββGET /dataβββΆ Server
Client βββResponseββ Server
(connection closed)
WebSocket (bidirectional, persistent connection):
Client ββHTTP UpgradeβββΆ Server
Client βββββ Handshake complete ββββ Server
Client βββββββββββββ Message βββββββΆ Server (full-duplex communication)
Client βββββββββββββ Message βββββββΆ Server
(connection maintained)
HTTP Upgrade Handshake:
Client request:
GET /ws HTTP/1.1
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Server response:
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
Basic Usageβ
go get github.com/gorilla/websocket
package main
import (
"fmt"
"log"
"net/http"
"github.com/gorilla/websocket"
)
// Upgrader upgrades HTTP connection to WebSocket
var upgrader = websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 1024,
// Allow all origins (must restrict in production)
CheckOrigin: func(r *http.Request) bool {
return true
},
}
func echoHandler(w http.ResponseWriter, r *http.Request) {
// Upgrade HTTP β WebSocket
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
log.Printf("Upgrade failed: %v", err)
return
}
defer conn.Close()
log.Printf("New connection: %s", conn.RemoteAddr())
for {
// Receive message
messageType, message, err := conn.ReadMessage()
if err != nil {
// Detect connection close
if websocket.IsUnexpectedCloseError(err,
websocket.CloseGoingAway, websocket.CloseAbnormalClosure) {
log.Printf("Unexpected close: %v", err)
}
break
}
log.Printf("Received: %s", message)
// Echo response
err = conn.WriteMessage(messageType, message)
if err != nil {
log.Printf("Write failed: %v", err)
break
}
}
}
func main() {
http.HandleFunc("/ws", echoHandler)
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "WebSocket echo server - connect to /ws")
})
log.Println("Server started: http://localhost:8080")
log.Fatal(http.ListenAndServe(":8080", nil))
}
Message Typesβ
package main
import (
"encoding/json"
"log"
"net/http"
"github.com/gorilla/websocket"
)
var upgrader = websocket.Upgrader{CheckOrigin: func(r *http.Request) bool { return true }}
// ChatMessage JSON message protocol
type ChatMessage struct {
Type string `json:"type"` // "message", "join", "leave", "error"
Room string `json:"room"`
Sender string `json:"sender"`
Content string `json:"content"`
}
func jsonHandler(w http.ResponseWriter, r *http.Request) {
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
return
}
defer conn.Close()
for {
// Handle by message type
mt, data, err := conn.ReadMessage()
if err != nil {
break
}
switch mt {
case websocket.TextMessage:
var msg ChatMessage
if err := json.Unmarshal(data, &msg); err != nil {
// Send error response on JSON parse failure
errMsg, _ := json.Marshal(ChatMessage{Type: "error", Content: "Invalid message format"})
conn.WriteMessage(websocket.TextMessage, errMsg)
continue
}
log.Printf("[%s] %s: %s", msg.Room, msg.Sender, msg.Content)
// Send response
resp, _ := json.Marshal(ChatMessage{
Type: "message",
Room: msg.Room,
Sender: "Server",
Content: "Message received: " + msg.Content,
})
conn.WriteMessage(websocket.TextMessage, resp)
case websocket.BinaryMessage:
// Handle binary data (files, images, etc.)
log.Printf("Binary message received: %d bytes", len(data))
conn.WriteMessage(websocket.BinaryMessage, data)
case websocket.CloseMessage:
log.Println("Client close request")
return
}
}
}
Ping/Pong Heartbeatβ
package main
import (
"log"
"net/http"
"time"
"github.com/gorilla/websocket"
)
const (
writeWait = 10 * time.Second // write timeout
pongWait = 60 * time.Second // pong wait time
pingPeriod = 54 * time.Second // ping send interval (shorter than pongWait)
maxMessageSize = 512 * 1024 // max message size (512KB)
)
var upgrader = websocket.Upgrader{CheckOrigin: func(r *http.Request) bool { return true }}
type Client struct {
conn *websocket.Conn
send chan []byte
}
func (c *Client) readPump() {
defer func() {
c.conn.Close()
close(c.send)
}()
c.conn.SetReadLimit(maxMessageSize)
c.conn.SetReadDeadline(time.Now().Add(pongWait))
// Refresh read deadline on pong receipt
c.conn.SetPongHandler(func(string) error {
c.conn.SetReadDeadline(time.Now().Add(pongWait))
return nil
})
for {
_, message, err := c.conn.ReadMessage()
if err != nil {
if websocket.IsUnexpectedCloseError(err,
websocket.CloseGoingAway, websocket.CloseAbnormalClosure) {
log.Printf("Read error: %v", err)
}
break
}
log.Printf("Received: %s", message)
c.send <- message // echo
}
}
func (c *Client) writePump() {
ticker := time.NewTicker(pingPeriod)
defer func() {
ticker.Stop()
c.conn.Close()
}()
for {
select {
case message, ok := <-c.send:
c.conn.SetWriteDeadline(time.Now().Add(writeWait))
if !ok {
c.conn.WriteMessage(websocket.CloseMessage, []byte{})
return
}
if err := c.conn.WriteMessage(websocket.TextMessage, message); err != nil {
return
}
case <-ticker.C:
// Send ping periodically
c.conn.SetWriteDeadline(time.Now().Add(writeWait))
if err := c.conn.WriteMessage(websocket.PingMessage, nil); err != nil {
return
}
}
}
}
func serveWs(w http.ResponseWriter, r *http.Request) {
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
return
}
client := &Client{
conn: conn,
send: make(chan []byte, 256),
}
// Separate read/write into separate goroutines
go client.writePump()
go client.readPump()
}
Practical Example: Multi-Room Real-Time Chat Serverβ
package main
import (
"encoding/json"
"log"
"net/http"
"sync"
"time"
"github.com/gorilla/websocket"
)
// Message chat message
type Message struct {
Type string `json:"type"` // "message", "join", "leave", "system"
Room string `json:"room"`
Sender string `json:"sender"`
Content string `json:"content"`
Time time.Time `json:"time"`
}
// Client connected client
type Client struct {
hub *Hub
conn *websocket.Conn
send chan []byte
username string
room string
}
// Hub manages clients per room
type Hub struct {
mu sync.RWMutex
rooms map[string]map[*Client]bool // room β clients
join chan *Client
leave chan *Client
message chan *RoomMessage
}
type RoomMessage struct {
room string
message []byte
}
func NewHub() *Hub {
return &Hub{
rooms: make(map[string]map[*Client]bool),
join: make(chan *Client),
leave: make(chan *Client),
message: make(chan *RoomMessage, 256),
}
}
func (h *Hub) Run() {
for {
select {
case client := <-h.join:
h.mu.Lock()
if h.rooms[client.room] == nil {
h.rooms[client.room] = make(map[*Client]bool)
}
h.rooms[client.room][client] = true
h.mu.Unlock()
// Join notification
msg, _ := json.Marshal(Message{
Type: "system",
Room: client.room,
Content: client.username + " has joined",
Time: time.Now(),
})
h.broadcast(client.room, msg, nil)
case client := <-h.leave:
h.mu.Lock()
if clients, ok := h.rooms[client.room]; ok {
delete(clients, client)
if len(clients) == 0 {
delete(h.rooms, client.room)
}
}
h.mu.Unlock()
close(client.send)
// Leave notification
msg, _ := json.Marshal(Message{
Type: "system",
Room: client.room,
Content: client.username + " has left",
Time: time.Now(),
})
h.broadcast(client.room, msg, nil)
case rm := <-h.message:
h.broadcast(rm.room, rm.message, nil)
}
}
}
// broadcast send message to all clients in the room
func (h *Hub) broadcast(room string, message []byte, except *Client) {
h.mu.RLock()
defer h.mu.RUnlock()
for client := range h.rooms[room] {
if client == except {
continue
}
select {
case client.send <- message:
default:
// Remove connection if buffer is full
go func(c *Client) { h.leave <- c }(client)
}
}
}
func (c *Client) readPump() {
defer func() { c.hub.leave <- c }()
c.conn.SetReadLimit(512 * 1024)
c.conn.SetReadDeadline(time.Now().Add(60 * time.Second))
c.conn.SetPongHandler(func(string) error {
c.conn.SetReadDeadline(time.Now().Add(60 * time.Second))
return nil
})
for {
_, data, err := c.conn.ReadMessage()
if err != nil {
break
}
var incoming Message
if err := json.Unmarshal(data, &incoming); err != nil {
continue
}
outgoing, _ := json.Marshal(Message{
Type: "message",
Room: c.room,
Sender: c.username,
Content: incoming.Content,
Time: time.Now(),
})
c.hub.message <- &RoomMessage{room: c.room, message: outgoing}
}
}
func (c *Client) writePump() {
ticker := time.NewTicker(54 * time.Second)
defer func() {
ticker.Stop()
c.conn.Close()
}()
for {
select {
case message, ok := <-c.send:
c.conn.SetWriteDeadline(time.Now().Add(10 * time.Second))
if !ok {
c.conn.WriteMessage(websocket.CloseMessage, []byte{})
return
}
if err := c.conn.WriteMessage(websocket.TextMessage, message); err != nil {
return
}
case <-ticker.C:
c.conn.SetWriteDeadline(time.Now().Add(10 * time.Second))
if err := c.conn.WriteMessage(websocket.PingMessage, nil); err != nil {
return
}
}
}
}
var upgrader = websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 1024,
CheckOrigin: func(r *http.Request) bool { return true },
}
func serveWs(hub *Hub, w http.ResponseWriter, r *http.Request) {
username := r.URL.Query().Get("username")
room := r.URL.Query().Get("room")
if username == "" || room == "" {
http.Error(w, "username and room parameters are required", http.StatusBadRequest)
return
}
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
log.Printf("Upgrade failed: %v", err)
return
}
client := &Client{
hub: hub,
conn: conn,
send: make(chan []byte, 256),
username: username,
room: room,
}
hub.join <- client
go client.writePump()
go client.readPump()
}
func main() {
hub := NewHub()
go hub.Run()
http.HandleFunc("/ws", func(w http.ResponseWriter, r *http.Request) {
serveWs(hub, w, r)
})
// Room list API
http.HandleFunc("/rooms", func(w http.ResponseWriter, r *http.Request) {
hub.mu.RLock()
rooms := make(map[string]int)
for room, clients := range hub.rooms {
rooms[room] = len(clients)
}
hub.mu.RUnlock()
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(rooms)
})
log.Println("Chat server started: ws://localhost:8080/ws?username=john&room=general")
// Test: wscat -c "ws://localhost:8080/ws?username=john&room=general"
log.Fatal(http.ListenAndServe(":8080", nil))
}
Pro Tipsβ
1. Separate read/write goroutines: Since WebSocket connections handle simultaneous reads and writes, always process them in separate goroutines
2. Validate Origin: In production, always implement CheckOrigin to prevent CSRF
CheckOrigin: func(r *http.Request) bool {
origin := r.Header.Get("Origin")
return origin == "https://example.com"
},
3. Limit message size: Use conn.SetReadLimit() to prevent large message bombs
4. Tune buffer channel size: The 256 in make(chan []byte, 256) is the maximum queue size per client. Set it appropriately so that slow clients do not block the server
5. Use structured message protocol: Define JSON message types instead of raw strings to make the client/server protocol explicit