snapws

package module
v1.1.3 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 19, 2025 License: MIT Imports: 23 Imported by: 0

README

snapws

Go Reference License

SnapWS is a minimal WebSocket library for Go.

It takes care of ping/pong, close frames, connection safety, rate limiting, and lifecycle management so you can just connect, read, and write — without boilerplate or extra complexity.

🧠 Why?
Using gorilla/websocket often felt like overkill. You had to write a lot of code, worry about race conditions, manually handle timeouts, and understand the WebSocket protocol more deeply than necessary.

snapws handles the boring stuff for you — so you can just send and receive messages.


✨ Features

  • ✅ Minimal and easy to use API.
  • ✅ Fully passes the autobahn-testsuite (not including PMCE)
  • ✅ Automatic handling of ping/pong and close frames.
  • ✅ Connection manager (useful when communicating between different clients like chat apps).
  • ✅ Room manager.
  • ✅ Rate limiter.
  • ✅ Message batching. (for more info)
  • ✅ Written completely in standard library amd Go offical libraries, no external libraries imported.
  • ✅ Support for middlewares and connect/disconnect hooks.

API Reference

API Reference

Examples

Examples

🚀 Getting Started

Install
go get github.com/Atheer-Ganayem/SnapWS
Basic echo example
package main

import (
	"context"
	"fmt"
	"net/http"

	snapws "github.com/Atheer-Ganayem/SnapWS"
)

var upgrader *snapws.Upgrader

func main() {
	upgrader = snapws.NewUpgrader(nil)

	http.HandleFunc("/echo", handler)

	fmt.Println("Server listening on port 8080")
	http.ListenAndServe(":8080", nil)
}

func handler(w http.ResponseWriter, r *http.Request) {
	conn, err := upgrader.Upgrade(w, r)
	if err != nil {
		return
	}
	defer conn.Close()

	for {
		data, err := conn.ReadString()
		if snapws.IsFatalErr(err) {
			return // Connection closed
		} else if err != nil {
			fmt.Println("Non-fatal error:", err)
			continue
		}

		err = conn.SendString(context.TODO(), data)
		if snapws.IsFatalErr(err) {
			return // Connection closed
		} else if err != nil {
			fmt.Println("Non-fatal error:", err)
			continue
		}
	}
}

Benchmark

For the benchmark i used lesismal/go-websocket-benchmark

20250809 16:38.55.590 [Connections] Report

Framework TPS Min Avg Max TP50 TP75 TP90 TP95 TP99 Used Total Success Failed Concurrency
snapws 28684 30ns 55.99ms 346.35ms 50ns 70ns 275.47ms 298.87ms 336.63ms 348.62ms 10000 10000 0 2000
fasthttp 28642 40ns 58.34ms 347.25ms 60ns 101ns 290.28ms 306.14ms 329.82ms 349.14ms 10000 10000 0 2000
gobwas 29390 40ns 57.45ms 336.80ms 60ns 90ns 291.22ms 305.66ms 329.38ms 340.25ms 10000 10000 0 2000
gorilla 30774 30ns 54.98ms 324.65ms 60ns 71ns 270.29ms 289.05ms 318.33ms 324.94ms 10000 10000 0 2000
gws 31822 30ns 53.05ms 312.51ms 40ns 60ns 263.19ms 280.10ms 307.31ms 314.25ms 10000 10000 0 2000
gws_std 29149 30ns 57.51ms 341.75ms 50ns 70ns 282.57ms 302.46ms 333.35ms 343.06ms 10000 10000 0 2000
nbio_blocking 31467 30ns 51.78ms 316.16ms 60ns 90ns 256.40ms 273.12ms 297.19ms 317.79ms 10000 10000 0 2000
nbio_mixed 31634 30ns 52.15ms 314.56ms 40ns 60ns 254.83ms 276.40ms 309.25ms 316.11ms 10000 10000 0 2000
nbio_nonblocking 31450 30ns 53.50ms 315.71ms 50ns 70ns 265.12ms 291.28ms 311.19ms 317.96ms 10000 10000 0 2000
nbio_std 31475 30ns 53.96ms 315.56ms 51ns 70ns 268.46ms 291.03ms 309.30ms 317.71ms 10000 10000 0 2000
nettyws 28249 30ns 60.69ms 353.68ms 60ns 71ns 298.53ms 320.95ms 343.66ms 353.99ms 10000 10000 0 2000
nhooyr 27858 40ns 60.12ms 356.48ms 61ns 150ns 296.83ms 314.80ms 342.78ms 358.95ms 10000 10000 0 2000
quickws 30345 30ns 55.77ms 324.83ms 50ns 70ns 274.96ms 291.11ms 319.53ms 329.54ms 10000 10000 0 2000
greatws 31196 30ns 54.83ms 316.69ms 60ns 80ns 271.19ms 287.38ms 309.14ms 320.55ms 10000 10000 0 2000
greatws_event 31293 30ns 54.57ms 316.67ms 60ns 100ns 269.73ms 285.75ms 308.87ms 319.56ms 10000 10000 0 2000

20250809 16:38.55.603 [BenchEcho] Report

