add reports, initial udp framework

This commit is contained in:
Sky Johnson 2025-07-21 13:48:43 -05:00
commit 28fd282f20
7 changed files with 2827 additions and 0 deletions

File diff suppressed because it is too large Load Diff

281
EQ2Emu_REPORT1.md Normal file
View File

@ -0,0 +1,281 @@
# EQ2EMu Protocol Structure Report
Overview
The EQ2EMu protocol is a custom UDP-based networking protocol designed for EverQuest II server
emulation. It implements reliability, compression, encryption, and packet fragmentation on top
of UDP.
Core Architecture
1. Protocol Layers
Application Layer
- EQApplicationPacket: High-level game packets containing game logic
- PacketStruct: Dynamic packet structure system using XML definitions
- DataBuffer: Serialization/deserialization buffer management
Protocol Layer
- EQProtocolPacket: Low-level protocol packets with sequencing and reliability
- EQStream: Stream management with connection state, retransmission, and flow control
- EQPacket: Base packet class with common functionality
2. Packet Types
Protocol Control Packets (opcodes.h)
OP_SessionRequest = 0x01 // Client initiates connection
OP_SessionResponse = 0x02 // Server accepts connection
OP_Combined = 0x03 // Multiple packets combined
OP_SessionDisconnect = 0x05 // Connection termination
OP_KeepAlive = 0x06 // Keep connection alive
OP_ServerKeyRequest = 0x07 // Request encryption key
OP_SessionStatResponse = 0x08 // Connection statistics
OP_Packet = 0x09 // Regular data packet
OP_Fragment = 0x0d // Fragmented packet piece
OP_OutOfOrderAck = 0x11 // Acknowledge out-of-order packet
OP_Ack = 0x15 // Acknowledge packet receipt
OP_AppCombined = 0x19 // Combined application packets
OP_OutOfSession = 0x1d // Out of session notification
3. Connection Flow
1. Session Establishment
- Client sends OP_SessionRequest with session parameters
- Server responds with OP_SessionResponse containing session ID and encryption key
- RC4 encryption initialized using the provided key
2. Data Transfer
- Packets are sequenced (16-bit sequence numbers)
- Reliable packets require acknowledgment
- Retransmission on timeout (default 500ms * 3.0 multiplier)
- Packet combining for efficiency (max 256 bytes combined)
3. Session Termination
- Either side sends OP_SessionDisconnect
- Graceful shutdown with cleanup of pending packets
4. Key Features
Compression
- Uses zlib compression (indicated by 0x5a flag byte)
- Applied after protocol header
- Decompression required before processing
Encryption
- RC4 stream cipher
- Separate keys for client/server directions
- Client uses ~key (bitwise NOT), server uses key directly
- 20 bytes of dummy data prime both ciphers
CRC Validation
- Custom CRC16 implementation
- Applied to entire packet except last 2 bytes
- Session packets exempt from CRC
Fragmentation
- Large packets split into fragments
- Each fragment marked with OP_Fragment
- Reassembly at receiver before processing
5. Packet Structure
Basic Protocol Packet
[1-2 bytes] Opcode (1 byte if < 0xFF, otherwise 2 bytes)
[variable] Payload
[2 bytes] CRC16 (if applicable)
Combined Packet Format
[1 byte] Size of packet 1
[variable] Packet 1 data
[1 byte] Size of packet 2
[variable] Packet 2 data
...
6. Dynamic Packet System
The PacketStruct system allows runtime-defined packet structures:
- XML Configuration: Packet structures defined in XML files
- Version Support: Multiple versions per packet type
- Data Types: int8/16/32/64, float, double, string, array, color, equipment
- Conditional Fields: Fields can be conditional based on other field values
- Oversized Support: Dynamic sizing for variable-length fields
Example structure:
<Struct Name="PlayerUpdate" ClientVersion="1096">
<Data ElementName="activity" Type="int32"/>
<Data ElementName="speed" Type="float"/>
<Data ElementName="x" Type="float"/>
<Data ElementName="y" Type="float"/>
<Data ElementName="z" Type="float"/>
</Struct>
7. Opcode Management
- Dynamic Mapping: Runtime opcode mapping via configuration files
- Version Support: Different opcode sets per client version
- Name Resolution: String names mapped to numeric opcodes
- Shared Memory Option: Multi-process opcode sharing support
8. Implementation Considerations for Porting
1. Memory Management
- Extensive use of dynamic allocation
- Custom safe_delete macros for cleanup
- Reference counting for shared packets
2. Threading
- Mutex protection for all shared resources
- Separate threads for packet processing
- Condition variables for synchronization
3. Platform Dependencies
- POSIX sockets and threading
- Network byte order conversions
- Platform-specific timing functions
4. Performance Optimizations
- Packet combining to reduce overhead
- Preallocated buffers for common sizes
- Fast CRC table lookup
5. Error Handling
- Graceful degradation on errors
- Extensive logging for debugging
- Automatic retransmission on failure
9. Critical Classes to Port
1. EQStream: Core connection management
2. EQPacket/EQProtocolPacket: Packet representation
3. PacketStruct: Dynamic packet structure system
4. DataBuffer: Serialization utilities
5. OpcodeManager: Opcode mapping system
6. Crypto: RC4 encryption wrapper
7. CRC16: Custom CRC implementation
10. Protocol Quirks
- Opcode byte order depends on value (0x00 prefix for opcodes < 0xFF)
- Special handling for session control packets (no CRC)
- 20-byte RC4 priming sequence required
- Custom CRC polynomial (0xEDB88320 reversed)
- Compression flag at variable offset based on opcode size
This protocol is designed for reliability over UDP while maintaining low latency for game
traffic. The dynamic packet structure system allows flexibility in supporting multiple client
versions without recompilation.
# UDP Network Layer Architecture
1. EQStreamFactory - The UDP Socket Manager
- Opens and manages the main UDP socket (line 148: socket(AF_INET, SOCK_DGRAM, 0))
- Binds to a specific port (line 153: bind())
- Sets socket to non-blocking mode (line 159: fcntl(sock, F_SETFL, O_NONBLOCK))
- Runs three main threads:
- ReaderLoop(): Uses recvfrom() to read incoming UDP packets (line 228)
- WriterLoop(): Manages outgoing packet transmission
- CombinePacketLoop(): Combines small packets for efficiency
2. EQStream - Per-Connection Protocol Handler
- Manages individual client connections over the shared UDP socket
- Handles packet serialization/deserialization
- Implements reliability layer (sequencing, acknowledgments, retransmission)
- WritePacket() method uses sendto() for actual UDP transmission (line 1614)
3. Key UDP Operations:
Receiving (EQStreamFactory::ReaderLoop):
recvfrom(sock, buffer, 2048, 0, (struct sockaddr *)&from, (socklen_t *)&socklen)
- Uses select() for non-blocking I/O
- Creates new EQStream instances for new connections (OP_SessionRequest)
- Routes packets to existing streams by IP:port mapping
Sending (EQStream::WritePacket):
sendto(eq_fd,(char *)buffer,length,0,(sockaddr *)&address,sizeof(address))
- Serializes protocol packets to byte arrays
- Applies compression and encryption if enabled
- Adds CRC checksums
- Sends via UDP to specific client address
4. Connection Multiplexing:
- Single UDP socket serves multiple clients
- Stream identification by IP:port combination (sprintf(temp, "%u.%d",
ntohl(from.sin_addr.s_addr), ntohs(from.sin_port)))
- Thread-safe stream management with mutex protection
So yes, EQStream and EQStreamFactory together provide a complete UDP networking layer that
implements a reliable, connection-oriented protocol on top of unreliable UDP datagrams. The
factory manages the socket and dispatches packets to individual stream instances that handle
the protocol logic for each client connection.
# TCP Usage in EQ2EMu
The TCP implementation serves a completely separate purpose from the main game client
communication:
1. HTTP Web Server (web_server.hpp)
- Purpose: Administrative/monitoring interface
- Features:
- REST API endpoints (/version, / root)
- SSL/TLS support with certificate-based encryption
- HTTP Basic Authentication with session management
- Database-driven user authentication and authorization levels
- JSON response formatting for API calls
- Use Cases:
- Server status monitoring
- Administrative controls
- Third-party tool integration
- Web-based dashboards
2. Server-to-Server Communication (tcp_connection.hpp)
- Purpose: Inter-server communication within EQ2EMu cluster
- Features:
- Custom packet framing protocol
- Optional zlib compression for bandwidth efficiency
- Message relay capabilities between servers
- Connection state management
- Use Cases:
- Login server to world server communication
- Zone server coordination
- Cross-server messaging
- Load balancing and failover
Key Differences: UDP vs TCP
| Aspect | UDP (Game Clients) | TCP (Admin/Server)
|
|------------|--------------------------------------------|------------------------------------
----|
| Protocol | Custom EQ2 protocol with reliability layer | Standard HTTP/Custom framing
|
| Encryption | RC4 stream cipher | SSL/TLS or none
|
| Clients | Game clients (players) | Web browsers/Admin tools/Other
servers |
| Port | 9001 (World), 9100 (Login) | Configurable web port
|
| Threading | 3 worker threads per factory | Thread-per-connection
|
Architecture Summary
Game Clients ←→ [UDP EQStream] ←→ World/Login Servers
Admin Tools ←→ [TCP WebServer] ←→ World/Login Servers
Other Servers ←→ [TCP Connection] ←→ World/Login Servers
The TCP implementation provides out-of-band management and inter-server communication, while
the UDP implementation handles all real-time game traffic. This separation allows for robust
administration without interfering with game performance.

