https://github.com/catalinfl/adler
High performance WebSocket server library built on top of gobwas/ws
https://github.com/catalinfl/adler
broadcast game-server go gobwas-ws golang hub multiplayer networking real-time rooms session websocket websocket-server
Last synced: about 2 months ago
JSON representation
High performance WebSocket server library built on top of gobwas/ws
- Host: GitHub
- URL: https://github.com/catalinfl/adler
- Owner: catalinfl
- License: mit
- Created: 2026-03-29T20:52:59.000Z (3 months ago)
- Default Branch: master
- Last Pushed: 2026-04-25T22:05:09.000Z (about 2 months ago)
- Last Synced: 2026-04-26T00:18:23.992Z (about 2 months ago)
- Topics: broadcast, game-server, go, gobwas-ws, golang, hub, multiplayer, networking, real-time, rooms, session, websocket, websocket-server
- Language: Go
- Homepage:
- Size: 127 KB
- Stars: 2
- Watchers: 0
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- Changelog: CHANGELOG.md
- License: LICENSE
Awesome Lists containing this project
README
# Adler
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.
## Features
- WebSocket upgrade from `net/http`
- Session lifecycle hooks
- Text, binary, and JSON messaging
- 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
## Roadmap
- [x] 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.
## Installation
```bash
go get github.com/catalinfl/adler
```
## Quick Start
```go
package 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))
}
```
## How The API Works
### 1. Create a server
Use `adler.New()` to build a server instance. You can pass configuration options at construction time:
```go
a := adler.New(
adler.WithDispatchAsync(true),
adler.WithMessageBufferSize(256),
adler.WithPingPeriod(30*time.Second),
)
```
### 2. Register handlers before serving requests
Handlers are set on the `Adler` instance and are called during the session lifecycle.
- `HandleConnect` runs after a session is registered.
- `HandleMessage` receives text frames.
- `HandleMessageBinary` receives binary frames.
- `HandlePong` receives pong frames.
- `HandleClose` receives close code and reason.
- `HandleSentMessage` and `HandleSentMessageBinary` run after server writes succeed.
- `HandleError` receives runtime errors from the session loop.
- `OnRoomJoin` and `OnRoomLeave` receive room membership events.
Example:
```go
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)
})
```
### 3. Serve the websocket endpoint
Call `HandleRequest` from an HTTP handler. It upgrades the connection and blocks until the session ends.
```go
http.HandleFunc("/ws", func(w http.ResponseWriter, r *http.Request) {
_ = a.HandleRequest(w, r)
})
```
### 4. Work with a Session
`Session` is the object you receive in callbacks. It exposes message writers and a small key-value store.
Messaging helpers:
- `WriteText([]byte)`
- `WriteTextWithDeadline([]byte, time.Duration)`
- `WriteBinary([]byte)`
- `WriteBinaryWithDeadline([]byte, time.Duration)`
- `WriteJSON(adler.Map)`
- `WriteJSONWithDeadline(any, time.Duration)`
- `Close(...[]byte)`
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)` and `Decr(key)` for `*int64` counters
Metadata:
- `Request()` returns the original HTTP request
- `Protocol()` returns the HTTP protocol string used during upgrade
- `LocalAddr()` and `RemoteAddr()` expose the connection addresses
- `Room()` returns the current room
- `UnsafeConn()` exposes the raw network connection when you need low-level control
Example session storage:
```go
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)))
})
```
### 5. Broadcast to all or some clients
Use server-level broadcast helpers when you want to send to multiple sessions.
- `Broadcast([]byte)` sends text to all connected sessions
- `BroadcastFilter([]byte, func(*Session) bool)` sends text to matching sessions
- `BroadcastBinary([]byte)` sends binary to all connected sessions
- `BroadcastBinaryFilter([]byte, func(*Session) bool)` sends binary to matching sessions
- `BroadcastJSON(adler.Map)` broadcasts JSON
- `BroadcastJSONFilter(adler.Map, func(*Session) bool)` broadcasts JSON to matching sessions
- `BroadcastOthers([]byte, *Session)` sends to everyone except the target session
- `SendTo([]byte, *Session)` sends only to one session
Example:
```go
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"
})
```
### 6. Group clients in rooms
Rooms help you manage subsets of sessions.
```go
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 one
- `DeleteRoom(name)` removes a room manually when it is empty
- `Name()` returns the room name
- `Len()` returns the number of members
- `Sessions()` returns a snapshot of current members
- `Join(*Session)` adds a session to the room
- `Leave(*Session)` removes a session
- `OpenRoom()` allows joins again
- `CloseRoom()` blocks new joins
- `Broadcast`, `BroadcastBinary`, `BroadcastFilter`, `BroadcastJSON`, `BroadcastJSONFilter`
### 7. Matchmaking with the Matchmaker Module
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()` and `RemoveFromQueue()` 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
The matchmaker sends these JSON messages:
- `"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 with `room_id` and player count
Example:
```go
import "github.com/catalinfl/adler/matchmaker"
mm := matchmaking.NewMatchmaker(a,
matchmaking.WithRoomSize(4),
matchmaking.WithMaxQueue(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)
}
})
```
### 8. Close handling
If the client sends a close frame, `HandleClose` receives the close status code and reason.
```go
a.HandleClose(func(s *adler.Session, code int, reason string) {
log.Printf("close: code=%d reason=%q", code, reason)
})
```
## Configuration
Use these options with `adler.New(...)`:
- `WithWriteWait(time.Duration)` interprets the argument as seconds; `WithWriteWait(10)` means 10 seconds
- `WithPongWait(time.Duration)` interprets the argument as seconds; `WithPongWait(60)` means 60 seconds, and `WithPongWait(0)` disables idle disconnects
- `WithPingPeriod(time.Duration)` interprets the argument as seconds; `WithPingPeriod(54)` means 54 seconds
- `WithMessageBufferSize(int)` sets the outbound queue size; start with 64-256 and increase only if you hit `ErrBufferFull` under normal bursts
- `WithDispatchAsync(bool)` switches inbound dispatch to goroutine-per-message when enabled
- `WithDeleteRoomOnEmpty(bool)` controls automatic room deletion when the last session leaves (default: `true`)
## Notes On Concurrency
- 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.
## Minimal Room Example
```go
room := a.NewRoom("test")
a.HandleConnect(func(s *adler.Session) {
_ = room.Join(s)
})
room.HandleJoin(func(s *adler.Session) {
_ = s.WriteText([]byte("joined room"))
})
```
## License
This project is licensed under the MIT License. See [LICENSE](LICENSE) for the full text.