Framework TPS EER Min Avg Max TP50 TP75 TP90 TP95 TP99 Used Total Success Failed Conns Concurrency Payload CPU Min CPU Avg CPU Max MEM Min MEM Avg MEM Max
snapws 236363 343.02 36.54us 42.18ms 155.55ms 40.15ms 44.80ms 51.95ms 54.03ms 60.17ms 8.46s 2000000 2000000 0 10000 10000 1024 492.55 689.07 721.93 278.23M 279.79M 282.48M
fasthttp 239613 377.94 31.85us 41.61ms 170.61ms 39.14ms 45.88ms 51.33ms 54.83ms 68.28ms 8.35s 2000000 2000000 0 10000 10000 1024 378.65 634.00 685.17 255.04M 263.28M 272.17M
gobwas 185234 202.44 42.28us 53.84ms 313.39ms 48.59ms 56.89ms 68.78ms 78.44ms 163.85ms 10.80s 2000000 2000000 0 10000 10000 1024 884.37 914.99 938.72 373.06M 373.92M 374.44M
gorilla 237741 368.23 31.83us 41.94ms 133.16ms 39.40ms 46.09ms 51.82ms 54.22ms 68.08ms 8.41s 2000000 2000000 0 10000 10000 1024 435.74 645.64 690.66 255.05M 263.41M 272.30M
gws 230658 349.94 32.61us 43.21ms 149.39ms 40.68ms 48.10ms 54.27ms 58.61ms 68.74ms 8.67s 2000000 2000000 0 10000 10000 1024 547.45 659.13 703.63 180.92M 195.75M 201.79M
gws_std 247967 376.79 30.24us 40.19ms 138.11ms 37.92ms 42.49ms 50.13ms 52.13ms 63.86ms 8.07s 2000000 2000000 0 10000 10000 1024 615.85 658.10 678.29 157.96M 181.97M 192.70M
nbio_blocking 245887 363.56 28.45us 40.52ms 143.64ms 38.32ms 42.78ms 50.39ms 52.63ms 61.22ms 8.13s 2000000 2000000 0 10000 10000 1024 666.77 676.33 685.51 172.87M 185.99M 194.12M
nbio_mixed 239852 382.37 27.60us 41.55ms 119.88ms 39.53ms 45.62ms 51.43ms 54.29ms 61.11ms 8.34s 2000000 2000000 0 10000 10000 1024 366.83 627.27 683.03 383.18M 439.26M 459.18M
nbio_nonblocking 222046 293.09 42.97us 44.92ms 169.98ms 42.77ms 49.27ms 56.72ms 61.13ms 71.59ms 9.01s 2000000 2000000 0 10000 10000 1024 745.24 757.62 767.76 59.64M 64.55M 67.52M
nbio_std 250083 373.14 31.71us 39.82ms 124.51ms 37.77ms 42.48ms 49.47ms 51.44ms 57.87ms 8.00s 2000000 2000000 0 10000 10000 1024 663.82 670.21 678.70 174.72M 178.93M 181.09M
nettyws 246737 370.00 24.45us 40.40ms 115.49ms 38.36ms 42.78ms 50.24ms 52.10ms 57.40ms 8.11s 2000000 2000000 0 10000 10000 1024 655.79 666.85 677.53 162.61M 163.68M 165.86M
nhooyr 194207 241.66 42.80us 51.35ms 173.62ms 49.38ms 55.01ms 61.00ms 63.37ms 71.74ms 10.30s 2000000 2000000 0 10000 10000 1024 471.50 803.63 848.54 365.11M 367.61M 370.11M
quickws 252792 388.74 29.61us 39.41ms 131.88ms 37.58ms 42.00ms 48.83ms 50.58ms 54.18ms 7.91s 2000000 2000000 0 10000 10000 1024 637.78 650.29 670.81 127.99M 129.62M 132.24M
greatws 232138 338.37 47.61us 42.96ms 120.72ms 41.19ms 46.60ms 53.14ms 55.58ms 61.57ms 8.62s 2000000 2000000 0 10000 10000 1024 593.87 686.06 705.71 53.09M 55.75M 57.70M
greatws_event 240881 386.32 42.47us 41.40ms 115.71ms 39.55ms 44.38ms 51.01ms 53.14ms 57.78ms 8.30s 2000000 2000000 0 10000 10000 1024 346.84 623.52 672.67 48.77M 52.11M 53.27M

20250809 16:38.55.610 [BenchRate] Report

Framework Duration EchoEER Packet Sent Bytes Sent Packet Recv Bytes Recv Conns SendRate Payload CPU Min CPU Avg CPU Max MEM Min MEM Avg MEM Max
snapws 10.00s 686.77 5709440 5.44G 5395180 5.15G 10000 200 1024 492.55 785.58 891.36 278.23M 281.49M 282.98M
fasthttp 10.00s 755.80 5997330 5.72G 5696392 5.43G 10000 200 1024 378.65 753.69 871.76 255.04M 282.46M 318.98M
gobwas 10.00s 381.06 3899360 3.72G 3547509 3.38G 10000 200 1024 554.21 930.96 1142.24 373.06M 380.17M 416.00M
gorilla 10.00s 729.47 5797780 5.53G 5460790 5.21G 10000 200 1024 435.74 748.59 860.77 255.05M 274.74M 286.30M
gws 10.00s 707.68 5573440 5.32G 5261550 5.02G 10000 200 1024 491.85 743.49 867.73 180.92M 210.12M 233.69M
gws_std 10.00s 738.69 5731410 5.47G 5399665 5.15G 10000 200 1024 241.92 730.98 966.15 157.96M 190.84M 201.57M
nbio_blocking 10.00s 714.49 5648280 5.39G 5313563 5.07G 10000 200 1024 238.71 743.69 954.78 172.87M 192.14M 196.99M
nbio_mixed 10.00s 724.91 5730050 5.46G 5399240 5.15G 10000 200 1024 366.83 744.81 861.81 383.18M 505.80M 579.18M
nbio_nonblocking 10.00s 649.80 5471200 5.22G 5148591 4.91G 10000 200 1024 294.85 792.33 1012.94 59.64M 98.42M 198.98M
nbio_std 10.00s 710.06 5559300 5.30G 5224786 4.98G 10000 200 1024 288.92 735.82 961.67 174.72M 187.30M 196.22M
nettyws 10.00s 731.41 5795280 5.53G 5485818 5.23G 10000 200 1024 214.93 750.04 1009.48 162.61M 169.25M 174.23M
nhooyr 10.00s 486.79 4862030 4.64G 4529391 4.32G 10000 200 1024 471.50 930.46 1095.54 365.11M 368.99M 370.61M
quickws 10.00s 723.30 5760040 5.49G 5446060 5.19G 10000 200 1024 354.88 752.95 942.73 113.04M 122.09M 132.24M
greatws 10.00s 643.54 5365640 5.12G 5004079 4.77G 10000 200 1024 555.87 777.59 911.84 53.09M 64.16M 75.27M
greatws_event 10.00s 685.13 5614890 5.35G 5254480 5.01G 10000 200 1024 346.84 766.93 899.18 48.77M 51.42M 53.27M

Documentation

Overview

Package snapws is a minimal WebSocket library for Go.

