socketio

package module
v0.5.1 Latest Latest
Warning

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

Go to latest
Published: Aug 11, 2026 License: MIT Imports: 17 Imported by: 0

README

socket.io

Go Test Go Lint Go Vuln Web Unit Web E2E Go Reference Go Report Card Go Version Release Last Commit Code Size PRs Welcome Docs

Node's Socket.IO, for Go.

A dependency-free Go port of the Socket.IO server. It implements the real wire protocol — Engine.IO v4 (HTTP long-polling and WebSocket, including the polling→WebSocket upgrade) and Socket.IO v5 (namespaces, rooms, events, and acknowledgements) — so it interoperates with standard Socket.IO clients. The WebSocket transport is implemented from scratch on net/http; there are no third-party dependencies.

package main

import (
	"net/http"

	socketio "github.com/malcolmston/socketio"
)

func main() {
	io := socketio.New()

	io.OnConnection(func(s *socketio.Socket) {
		s.Join("general")

		s.On("chat", func(args []any) []any {
			io.To("general").Emit("chat", args...) // broadcast to the room
			return nil
		})

		s.On("ping", func(args []any) []any {
			return []any{"pong"} // acknowledgement reply
		})
	})

	http.Handle(socketio.DefaultPath, io)
	http.ListenAndServe(":3000", nil)
}

Install

go get github.com/malcolmston/socketio

Design

The implementation is layered exactly like the Node original:

Layer Package Responsibility
Engine.IO codec engineio packet + polling-payload encode/decode
WebSocket internal/ws RFC 6455 server (handshake, framing, control frames)
Socket.IO codec socketio (protocol.go) CONNECT/EVENT/ACK/... packet encode/decode
Server socketio transports, sessions, namespaces, rooms, sockets

Both codecs are pure and independently unit-tested; the server is exercised end-to-end over both transports.

The API

Server
io := socketio.New()                 // default options
io := socketio.New(socketio.Options{ // or configure
	PingInterval: 25 * time.Second,
	PingTimeout:  20 * time.Second,
})

io.OnConnection(func(s *socketio.Socket) { ... }) // default namespace
io.Of("/admin").OnConnection(func(s *socketio.Socket) { ... })
io.Emit("event", data)               // broadcast to everyone
io.To("room").Emit("event", data)    // broadcast to a room

An *socketio.Server is an http.Handler; mount it at socketio.DefaultPath (/socket.io/).

Hardening a public deployment

Four Options matter once the server faces the internet:

io := socketio.New(socketio.Options{
	// Reject cross-site WebSocket handshakes. A WebSocket upgrade is not
	// subject to the same-origin policy and triggers no CORS preflight, so
	// without this any page can open an authenticated socket to you.
	// Setting it also enables Access-Control-Allow-Credentials, which is
	// withheld while every origin is accepted.
	CheckOrigin: socketio.AllowedOrigins("https://app.example.com"),

	// Believe X-Forwarded-For only behind a proxy that overwrites it.
	// Otherwise Handshake().Address is the real socket peer.
	TrustProxy: false,

	// Cap concurrent Engine.IO sessions (default DefaultMaxSessions).
	// Negative disables the cap.
	MaxSessions: 65536,

	// Bounds POSTed polling bodies and buffered binary attachments.
	MaxPayload: 1_000_000,
})

Independently of configuration, the server bounds what a peer can make it allocate: WebSocket frames and reassembled fragmented messages are limited to 1 MiB, a BINARY_EVENT may declare at most MaxAttachments (1024) buffers, and a client that stops reading has its outbound queue capped rather than being allowed to stall a room broadcast.

Socket
s.ID()                               // unique socket id
s.Auth()                             // CONNECT auth payload
s.On("event", handler)               // register a handler
s.Emit("event", args...)             // send to this client
s.EmitWithAck("event", cb, args...)  // send and await the client's ack
s.Join("room"); s.Leave("room")      // room membership
s.To("room").Emit("event", data)     // broadcast to a room, excluding self
s.Broadcast().Emit("event", data)    // everyone except this socket
s.OnDisconnect(func(reason string){})
s.Disconnect(true)                   // force-disconnect
Per-socket data (session-like state)

Attach arbitrary state to a socket for the lifetime of the connection — the equivalent of socket.data:

io.OnConnection(func(s *socketio.Socket) {
	s.Set("user", currentUser).Set("tenant", "acme")

	s.On("action", func(args []any) []any {
		user := s.GetString("user")
		v, _ := s.Get("tenant")
		_ = v
		return nil
	})
})

s.Set, s.Get, s.GetString, s.Delete, and s.Data() manage the store; it is concurrency-safe.

Handlers & acknowledgements

An event handler receives the event arguments and returns an optional acknowledgement payload:

s.On("ping", func(args []any) []any {
	return []any{"pong"} // sent back only if the client requested an ack
})

Return nil for no acknowledgement.

Rooms & broadcasting
s.Join("room42")
io.To("room42").Emit("news", payload)          // all members
s.To("room42").Emit("news", payload)           // members except the sender
io.Of("/admin").To("ops").Emit("alert", data)  // per-namespace

Connection middleware

Gate connections with Use (the equivalent of io.use). Calling next(err) rejects the connection with a connect_error:

io.Use(func(s *socketio.Socket, next func(error)) {
	if s.Auth() == nil {
		next(errors.New("unauthorized"))
		return
	}
	next(nil)
})
io.Of("/admin").Use(adminOnly) // per-namespace middleware

Go client

A Go client ships in client, built on the same transport stack:

import "github.com/malcolmston/socketio/client"

c, _ := client.Dial("http://localhost:3000")
defer c.Close()

c.On("news", func(args []any) []any { fmt.Println(args); return nil })
c.Emit("hello", "world")

reply, _ := c.EmitWithAck("ping", 5*time.Second) // blocks for the ack

The client supports binary payloads and automatic reconnection:

c, _ := client.Dial(url, client.Options{Reconnection: true})
c.On("reconnect", func(args []any) []any { log.Println("reconnected"); return nil })

The server side can likewise request an acknowledgement and block for it with socket.EmitAck(event, timeout, args...). Server.Close() disconnects all sessions.

Using with express (or any net/http handler)

