arctic

package module
v0.0.1 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: 17 Imported by: 0

README

Tests Made with Golang

arctic

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

Features

  • TCP and UDP support
  • 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.

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

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")
	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)

func RegisterGobType

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

Types

type Client

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

func NewClient

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

func (*Client) Close

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

func (*Client) Connect

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

func (*Client) LocalAddr

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

func (*Client) Metadata

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

func (*Client) OnClose

func (client *Client) OnClose(handler CloseHandler)

func (*Client) OnError

func (client *Client) OnError(handler ErrorHandler)

func (*Client) OnMessage

func (client *Client) OnMessage(handler MessageHandler)

func (*Client) RemoteAddr

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

func (*Client) Send

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

func (*Client) Use

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

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)

func (*GobClient[MessageType]) Connect

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

func (*GobClient[MessageType]) OnMessage

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

func (*GobClient[MessageType]) Send

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

func (*GobClient[MessageType]) Use

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

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)

func (*GobServer[MessageType]) Close

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

func (*GobServer[MessageType]) Listen

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

func (*GobServer[MessageType]) OnClient

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

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)

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"
)

type Server

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

func NewServer

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

func (*Server) Addr

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

func (*Server) Close

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

func (*Server) Listen

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

func (*Server) OnClient

func (server *Server) OnClient(handler ClientHandler)

func (*Server) OnClose

func (server *Server) OnClose(handler CloseHandler)

func (*Server) OnError

func (server *Server) OnError(handler ErrorHandler)

type ServerClient

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

func (*ServerClient) ID

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

type ServerConfig

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

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