3
go.mod Normal file
View File

@ -0,0 +1,3 @@
module eq2emu
go 1.24.5

31
internal/udp/config.go Normal file
View File

@ -0,0 +1,31 @@
package udp
import "time"
// Configuration constants
const (
DefaultMTU = 1400
DefaultWindowSize = 256
DefaultRetryAttempts = 5
DefaultTimeout = 30 * time.Second
RetransmitTimeout = 3 * time.Second
KeepAliveInterval = 10 * time.Second
)
// Config holds configuration for reliable UDP connections
type Config struct {
MTU int
WindowSize uint16
RetryAttempts int
Timeout time.Duration
}
// DefaultConfig returns a default configuration
func DefaultConfig() *Config {
return &Config{
MTU: DefaultMTU,
WindowSize: DefaultWindowSize,
RetryAttempts: DefaultRetryAttempts,
Timeout: DefaultTimeout,
}
}

140
internal/udp/middleware.go Normal file
View File

@ -0,0 +1,140 @@
package udp
import (
"net"
"sync"
"time"
)
// Middleware interface for processing packets
type Middleware interface {
ProcessOutbound(data []byte, next func([]byte) (int, error)) (int, error)
ProcessInbound(data []byte, next func([]byte) (int, error)) (int, error)
Close() error
}
// Builder for fluent middleware configuration
type Builder struct {
address string
config *Config
middlewares []Middleware
}
// NewBuilder creates a new connection builder
func NewBuilder() *Builder {
return &Builder{
config: DefaultConfig(),
}
}
// Address sets the connection address
func (b *Builder) Address(addr string) *Builder {
b.address = addr
return b
}
// Config sets the UDP configuration
func (b *Builder) Config(config *Config) *Builder {
b.config = config
return b
}
// Use adds middleware to the stack
func (b *Builder) Use(middleware Middleware) *Builder {
b.middlewares = append(b.middlewares, middleware)
return b
}
// Listen creates a listener with middleware
func (b *Builder) Listen() (Listener, error) {
listener, err := Listen(b.address, b.config)
if err != nil {
return nil, err
}
return &middlewareListener{listener, b.middlewares}, nil
}
// Dial creates a client connection with middleware
func (b *Builder) Dial() (Conn, error) {
conn, err := Dial(b.address, b.config)
if err != nil {
return nil, err
}
return newMiddlewareConn(conn, b.middlewares), nil
}
// middlewareConn wraps a connection with middleware stack
type middlewareConn struct {
conn Conn
middlewares []Middleware
closeOnce sync.Once
}
func newMiddlewareConn(conn Conn, middlewares []Middleware) *middlewareConn {
return &middlewareConn{
conn: conn,
middlewares: middlewares,
}
}
func (m *middlewareConn) Write(data []byte) (int, error) {
return m.processOutbound(0, data)
}
func (m *middlewareConn) Read(data []byte) (int, error) {
n, err := m.conn.Read(data)
if err != nil {
return n, err
}
return m.processInbound(len(m.middlewares)-1, data[:n])
}
func (m *middlewareConn) processOutbound(index int, data []byte) (int, error) {
if index >= len(m.middlewares) {
return m.conn.Write(data)
}
return m.middlewares[index].ProcessOutbound(data, func(processed []byte) (int, error) {
return m.processOutbound(index+1, processed)
})
}
func (m *middlewareConn) processInbound(index int, data []byte) (int, error) {
if index < 0 {
return len(data), nil
}
return m.middlewares[index].ProcessInbound(data, func(processed []byte) (int, error) {
return m.processInbound(index-1, processed)
})
}
func (m *middlewareConn) Close() error {
m.closeOnce.Do(func() {
for _, middleware := range m.middlewares {
middleware.Close()
}
})
return m.conn.Close()
}
func (m *middlewareConn) LocalAddr() net.Addr { return m.conn.LocalAddr() }
func (m *middlewareConn) RemoteAddr() net.Addr { return m.conn.RemoteAddr() }
func (m *middlewareConn) SetReadDeadline(t time.Time) error { return m.conn.SetReadDeadline(t) }
func (m *middlewareConn) SetWriteDeadline(t time.Time) error { return m.conn.SetWriteDeadline(t) }
type middlewareListener struct {
listener Listener
middlewares []Middleware
}
func (l *middlewareListener) Accept() (Conn, error) {
conn, err := l.listener.Accept()
if err != nil {
return nil, err
}
return newMiddlewareConn(conn, l.middlewares), nil
}
func (l *middlewareListener) Close() error { return l.listener.Close() }
func (l *middlewareListener) Addr() net.Addr { return l.listener.Addr() }

