Improved network communication

This commit is contained in:
2024-01-24 20:57:31 +01:00
parent 26c52a00b3
commit 2738895efe
49 changed files with 427 additions and 625 deletions

View File

@ -1,15 +0,0 @@
class_name Client
var peer: PacketPeerUDP
var address: String
var last_packet: int
var auth_token: String
var account: Account
var player: Player
func _init(p: PacketPeerUDP, a: String):
peer = p
address = a
func is_logged_in() -> bool:
return auth_token != ""

View File

@ -1,12 +0,0 @@
extends Node
var accounts := {
"user1": Account.new("user1", "password"),
"user2": Account.new("user2", "password"),
}
func get_account(username: String):
if !accounts.has(username):
return null
return accounts[username]

View File

@ -1,98 +0,0 @@
class_name Server
extends NetworkNode
## Port number.
@export var port := 4242
## Timeout in milliseconds.
@export var timeout := 5000
## Maximum number of pending connections.
@export var max_pending_connections := 4096
var server := UDPServer.new()
var clients := {}
var packet_count := 0
var now := 0
func _init():
super._init()
server.set_max_pending_connections(max_pending_connections)
server.listen(port)
Performance.add_custom_monitor("Server/Clients", get_client_count)
Performance.add_custom_monitor("Server/Packets", get_packet_count)
func _process(_delta):
server.poll()
func _physics_process(_delta):
now = Time.get_ticks_msec()
# Accept new connections
while server.is_connection_available():
var peer := server.take_connection()
peer_to_client(peer)
# Process packets from clients
for address in clients:
var client = clients[address]
var peer = client.peer
while peer.get_available_packet_count() > 0:
client.last_packet = now
var packet = peer.get_packet()
handle_packet(packet, peer)
packet_count += 1
# Disconnect
for address in clients.keys():
var client = clients[address]
var last_packet_time = client.last_packet
if now - last_packet_time > timeout:
peer_disconnected(client)
clients.erase(address)
func broadcast(packet: PackedByteArray):
for address in clients:
clients[address].peer.put_packet(packet)
func broadcast_others(packet: PackedByteArray, exclude: Client):
for address in clients:
var client = clients[address]
if client == exclude:
continue
client.peer.put_packet(packet)
func get_client_count() -> int:
return clients.size()
func get_packet_count() -> int:
var tmp := packet_count
packet_count = 0
return tmp
func peer_address(peer: PacketPeerUDP):
return "%s:%d" % [peer.get_packet_ip(), peer.get_packet_port()]
func peer_connected(c: Client):
print("[%s] Connected." % c.address)
func peer_disconnected(c: Client):
print("[%s] Disconnected." % c.address)
if c.player:
c.player.queue_free()
func peer_to_client(peer: PacketPeerUDP) -> Client:
var address = peer_address(peer)
if !clients.has(address):
var client = Client.new(peer, address)
clients[address] = client
peer_connected(client)
return client
return clients[address]

View File

@ -1,27 +0,0 @@
[gd_scene load_steps=6 format=3 uid="uid://b0e7n717sqeo6"]
[ext_resource type="Script" path="res://Database.gd" id="1_58s18"]
[ext_resource type="Script" path="res://Server.gd" id="1_gwiwx"]
[ext_resource type="Script" path="res://handler/Ping.gd" id="3_l2i5g"]
[ext_resource type="Script" path="res://handler/Login.gd" id="4_7aotg"]
[ext_resource type="PackedScene" uid="uid://148x2hp1hjq0" path="res://player/Player.tscn" id="5_26wo2"]
[node name="Main" type="Node"]
[node name="Server" type="Node" parent="."]
unique_name_in_owner = true
script = ExtResource("1_gwiwx")
[node name="Database" type="Node" parent="."]
unique_name_in_owner = true
script = ExtResource("1_58s18")
[node name="Ping" type="Node" parent="."]
script = ExtResource("3_l2i5g")
[node name="Login" type="Node" parent="."]
script = ExtResource("4_7aotg")
player_scene = ExtResource("5_26wo2")
[node name="Players" type="Node3D" parent="."]
unique_name_in_owner = true

6
server/core/Handler.go Normal file
View File

@ -0,0 +1,6 @@
package core
import "net"
// Handler is a byte code specific packet handler.
type Handler func([]byte, *net.UDPAddr, *Server) error

