Adler is a lightweight WebSocket server toolkit for Go. It gives you a small, focused API for upgrading HTTP requests, handling sessions, broadcasting messages, and organizing clients into rooms.
- WebSocket upgrade from
net/http - Session lifecycle hooks
- Text, binary, and JSON messaging
- Protocol-aware JSON or Protobuf serialization (
Write,BroadcastAny) - Global broadcast, filtered broadcast, and targeted session sends
- Rooms with join, leave, and room-level broadcast helpers
- Per-session key-value storage
- Optional access to the underlying request, protocol, and connection
- Add a dedicated matchmaker module for queue creation, queue management, and match orchestration.
- Add ELO-based matchmaking with configurable rating buckets and match quality rules.
go get github.com/catalinfl/adlerpackage main
import (
"log"
"net/http"
"github.com/catalinfl/adler"
)
func main() {
a := adler.New()
a.HandleConnect(func(s *adler.Session) {
log.Println("connected:", s.RemoteAddr())
})
a.HandleMessage(func(s *adler.Session, msg []byte) {
_ = s.WriteText([]byte("echo: " + string(msg)))
})
a.HandleError(func(s *adler.Session, err error) {
log.Println("ws error:", err)
})
http.HandleFunc("/ws", func(w http.ResponseWriter, r *http.Request) {
if err := a.HandleRequest(w, r); err != nil {
log.Println("handle request:", err)
}
})
log.Fatal(http.ListenAndServe(":8080", nil))
}Use adler.New() to build a server instance. You can pass configuration options at construction time:
a := adler.New(
adler.WithDispatchAsync(true),
adler.WithMessageBufferSize(256),
adler.WithPingPeriod(30),
)Handlers are set on the Adler instance and are called during the session lifecycle.
HandleConnectruns after a session is registered.HandleMessagereceives text frames.HandleMessageBinaryreceives binary frames.HandlePongreceives pong frames.HandleClosereceives close code and reason.HandleSentMessageandHandleSentMessageBinaryrun after server writes succeed.HandleErrorreceives runtime errors from the session loop.OnRoomJoinandOnRoomLeavereceive room membership events.
Example:
a.HandleConnect(func(s *adler.Session) {
s.Set("userID", "123")
})
a.HandleClose(func(s *adler.Session, code int, reason string) {
log.Printf("client closed: code=%d reason=%q", code, reason)
})Call HandleRequest from an HTTP handler. It upgrades the connection and blocks until the session ends.
http.HandleFunc("/ws", func(w http.ResponseWriter, r *http.Request) {
_ = a.HandleRequest(w, r)
})Session is the object you receive in callbacks. It exposes message writers and a small key-value store.
Messaging helpers:
Write(any)(protocol-aware JSON text or Protobuf binary)WriteText([]byte)WriteTextWithDeadline([]byte, time.Duration)WriteBinary([]byte)WriteBinaryWithDeadline([]byte, time.Duration)WriteJSON(adler.Map)WriteJSONWithDeadline(any, time.Duration)Close(...[]byte)
Protocol-aware helpers (Write, BroadcastAny, Room.BroadcastAny) use the server
protocol (JSON by default). Set adler.WithProtocol(adler.Protobuf) to emit
protobuf binary frames.
Storage helpers:
Set(key, value)SetNX(key, value)Get(key)GetString(key)GetInt(key)GetInt64(key)GetFloat(key)GetBool(key)Has(key)Unset(key)Keys()Values()Clear()Incr(key)andDecr(key)for*int64counters
Metadata:
Request()returns the original HTTP requestProtocol()returns the HTTP protocol string used during upgradeLocalAddr()andRemoteAddr()expose the connection addressesRoom()returns the current roomUnsafeConn()exposes the raw network connection when you need low-level control
Example session storage:
a.HandleConnect(func(s *adler.Session) {
s.Set("role", "admin")
s.SetNX("seen", true)
})
a.HandleMessage(func(s *adler.Session, msg []byte) {
role, _ := s.GetString("role")
_ = s.WriteText([]byte("role=" + role + " msg=" + string(msg)))
})Use server-level broadcast helpers when you want to send to multiple sessions.
Broadcast([]byte)sends text to all connected sessionsBroadcastFilter([]byte, func(*Session) bool)sends text to matching sessionsBroadcastBinary([]byte)sends binary to all connected sessionsBroadcastBinaryFilter([]byte, func(*Session) bool)sends binary to matching sessionsBroadcastJSON(adler.Map)broadcasts JSONBroadcastJSONFilter(adler.Map, func(*Session) bool)broadcasts JSON to matching sessionsBroadcastAny(any)broadcasts using the configured protocolBroadcastOthers([]byte, *Session)sends to everyone except the target sessionSendTo([]byte, *Session)sends only to one session
Example:
a.Broadcast([]byte("server says hello"))
a.SendTo([]byte("private message"), session)
a.BroadcastFilter([]byte("admins only"), func(s *adler.Session) bool {
role, _ := s.GetString("role")
return role == "admin"
})Rooms help you manage subsets of sessions.
room := a.NewRoom("lobby")
a.HandleConnect(func(s *adler.Session) {
_ = room.Join(s)
})
room.Broadcast([]byte("welcome to the lobby"))Room helpers:
NewRoom(name)returns an existing room or creates oneDeleteRoom(name)removes a room manually when it is emptyName()returns the room nameLen()returns the number of membersSessions()returns a snapshot of current membersJoin(*Session)adds a session to the roomLeave(*Session)removes a sessionOpenRoom()allows joins againCloseRoom()blocks new joinsBroadcast,BroadcastBinary,BroadcastFilter,BroadcastJSON,BroadcastJSONFilter,BroadcastAny
The matchmaker module provides queue-based matchmaking that groups sessions into rooms automatically. It uses a background goroutine to manage queues and ensure fair player distribution.
Features:
- Non-blocking queue operations:
AddToQueue()andRemoveFromQueue()send commands to a worker goroutine - Main and waiting queues: Keeps a main queue and an optional waiting queue when the main queue reaches capacity
- Automatic room creation: Creates Adler rooms when enough players are queued
- JSON event notifications: Sends events to sessions as they move through the queue
Matchmaker queue events are defined in matchmaker/matchmaking.proto and
emitted as protobuf QueueStatus messages. When the Adler protocol is JSON (default),
these events are marshaled to the same JSON shapes as before:
"queue_joined"- Session added to main queue"wait_queue_joined"- Session added to waiting queue (main is full)"promoted_to_queue"- Session promoted from waiting to main queue"match_found"- Match room created withroom_idand player count
Example:
import "github.com/catalinfl/adler/matchmaker"
mm := matchmaking.NewMatchmaker(a,
matchmaking.WithRoomSize(4),
matchmaking.WithQueueLength(20),
)
a.HandleConnect(func(s *adler.Session) {
_ = mm.AddToQueue(s)
})
a.HandleMessage(func(s *adler.Session, msg []byte) {
if string(msg) == "leave_queue" {
mm.RemoveFromQueue(s)
}
})
// Use adler.WithProtocol(adler.Protobuf) to send queue events as protobuf frames.If the client sends a close frame, HandleClose receives the close status code and reason.
a.HandleClose(func(s *adler.Session, code int, reason string) {
log.Printf("close: code=%d reason=%q", code, reason)
})Use these options with adler.New(...):
WithWriteWait(time.Duration)interprets the argument as seconds;WithWriteWait(10)means 10 secondsWithPongWait(time.Duration)interprets the argument as seconds;WithPongWait(60)means 60 seconds, andWithPongWait(0)disables idle disconnectsWithPingPeriod(time.Duration)interprets the argument as seconds;WithPingPeriod(54)means 54 secondsWithMessageBufferSize(int)sets the outbound queue size; start with 64-256 and increase only if you hitErrBufferFullunder normal burstsWithDispatchAsync(bool)switches inbound dispatch to goroutine-per-message when enabledWithDeleteRoomOnEmpty(bool)controls automatic room deletion when the last session leaves (default:true)WithProtocol(adler.Protocol)selectsadler.JSON(default) oradler.Protobuffor protocol-aware messaging
- Session storage is protected by an internal mutex.
- Server broadcast methods are safe for normal concurrent use.
UnsafeConn()bypasses Adler's internal coordination; only use it if you manage access carefully.
room := a.NewRoom("test")
a.HandleConnect(func(s *adler.Session) {
_ = room.Join(s)
})
room.HandleJoin(func(s *adler.Session) {
_ = s.WriteText([]byte("joined room"))
})This project is licensed under the MIT License. See LICENSE for the full text.