Attach the Socket.IO server to an existing HTTP server, letting express (or an http.ServeMux) handle everything else — exactly like new Server(httpServer) in Node:

app := express.New()                       // your routes
app.Get("/api/hello", helloHandler)

io := socketio.New()                       // your socket handlers
io.OnConnection(func(s *socketio.Socket) { ... })

// io.Handler intercepts /socket.io/ and delegates the rest to app.
http.ListenAndServe(":3000", io.Handler(app))

io.Handler(next) takes any http.Handler; pass nil to 404 non-Socket.IO requests. You can also mount it the other way — as express middleware — with app.Use("/socket.io", express.WrapHandler(io)), or register it on a mux with io.Attach(mux).

Binary events

Emit and receive []byte payloads; they are carried as Socket.IO binary attachments (native WebSocket binary frames, or base64 over polling):

s.Emit("blob", []byte{0x00, 0x01, 0x02})
s.On("upload", func(args []any) []any {
	data := args[0].([]byte)
	return []any{len(data)}
})

Server-wide operations

io.FetchSockets()                 // all connected sockets
io.SocketsJoin("room")            // make every socket join a room
io.SocketsLeave("room")
io.DisconnectSockets(true)        // disconnect everyone
io.ServerSideEmit("event", data)  // server-to-server event (single-node: local)

Rooms are stored behind a pluggable Adapter (ns.SetAdapter); the default is in-process. Broadcast flags io.To("r").Volatile().Compress(false).Emit(...) are supported (advisory on this single-node implementation).

Scaling out with Redis

For multiple server instances, install a Broadcaster so broadcasts fan out across nodes. The redis subpackage provides one, speaking the Redis pub/sub protocol directly (no third-party client):

import "github.com/malcolmston/socketio/redis"

bc, _ := redis.New(redis.Options{Addr: "localhost:6379", Channel: "socket.io"})
io.SetBroadcaster(bc)

With a broadcaster installed, io.To(room).Emit(...) on any node is delivered to matching sockets on every node. Broadcaster is a small interface (Publish / OnMessage / Close), so any pub/sub transport can back it.

Transports

  • HTTP long-polling — the default the JS client opens with; fully supported (handshake, GET poll, POST).
  • WebSocket — implemented from scratch (RFC 6455). Clients may connect directly (transports: ['websocket']) or start on polling and upgrade; the Engine.IO 2probe/3probe/5 upgrade handshake is handled.

Status & scope

Supported: Engine.IO v4 framing, both transports + upgrade, heartbeat, the Socket.IO v5 text protocol (CONNECT, DISCONNECT, EVENT, ACK, CONNECT_ERROR), multiple namespaces, rooms, broadcasting, and acknowledgements in both directions. Binary attachments (BINARY_EVENT/BINARY_ACK) are parsed but the convenience API focuses on JSON payloads.

Example

go run ./examples/chat

License

MIT

Documentation

Overview

Package socketio is a pure-Go port of the Node.js Socket.IO server. It implements the Engine.IO v4 transport layer (HTTP long-polling and WebSocket, with the polling→websocket upgrade) and the Socket.IO v5 protocol (namespaces, rooms, events, and acknowledgements), exposing an API that mirrors the original JavaScript library so that idioms such as io.on("connection"), socket.join(room), and io.to(room).emit(...) translate almost line for line:

io := socketio.New()
io.OnConnection(func(s *socketio.Socket) {
	s.On("chat", func(args []any) []any {
		io.To("room1").Emit("chat", args...)
		return nil
	})
	s.Join("room1")
})
http.Handle("/socket.io/", io)
http.ListenAndServe(":3000", nil)

Reach for this package when you want real-time, bidirectional, event-based communication between a Go backend and Socket.IO clients (browsers, mobile apps, or the companion client package) without pulling in cgo or third-party dependencies — the entire stack, down to the RFC 6455 WebSocket framing, is built on the standard library. A *Server is an http.Handler, so it mounts on any net/http server or router: use ServeHTTP directly, Attach it to a *http.ServeMux, or wrap an existing handler with Handler to coexist with a REST/Express-style application on the same port.

Internally each connected client owns one Engine.IO session (a conn), identified by a session id (sid). A session begins over HTTP long-polling and is transparently upgraded to WebSocket via the Engine.IO probe handshake; the engineio subpackage encodes the transport frames and the polling payload, while this package encodes the Socket.IO layer on top — CONNECT, EVENT, ACK, DISCONNECT, and their binary variants. Events carry JSON arguments; any []byte in a payload is transmitted out-of-band as a BINARY_EVENT with placeholder markers (see binary.go). A single session may be attached to several Namespaces at once, each of which multiplexes its own sockets, rooms, and connection middleware over the shared transport.

The concurrency and delivery semantics follow from that design. The server sends periodic heartbeat pings and disconnects a session whose pong is overdue by more than PingInterval+PingTimeout. Inbound events are dispatched on their own goroutine so a handler may block on an acknowledgement without stalling the read loop, which means handlers for the same socket can run concurrently and must guard any shared state; ordering of delivery to the wire is preserved, but ordering between independently dispatched handlers is not. Socket.Emit targets one client and returns an error, whereas the broadcast forms (Server.Emit, Namespace.Emit, and BroadcastOperator.Emit) fan out to many recipients and deliberately do not surface a per-socket error. Rooms are managed by a pluggable Adapter; the default keeps all membership in process.

Parity with the Node reference implementation is close but not total. The wire protocols are compatible, so this server interoperates with the official JavaScript client and this module's client package. Room broadcasting, acknowledgements, connection middleware (Use), per-socket data (Set/Get), and multi-node scale-out (via SetBroadcaster and the redis subpackage) are all supported. Differences reflect Go idioms and scope: handlers use typed func([]any) []any signatures rather than variadic JS callbacks, there is no built-in adapter persistence beyond what a Broadcaster provides, and features tied to the Node runtime (such as the admin UI or the msgpack parser) are out of scope. See COMPATIBILITY.md in the repository for the authoritative matrix.

Index

Examples

Constants

View Source
const DefaultMaxSessions = 65536

DefaultMaxSessions is the concurrent-session cap applied when Options.MaxSessions is zero. It is deliberately far above what a single process is expected to serve: it exists to bound a handshake flood, not to throttle legitimate traffic.

View Source
const DefaultPath = "/socket.io/"

