Documentation
¶
Overview ¶
Package udp provides a high-performance, stateless UDP server implementation designed for robustness and low-latency datagram processing.
1. ARCHITECTURE ¶
This package implements a production-grade UDP server that minimizes the overhead of connection management. It is designed to scale horizontally by leveraging asynchronous datagram processing.
┌─────────────────────────────────────────────────────────────┐ │ UDP SERVER ARCHITECTURE │ ├─────────────────────────────────────────────────────────────┤ │ │ │ [ NETWORK INTERFACE ] [ KERNEL SPACE ] [ USER SPACE ] │ │ │ │ │ UDP Datagram 1 --------> [ Receive Buffer ] <--- [ Read() ] │ UDP Datagram 2 --------> [ Receive Buffer ] <--- [ Read() ] │ │ │ │ │ v v v │ [ EVENT DISPATCH ] [ CONTEXT WRAPPER ] [ USER HANDLER ] │ │ │ │ │ ┌──────────────┐ ┌────────────────┐ ┌───────────────┐ │ │ Done() │ <─────── │ context.Context│ <──│ HandlerFunc │ │ │ (Cancel) │ └────────────────┘ │ (Goroutine) │ │ └──────────────┘ └───────┬───────┘ │ ^ │ │ │ ┌─────────────────────────────────────┘ │ ┌──────────────┐ │ ┌───────────────────┐ │ │ Gone Channel │ ─┴─────> │ Shutdown Signal │ │ │ (gnc Broadcast) │ (Instant Exit) │ │ └──────────────┘ └───────────────────┘ └─────────────────────────────────────────────────────────────┘
2. KEY FEATURES & OPTIMIZATIONS ¶
Stateless Operation: Unlike TCP, the UDP server maintains no session state by default. This allows the server to handle millions of datagrams with near-zero memory footprint for per-client management.
Event-Driven Shutdown (Gone Channel): Traditional UDP servers often rely on periodic polling to check for shutdown. This package uses the "gnc" broadcast channel. When the server is closed, the 'gnc' channel is closed instantly, notifying the main listener loop to exit without any wait.
Atomic State Control: All lifecycle flags (IsRunning, IsGone) are managed via atomic operations (sync/atomic), ensuring lock-free thread safety across monitoring goroutines.
Hook-Based Tuning: The UpdateConn callback provides a hook to call SetReadBuffer and SetWriteBuffer directly on the underlying net.UDPConn, which is critical for preventing packet drops under high-bandwidth loads.
3. DATA FLOW ¶
The following diagram illustrates the flow of a datagram through the server:
[PEER] [KERNEL BUFFER] [SERVER LOOP] [HANDLER] │ │ │ │ │──(UDP Datagram)────>│ │ │ │ │ │ │ │ │<────(Blocking Read)─────│ │ │ │ │ │ │ │────────(Payload)───────>│ │ │ │ │ │ │ │ │────────(Data)──────>│ │ │ │ │ │ │ │<──────(Processing)──│ │ │ │ │ │<───(UDP Response)───┼─────────────────────────┼────────(WriteTo)────│ │ │ │ │
4. UDP HANDLING SEMANTICS & CAVEATS (RFC 768) RFC 768)" aria-label="Go to 4. UDP HANDLING SEMANTICS & CAVEATS (RFC 768)">¶
UDP is inherently connectionless, which has specific implications for this server:
Shared Socket: There is only one listener socket for all incoming data. This means only one HandlerFunc is spawned per Listen() call. The handler is responsible for managing its own concurrency if needed.
Reliability (RFC 768): This package does NOT implement retries, ACKs, or message ordering. If your application requires these, you must implement them within your HandlerFunc or use a protocol like TCP.
Max Payload: Datagrams exceeding the MTU (typically 1500 bytes) may be fragmented by the network stack. It's recommended to keep datagram sizes under 1472 bytes for IPv4 or 1280 bytes for IPv6 for maximum reliability.
5. BEST PRACTICES & PERFORMANCE TUNING ¶
High-Throughput Tuning: Always increase kernel buffers for high-load UDP servers to prevent "ICMP Destination Unreachable" or packet drops:
updateFn := func(conn net.Conn) { if udp, ok := conn.(*net.UDPConn); ok { _ = udp.SetReadBuffer(2 * 1024 * 1024) // 2MB Read Buffer _ = udp.SetWriteBuffer(2 * 1024 * 1024) // 2MB Write Buffer } }
Buffer Management: To minimize Garbage Collector (GC) pressure, use a sync.Pool for the buffers used within the handler:
var bufPool = sync.Pool{ New: func() any { return make([]byte, 65535) }, } // Inside handler... buf := bufPool.Get().([]byte) defer bufPool.Put(buf) n, remoteAddr, _ := ctx.Read(buf)
6. USE CASES ¶
- Case 1: High-Performance Metrics Gathering (StatsD-like systems).
- Case 2: Real-time Streaming (Audio/Video datagrams).
- Case 3: Network Probing & Monitoring Tools.
- Case 4: DNS-like request/response protocols.
7. CONCRETE IMPLEMENTATION EXAMPLE ¶
package main
import (
"context"
"log"
"github.com/nabbar/golib/socket"
"github.com/nabbar/golib/socket/config"
"github.com/nabbar/golib/socket/server/udp"
)
func main() {
// 1. Define the handler (spawns once)
handler := func(ctx socket.Context) {
buf := make([]byte, 65535)
for {
n, err := ctx.Read(buf)
if err != nil {
return
}
log.Printf("Received %d bytes: %s", n, string(buf[:n]))
}
}
// 2. Setup Config
cfg := config.Server{
Network: "udp",
Address: ":1234",
}
// 3. Instantiate and Start
srv, _ := udp.New(nil, handler, cfg)
srv.Listen(context.Background())
}
Package udp provides a high-performance, stateless UDP server implementation.
Example (BasicServer) ¶
Basic Echo Server Example ¶
This example demonstrates the simplest possible setup for a UDP server. The handler reads exactly one datagram and prints it to the console.
Logic Flow ¶
- Configure the server on a dynamic port (":0").
- Define a handler function.
- Start the server using a cancellable context.
package main
import (
"context"
"fmt"
"io"
"time"
libptc "github.com/nabbar/golib/network/protocol"
libsck "github.com/nabbar/golib/socket"
sckcfg "github.com/nabbar/golib/socket/config"
"github.com/nabbar/golib/socket/server/udp"
)
func main() {
// Create server configuration
cfg := sckcfg.Server{
Network: libptc.NetworkUDP,
Address: ":0", // Let OS choose available port
}
// Define simple handler
handler := func(ctx libsck.Context) {
defer ctx.Close()
buf := make([]byte, 1024)
n, err := ctx.Read(buf)
if err != nil && err != io.EOF {
return
}
fmt.Printf("Received: %s\n", buf[:n])
}
// Create server
srv, err := udp.New(nil, handler, cfg)
if err != nil {
fmt.Printf("Error: %v\n", err)
return
}
// Start server (would block in real usage)
ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
defer cancel()
go func() {
_ = srv.Listen(ctx)
}()
fmt.Println("UDP server created successfully")
}
Output: UDP server created successfully
Example (ConfigFromStruct) ¶
Example_configFromStruct demonstrates using config.Server struct.
This example shows the recommended way to create a server using the config package for type-safe configuration.
package main
import (
"fmt"
libptc "github.com/nabbar/golib/network/protocol"
libsck "github.com/nabbar/golib/socket"
sckcfg "github.com/nabbar/golib/socket/config"
"github.com/nabbar/golib/socket/server/udp"
)
func main() {
// Create configuration
cfg := sckcfg.Server{
Network: libptc.NetworkUDP,
Address: ":9008",
}
// Validate configuration
if err := cfg.Validate(); err != nil {
fmt.Printf("Invalid configuration: %v\n", err)
return
}
// Define handler
handler := func(ctx libsck.Context) {
buf := make([]byte, 1024)
_, _ = ctx.Read(buf)
}
// Create server
srv, err := udp.New(nil, handler, cfg)
if err != nil {
fmt.Printf("Error: %v\n", err)
return
}
fmt.Printf("Server configured for %s on %s\n",
cfg.Network.String(), cfg.Address)
_ = srv
}
Output: Server configured for udp on :9008
Example (EchoServer) ¶
Example_echoServer demonstrates a complete UDP echo server.
This example shows a server that echoes back received datagrams to the sender using ReadFrom/WriteTo pattern.
package main
import (
"fmt"
"io"
"log"
libptc "github.com/nabbar/golib/network/protocol"
libsck "github.com/nabbar/golib/socket"
sckcfg "github.com/nabbar/golib/socket/config"
"github.com/nabbar/golib/socket/server/udp"
)
func main() {
cfg := sckcfg.Server{
Network: libptc.NetworkUDP,
Address: ":9001",
}
// Echo handler using ReadFrom/WriteTo
handler := func(ctx libsck.Context) {
// Type assertion to get UDP connection
type udpContext interface {
libsck.Context
// Would need access to underlying connection for WriteTo
}
// For demonstration, just read
buf := make([]byte, 65507) // Max UDP datagram
for {
n, err := ctx.Read(buf)
if err != nil {
if err == io.EOF || err == io.ErrClosedPipe {
return
}
log.Printf("Read error: %v", err)
return
}
// In real implementation, would echo back using WriteTo
_ = buf[:n]
}
}
srv, err := udp.New(nil, handler, cfg)
if err != nil {
fmt.Printf("Error: %v\n", err)
return
}
fmt.Printf("Echo server configured on %s\n", cfg.Address)
_ = srv
}
Output: Echo server configured on :9001
Example (ErrorHandling) ¶
Example_errorHandling demonstrates comprehensive error handling.
This example shows how to handle various error scenarios including invalid configuration, connection errors, and I/O errors.
package main
import (
"fmt"
libptc "github.com/nabbar/golib/network/protocol"
libsck "github.com/nabbar/golib/socket"
sckcfg "github.com/nabbar/golib/socket/config"
"github.com/nabbar/golib/socket/server/udp"
)
func main() {
// Attempt to create server with invalid configuration
invalidCfg := sckcfg.Server{
Network: libptc.NetworkUDP,
Address: "", // Invalid: empty address
}
handler := func(ctx libsck.Context) {}
_, err := udp.New(nil, handler, invalidCfg)
if err != nil {
fmt.Printf("Configuration error detected: %v\n", err)
return
}
fmt.Println("This should not print")
}
Output: Configuration error detected: invalid listen address
Example (GracefulShutdown) ¶
Example_gracefulShutdown demonstrates proper server shutdown.
This example shows how to handle graceful shutdown using context cancellation and the Shutdown() method.
package main
import (
"context"
"fmt"
"log"
"time"
libptc "github.com/nabbar/golib/network/protocol"
libsck "github.com/nabbar/golib/socket"
sckcfg "github.com/nabbar/golib/socket/config"
"github.com/nabbar/golib/socket/server/udp"
)
func main() {
cfg := sckcfg.Server{
Network: libptc.NetworkUDP,
Address: ":9004",
}
handler := func(ctx libsck.Context) {
buf := make([]byte, 1024)
for {
_, err := ctx.Read(buf)
if err != nil {
return // Exit on error
}
}
}
srv, err := udp.New(nil, handler, cfg)
if err != nil {
fmt.Printf("Error: %v\n", err)
return
}
// Start server with cancellable context
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
// Start server in goroutine
go func() {
if err := srv.Listen(ctx); err != nil {
if err != context.Canceled {
log.Printf("Listen error: %v", err)
}
}
}()
// Wait for server to start
time.Sleep(10 * time.Millisecond)
// Trigger shutdown
cancel()
// Wait with timeout
shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 5*time.Second)
defer shutdownCancel()
if err := srv.Shutdown(shutdownCtx); err != nil {
fmt.Printf("Shutdown error: %v\n", err)
return
}
fmt.Println("Server shut down gracefully")
}
Output: Server shut down gracefully
Example (IntegrationTest) ¶
Example_integrationTest demonstrates testing UDP servers.
This example shows how to write integration tests for UDP servers by creating a server and client for testing.
package main
import (
"context"
"fmt"
"time"
libptc "github.com/nabbar/golib/network/protocol"
libsck "github.com/nabbar/golib/socket"
sckcfg "github.com/nabbar/golib/socket/config"
"github.com/nabbar/golib/socket/server/udp"
)
func main() {
cfg := sckcfg.Server{
Network: libptc.NetworkUDP,
Address: "127.0.0.1:0", // Bind to loopback, random port
}
receivedData := make(chan []byte, 1)
handler := func(ctx libsck.Context) {
buf := make([]byte, 1024)
n, err := ctx.Read(buf)
if err != nil {
return
}
receivedData <- buf[:n]
}
srv, err := udp.New(nil, handler, cfg)
if err != nil {
fmt.Printf("Error: %v\n", err)
return
}
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
go func() {
_ = srv.Listen(ctx)
}()
time.Sleep(10 * time.Millisecond)
fmt.Println("Integration test server ready")
// In real test, would send test datagram and verify
Output:
Example (MulticastServer) ¶
Example_multicastServer demonstrates handling multicast UDP traffic.
This example shows configuration for a server that can receive multicast datagrams (concept demonstration).
package main
import (
"fmt"
"net"
libptc "github.com/nabbar/golib/network/protocol"
libsck "github.com/nabbar/golib/socket"
sckcfg "github.com/nabbar/golib/socket/config"
"github.com/nabbar/golib/socket/server/udp"
)
func main() {
cfg := sckcfg.Server{
Network: libptc.NetworkUDP,
Address: ":9005",
}
// Configure multicast options
updateConn := func(conn net.Conn) {
if udpConn, ok := conn.(*net.UDPConn); ok {
// In real implementation, would join multicast group
// using udpConn.SetMulticastInterface() and related methods
_ = udpConn
fmt.Println("Multicast options configured")
}
}
handler := func(ctx libsck.Context) {
buf := make([]byte, 1024)
_, _ = ctx.Read(buf)
}
srv, err := udp.New(updateConn, handler, cfg)
if err != nil {
fmt.Printf("Error: %v\n", err)
return
}
fmt.Println("Multicast server configured")
_ = srv
}
Output: Multicast server configured
Example (ServerWithCallbacks) ¶
Server with Monitoring Callbacks ¶
This example shows how to register observability hooks for monitoring. These callbacks are executed asynchronously and do not block the datagram flow.
package main
import (
"fmt"
"net"
libptc "github.com/nabbar/golib/network/protocol"
libsck "github.com/nabbar/golib/socket"
sckcfg "github.com/nabbar/golib/socket/config"
"github.com/nabbar/golib/socket/server/udp"
)
func main() {
cfg := sckcfg.Server{
Network: libptc.NetworkUDP,
Address: ":9002",
}
handler := func(ctx libsck.Context) {
// Handler implementation
buf := make([]byte, 1024)
_, _ = ctx.Read(buf)
}
srv, err := udp.New(nil, handler, cfg)
if err != nil {
fmt.Printf("Error: %v\n", err)
return
}
// Register error callback
srv.RegisterFuncError(func(errs ...error) {
for _, e := range errs {
if e != nil {
fmt.Printf("Server error: %v\n", e)
}
}
})
// Register connection info callback
srv.RegisterFuncInfo(func(local, remote net.Addr, state libsck.ConnState) {
fmt.Printf("Connection event: %s (local: %v, remote: %v)\n",
state.String(), local, remote)
})
// Register server info callback
srv.RegisterFuncInfoServer(func(msg string) {
fmt.Printf("Server info: %s\n", msg)
})
fmt.Println("Callbacks registered successfully")
}
Output: Callbacks registered successfully
Example (SocketConfiguration) ¶
Example_socketConfiguration demonstrates custom socket options.
This example shows how to use UpdateConn callback to configure socket buffer sizes and other options.
package main
import (
"fmt"
"net"
libptc "github.com/nabbar/golib/network/protocol"
libsck "github.com/nabbar/golib/socket"
sckcfg "github.com/nabbar/golib/socket/config"
"github.com/nabbar/golib/socket/server/udp"
)
func main() {
cfg := sckcfg.Server{
Network: libptc.NetworkUDP,
Address: ":9003",
}
// Configure socket with custom options
updateConn := func(conn net.Conn) {
if udpConn, ok := conn.(*net.UDPConn); ok {
// Set large read buffer for high throughput
_ = udpConn.SetReadBuffer(1024 * 1024) // 1MB
// Set large write buffer
_ = udpConn.SetWriteBuffer(1024 * 1024) // 1MB
fmt.Println("Socket buffers configured")
}
}
handler := func(ctx libsck.Context) {
buf := make([]byte, 1024)
_, _ = ctx.Read(buf)
}
srv, err := udp.New(updateConn, handler, cfg)
if err != nil {
fmt.Printf("Error: %v\n", err)
return
}
fmt.Println("Server created with custom socket configuration")
_ = srv
}
Output: Server created with custom socket configuration
Example (StateMonitoring) ¶
Example_stateMonitoring demonstrates server state monitoring.
This example shows how to monitor server state using IsRunning(), IsGone(), and OpenConnections() methods.
package main
import (
"context"
"fmt"
"time"
libptc "github.com/nabbar/golib/network/protocol"
libsck "github.com/nabbar/golib/socket"
sckcfg "github.com/nabbar/golib/socket/config"
"github.com/nabbar/golib/socket/server/udp"
)
func main() {
cfg := sckcfg.Server{
Network: libptc.NetworkUDP,
Address: ":9007",
}
handler := func(ctx libsck.Context) {
buf := make([]byte, 1024)
_, _ = ctx.Read(buf)
}
srv, err := udp.New(nil, handler, cfg)
if err != nil {
fmt.Printf("Error: %v\n", err)
return
}
// Check initial state
fmt.Printf("Running: %v\n", srv.IsRunning())
fmt.Printf("Gone: %v\n", srv.IsGone())
fmt.Printf("Connections: %d\n", srv.OpenConnections())
// Start server
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
go func() {
_ = srv.Listen(ctx)
}()
// Wait for server to start
time.Sleep(10 * time.Millisecond)
fmt.Printf("After start - Running: %v\n", srv.IsRunning())
fmt.Printf("After start - Gone: %v\n", srv.IsGone())
fmt.Printf("After start - Connections: %d\n", srv.OpenConnections())
// Shutdown
cancel()
time.Sleep(100 * time.Millisecond)
fmt.Printf("After shutdown - Gone: %v\n", srv.IsGone())
}
Output: Running: false Gone: true Connections: 0 After start - Running: true After start - Gone: false After start - Connections: 0 After shutdown - Gone: true
Index ¶
Examples ¶
Constants ¶
This section is empty.
Variables ¶
var ( // ErrInvalidAddress is returned when the provided listen address fails validation. // // # Validation Logic // // The address is parsed using net.ResolveUDPAddr. Common valid formats: // - ":port" (all interfaces, IPv4 and IPv6) // - "0.0.0.0:port" (all IPv4 interfaces) // - "127.0.0.1:port" (loopback only) // - "[::1]:port" (IPv6 loopback) // // # Use Case // // Typically returned during RegisterServer() or at the start of Listen(). ErrInvalidAddress = fmt.Errorf("invalid listen address") // ErrInvalidHandler is returned when trying to start the server with a nil handler. // // # Mandatory Requirement // // Since the server only spawns a single handler for all UDP traffic, a non-nil // HandlerFunc is required by the New() constructor. ErrInvalidHandler = fmt.Errorf("invalid handler") // ErrShutdownTimeout is returned when the graceful shutdown period expires. // // # Shutdown Logic // // When Shutdown(ctx) is called, the server: // 1. Closes the broadcast channel (gnc). // 2. Closes the listener socket. // 3. Monitors the IsRunning flag for cleanup completion. // // If the provided context 'ctx' times out before the cleanup is finished, // this error is returned. ErrShutdownTimeout = fmt.Errorf("timeout on stopping socket") // ErrInvalidInstance is returned when operating on a nil *srv pointer. // // # Defensive Coding // // This error prevents panics in scenarios where methods are called on an // uninitialized or nil server instance. ErrInvalidInstance = fmt.Errorf("invalid socket instance") )
Functions ¶
This section is empty.
Types ¶
type ServerUdp ¶ added in v1.19.0
type ServerUdp interface {
libsck.Server
// RegisterServer configures the UDP address the server will bind to.
//
// # Requirements
//
// 1. Must be called BEFORE Listen().
// 2. Address must be in the format "host:port" or ":port".
//
// Parameters:
// - address: String representing the network address.
//
// Returns:
// - error: ErrInvalidAddress if the address is empty or malformed.
RegisterServer(address string) error
}
ServerUdp defines the specialized interface for a UDP datagram server.
Interface Hierarchy ¶
This interface extends the core github.com/nabbar/golib/socket.Server but with several important semantic differences:
- Stateless Nature: Unlike a TCP server, a UDP server doesn't "maintain" connections. OpenConnections() will always return 0 (or 1 if the socket is listening).
- Datagram Management: The server is purely datagram-oriented; no automatic handshake or flow control is provided by this layer.
- Synchronicity: Only one main handler goroutine is spawned per Listen().
Inherited Methods (from socket.Server) ¶
- Listen(context.Context) error: Starts the UDP listener. Blocks until the context is cancelled or Shutdown is called.
- Shutdown(context.Context) error: Gracefully closes the listener and cleans up internal resources within the given context deadline.
- Close() error: Immediate, non-blocking shutdown.
- IsRunning() bool: Thread-safe check of whether the socket is listening.
- IsGone() bool: Thread-safe check of whether the server is in the "Stopped" state.
- RegisterFuncError(FuncError): Registers a callback for server-level errors.
- RegisterFuncInfo(FuncInfo): Registers a callback for connection events.
- RegisterFuncInfoSrv(FuncInfoSrv): Registers a callback for lifecycle messages.
func New ¶
func New(upd libsck.UpdateConn, hdl libsck.HandlerFunc, cfg sckcfg.Server) (ServerUdp, error)
New creates and initializes a new UDP server instance.
Design Principle ¶
This constructor uses a combination of mandatory and optional parameters to ensure the server is always in a valid, startable state.
Parameters:
- upd: Optional UpdateConn callback. Use this to configure the raw socket (e.g., SetReadBuffer, SetWriteBuffer, JoinMulticastGroup).
- hdl: Mandatory HandlerFunc. This is the main entry point for datagram processing. It is executed in a dedicated goroutine when Listen() starts.
- cfg: Server configuration struct, providing the initial address and network protocol.
Implementation Notes ¶
During initialization:
- Atomic values are allocated for all status flags and callbacks.
- A default (no-op) broadcast channel (gnc) is created to prevent nil dereferences.
- RegisterServer is called with the address provided in cfg.
- The server is initially marked as 'gon=true' (not running).
Returns:
- ServerUdp: The initialized server instance.
- error: If validation fails (e.g., nil handler or invalid address).