SnapWS makes WebSockets effortless: you connect, read, and write, while the library takes care of everything else — ping/pong, safe concurrent access, connection lifecycle management, rate-limiting, and many other things.

Philosophy

No boilerplate, no juggling goroutines, no protocol details. You write application logic; SnapWS handles the hard parts.

Features

- Minimal and easy to use API. - Fully passes the autobahn-testsuite - Automatic handling of ping/pong and close frames. - Connection management built-in (useful when communicating between different clients like chat apps). - Built-in easy to use rate limiter. - Written completely in standard library amd Go offical libraries (golang.org/x), no external libraries imported. - Support for middlewares and connect/disconnect hooks.

SnapWS is for developers who want WebSockets to “just work” with minimal effort and minimal code.

Index

Constants

View Source
const (
	CloseNormalClosure           uint16 = 1000
	CloseGoingAway               uint16 = 1001
	CloseProtocolError           uint16 = 1002
	CloseUnsupportedData         uint16 = 1003
	CloseInvalidFramePayloadData uint16 = 1007
	ClosePolicyViolation         uint16 = 1008
	CloseMessageTooBig           uint16 = 1009
	CloseMandatoryExtension      uint16 = 1010
	CloseInternalServerErr       uint16 = 1011
)
View Source
const (
	OpcodeContinuation = 0x0 // Continuation frame
	OpcodeText         = 0x1 // Text frame (UTF-8)
	OpcodeBinary       = 0x2 // Binary frame
	OpcodeClose        = 0x8 // Connection close
	OpcodePing         = 0x9 // Ping
	OpcodePong         = 0xA // Pong

)
View Source
const (
	// Drops the message without closing the connection.
	BackpressureDrop = iota
	// Drops and closes the connection.
	BackpressureClose
	// Blocks until the message is sent.
	//
	// Not recommended to use excpet for very very special cases.
	BackpressureWait
)
View Source
const (
	DefaultMaxMessageSize        = 1 << 20 // 1MB
	DefaultReadBufferSize        = 4096
	DefaultWriteBufferSize       = 4096
	DefaultBroadcastChannelsSize = 8
)
View Source
const GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
View Source
const MaxControlFramePayload = 125
View Source
const MaxHeaderSize = 14

Variables

View Source
var (
	ErrTest                    = errors.New("this is just an error for testing")
	ErrWrongMethod             = errors.New("wrong method, the request method must be GET")
	ErrMissingUpgradeHeader    = errors.New("mssing Upgrade header")
	ErrInvalidUpgradeHeader    = errors.New("invalid Upgrade header")
	ErrMissingConnectionHeader = errors.New("mssing connection header")
	ErrInvalidConnectionHeader = errors.New("invalid connection header")
	ErrMissingVersionHeader    = errors.New("mssing version header")
	ErrInvalidVersionHeader    = errors.New("invalid version header")
	ErrMissingSecKey           = errors.New("mssing Sec-WebSocket-Key header")
	ErrInvalidSecKey           = errors.New("invalid Sec-WebSocket-Key header")
	ErrUnsupportedSubProtocols = errors.New("unsupported Sec-WebSocket-Protocol")
	ErrConnNotFound            = errors.New("can't unregister a non existing connection")
	ErrInvalidOPCODE           = errors.New("invalid OPCODE")
	ErrTooLargePayload         = errors.New("payload length too large")
	ErrExpectedContinuation    = errors.New("invalid frame sequence: expected continuation")
	ErrInvalidControlFrame     = errors.New("invalid control frame")
	ErrUnnegotiatedRsvBits     = errors.New("receve unnegotiated reserved bits")

	ErrMessageTooLarge  = errors.New("message received from client was too large")
	ErrTooMuchFragments = errors.New("receive too much fragments for a signle message")
	ErrInvalidUTF8      = errors.New("invalid utf8 data")
	// ErrMessageTypeMismatch is returned when the received WebSocket message type
	// does not match the expected type (e.g., expecting text but received binary).
	ErrMessageTypeMismatch     = errors.New("websocket message type did not match expected type")
	ErrChannelClosed           = errors.New("channel is closed")
	ErrConnClosed              = errors.New("connection is closed")
	ErrWriterClosed            = errors.New("writer is closed")
	ErrWriterUnintialized      = errors.New("writer is not intialized")
	ErrWriterNotClosed         = errors.New("cannot get the next writer without closing the previous one")
	ErrInvalidPayloadLength    = errors.New("invalid payload length")
	ErrInsufficientHeaderSpace = errors.New("no enough space to write headers")
	ErrInternalServer          = errors.New("something went wrong")
	ErrTimeout                 = errors.New("time out")

	ErrPingAlreadySent = errors.New("ping already sent")

	// Batch errors
	ErrBatchingUninitialized = errors.New("uninitialized flusher or message batch")
	ErrFlusherClosed         = errors.New("flusher closed")
)

Functions

func GetArg added in v1.0.2

func GetArg[T any](args []any, index int) (T, bool)

A helper fucntion for room hooks.

Receives a type as a generic value, an args slice, and an index. If the args slice has a value at args[index] of the given type. it returns the casted value and true. Otherwise it returns the zero value of the given generic type and false.

func IsFatalErr

func IsFatalErr(err error) bool

Check if the given error is of type FatalError.

Types

type BackpressureStrategy added in v0.2.1

type BackpressureStrategy int

type BatchStrategy added in v1.1.0

type BatchStrategy int

BatchStrategy defines the method used for combining multiple messages into a single batch.

const (
	// StrategyJSON batches messages as elements in a JSON array and sends as binary websocket message.
	// Format: [message1, message2, message3, ...]
	StrategyJSON BatchStrategy = iota
	// StrategyLengthPrefix batches messages with 4-byte big-endian length prefixes before each message.
	// Format: [uint32][message1][uint32][message2][uint32][message3]...
	StrategyLengthPrefix
	// Custom strategy set by the user.
	StrategyCustom
)

type Conn

type Conn struct {

	// Empty string means its a raw websocket
	SubProtocol string
	MetaData    sync.Map
	// contains filtered or unexported fields
}

Conn represents a single WebSocket connection. It owns the underlying network connection and manages reading/writing frames, assembling messages, handling control frames (ping/pong/close), and lifecycle state.