DefaultPath is the HTTP path Socket.IO serves from.

View Source
const MaxAttachments = 1024

MaxAttachments bounds the number of binary buffers a single BINARY_EVENT or BINARY_ACK packet may declare in its "<n>-" wire prefix. The count is attacker-controlled — it arrives on the wire before any buffer does — so it is validated at decode time rather than trusted by the code that pre-allocates per-attachment storage. A packet declaring more than this is rejected with ErrInvalidPacket. The limit is far above any realistic payload (socket.io's own encoder emits one attachment per []byte argument).

View Source
const ProtocolVersion = 5

ProtocolVersion is the Socket.IO protocol revision implemented here (v5, which rides on Engine.IO v4).

Variables

View Source
var ErrAckTimeout = errors.New("socketio: ack timeout")

ErrAckTimeout is returned by EmitAck when the client does not acknowledge the event within the supplied timeout.

View Source
var ErrEmptyEvent = errors.New("socketio: empty event name")

ErrEmptyEvent indicates an empty event name.

View Source
var ErrInvalidPacket = errors.New("socketio: invalid packet")

ErrInvalidPacket indicates a malformed Socket.IO packet.

View Source
var ErrReservedEvent = errors.New("socketio: reserved event name")

ErrReservedEvent indicates an attempt to use a reserved event name for an application event.

View Source
var ErrTooManyAttachments = fmt.Errorf("%w: too many binary attachments (max %d)", ErrInvalidPacket, MaxAttachments)

ErrTooManyAttachments indicates a BINARY_EVENT/BINARY_ACK packet declaring more than MaxAttachments binary buffers. It wraps ErrInvalidPacket so callers that only care about malformed input can keep testing for that.

Functions

func AllowedOrigins added in v0.5.0

func AllowedOrigins(origins ...string) func(*http.Request) bool

AllowedOrigins builds an Options.CheckOrigin function that admits only the listed origins. It is the Go equivalent of configuring socket.io's `cors: { origin: [...] }`, and exists because the zero value of Options accepts every origin:

io := socketio.New(socketio.Options{
	CheckOrigin: socketio.AllowedOrigins("https://app.example.com"),
})

Comparison is on the exact Origin header value (scheme, host and port), case-insensitively for the scheme and host as browsers always send them lowercased. A request with no Origin header is allowed: only browsers send one, and the same-origin policy — the thing an allowlist protects — does not apply to a non-browser client, which could set any value it liked anyway.

Setting CheckOrigin matters most for the WebSocket transport. A WebSocket handshake is *not* subject to the same-origin policy: any page on any site can open a socket to this server and, if the browser attaches cookies, act with the visitor's authority ("cross-site WebSocket hijacking"). Unlike the polling transport, no CORS preflight stands in the way, so the Origin check is the only defense. Pass "*" to allow everything explicitly (the same as leaving CheckOrigin nil, but self-documenting).

Example

ExampleAllowedOrigins locks a server down to the origins a browser app is actually served from. This matters most for the WebSocket transport: a WebSocket handshake is an ordinary GET that the same-origin policy does not restrict and that triggers no CORS preflight, so without an Origin check any page a user visits can open an authenticated socket to this server — cross-site WebSocket hijacking. Setting CheckOrigin also switches on Access-Control-Allow-Credentials for the polling transport, which is withheld while every origin is accepted.

package main

import (
	"fmt"
	"net/http"
	"net/http/httptest"

	socketio "github.com/malcolmston/socketio"
)

func main() {
	io := socketio.New(socketio.Options{
		CheckOrigin: socketio.AllowedOrigins(
			"https://app.example.com",
			"https://admin.example.com",
		),
	})
	defer io.Close()

	ts := httptest.NewServer(io)
	defer ts.Close()

	probe := func(origin string) int {
		req, _ := http.NewRequest(http.MethodGet, ts.URL+"/socket.io/?EIO=4&transport=polling", nil)
		req.Header.Set("Origin", origin)
		resp, err := http.DefaultClient.Do(req)
		if err != nil {
			return 0
		}
		resp.Body.Close()
		return resp.StatusCode
	}

	fmt.Println("app.example.com  ->", probe("https://app.example.com"))
	fmt.Println("evil.example     ->", probe("https://evil.example"))
}
Output:
app.example.com  -> 200
evil.example     -> 403

func IsReservedEvent added in v0.3.0

func IsReservedEvent(name string) bool

IsReservedEvent reports whether name is a Socket.IO reserved event name (such as "connect", "disconnect", or "connect_error"). Reserved names are emitted by the library itself and must not be used for application events, mirroring the RESERVED_EVENTS guard in the JavaScript server.

func Reconstruct

func Reconstruct(data any, buffers [][]byte) (any, error)

Reconstruct walks a payload replacing {"_placeholder":true,"num":N} markers with the matching binary buffer. It is exported for client implementations that reassemble incoming binary packets.

Every placeholder index is validated against the buffer list: a marker whose num is negative or points past the last buffer is rejected with an error wrapping ErrInvalidPacket, mirroring socket.io-parser's binary reconstructor, which throws "illegal attachments" rather than handing the raw placeholder object to the application. The index and the buffer count are both attacker-controlled (they arrive on the wire), so a mismatch is a malformed frame, not data.

func ReservedEvents added in v0.3.0

func ReservedEvents() []string

ReservedEvents returns a sorted copy of the reserved event names, useful for documentation and tooling.

func ValidateEventName added in v0.3.0

func ValidateEventName(name string) error

ValidateEventName checks that name is usable as an application event name: it must be non-empty and must not collide with a reserved Socket.IO event. It returns ErrEmptyEvent or ErrReservedEvent on failure, and nil when the name is safe to emit.

Types

type Adapter

type Adapter interface {
	// Add registers a socket in the namespace.
	Add(socketID string, s *Socket)
	// Remove deletes a socket and drops it from every room.
	Remove(socketID string)
	// Join adds a socket to a room.
	Join(socketID, room string)
	// Leave removes a socket from a room.
	Leave(socketID, room string)
	// SocketsInRoom returns the sockets that are members of a room.
	SocketsInRoom(room string) []*Socket
	// AllSockets returns every socket in the namespace.
	AllSockets() []*Socket
	// Get returns a socket by id.
	Get(socketID string) (*Socket, bool)
}

Adapter stores which sockets belong to a namespace and its rooms. The default implementation keeps everything in process; supplying a custom Adapter (e.g. backed by Redis) is the extension point for scaling a namespace across multiple server instances.

type BroadcastOperator

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

BroadcastOperator emits events to a filtered set of sockets in a namespace — optionally scoped to one or more rooms and excluding specific sockets. It is returned by Namespace.To, Server.To, and Socket.To, and is the equivalent of io.to(room).emit(...).

func (*BroadcastOperator) Compress

func (b *BroadcastOperator) Compress(on bool) *BroadcastOperator

Compress sets whether the payload should be compressed by the transport. It is advisory in this implementation.

func (*BroadcastOperator) Emit

func (b *BroadcastOperator) Emit(event string, args ...any)

Emit sends an event to every socket matched by the operator. When a cluster Broadcaster is installed, the broadcast is published to all nodes (which each deliver it to their local sockets); otherwise it is delivered locally.

func (*BroadcastOperator) Except

func (b *BroadcastOperator) Except(socketID string) *BroadcastOperator

Except excludes a socket id from the broadcast.

func (*BroadcastOperator) ExceptRoom added in v0.3.0

func (b *BroadcastOperator) ExceptRoom(rooms ...string) *BroadcastOperator

ExceptRoom excludes every socket that is a member of any of the given rooms from the broadcast — the equivalent of io.except(room). Unlike Except, which excludes an individual socket id, ExceptRoom excludes whole rooms.

func (*BroadcastOperator) In

In is an alias for To, matching socket.io's io.in(room).

func (*BroadcastOperator) Local added in v0.3.0

Local restricts the broadcast to sockets connected to this node, suppressing cluster fan-out even when a Broadcaster is installed — the equivalent of io.local.emit(...). It is a no-op on a single-node deployment.

func (*BroadcastOperator) Timeout added in v0.3.0

Timeout records an acknowledgement timeout for the broadcast, matching io.timeout(ms).emit(...). It is stored for callers that aggregate acks; the convenience Emit itself is fire-and-forget.

func (*BroadcastOperator) To

To narrows the broadcast to an additional room.

func (*BroadcastOperator) Volatile

func (b *BroadcastOperator) Volatile() *BroadcastOperator

Volatile marks the broadcast as volatile: messages that cannot be delivered immediately (e.g. to a client mid-reconnect) may be dropped. On this single-node, buffered implementation it is advisory.

type BroadcastTarget

type BroadcastTarget interface {
	Emitter
	RoomTargeter
}

BroadcastTarget is the combination satisfied by the room-addressable emitters (server, namespace, and broadcast operator): they can both narrow to a room and emit to the resulting set.

type Broadcaster

type Broadcaster interface {
	// Publish sends a serialized broadcast to all instances (including this
	// one — pub/sub echoes to the publisher).
	Publish(data []byte) error
	// OnMessage registers the handler invoked for every received broadcast.
	OnMessage(func(data []byte))
	// Close shuts the broadcaster down.
	Close() error
}

Broadcaster fans broadcasts out to other server instances. Installing one (Server.SetBroadcaster) turns a single-node server into a cluster member: a broadcast is published once and each node delivers it to its own local sockets. The reference implementation is the Redis adapter in the redis subpackage, but any pub/sub transport can implement this interface.

type Decoder added in v0.3.0

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

Decoder reassembles a packet from the stream of transport frames produced by an Encoder. It is the Go equivalent of socket.io-parser's Decoder and is stateful: feed it frames with Add in arrival order and it returns a completed packet once all of a packet's frames have been seen. A single Decoder handles one packet at a time and must not be used concurrently.

func NewDecoder added in v0.3.0

func NewDecoder() *Decoder

NewDecoder returns an empty Decoder ready to accept frames.

func (*Decoder) Add added in v0.3.0

func (d *Decoder) Add(frame any) (*Packet, error)

Add feeds one transport frame to the decoder. The frame must be a string (a text frame, i.e. a Socket.IO text packet) or a []byte (a binary attachment frame). Add returns a non-nil packet when the frame completes a packet, or (nil, nil) when more frames are required (a binary header awaiting its attachments). A malformed frame, or a binary attachment arriving with no header pending, returns ErrInvalidPacket.

Reassembled binary packets are rewritten to their plain EVENT/ACK type (with the attachment count cleared) once every {"_placeholder":true,"num":N} marker has been replaced by its buffer — mirroring socket.io-parser, whose Decoder hands consumers an ordinary EVENT/ACK. A placeholder index that points past the attachments supplied is rejected with an error wrapping ErrInvalidPacket.

func (*Decoder) Pending added in v0.3.0

func (d *Decoder) Pending() bool

Pending reports whether the decoder is mid-packet, waiting for binary attachment frames to complete the packet whose header it already received.

func (*Decoder) Reset added in v0.3.0

func (d *Decoder) Reset()

Reset discards any partially-decoded packet, returning the decoder to its empty state.

type Emitter

type Emitter interface {
	Emit(event string, args ...any)
}

Emitter is anything that can broadcast an event to a set of sockets without a per-socket error — the server, a namespace, and a broadcast operator all fan an event out to many recipients, so a delivery error to any single socket is not surfaced. (Socket.Emit, which targets one client, deliberately returns an error and is therefore not an Emitter.)

type Encoder added in v0.3.0

type Encoder struct{}

Encoder encodes Socket.IO packets into the sequence of transport frames they occupy. It is the Go equivalent of socket.io-parser's Encoder: a plain, stateless value that is safe to share across goroutines.

func NewEncoder added in v0.3.0

func NewEncoder() *Encoder

NewEncoder returns a ready-to-use Encoder.

func (*Encoder) Encode added in v0.3.0

func (e *Encoder) Encode(p Packet) ([]any, error)

Encode renders a packet to the ordered frames it occupies on the wire. The first element is always the packet's text form (a Go string); when the packet carries []byte values the type is promoted to its binary variant, the text header declares the attachment count, and each following element is a raw binary buffer ([]byte). A caller writes element 0 as a text frame and every subsequent element as a binary frame, in order.

type EventHandler

type EventHandler func(args []any) []any

EventHandler handles an inbound event. It receives the event arguments and may return a non-nil slice to acknowledge the event (sent back to the client when the event requested an ack).

type Handshake added in v0.3.0

type Handshake struct {
	// Headers is the set of HTTP request headers.
	Headers http.Header
	// Time is when the handshake was processed.
	Time time.Time
	// Address is the remote network address the connection originated from.
	// It is the port-stripped RemoteAddr unless the handshake was built with
	// NewProxiedHandshake or Options.TrustProxy, in which case a present
	// X-Forwarded-For takes precedence. See NewHandshake for why the header is
	// not trusted by default.
	Address string
	// XDomain reports whether the request carried an Origin header (a
	// cross-origin, browser-initiated request).
	XDomain bool
	// Secure reports whether the request arrived over TLS (https/wss).
	Secure bool
	// Issued is the handshake time in Unix milliseconds, matching the JS
	// handshake.issued field.
	Issued int64
	// URL is the request URI (path and query) of the handshake request.
	URL string
	// Query holds the parsed query-string parameters.
	Query url.Values
	// Auth is the authentication payload the client supplied with CONNECT.
	Auth any
}

Handshake describes the details of the HTTP request that established a connection — the Go equivalent of Socket.IO's socket.handshake object. It is captured once, at connection time, and never changes for the life of the socket. Middleware and connection handlers inspect it to authorize or annotate a connection (origin, forwarded address, query string, auth payload).

func NewHandshake added in v0.3.0

func NewHandshake(r *http.Request, auth any) *Handshake

NewHandshake builds a Handshake from an incoming HTTP request and the auth payload sent with the Socket.IO CONNECT packet. Address is taken from the request's RemoteAddr with any port stripped, matching engine.io, which reports the address of the socket the request actually arrived on.

X-Forwarded-For is deliberately NOT consulted: it is a client-supplied header that any peer can set to an arbitrary value, so honoring it unconditionally lets a client choose the address that authorization, rate limiting, and audit logging see. Use NewProxiedHandshake (or Options.TrustProxy on the server) only when the server sits behind a reverse proxy that overwrites the header.

func NewProxiedHandshake added in v0.5.0

func NewProxiedHandshake(r *http.Request, auth any) *Handshake

NewProxiedHandshake is NewHandshake for a server running behind a trusted reverse proxy: Address is the first entry of the X-Forwarded-For header when one is present, falling back to RemoteAddr otherwise. Only use it when a proxy you control is guaranteed to replace (not append to) any client-supplied X-Forwarded-For, because the value is otherwise attacker-chosen.

func (*Handshake) Get added in v0.3.0

func (h *Handshake) Get(header string) string

Get returns the first value of a request header (case-insensitive), or "" if absent — a convenience over reaching into Headers directly.

func (*Handshake) Param added in v0.3.0

func (h *Handshake) Param(key string) string

Param returns the first value of a query-string parameter, or "" if absent.

type Namespace

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

Namespace is a communication channel that partitions a Socket.IO server. Each namespace has its own set of connected sockets, rooms, and connection handlers. The default namespace is "/".

func (*Namespace) DisconnectSockets

func (ns *Namespace) DisconnectSockets(closeTransport bool)

DisconnectSockets disconnects every socket in the namespace.

func (*Namespace) Emit

func (ns *Namespace) Emit(event string, args ...any)

Emit broadcasts an event to every socket in the namespace.

func (*Namespace) Except added in v0.3.0

func (ns *Namespace) Except(room string) *BroadcastOperator

Except returns a broadcast operator over the namespace that excludes every socket in the given room — the equivalent of io.except(room).

func (*Namespace) FetchSockets

func (ns *Namespace) FetchSockets() []*Socket

FetchSockets returns all sockets in the namespace (alias for Sockets, matching io.fetchSockets()).

func (*Namespace) In added in v0.3.0

func (ns *Namespace) In(room string) *BroadcastOperator

In is an alias for To, scoping a broadcast to a room within this namespace — the equivalent of io.in(room).

func (*Namespace) Local added in v0.3.0

func (ns *Namespace) Local() *BroadcastOperator

Local returns a broadcast operator over the namespace restricted to this node — the equivalent of io.local.

func (*Namespace) Name

func (ns *Namespace) Name() string

Name returns the namespace name (e.g. "/" or "/admin").

func (*Namespace) OnConnection

func (ns *Namespace) OnConnection(fn func(*Socket)) *Namespace

OnConnection registers a handler invoked for each new socket that connects to this namespace.

func (*Namespace) SetAdapter

func (ns *Namespace) SetAdapter(a Adapter) *Namespace

SetAdapter replaces the namespace's room adapter (e.g. with a Redis-backed one for multi-node scale-out). Call it before any sockets connect.

func (*Namespace) Sockets

func (ns *Namespace) Sockets() []*Socket

Sockets returns all sockets currently connected to the namespace.

func (*Namespace) SocketsInRoom

func (ns *Namespace) SocketsInRoom(room string) []*Socket

SocketsInRoom returns the sockets that are members of a room.

func (*Namespace) SocketsJoin

func (ns *Namespace) SocketsJoin(rooms ...string)

SocketsJoin makes every socket in the namespace join the given rooms.

func (*Namespace) SocketsLeave

func (ns *Namespace) SocketsLeave(rooms ...string)

SocketsLeave makes every socket in the namespace leave the given rooms.

func (*Namespace) To

func (ns *Namespace) To(room string) *BroadcastOperator

To returns a broadcast operator scoped to a room within this namespace.

func (*Namespace) Use

func (ns *Namespace) Use(fn func(socket *Socket, next func(err error))) *Namespace

Use registers connection middleware for the namespace. Each middleware runs for every incoming connection before the connection handler; calling next with a non-nil error rejects the connection with a CONNECT_ERROR carrying the error's message — the equivalent of io.use((socket, next) => ...).

type Options

type Options struct {
	// Path is the HTTP path the server handles (default "/socket.io/").
	Path string
	// PingInterval is how often the server sends heartbeat pings.
	PingInterval time.Duration
	// PingTimeout is how long the server waits for a pong before disconnecting.
	PingTimeout time.Duration
	// MaxPayload advertises the maximum HTTP payload size to clients.
	MaxPayload int
	// CheckOrigin, if set, authorizes cross-origin requests. When nil, all
	// origins are allowed — including cross-site WebSocket handshakes, which
	// are not subject to the same-origin policy. Set it on any deployment where
	// a connection carries ambient authority (cookies, session headers).
	CheckOrigin func(r *http.Request) bool
	// TrustProxy makes Socket.Handshake().Address honor the first entry of the
	// X-Forwarded-For header. Leave it false unless a reverse proxy you control
	// overwrites that header: it is otherwise fully client-controlled.
	TrustProxy bool
	// MaxSessions caps the number of concurrent Engine.IO sessions the server
	// will hold. Every unauthenticated handshake request allocates a session and
	// a heartbeat goroutine that survive until the ping timeout elapses, so
	// without a cap a request flood grows the session map without bound. When
	// the cap is reached, further handshakes are refused with 503 while existing
	// sessions keep working. Zero selects DefaultMaxSessions; a negative value
	// disables the cap entirely.
	MaxSessions int
}

Options configures a Server.

Example (TrustProxy)

ExampleOptions_trustProxy shows when the X-Forwarded-For header may be believed. Socket.Handshake().Address defaults to the address of the socket the request actually arrived on, because X-Forwarded-For is a plain request header any client can set — trusting it unconditionally would let a client pick the address that rate limiting, authorization and audit logs see. Turn TrustProxy on only when a reverse proxy you control *overwrites* the header.

package main

import (
	"fmt"
	"net/http"
	"net/http/httptest"

	socketio "github.com/malcolmston/socketio"
)

func main() {
	behindProxy := socketio.New(socketio.Options{TrustProxy: true})
	defer behindProxy.Close()

	direct := socketio.New() // TrustProxy is false by default
	defer direct.Close()

	r := httptest.NewRequest(http.MethodGet, "/socket.io/", nil)
	r.RemoteAddr = "10.0.0.9:51234"
	r.Header.Set("X-Forwarded-For", "203.0.113.7, 10.0.0.1")

	fmt.Println("trusted:  ", socketio.NewProxiedHandshake(r, nil).Address)
	fmt.Println("untrusted:", socketio.NewHandshake(r, nil).Address)
}
Output:
trusted:   203.0.113.7
untrusted: 10.0.0.9

type Packet

type Packet struct {
	// Type is the packet's Socket.IO type (Connect, Event, Ack, ...).
	Type PacketType
	// Namespace the packet targets; defaults to "/".
	Namespace string
	// ID is the acknowledgement id when the packet requests or answers an ack;
	// nil otherwise.
	ID *uint64
	// Data is the decoded JSON payload: an array for Event/Ack ([name, args...]
	// or [args...]) and an object for Connect/ConnectError.
	Data any
	// contains filtered or unexported fields
}

Packet is a decoded Socket.IO protocol packet.

func DecodePacket

func DecodePacket(s string) (Packet, error)

DecodePacket parses a Socket.IO packet from its wire form.

func (Packet) Args

func (p Packet) Args() []any

Args returns the event arguments (everything after the event name) for an Event packet, or the full array for an Ack packet.

func (Packet) Attachments

func (p Packet) Attachments() int

Attachments returns the declared number of binary attachments for a BINARY_EVENT/BINARY_ACK packet.

func (Packet) Encode

func (p Packet) Encode() (string, error)

Encode renders a packet to its Socket.IO wire form (the string carried inside an Engine.IO MESSAGE packet).

func (Packet) EncodeBinary

func (p Packet) EncodeBinary() (text string, buffers [][]byte, err error)

EncodeBinary encodes a packet, extracting any binary attachments. When the payload contains []byte values the packet type is promoted to its binary variant and the buffers are returned separately; otherwise buffers is nil. It is exported for client implementations.

func (Packet) EventName

func (p Packet) EventName() string

EventName returns the event name for an Event/BinaryEvent packet.

func (Packet) HasBinaryData added in v0.3.0

func (p Packet) HasBinaryData() bool

HasBinaryData reports whether the packet's payload contains any []byte values, i.e. whether encoding it will produce binary attachment frames.

type PacketType

type PacketType byte

PacketType identifies a Socket.IO packet.

const (
	// Connect initiates a namespace connection.
	Connect PacketType = iota
	// Disconnect leaves a namespace.
	Disconnect
	// Event carries an application event and its arguments.
	Event
	// Ack answers an Event that requested acknowledgement.
	Ack
	// ConnectError reports a failed namespace connection.
	ConnectError
	// BinaryEvent is an Event with binary attachments (decoded as text here).
	BinaryEvent
	// BinaryAck is an Ack with binary attachments (decoded as text here).
	BinaryAck
)

func (PacketType) String

func (t PacketType) String() string

String returns the Socket.IO name of the packet type (e.g. "EVENT", "ACK"), or "UNKNOWN" for an unrecognized value.

type RoomTargeter

type RoomTargeter interface {
	To(room string) *BroadcastOperator
}

RoomTargeter is anything that can scope a broadcast to a room, returning a *BroadcastOperator for further chaining (.To/.Except/.Emit). The server, a namespace, an individual socket, and an existing operator all expose this, mirroring socket.io's io.to(room) / socket.to(room) API.

type Server

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

Server is a Socket.IO server. It implements http.Handler and should be mounted at its configured Path.

Example

ExampleServer wires up a complete Socket.IO server the way a chat backend would. It creates a server with New, registers a connection handler that runs for every client that connects to the default namespace, and inside that handler joins the socket to a room, registers a "chat" event handler that re-broadcasts each message to everyone in that room via To(...).Emit, and installs a disconnect callback. The server is an http.Handler, so it is mounted here on an httptest.Server (a real net/http server would use http.Handle plus http.ListenAndServe at the same DefaultPath). The example then shuts everything down cleanly with Close on both the HTTP server and the Socket.IO server. The reader should take away the end-to-end shape of a server — New, OnConnection, Join, On, room broadcast, and mounting on HTTP — which mirrors the JavaScript io.on("connection")/socket.join/io.to().emit API.

package main

import (
	"fmt"
	"net/http/httptest"

	socketio "github.com/malcolmston/socketio"
)

func main() {
	io := socketio.New()
	io.OnConnection(func(s *socketio.Socket) {
		// Every socket joins a shared room on connect.
		s.Join("room1")

		// Re-broadcast each chat message to the whole room.
		s.On("chat", func(args []any) []any {
			io.To("room1").Emit("chat", args...)
			return nil
		})

		s.OnDisconnect(func(reason string) {
			// Clean up per-socket state here.
			_ = reason
		})
	})

	// A *Server is an http.Handler. In production:
	//
	//	http.Handle(socketio.DefaultPath, io)
	//	http.ListenAndServe(":3000", nil)
	//
	// Here httptest hosts it so the example is self-contained.
	ts := httptest.NewServer(io)
	defer ts.Close()
	defer io.Close()

	fmt.Println("serving Socket.IO at", socketio.DefaultPath)
}
Output:
serving Socket.IO at /socket.io/

func New

func New(opts ...Options) *Server

New creates a Server. An optional Options may be supplied.

func (*Server) Attach

func (s *Server) Attach(mux *http.ServeMux)

Attach registers the server on an http.ServeMux at its Path.

func (*Server) Close

func (s *Server) Close()

Close disconnects every connected session and shuts the server down. It does not stop the underlying http.Server (the caller owns that).

func (*Server) DisconnectSockets

func (s *Server) DisconnectSockets(closeTransport bool)

DisconnectSockets disconnects every socket in the default namespace.

func (*Server) Emit

func (s *Server) Emit(event string, args ...any)

Emit broadcasts an event to every socket in the default namespace.

func (*Server) Except added in v0.3.0

func (s *Server) Except(room string) *BroadcastOperator

Except returns a broadcast operator over the default namespace excluding every socket in the given room — the equivalent of io.except(room).

func (*Server) FetchSockets

func (s *Server) FetchSockets() []*Socket

FetchSockets returns all sockets in the default namespace.

func (*Server) Handler

func (s *Server) Handler(next http.Handler) http.Handler

Handler wraps the server so it intercepts Socket.IO requests (those under its Path) and delegates everything else to next — the Go equivalent of attaching Socket.IO to an existing HTTP server that is otherwise served by Express:

app := express.New()          // your routes
io := socketio.New()          // your socket handlers
http.ListenAndServe(":3000", io.Handler(app))

next may be any http.Handler (an *express.Application, an http.ServeMux, ...); pass nil to 404 non-Socket.IO requests.

func (*Server) In added in v0.3.0

func (s *Server) In(room string) *BroadcastOperator

In scopes a server-wide broadcast to a room in the default namespace — the equivalent of io.in(room).

func (*Server) Local added in v0.3.0

func (s *Server) Local() *BroadcastOperator

Local returns a broadcast operator over the default namespace restricted to this node — the equivalent of io.local.

func (*Server) Of

func (s *Server) Of(name string) *Namespace

Of returns the namespace with the given name, creating it if necessary. A name without a leading slash has one added.

func (*Server) OnConnection

func (s *Server) OnConnection(fn func(*Socket))

OnConnection registers a handler invoked when a socket connects to the default namespace. It is the equivalent of io.on("connection", ...).

func (*Server) OnServerEvent

func (s *Server) OnServerEvent(event string, handler func(args []any)) *Server

OnServerEvent registers a handler for server-side events delivered via ServerSideEmit. On this single-node implementation these are local; a multi-node deployment would relay them between servers through an adapter.

func (*Server) ServeHTTP

func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request)

