407 lines
8.9 KiB
Go
407 lines
8.9 KiB
Go
package eq2net
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/panjf2000/gnet/v2"
|
|
)
|
|
|
|
// ServerConfig contains configuration for the EQ2 server
|
|
type ServerConfig struct {
|
|
// Network settings
|
|
Address string // Listen address (e.g., ":9000")
|
|
MaxConnections int // Maximum concurrent connections
|
|
ReadBufferSize int // UDP read buffer size
|
|
WriteBufferSize int // UDP write buffer size
|
|
|
|
// Stream settings
|
|
StreamConfig *StreamConfig // Default config for new streams
|
|
|
|
// Performance settings
|
|
NumEventLoops int // Number of gnet event loops (0 = NumCPU)
|
|
ReusePort bool // Enable SO_REUSEPORT for load balancing
|
|
}
|
|
|
|
// DefaultServerConfig returns a default server configuration
|
|
func DefaultServerConfig() *ServerConfig {
|
|
return &ServerConfig{
|
|
Address: ":9000",
|
|
MaxConnections: 10000,
|
|
ReadBufferSize: 65536,
|
|
WriteBufferSize: 65536,
|
|
StreamConfig: DefaultStreamConfig(),
|
|
NumEventLoops: 0,
|
|
ReusePort: true,
|
|
}
|
|
}
|
|
|
|
// EQ2Server implements a gnet-based EverQuest 2 server
|
|
type EQ2Server struct {
|
|
gnet.BuiltinEventEngine
|
|
|
|
config *ServerConfig
|
|
engine gnet.Engine
|
|
engineSet bool // Track if engine has been set
|
|
addr net.Addr
|
|
|
|
// Connection management
|
|
streams map[string]*serverStream // Key: remote address string
|
|
streamsMu sync.RWMutex
|
|
|
|
// Callbacks
|
|
onNewConnection func(*EQStream)
|
|
onConnectionClosed func(*EQStream, string)
|
|
|
|
// Lifecycle
|
|
ctx context.Context
|
|
cancel context.CancelFunc
|
|
wg sync.WaitGroup
|
|
}
|
|
|
|
// serverStream wraps an EQStream with server-specific data
|
|
type serverStream struct {
|
|
stream *EQStream
|
|
lastActive time.Time
|
|
conn gnet.Conn
|
|
}
|
|
|
|
// NewEQ2Server creates a new EQ2 server
|
|
func NewEQ2Server(config *ServerConfig) *EQ2Server {
|
|
if config == nil {
|
|
config = DefaultServerConfig()
|
|
}
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
|
|
return &EQ2Server{
|
|
config: config,
|
|
streams: make(map[string]*serverStream),
|
|
ctx: ctx,
|
|
cancel: cancel,
|
|
}
|
|
}
|
|
|
|
// Start begins listening for connections
|
|
func (s *EQ2Server) Start() error {
|
|
// Configure gnet options
|
|
opts := []gnet.Option{
|
|
gnet.WithMulticore(true),
|
|
gnet.WithReusePort(s.config.ReusePort),
|
|
gnet.WithSocketRecvBuffer(s.config.ReadBufferSize),
|
|
gnet.WithSocketSendBuffer(s.config.WriteBufferSize),
|
|
gnet.WithTicker(true),
|
|
}
|
|
|
|
if s.config.NumEventLoops > 0 {
|
|
opts = append(opts, gnet.WithNumEventLoop(s.config.NumEventLoops))
|
|
}
|
|
|
|
// Start cleanup worker
|
|
s.wg.Add(1)
|
|
go s.cleanupWorker()
|
|
|
|
// Start gnet server
|
|
return gnet.Run(s, "udp://"+s.config.Address, opts...)
|
|
}
|
|
|
|
// Stop gracefully shuts down the server
|
|
func (s *EQ2Server) Stop() error {
|
|
// Signal shutdown
|
|
s.cancel()
|
|
|
|
// Close all streams
|
|
s.streamsMu.Lock()
|
|
for _, ss := range s.streams {
|
|
ss.stream.Close()
|
|
}
|
|
s.streamsMu.Unlock()
|
|
|
|
// Wait for cleanup
|
|
done := make(chan struct{})
|
|
go func() {
|
|
s.wg.Wait()
|
|
close(done)
|
|
}()
|
|
|
|
select {
|
|
case <-done:
|
|
case <-time.After(10 * time.Second):
|
|
// Force shutdown after timeout
|
|
}
|
|
|
|
// Stop gnet engine
|
|
if s.engineSet {
|
|
return s.engine.Stop(s.ctx)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// gnet event handlers
|
|
|
|
// OnBoot is called when the server starts
|
|
func (s *EQ2Server) OnBoot(eng gnet.Engine) (action gnet.Action) {
|
|
s.engine = eng
|
|
s.engineSet = true
|
|
// Parse and store the address
|
|
addr, err := net.ResolveUDPAddr("udp", s.config.Address)
|
|
if err == nil {
|
|
s.addr = addr
|
|
}
|
|
fmt.Printf("EQ2 server started on %s\n", s.config.Address)
|
|
return gnet.None
|
|
}
|
|
|
|
// OnShutdown is called when the server stops
|
|
func (s *EQ2Server) OnShutdown(eng gnet.Engine) {
|
|
fmt.Println("EQ2 server shutting down")
|
|
}
|
|
|
|
// OnTraffic handles incoming UDP packets
|
|
func (s *EQ2Server) OnTraffic(c gnet.Conn) (action gnet.Action) {
|
|
// Read the packet
|
|
buf, err := c.Next(-1)
|
|
if err != nil {
|
|
return gnet.None
|
|
}
|
|
|
|
// Get remote address
|
|
remoteAddr := c.RemoteAddr()
|
|
if remoteAddr == nil {
|
|
return gnet.None
|
|
}
|
|
|
|
addrStr := remoteAddr.String()
|
|
|
|
// Look up or create stream
|
|
s.streamsMu.RLock()
|
|
ss, exists := s.streams[addrStr]
|
|
s.streamsMu.RUnlock()
|
|
|
|
if !exists {
|
|
// Check for session request
|
|
if len(buf) >= 2 {
|
|
opcode := uint16(buf[0])<<8 | uint16(buf[1])
|
|
if opcode == OPSessionRequest {
|
|
// Create new stream
|
|
ss = s.createStream(c, remoteAddr)
|
|
if ss == nil {
|
|
return gnet.None
|
|
}
|
|
} else {
|
|
// Not a session request, send out-of-session
|
|
s.sendOutOfSession(c, remoteAddr)
|
|
return gnet.None
|
|
}
|
|
} else {
|
|
return gnet.None
|
|
}
|
|
}
|
|
|
|
// Update last activity
|
|
ss.lastActive = time.Now()
|
|
|
|
// Process packet in stream
|
|
ss.stream.handleIncomingPacket(buf)
|
|
|
|
return gnet.None
|
|
}
|
|
|
|
// OnTick is called periodically
|
|
func (s *EQ2Server) OnTick() (delay time.Duration, action gnet.Action) {
|
|
// Tick interval for maintenance tasks
|
|
return 100 * time.Millisecond, gnet.None
|
|
}
|
|
|
|
// createStream creates a new stream for a client
|
|
func (s *EQ2Server) createStream(c gnet.Conn, remoteAddr net.Addr) *serverStream {
|
|
// Check connection limit
|
|
s.streamsMu.Lock()
|
|
defer s.streamsMu.Unlock()
|
|
|
|
if len(s.streams) >= s.config.MaxConnections {
|
|
return nil
|
|
}
|
|
|
|
// Create stream config (copy from default)
|
|
streamConfig := *s.config.StreamConfig
|
|
|
|
// Create new stream
|
|
stream := NewEQStream(&streamConfig)
|
|
|
|
// Set up callbacks
|
|
stream.SetCallbacks(
|
|
func() {
|
|
// On connect
|
|
if s.onNewConnection != nil {
|
|
s.onNewConnection(stream)
|
|
}
|
|
},
|
|
func(reason string) {
|
|
// On disconnect
|
|
s.removeStream(remoteAddr.String())
|
|
if s.onConnectionClosed != nil {
|
|
s.onConnectionClosed(stream, reason)
|
|
}
|
|
},
|
|
nil, // Error handler
|
|
)
|
|
|
|
// Create server stream wrapper
|
|
ss := &serverStream{
|
|
stream: stream,
|
|
lastActive: time.Now(),
|
|
conn: c,
|
|
}
|
|
|
|
// Store in map
|
|
s.streams[remoteAddr.String()] = ss
|
|
|
|
// Create a custom PacketConn wrapper for this stream
|
|
packetConn := &gnetPacketConn{
|
|
conn: c,
|
|
localAddr: s.addr,
|
|
remoteAddr: remoteAddr,
|
|
server: s,
|
|
}
|
|
|
|
// Connect the stream (in server mode, this just sets up the connection)
|
|
go func() {
|
|
if err := stream.Connect(packetConn, remoteAddr); err != nil {
|
|
s.removeStream(remoteAddr.String())
|
|
}
|
|
}()
|
|
|
|
return ss
|
|
}
|
|
|
|
// removeStream removes a stream from the server
|
|
func (s *EQ2Server) removeStream(addrStr string) {
|
|
s.streamsMu.Lock()
|
|
defer s.streamsMu.Unlock()
|
|
|
|
if ss, exists := s.streams[addrStr]; exists {
|
|
ss.stream.Close()
|
|
delete(s.streams, addrStr)
|
|
}
|
|
}
|
|
|
|
// sendOutOfSession sends an out-of-session packet
|
|
func (s *EQ2Server) sendOutOfSession(c gnet.Conn, remoteAddr net.Addr) {
|
|
packet := NewEQProtocolPacket(OPOutOfSession, nil)
|
|
data := packet.Serialize(0)
|
|
c.AsyncWrite(data, nil)
|
|
}
|
|
|
|
// cleanupWorker periodically cleans up inactive connections
|
|
func (s *EQ2Server) cleanupWorker() {
|
|
defer s.wg.Done()
|
|
|
|
ticker := time.NewTicker(30 * time.Second)
|
|
defer ticker.Stop()
|
|
|
|
for {
|
|
select {
|
|
case <-s.ctx.Done():
|
|
return
|
|
|
|
case <-ticker.C:
|
|
s.cleanupInactiveStreams()
|
|
}
|
|
}
|
|
}
|
|
|
|
// cleanupInactiveStreams removes streams that have been inactive too long
|
|
func (s *EQ2Server) cleanupInactiveStreams() {
|
|
timeout := 5 * time.Minute
|
|
now := time.Now()
|
|
|
|
s.streamsMu.Lock()
|
|
defer s.streamsMu.Unlock()
|
|
|
|
for addr, ss := range s.streams {
|
|
if now.Sub(ss.lastActive) > timeout {
|
|
ss.stream.Close()
|
|
delete(s.streams, addr)
|
|
}
|
|
}
|
|
}
|
|
|
|
// SetCallbacks sets server event callbacks
|
|
func (s *EQ2Server) SetCallbacks(onNew func(*EQStream), onClosed func(*EQStream, string)) {
|
|
s.onNewConnection = onNew
|
|
s.onConnectionClosed = onClosed
|
|
}
|
|
|
|
// GetStream returns the stream for a given address
|
|
func (s *EQ2Server) GetStream(addr string) *EQStream {
|
|
s.streamsMu.RLock()
|
|
defer s.streamsMu.RUnlock()
|
|
|
|
if ss, exists := s.streams[addr]; exists {
|
|
return ss.stream
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// GetAllStreams returns all active streams
|
|
func (s *EQ2Server) GetAllStreams() []*EQStream {
|
|
s.streamsMu.RLock()
|
|
defer s.streamsMu.RUnlock()
|
|
|
|
streams := make([]*EQStream, 0, len(s.streams))
|
|
for _, ss := range s.streams {
|
|
streams = append(streams, ss.stream)
|
|
}
|
|
return streams
|
|
}
|
|
|
|
// gnetPacketConn implements net.PacketConn for gnet connections
|
|
type gnetPacketConn struct {
|
|
conn gnet.Conn
|
|
localAddr net.Addr
|
|
remoteAddr net.Addr
|
|
server *EQ2Server
|
|
}
|
|
|
|
func (g *gnetPacketConn) ReadFrom(p []byte) (n int, addr net.Addr, err error) {
|
|
// This is handled by OnTraffic, not used in server mode
|
|
return 0, nil, fmt.Errorf("not implemented for server mode")
|
|
}
|
|
|
|
func (g *gnetPacketConn) WriteTo(p []byte, addr net.Addr) (n int, err error) {
|
|
// Write to the gnet connection
|
|
err = g.conn.AsyncWrite(p, nil)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
return len(p), nil
|
|
}
|
|
|
|
func (g *gnetPacketConn) Close() error {
|
|
// Connection lifecycle is managed by server
|
|
return nil
|
|
}
|
|
|
|
func (g *gnetPacketConn) LocalAddr() net.Addr {
|
|
return g.localAddr
|
|
}
|
|
|
|
func (g *gnetPacketConn) SetDeadline(t time.Time) error {
|
|
// Not implemented for UDP
|
|
return nil
|
|
}
|
|
|
|
func (g *gnetPacketConn) SetReadDeadline(t time.Time) error {
|
|
// Not implemented for UDP
|
|
return nil
|
|
}
|
|
|
|
func (g *gnetPacketConn) SetWriteDeadline(t time.Time) error {
|
|
// Not implemented for UDP
|
|
return nil
|
|
} |