arctic

package module
v0.0.3 Latest Latest
Warning

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

Go to latest
Published: Aug 25, 2026 License: AGPL-3.0 Imports: 20 Imported by: 0

README

Tests Made with Golang

arctic

A TCP/UDP client-server abstraction library written in Go

Features

  • TCP and UDP support
  • Native and browser WebSocket clients
  • WebSocket server sidecars with shared connection handlers and lifecycle
  • Event-driven architecture
  • Connection management
  • Optional Gob encoding/decoding
  • Graceful shutdown
  • Configurable timeouts and buffer sizes
  • Middleware support for custom processing
  • Client metadata handshake support
  • Optional unsafe zero-copy receive buffers

UDP is datagram-based. Arctic tracks server-side UDP peers as virtual clients keyed by remote address. Raw UDP remains best-effort and does not add reliability, ordering, retransmits, or remote connection teardown semantics. Use TCP when you need reliable ordered delivery. Use UDP when you want fast best-effort datagrams and can tolerate packet loss or handle reliability at the application level.

WebSocket Sidecars

A WebSocket sidecar lets browser and native WebSocket clients use the same handlers as the primary TCP or UDP server. The sidecar has its own TCP listener; a UDP primary may use the same numeric port because TCP and UDP have separate port namespaces.

server, err := arctic.NewServer(arctic.ServerConfig{
    BindAddress: ":8080",
    Protocol: arctic.ProtocolTCP,
})
if err != nil {
    log.Fatal(err)
}

sidecar, err := arctic.NewWebSocketServer(arctic.WebSocketServerConfig{
    BindAddress: ":8081",
    Path: "/arctic",
    OriginPatterns: []string{"example.com"},
})
if err != nil {
    log.Fatal(err)
}
if err = server.AddWebSocketSidecar(sidecar); err != nil {
    log.Fatal(err)
}

server.OnClient(func(client *arctic.ServerClient) {
    client.OnMessage(func(message []byte) {
        _ = client.Send(message)
    })
})

if err = server.Listen(); err != nil {
    log.Fatal(err)
}

The same client API works in native Go and in GOOS=js GOARCH=wasm browser builds:

client, err := arctic.NewClient(arctic.ClientConfig{
    ServerAddress: "wss://example.com/arctic",
    Protocol: arctic.ProtocolWebSocket,
})

Starting the primary listener and its sidecars is transactional: if an endpoint cannot be bound, listeners already started for that server are closed and Listen returns an error wrapping ErrEndpointInUse when the operating system reports a port collision. Closing the primary server closes its sidecars and all clients.

Client Metadata

Clients can attach JSON-compatible metadata to the connection. For TCP and Gob-over-TCP, Arctic sends it as an internal open handshake, so server OnClient handlers can read it before normal OnMessage traffic starts.

client, err = arctic.NewClient(arctic.ClientConfig{
    ServerAddress: "localhost:8080",
    Metadata: map[string]any{
        "tenant": "acme",
        "trace_id": "demo-123",
        "debug": true,
    },
})

Read metadata from raw or Gob server clients with Metadata():

server.OnClient(func(client *arctic.ServerClient) {
    var metadata map[string]any = client.Metadata()
    log.Printf("client metadata: %#v", metadata)
})

Metadata values should be JSON-compatible: strings, booleans, numbers, nil, arrays, and objects. Numeric values are decoded as json.Number on the server side.

UDP metadata is best-effort. Arctic sends it as an initial datagram, but UDP can drop or reorder datagrams, so a UDP OnClient handler may run before metadata is available. If a valid metadata datagram arrives later, Arctic stores it for future Metadata() calls, but OnClient is not called again.

Arctic reserves the \x00arctic.metadata.v1\x00 prefix for its metadata handshake. Any first TCP frame or first UDP datagram beginning with that prefix is treated as metadata, not application data, and is rejected if the remaining bytes are not valid JSON. If you send arbitrary binary payloads, ensure the first message/datagram does not start with this reserved prefix unless you intend to send metadata.

Unsafe Zero-Copy

By default, Arctic gives each OnMessage call a safe message slice that can be retained after the handler returns. Set UnsafeZeroCopy: true on ClientConfig or ServerConfig to reduce receive-side allocations by reusing internal read buffers.

