No description
| benchmark_test.go | ||
| conn.go | ||
| crc.go | ||
| crc_test.go | ||
| crypto.go | ||
| crypto_test.go | ||
| errors.go | ||
| examples.md | ||
| go.mod | ||
| listener.go | ||
| opcode.go | ||
| opcode_test.go | ||
| packet.go | ||
| packet_test.go | ||
| pool.go | ||
| protocol.go | ||
| protocol_test.go | ||
| README.md | ||
| testing.go | ||
Protocol
High-performance UDP packet listener for EQ2 emulator client communication.
Overview
The protocol package provides a robust, performant UDP listener designed for handling client connections in EverQuest 2 server emulators. It features:
- Worker Pool Architecture - N worker goroutines process packets concurrently (where N = CPU count by default)
- Zero Allocations - Buffer pooling eliminates allocations in the hot path
- Connection Tracking - Stateful peer tracking for bidirectional communication
- Graceful Shutdown - Clean listener and connection lifecycle management
- Thread-Safe - All operations are safe for concurrent use
- Pluggable Logging - Optional logger interface for debugging and monitoring
Installation
go get git.sharkk.net/eq2go/protocol
Quick Start
Basic Server
package main
import (
"context"
"fmt"
"log"
"git.sharkk.net/eq2go/protocol"
)
func main() {
// Create listener with defaults
cfg := protocol.DefaultConfig()
cfg.Address = "0.0.0.0:9000"
listener := protocol.NewListener(cfg)
// Register packet handler
listener.HandleFunc(func(ctx context.Context, conn protocol.Conn, data []byte) error {
fmt.Printf("Received %d bytes from %s\n", len(data), conn.RemoteAddr())
// Echo response
return conn.Send(ctx, data)
})
// Start listening (blocking)
log.Fatal(listener.ListenAndServe())
}
With TUI Logging
package main
import (
"context"
"fmt"
"git.sharkk.net/eq2go/protocol"
"git.sharkk.net/eq2go/tui"
)
// Implement protocol.Logger using TUI
type protocolLogger struct {
ui *tui.TUI
}
func (l *protocolLogger) Debug(msg string, fields ...any) {
l.ui.WriteLine(tui.FormatLog(tui.TagDebug, msg))
}
func (l *protocolLogger) Info(msg string, fields ...any) {
l.ui.WriteLine(tui.FormatLog(tui.TagInfo, msg))
}
func (l *protocolLogger) Warn(msg string, fields ...any) {
l.ui.WriteLine(tui.FormatLog(tui.TagWarning, msg))
}
func (l *protocolLogger) Error(msg string, fields ...any) {
l.ui.WriteLine(tui.FormatLog(tui.TagError, msg))
}
func main() {
// Create TUI
ui, _ := tui.New(tui.Config{
ServerName: "Login",
ServerVersion: "0.0.1",
})
ui.Start()
defer ui.Stop()
// Create listener
cfg := protocol.DefaultConfig()
cfg.Address = "0.0.0.0:9000"
cfg.Logger = &protocolLogger{ui: ui}
listener := protocol.NewListener(cfg)
listener.HandleFunc(func(ctx context.Context, conn protocol.Conn, data []byte) error {
ui.WriteLine(tui.FormatLog(tui.TagDebug,
fmt.Sprintf("Received %d bytes from %s", len(data), conn.RemoteAddr())))
return nil
})
// Run in background
go listener.ListenAndServe()
// Process TUI commands
ui.ProcessCommands()
}
Graceful Shutdown
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
err := listener.Shutdown(ctx)
if err != nil {
log.Printf("Shutdown error: %v", err)
}
Configuration
Config Struct
type Config struct {
// Address to bind to (required)
Address string
// UDP socket buffer sizes (default: 64KB each)
ReadBufferSize int
WriteBufferSize int
// Maximum packet size (default: 8KB)
MaxPacketSize int
// Worker pool size (default: runtime.NumCPU())
WorkerCount int
// Packet queue size (default: 10000)
ChannelBufferSize int
// Connection idle timeout (default: 5 minutes)
IdleTimeout time.Duration
// Cleanup interval (default: 1 minute)
CleanupInterval time.Duration
// Optional logger
Logger Logger
// Optional error handler
ErrorHandler func(error)
}
Default Values
cfg := protocol.DefaultConfig()
// ReadBufferSize: 65536 (64KB)
// WriteBufferSize: 65536 (64KB)
// MaxPacketSize: 8192 (8KB)
// WorkerCount: runtime.NumCPU()
// ChannelBufferSize: 10000
// IdleTimeout: 5 minutes
// CleanupInterval: 1 minute
API Reference
Listener Interface
type Listener interface {
// Start listening (blocking)
ListenAndServe() error
// Graceful shutdown
Shutdown(ctx context.Context) error
// Register handler
HandleFunc(handler HandlerFunc)
// Set logger
SetLogger(logger Logger)
// Get local address
LocalAddr() string
}
Conn Interface
type Conn interface {
// Send data to peer
Send(ctx context.Context, data []byte) error
// Get addresses
RemoteAddr() string
LocalAddr() string
// Get connection context
Context() context.Context
// Close connection
Close() error
}
Handler Signature
type HandlerFunc func(ctx context.Context, conn Conn, data []byte) error
Important: The data slice is only valid for the duration of the handler call. Handlers must copy data if they need to retain it after returning.
Logger Interface
type Logger interface {
Debug(msg string, fields ...any)
Info(msg string, fields ...any)
Warn(msg string, fields ...any)
Error(msg string, fields ...any)
}
This interface matches servertalk.Logger for consistency across packages.
Architecture
Worker Pool Model
UDP Socket
↓
readLoop (1 goroutine)
↓
packetQueue (buffered channel)
↓
workerLoop × N (N = CPU count)
↓
handler execution
Connection Lifecycle
- Packet arrives from new peer
- Worker creates
udpConnand adds to connection map - Handler is called with connection reference
- Connection remains in map for subsequent packets
- After
IdleTimeout, cleanup goroutine removes connection - On shutdown, all connections are closed
Performance Optimizations
- Buffer Pooling:
sync.Poolreuses byte slices (zero allocations) - Worker Pool: Pre-allocated goroutines prevent spawn overhead
- Buffered Queue: 10,000 packet buffer absorbs traffic spikes
- RWMutex: Connection map optimized for concurrent reads
- Lazy Cleanup: Idle connections removed periodically, not immediately
Performance
Benchmark results (on modern hardware):
BenchmarkBufferPool-8 50000000 24.5 ns/op 0 B/op 0 allocs/op
BenchmarkPacketProcessing-8 100000 11234 ns/op 512 B/op 8 allocs/op
BenchmarkConnectionLookup-8 20000000 62.3 ns/op 0 B/op 0 allocs/op
BenchmarkSendLatency-8 10000 142567 ns/op 128 B/op 2 allocs/op
Performance Goals
- Throughput: >100,000 packets/second
- Latency: <1ms send latency (local network)
- Allocations: Zero allocations in buffer pool
- CPU: Scales with worker count (default: CPU cores)
- Memory: Bounded by buffer pool and connection map
Thread Safety
Thread-Safe Operations
- All
Listenermethods - All
Connmethods - Connection map reads/writes
- Handler execution (isolated per packet)
- Logger calls
- Buffer pool operations
Not Thread-Safe
- Individual
Configmodifications after passing toNewListener()
Testing
Run Tests
go test -v
Run Benchmarks
go test -bench=. -benchmem
Example Test
func TestEchoServer(t *testing.T) {
cfg := protocol.DefaultConfig()
cfg.Address = "127.0.0.1:0"
listener := protocol.NewListener(cfg)
listener.HandleFunc(func(ctx context.Context, conn protocol.Conn, data []byte) error {
return conn.Send(ctx, data) // Echo
})
go listener.ListenAndServe()
defer listener.Shutdown(context.Background())
// ... send packets and verify responses ...
}
Mock Utilities
The package provides mock implementations for testing:
// Mock connection
conn := protocol.NewMockConn("127.0.0.1:12345", "0.0.0.0:9000")
conn.Send(ctx, data)
sent := conn.GetSent() // Retrieve all sent data
// Mock logger
logger := protocol.NewMockLogger()
logger.Info("test message")
logs := logger.GetInfoLog() // Retrieve logged messages
Error Handling
Standard Errors
var (
ErrListenerClosed = errors.New("listener closed")
ErrPacketTooLarge = errors.New("packet exceeds max size")
ErrInvalidAddress = errors.New("invalid address format")
ErrTimeout = errors.New("operation timeout")
ErrNoHandler = errors.New("no handler registered")
)
PacketError
Wraps handler errors with context:
type PacketError struct {
Addr string // Remote address
Err error // Underlying error
}
Error Handler
cfg.ErrorHandler = func(err error) {
log.Printf("Protocol error: %v", err)
}
If ErrorHandler is nil and Logger is set, errors are logged automatically.
Future Enhancements
Potential additions after initial release:
- Optional compression support (LZ4, zlib)
- Optional encryption support (TLS-like handshake)
- Rate limiting per connection
- Connection authentication
- Multi-handler routing (protocol/app subpackage)
- Packet replay protection
- Connection migration support
Dependencies
- Standard Library Only:
net,sync,time,context,errors,runtime - No External Dependencies: Fully self-contained
- No Circular Dependencies: Foundation layer for login/world servers
License
Part of the EQ2Go project.