Socket.io
WebSocket wrapper for Fiber that implements the Engine.IO v4 / Socket.IO v5 wire protocol, making it fully compatible with the official socket.io-client library.
For applications that used older socketio releases as a plain WebSocket event bus, migrate to github.com/gofiber/contrib/v3/websocket/event. A deprecated compatibility shim is available at github.com/gofiber/contrib/v3/socketio/legacy.
Compatible with Fiber v3.
Featuresโ
This middleware implements the full Engine.IO v4 / Socket.IO v5 wire protocol. Highlights:
- Synchronous handshake. The Engine.IO OPEN / Socket.IO CONNECT exchange completes before the user
New()callback returns, so emits issued inside the callback are ordered after the handshake reply. - HTTP long-polling fallback (opt-in). Set
socketio.EnablePolling = trueand mount the same handler forGETandPOSTto accepttransport=pollingclients. Polling sessions speak the same Engine.IO v4 / Socket.IO v5 wire protocol over HTTP and route through the same listener API (Emit, Ack, Close, Broadcast). Polling-to-WebSocket transport upgrade is not yet implemented; sessions that connect via polling stay on polling. - Namespaces and handshake auth. The negotiated namespace is honoured for inbound and outbound packets; the client's connect-time
authpayload is exposed viaWebsocket.HandshakeAuth()andEventPayload.HandshakeAuth. - Inbound acks. Client-initiated callbacks surface as
EventPayload.HasAck/AckID; reply once withpayload.Ack(args...). - Outbound acks. Server-initiated
EmitWithAck,EmitWithAckTimeout, andEmitWithAckArgsround-trip a callback id and invoke the supplied callback when the client acks (or on timeout/disconnect). - Multi-arg events. Inbound events expose every argument tuple as
EventPayload.Args [][]byte; outboundEmitArgs/EmitWithAckArgssend pre-encoded JSON tuples. - Deterministic heartbeat. Server PINGs every
PingInterval; the connection is torn down if no PONG arrives withinPingTimeout. The heartbeat is a runtime timer, not a goroutine, and an idle read deadline ofPingInterval + PingTimeoutbacks it up. - Closing handshake.
Closequeues the SIO DISCONNECT packet and a Close frame behind the emits already pending, keeps reading until the peer's Close frame arrives (bounded byCloseTimeout), then closes the socket, sosocket.io-clientreportsio server disconnectand does not reconnect. A client SIO DISCONNECT is answered the same way. Every tear-down releases the socket, including when a write is stalled on a peer that stopped reading. - Two goroutines per connection. The upgrade handler's goroutine runs the read loop and one goroutine serialises writes; polling sessions own no goroutine at all.
- Zero-copy inbound, SWAR outbound. Inbound events are validated once and split into sub-slices of the frame they arrived in; outbound frames are built in a single pre-sized allocation with the SWAR JSON string encoder from
gofiber/utils, and a broadcast builds each namespace's frame once for all its recipients. - Coalesced writes. On top of the websocket middleware's write coalescing, replies to a burst of pipelined client frames leave in one write.
- EIO 0x1E batched frames. Multi-packet WebSocket frames separated by ASCII RS (
0x1E) are parsed correctly, with a hard cap (MaxBatchPackets) to prevent slice-header amplification. - Reserved-event-name guard. User code cannot register or emit names reserved by the protocol (e.g.
connect,disconnect). - EIO version validation. Handshakes that advertise an unsupported
EIOversion are rejected. - Auth payload validation. The auth blob must be a JSON object and is bounded by
MaxAuthPayload; oversize or malformed payloads are answered with CONNECT_ERROR. - DoS hardening.
MaxPayload,MaxBatchPackets,MaxEventNameLength,MaxEventArgs, andMaxAuthPayloadbound every attacker-controlled length and count. - Lock-free listener registry plus
atomic.Bool isAlive, removing the per-event mutex from the hot path. - Optional drop-frames-on-overflow. When
DropFramesOnOverflowis true, a saturated send queue drops the offending frame and firesEventErrorinstead of tearing down the connection. - Graceful drain. The package-level
Shutdown(ctx)closes every active socket and waits for each worker to exit (or untilctxis cancelled).
Known limitationsโ
- One namespace per Engine.IO connection. Each WebSocket binds the namespace negotiated during the SIO CONNECT packet; multiplexing several namespaces over one EIO connection is not supported. A CONNECT for another namespace on an established connection is answered with CONNECT_ERROR (
Invalid namespace) instead of an acknowledgement whose events would never be delivered. - No BINARY_EVENT (5) / BINARY_ACK (6). Binary Socket.IO frames are passed through as raw
EventMessagedata; attachment reassembly is not implemented. - No connection-state recovery. Resume-on-reconnect (Socket.IO's
connectionStateRecoveryfeature) is not implemented; reconnects always start a fresh session. - No polling-to-WebSocket transport upgrade. When polling is enabled, sessions that open with
transport=pollingadvertise an emptyupgradesarray and stay on polling for the session lifetime. Clients that need WebSocket from the start should configuretransports: ['websocket']. - No JSONP polling fallback. JSONP requests (
?j=N) are rejected with engine.io error code 3. Modern browsers use XHR2/fetch; JSONP support is not planned. - CORS is not handled by the middleware. Mount
github.com/gofiber/fiber/v3/middleware/cors(or your preferred CORS middleware) upstream of the polling route to control the policy. Long-poll holds connections open for up to ~25s by default, so reverse-proxy timeouts must accommodate (e.g. nginxproxy_read_timeout >= 60sandproxy_buffering off).
Production hardening notesโ
- Rate limiting. Each polling open allocates a
*Websocketand arms a heartbeat timer (no goroutine). WithEnablePolling = truean unauthenticated client can create sessions untilHandshakeTimeoutreaps idle ones (10s default). Mountgithub.com/gofiber/fiber/v3/middleware/limiterupstream of the route to bound concurrent session creation. - Write timeout. A long-poll GET response that the client never reads pins a fasthttp worker on TCP backpressure. Configure
fiber.Config{WriteTimeout: ...}(a few seconds is typically appropriate) so abandoned reads do not strand workers. - Burst sizing.
PollQueueMaxFrames(default1024) bounds the per-session outbound buffer. With the defaultDropFramesOnOverflow = falsea synchronous burst of more than 1024 emits inside a single listener call disconnects the session withErrSendQueueClosed. Either pace large bursts across drains, raisePollQueueMaxFrames, or setDropFramesOnOverflow = trueto tolerate overflow at the cost of dropped frames +EventError. - Listener panics. Both transports recover panics inside the
New()callback and inside event listeners; the panic value is logged via the packageLoggerhook. Avoidpanic(string(attackerControlledBytes))to prevent log injection in downstream consumers.
Performanceโ
The hot paths avoid encoding/json's reflection and copy nothing they do not have to. Inbound frames are handed over by the websocket middleware's pooled ReadMessage, validated once, and split into sub-slices; outbound frames are assembled in one pre-sized buffer with gofiber/utils' SWAR JSON string encoder; a broadcast builds each namespace's frame once and shares it between recipients. Each WebSocket connection costs two goroutines (the upgrade handler running the read loop, plus the writer) and one runtime timer instead of four goroutines; a polling session costs none.
benchstat over go test -run '^$' -bench . -benchmem -count=6 on a 4 vCPU runner, before and after (in-memory listener, root namespace):
| Benchmark | Before | After | Change |
|---|---|---|---|
| Parse inbound event (name + 3 args) | 2.71 ยตs, 16 allocs | 0.75 ยตs, 2 allocs | -72% time |
| Build outbound event, JSON argument | 716 ns, 5 allocs | 421 ns, 1 alloc | -41% time |
| Build outbound event, raw-text argument | 1.13 ยตs, 12 allocs | 198 ns, 1 alloc | -82% time |
| Round trip: emit, listener, reply (1 connection) | 15.9 ยตs, 24 allocs | 8.5 ยตs, 10 allocs | -47% time |
| Broadcast to 1024 subscribers | 1.58 ms, 7192 allocs | 0.54 ms, 2009 allocs | -66% time |
| Connect: handshake and close | 119 ยตs, 143 allocs | 140 ยตs, 155 allocs | +17% time |
The connect path is the one that got slower; almost all of it is the websocket middleware's upgrade now running through net/http's upgrader on a fasthttp hijack, which is what makes write coalescing possible, and the same shift appears when the previous socketio code is built against it.
Configurationโ
All tunables are package-level variables; override before the first connection is accepted.
| Variable | Default | Meaning |
|---|---|---|
PingInterval | 25s | How often the server emits Engine.IO PING. |
PingTimeout | 20s | Grace window for the client PONG before the connection is killed. |
HandshakeTimeout | 10s | Hard deadline for completing EIO OPEN + SIO CONNECT. |
MaxPayload | 1_000_000 (1 MB) | Max bytes per inbound WebSocket frame; advertised to the client. |
MaxAuthPayload | 8 KiB | Max bytes for the SIO CONNECT auth JSON. |
MaxBatchPackets | 256 | Max EIO packets in a single 0x1E-batched frame. |
MaxEventNameLength | 256 | Max length of an inbound SIO event name. |
MaxEventArgs | 256 | Max elements (event name included) in an inbound SIO EVENT or ACK array. |
CloseTimeout | 5s | One budget for the whole tear-down after Close: waiting for a slot in a saturated send queue, reading for the peer's Close frame and letting a stalled write finish all share it. Zero closes the socket as soon as the frames are queued. |
WriteTimeout | 0 (disabled) | Deadline for a single WebSocket frame write by the send goroutine; a peer that stops reading is otherwise only detected by the heartbeat. |
OutboundAckTimeout | 30s | Default ack deadline for EmitWithAck. |
SendQueueSize | 100 | Capacity of the per-connection outbound queue. |
DropFramesOnOverflow | false | If true, drop the offending frame on overflow (fires EventError). |
RetrySendTimeout | 20ms | Deprecated: no effect. The websocket middleware allocates a connection per upgrade, so there is no released connection to retry against. |
MaxSendRetry | 5 | Deprecated: no effect; see RetrySendTimeout. |
ReadTimeout | 10ms | Deprecated: no longer consulted by the read loop; kept for backward compatibility. |
EnablePolling | false | If true, the handler returned from New also serves Engine.IO HTTP long-polling on GET/POST. |
PollingMaxBufferSize | 1_000_000 | Cap on a single polling HTTP body (request POST or response GET drain). The drain that ends a session always carries the SIO DISCONNECT and EIO CLOSE packets; farewell frames that do not fit beside them are dropped. |
MaxPollWait | 30s | Maximum time a long-poll GET blocks waiting for outbound frames. |
PollQueueMaxFrames | 1024 | Cap on buffered outbound frames per polling session; overflow honors DropFramesOnOverflow. The SIO DISCONNECT and EIO CLOSE packets queued by Close are exempt, so a queue an EventClose listener filled still ends the session cleanly. |
Use socketio.Shutdown(ctx) from fiber.App.ShutdownWithContext for a deterministic drain.
Go version supportโ
We only support the latest two versions of Go. Visit https://go.dev/doc/devel/release for more information.
Installโ
go get -u github.com/gofiber/fiber/v3
go get -u github.com/gofiber/contrib/v3/socketio
Protocol compatibilityโ
The middleware automatically handles the Engine.IO / Socket.IO handshake so you do not need any special server-side code; just point your socket.io-client at the WebSocket endpoint.
Required client configurationโ
The default socket.io-client transport order is ['polling', 'websocket']. The middleware supports both, but polling is opt-in:
WebSocket only (default):
import { io } from "socket.io-client";
const socket = io("http://localhost:3000", {
path: "/ws", // match the Fiber route
transports: ["websocket"], // skip polling
});
Polling (or polling + websocket fallback): enable EnablePolling server-side and mount the handler for both GET and POST:
socketio.EnablePolling = true
h := socketio.New(func(kws *socketio.Websocket) { /* ... */ })
app.Get("/ws", h)
app.Post("/ws", h)
// Optionally allow CORS preflight:
// app.Options("/ws", h)
import { io } from "socket.io-client";
// Default transport order: polling first, then upgrade attempt. Since this
// implementation does not yet upgrade polling sessions to WebSocket, the
// session stays on polling. For a forced WebSocket connect use
// transports: ["websocket"]; for polling-only use transports: ["polling"].
const socket = io("http://localhost:3000", {
path: "/ws",
});
CORS is not handled by the middleware. If your client connects from a different origin, mount your preferred CORS middleware (e.g.
github.com/gofiber/fiber/v3/middleware/cors) upstream of the route. Long-polling holds a request open for up to ~25s by default, so reverse-proxy timeouts must accommodate (proxy_read_timeout>= 60s on nginx,proxy_buffering off).
Polling pitfallsโ
- Forgot to mount POST. Polling clients send packets via POST; without
app.Post(path, h)(orapp.All(...)) the server returns 404 and the client loops withtransport error. Always mount bothGETandPOSTfor polling routes. kws.Connis nil on polling. Usekws.IsPolling()to branch, or stick to the transport-agnosticEmit,EmitEvent,EmitArgs,EmitWithAck*,Broadcast,Ack, andClosemethods. They all work identically on both transports.- Snapshot vs live request state.
kws.Locals,kws.Params,kws.Query,kws.Cookiesare captured at session-open time on polling sessions (because fasthttp recycles the request context after the OPEN handler returns). Store mutable per-connection data viakws.SetAttributeinstead. - Burst bigger than
PollQueueMaxFrames. With the defaultDropFramesOnOverflow = false, emitting more thanPollQueueMaxFrames(1024) frames before any GET drains them tears the session down withErrSendQueueClosed. Either pace bursts, raisePollQueueMaxFrames, or setDropFramesOnOverflow = trueto drop the offending frames + fireEventError(ErrSendQueueOverflow)instead. - Body limit collision. If your Fiber app sets
BodyLimitlower thanPollingMaxBufferSize, fasthttp rejects the POST before our handler runs. KeepBodyLimit>=PollingMaxBufferSize.
Tunable globalsโ
These package-level variables can be overridden before the first connection is accepted (typically in init() or early in main). They control timing and limits for the Engine.IO / Socket.IO transport.
| Variable | Default | Description |
|---|---|---|
PingInterval | 25 * time.Second | Interval between Engine.IO PING frames sent by the server to keep the connection alive. |
PingTimeout | 20 * time.Second | How long the server waits for the client's PONG before considering the connection dead. |
HandshakeTimeout | 10 * time.Second | Maximum time allowed for the Engine.IO / Socket.IO handshake (including namespace CONNECT) to complete. |
CloseTimeout | 5 * time.Second | Budget for the whole tear-down after Close or a client SIO DISCONNECT; the socket is closed once the peer's Close frame arrives or the budget is spent, whatever it was spent on. |
WriteTimeout | 0 (disabled) | Deadline for a single WebSocket frame write; zero relies on the heartbeat to detect a peer that stopped reading. |
MaxPayload | 1 << 20 (1 MiB) | Maximum size in bytes for a single inbound WebSocket frame; oversize messages close the socket. |
MaxAuthPayload | 8 << 10 (8 KiB) | Maximum size in bytes for the Socket.IO CONNECT auth JSON. |
MaxBatchPackets | 256 | Maximum number of Engine.IO packets accepted in a single 0x1E-batched frame. |
MaxEventNameLength | 256 | Maximum length of an inbound Socket.IO event name. |
MaxEventArgs | 256 | Maximum number of elements, event name included, in an inbound Socket.IO EVENT or ACK array. |
OutboundAckTimeout | 30 * time.Second | Default timeout used by EmitWithAck when no per-call timeout is supplied. |
DropFramesOnOverflow | false | If true, saturated outbound queues drop the offending frame and fire EventError. |
RetrySendTimeout | 20 * time.Millisecond | Deprecated: no effect; the send goroutine writes each frame exactly once. |
MaxSendRetry | 5 | Deprecated: no effect; see RetrySendTimeout. |
EnablePolling | false | If true, the handler also accepts Engine.IO HTTP long-polling on GET/POST (opt-in fallback). |
PollingMaxBufferSize | 1_000_000 | Cap on a single polling HTTP body (POST request body or GET drain response body), in bytes; the packets Close queues always fit in the drain that ends the session. |
MaxPollWait | 30 * time.Second | Maximum time a long-poll GET blocks waiting for outbound frames before returning an empty 200. |
PollQueueMaxFrames | 1024 | Maximum buffered outbound frames per polling session before overflow handling applies; the packets Close queues are exempt. |
func init() {
socketio.PingInterval = 15 * time.Second
socketio.PingTimeout = 10 * time.Second
socketio.MaxPayload = 4 << 20 // 4 MiB
}
Message formatโ
All messages are exchanged as Socket.IO events.
| Side | API call | Wire format |
|---|---|---|
| Server โ Client | kws.Emit([]byte("hello")) | 42["message","hello"] |
| Server โ Client | kws.EmitEvent("greet", data) | 42["greet",<data>] |
| Client โ Server | socket.emit("message", obj) | fires EventMessage with obj |
| Client โ Server | socket.emit("custom", obj) | fires the "custom" event |
Note:
Emit,EmitEvent,EmitArgs, and ack-emitting variants pass valid JSON through unchanged. Raw text bytes are encoded as JSON strings for compatibility with older examples.
Acks, namespaces, handshake authโ
The middleware implements the full Socket.IO v5 ack flow and forwards the client's connect-time auth payload to your handlers.
Multi-argument emitsโ
EmitArgs and EmitWithAckArgs accept a variadic list of values, so you can send richer event tuples without manually concatenating arrays. Valid JSON is passed through unchanged; raw text is encoded as a JSON string.
// 42["greet","hi",{"id":1}]
kws.EmitArgs("greet", []byte(`"hi"`), []byte(`{"id":1}`))
Server-initiated acksโ
EmitWithAck (and EmitWithAckTimeout) emit an event with an ack id and invoke the supplied callback once the client acks, or with an error when the timeout expires. EmitWithAck uses OutboundAckTimeout; EmitWithAckTimeout takes a per-call duration plus a structured AckCallback that distinguishes timeout from disconnect.
kws.EmitWithAckTimeout("ping", []byte(`"hello"`), 3*time.Second, func(ack []byte, err error) {
if err != nil {
log.Printf("ack failed: %v", err)
return
}
// ack is the raw JSON the client passed to its callback (single value
// or a JSON-array literal for multi-arg acks).
})
Client-initiated acksโ
When the client emits with a callback, the inbound event payload carries an ack id. Use HasAck and AckID to detect it, then send a single ack reply via EventPayload.Ack:
socketio.On("greet", func(ep *socketio.EventPayload) {
if ep.HasAck {
// ep.Args holds the raw JSON arguments the client sent.
_ = ep.Ack([]byte(`"ok"`))
}
})
Namespacesโ
The middleware honours the namespace negotiated during the Socket.IO CONNECT packet. Events emitted from the server are routed back on the same namespace the client joined; no extra configuration is required on the Go side.
Handshake authโ
The client's auth payload must be a JSON object. It is parsed during the Socket.IO handshake and exposed to handlers as EventPayload.HandshakeAuth (raw JSON bytes). It is most commonly inspected on EventConnect:
// client
const socket = io("http://localhost:3000", {
path: "/ws",
transports: ["websocket"],
auth: { token: "secret" },
});
socketio.On(socketio.EventConnect, func(ep *socketio.EventPayload) {
// ep.HandshakeAuth == []byte(`{"token":"secret"}`)
var auth struct{ Token string `json:"token"` }
_ = json.Unmarshal(ep.HandshakeAuth, &auth)
})
Signaturesโ
// Initialize new socketio in the callback this will
// execute a callback that expects kws *Websocket Object
// and optional config websocket.Config
func New(callback func(kws *Websocket), config ...websocket.Config) func(fiber.Ctx) error
// Add listener callback for an event into the listeners list
func On(event string, callback func(payload *EventPayload))
// Emit the message to a specific socket uuids list
// Ignores all errors
func EmitToList(uuids []string, message []byte, mType ...int)
// Emit to a specific socket connection
func EmitTo(uuid string, message []byte, mType ...int) error
// Broadcast to all the active connections
func Broadcast(message []byte, mType ...int)
// Fire custom event on all connections
func Fire(event string, data []byte)
// Emit a named event with multiple arguments
// (e.g. EmitArgs("greet", []byte(`"hi"`), []byte(`{"id":1}`)))
func (kws *Websocket) EmitArgs(event string, args ...[]byte)
// Emit a named event and invoke cb when the client acks (or on timeout /
// disconnect). The default deadline is OutboundAckTimeout. The callback
// receives the raw JSON ack value (or nil on timeout/disconnect).
func (kws *Websocket) EmitWithAck(event string, data []byte, cb func(ack []byte))
// Like EmitWithAck but with a per-call timeout and a structured AckCallback
// that distinguishes ErrAckTimeout from ErrAckDisconnected. Pass timeout = 0
// to disable the timeout.
func (kws *Websocket) EmitWithAckTimeout(event string, data []byte, timeout time.Duration, cb AckCallback)
// Multi-argument variant of EmitWithAck. The callback receives the slice of
// raw ack arguments the client supplied (or an error on timeout /
// disconnect). Uses OutboundAckTimeout.
func (kws *Websocket) EmitWithAckArgs(event string, args [][]byte, cb func([][]byte, error))
// HandshakeAuth returns the raw JSON auth payload sent by the client at
// connect time (nil if the client did not provide one).
func (kws *Websocket) HandshakeAuth() json.RawMessage
// Ack sends a Socket.IO ACK frame back to the client for the inbound event
// represented by this payload. Idempotent: only the first invocation
// produces a wire frame; later calls return ErrAckAlreadySent.
func (ep *EventPayload) Ack(args ...[]byte) error
Exampleโ
Go serverโ
package main
import (
"encoding/json"
"fmt"
"log"
"github.com/gofiber/contrib/v3/socketio"
"github.com/gofiber/contrib/v3/websocket"
"github.com/gofiber/fiber/v3"
)
// MessageObject Basic chat message object
type MessageObject struct {
Data string `json:"data"`
From string `json:"from"`
Event string `json:"event"`
To string `json:"to"`
}
func main() {
// The key for the map is message.to
clients := make(map[string]string)
// Start a new Fiber application
app := fiber.New()
// Setup the middleware to retrieve the data sent in first GET request
app.Use(func(c fiber.Ctx) error {
// IsWebSocketUpgrade returns true if the client
// requested upgrade to the WebSocket protocol.
if websocket.IsWebSocketUpgrade(c) {
c.Locals("allowed", true)
return c.Next()
}
return fiber.ErrUpgradeRequired
})
// Multiple event handling supported
socketio.On(socketio.EventConnect, func(ep *socketio.EventPayload) {
fmt.Printf("Connection event 1 - User: %s", ep.Kws.GetStringAttribute("user_id"))
})
// Custom event handling supported
socketio.On("CUSTOM_EVENT", func(ep *socketio.EventPayload) {
fmt.Printf("Custom event - User: %s", ep.Kws.GetStringAttribute("user_id"))
// --->
// DO YOUR BUSINESS HERE
// --->
})
// On message event
socketio.On(socketio.EventMessage, func(ep *socketio.EventPayload) {
fmt.Printf("Message event - User: %s - Message: %s", ep.Kws.GetStringAttribute("user_id"), string(ep.Data))
message := MessageObject{}
// Unmarshal the json message
// {
// "from": "<user-id>",
// "to": "<recipient-user-id>",
// "event": "CUSTOM_EVENT",
// "data": "hello"
//}
err := json.Unmarshal(ep.Data, &message)
if err != nil {
fmt.Println(err)
return
}
// Fire custom event based on some
// business logic
if message.Event != "" {
ep.Kws.Fire(message.Event, []byte(message.Data))
}
// Emit the message directly to specified user
err = ep.Kws.EmitTo(clients[message.To], ep.Data, socketio.TextMessage)
if err != nil {
fmt.Println(err)
}
})
// On disconnect event
socketio.On(socketio.EventDisconnect, func(ep *socketio.EventPayload) {
// Remove the user from the local clients
delete(clients, ep.Kws.GetStringAttribute("user_id"))
fmt.Printf("Disconnection event - User: %s", ep.Kws.GetStringAttribute("user_id"))
})
// On close event
// This event is called when the server disconnects the user actively with .Close() method
socketio.On(socketio.EventClose, func(ep *socketio.EventPayload) {
// Remove the user from the local clients
delete(clients, ep.Kws.GetStringAttribute("user_id"))
fmt.Printf("Close event - User: %s", ep.Kws.GetStringAttribute("user_id"))
})
// On error event
socketio.On(socketio.EventError, func(ep *socketio.EventPayload) {
fmt.Printf("Error event - User: %s", ep.Kws.GetStringAttribute("user_id"))
})
app.Get("/ws/:id", socketio.New(func(kws *socketio.Websocket) {
// Retrieve the user id from endpoint
userId := kws.Params("id")
// Add the connection to the list of the connected clients
// The UUID is generated randomly and is the key that allow
// socketio to manage Emit/EmitTo/Broadcast
clients[userId] = kws.UUID
// Every websocket connection has an optional session key => value storage
kws.SetAttribute("user_id", userId)
// Broadcast to all the connected users the newcomer
newUserMsg, _ := json.Marshal(fmt.Sprintf("New user connected: %s and UUID: %s", userId, kws.UUID))
kws.Broadcast(newUserMsg, true, socketio.TextMessage)
// Write welcome message. Raw text is encoded as a JSON string.
welcomeMsg, _ := json.Marshal(fmt.Sprintf("Hello user: %s with UUID: %s", userId, kws.UUID))
kws.Emit(welcomeMsg, socketio.TextMessage)
}))
log.Fatal(app.Listen(":3000"))
}
TypeScript / JavaScript clientโ
import { io } from "socket.io-client";
const socket = io("http://localhost:3000", {
path: "/ws",
transports: ["websocket"],
});
socket.on("connect", () => {
console.log("connected, sid =", socket.id);
// Send a message to the server
socket.emit("message", {
from: "user1",
to: "user2",
event: "",
data: "hello",
});
});
socket.on("message", (data: unknown) => {
console.log("received message:", data);
});
socket.on("disconnect", (reason) => {
console.log("disconnected:", reason);
});
Supported eventsโ
| Const | Event | Description |
|---|---|---|
| EventMessage | message | Fired when a socket.emit("message", โฆ) event is received from the client |
| EventPing | ping | Fired when a WebSocket PING control frame is received (RFC 6455); Data carries its payload. Engine.IO PING is server-originated and not surfaced via this event. |
| EventPong | pong | Fired when an Engine.IO PONG ("3") replies to the server's heartbeat or when a WebSocket PONG control frame is received. |
| EventDisconnect | disconnect | Fired exactly once on disconnection. Error is nil for a clean close (Close, a client SIO DISCONNECT, or a peer Close frame with code 1000, 1001 or none, RFC 6455 section 11.7) and carries the cause otherwise. |
| EventConnect | connect | Fired after the Engine.IO / Socket.IO handshake completes; ep.HandshakeAuth is populated with the client's auth payload (raw JSON, nil if not provided) |
| EventClose | close | Fired exactly once when the connection is actively closed from the server via Close, before the closing frames are queued: frames a listener emits still reach the client. Different from client disconnection |
| EventError | error | Fired when some error appears useful also for debugging websockets |
Custom events map directly to the event name used in socket.emit("myEvent", โฆ) on the client and kws.EmitEvent("myEvent", data) on the server.
Event Payload objectโ
| Variable | Type | Description |
|---|---|---|
| Kws | *Websocket | The connection object |
| Name | string | The name of the event |
| SocketUUID | string | Unique connection UUID |
| SocketAttributes | map[string]any | Snapshot of the connection's attributes at dispatch time; nil when none were set |
| Error | error | (optional) Fired from disconnection or error events |
| Data | []byte | Raw JSON of the event payload (first argument of socket.emit); a sub-slice of the frame the event arrived in, safe to retain |
| Args | [][]byte | All raw JSON arguments after the event name; useful when the client emits multiple values |
| AckID | uint64 | Ack id assigned by the client when it emitted with a callback (0 if HasAck is false) |
| HasAck | bool | True when the inbound event expects an ack reply; respond via EventPayload.Ack(args...) |
| HandshakeAuth | json.RawMessage | Raw JSON auth payload from the Socket.IO handshake; populated on EventConnect listeners (use Kws.HandshakeAuth() elsewhere) |
Socket instance functionsโ
| Name | Type | Description |
|---|---|---|
| SetAttribute | void | Set a specific attribute for the specific socket connection |
| GetUUID | string | Get socket connection UUID |
| SetUUID | error | Set socket connection UUID |
| GetAttribute | string | Get a specific attribute from the socket attributes |
| EmitToList | void | Emit the message to a specific socket uuids list |
| EmitTo | error | Emit to a specific socket connection |
| Broadcast | void | Broadcast to all the active connections except broadcasting the message to itself |
| Fire | void | Fire custom event |
| Emit | void | Send data as a "message" socket.io event; valid JSON is passed through, raw text is JSON-encoded |
| EmitEvent | void | Send a named socket.io event; valid JSON is passed through, raw text is JSON-encoded |
| EmitArgs | void | Emit a named event with multiple arguments; valid JSON is passed through, raw text is JSON-encoded |
| EmitWithAck | void | Emit an event and invoke cb(ack) when the client acks (uses OutboundAckTimeout) |
| EmitWithAckTimeout | void | Like EmitWithAck but with a per-call timeout and a structured AckCallback |
| EmitWithAckArgs | void | Multi-arg variant; cb([][]byte, error) receives the ack tuple (uses OutboundAckTimeout) |
| HandshakeAuth | json.RawMessage | Raw JSON auth payload sent by the client at connect time (nil if absent) |
| IsAlive | bool | Reports whether the underlying connection is still open and the heartbeat loop is running |
| IsPolling | bool | Reports whether the session is bound to HTTP long-polling rather than WebSocket; when true, Conn is nil |
| Close | void | Actively close the connection from the server |
Note: the FastHTTP connection can be accessed directly from the instance
kws.Conn
kws.Conn is nil for HTTP long-polling sessions. Code that touches the underlying WebSocket directly should guard with if kws.Conn != nil or check the transport via the absence of kws.Conn. Listener APIs (Emit, Ack, Close, Broadcast, EmitWithAck, etc.) work transparently on both transports.