ServeHTTP implements http.Handler and dispatches Engine.IO transport requests.

func (*Server) ServerSideEmit

func (s *Server) ServerSideEmit(event string, args ...any)

ServerSideEmit emits an event to other servers (and this one), the equivalent of io.serverSideEmit. Single-node: it invokes locally registered handlers.

func (*Server) SetBroadcaster

func (s *Server) SetBroadcaster(b Broadcaster) *Server

SetBroadcaster installs a cluster broadcaster. Once set, every room/namespace broadcast is published through it and delivered to local sockets when the message is received back, so all nodes in the cluster stay in sync.

func (*Server) Sockets

func (s *Server) Sockets() []*Socket

Sockets returns all connected sockets in the default namespace.

func (*Server) SocketsJoin

func (s *Server) SocketsJoin(rooms ...string)

SocketsJoin makes every socket in the default namespace join the given rooms.

func (*Server) SocketsLeave

func (s *Server) SocketsLeave(rooms ...string)

SocketsLeave makes every socket in the default namespace leave the given rooms.

func (*Server) To

func (s *Server) To(room string) *BroadcastOperator

To returns a broadcast operator scoped to a room in the default namespace.

func (*Server) Use

func (s *Server) Use(fn func(socket *Socket, next func(err error))) *Server

Use registers connection middleware on the default namespace.