With UnsafeZeroCopy enabled, the []byte passed to OnMessage is only valid during that handler call. Copy it before storing it, sending it to another goroutine, or keeping it after the handler returns:

client.OnMessage(func(message []byte) {
    saved := append([]byte{}, message...)
    _ = saved
})

This option can improve allocation and GC behavior for raw TCP and UDP messages. It does not remove the operating system socket copy, and it does not affect TCP Gob stream decoding.

Testing

Tests are organized by purpose:

  • tests/implementation: TCP, UDP, Gob, close handling, and zero-copy behavior tests
  • tests/coverage: constructor, validation, registry, and coverage-focused smoke tests
  • tests/benchmark: TCP and UDP benchmark suites
  • tests/internal/testutil: shared test helpers

Useful commands:

go test ./...
go test ./tests/coverage ./tests/implementation -coverpkg=github.com/z46-dev/arctic -coverprofile=coverage.out
go tool cover -func=coverage.out
go test ./tests/benchmark -bench . -benchmem

Basic Usage

Set up a server:

package main

import (
    "log"
    "fmt"
    "time"

    "github.com/z46-dev/arctic"
)

func main() {
    var (
        server *arctic.Server
        err error
    )

    if server, err = arctic.NewServer(arctic.ServerConfig{
        BindAddress: "[::]:8080",
        BufferSize: 1024,
        Timeout: 5 * time.Second,
    }); err != nil {
        log.Fatalf("Failed to create server: %v", err)
    }

    server.OnClient(func(client *arctic.ServerClient) {
        var logPrefix string = fmt.Sprintf("[%d | %s]", client.ID(), client.RemoteAddr())

        log.Printf("%s Client connected", logPrefix)

        client.OnMessage(func(msg []byte) {
            log.Printf("%s Received message: %s", logPrefix, string(msg))
            client.Send([]byte("Message received"))
        })

        client.OnClose(func() {
            log.Printf("%s Client disconnected", logPrefix)
        })

        client.OnError(func(err error) {
            log.Printf("%s Client error: %v", logPrefix, err)
        })
    })

    if err = server.Listen(); err != nil {
        log.Fatalf("Failed to start server: %v", err)
    }
}

Set up a client:

package main

import (
    "log"
    "time"

    "github.com/z46-dev/arctic"
)

func main() {
    var (
        client *arctic.Client
        err error
    )

    if client, err = arctic.NewClient(arctic.ClientConfig{
        ServerAddress: "localhost:8080",
        BufferSize: 1024,
        Timeout: 5 * time.Second,
    }); err != nil {
        log.Fatalf("Failed to create client: %v", err)
    }

    client.OnMessage(func(msg []byte) {
        log.Printf("Received message from server: %s", string(msg))
    })

    client.OnClose(func() {
        log.Println("Connection closed by server")
    })

    client.OnError(func(err error) {
        log.Printf("Client error: %v", err)
    })

    if err = client.Connect(); err != nil {
        log.Fatalf("Failed to connect to server: %v", err)
    }

    // Send a message to the server
    if err = client.Send([]byte("Hello, Server!")); err != nil {
        log.Printf("Failed to send message: %v", err)
    }

    // Keep the client running to receive messages
    time.Sleep(time.Second)
}

See runnable examples in the examples directory for more usage patterns, including UDP, Gob encoding, and metadata.

Or run them yourself with:

go run github.com/z46-dev/arctic/examples/tcp_basic@latest
go run github.com/z46-dev/arctic/examples/udp_basic@latest
go run github.com/z46-dev/arctic/examples/gob_basic@latest
go run github.com/z46-dev/arctic/examples/metadata_basic@latest
go run github.com/z46-dev/arctic/examples/websocket_native@latest

The browser WebSocket example includes a Go WASM client and a page that loads it. See examples/websocket_browser for build and browser instructions.

You can pass -addr <address> to override the default bind/connect address for the example. For example:

go run github.com/z46-dev/arctic/examples/tcp_basic@latest -addr localhost:9090

Documentation

Index

Constants

This section is empty.

Variables