func (*Conn) Batch added in v1.1.0

func (conn *Conn) Batch(data []byte) error

Batch adds raw byte data to the current message batch.

The data is stored as-is without any encoding or validation. For JSON batching, the data should already be valid JSON. For prefix batching, any binary data is acceptable.

This method will fail if:

  • Batching is not enabled
  • The flusher has been closed
  • The flusher's context has been cancelled

Returns:

  • error: nil on success, or an error describing why the message couldn't be added to the batch.

func (*Conn) BatchJSON added in v1.1.0

func (conn *Conn) BatchJSON(v interface{}) error

BatchJSON marshals the provided value to JSON and adds it to the current message batch.

This is a convenience method that combines json.Marshal with Batch. The JSON encoding is performed immediately, so any marshaling errors are returned synchronously.

Returns:

  • error: JSON marshaling error or batching error.

func (*Conn) Close

func (conn *Conn) Close()

Closes the conn normaly.

func (*Conn) CloseWithCode added in v0.2.7

func (conn *Conn) CloseWithCode(code uint16, reason string)

CloseWithCode closes the connection with the given code and reason.

func (*Conn) NetConn added in v0.4.5

func (conn *Conn) NetConn() net.Conn

Returns the underlying net conn.

func (*Conn) NextReader

func (conn *Conn) NextReader() (io.Reader, uint8, error)

NextReader returns an io.Reader for the next WebSocket message. It blocks until a data frame is available. The returned reader allows streaming the message payload frame-by-frame, and the second return value indicates the message type (e.g., Text or Binary). All errors return are of type snap.FatalError indicating that the error is fatal and the connection got closed.

func (*Conn) NextWriter

func (conn *Conn) NextWriter(ctx context.Context, msgType uint8) (*ConnWriter, error)

NextWriter locks the data write stream and returns a new writer for the given message type. Must call Close() on the returned writer to release the lock. The context given, is to be used for all the writer's functions as long is its not closed, This mean its used when its trying to optain the next writer and flushing. After you close the writer and call NextWriter again, you must give it a new context.

func (*Conn) Ping

func (conn *Conn) Ping() error

Ping sends a WebSocket ping frame and waits for it to be sent. Ping\Pong frames are already handeled by the library, you dont need to habdle them manually. If a ping has been sent and a pong frame hasn't been received yet, the function will return a non-nil error snapws.ErrPingAlreadySent. So, if you wish to send a ping manually, you should keep trying until the error is not snapws.ErrPingAlreadySent.

func (*Conn) ReadBinary

func (conn *Conn) ReadBinary() (data []byte, err error)

ReadBinary returns the binary payload from a WebSocket binary message.

If the received message is not of type binary, it returns snapws.ErrMessageTypeMismatch without closing the connection. The returned error must be checked. If it's of type snapws.FatalError, that indicates the connection was closed due to an I/O or protocol error. Any other error means the connection is still open, and you may retry or continue using it.

func (*Conn) ReadJSON

func (conn *Conn) ReadJSON(v any) error

ReadJSON reads a text WebSocket message and unmarshals its payload into the given value.

This method expects the message to be of type text and contain valid UTF-8 encoded JSON. If the message is not of type text, it returns snapws.ErrMessageTypeMismatch without closing the connection. if the text is not a valid json, an error will be returned without closing the connection. The returned error must be checked. If it's of type snapws.FatalError, that indicates the connection was closed due to an I/O or protocol error. Any other error means the connection is still open, and you may retry or continue using it.

func (*Conn) ReadMessage

func (conn *Conn) ReadMessage() (msgType uint8, data []byte, err error)

ReadMessage reads the next complete WebSocket message into memory. It returns the message type (e.g., Text or Binary), the full payload, and any error encountered. If SkipUTF8Validation is not enabled, the message will be UTF8 validated, if it fails, the connection would be closed for InvalidFramePayloadData.

func (*Conn) ReadString

func (conn *Conn) ReadString() ([]byte, error)

ReadString returns the payload from a WebSocket text message.

If SkipUTF8Validation is not enabled, the message will be UTF8 validated, if it fails, the connection would be closed for InvalidFramePayloadData. If the received message is not of type text, it returns snapws.ErrMessageTypeMismatch without closing the connection. The returned error must be checked. If it's of type snapws.FatalError, that indicates the connection was closed due to an I/O or protocol error. Any other error means the connection is still open, and you may retry or continue using it.

func (*Conn) SendBytes

func (conn *Conn) SendBytes(ctx context.Context, p []byte) error

SendBytes sends the given byte slice as a WebSocket binary message.

This is a shorthand for SendMessage with OpcodeBinary. The returned error must be checked. If it's a snapws.FatalError, the connection was closed due to an I/O or protocol error. Other errors indicate the connection is still alive and can be reused.

func (*Conn) SendJSON

func (conn *Conn) SendJSON(ctx context.Context, v any) error

SendJSON sends the given value as a JSON-encoded WebSocket text message.

If marshaling fails, the method returns the original marshaling error. The message will be split into fragments if necessary.

The returned error must be checked. If it's of type snapws.FatalError, that indicates the connection was closed due to an I/O or protocol error. Any other error means the connection is still open, and you may retry or continue using it.

func (*Conn) SendMessage added in v0.4.2

func (conn *Conn) SendMessage(ctx context.Context, opcode uint8, b []byte) error

Receives a context, opcode (text or binary), and a slice of bytes.

  • Tries to optain the next writer
  • sends a message of opcode (the given opcode) with the data of the given byte slice.

It returns an error. Errors must be checked if fatal.

func (*Conn) SendString

func (conn *Conn) SendString(ctx context.Context, p []byte) error

SendString sends the given byte slice as a WebSocket binary message.

The Websocket protocl states that text messages must be a valid UTF-8, but SnapWS doesnt enforce UTF-8 validation in send methods even if SkipUTF8Validation is set to false. You're trusted to send valid data.

This is a shorthand for SendMessage with OpcodeText. The returned error must be checked. If it's a snapws.FatalError, the connection was closed due to an I/O or protocol error. Other errors indicate the connection is still alive and can be reused.

