diff options
Diffstat (limited to 'game_server')
-rw-r--r-- | game_server/Cargo.toml | 13 | ||||
-rwxr-xr-x | game_server/build.sh | 21 | ||||
-rw-r--r-- | game_server/err | 73 | ||||
-rw-r--r-- | game_server/rbuild.sh | 2 | ||||
-rw-r--r-- | game_server/src/backend_connection.rs | 31 | ||||
-rw-r--r-- | game_server/src/gameserver.rs | 147 | ||||
-rw-r--r-- | game_server/src/group.rs | 8 | ||||
-rw-r--r-- | game_server/src/lobby.rs | 35 | ||||
-rw-r--r-- | game_server/src/main.rs | 20 | ||||
-rw-r--r-- | game_server/src/test_group.rs | 28 | ||||
-rw-r--r-- | game_server/src/ws_test.html | 66 |
11 files changed, 444 insertions, 0 deletions
diff --git a/game_server/Cargo.toml b/game_server/Cargo.toml new file mode 100644 index 0000000..97c0e77 --- /dev/null +++ b/game_server/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "game-server" +version = "0.1.0" +authors = ["natrixaeria", "truedoctor"] +edition = "2018" +description = "A general game server for connections to web clients. Currently (on the way to) deploying a skribbl.io like game." + +[dependencies] +log = "0.4" +pretty_env_logger = "0.3" +reqwest = "0.9" +websocket = "0.22" +hyper = "0.10" diff --git a/game_server/build.sh b/game_server/build.sh new file mode 100755 index 0000000..1eb61a1 --- /dev/null +++ b/game_server/build.sh @@ -0,0 +1,21 @@ +#!/usr/bin/env sh + +case $1 in + ("") + if rustup run stable cargo --color always build; then + echo build success! + RUST_LOG=debug target/debug/game-server + else + echo build failed! + fi + ;; + -r) + sh build.sh &> err && cat err | tac + ;; + -c) + rustup run stable cargo clean + ;; + *) + echo invalid argument + ;; +esac diff --git a/game_server/err b/game_server/err new file mode 100644 index 0000000..6ca8a6f --- /dev/null +++ b/game_server/err @@ -0,0 +1,73 @@ +[0m[0m[1m[32m Compiling[0m game-server v0.1.0 (/home/jan/projects/DiscoBot/game_server) +[0m[1m[38;5;9merror[E0277][0m[0m[1m: the trait bound `(): futures::future::Future` is not satisfied[0m +[0m [0m[0m[1m[38;5;12m--> [0m[0msrc/backend_connection.rs:32:24[0m +[0m [0m[0m[1m[38;5;12m|[0m +[0m[1m[38;5;12m32[0m[0m [0m[0m[1m[38;5;12m| [0m[0m hyper::rt::run(hyper::rt::lazy(|| {[0m +[0m [0m[0m[1m[38;5;12m| [0m[0m [0m[0m[1m[38;5;9m^^^^^^^^^^^^^^^[0m[0m [0m[0m[1m[38;5;9mthe trait `futures::future::Future` is not implemented for `()`[0m +[0m [0m[0m[1m[38;5;12m|[0m +[0m [0m[0m[1m[38;5;12m= [0m[0m[1mnote[0m[0m: required because of the requirements on the impl of `futures::future::IntoFuture` for `()`[0m +[0m [0m[0m[1m[38;5;12m= [0m[0m[1mnote[0m[0m: required by `futures::future::lazy::lazy`[0m + +[0m[1m[38;5;9merror[E0599][0m[0m[1m: no method named `wait` found for type `std::result::Result<futures::future::map_err::MapErr<futures::future::map::Map<hyper::client::ResponseFuture, [closure@src/backend_connection.rs:54:34: 54:68]>, [closure@src/backend_connection.rs:55:38: 55:73]>, http::uri::InvalidUri>` in the current scope[0m +[0m [0m[0m[1m[38;5;12m--> [0m[0msrc/backend_connection.rs:56:24[0m +[0m [0m[0m[1m[38;5;12m|[0m +[0m[1m[38;5;12m56[0m[0m [0m[0m[1m[38;5;12m| [0m[0m }).wait();[0m +[0m [0m[0m[1m[38;5;12m| [0m[0m [0m[0m[1m[38;5;9m^^^^[0m +[0m [0m[0m[1m[38;5;12m|[0m +[0m [0m[0m[1m[38;5;12m= [0m[0m[1mnote[0m[0m: the method `wait` exists but the following trait bounds were not satisfied:[0m +[0m `&mut std::result::Result<futures::future::map_err::MapErr<futures::future::map::Map<hyper::client::ResponseFuture, [closure@src/backend_connection.rs:54:34: 54:68]>, [closure@src/backend_connection.rs:55:38: 55:73]>, http::uri::InvalidUri> : futures::future::Future`[0m + +[0m[1m[38;5;9merror[E0308][0m[0m[1m: mismatched types[0m +[0m [0m[0m[1m[38;5;12m--> [0m[0msrc/backend_connection.rs:58:17[0m +[0m [0m[0m[1m[38;5;12m|[0m +[0m[1m[38;5;12m58[0m[0m [0m[0m[1m[38;5;12m| [0m[0m res[0m +[0m [0m[0m[1m[38;5;12m| [0m[0m [0m[0m[1m[38;5;9m^^^[0m[0m [0m[0m[1m[38;5;9mexpected (), found enum `std::result::Result`[0m +[0m [0m[0m[1m[38;5;12m|[0m +[0m [0m[0m[1m[38;5;12m= [0m[0m[1mnote[0m[0m: expected type `[0m[0m[1m()[0m[0m`[0m +[0m found type `[0m[0m[1mstd::result::Result<http::response::Response<hyper::body::body::Body>, http::uri::InvalidUri>[0m[0m`[0m + +[0m[1m[38;5;9merror[E0277][0m[0m[1m: the trait bound `(): futures::future::Future` is not satisfied[0m +[0m [0m[0m[1m[38;5;12m--> [0m[0msrc/backend_connection.rs:32:24[0m +[0m [0m[0m[1m[38;5;12m|[0m +[0m[1m[38;5;12m32[0m[0m [0m[0m[1m[38;5;12m| [0m[0m [0m[0m hyper::rt::run(hyper::rt::lazy(|| {[0m +[0m [0m[0m[1m[38;5;12m| [0m[0m [0m[0m[1m[38;5;9m________________________^[0m +[0m[1m[38;5;12m33[0m[0m [0m[0m[1m[38;5;12m| [0m[0m[1m[38;5;9m|[0m[0m [0m[0m let client = hyper::Client::builder()[0m +[0m[1m[38;5;12m34[0m[0m [0m[0m[1m[38;5;12m| [0m[0m[1m[38;5;9m|[0m[0m [0m[0m .build::<_, hyper::Body>([0m +[0m[1m[38;5;12m35[0m[0m [0m[0m[1m[38;5;12m| [0m[0m[1m[38;5;9m|[0m[0m [0m[0m HttpsConnector::new(4).unwrap()[0m +[0m[1m[38;5;12m...[0m[0m [0m[0m[1m[38;5;9m|[0m +[0m[1m[38;5;12m59[0m[0m [0m[0m[1m[38;5;12m| [0m[0m[1m[38;5;9m|[0m[0m [0m[0m }[0m +[0m[1m[38;5;12m60[0m[0m [0m[0m[1m[38;5;12m| [0m[0m[1m[38;5;9m|[0m[0m [0m[0m }));[0m +[0m [0m[0m[1m[38;5;12m| [0m[0m[1m[38;5;9m|__________^[0m[0m [0m[0m[1m[38;5;9mthe trait `futures::future::Future` is not implemented for `()`[0m +[0m [0m[0m[1m[38;5;12m|[0m +[0m [0m[0m[1m[38;5;12m= [0m[0m[1mnote[0m[0m: required because of the requirements on the impl of `futures::future::IntoFuture` for `()`[0m +[0m [0m[0m[1m[38;5;12m= [0m[0m[1mnote[0m[0m: required by `futures::future::lazy::Lazy`[0m + +[0m[1m[38;5;9merror[E0277][0m[0m[1m: the trait bound `(): futures::future::Future` is not satisfied[0m +[0m [0m[0m[1m[38;5;12m--> [0m[0msrc/backend_connection.rs:32:9[0m +[0m [0m[0m[1m[38;5;12m|[0m +[0m[1m[38;5;12m32[0m[0m [0m[0m[1m[38;5;12m| [0m[0m hyper::rt::run(hyper::rt::lazy(|| {[0m +[0m [0m[0m[1m[38;5;12m| [0m[0m [0m[0m[1m[38;5;9m^^^^^^^^^^^^^^[0m[0m [0m[0m[1m[38;5;9mthe trait `futures::future::Future` is not implemented for `()`[0m +[0m [0m[0m[1m[38;5;12m|[0m +[0m [0m[0m[1m[38;5;12m= [0m[0m[1mnote[0m[0m: required because of the requirements on the impl of `futures::future::IntoFuture` for `()`[0m +[0m [0m[0m[1m[38;5;12m= [0m[0m[1mnote[0m[0m: required by `hyper::rt::run`[0m + +[0m[1m[38;5;9merror[E0063][0m[0m[1m: missing field `res_receiver` in initializer of `backend_connection::BackendConnection`[0m +[0m [0m[0m[1m[38;5;12m--> [0m[0msrc/backend_connection.rs:62:9[0m +[0m [0m[0m[1m[38;5;12m|[0m +[0m[1m[38;5;12m62[0m[0m [0m[0m[1m[38;5;12m| [0m[0m BackendConnection {[0m +[0m [0m[0m[1m[38;5;12m| [0m[0m [0m[0m[1m[38;5;9m^^^^^^^^^^^^^^^^^[0m[0m [0m[0m[1m[38;5;9mmissing `res_receiver`[0m + +[0m[1m[38;5;9merror[E0609][0m[0m[1m: no field `request_sender` on type `&backend_connection::BackendConnection`[0m +[0m [0m[0m[1m[38;5;12m--> [0m[0msrc/backend_connection.rs:69:14[0m +[0m [0m[0m[1m[38;5;12m|[0m +[0m[1m[38;5;12m69[0m[0m [0m[0m[1m[38;5;12m| [0m[0m self.request_sender.send([0m +[0m [0m[0m[1m[38;5;12m| [0m[0m [0m[0m[1m[38;5;9m^^^^^^^^^^^^^^[0m + +[0m[1m[38;5;9merror[0m[0m[1m: aborting due to 7 previous errors[0m + +[0m[1mSome errors have detailed explanations: E0063, E0277, E0308, E0599, E0609.[0m +[0m[1mFor more information about an error, try `rustc --explain E0063`.[0m +[0m[0m[1m[31merror:[0m Could not compile `game-server`. + +To learn more, run the command again with --verbose. +build failed! diff --git a/game_server/rbuild.sh b/game_server/rbuild.sh new file mode 100644 index 0000000..22b10b5 --- /dev/null +++ b/game_server/rbuild.sh @@ -0,0 +1,2 @@ +#!/bin/bash +sh build.sh &> err && cat err | tac diff --git a/game_server/src/backend_connection.rs b/game_server/src/backend_connection.rs new file mode 100644 index 0000000..9307c4a --- /dev/null +++ b/game_server/src/backend_connection.rs @@ -0,0 +1,31 @@ +use reqwest::{Response, Client, Url, UrlError, Error as ReqError}; + +pub struct BackendConnection { + host: String, + client: Client, + last_response: Option<Result<Response, ReqError>> +} + +impl BackendConnection { + pub fn new(host: &str) -> Self { + BackendConnection { + host: host.to_string(), + client: Client::new(), + last_response: None + } + } + + pub fn request(&mut self, location: &str) -> Result<(), UrlError> { + Ok(self.last_response = + Some(self.client.get(Url::parse(&format!("{}{}", self.host, location))?) + .send())) + } + + pub fn get_response(&self) -> &Option<Result<Response, ReqError>> { + &self.last_response + } + + pub fn host_name<'a>(&'a self) -> &'a str { + &self.host + } +} diff --git a/game_server/src/gameserver.rs b/game_server/src/gameserver.rs new file mode 100644 index 0000000..9334a27 --- /dev/null +++ b/game_server/src/gameserver.rs @@ -0,0 +1,147 @@ +use websocket::{OwnedMessage, + sync::Server, + client::sync::Client, + server::{NoTlsAcceptor, InvalidConnection, + sync::AcceptResult}}; +use std::net::{SocketAddr, ToSocketAddrs, TcpStream}; +use std::sync::mpsc; +use std::sync::mpsc::{Sender, Receiver}; +use super::lobby::Lobby; +use super::backend_connection::BackendConnection; + +const PROTOCOL: &str = "tuesday"; + +type Token = u32; + +#[derive(Debug)] +pub enum GameServerError { + BindError(std::io::Error), + HandshakeRequestError, + InvalidProtocolError, + AcceptError(std::io::Error) +} + +pub struct GameServer { + addr: SocketAddr, + lobby: Lobby, + backend: BackendConnection, +} + +pub struct GameClient { + addr: SocketAddr, + client: Client<TcpStream>, +} + +impl GameClient { + fn from_raw(client: Client<TcpStream>) -> Result<Self, ()> { + let addr = client.peer_addr().map_err(|_| ())?; + info!("got a client connection from: {}", addr); + Ok(GameClient { + addr, + client, + }) + } + + fn require_token(&mut self) -> Option<Token> { + let message = self.client + .recv_message() + .ok()?; + if let OwnedMessage::Text(text) = message { + text.parse::<Token>().ok() + } else { + None + } + } +} + +type ClientConnection = Result<GameClient, GameServerError>; + +impl GameServer { + pub fn new<T: ToSocketAddrs>(addr: T) -> Self { + let addr = addr.to_socket_addrs().unwrap().next().unwrap(); + debug!("ws address: {}", addr); + info!("create lobby"); + let lobby = Lobby::new(); + let backend = BackendConnection::new("https://kobert.dev"); + info!("got a C# backend connection"); + GameServer { + addr, + lobby, + backend, + } + } + + pub fn run(&self) -> Result<(), GameServerError> { + let reader = self.read_clients(); + loop { + let mut connection = reader.recv().unwrap()?; + self.add_client(connection); + } + Ok(()) + } + + fn add_client(&self, mut client: GameClient) { + std::thread::spawn(move || { + println!("Token: {:?}", client.require_token()); + loop { std::thread::sleep(std::time::Duration::from_millis(100)); } + }); + } + + fn read_clients(&self) -> Receiver<ClientConnection> { + let (s, r): (Sender<ClientConnection>, Receiver<ClientConnection>) + = mpsc::channel(); + let addr = self.addr; + std::thread::spawn(move || { + let result = Self::handle_requests(addr, &s).or_else(|e| s.send(Err(e))); + }); + r + } + + fn handle_requests(addr: SocketAddr, s: &Sender<ClientConnection>) -> Result<(), GameServerError> { + let server = match Server::<NoTlsAcceptor>::bind(addr) { + Ok(v) => v, + Err(e) => { + error!("websocket binding error"); + Err(GameServerError::BindError(e))? + }, + }; + info!("webserver is being launched"); + for req in server { + s.send(Ok(Self::handle_request(req)?)).unwrap(); + } + info!("webserver is being shut down"); + Ok(()) + } + + fn handle_request(req: AcceptResult<TcpStream>) -> ClientConnection { + match req { + Ok(req) => { + if !req.protocols().contains(&PROTOCOL.to_string()) { + warn!("a client tried to connect without {} protocol", PROTOCOL); + req.reject().unwrap(); + Err(GameServerError::InvalidProtocolError) + } else { + match req.use_protocol(PROTOCOL).accept() { + Ok(client) => { + match GameClient::from_raw(client) { + Ok(client) => Ok(client), + Err(_) => { + error!("could not create a client"); + Err(GameServerError::HandshakeRequestError) + } + } + }, + Err((_, e)) => { + warn!("client handshake failed"); + Err(GameServerError::AcceptError(e)) + } + } + } + }, + Err(e) => { + warn!("invalid client request"); + Err(GameServerError::HandshakeRequestError) + } + } + } +} diff --git a/game_server/src/group.rs b/game_server/src/group.rs new file mode 100644 index 0000000..55e4fbf --- /dev/null +++ b/game_server/src/group.rs @@ -0,0 +1,8 @@ +pub type GroupId = u32; + +pub trait Group { + fn id(&self) -> GroupId; + fn name(&self) -> String; + + fn run(&self); +} diff --git a/game_server/src/lobby.rs b/game_server/src/lobby.rs new file mode 100644 index 0000000..fe3bdee --- /dev/null +++ b/game_server/src/lobby.rs @@ -0,0 +1,35 @@ +use std::collections::HashMap; + +use super::group::{Group, GroupId}; + +pub struct Lobby { + groups: HashMap<GroupId, Box<Group>>, +} + +impl Lobby { + pub fn new() -> Lobby { + Self { + groups: HashMap::new(), + } + } + + pub fn add_group(&mut self, group: Box<Group>) { + self.groups.insert(group.id(), group); + } + + pub fn iter<'a>(&'a self) -> GroupIterator<'a> { + GroupIterator { groups: self.groups.values() } + } +} + +pub struct GroupIterator<'a> { + groups: std::collections::hash_map::Values<'a, GroupId, Box<Group>> +} + +impl<'a> Iterator for GroupIterator<'a> { + type Item = &'a Box<Group>; + + fn next(&mut self) -> Option<Self::Item> { + self.groups.next() + } +} diff --git a/game_server/src/main.rs b/game_server/src/main.rs new file mode 100644 index 0000000..e129283 --- /dev/null +++ b/game_server/src/main.rs @@ -0,0 +1,20 @@ +mod group; +mod test_group; +mod lobby; +mod gameserver; +mod backend_connection; + +#[macro_use] extern crate log; +use pretty_env_logger; + +use backend_connection::BackendConnection; + +fn main() { + pretty_env_logger::init(); + + let addr = ("127.0.0.1", 5001); + info!("create game server on {:?}", addr); + let gameserver = gameserver::GameServer::new(addr); + gameserver.run().unwrap(); + +} diff --git a/game_server/src/test_group.rs b/game_server/src/test_group.rs new file mode 100644 index 0000000..bd570e3 --- /dev/null +++ b/game_server/src/test_group.rs @@ -0,0 +1,28 @@ +use super::group::{Group, GroupId}; + +pub struct TestGroup { + id: GroupId, + name: String, +} + +impl Group for TestGroup { + fn id(&self) -> GroupId { + self.id + } + + fn name(&self) -> String { + self.name.clone() + } + + fn run(&self) { + let id = self.id; + let name = self.name.to_owned(); + std::thread::spawn(move || /*loop { println!("> group nr.{} wishes you: '{}'", id, name) }*/()); + } +} + +impl TestGroup { + pub fn new(id: GroupId, name: String) -> Self { + TestGroup { id, name } + } +} diff --git a/game_server/src/ws_test.html b/game_server/src/ws_test.html new file mode 100644 index 0000000..ea259b7 --- /dev/null +++ b/game_server/src/ws_test.html @@ -0,0 +1,66 @@ +<!doctype html> +<html> + <head> + <title>WS Test</title> + <style> +.b { + border-bottom: 1px solid black; +} + </style> + </head> + <body style='background: black; color: white'> + <div id='cons'>connected</div><br> + <button onclick='test_connection()'>Launch</button><br> + <span>Server address: </span><input id='addr'></input> + <div id='chat' style='background: rgb(20, 20, 20); padding-left: 20px; margin: 40px' /> + </body> + <script> +function get_addr() { + return document.getElementById('addr').value; +} + +function test_connection() { + let a = 'ws://' + get_addr(); + add_text('create a new connection at "' + a + '"'); + const ws = new WebSocket(a, 'tuesday'); + ws.addEventListener('open', function (event) { + add_text('connection established'); + toggle_connected(true); + ws.send('1230123'); + }); + ws.addEventListener('error', function (event) { + add_text('ws error occured: "' + event + '"'); + toggle_connected(false); + }); + ws.addEventListener('close', function (event) { + add_text('ws is closed now'); + toggle_connected(false); + }); + ws.addEventListener('message', function (event) { + add_text('got ws message: ' + event.data); + }); +} + +function add_text(text, color='white') { + let c = document.getElementById('chat'); + let n = document.createElement('span'); + n.setAttribute('class', 'b'); + n.style = 'color: ' + color; + n.textContent = (new Date()).toTimeString().substring(0, 8) + '|> '+ text; + c.appendChild(n); + c.appendChild(document.createElement('br')); +} +function toggle_connected(con) { + let c = document.getElementById('cons'); + if (con) { + c.style = 'background: green' + c.textContent = 'connected'; + } else { + c.style = 'background: red' + c.textContent = 'not connected'; + } +} +toggle_connected(false); +add_text("JS loaded"); + </script> +</html> |