type Socket

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

Socket is a single client connection to a namespace. It is the primary object applications interact with — registering event handlers, emitting events, and joining rooms.

func (*Socket) Auth

func (s *Socket) Auth() any

Auth returns the authentication payload the client sent with CONNECT.

func (*Socket) Broadcast

func (s *Socket) Broadcast() *BroadcastOperator

Broadcast returns an operator targeting every other socket in the namespace.

func (*Socket) Data

func (s *Socket) Data() map[string]any

Data returns a copy of all values stored on the socket.

func (*Socket) Delete

func (s *Socket) Delete(key string) *Socket

Delete removes a stored value.

func (*Socket) Disconnect

func (s *Socket) Disconnect(closeTransport bool)

Disconnect closes the socket, optionally closing the underlying transport.

func (*Socket) Emit

func (s *Socket) Emit(event string, args ...any) error

Emit sends an event to this socket. Emitting a reserved Socket.IO event name (connect, connect_error, disconnect, disconnecting, ...) returns ErrReservedEvent without sending anything, matching the JavaScript server, which throws for those names — they are the library's own lifecycle signals and must not be spoofed as application events.

func (*Socket) EmitAck

func (s *Socket) EmitAck(event string, timeout time.Duration, args ...any) ([]any, error)

EmitAck sends an event and blocks until the client acknowledges it or timeout elapses, returning the acknowledgement arguments. On timeout it returns ErrAckTimeout and forgets the acknowledgement, so a client that never answers (or answers late) cannot make the socket accumulate pending callbacks.