type ConnReader

type ConnReader struct {
	// contains filtered or unexported fields
}

ConnReader provides an io.Reader for a single WebSocket message.

Concurrency:

  • Only one goroutine may read from a Conn (and thus its ConnReader) at a time. Concurrent reads on the same connection or the same ConnReader are not supported.

func (*ConnReader) Read

func (r *ConnReader) Read(p []byte) (n int, err error)

Read implements the io.Reader interface for ConnReader. It reads WebSocket message payload data into p, handling continuation frames, unmasking (if required), and connection closure. This function allows the application to stream data frame-by-frame.

Behavior:

  • Returns io.EOF when the final frame of a message has been fully read.
  • If additional frames are part of the same message (continuation frames), it will transparently fetch and continue reading them.
  • If a fatal error occurs (protocol violation, closed connection, etc.), the connection will be closed and a fatal error will be returned.

type ConnWriter

type ConnWriter struct {
	// contains filtered or unexported fields
}

ConnWriter is NOT safe for concurrent use. Only one goroutine may call Write/Flush/Close at a time. Use Conn.NextWriter to safely obtain exclusive access to a writer.

func (*ConnWriter) Close

func (w *ConnWriter) Close() error

Close flushes the final frame and releases the writer lock.

func (*ConnWriter) Flush

func (w *ConnWriter) Flush() error

flush sends the current buffer as a WebSocket frame with FIN set to false. If you want to end the message (FIN=true), you have to use the Close() method of the writer.

Return values:

  • If it returns a fatal error, the connection is closed and cannot be reused.
  • If it returns a non-fatal, non-nil error, the connection is still alive, and you may attempt to flush again. note: If err == context.Canceled or DeadlineExceeded, connection is alive but ctx is done. No point in retrying.
  • If it returns nil, the flush was successful and the message has been sent.

func (*ConnWriter) Write

func (w *ConnWriter) Write(p []byte) (n int, err error)

Write appends bytes to the writer buffer and flushes if full. Automatically handles splitting into multiple frames.

type ControlWriter added in v0.4.0

type ControlWriter struct {
	// contains filtered or unexported fields
}

Used to write control frames. This is needed because control frames can be written mid data frames.

type FatalError

type FatalError struct {
	Err error
}

func (*FatalError) Error

func (e *FatalError) Error() string

type ManagedConn added in v0.3.0

type ManagedConn[KeyType comparable] struct {
	*Conn
	Key     KeyType
	Manager *Manager[KeyType]
}

ManagedConn is a Conn that is tracked by a Manager. It links a connection to a unique key so that the Manager can manage it safely (fetch/add/remove).

type Manager

type Manager[KeyType comparable] struct {
	Upgrader *Upgrader

	OnRegister   func(conn *ManagedConn[KeyType])
	OnUnregister func(conn *ManagedConn[KeyType])
	// contains filtered or unexported fields
}

Manager tracks active WebSocket connections in a thread-safe way.

Key points:

  • Generic over KeyType (e.g., user ID), which must be comparable.
  • Each connection is stored as *ManagedConn[KeyType] in the Conns map.
  • Manager provides safe fetch/add/remove of connections without requiring additional synchronization in user code.
  • Thread-safety is enforced with sync.RWMutex (expect some performance overhead).
  • Broadcast messages.

The Upgrader field handles the WebSocket upgrade, and the optional OnRegister / OnUnregister callbacks are invoked when connections are added or removed.

func NewManager

func NewManager[KeyType comparable](u *Upgrader) *Manager[KeyType]

Creates a new manager. KeyType is the type of the key of the conns map. KeyType must be comparable.

func (*Manager[KeyType]) BatchBroadcast added in v1.1.0

func (m *Manager[KeyType]) BatchBroadcast(ctx context.Context, data []byte, exclude ...KeyType) (int, error)

BatchBroadcast loops over the manager's connections and adds the given data into their batch.

Returns:

  • int: number of connections that successfully received the message in thier queue (doesnt necessarily mean they sent/batched it successfully)
  • error: context cancellation error, flusher errors, or nil if completed normally

func (*Manager[KeyType]) BatchBroadcastJSON added in v1.1.0

func (m *Manager[KeyType]) BatchBroadcastJSON(ctx context.Context, v interface{}, exclude ...KeyType) (int, error)

BatchBroadcastJSON is just a helper method that marshals the given v into json and calls BatchBraodcast.

Returns:

  • int: number of connections that successfully received the message in thier queue (doesnt necessarily mean they sent/batched it successfully)
  • error: context cancellation error, mrashal error, flusher errors, or nil if completed normally

func (*Manager[KeyType]) BroadcastBytes

func (m *Manager[KeyType]) BroadcastBytes(ctx context.Context, data []byte, exclude ...KeyType) (int, error)

broadcast sends a message of opcode binary to all active connections except the connection of key "exclude". It takes a context.Context, data as a slice of bytes, and an optional "exclude" which are the keys of connections to exclude from the broadcast It returns "n" the number of successfull writes, and an error.

func (*Manager[KeyType]) BroadcastJSON added in v1.1.2

func (m *Manager[KeyType]) BroadcastJSON(ctx context.Context, v interface{}, exclude ...KeyType) (int, error)

This is a convenience method that marshals the given value into json and calls BroadcastString.

func (*Manager[KeyType]) BroadcastString

func (m *Manager[KeyType]) BroadcastString(ctx context.Context, data []byte, exclude ...KeyType) (int, error)

broadcast sends a message of opcode text to all active connections except the connection of key "exclude". It takes a context.Context, data as a slice of bytes, and an optional "exclude" which are the keys of connections to exclude from the broadcast. It returns "n" the number of successfull writes, and an error.

func (*Manager[KeyType]) Connect

func (m *Manager[KeyType]) Connect(key KeyType, w http.ResponseWriter, r *http.Request) (*ManagedConn[KeyType], error)

Connect does 2 things:

  • Upgrades the connection.
  • connect the user to the manager & runs the hooks.

Any error retuened by this method is a handhshake error, and the response is handled by the handshake. You shouln't write to the http writer after this fucntion is called even if it return a non-nil error.

func (*Manager[KeyType]) Get added in v1.1.0