View Source
var (
	ErrProtocolUnsupported       error = errors.New("arctic: protocol is not supported")
	ErrClientNotConnected        error = errors.New("arctic: client is not connected")
	ErrServerAlreadyListening    error = errors.New("arctic: server is already listening")
	ErrEndpointInUse             error = errors.New("arctic: endpoint is already in use")
	ErrSidecarAlreadyAttached    error = errors.New("arctic: websocket sidecar is already attached")
	ErrMessageTooLarge           error = errors.New("arctic: message exceeds configured buffer size")
	ErrMetadataInvalid           error = errors.New("arctic: metadata is invalid")
	ErrGobTypeInvalid            error = errors.New("arctic: gob message type must be a struct")
	ErrGobTypeNotRegistered      error = errors.New("arctic: gob message type is not registered")
	ErrGobTypeRegistrationFailed error = errors.New("arctic: gob message type registration failed")
)

Functions

func IsGobTypeRegistered

func IsGobTypeRegistered[MessageType any]() (registered bool)

IsGobTypeRegistered reports whether a Gob message type is registered.

func RegisterGobType

func RegisterGobType[MessageType any](sample MessageType) (err error)

RegisterGobType registers a struct type for Arctic Gob clients and servers.

Types

type Client

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

func NewClient

func NewClient(config ClientConfig) (client *Client, err error)

NewClient creates a raw client for the configured transport.

func (*Client) Close

func (client *Client) Close() (err error)

Close releases the client connection and invokes its close handler once.

func (*Client) Connect

func (client *Client) Connect() (err error)

Connect opens the client's configured transport and starts receiving messages.

func (*Client) LocalAddr

func (client *Client) LocalAddr() (addr net.Addr)

LocalAddr returns the local transport address when connected.

func (*Client) Metadata

func (client *Client) Metadata() (metadata map[string]any)

Metadata returns an isolated snapshot of the connection metadata.

func (*Client) OnClose

func (client *Client) OnClose(handler CloseHandler)

OnClose sets the client's close handler.

func (*Client) OnError

func (client *Client) OnError(handler ErrorHandler)

OnError sets the client's asynchronous error handler.

func (*Client) OnMessage

func (client *Client) OnMessage(handler MessageHandler)

OnMessage sets the client's raw message handler.

func (*Client) RemoteAddr

func (client *Client) RemoteAddr() (addr net.Addr)

RemoteAddr returns the remote transport address when connected.

func (*Client) Send

func (client *Client) Send(message []byte) (err error)

Send transmits one raw message to the remote peer.

func (*Client) Use

func (client *Client) Use(middleware ...Middleware)

Use appends middleware to the client's raw message pipeline.

type ClientConfig

type ClientConfig struct {
	ServerAddress  string
	Protocol       Protocol
	BufferSize     int
	Timeout        time.Duration
	UnsafeZeroCopy bool
	Metadata       map[string]any
}

type ClientHandler

type ClientHandler func(*ServerClient)

type CloseHandler

type CloseHandler func()

type ErrorHandler

type ErrorHandler func(error)

type GobClient

type GobClient[MessageType any] struct {
	*Client
	// contains filtered or unexported fields
}

func NewGobClient

func NewGobClient[MessageType any](config ClientConfig) (client *GobClient[MessageType], err error)

NewGobClient creates a typed Gob client for the configured transport.

func (*GobClient[MessageType]) Connect

func (client *GobClient[MessageType]) Connect() (err error)

Connect opens the typed client's transport and starts decoding messages.

func (*GobClient[MessageType]) OnMessage

func (client *GobClient[MessageType]) OnMessage(handler GobMessageHandler[MessageType])

OnMessage sets the typed message handler.

func (*GobClient[MessageType]) Send

func (client *GobClient[MessageType]) Send(message MessageType) (err error)

Send encodes and transmits one typed message.

func (*GobClient[MessageType]) Use

func (client *GobClient[MessageType]) Use(middleware ...GobMiddleware[MessageType])

Use appends middleware to the typed message pipeline.

type GobClientHandler

type GobClientHandler[MessageType any] func(*GobServerClient[MessageType])

type GobMessageContext

type GobMessageContext[MessageType any] struct {
	Client  *GobClient[MessageType]
	Message MessageType
}