func (*Socket) EmitWithAck

func (s *Socket) EmitWithAck(event string, ackFn func(args []any), args ...any) error

EmitWithAck sends an event and invokes ackFn with the client's acknowledgement arguments. If the packet cannot be sent the pending callback is discarded and the send error is returned; ackFn is not invoked.

func (*Socket) Except added in v0.3.0

func (s *Socket) Except(room string) *BroadcastOperator

Except returns a broadcast operator over the namespace that excludes this socket and every member of the given room — the equivalent of socket.except(room).

func (*Socket) Get

func (s *Socket) Get(key string) (any, bool)

Get returns a value previously stored with Set, and whether it was present.

func (*Socket) GetString

func (s *Socket) GetString(key string) string

GetString returns a stored string value, or "" if missing / not a string.

func (*Socket) Handshake added in v0.5.0

func (s *Socket) Handshake() *Handshake

Handshake returns the details of the HTTP request that established this socket's session — headers, query string, address, TLS flag, and the auth payload sent with CONNECT — the equivalent of socket.handshake in the JavaScript API. It is nil only for sockets created outside an HTTP request (in tests). The returned value is a per-socket snapshot; mutating it does not affect the session or other namespaces on it.

func (*Socket) ID

func (s *Socket) ID() string

ID returns the socket's unique identifier.

