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