103
server/core/Server.go Normal file
View File

@ -0,0 +1,103 @@
package core
import (
"fmt"
"net"
)
// Server represents a UDP server.
type Server struct {
handlers [256]Handler
socket *net.UDPConn
packetCount int
}
// New creates a new server.
func New() *Server {
return &Server{}
}
// SetHandler sets the handler for the given byte code.
func (s *Server) SetHandler(code byte, handler Handler) {
s.handlers[code] = handler
}
// Run starts the server on the given port.
func (s *Server) Run(port int) {
s.socket = listen(port)
defer s.socket.Close()
s.read()
}
// PacketCount returns the number of processed packets.
func (s *Server) PacketCount() int {
return s.packetCount
}
// ResetPacketCount resets the number of processed packets to zero.
func (s *Server) ResetPacketCount() {
s.packetCount = 0
}
// Send sends the data prefixed with the byte code to the client.
func (s *Server) Send(code byte, data []byte, address *net.UDPAddr) error {
data = append([]byte{code}, data...)
_, err := s.socket.WriteToUDP(data, address)
return err
}
// listen creates a socket for the server and starts listening for incoming packets.
func listen(port int) *net.UDPConn {
addr, err := net.ResolveUDPAddr("udp", fmt.Sprintf(":%d", port))
if err != nil {
panic(err)
}
connection, err := net.ListenUDP("udp", addr)
if err != nil {
panic(err)
}
return connection
}
// read is a blocking call which will read incoming packets and handle them.
func (s *Server) read() {
buffer := make([]byte, 4096)
for {
n, addr, err := s.socket.ReadFromUDP(buffer)
if err != nil {
fmt.Println("Error reading from UDP connection:", err)
continue
}
if n == 0 {
continue
}
s.handle(buffer[:n], addr)
}
}
// handle deals with an incoming packet.
func (s *Server) handle(data []byte, addr *net.UDPAddr) {
handler := s.handlers[data[0]]
if handler == nil {
fmt.Printf("No callback registered for packet type %d\n", data[0])
return
}
err := handler(data[1:], addr, s)
if err != nil {
fmt.Println(err)
}
s.packetCount++
}

31
server/game/Client.go Normal file
View File

@ -0,0 +1,31 @@
package game
import (
"net"
"time"
)
// Client represents a logged in client.
type Client struct {
address *net.UDPAddr
lastPacket time.Time
authToken string
player Player
}
// NewClient creates a new client.
func NewClient(address *net.UDPAddr) *Client {
return &Client{
address: address,
}
}
// String shows the client address.
func (c *Client) String() string {
return c.address.String()
}
// KeepAlive sets the last packet time to now.
func (c *Client) KeepAlive() {
c.lastPacket = time.Now()
}

72
server/game/Login.go Normal file
View File

@ -0,0 +1,72 @@
package game
import (
"crypto/rand"
"encoding/base64"
"encoding/binary"
"encoding/json"
"errors"
"fmt"
"math"
"net"
"server/core"
"server/packet"
)
var (
Clients = NewManager()
LoginSuccess = []byte{0}
LoginFailure = []byte{1}
)
// Login checks the account credentials and gives a network peer access to an account.
func Login(data []byte, address *net.UDPAddr, server *core.Server) error {
var loginRequest [2]string
err := json.Unmarshal(data, &loginRequest)
if err != nil {
server.Send(packet.Login, LoginFailure, address)
return err
}
username := loginRequest[0]
password := loginRequest[1]
if password != "password" {
server.Send(packet.Login, LoginFailure, address)
return errors.New("login failure")
}
randomBytes := make([]byte, 32)
_, err = rand.Read(randomBytes)
if err != nil {
server.Send(packet.Login, LoginFailure, address)
return err
}
client := Clients.Get(address)
client.KeepAlive()
client.authToken = base64.StdEncoding.EncodeToString(randomBytes)
client.player.Name = username
client.player.Position.X = 5.0
playerState := []byte(client.player.Name + "\u0000")
playerState = appendVector3(playerState, client.player.Position)
server.Send(packet.Login, append(LoginSuccess, []byte(client.authToken)...), address)
server.Send(packet.PlayerState, playerState, address)
fmt.Printf("%s logged in.\n", username)
return nil
}
func appendVector3(data []byte, vector Vector3) []byte {
bits := math.Float32bits(vector.X)
data = binary.LittleEndian.AppendUint32(data, bits)
bits = math.Float32bits(vector.Y)
data = binary.LittleEndian.AppendUint32(data, bits)
bits = math.Float32bits(vector.Z)
return binary.LittleEndian.AppendUint32(data, bits)
}