func (*Socket) In added in v0.3.0

func (s *Socket) In(room string) *BroadcastOperator

In returns a broadcast operator that targets a room while excluding this socket — the equivalent of socket.in(room). It behaves like To.

func (*Socket) Join

func (s *Socket) Join(rooms ...string) *Socket

Join adds the socket to a room.

func (*Socket) Leave

func (s *Socket) Leave(rooms ...string) *Socket

Leave removes the socket from a room.

func (*Socket) ListenersAny added in v0.3.0

func (s *Socket) ListenersAny() []func(event string, args []any)

ListenersAny returns a snapshot of the currently registered catch-all listeners — the equivalent of socket.listenersAny().

func (*Socket) Namespace

func (s *Socket) Namespace() *Namespace

Namespace returns the namespace this socket belongs to.

func (*Socket) Off

func (s *Socket) Off(event string) *Socket

Off removes all handlers for an event.

func (*Socket) OffAny added in v0.3.0

func (s *Socket) OffAny() *Socket

OffAny removes catch-all listeners. Called with no argument it removes every catch-all listener; called with one listener it is a no-op placeholder for API symmetry (Go funcs are not comparable, so individual removal is not supported) and clears all listeners. Use ListenersAny to inspect the current set. Returns the socket for chaining.

func (*Socket) On