84
internal/udp/packet.go Normal file
View File

@ -0,0 +1,84 @@
package udp
import (
"encoding/binary"
"fmt"
"hash/crc32"
"time"
)
// Packet types
const (
PacketTypeData uint8 = iota
PacketTypeAck
PacketTypeSessionRequest
PacketTypeSessionResponse
PacketTypeKeepAlive
PacketTypeDisconnect
PacketTypeFragment
)
// packet represents a protocol packet
type packet struct {
Type uint8
Sequence uint16
Ack uint16
Session uint32
Data []byte
CRC uint32
}
// Marshal serializes the packet
func (p *packet) Marshal() []byte {
dataLen := len(p.Data)
buf := make([]byte, 15+dataLen) // Fixed header + data
buf[0] = p.Type
binary.BigEndian.PutUint16(buf[1:3], p.Sequence)
binary.BigEndian.PutUint16(buf[3:5], p.Ack)
binary.BigEndian.PutUint32(buf[5:9], p.Session)
binary.BigEndian.PutUint16(buf[9:11], uint16(dataLen))
copy(buf[11:11+dataLen], p.Data)
// Calculate CRC32 for header + data
p.CRC = crc32.ChecksumIEEE(buf[:11+dataLen])
binary.BigEndian.PutUint32(buf[11+dataLen:], p.CRC)
return buf
}
// Unmarshal deserializes the packet
func (p *packet) Unmarshal(data []byte) error {
if len(data) < 15 {
return fmt.Errorf("packet too short: %d bytes", len(data))
}
p.Type = data[0]
p.Sequence = binary.BigEndian.Uint16(data[1:3])
p.Ack = binary.BigEndian.Uint16(data[3:5])
p.Session = binary.BigEndian.Uint32(data[5:9])
dataLen := binary.BigEndian.Uint16(data[9:11])
if len(data) < 15+int(dataLen) {
return fmt.Errorf("incomplete packet: expected %d bytes, got %d", 15+dataLen, len(data))
}
p.Data = make([]byte, dataLen)
copy(p.Data, data[11:11+dataLen])
p.CRC = binary.BigEndian.Uint32(data[11+dataLen:])
// Verify CRC
expectedCRC := crc32.ChecksumIEEE(data[:11+dataLen])
if p.CRC != expectedCRC {
return fmt.Errorf("CRC mismatch: expected %x, got %x", expectedCRC, p.CRC)
}
return nil
}
// pendingPacket represents a packet awaiting acknowledgment
type pendingPacket struct {
packet *packet
timestamp time.Time
attempts int
}