type GobMessageHandler

type GobMessageHandler[MessageType any] func(MessageType)

type GobMiddleware

type GobMiddleware[MessageType any] func(*GobMessageContext[MessageType], GobNext[MessageType]) error

type GobNext

type GobNext[MessageType any] func(*GobMessageContext[MessageType]) error

type GobServer

type GobServer[MessageType any] struct {
	*Server
	// contains filtered or unexported fields
}

func NewGobServer

func NewGobServer[MessageType any](config ServerConfig) (server *GobServer[MessageType], err error)

NewGobServer creates a typed Gob server.

func (*GobServer[MessageType]) Close

func (server *GobServer[MessageType]) Close() (err error)

Close stops the typed server and clears its client registry.

func (*GobServer[MessageType]) Listen

func (server *GobServer[MessageType]) Listen() (err error)

Listen starts the typed server and every attached sidecar.

func (*GobServer[MessageType]) OnClient

func (server *GobServer[MessageType]) OnClient(handler GobClientHandler[MessageType])

OnClient sets the handler invoked for typed clients.

type GobServerClient

type GobServerClient[MessageType any] struct {
	*GobClient[MessageType]
	// contains filtered or unexported fields
}

func (*GobServerClient[MessageType]) ID

func (serverClient *GobServerClient[MessageType]) ID() (id int)

ID returns the server-assigned typed client identifier.

type MessageContext

type MessageContext struct {
	Client  *Client
	Message []byte
}

type MessageHandler

type MessageHandler func([]byte)

type Middleware

type Middleware func(*MessageContext, Next) error

type Next

type Next func(*MessageContext) error

type Protocol

type Protocol string
const (
	ProtocolTCP       Protocol = "tcp"
	ProtocolUDP       Protocol = "udp"
	ProtocolWebSocket Protocol = "websocket"
)

type Server

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

func NewServer

func NewServer(config ServerConfig) (server *Server, err error)

NewServer creates a TCP or UDP server.

func (*Server) AddWebSocketSidecar added in v0.0.2

func (server *Server) AddWebSocketSidecar(sidecar *WebSocketServer) (err error)

AddWebSocketSidecar attaches a WebSocket listener to the server's lifecycle.

func (*Server) Addr

func (server *Server) Addr() (addr net.Addr)

Addr returns the primary listener's bound address.

func (*Server) Close

func (server *Server) Close() (err error)

Close stops all listeners and connected clients.

func (*Server) Listen

func (server *Server) Listen() (err error)

Listen starts the primary listener and every attached sidecar.

func (*Server) OnClient

func (server *Server) OnClient(handler ClientHandler)

OnClient sets the handler invoked for newly discovered clients.

func (*Server) OnClose

func (server *Server) OnClose(handler CloseHandler)

OnClose sets the server shutdown handler.

func (*Server) OnError

func (server *Server) OnError(handler ErrorHandler)

OnError sets the server's asynchronous error handler.

type ServerClient

type ServerClient struct {
	*Client
	// contains filtered or unexported fields
}

func (*ServerClient) ID

func (serverClient *ServerClient) ID() (id int)

ID returns the server-assigned client identifier.

type ServerConfig

type ServerConfig struct {
	BindAddress    string
	Protocol       Protocol
	BufferSize     int
	Timeout        time.Duration
	UnsafeZeroCopy bool
}

type WebSocketServer added in v0.0.2

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

func NewWebSocketServer added in v0.0.2

func NewWebSocketServer(config WebSocketServerConfig) (server *WebSocketServer, err error)

NewWebSocketServer creates a sidecar that can be attached to an Arctic server.

func (*WebSocketServer) Addr added in v0.0.2

func (server *WebSocketServer) Addr() (addr net.Addr)

Addr returns the bound address after the sidecar starts listening.

type WebSocketServerConfig added in v0.0.2

type WebSocketServerConfig struct {
	BindAddress    string
	Path           string
	OriginPatterns []string
	Timeout        time.Duration
}

Directories

Path Synopsis
examples
gob_basic command
metadata_basic command
tcp_basic command
udp_basic command
tests

Jump to

Keyboard shortcuts

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