func (s *Socket) On(event string, handler EventHandler) *Socket

On registers a handler for an event.

func (*Socket) OnAny added in v0.3.0

func (s *Socket) OnAny(fn func(event string, args []any)) *Socket

OnAny registers a catch-all listener invoked for every inbound event on this socket, in registration order and before the named handlers run. The callback receives the event name and its arguments. Returns the socket for chaining — the equivalent of socket.onAny(listener).

func (*Socket) OnDisconnect

func (s *Socket) OnDisconnect(fn func(reason string)) *Socket

OnDisconnect registers a callback invoked when the socket disconnects.

func (*Socket) OnDisconnecting added in v0.5.0

func (s *Socket) OnDisconnecting(fn func(reason string)) *Socket

OnDisconnecting registers a callback invoked as the socket begins to disconnect, before it has left its rooms — the equivalent of socket.on("disconnecting"). Unlike OnDisconnect, the socket's Rooms() are still populated when this fires, so a handler can record which rooms the socket was in on its way out. It runs with the same reason string that the subsequent disconnect carries.

func (*Socket) PendingAcks added in v0.5.0

func (s *Socket) PendingAcks() int

PendingAcks reports how many acknowledgements this socket is still waiting for. It is primarily useful in tests and for instrumentation.

func (*Socket) PrependAny added in v0.3.0

func (s *Socket) PrependAny(fn func(event string, args []any)) *Socket

PrependAny registers a catch-all listener at the front of the catch-all chain, so it runs before previously registered catch-all listeners — the equivalent of socket.prependAny(listener).

func (*Socket) Rooms

func (s *Socket) Rooms() []string

Rooms returns the set of rooms the socket is currently in.

func (*Socket) Set

func (s *Socket) Set(key string, value any) *Socket

Set stores an arbitrary value on the socket, persisting for the lifetime of the connection — the equivalent of Socket.IO's socket.data. Use it to attach session-like state (the authenticated user, a tenant id, ...).

func (*Socket) To

func (s *Socket) To(room string) *BroadcastOperator

To returns a broadcast operator that targets a room, excluding this socket — the equivalent of socket.to(room).

Directories

Path Synopsis
Package client is a Go Socket.IO client.
Package client is a Go Socket.IO client.
docs
gen command
Command gendocs generates a static HTML documentation site for a Go module using only the standard library (go/doc, go/parser).
Command gendocs generates a static HTML documentation site for a Go module using only the standard library (go/doc, go/parser).
Package engineio implements the Engine.IO v4 protocol codec — the transport framing layer that Socket.IO is built on.
Package engineio implements the Engine.IO v4 protocol codec — the transport framing layer that Socket.IO is built on.
examples
chat command
Command chat is a small Socket.IO server in Go: it echoes messages and broadcasts chat messages to everyone in a room.
Command chat is a small Socket.IO server in Go: it echoes messages and broadcasts chat messages to everyone in a room.
client command
Command client connects to a Socket.IO server using the Go client and exchanges a couple of events.
Command client connects to a Socket.IO server using the Go client and exchanges a couple of events.
internal
ws
Package ws is a minimal, dependency-free RFC 6455 WebSocket implementation, sufficient to carry Engine.IO/Socket.IO traffic.
Package ws is a minimal, dependency-free RFC 6455 WebSocket implementation, sufficient to carry Engine.IO/Socket.IO traffic.
Package redis provides a Redis-backed Broadcaster for socketio, enabling multi-node scale-out: broadcasts are relayed between server instances over Redis pub/sub so a message emitted on one node reaches sockets connected to any node.
Package redis provides a Redis-backed Broadcaster for socketio, enabling multi-node scale-out: broadcasts are relayed between server instances over Redis pub/sub so a message emitted on one node reaches sockets connected to any node.

Jump to

Keyboard shortcuts

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