68
server/game/Manager.go Normal file
View File

@ -0,0 +1,68 @@
package game
import (
"net"
"sync"
"sync/atomic"
"time"
)
// Manager keeps tracks of all player connections.
type Manager struct {
clients sync.Map
count atomic.Int64
}
// NewManager creates a new manager.
func NewManager() *Manager {
manager := &Manager{}
timeout := 5 * time.Second
interval := time.Second
go func() {
ticker := time.NewTicker(interval)
defer ticker.Stop()
for range ticker.C {
now := time.Now()
manager.clients.Range(func(key, value interface{}) bool {
item := value.(*Client)
if !item.lastPacket.IsZero() && now.After(item.lastPacket.Add(timeout)) {
manager.clients.Delete(key)
manager.count.Add(-1)
}
return true
})
}
}()
return manager
}
// Get either returns a new or existing client for the requested address.
func (m *Manager) Get(addr *net.UDPAddr) *Client {
obj, exists := m.clients.Load(addr.String())
if exists {
return obj.(*Client)
}
client := NewClient(addr)
m.clients.Store(addr.String(), client)
m.count.Add(1)
return client
}
// Contains tells you whether the address is already a registered client.
func (m *Manager) Contains(addr *net.UDPAddr) bool {
_, exists := m.clients.Load(addr.String())
return exists
}
// Count returns the number of clients.
func (m *Manager) Count() int {
return int(m.count.Load())
}

18
server/game/Ping.go Normal file
View File

@ -0,0 +1,18 @@
package game
import (
"net"
"server/core"
"server/packet"
)
// Ping is used as a heartbeat and latency check.
func Ping(data []byte, address *net.UDPAddr, server *core.Server) error {
server.Send(packet.Ping, data, address)
if Clients.Contains(address) {
Clients.Get(address).KeepAlive()
}
return nil
}

6
server/game/Player.go Normal file
View File

@ -0,0 +1,6 @@
package game
type Player struct {
Name string `json:"name"`
Position Vector3 `json:"position"`
}

7
server/game/Vector3.go Normal file
View File

@ -0,0 +1,7 @@
package game
type Vector3 struct {
X float32 `json:"x"`
Y float32 `json:"y"`
Z float32 `json:"z"`
}

3
server/go.mod Normal file
View File

@ -0,0 +1,3 @@
module server
go 1.21.6

View File

@ -1,3 +0,0 @@
class_name Cell
var player_list := [] as Array[Player]

View File

@ -1,31 +0,0 @@
extends Node
## Width of the grid in cells.
const width := 100
## Height of the grid in cells.
const height := 100
## The size of a single cell.
const cell_size := 10.0
var cells: Array[Cell]
func _init():
cells.resize(width * height)
for i in cells.size():
cells[i] = Cell.new()
func notify_cell_changed(player: Player, old_pos: Vector2i, new_pos: Vector2i):
print(player.name, " cell changed! ", old_pos, " ", new_pos)
add_player(player, new_pos)
remove_player(player, old_pos)
func add_player(player: Player, coords: Vector2i):
var cell := cells[coords.x + coords.y * height]
cell.player_list.append(player)
func remove_player(player: Player, coords: Vector2i):
var cell := cells[coords.x + coords.y * height]
cell.player_list.erase(player)

View File