576
internal/udp/server.go Normal file
View File

@ -0,0 +1,576 @@
package udp
import (
"context"
"fmt"
"net"
"sync"
"sync/atomic"
"time"
)
// Conn represents a reliable UDP connection
type Conn interface {
Read(b []byte) (n int, err error)
Write(b []byte) (n int, err error)
Close() error
LocalAddr() net.Addr
RemoteAddr() net.Addr
SetReadDeadline(t time.Time) error
SetWriteDeadline(t time.Time) error
}
// Listener listens for incoming reliable UDP connections
type Listener interface {
Accept() (Conn, error)
Close() error
Addr() net.Addr
}
// stream implements a reliable UDP stream
type stream struct {
conn *net.UDPConn
remoteAddr *net.UDPAddr
localAddr *net.UDPAddr
session uint32
config *Config
// Sequence tracking
sendSeq uint32
recvSeq uint16
lastAckSent uint16
// Channels for communication
inbound chan []byte
outbound chan []byte
control chan *packet
done chan struct{}
closeOnce sync.Once
// Reliability tracking
pending map[uint16]*pendingPacket
pendingMutex sync.RWMutex
outOfOrder map[uint16][]byte
oooMutex sync.RWMutex
// Flow control
windowSize uint16
// Read/Write deadlines
readDeadline atomic.Value
writeDeadline atomic.Value
// Last activity for keep-alive
lastActivity time.Time
activityMutex sync.RWMutex
}
// newStream creates a new reliable UDP stream
func newStream(conn *net.UDPConn, remoteAddr *net.UDPAddr, session uint32, config *Config) *stream {
s := &stream{
conn: conn,
remoteAddr: remoteAddr,
localAddr: conn.LocalAddr().(*net.UDPAddr),
session: session,
config: config,
windowSize: config.WindowSize,
inbound: make(chan []byte, 256),
outbound: make(chan []byte, 256),
control: make(chan *packet, 64),
done: make(chan struct{}),
pending: make(map[uint16]*pendingPacket),
outOfOrder: make(map[uint16][]byte),
lastActivity: time.Now(),
}
// Start background goroutines
go s.writeLoop()
go s.retransmitLoop()
go s.keepAliveLoop()
return s
}
// Read implements Conn.Read
func (s *stream) Read(b []byte) (n int, err error) {
ctx, cancel := s.getReadDeadlineContext()
defer cancel()
select {
case data := <-s.inbound:
n = copy(b, data)
if n < len(data) {
return n, fmt.Errorf("buffer too small: need %d bytes, got %d", len(data), len(b))
}
return n, nil
case <-s.done:
return 0, fmt.Errorf("connection closed")
case <-ctx.Done():
return 0, fmt.Errorf("read timeout")
}
}
// Write implements Conn.Write
func (s *stream) Write(b []byte) (n int, err error) {
if len(b) == 0 {
return 0, nil
}
// Fragment large packets
mtu := s.config.MTU - 15 // Account for packet header
if len(b) <= mtu {
return s.writePacket(b)
}
// Fragment the data
sent := 0
for sent < len(b) {
end := sent + mtu
if end > len(b) {
end = len(b)
}
n, err := s.writePacket(b[sent:end])
sent += n
if err != nil {
return sent, err
}
}
return sent, nil
}
// writePacket writes a single packet
func (s *stream) writePacket(data []byte) (int, error) {
ctx, cancel := s.getWriteDeadlineContext()
defer cancel()
select {
case s.outbound <- data:
s.updateActivity()
return len(data), nil
case <-s.done:
return 0, fmt.Errorf("connection closed")
case <-ctx.Done():
return 0, fmt.Errorf("write timeout")
}
}
// writeLoop handles outbound packet transmission
func (s *stream) writeLoop() {
defer close(s.outbound)
for {
select {
case data := <-s.outbound:
s.sendDataPacket(data)
case ctrlPacket := <-s.control:
s.sendControlPacket(ctrlPacket)
case <-s.done:
return
}
}
}
// sendDataPacket sends a data packet with reliability
func (s *stream) sendDataPacket(data []byte) {
seq := uint16(atomic.AddUint32(&s.sendSeq, 1) - 1)
pkt := &packet{
Type: PacketTypeData,
Sequence: seq,
Ack: s.lastAckSent,
Session: s.session,
Data: data,
}
// Store for retransmission
s.pendingMutex.Lock()
s.pending[seq] = &pendingPacket{
packet: pkt,
timestamp: time.Now(),
attempts: 0,
}
s.pendingMutex.Unlock()
s.sendRawPacket(pkt)
}
// sendControlPacket sends control packets (ACKs, etc.)
func (s *stream) sendControlPacket(pkt *packet) {
pkt.Session = s.session
s.sendRawPacket(pkt)
}
// sendRawPacket sends a packet over UDP
func (s *stream) sendRawPacket(pkt *packet) {
data := pkt.Marshal()
s.conn.WriteToUDP(data, s.remoteAddr)
}
// handlePacket processes an incoming packet
func (s *stream) handlePacket(pkt *packet) {
s.updateActivity()
switch pkt.Type {
case PacketTypeData:
s.handleDataPacket(pkt)
case PacketTypeAck:
s.handleAckPacket(pkt)
case PacketTypeKeepAlive:
s.sendAck(pkt.Sequence)
case PacketTypeDisconnect:
s.Close()
}
}
// handleDataPacket processes incoming data packets
func (s *stream) handleDataPacket(pkt *packet) {
// Send ACK
s.sendAck(pkt.Sequence)
// Check sequence order
expectedSeq := s.recvSeq + 1
if pkt.Sequence == expectedSeq {
// In order - deliver immediately
s.deliverData(pkt.Data)
s.recvSeq = pkt.Sequence
// Check for buffered out-of-order packets
s.processOutOfOrder()
} else if pkt.Sequence > expectedSeq {
// Future packet - buffer it
s.oooMutex.Lock()
s.outOfOrder[pkt.Sequence] = pkt.Data
s.oooMutex.Unlock()
}
// Past packets are ignored (duplicate)
}
// processOutOfOrder delivers buffered in-order packets
func (s *stream) processOutOfOrder() {
s.oooMutex.Lock()
defer s.oooMutex.Unlock()
for {
nextSeq := s.recvSeq + 1
if data, exists := s.outOfOrder[nextSeq]; exists {
s.deliverData(data)
s.recvSeq = nextSeq
delete(s.outOfOrder, nextSeq)
} else {
break
}
}
}
// deliverData delivers data to the application
func (s *stream) deliverData(data []byte) {
select {
case s.inbound <- data:
case <-s.done:
default:
// Channel full - would block
}
}
// handleAckPacket processes acknowledgment packets
func (s *stream) handleAckPacket(pkt *packet) {
s.pendingMutex.Lock()
defer s.pendingMutex.Unlock()
if pending, exists := s.pending[pkt.Sequence]; exists {
delete(s.pending, pkt.Sequence)
_ = pending // Packet acknowledged
}
}
// sendAck sends an acknowledgment
func (s *stream) sendAck(seq uint16) {
s.lastAckSent = seq
ackPkt := &packet{
Type: PacketTypeAck,
Sequence: seq,
Ack: seq,
}
select {
case s.control <- ackPkt:
case <-s.done:
default:
}
}
// retransmitLoop handles packet retransmission
func (s *stream) retransmitLoop() {
ticker := time.NewTicker(time.Second)
defer ticker.Stop()
for {
select {
case <-ticker.C:
s.checkRetransmissions()
case <-s.done:
return
}
}
}
// checkRetransmissions checks for packets needing retransmission
func (s *stream) checkRetransmissions() {
now := time.Now()
s.pendingMutex.Lock()
defer s.pendingMutex.Unlock()
for seq, pending := range s.pending {
if now.Sub(pending.timestamp) > RetransmitTimeout {
if pending.attempts >= s.config.RetryAttempts {
// Too many attempts - close connection
delete(s.pending, seq)
go s.Close()
return
}
// Retransmit
pending.attempts++
pending.timestamp = now
s.sendRawPacket(pending.packet)
}
}
}
// keepAliveLoop sends periodic keep-alive packets
func (s *stream) keepAliveLoop() {
ticker := time.NewTicker(KeepAliveInterval)
defer ticker.Stop()
for {
select {
case <-ticker.C:
s.activityMutex.RLock()
idle := time.Since(s.lastActivity)
s.activityMutex.RUnlock()
if idle > KeepAliveInterval {
keepAlive := &packet{Type: PacketTypeKeepAlive}
select {
case s.control <- keepAlive:
case <-s.done:
return
}
}
case <-s.done:
return
}
}
}
// updateActivity updates the last activity timestamp
func (s *stream) updateActivity() {
s.activityMutex.Lock()
s.lastActivity = time.Now()
s.activityMutex.Unlock()
}
// Close implements Conn.Close
func (s *stream) Close() error {
s.closeOnce.Do(func() {
// Send disconnect packet
disconnect := &packet{Type: PacketTypeDisconnect}
select {
case s.control <- disconnect:
default:
}
close(s.done)
})
return nil
}
// Address methods
func (s *stream) LocalAddr() net.Addr { return s.localAddr }
func (s *stream) RemoteAddr() net.Addr { return s.remoteAddr }
// Deadline methods
func (s *stream) SetReadDeadline(t time.Time) error {
s.readDeadline.Store(t)
return nil
}
func (s *stream) SetWriteDeadline(t time.Time) error {
s.writeDeadline.Store(t)
return nil
}
func (s *stream) getReadDeadlineContext() (context.Context, context.CancelFunc) {
if deadline, ok := s.readDeadline.Load().(time.Time); ok && !deadline.IsZero() {
return context.WithDeadline(context.Background(), deadline)
}
return context.Background(), func() {}
}
func (s *stream) getWriteDeadlineContext() (context.Context, context.CancelFunc) {
if deadline, ok := s.writeDeadline.Load().(time.Time); ok && !deadline.IsZero() {
return context.WithDeadline(context.Background(), deadline)
}
return context.Background(), func() {}
}
// listener implements a reliable UDP listener
type listener struct {
conn *net.UDPConn
config *Config
streams map[string]*stream
mutex sync.RWMutex
incoming chan *stream
done chan struct{}
}
// Listen creates a new reliable UDP listener
func Listen(address string, config *Config) (Listener, error) {
if config == nil {
config = DefaultConfig()
}
addr, err := net.ResolveUDPAddr("udp", address)
if err != nil {
return nil, err
}
conn, err := net.ListenUDP("udp", addr)
if err != nil {
return nil, err
}
l := &listener{
conn: conn,
config: config,
streams: make(map[string]*stream),
incoming: make(chan *stream, 16),
done: make(chan struct{}),
}
go l.readLoop()
return l, nil
}
// readLoop handles incoming UDP packets
func (l *listener) readLoop() {
buf := make([]byte, 2048)
for {
select {
case <-l.done:
return
default:
}
n, addr, err := l.conn.ReadFromUDP(buf)
if err != nil {
continue
}
pkt := &packet{}
if err := pkt.Unmarshal(buf[:n]); err != nil {
continue
}
l.handlePacket(pkt, addr)
}
}
// handlePacket routes packets to appropriate streams
func (l *listener) handlePacket(pkt *packet, addr *net.UDPAddr) {
streamKey := addr.String()
l.mutex.RLock()
stream, exists := l.streams[streamKey]
l.mutex.RUnlock()
if !exists && pkt.Type == PacketTypeSessionRequest {
// New connection
session := pkt.Session
stream = newStream(l.conn, addr, session, l.config)
l.mutex.Lock()
l.streams[streamKey] = stream
l.mutex.Unlock()
// Send session response
response := &packet{
Type: PacketTypeSessionResponse,
Session: session,
}
stream.sendControlPacket(response)
select {
case l.incoming <- stream:
case <-l.done:
}
} else if exists {
stream.handlePacket(pkt)
}
}
// Accept implements Listener.Accept
func (l *listener) Accept() (Conn, error) {
select {
case stream := <-l.incoming:
return stream, nil
case <-l.done:
return nil, fmt.Errorf("listener closed")
}
}
// Close implements Listener.Close
func (l *listener) Close() error {
close(l.done)
l.mutex.Lock()
defer l.mutex.Unlock()
for _, stream := range l.streams {
stream.Close()
}
return l.conn.Close()
}
// Addr implements Listener.Addr
func (l *listener) Addr() net.Addr {
return l.conn.LocalAddr()
}
// Dial creates a client connection to a reliable UDP server
func Dial(address string, config *Config) (Conn, error) {
if config == nil {
config = DefaultConfig()
}
addr, err := net.ResolveUDPAddr("udp", address)
if err != nil {
return nil, err
}
conn, err := net.DialUDP("udp", nil, addr)
if err != nil {
return nil, err
}
session := uint32(time.Now().Unix())
stream := newStream(conn, addr, session, config)
// Send session request
request := &packet{
Type: PacketTypeSessionRequest,
Session: session,
}
stream.sendControlPacket(request)
return stream, nil
}