func (m *Manager[KeyType]) Get(key KeyType) *ManagedConn[KeyType]

Receives a key, returns a pointer to the connection associated with the key. If the connection exists, it will return a pointer to it. If the connection deosn't exists, it will return a nil pointer.

func (*Manager[KeyType]) GetAllConns

func (m *Manager[KeyType]) GetAllConns(exclude ...KeyType) []*ManagedConn[KeyType]

except the conns that appear in "exclude".

func (*Manager[KeyType]) GetAllConnsAsConn added in v1.1.0

func (m *Manager[KeyType]) GetAllConnsAsConn(exclude ...KeyType) []*Conn

Get all connections associated with the manager as a slice of pointers to a conn except the conn that appear in "exclude".

func (*Manager[KeyType]) Register

func (m *Manager[KeyType]) Register(key KeyType, conn *ManagedConn[KeyType])

Adds a conn[KeyType] to the manager (Manager[KeyType]). Receives a key and a pointer to a conn. If the key already exists, it will close the connection associated with the key, and replace it with the new connection received by the fucntion.

func (*Manager[KeyType]) Shutdown

func (m *Manager[KeyType]) Shutdown()

Shut downs the manager: - Closes all connections normaly. - Clears the conns map.

type MiddlewareErr added in v0.2.7

type MiddlewareErr struct {
	Code    int
	Message string
}

An error that can be returned by a middleware. It's usefull if you want to explicitly set the HTTP response code and error message.

func AsMiddlewareErr added in v0.2.7

func AsMiddlewareErr(err error) (*MiddlewareErr, bool)

func NewMiddlewareErr added in v0.2.7

func NewMiddlewareErr(code int, message string) *MiddlewareErr

func (*MiddlewareErr) Error added in v0.2.7

func (err *MiddlewareErr) Error() string

type Middlware

type Middlware func(w http.ResponseWriter, r *http.Request) error

A function representing a middleware that will be ran after validating the websocket upgrade request and before switching protocols. If a non-nil error returns the connection wont be accepted. It is prefered to return an error of type snapws.MiddlewareErr.

type Middlwares

type Middlwares []Middlware

type Options added in v0.2.1

type Options struct {
	// Ran before finalizing and accepting the handshake.
	Middlwares []Middlware
	// Ran when the connection finalizes.
	OnConnect func(conn *Conn)
	// // Ran when connection closes.
	OnDisconnect func(conn *Conn)

	// If not set it will default to 5 seconds.
	WriteWait time.Duration
	// Should be larger than PingEvery. If not set it will default to 60 seconds.
	ReadWait time.Duration
	// If not set it will default to 50 seconds.
	PingEvery time.Duration

	// This is the max size of message sent by the client. if not set it will default to 1MB
	// -1 means there is no max size.
	// Note: the size includes the frame/frames header/header and masking key/keys.
	// PLEASE DONT USER -1 UNLESS YOU KNOW WHAT YOU ARE DOING
	MaxMessageSize int
	// maximum fragremnt allowed per an incoming message.
	// if not set it will default to 0, which means there is no limit.
	ReaderMaxFragments int
	// if not set it will use the default http buffer (4kb)
	ReadBufferSize int
	// if not set it will use the default http buffer (4kb)
	WriteBufferSize int

	// Buffer pooling can reduce GC pressure in workloads with large messages and very high throughput,
	// but may increase latency in some scenarios. Enabled by default.
	DisableWriteBuffersPooling bool

	// subProtocols defines the list of supported WebSocket sub-protocols by the server.
	// During the handshake, the server will select the first matching protocol from the
	// client's Sec-WebSocket-Protocol header, based on the client's order of preference.
	// If no match is found, the behavior depends on the value of rejectRaw.
	SubProtocols []string
	// rejectRaw determines whether to reject clients that do not propose any matching
	// sub-protocols. If set to true, the connection will be rejected when:
	//   - The client does not include any Sec-WebSocket-Protocol header.
	//   - Or none of the client's protocols match the supported subProtocols list.
	//
	// If false, such connections will be accepted as raw WebSocket connections.
	RejectRaw bool

	// Defines the size of the buffered channel per connection that receive broadcast messages.
	//
	// Default is 8.
	BroadcastChannelsSize int
	// BackpressureStrategy controls the behavior when the per-conn broadcast channel is full:
	// 	- snapws.BackpressureDrop (default): when the channel is full the message will be droped.
	// 	- snapws.BackpressureClose: when the channel is full the connection will close.
	// 	- snapws.BackpressureWait (not recommended): when the channel is full the reading loop will block
	// 	until it succeeds to send the message.
	BroadcastBackpressure BackpressureStrategy

	// SkipUTF8Validation disables UTF-8 validation for text frames.
	// if UTF8 validtion is enabled, the library would be validating text messages only when
	// ReadString() is called. Not on every read (NextReader(), Read()).
	// UTF-8 for text messages is required by RFC 6455.
	// If you need max performance and don't care about the validity of the message.
	// you can disable it.
	// Default: false (validation enabled)
	SkipUTF8Validation bool

	// If not set it will default to DefaultMaxMessageSize (1MB).
	//
	// Max size of the batched messages. For instance, you have 3 batched messages each of size 4kb,
	// the batch is of size 12kb < 1MB.
	MaxBatchSize int
}

func (*Options) WithDefault added in v0.2.1

func (opt *Options) WithDefault()

type PrefixSuffixFunc added in v1.1.2

type PrefixSuffixFunc func(conn *Conn, messages [][]byte) []byte

PrefixSuffixFunc defines the signature of "PrefixFunc" and "SuffixFunc" fields for the Flusher.

type RateLimiter added in v0.6.0

type RateLimiter struct {

	// onLimitExceeded is called when a connection exceeds its rate limit.
	// The function receives the offending connection and can perform actions
	// like logging, metrics collection, or closing the connection.
	//
	// Important: If this function closes the connection, it MUST return a
	// non-nil error to signal that the connection should be terminated.
	// Returning nil means the message should be dropped but the connection
	// should remain open.
	OnRateLimitHit func(conn *Conn) error
	// contains filtered or unexported fields
}

