Repository navigation
Connections
- You need to know which of
Conn,NSConnorRoomto reach for. - You're storing per-connection state (a user record, a rate-limit counter) and want it to live and die with the connection.
- You want to list, inspect or act on every connection the server currently holds.
- You're deciding how a connection gets its ID, or handling connect/disconnect at the whole-connection level rather than per namespace.
One websocket is one *neffos.Conn. It can be connected to zero or more namespaces, each represented by its own *neffos.NSConn (see Namespaces), and each NSConn can be joined to zero or more rooms, each a *neffos.Room (see Rooms). This page is about the outermost layer, Conn itself: identity, per-connection storage, server-wide inspection, and the whole-connection lifecycle hooks.
type IDGenerator func(w http.ResponseWriter, r *http.Request) string
var DefaultIDGenerator IDGenerator = func(http.ResponseWriter, *http.Request) string {
// a random UUID v4, or a unix timestamp if that fails.
}Server.IDGenerator runs once per handshake, server-side, and its return value becomes Conn.ID(). The default is a random UUID, unique for practical purposes across any number of servers. A custom generator, like the chat examples' "use the authenticated user's name", is not: two different server processes can both mint the ID "alice" for two different physical connections. That is fine for a single server, and fine for scale out too as long as you accept that a Message.To-targeted message reaches whichever connection currently holds that ID, on whichever server subscribes to it.
A Go client sends identifying information as a real handshake header; a browser cannot set custom headers during a websocket handshake, so it has to go as a URL query parameter instead. A generator that supports both looks like the one in _examples/01-getting-started/02-namespaces onward:
srv.IDGenerator = func(w http.ResponseWriter, r *http.Request) string {
if name := r.Header.Get("X-Username"); name != "" {
return name
}
if name := r.URL.Query().Get("name"); name != "" {
return name
}
return neffos.DefaultIDGenerator(w, r)
}func (c *Conn) Set(key string, value any)
func (c *Conn) Value[T any](key string) (T, bool)
func (c *Conn) Increment(key string) int
func (c *Conn) Decrement(key string) intA Conn carries its own map[string]any, guarded by a lock, that lives exactly as long as the connection. Set stores anything; Value[T] reads it back typed, and reports false when the key is missing or holds a value of another type, so there is no type assertion to write (Value[any] returns whatever was stored, which is what the old Get did). Increment/Decrement treat the stored value as an int and are safe to call concurrently from different event callbacks on the same connection, which makes them a convenient per-connection counter without reaching for your own sync/atomic.
c.Conn.Set("user", user)
// later, in another event of the same connection:
user, ok := c.Conn.Value[User]("user")
if !ok {
return errors.New("not authenticated")
}A rate-limit handler is the common use: count events in a window, and close the connection once it floods. _examples/05-connections/rate-limit keeps a one-second sliding window with nothing but Increment and a deferred Decrement:
neffos.Events{
"Say": func(c *neffos.NSConn, msg neffos.Message) error {
if n := c.Conn.Increment("recent"); n > limit {
log.Printf("[%s] closed: %d messages in the last %s", c.Conn.ID(), n, window)
return neffos.CloseError{Code: neffos.ClosePolicyViolation, Reason: "rate limit"}
}
time.AfterFunc(window, func() { c.Conn.Decrement("recent") })
msg.Body = fmt.Appendf(nil, "%s: %s", c.Conn.ID(), msg.Body)
c.BroadcastOthers(msg)
return nil
},
}Each message increments "recent" and schedules a matching Decrement one second later, so "recent" always holds the count from the last second; cross that count and the handler returns a neffos.CloseError instead of relaying the message. Returning a CloseError from a handler closes the connection with that code and reason instead of just sending an error reply; see Close codes and shutdown.
func (s *Server) GetConnections() map[string]*Conn
func (s *Server) GetConnectionsByNamespace(namespace string) map[string]*NSConn
func (s *Server) GetTotalConnections() uint64GetTotalConnections is an atomic read, cheap enough to call as often as you like. GetConnections and GetConnectionsByNamespace build a fresh map on every call and are explicitly documented as not fast: use them for debugging, logging, or an occasional admin endpoint, not on a hot path. Both are safe to call concurrently, including from inside a Server.Do callback (below).
_examples/05-connections/inspect-and-do's list command combines all three into one line per connection:
// list logs every connection with the namespaces it is in.
func list(srv *neffos.Server) {
in := map[string][]string{} // connection ID to namespaces
for _, ns := range namespaces {
for id := range srv.GetConnectionsByNamespace(ns) {
in[id] = append(in[id], ns)
}
}
conns := srv.GetConnections()
var lines []string
for _, id := range slices.Sorted(maps.Keys(conns)) {
line := fmt.Sprintf("%s (%s", id, strings.Join(in[id], ", "))
if c := conns[id]; c.WasReconnected() {
line += fmt.Sprintf(", reconnect %d", c.ReconnectTries)
}
lines = append(lines, line+")")
}
log.Printf("%d connections: %s", srv.GetTotalConnections(), strings.Join(lines, ", "))
}func (s *Server) Do(fn func(*Conn), async bool)Do runs fn once per currently connected Conn. With async == false it blocks until every connection has been visited; with true it returns immediately. fn runs on the server's own dispatch goroutine, so keep it short: long work inside fn delays other Do calls, disconnect notifications and synchronous broadcasts until it returns. The same example's "news <text>" operator command uses it to reach only the connections that entered a second namespace:
switch cmd {
case "news":
sent := 0
srv.Do(func(c *neffos.Conn) {
if ns := c.Namespace("news"); ns != nil && ns.Emit("Headline", []byte(arg)) {
sent++ // Do with async=false returns after the last call, so no lock is needed
}
}, false)
log.Printf("headline sent to %d of %d", sent, srv.GetTotalConnections())
}"drop <name>", in the same example, calls Conn.DisconnectAll from outside Do, since it is slow enough (it waits for the client's answer) that it should not run on the dispatch goroutine:
for scanner.Scan() {
switch cmd {
case "drop":
c, ok := srv.GetConnections()[arg]
if !ok {
log.Printf("drop %s: not connected", arg)
continue
}
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
if err := c.DisconnectAll(ctx); err != nil {
log.Printf("drop %s: %v", arg, err)
}
cancel()
}
}Conn.DisconnectAll(ctx) leaves from every namespace and room but keeps the socket; Conn.Close() and Conn.Terminate(code, reason) end the socket itself (Close is Terminate(CloseNormalClosure, "")). See Close codes and shutdown for the difference between closing one connection this way and shutting the whole server down.
type Server struct {
OnUpgradeError func(err error)
OnConnect func(c *Conn) error
OnDisconnect func(c *Conn)
FireDisconnectAlways bool
}-
OnUpgradeErrorruns when the HTTP-to-websocket upgrade itself fails, before anyConnexists; there is nothing to act on but the error. -
OnConnectruns once the handshake succeeds, before the connection can send or receive anything else. A non-nil return refuses the connection: the remote side gets the error text, and the socket closes right after. Return a plainerrorto close with the default code (ClosePolicyViolation, 1008), or aneffos.CloseError{Code: ..., Reason: ...}to choose the code yourself, the same as returning one from an event handler. -
OnDisconnectruns once the connection is gone, from any cause.c.Err()says what closed it, andneffos.CloseStatus(c.Err())turns that into a close code. A peer that drops with a clean FIN and no close frame still reports a code,neffos.CloseAbnormalClosure(1006), on all three backends;CloseStatusreturns-1only when there was no code at all (a timeout, the heartbeat, or a hard TCP reset):srv.OnDisconnect = func(c *neffos.Conn) { switch err := c.Err(); { case errors.Is(err, neffos.ErrMessageTooBig): log.Printf("[%s] sent an oversized message", c.ID()) case neffos.IsTimeoutError(err): log.Printf("[%s] timed out", c.ID()) default: log.Printf("[%s] disconnected, close status %d", c.ID(), neffos.CloseStatus(err)) } }
-
FireDisconnectAlways(defaultfalse) controls one edge case: whenOnConnectitself callsClose()or returns a non-nil error, shouldOnDisconnectstill fire for that connection? Set it totrueif your bookkeeping (metrics, a connection registry) needs to see a disconnect for every connect it saw, including onesOnConnectrejected.
type Conn struct {
ReconnectTries int
// ...
}
func (c *Conn) WasReconnected() boolWasReconnected reports whether the current connection is the result of a client redialing after a drop, and ReconnectTries is how many attempts that took. Both are meaningful inside OnConnect, inside any event handler, or in OnDisconnect. See Reconnection for how the client side signals this and what the server does with the X-Websocket-Reconnect header.
Nothing in neffos groups connections by anything other than their Conn.ID(). A user with a phone and a laptop open is, as far as the server is concerned, two unrelated connections. Grouping them, sending to "all of this user's devices", and deciding what "online" means once there can be more than one connection per user, is application code: a small registry keyed by user, populated in OnNamespaceConnected and cleaned up in OnNamespaceDisconnect. See _examples/05-connections/users-and-devices for a complete version ("name#n" connection IDs, a registry of *NSConn sets, and a Tell event that reaches every device of a user plus a copy back to the sender's other devices).
-
_examples/05-connections/users-and-devices: one user, several connections, a registry that tracks "online" across all of a user's devices. -
_examples/05-connections/rate-limit:Increment/Decrementplus aCloseErrorfor a connection that floods. -
_examples/05-connections/inspect-and-do:GetConnections,GetConnectionsByNamespaceandServer.Dofrom an admin command.
-
Namespaces and Rooms, the two layers above a bare
Conn -
Close codes and shutdown,
Terminate,CloseErrorandCloseStatusin full -
Reconnection,
WasReconnected,ReconnectTriesand the client side that sets them -
Authentication, storing the authenticated user with
Conn.SetduringOnConnect
Home | About | Project | Getting Started | Technical Docs | Copyright © 2019-2026 Gerasimos Maropoulos. Documentation terms of use.
Getting started
Concepts
Messaging
Production
Scale out