Files
Syncplay/server.odin
T

197 lines
4.7 KiB
Odin
Raw Normal View History

2026-08-11 10:55:02 +02:00
package main
import "base:runtime"
import "core:fmt"
import "core:net"
import "core:sync"
import "core:thread"
import "core:sys/posix"
MAX_SERVER_CLIENTS :: 16
Server_State :: struct {
mutex: sync.Mutex,
clients: [MAX_SERVER_CLIENTS]Server_Client,
next_id: u64,
}
Server_Client :: struct {
state: ^Server_State,
socket: net.TCP_Socket,
endpoint: net.Endpoint,
id: u64,
active: bool,
}
server_disconnect_locked :: proc(client: ^Server_Client, reason: string) {
if !client.active {
return
}
client.active = false
_ = net.shutdown(client.socket, .Both)
net.close(client.socket)
fmt.printfln("server: client %d disconnected (%s)", client.id, reason)
}
server_broadcast :: proc(sender: ^Server_Client, event: Playback_Event) {
state := sender.state
sync.mutex_lock(&state.mutex)
defer sync.mutex_unlock(&state.mutex)
if !sender.active {
return
}
outbound_event := event
outbound_event.client_id = sender.id
log_playback_event("server", sender.id, outbound_event)
line := encode_protocol_event(outbound_event)
defer delete(line)
for &client in state.clients {
if !client.active || client.id == sender.id {
continue
}
written, send_err := net.send_tcp(client.socket, transmute([]byte)line)
if send_err != nil || written != len(line) {
server_disconnect_locked(&client, "send failed")
}
}
}
server_client_loop :: proc(data: rawptr) {
defer runtime.default_temp_allocator_destroy(auto_cast context.temp_allocator.data)
client := cast(^Server_Client)data
framer: Line_Framer
framer_init(&framer)
defer framer_destroy(&framer)
buf: [2048]byte
disconnect_reason := "connection closed"
for {
count, recv_err := net.recv_tcp(client.socket, buf[:])
if recv_err != nil {
disconnect_reason = "receive failed"
break
}
if count == 0 {
break
}
frames, frame_err := framer_push(&framer, buf[:count])
if frame_err != .None {
destroy_frames(frames)
disconnect_reason = "oversized message"
break
}
valid := true
for frame in frames {
if len(frame) == 0 {
continue
}
event, protocol_err := parse_protocol_event(transmute([]byte)frame)
if protocol_err != .None {
fmt.printfln(
"server: client %d sent invalid message: %s",
client.id,
protocol_error_string(protocol_err),
)
disconnect_reason = "invalid message"
valid = false
break
}
server_broadcast(client, event)
}
destroy_frames(frames)
if !valid {
break
}
}
sync.mutex_lock(&client.state.mutex)
server_disconnect_locked(client, disconnect_reason)
sync.mutex_unlock(&client.state.mutex)
}
run_server :: proc(options: Options) -> bool {
address, ok := net.parse_ip4_address(options.host)
if !ok {
fmt.eprintfln("server: host must be an IPv4 address: %s", options.host)
return false
}
listener, listen_err := net.listen_tcp(net.Endpoint{address = address, port = options.port})
if listen_err != nil {
fmt.eprintfln("server: could not listen on %s:%d: %v", options.host, options.port, listen_err)
return false
}
register_server_listener(posix.FD(net.Socket(listener)))
defer clear_server_listener()
state := new(Server_State)
fmt.printfln("server: listening on %s:%d", options.host, options.port)
for {
socket, endpoint, accept_err := net.accept_tcp(listener)
if accept_err != nil {
if should_shutdown() {
fmt.println("server: shutdown requested")
return true
}
fmt.eprintfln("server: accept failed: %v", accept_err)
net.close(listener)
return false
}
client: ^Server_Client
client_ready := false
sync.mutex_lock(&state.mutex)
for &candidate in state.clients {
if !candidate.active {
client = &candidate
break
}
}
if client != nil {
state.next_id += 1
client^ = Server_Client{
state = state,
socket = socket,
endpoint = endpoint,
id = state.next_id,
active = true,
}
welcome := encode_welcome(client.id)
welcome_length := len(welcome)
written, welcome_err := net.send_tcp(client.socket, transmute([]byte)welcome)
delete(welcome)
if welcome_err == nil && written == welcome_length {
client_ready = true
} else {
server_disconnect_locked(client, "could not send welcome")
}
}
sync.mutex_unlock(&state.mutex)
if client == nil {
fmt.eprintln("server: connection rejected: client limit reached")
net.close(socket)
continue
}
if !client_ready {
continue
}
remote := net.endpoint_to_string(endpoint)
fmt.printfln("server: client %d connected from %s", client.id, remote)
if thread.create_and_start_with_data(
rawptr(client),
server_client_loop,
init_context = context,
self_cleanup = true,
) == nil {
sync.mutex_lock(&state.mutex)
server_disconnect_locked(client, "could not start client thread")
sync.mutex_unlock(&state.mutex)
}
}
}