RateLimiter manages per-connection message rate limiting for WebSocket connections. It uses a token bucket algorithm to limit the number of messages each connection can send per second, with configurable burst capacity.

The rate limiter operates on WebSocket messages (not frames), meaning: - Only data frames (text/binary) count toward the limit - Control frames (ping, pong, close) are ignored - Fragmented messages count as a single message

note: if you want to close the connection you must close it manually in SetOnLimitExceeded and return a non-nil error.

Example usage:

limiter := NewRateLimiter(10, 5) // 10 msg/sec, burst of 5
limiter.SetOnLimitExceeded(func(conn *Conn) error {
    log.Printf("Rate limit exceeded for connection %v", conn.RemoteAddr())
    return nil // Don't close connection
})
upgrader.Limiter = limiter

func NewRateLimiter added in v0.6.0

func NewRateLimiter(mps, burst int) *RateLimiter

NewRateLimiter creates and returns a new RateLimiter. The limiter uses a token bucket algorithm, where:

  • mps: number of tokens (messages) added per second.
  • burst: maximum number of tokens the bucket can hold at once.

Example:

mps = 3, burst = 5
- At most 5 messages can be allowed immediately if the bucket is full.
- Each second, 3 new tokens are added, up to the burst limit.

type Room added in v0.7.0

type Room[keyType comparable] struct {
	Key keyType // key in the parent roomManager

	// Called when a connection is added to the room.
	//If not set it will default to "DefaultOnJoin" in the RoomManager.
	OnJoin roomHook[keyType]
	// Called when a connection is removed from the room.
	//If not set it will default to "DefaultOnLeave" in the RoomManager.
	OnLeave roomHook[keyType]
	// contains filtered or unexported fields
}

Room represents a group of WebSocket connections that can receive broadcast messages together. Rooms are identified by a comparable key type and provide thread-safe operations for managing connections and broadcasting messages.

Typical usage:

  • Chat rooms where users in the same room receive each other's messages
  • Game lobbies where players receive game state updates
  • Collaborative editing where document collaborators receive changes

func (*Room[keyType]) Add added in v0.7.0

func (r *Room[keyType]) Add(conn *Conn, args ...any)

Add adds a connection to the room. The connection will be automatically removed from the room when it closes.

Receives args as an optional value, these args will be passed to the OnJoin hook.

Thread-safe: Multiple goroutines can call this concurrently. If the connection is already in the room, this is a no-op.

func (*Room[keyType]) BatchBroadcast added in v1.1.0

func (r *Room[keyType]) BatchBroadcast(ctx context.Context, data []byte, exclude ...*Conn) (int, error)

BatchBroadcast loops over the room's connections and adds the given data into their batch.

Returns:

  • int: number of connections that successfully received the message in thier queue (doesnt necessarily mean they sent/batched it successfully)
  • error: context cancellation error, flusher errors, or nil if completed normally

func (*Room[keyType]) BatchBroadcastJSON added in v1.1.0

func (r *Room[keyType]) BatchBroadcastJSON(ctx context.Context, v interface{}, exclude ...*Conn) (int, error)

BatchBroadcastJSON is just a helper method that marshals the given v into json and calls BatchBraodcast.

Returns:

  • int: number of connections that successfully received the message in thier queue (doesnt necessarily mean they sent/batched it successfully)
  • error: context cancellation error, mrashal error, flusher errors, or nil if completed normally

func (*Room[keyType]) BroadcastBytes added in v0.7.0

func (r *Room[keyType]) BroadcastBytes(ctx context.Context, data []byte, exclude ...*Conn) (int, error)

BroadcastBytes sends a binary message to all connections in the room.

Returns the number of connections that successfully received the message and any error encountered during broadcasting.

Thread-safe: Can be called concurrently from multiple goroutines.

func (*Room[keyType]) BroadcastJSON added in v1.1.2

func (r *Room[keyType]) BroadcastJSON(ctx context.Context, v interface{}, exclude ...*Conn) (int, error)

This is a convenience method that marshals the given value into json and calls BroadcastString.

func (*Room[keyType]) BroadcastString added in v0.7.0

func (r *Room[keyType]) BroadcastString(ctx context.Context, data []byte, exclude ...*Conn) (int, error)

BroadcastString sends a text message to all connections in the room.

Returns the number of connections that successfully received the message and any error encountered during broadcasting.

Thread-safe: Can be called concurrently from multiple goroutines.

func (*Room[keyType]) Close added in v0.7.0

func (r *Room[keyType]) Close()

Close removes all connections from the room and deletes the room from its manager. All connections in the room will be removed but not closed - they remain active and can be added to other rooms.

After calling Close, this room instance should not be used.

func (*Room[keyType]) GetAllConns added in v1.1.0

func (r *Room[keyType]) GetAllConns(exclude ...*Conn) []*Conn

Receives optional values "exclude". Returns a slice of pointers to connections excluding the "excludes".

func (*Room[keyType]) Move added in v0.7.0

func (r *Room[keyType]) Move(conn *Conn, newRoom keyType)

Move removes a connection from this room and adds it to another room. If the target room doesn't exist, the connection is only removed from the current room.

The target room must exist in the same RoomManager as the current room.

Thread-safe: The move operation is atomic from the perspective of the connection.

func (*Room[keyType]) Remove added in v0.7.0

func (r *Room[keyType]) Remove(conn *Conn, args ...any)

Remove removes a connection from the room. If the connection is not in the room, this is a no-op.

// Receives args as an optional value, these args will be passed to the OnLeave hook.

Thread-safe: Multiple goroutines can call this concurrently.

func (*Room[keyType]) RemoveAll added in v0.7.0

func (r *Room[keyType]) RemoveAll()

RemoveAll removes all connections from the room. The connections are not closed, just removed from the room.

Thread-safe: Can be called concurrently with other room operations.

type RoomManager added in v0.7.0

type RoomManager[keyType comparable] struct {
	Upgrader *Upgrader

	// This is used when the "OnJoin" field of a room is unset.
	DefaultOnJoin roomHook[keyType]
	// This is used when the "OnLeave" field of a room is unset.
	DefaultOnLeave roomHook[keyType]
	// contains filtered or unexported fields
}

