udp

package
v1.20.0 Latest Latest
Warning

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

Go to latest
Published: Mar 10, 2026 License: MIT Imports: 10 Imported by: 0

README

UDP Client

Go Version License Coverage

Thread-safe UDP client for connectionless datagram communication with context integration, callback support, and panic recovery.


Table of Contents


Overview

The UDP Client package provides a production-ready implementation for UDP datagram communication in Go. Unlike TCP, UDP is connectionless and unreliable, making it suitable for applications where speed and low latency are more important than guaranteed delivery. This package wraps the complexity of UDP socket management while providing modern Go idioms like context support and callback mechanisms.

Design Philosophy
  1. Connectionless by Nature: Embraces UDP's stateless design while providing convenience methods
  2. Thread-Safe Operations: All methods safe for concurrent use via atomic state management
  3. Context Integration: First-class support for cancellation, timeouts, and deadlines
  4. Non-Blocking Callbacks: Asynchronous event notifications without blocking I/O
  5. Production-Ready: Comprehensive testing with 77% coverage, panic recovery, and race detection
Key Features
  • Thread-Safe: All operations safe for concurrent access without external synchronization
  • Context-Aware: Full context.Context integration for cancellation and timeouts
  • Event Callbacks: Asynchronous error and state change notifications
  • One-Shot Operations: Convenient Once() method for fire-and-forget patterns
  • Panic Recovery: Automatic recovery from callback panics with detailed logging
  • Zero External Dependencies: Only Go standard library and internal golib packages
  • TLS No-Op: SetTLS() is a documented no-op (UDP doesn't support TLS)
  • IPv4/IPv6 Support: Works with both IP protocols

Architecture

Component Diagram
┌─────────────────────────────────────────────────────────┐
│                    ClientUDP Interface                  │
├─────────────────────────────────────────────────────────┤
│  Connect() │ Write() │ Read() │ Close() │ Once()        │
│  RegisterFuncError() │ RegisterFuncInfo()               │
└──────────────┬──────────────────────────────────────────┘
               │
               ▼
┌─────────────────────────────────────────────────────────┐
│          Internal Implementation (cli struct)           │
├─────────────────────────────────────────────────────────┤
│  • atomic.Map for thread-safe state storage             │
│  • net.UDPConn for socket operations                    │
│  • Async callback execution in goroutines               │
│  • Panic recovery with runner.RecoveryCaller            │
└──────────────┬──────────────────────────────────────────┘
               │
               ▼
┌─────────────────────────────────────────────────────────┐
│               Go Standard Library                       │
│     net.DialUDP │ net.UDPConn │ context.Context         │
└─────────────────────────────────────────────────────────┘
Data Flow

Write Operation:

Client.Write(data) → Check Connection → UDPConn.Write() → Trigger Callbacks

Read Operation:

Client.Read(buffer) → Check Connection → UDPConn.Read() → Trigger Callbacks

Once Operation (One-shot request/response):

Client.Once(ctx, req, rsp) → Connect → Write → [Read if rsp] → Close → Callbacks
State Management

The client uses atomic.Map for lock-free state storage:

Key Type Purpose
keyConn *net.UDPConn Active UDP socket
keyAddr *net.UDPAddr Remote endpoint address
keyFctErr FuncError Error callback function
keyFctInfo FuncInfo Info callback function

All state transitions are atomic, ensuring thread-safe concurrent access without mutexes.


Performance

Benchmarks

Performance results from benchmark tests on a standard development machine:

Operation Median Mean Notes
Client Creation <100µs ~50µs Memory allocation only
Connect <300µs ~150µs UDP socket association
Write (Small) <5ms ~2ms 13-byte datagram
Write (Large) <10ms ~5ms 1400-byte datagram
Throughput 100 msgs/<500ms - Sequential writes
State Check <100µs ~10µs Atomic IsConnected()
Close <5ms ~2ms Socket cleanup
Full Cycle <20ms ~10ms Create-connect-write-close
Memory Usage
  • Base Client: ~200 bytes per instance
  • Per Connection: ~4KB (kernel UDP buffer)
  • Callback Storage: Negligible (function pointers only)
  • No Allocations: Write operations are allocation-free
Scalability
  • Concurrent Clients: Limited only by system file descriptors
  • Message Rate: Suitable for high-frequency communication (1000+ msg/sec)
  • Thread Safety: Zero contention on state checks (atomic operations)
  • CPU Usage: <1% for typical workloads

Use Cases

1. Service Discovery

Pattern: Broadcast/multicast queries for network services
Advantages: Low latency, minimal overhead, no connection setup
Example: Finding available servers on local network

2. Metrics Collection

Pattern: Send telemetry to monitoring systems (StatsD, InfluxDB)
Advantages: Fire-and-forget, non-blocking, tolerates packet loss
Example: Application performance metrics

3. Real-Time Gaming

Pattern: Player state synchronization and game events
Advantages: Ultra-low latency, packet loss acceptable
Example: Multiplayer position updates

4. IoT Communication

Pattern: Device-to-server communication with minimal overhead
Advantages: Simple protocol, low power consumption
Example: Sensor data transmission

5. DNS Queries

Pattern: Custom DNS resolution or proxying
Advantages: Standard protocol support, one-shot request/response
Example: Local DNS cache or filtering proxy

6. Syslog Client

Pattern: Remote logging without delivery guarantees
Advantages: Non-blocking, asynchronous, simple
Example: Application log aggregation


Quick Start

Installation
go get github.com/nabbar/golib/socket/client/udp
Basic Example
package main

import (
    "context"
    "log"
    
    "github.com/nabbar/golib/socket/client/udp"
)

func main() {
    // Create client
    client, err := udp.New("localhost:8080")
    if err != nil {
        log.Fatal(err)
    }
    defer client.Close()
    
    // Connect (associates socket with remote address)
    ctx := context.Background()
    if err := client.Connect(ctx); err != nil {
        log.Fatal(err)
    }
    
    // Send data
    if _, err := client.Write([]byte("Hello, UDP!")); err != nil {
        log.Fatal(err)
    }
}
With Callbacks
client, _ := udp.New("localhost:8080")
defer client.Close()

// Register error callback
client.RegisterFuncError(func(errs ...error) {
    for _, err := range errs {
        log.Printf("Error: %v", err)
    }
})

// Register info callback
client.RegisterFuncInfo(func(local, remote net.Addr, state libsck.ConnState) {
    log.Printf("State: %s (%s -> %s)", state, local, remote)
})

client.Connect(context.Background())
client.Write([]byte("message"))
One-Shot Request
client, _ := udp.New("localhost:8080")

request := bytes.NewBufferString("query")
err := client.Once(context.Background(), request, func(reader io.Reader) {
    response, _ := io.ReadAll(reader)
    fmt.Printf("Response: %s\n", response)
})
// Socket automatically closed after Once()
Context Timeout
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()

client, _ := udp.New("localhost:8080")
defer client.Close()

if err := client.Connect(ctx); err != nil {
    log.Printf("Connect timeout: %v", err)
}

Best Practices

  1. Always Close: Use defer client.Close() to prevent socket leaks
  2. Set Timeouts: Use context deadlines for all operations
  3. Handle Errors: Check Write() errors (UDP can fail silently)
  4. Use Callbacks: Prefer async error handling over polling
  5. Datagram Size: Keep ≤1472 bytes to avoid IP fragmentation
  6. No Guarantees: Never rely on delivery or ordering
  7. Thread Safety: All methods are concurrent-safe
  8. Callback Sync: Use mutexes when accessing shared state in callbacks

Testing Best Practices:

  • Run tests with -race flag
  • Test with realistic packet loss rates
  • Use Once() for simple request/response patterns
  • Monitor callback execution with timeouts

See TESTING.md for comprehensive testing documentation.


API Reference

ClientUDP Interface
type ClientUDP interface {
    // Connection lifecycle
    Connect(ctx context.Context) error
    IsConnected() bool
    Close() error
    
    // Data transfer
    Write(p []byte) (n int, err error)
    Read(p []byte) (n int, err error)
    Once(ctx context.Context, req io.Reader, rsp func(io.Reader)) error
    
    // Callbacks
    RegisterFuncError(f FuncError)
    RegisterFuncInfo(f FuncInfo)
    
    // TLS (no-op for UDP)
    SetTLS(enable bool, config *tls.Config, host string) error
}
Constructor

func New(address string) (ClientUDP, error)

Creates a new UDP client for the specified remote address.

  • address: Format "host:port" or "ip:port" (IPv4/IPv6)
  • Returns: Client instance or ErrAddress if invalid
client, err := udp.New("192.168.1.100:9000")
client, err := udp.New("[::1]:8080") // IPv6
Callbacks

FuncError Callback:

type FuncError func(errs ...error)

Invoked asynchronously when errors occur. Panics are automatically recovered and logged.

FuncInfo Callback:

type FuncInfo func(local, remote net.Addr, state ConnState)

Invoked asynchronously on state changes:

  • ConnectionDial: Socket created
  • ConnectionNew: Socket associated with remote address
  • ConnectionRead: Data received
  • ConnectionWrite: Data sent
  • ConnectionClose: Socket closed
Error Handling

Error Constants:

  • ErrInstance: Client instance is nil
  • ErrConnection: No active connection
  • ErrAddress: Invalid address format

All errors implement error interface and are comparable with errors.Is().

Monitoring

Monitor client health via callbacks and state checks:

client.RegisterFuncError(func(errs ...error) {
    metrics.IncrementErrorCount()
})

client.RegisterFuncInfo(func(_, _ net.Addr, state libsck.ConnState) {
    metrics.RecordStateChange(state.String())
})

// Periodic health check
if !client.IsConnected() {
    client.Connect(context.Background())
}

Contributing

This package is part of the golib project.

How to Contribute:

  1. Fork the repository
  2. Create a feature branch
  3. Write tests for new features
  4. Ensure all tests pass with -race flag
  5. Submit a pull request

Contribution Guidelines:

  • Follow Go best practices and idioms
  • Maintain or improve code coverage (currently 77%)
  • Add tests for all new features
  • Update documentation as needed
  • Run go fmt and go vet before committing

Improvements & Security

Potential Improvements:

  • Higher test coverage for Once() method (currently 61.9%)
  • Additional benchmarks for concurrent scenarios
  • IPv6-specific optimization paths
  • Multicast support

Security Considerations:

  • UDP doesn't provide encryption (use application-layer security)
  • No built-in authentication (implement at protocol level)
  • Susceptible to amplification attacks (validate packet sources)
  • Rate limiting recommended for public-facing services

Reporting Security Issues:
See TESTING.md - Reporting Bugs & Vulnerabilities


Resources

Documentation:

Related Packages:

Learning Resources:


AI Transparency

In compliance with EU AI Act Article 50.4: AI assistance was used for testing, documentation, and code generation under human supervision. All core functionality is human-designed, reviewed, and validated. Test coverage and race detection ensure production quality.


License

MIT License - Copyright (c) 2025 Nicolas JUHEL

See LICENSE file for full details.

Maintained By: Nicolas JUHEL
Package: github.com/nabbar/golib/socket/client/udp

Documentation

Overview

Package udp provides a UDP client implementation with callback mechanisms for datagram communication.

Overview

This package implements the github.com/nabbar/golib/socket.Client interface for the UDP protocol, providing a connectionless datagram client with features including:

  • Connectionless UDP datagram sending and receiving
  • Thread-safe state management using atomic operations
  • Callback hooks for errors and informational messages
  • Context-aware operations
  • One-shot request/response operation support
  • No TLS support (UDP doesn't support TLS natively; use DTLS if encryption is required)

Unlike TCP clients which maintain persistent connections with handshakes, UDP clients operate in a connectionless mode where each datagram is independent. This makes UDP ideal for scenarios requiring low latency, multicast/broadcast communication, or where connection overhead is undesirable.

Design Philosophy

The package follows these core design principles:

1. **Connectionless by Nature**: No persistent connection state, minimal overhead 2. **Thread Safety**: All operations use atomic primitives and are safe for concurrent use 3. **Callback-Based**: Flexible event notification through registered callbacks 4. **Context Integration**: Full support for context-based cancellation and deadlines 5. **Standard Compliance**: Implements standard socket.Client interface 6. **Zero Dependencies**: Only standard library and golib packages

Architecture

## Component Structure

┌─────────────────────────────────────────────────────────────┐
│                   ClientUDP Interface                       │
│            (extends socket.Client interface)                │
└────────────────────────┬────────────────────────────────────┘
                         │
                         │ implements
                         │
┌────────────────────────▼────────────────────────────────────┐
│                  cli (internal struct)                      │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ Atomic Map (libatm.Map[uint8]):                         │ │
│ │  - keyNetAddr:  Remote address (string)                 │ │
│ │  - keyFctErr:   Error callback (libsck.FuncError)       │ │
│ │  - keyFctInfo:  Info callback (libsck.FuncInfo)         │ │
│ │  - keyNetConn:  Active UDP socket (net.Conn)            │ │
│ └─────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
                         │
                         │ uses
                         │
┌────────────────────────▼────────────────────────────────────┐
│               *net.UDPConn                                  │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ Standard library UDP socket:                            │ │
│ │  - Read([]byte) (n int, err error)                      │ │
│ │  - Write([]byte) (n int, err error)                     │ │
│ │  - Close() error                                        │ │
│ │  - LocalAddr() net.Addr                                 │ │
│ │  - RemoteAddr() net.Addr                                │ │
│ └─────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘

## Data Flow

The client follows this execution flow:

  1. New(address) creates client instance with remote address ↓
  2. RegisterFuncError/RegisterFuncInfo (optional) sets callbacks ↓
  3. Connect(ctx) called: a. Creates UDP socket using net.Dialer b. Associates socket with remote address c. Stores socket in atomic map d. Triggers ConnectionDial and ConnectionNew callbacks ↓
  4. Read/Write operations: - Write() sends complete datagram to remote address - Read() receives complete datagram from socket - Triggers ConnectionRead/ConnectionWrite callbacks ↓
  5. Close() or Once() completion: - Triggers ConnectionClose callback - Closes UDP socket - Removes socket from state

## State Machine

┌─────────┐     New()      ┌─────────────┐
│  Start  │───────────────▶│  Created    │
└─────────┘                └──────┬──────┘
                                  │ (optional)
                                  │ RegisterFunc*
                           ┌──────▼──────┐
                           │ Configured  │
                           └──────┬──────┘
                                  │ Connect()
                                  │
                           ┌──────▼──────┐
                           │ Associated  │◀────┐
                           │(IsConnected)│     │ Read/Write
                           └──────┬──────┘     │
                                  │            │
                                  ├────────────┘
                                  │
                                  │ Close()
                           ┌──────▼──────┐
                           │   Closed    │
                           └─────────────┘

Key Features

## Connectionless Operation

UDP is fundamentally connectionless. Unlike TCP:

  • No handshake or connection establishment
  • No persistent connection state tracking
  • Each datagram is independent
  • No guarantee of delivery, ordering, or duplicate prevention
  • Lower latency and overhead

The client reflects this by:

  • Connect() only associates the socket with a remote address
  • Write() sends one complete datagram per call
  • Read() receives one complete datagram per call
  • No connection lifecycle beyond socket creation/destruction

## Thread Safety

All mutable state uses atomic operations:

  • Atomic map (libatm.Map[uint8]) for all client state
  • Safe concurrent access to all methods
  • No locks required for external synchronization

This ensures thread-safe access, allowing:

  • Concurrent calls to IsConnected() from multiple goroutines
  • Safe callback registration while operations are in progress
  • Safe Close() from any goroutine

Important: While the client methods are thread-safe, the underlying UDP socket is NOT safe for concurrent Read() or concurrent Write() calls. Avoid calling Read() from multiple goroutines or Write() from multiple goroutines simultaneously.

## Context Integration

The client fully supports Go's context.Context:

  • Connect() accepts context for timeout and cancellation
  • Once() accepts context for the entire operation
  • Context cancellation triggers immediate operation abort

## Callback System

Two types of callbacks for event notification:

1. **FuncError**: Error notifications

  • Called on any error during operations (asynchronously)
  • Receives variadic errors
  • Should not block (executed in separate goroutine)

2. **FuncInfo**: Datagram operation events

  • Called for Connect/Read/Write/Close events (asynchronously)
  • Receives local addr, remote addr, and state
  • Useful for monitoring, logging, and debugging
  • Should not block (executed in separate goroutine)

Performance Characteristics

## Memory Usage

Base overhead:     ~200 bytes (struct + atomic map)
Per UDP socket:    OS-dependent (~4KB typical)
Total idle:        ~4KB

Since UDP is connectionless, memory usage is constant regardless of datagram count (assuming no buffering in handler).

## Throughput

UDP throughput is primarily limited by:

  • Network bandwidth
  • Maximum datagram size (typically 1472 bytes to avoid fragmentation)
  • OS socket buffer size

The package itself adds minimal overhead (<1% typical).

## Latency

Operation latencies (typical):

  • New(): ~1µs (struct allocation)
  • Connect(): ~100µs to 1ms (socket creation)
  • Write(): ~10-100µs (datagram send)
  • Read(): ~10-100µs (datagram receive, blocking until data arrives)
  • Close(): ~100µs (socket cleanup)

Limitations and Trade-offs

## Protocol Limitations (UDP inherent)

1. **No Reliability**: Datagrams may be lost without notification

  • Workaround: Implement application-level acknowledgments

2. **No Ordering**: Datagrams may arrive out of order

  • Workaround: Add sequence numbers at application level

3. **No Duplicate Prevention**: Same datagram may arrive multiple times

  • Workaround: Implement deduplication using unique identifiers

4. **Limited Datagram Size**: Typically 65,507 bytes max (IPv4)

  • Practical limit: 1472 bytes to avoid IP fragmentation (Ethernet MTU)
  • Workaround: Fragment large messages at application level

5. **No Encryption**: UDP has no native encryption (unlike TLS for TCP)

  • Workaround: Use DTLS (not implemented) or application-level encryption
  • Note: SetTLS() is a no-op for UDP clients

6. **No Flow Control**: No backpressure mechanism

  • Workaround: Implement application-level rate limiting

7. **No Congestion Control**: Can overwhelm network

  • Workaround: Implement application-level bandwidth management

## Implementation Limitations

1. **No TLS Support**: SetTLS() always returns nil (no-op)

  • UDP does not support TLS
  • Use DTLS externally if encryption needed

2. **Single Remote Address**: Each client instance targets one remote address

  • Cannot send to multiple destinations
  • Create multiple client instances for multiple targets

3. **No Concurrent Read/Write**: Underlying socket not safe for concurrent I/O

  • Don't call Read() from multiple goroutines
  • Don't call Write() from multiple goroutines
  • Different operations (Read + Write) are safe concurrently

4. **Fire-and-Forget Nature**: Write() success doesn't mean data was received

  • Write() only confirms datagram was queued for sending
  • No confirmation of remote receipt

Use Cases

UDP clients are ideal for:

## Real-time Applications

  • Gaming clients (low latency critical)
  • Voice/video streaming (occasional loss acceptable)
  • Live data feeds (latest data more important than old)
  • Real-time sensor data transmission

## Request-Response Protocols

  • DNS queries (single request, single response)
  • SNMP monitoring (simple queries)
  • DHCP client (configuration requests)
  • Lightweight RPC systems

## Broadcast/Multicast

  • Service discovery clients
  • Network monitoring agents
  • Event distribution subscribers

## High-Frequency Data

  • Time synchronization clients (NTP)
  • Metrics collection (StatsD-like)
  • Log shipping (lossy acceptable)

Best Practices

## Client Creation and Lifecycle

// Good: Create client with proper lifecycle
client, err := udp.New("server.example.com:8080")
if err != nil {
    log.Fatalf("Failed to create client: %v", err)
}
defer client.Close()

ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()

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

## Datagram Size Management

// Good: Keep datagrams small to avoid fragmentation
const maxSafeDatagramSize = 1400 // Well below 1472 byte Ethernet limit

data := []byte("payload data")
if len(data) > maxSafeDatagramSize {
    // Fragment at application level
    log.Warn("Datagram too large, will fragment")
}

n, err := client.Write(data)
if err != nil {
    log.Errorf("Write failed: %v", err)
} else if n != len(data) {
    log.Warn("Partial write")
}

## Error Handling

// Register error callback for centralized error logging
client.RegisterFuncError(func(errs ...error) {
    for _, err := range errs {
        if err != nil {
            log.Printf("UDP client error: %v", err)
        }
    }
})

## One-Shot Operations

// Use Once() for simple request/response patterns
request := bytes.NewBufferString("QUERY")
err := client.Once(ctx, request, func(reader io.Reader) {
    buf := make([]byte, 1500)
    n, err := reader.Read(buf)
    if err != nil {
        log.Printf("Read error: %v", err)
        return
    }
    log.Printf("Response: %s", buf[:n])
})
// Socket automatically closed

## Timeout Management

// Good: Use context timeouts for operations
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()

if err := client.Connect(ctx); err != nil {
    if ctx.Err() == context.DeadlineExceeded {
        log.Warn("Connection timeout")
    }
    return err
}

## Callback Registration

// Register callbacks for monitoring
client.RegisterFuncInfo(func(local, remote net.Addr, state socket.ConnState) {
    log.Printf("UDP state: %v (local: %v, remote: %v)",
        state.String(), local, remote)
})

Comparison with TCP Client

┌─────────────────────┬──────────────────┬──────────────────┐
│     Feature         │   UDP Client     │   TCP Client     │
├─────────────────────┼──────────────────┼──────────────────┤
│ Connection Model    │ Connectionless   │ Connection-based │
│ Reliability         │ None             │ Guaranteed       │
│ Ordering            │ Not guaranteed   │ Guaranteed       │
│ Flow Control        │ None             │ Yes (TCP)        │
│ Congestion Control  │ None             │ Yes (TCP)        │
│ Handshake           │ None             │ 3-way handshake  │
│ Message Boundaries  │ Preserved        │ Stream-based     │
│ TLS Support         │ No (no-op)       │ Yes              │
│ Latency             │ Lower            │ Higher           │
│ Overhead            │ Minimal          │ Higher           │
│ Use Cases           │ Real-time, IoT   │ Reliable transfer│
└─────────────────────┴──────────────────┴──────────────────┘

Error Handling

The package defines these specific errors:

  • ErrAddress: Empty or malformed remote address in New()
  • ErrConnection: Operation attempted without connection (call Connect() first)
  • ErrInstance: Operation on nil client instance (programming error)

All errors are logged via the registered FuncError callback if set.

Thread Safety

**Concurrent-safe operations:**

  • IsConnected(): Always safe
  • RegisterFuncError/Info(): Safe at any time
  • Connect(): Safe, replaces existing socket if called multiple times
  • Close(): Safe from any goroutine

**Not concurrent-safe (underlying socket limitation):**

  • Multiple concurrent Read() calls: Not safe
  • Multiple concurrent Write() calls: Not safe
  • Concurrent Read() + Write(): Safe (different operations)

Examples

See example_test.go for comprehensive usage examples including:

  • Basic UDP client usage
  • Client with callbacks
  • One-shot request/response
  • Error handling
  • Context cancellation
  • Datagram size management

See Also

  • github.com/nabbar/golib/socket - Base interfaces and types
  • github.com/nabbar/golib/socket/config - Configuration builder
  • github.com/nabbar/golib/socket/server/udp - UDP server implementation
  • github.com/nabbar/golib/socket/client/tcp - TCP client implementation
  • github.com/nabbar/golib/network/protocol - Network protocol definitions

Package Status

This package is production-ready and stable. It has been tested in various production environments handling millions of datagrams.

For security vulnerabilities, please report privately via GitHub Security Advisories rather than public issues.

Example (BasicClient)

Example_basicClient demonstrates the simplest UDP client setup.

This example shows minimal configuration for a UDP client that sends a datagram to a remote server.

package main

import (
	"context"
	"fmt"

	"github.com/nabbar/golib/socket/client/udp"
)

func main() {
	// Create client
	client, err := udp.New("localhost:8080")
	if err != nil {
		fmt.Printf("Error: %v\n", err)
		return
	}
	defer client.Close()

	// Connect (associate socket with remote address)
	ctx := context.Background()
	if err := client.Connect(ctx); err != nil {
		fmt.Printf("Error: %v\n", err)
		return
	}

	// Send datagram
	data := []byte("Hello, UDP!")
	n, err := client.Write(data)
	if err != nil {
		fmt.Printf("Error: %v\n", err)
		return
	}

	fmt.Printf("Sent %d bytes\n", n)
}
Output:

Sent 11 bytes
Example (CallbackOrdering)

Example_callbackOrdering demonstrates callback execution order.

This example shows the order in which callbacks are triggered during client lifecycle operations.

package main

import (
	"context"
	"fmt"
	"log"
	"net"
	"sync"
	"time"

	libsck "github.com/nabbar/golib/socket"
	"github.com/nabbar/golib/socket/client/udp"
)

func main() {
	client, err := udp.New("localhost:8093")
	if err != nil {
		log.Fatal(err)
	}
	defer client.Close()

	var (
		events []string
		mu     sync.Mutex
	)

	client.RegisterFuncInfo(func(local, remote net.Addr, state libsck.ConnState) {
		mu.Lock()
		events = append(events, state.String())
		mu.Unlock()
	})

	ctx := context.Background()
	_ = client.Connect(ctx)

	// Allow callbacks to execute (they're async)
	time.Sleep(50 * time.Millisecond)

	// Note: Exact order may vary due to async execution
	mu.Lock()
	eventCount := len(events)
	mu.Unlock()
	fmt.Printf("Events captured: %d\n", eventCount)
}
Output:

Events captured: 2
Example (ClientWithCallbacks)

Example_clientWithCallbacks demonstrates callback registration.

This example shows how to register callbacks for error handling and operation monitoring.

package main

import (
	"context"
	"fmt"
	"net"

	libsck "github.com/nabbar/golib/socket"
	"github.com/nabbar/golib/socket/client/udp"
)

func main() {
	client, err := udp.New("localhost:8081")
	if err != nil {
		fmt.Printf("Error: %v\n", err)
		return
	}
	defer client.Close()

	// Register error callback
	client.RegisterFuncError(func(errs ...error) {
		for _, e := range errs {
			if e != nil {
				fmt.Printf("Client error: %v\n", e)
			}
		}
	})

	// Register info callback
	client.RegisterFuncInfo(func(local, remote net.Addr, state libsck.ConnState) {
		fmt.Printf("State: %s\n", state.String())
	})

	ctx := context.Background()
	if err := client.Connect(ctx); err != nil {
		fmt.Printf("Error: %v\n", err)
		return
	}

	fmt.Println("Callbacks registered")
	// Note: Callback state changes are asynchronous and output may vary
}
Example (ContextTimeout)

Example_contextTimeout demonstrates context timeout handling.

This example shows how to use context timeouts to limit operation duration.

package main

import (
	"context"
	"fmt"
	"time"

	"github.com/nabbar/golib/socket/client/udp"
)

func main() {
	client, err := udp.New("localhost:8084")
	if err != nil {
		fmt.Printf("Error: %v\n", err)
		return
	}
	defer client.Close()

	// Create context with timeout
	ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
	defer cancel()

	if err := client.Connect(ctx); err != nil {
		if ctx.Err() == context.DeadlineExceeded {
			fmt.Println("Connection timeout")
			return
		}
		fmt.Printf("Error: %v\n", err)
		return
	}

	fmt.Println("Connected successfully")
}
Output:

Connected successfully
Example (DatagramSizeManagement)

Example_datagramSizeManagement demonstrates proper datagram sizing.

This example shows how to manage datagram sizes to avoid IP fragmentation and ensure reliable delivery.

package main

import (
	"context"
	"fmt"

	"github.com/nabbar/golib/socket/client/udp"
)

func main() {
	client, err := udp.New("localhost:8085")
	if err != nil {
		fmt.Printf("Error: %v\n", err)
		return
	}
	defer client.Close()

	ctx := context.Background()
	if err := client.Connect(ctx); err != nil {
		fmt.Printf("Error: %v\n", err)
		return
	}

	// Safe datagram size (well below 1472 byte Ethernet MTU limit)
	const maxSafeSize = 1400

	data := make([]byte, maxSafeSize)
	for i := range data {
		data[i] = byte(i % 256)
	}

	n, err := client.Write(data)
	if err != nil {
		fmt.Printf("Error: %v\n", err)
		return
	}

	fmt.Printf("Sent datagram of %d bytes (safe size)\n", n)
}
Output:

Sent datagram of 1400 bytes (safe size)
Example (EmptyWrite)

Example_emptyWrite demonstrates writing empty datagrams.

This example shows that writing empty data is allowed and can be used as a keepalive or ping mechanism.

package main

import (
	"context"
	"fmt"

	"github.com/nabbar/golib/socket/client/udp"
)

func main() {
	client, err := udp.New("localhost:8092")
	if err != nil {
		fmt.Printf("Error: %v\n", err)
		return
	}
	defer client.Close()

	ctx := context.Background()
	if err := client.Connect(ctx); err != nil {
		fmt.Printf("Error: %v\n", err)
		return
	}

	// Write empty datagram
	n, err := client.Write([]byte{})
	if err != nil {
		fmt.Printf("Error: %v\n", err)
		return
	}

	fmt.Printf("Sent empty datagram: %d bytes\n", n)
}
Output:

Sent empty datagram: 0 bytes
Example (ErrorHandling)

Example_errorHandling demonstrates comprehensive error handling.

This example shows how to handle various error scenarios including connection errors and I/O errors.

package main

import (
	"fmt"

	"github.com/nabbar/golib/socket/client/udp"
)

func main() {
	// Attempt to create client with invalid address
	_, err := udp.New("")
	if err != nil {
		fmt.Printf("Creation error: %v\n", err)
	}

	// Valid client
	client, err := udp.New("localhost:8086")
	if err != nil {
		fmt.Printf("Error: %v\n", err)
		return
	}
	defer client.Close()

	// Attempt operation before connection
	_, err = client.Write([]byte("test"))
	if err != nil {
		fmt.Printf("Write error before connect: %v\n", err)
	}

}
Output:

Creation error: invalid dial address
Write error before connect: invalid connection
Example (FireAndForget)

Example_fireAndForget demonstrates fire-and-forget pattern.

This example shows sending datagrams without waiting for responses, which is a common UDP pattern.

package main

import (
	"context"
	"fmt"

	"github.com/nabbar/golib/socket/client/udp"
)

func main() {
	client, err := udp.New("localhost:8083")
	if err != nil {
		fmt.Printf("Error: %v\n", err)
		return
	}
	defer client.Close()

	ctx := context.Background()
	if err := client.Connect(ctx); err != nil {
		fmt.Printf("Error: %v\n", err)
		return
	}

	// Send multiple datagrams
	messages := []string{"msg1", "msg2", "msg3"}
	for _, msg := range messages {
		_, err := client.Write([]byte(msg))
		if err != nil {
			fmt.Printf("Error: %v\n", err)
			continue
		}
	}

	fmt.Printf("Sent %d messages\n", len(messages))
	// Note: This example requires a UDP server listening on localhost:8083
}
Example (Ipv6Address)

Example_ipv6Address demonstrates using IPv6 addresses.

This example shows how to create a client with an IPv6 address.

package main

import (
	"fmt"

	"github.com/nabbar/golib/socket/client/udp"
)

func main() {
	// IPv6 addresses must be enclosed in brackets
	client, err := udp.New("[::1]:8091")
	if err != nil {
		fmt.Printf("Error: %v\n", err)
		return
	}
	defer client.Close()

	fmt.Println("IPv6 client created successfully")
}
Output:

IPv6 client created successfully
Example (MultipleMessages)

Example_multipleMessages demonstrates sending multiple messages.

This example shows sending a sequence of datagrams over the same associated socket.

package main

import (
	"context"
	"fmt"

	"github.com/nabbar/golib/socket/client/udp"
)

func main() {
	client, err := udp.New("localhost:8088")
	if err != nil {
		fmt.Printf("Error: %v\n", err)
		return
	}
	defer client.Close()

	ctx := context.Background()
	if err := client.Connect(ctx); err != nil {
		fmt.Printf("Error: %v\n", err)
		return
	}

	// Send sequence of messages
	count := 0
	for i := 0; i < 5; i++ {
		msg := fmt.Sprintf("Message %d", i)
		n, err := client.Write([]byte(msg))
		if err != nil {
			fmt.Printf("Error: %v\n", err)
			continue
		}
		if n > 0 {
			count++
		}
	}

	fmt.Printf("Successfully sent %d messages\n", count)
	// Note: This example requires a UDP server listening on localhost:8088
}
Example (OneShotRequest)

Example_oneShotRequest demonstrates one-shot request/response pattern.

This example shows the Once() method for simple request/response operations that don't require persistent socket association.

package main

import (
	"bytes"
	"context"
	"fmt"
	"io"
	"time"

	"github.com/nabbar/golib/socket/client/udp"
)

func main() {
	client, err := udp.New("localhost:8082")
	if err != nil {
		fmt.Printf("Error: %v\n", err)
		return
	}

	ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
	defer cancel()

	// Prepare request
	request := bytes.NewBufferString("QUERY")

	// Send and optionally receive response
	err = client.Once(ctx, request, func(reader io.Reader) {
		// In real usage, would read response here
		// For example only, we skip the read
		fmt.Println("Request sent successfully")
	})

	if err != nil {
		fmt.Printf("Error: %v\n", err)
		return
	}

	// Socket automatically closed after Once()
	fmt.Printf("Connection closed: %v\n", !client.IsConnected())
}
Output:

Request sent successfully
Connection closed: true
Example (Reconnection)

Example_reconnection demonstrates reconnecting a client.

This example shows that calling Connect() multiple times replaces the existing socket association.

package main

import (
	"context"
	"fmt"

	"github.com/nabbar/golib/socket/client/udp"
)

func main() {
	client, err := udp.New("localhost:8089")
	if err != nil {
		fmt.Printf("Error: %v\n", err)
		return
	}
	defer client.Close()

	ctx := context.Background()

	// Initial connection
	if err := client.Connect(ctx); err != nil {
		fmt.Printf("Error: %v\n", err)
		return
	}
	fmt.Printf("First connect: %v\n", client.IsConnected())

	// Reconnect (replaces socket)
	if err := client.Connect(ctx); err != nil {
		fmt.Printf("Error: %v\n", err)
		return
	}
	fmt.Printf("Second connect: %v\n", client.IsConnected())

}
Output:

First connect: true
Second connect: true
Example (StateMonitoring)

Example_stateMonitoring demonstrates client state monitoring.

This example shows how to check client connection state using the IsConnected() method.

package main

import (
	"context"
	"fmt"

	"github.com/nabbar/golib/socket/client/udp"
)

func main() {
	client, err := udp.New("localhost:8087")
	if err != nil {
		fmt.Printf("Error: %v\n", err)
		return
	}

	// Check initial state
	fmt.Printf("Initially connected: %v\n", client.IsConnected())

	ctx := context.Background()
	if err := client.Connect(ctx); err != nil {
		fmt.Printf("Error: %v\n", err)
		return
	}

	fmt.Printf("After connect: %v\n", client.IsConnected())

	client.Close()
	fmt.Printf("After close: %v\n", client.IsConnected())

}
Output:

Initially connected: false
After connect: true
After close: false
Example (TlsNoOp)

Example_tlsNoOp demonstrates that TLS is not supported.

This example shows that SetTLS() is a no-op for UDP clients since UDP doesn't support TLS natively.

package main

import (
	"fmt"

	"github.com/nabbar/golib/socket/client/udp"
)

func main() {
	client, err := udp.New("localhost:8090")
	if err != nil {
		fmt.Printf("Error: %v\n", err)
		return
	}
	defer client.Close()

	// SetTLS is a no-op for UDP (returns nil)
	err = client.SetTLS(true, nil, "localhost")
	if err == nil {
		fmt.Println("TLS not supported (no-op, returns nil)")
	}

}
Output:

TLS not supported (no-op, returns nil)

Index

Examples

Constants

This section is empty.

Variables

View Source
var (
	// ErrInstance is returned when a nil client instance is used for operations.
	// This typically indicates a programming error where a method is called on
	// a nil pointer or an uninitialized client.
	ErrInstance = fmt.Errorf("invalid instance")

	// ErrConnection is returned when attempting to perform I/O operations
	// on a client that hasn't called Connect(), or when the underlying socket
	// is nil or invalid. Call Connect() before performing operations.
	ErrConnection = fmt.Errorf("invalid connection")

	// ErrAddress is returned by New() when the provided address is empty,
	// malformed, or cannot be resolved as a valid UDP address. The address
	// must be in the format "host:port" (e.g., "localhost:8080", "192.168.1.1:9000").
	ErrAddress = fmt.Errorf("invalid dial address")
)

Functions

This section is empty.

Types

type ClientUDP

type ClientUDP interface {
	libsck.Client
}

ClientUDP represents a UDP client that implements the socket.Client interface.

This interface extends github.com/nabbar/golib/socket.Client and provides all standard socket operations for UDP datagram communication:

  • Connect(ctx) - Associate socket with remote address
  • IsConnected() - Check if socket is associated
  • Read(p []byte) - Read datagram from socket
  • Write(p []byte) - Write datagram to socket
  • Close() - Close the socket
  • Once(ctx, request, response) - One-shot request/response operation
  • SetTLS(enable, config, serverName) - No-op for UDP (always returns nil)
  • RegisterFuncError(f) - Register error callback
  • RegisterFuncInfo(f) - Register datagram info callback

Important UDP characteristics:

  • Connectionless: No handshake or persistent connection state
  • Unreliable: Datagrams may be lost without notification
  • Unordered: Datagrams may arrive out of order
  • Message boundaries: Each Write() sends one datagram
  • No TLS: UDP doesn't support encryption natively (use DTLS if needed)

All operations are thread-safe and use atomic operations internally. The client maintains minimal state for the associated remote address.

See github.com/nabbar/golib/socket package for interface details.

func New

func New(address string) (ClientUDP, error)

New creates a new UDP client for the specified address.

The address parameter must be in the format "host:port", where:

  • host can be a hostname, IPv4 address, IPv6 address (in brackets), or empty
  • port must be a valid port number (1-65535)

Examples of valid addresses:

  • "localhost:8080"
  • "192.168.1.1:9000"
  • "[::1]:8080" (IPv6)
  • ":8080" (binds to all interfaces)

The client is created in a disconnected state. Use Connect() to associate the socket with the remote address. The address is validated but no network operation is performed during construction.

UDP-specific notes:

  • "Connect" doesn't establish a connection, it associates the socket
  • Maximum datagram size is typically 65507 bytes (65535 - 8 byte header - 20 byte IP header)
  • Consider using smaller sizes (< 1472 bytes) to avoid IP fragmentation
  • No guarantee of delivery or ordering

Returns:

  • ClientUDP: A new client instance if successful
  • error: ErrAddress if address is empty or malformed, or a net.Error if the address cannot be resolved as a valid UDP address

Example:

client, err := udp.New("localhost:8080")
if err != nil {
    log.Fatal(err)
}
defer client.Close()

ctx := context.Background()
if err := client.Connect(ctx); err != nil {
    log.Fatal(err)
}

// Send small datagram
data := []byte("Hello, UDP!")
n, err := client.Write(data)
if err != nil {
    log.Fatal(err)
}

Jump to

Keyboard shortcuts

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