@ -1,65 +0,0 @@
extends PacketHandler
enum {
SUCCESS = 0,
FAIL = 1,
}
## Player scene instantiated for each client.
@export var player_scene: PackedScene
func _ready():
%Server.set_handler(Packet.LOGIN, self)
func handle_packet(data: PackedByteArray, peer: PacketPeer):
var client = %Server.peer_to_client(peer)
if client.is_logged_in():
return
var data_string = data.get_string_from_utf8()
var login_request = JSON.parse_string(data_string)
if login_request.size() < 2:
login_fail(peer)
return
var username = login_request[0]
var password = login_request[1]
var account = %Database.get_account(username)
if account == null || account.password != password:
login_fail(peer)
return
client.auth_token = generate_auth_token()
client.account = account
spawn_player(client)
login_success(peer, client.auth_token)
func spawn_player(client: Client):
var player := player_scene.instantiate()
player.client = client
player.server = %Server
player.name = client.address
client.player = player
%Players.add_child(player)
func generate_auth_token() -> String:
var crypto = Crypto.new()
var buffer = crypto.generate_random_bytes(32)
return Marshalls.raw_to_base64(buffer)
func login_success(peer: PacketPeer, auth_token: String):
var buffer := StreamPeerBuffer.new()
buffer.put_8(Packet.LOGIN)
buffer.put_8(SUCCESS)
buffer.put_data(auth_token.to_ascii_buffer())
peer.put_packet(buffer.data_array)
func login_fail(peer: PacketPeer):
var buffer := StreamPeerBuffer.new()
buffer.put_8(Packet.LOGIN)
buffer.put_8(FAIL)
peer.put_packet(buffer.data_array)

View File

@ -1,13 +0,0 @@
extends PacketHandler
func _ready():
%Server.set_handler(Packet.PING, self)
func handle_packet(data: PackedByteArray, peer: PacketPeer):
var buffer := StreamPeerBuffer.new()
buffer.put_8(Packet.PING)
if data.size() > 0:
buffer.put_8(data[0])
peer.put_packet(buffer.data_array)

32
server/main.go Normal file
View File

@ -0,0 +1,32 @@
package main
import (
"fmt"
"server/core"
"server/game"
"server/packet"
"time"
)
func main() {
// Init server
server := core.New()
server.SetHandler(packet.Ping, game.Ping)
server.SetHandler(packet.Login, game.Login)
// Show statistics
last := time.Now()
go func() {
for {
if time.Now().After(last.Add(time.Second)) {
fmt.Printf("%d packets per second, %d clients\n", server.PacketCount(), game.Clients.Count())
last = time.Now()
server.ResetPacketCount()
}
}
}()
// Start listening
server.Run(4242)
}

8
server/packet/packet.go Normal file
View File

@ -0,0 +1,8 @@
package packet
const (
Ping = 1
Login = 2
Logout = 3
PlayerState = 10
)

View File

@ -1,24 +0,0 @@
class_name Player
extends CharacterBody3D
var client: Client
var server: Server
var cell: Vector2i
func _ready():
print("Server player spawned")
Grid.notify_cell_changed(self, cell, cell)
# var buffer := StreamPeerBuffer.new()
# buffer.put_8(Packet.STATE)
# server.broadcast(buffer.data_array)
func _physics_process(_delta):
move_and_slide()
update_grid()
func update_grid():
var new_cell := Vector2i(int(position.x / Grid.cell_size), int(position.z / Grid.cell_size))
if new_cell != cell:
Grid.notify_cell_changed(self, cell, new_cell)
cell = new_cell

View File

@ -1,14 +0,0 @@
[gd_scene load_steps=3 format=3 uid="uid://148x2hp1hjq0"]
[ext_resource type="Script" path="res://player/Player.gd" id="1_46xlc"]
[sub_resource type="CapsuleShape3D" id="CapsuleShape3D_y8kaq"]
radius = 0.25
height = 1.6
[node name="Player" type="CharacterBody3D"]
script = ExtResource("1_46xlc")
[node name="Collision" type="CollisionShape3D" parent="."]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.8, 0)
shape = SubResource("CapsuleShape3D_y8kaq")

View File

@ -1,38 +0,0 @@
; Engine configuration file.
; It's best edited using the editor UI and not directly,
; since the parameters that go here are not all obvious.
;
; Format:
; [section] ; section goes between []
; param=value ; assign values to parameters
config_version=5
[application]
config/name="Server"
run/main_scene="res://Server.tscn"
config/features=PackedStringArray("4.2", "Forward Plus")
run/max_fps=100
run/low_processor_mode_sleep_usec=1
[audio]
driver/driver="Dummy"
[autoload]
Grid="*res://grid/Grid.gd"
[debug]
settings/stdout/verbose_stdout=true
[editor]
run/main_run_args="--headless --text-driver Dummy"
[physics]
3d/run_on_separate_thread=true
common/physics_ticks_per_second=100