RoomManager handles creation, lookup, and lifecycle of rooms. It provides thread-safe operations for managing multiple rooms and their connections. Each room is identified by a unique comparable key.

func NewRoomManager added in v0.7.0

func NewRoomManager[keyType comparable](upgrader *Upgrader) *RoomManager[keyType]

NewRoomManager creates a new RoomManager with the given WebSocket upgrader. If upgrader is nil, a default upgrader with standard options will be created.

The keyType must be a comparable type (string, int, custom types with comparable fields). This key will be used to uniquely identify and retrieve rooms.

func (*RoomManager[keyType]) Add added in v0.7.0

func (rm *RoomManager[keyType]) Add(key keyType) *Room[keyType]

Add creates a new room with the given key, or returns the existing room if it already exists. This operation is idempotent - calling it multiple times with the same key is safe and will always return the same room instance.

Thread-safe: Multiple goroutines can call this concurrently.

func (*RoomManager[keyType]) Connect added in v0.7.0

func (rm *RoomManager[keyType]) Connect(w http.ResponseWriter, r *http.Request, roomKey keyType, args ...any) (*Conn, *Room[keyType], error)

Connect upgrades an HTTP connection to WebSocket and adds it to the specified room. If the room doesn't exist, it will be created automatically.

Receives args as an optinal value, these args will be passed to both the OnJoin AND OnLeave hooks.

This is a convenience method that combines WebSocket upgrade, room creation/lookup, and connection addition in a single atomic operation.

Returns the upgraded connection, the room it was added to, and any error from the upgrade process.

func (*RoomManager[keyType]) Get added in v0.7.0

func (rm *RoomManager[keyType]) Get(key keyType) *Room[keyType]

Get retrieves a room by its key. Returns nil if the room doesn't exist.

The returned room pointer must be checked for nil before use:

if room := manager.Get("lobby"); room != nil {
    room.BroadcastString(ctx, message)
}

Thread-safe: Multiple goroutines can call this concurrently.

func (*RoomManager[keyType]) GetRoomKeys added in v1.1.1

func (rm *RoomManager[keyType]) GetRoomKeys() []keyType

GetRoomKeys returns a slice of all rooms keys associated with the room manager.

func (*RoomManager[keyType]) Shutdown added in v0.7.0

func (rm *RoomManager[keyType]) Shutdown()

Shutdown closes all rooms and their connections. This should be called when shutting down the application to ensure all WebSocket connections are properly closed.

After calling Shutdown, the RoomManager should not be used.

type SendFunc added in v1.1.0

type SendFunc func(ctx context.Context, conn *Conn, messages [][]byte) error

SendFunc is invoked for each connection when sending the batched messages.

The function is responsible for encoding and writing the messages according to the user’s own format or transport rules.

type Upgrader added in v0.3.0

type Upgrader struct {
	*Options

	Limiter *RateLimiter
	Flusher *batchFlusher
	// contains filtered or unexported fields
}

Used to upgrader HTTP connections to Websocket connections. Hold snap.Options. If you wanna learn more about the options go see their docs.

func NewUpgrader added in v0.3.0

func NewUpgrader(opts *Options) *Upgrader

Created a new upgrader with the given options. If options is nil, then it will assign a new options with default values.

func (*Upgrader) EnableBatching added in v1.1.0

func (u *Upgrader) EnableBatching(ctx context.Context, flushEvery time.Duration, sendFunc SendFunc)

EnableBatching configures connections to batch messages using a custon defined format. (only if you use methods like: Batch(), BatchJSON(), BroadcastBatch(), etc...)

It's recommended to use the pre-configured formats (EnableJSONBatching or EnablePrefixBatching) unless you want something speceific.

If batching is already enabled, it will be properly closed before the new one starts.

Parameters:

  • ctx: context for the flusher lifecycle control
  • flushEvery: time interval between automatic batch flushes (0 uses default 50ms)
  • sendFunc: SendFunc is invoked for each connection when sending the batched messages. The function is responsible for encoding and writing the messages according to the user’s own format or transport rules.

func (*Upgrader) EnableJSONBatching added in v1.1.0

func (u *Upgrader) EnableJSONBatching(ctx context.Context, flushEvery time.Duration)

EnableJSONBatching configures connections to batch messages using JSON array format. (only if you use methods like: Batch(), BatchJSON(), BroadcastBatch(), etc...)

Messages are collected and sent as a single websocket binary message containing a JSON array. This is ideal for JavaScript clients that can easily parse JSON arrays.

If batching is already enabled, it will be properly closed before the new one starts.

Parameters:

  • ctx: context for the flusher lifecycle control
  • flushEvery: time interval between automatic batch flushes (0 uses default 50ms)

func (*Upgrader) EnablePrefixBatching added in v1.1.0

func (u *Upgrader) EnablePrefixBatching(ctx context.Context, flushEvery time.Duration)

EnablePrefixBatching configures connections to batch messages using length-prefixed binary format. (only if you use methods like: Batch(), BatchJSON(), BroadcastBatch(), etc...)

Each message is prefixed with its length as a 4-byte big-endian integer, allowing clients to parse individual messages from the batch. This format is more efficient than JSON for binary data and provides precise message boundaries.

Binary format: [uint32][message1][uint32][message2][uint32][message3]...

If batching is already enabled, it will be properly closed before the new one starts.

Parameters:

  • ctx: context for the flusher lifecycle control
  • flushEvery: time interval between automatic batch flushes (0 uses default 50ms)

func (*Upgrader) Upgrade added in v0.3.0

func (u *Upgrader) Upgrade(w http.ResponseWriter, r *http.Request) (*Conn, error)

Upgrades an HTTP connection to a Websocket connection. Receives (w http.ResponseWriter, r *http.Request) and returns a pointer to a snapws.Conn and an err. It checks method, headers, and selects an appopiate sub-protocol and runs the middlewares and the onConnect hook if they exist, and finnaly it responds to the client (both if the upgrade succeeds or fails).

func (*Upgrader) Use added in v0.3.0

func (u *Upgrader) Use(mw Middlware)

Appends the receive middleware to the middlewares slice of the upgrader.

Directories

Path Synopsis
cmd
examples/echo command
test command

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL