add reports, initial udp framework
This commit is contained in:
commit
28fd282f20
1712
EQ2EMU_Architecture_White_Paper.md
Normal file
1712
EQ2EMU_Architecture_White_Paper.md
Normal file
File diff suppressed because it is too large
Load Diff
281
EQ2Emu_REPORT1.md
Normal file
281
EQ2Emu_REPORT1.md
Normal 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.
|
31
internal/udp/config.go
Normal file
31
internal/udp/config.go
Normal 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
140
internal/udp/middleware.go
Normal 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
84
internal/udp/packet.go
Normal 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
576
internal/udp/server.go
Normal 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
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user