WebSocket
Full-duplex real-time communication
WebSocket provides full-duplex, bidirectional communication between client and server over a single TCP connection. Forge handles the HTTP upgrade, connection management, and provides a clean Connection interface for reading and writing messages.
Basic Usage
Register a WebSocket route using router.WebSocket(). The handler receives both the request context and a Connection.
r := app.Router()
r.WebSocket("/ws/echo", func(ctx forge.Context, conn forge.Connection) error {
defer conn.Close()
for {
msg, err := conn.Read()
if err != nil {
return err // Connection closed or error
}
if err := conn.Write(msg); err != nil {
return err
}
}
})WebSocketHandler Signature
type WebSocketHandler func(ctx forge.Context, conn forge.Connection) errorThe handler is called once per WebSocket connection. When the handler returns, the connection is closed.
Connection Interface
The Connection interface provides all methods needed to interact with a WebSocket connection.
type Connection interface {
// ID returns a unique connection identifier
ID() string
// Read reads the next message (blocks until message arrives)
Read() ([]byte, error)
// ReadJSON reads and unmarshals JSON from the connection
ReadJSON(v any) error
// Write sends a text message
Write(data []byte) error
// WriteJSON marshals and sends JSON
WriteJSON(v any) error
// Close closes the connection
Close() error
// Context returns the connection's context (cancelled on close)
Context() context.Context
// RemoteAddr returns the client's address
RemoteAddr() string
// LocalAddr returns the server's address
LocalAddr() string
}JSON Messaging
Use ReadJSON and WriteJSON for structured message exchange without manual marshaling.
type ClientMessage struct {
Type string `json:"type"`
Payload any `json:"payload"`
}
type ServerMessage struct {
Type string `json:"type"`
Data any `json:"data"`
Timestamp int64 `json:"timestamp"`
}
r.WebSocket("/ws/api", func(ctx forge.Context, conn forge.Connection) error {
defer conn.Close()
for {
var msg ClientMessage
if err := conn.ReadJSON(&msg); err != nil {
return err
}
response := ServerMessage{
Type: "response",
Data: processMessage(msg),
Timestamp: time.Now().Unix(),
}
if err := conn.WriteJSON(response); err != nil {
return err
}
}
})Chat Room Example
A complete multi-room chat server demonstrating connection management, broadcasting, and graceful shutdown.
package main
import (
"sync"
"time"
"github.com/xraph/forge"
)
type ChatRoom struct {
mu sync.RWMutex
clients map[string]forge.Connection
}
func NewChatRoom() *ChatRoom {
return &ChatRoom{
clients: make(map[string]forge.Connection),
}
}
func (r *ChatRoom) Join(conn forge.Connection) {
r.mu.Lock()
defer r.mu.Unlock()
r.clients[conn.ID()] = conn
}
func (r *ChatRoom) Leave(conn forge.Connection) {
r.mu.Lock()
defer r.mu.Unlock()
delete(r.clients, conn.ID())
}
func (r *ChatRoom) Broadcast(sender string, msg any) {
r.mu.RLock()
defer r.mu.RUnlock()
for id, client := range r.clients {
if id != sender {
client.WriteJSON(msg)
}
}
}
type ChatMessage struct {
Type string `json:"type"`
User string `json:"user"`
Content string `json:"content"`
Time string `json:"time"`
}
var rooms = struct {
mu sync.RWMutex
rooms map[string]*ChatRoom
}{rooms: make(map[string]*ChatRoom)}
func getOrCreateRoom(name string) *ChatRoom {
rooms.mu.Lock()
defer rooms.mu.Unlock()
if room, ok := rooms.rooms[name]; ok {
return room
}
room := NewChatRoom()
rooms.rooms[name] = room
return room
}
func main() {
app := forge.New(forge.WithAppName("chat-server"))
r := app.Router()
r.WebSocket("/ws/chat/:room", func(ctx forge.Context, conn forge.Connection) error {
roomName := ctx.Param("room")
userName := ctx.Query("user")
if userName == "" {
userName = "anonymous"
}
room := getOrCreateRoom(roomName)
room.Join(conn)
defer room.Leave(conn)
// Announce join
room.Broadcast(conn.ID(), ChatMessage{
Type: "system",
User: userName,
Content: userName + " joined the room",
Time: time.Now().Format(time.RFC3339),
})
// Read messages
for {
var incoming ChatMessage
if err := conn.ReadJSON(&incoming); err != nil {
// Announce leave on disconnect
room.Broadcast(conn.ID(), ChatMessage{
Type: "system",
User: userName,
Content: userName + " left the room",
Time: time.Now().Format(time.RFC3339),
})
return nil
}
incoming.User = userName
incoming.Type = "message"
incoming.Time = time.Now().Format(time.RFC3339)
room.Broadcast(conn.ID(), incoming)
}
},
forge.WithSummary("Chat room"),
forge.WithTags("chat"),
forge.WithWebSocketMessages(&ChatMessage{}, &ChatMessage{}),
)
app.Run()
}Using Context for Lifecycle
The connection's Context() is cancelled when the connection closes. Use it to coordinate goroutines.
r.WebSocket("/ws/live", func(ctx forge.Context, conn forge.Connection) error {
defer conn.Close()
// Background sender
go func() {
ticker := time.NewTicker(5 * time.Second)
defer ticker.Stop()
for {
select {
case <-conn.Context().Done():
return
case t := <-ticker.C:
conn.WriteJSON(map[string]string{
"type": "heartbeat",
"time": t.Format(time.RFC3339),
})
}
}
}()
// Read loop
for {
msg, err := conn.Read()
if err != nil {
return nil // Connection closed
}
// Process message
_ = msg
}
})Route Options
WebSocket routes support the same route options as regular HTTP routes.
r.WebSocket("/ws/protected", handler,
forge.WithName("protectedWS"),
forge.WithSummary("Protected WebSocket"),
forge.WithTags("streaming", "auth"),
forge.WithMiddleware(authMiddleware),
forge.WithAuth("jwt"),
forge.WithWebSocketMessages(&Request{}, &Response{}),
)Connection Information
r.WebSocket("/ws/info", func(ctx forge.Context, conn forge.Connection) error {
info := map[string]string{
"connectionId": conn.ID(), // e.g., "ws_1700000000000"
"remoteAddr": conn.RemoteAddr(), // e.g., "192.168.1.10:54321"
"localAddr": conn.LocalAddr(), // e.g., "0.0.0.0:8080"
}
return conn.WriteJSON(info)
})Connection IDs are generated using nanosecond timestamps (ws_ prefix) and are unique within a running server instance.
How is this guide?