improved communication handling on the server

This commit is contained in:
troido 2019-09-24 22:57:37 +02:00
parent 10bdb33255
commit 84d899aa45
5 changed files with 152 additions and 78 deletions

View File

@ -79,9 +79,9 @@ class Client:
self.close("error: name is already taken") self.close("error: name is already taken")
return return
if error == "invalidname": if error == "invalidname":
self.close("error: "+ msg[2]) self.close("Invalid name error: "+ msg[2:])
return return
self.log(error) self.log(" ".join(msg[1:]))
if msgType == 'field': if msgType == 'field':
field = msg[1] field = msg[1]
fieldWidth = field['width'] fieldWidth = field['width']

View File

@ -17,7 +17,7 @@ class Game:
def __init__(self, socketType, worldFile=None, saveDir=None, saveInterval=1): def __init__(self, socketType, worldFile=None, saveDir=None, saveInterval=1):
self.server = gameserver.GameServer(self, socketType) self.server = gameserver.GameServer(socketType)
worldLoader = worldloader.WorldLoader(saveDir) worldLoader = worldloader.WorldLoader(saveDir)
roomLoader = roomloader.RoomLoader(worldFile, os.path.join(saveDir, "rooms")) roomLoader = roomloader.RoomLoader(worldFile, os.path.join(saveDir, "rooms"))

View File

@ -4,16 +4,21 @@
import json import json
import queue import queue
import string import string
import threading
from . import view from . import view
from . import socketserver as server from . import socketserver as server
from . import player from . import player
import re
nameRegex = re.compile("(~|\w)\w*")
class GameServer: class GameServer:
def __init__(self, game, socketType): def __init__(self, socketType):
self.serv = server.Server(socketType, self.newConnection, self.receive, self.close) self.serv = server.Server(socketType, self.newConnection, self.receive, self.close)
@ -21,12 +26,13 @@ class GameServer:
self.players = {} self.players = {}
self.game = game
self.messages = queue.Queue() self.messages = queue.Queue()
def start(self, address): def start(self, address):
self.serv.start(address)
self.listener = threading.Thread(target=self.serv.listen, daemon=True, args=(address,))
self.listener.start()
def sendState(self, view): def sendState(self, view):
@ -43,49 +49,54 @@ class GameServer:
def receive(self, n, data): def receive(self, n, data):
try: try:
data = json.loads(data.decode('utf-8')) try:
if isinstance(data[0], str): data = json.loads(data.decode('utf-8'))
data = [data] except json.JSONDecodeError:
for msg in data: self.error(n, "invalidmessage", "Invalid JSON")
msgType = msg[0] return
if msgType == "name": if not isinstance(data, list) or len(data) != 2:
name = msg[1] self.error(n, "invalidmessage", "Message must be a json list of length 2")
return
if name in self.players: msg = data
self.error(n, "nametaken", "another connection to this player already exists") msgType = msg[0]
return if msgType == "name":
if len(name) < 1: name = msg[1]
self.error(n, "invalidname", "name needs at least one character")
return if len(name) < 1:
if name[0] not in string.ascii_letters + string.digits: self.error(n, "invalidname", "name needs at least one character")
if name[0] != "~":
self.error(n, "invalidname", "custom name must start with an alphanumeric character")
return
if name[1:] != self.serv.getUsername(n):
self.error(n, "invalidname", "tildenames are only available on unix sockets and when the rest of the name equals the username")
return
if any(char not in string.ascii_letters + string.digits + string.punctuation for char in name):
self.error(n, "invalidname", "names can only consist of printable ascii characters")
return
self.connections[n] = name
self.players[name] = n
self.messages.put(("join", name))
print("new player: "+name)
self.broadcast("{} has connected".format(name), "connect")
return return
elif msgType == "input": if len(bytes(name, "utf-8")) > 256:
if n in self.connections: self.error(n, "invalidname", "name may not be longer than 256 utf8 bytes")
self.messages.put(("input", self.connections[n], msg[1])) return
elif msgType == "chat": if nameRegex.match(name) is None:
if n in self.connections: self.error(n, "invalidname", "Name must match the regex: {}".format(nameRegex.pattern))
name = self.connections[n] return
message = name + ": " + msg[1] if name[0] == "~" and name[1:] != self.serv.getUsername(n):
print(message) self.error(n, "invalidname", "tildenames are only available on unix sockets and when the rest of the name equals the username")
self.broadcast(message, "chat") return
if name in self.players:
self.error(n, "nametaken", "another connection to this player already exists")
return
self.connections[n] = name
self.players[name] = n
self.messages.put(("join", name))
print("new player: "+name)
self.broadcast("{} has connected".format(name), "connect")
elif msgType == "input":
if n in self.connections:
self.messages.put(("input", self.connections[n], msg[1]))
elif msgType == "chat":
if n in self.connections:
name = self.connections[n]
if not msg[1].isprintable():
self.error("invalidmessage", "Chat message may only contain printable unicode characters")
message = name + ": " + msg[1]
print(message)
self.broadcast(message, "chat")
except Exception as e: except Exception as e:
print(e) print(e)
self.error(n, "invalidmessage", repr(e)) self.error(n, "invalidmessage", "An unknown error occured in handling the message")
def error(self, n, errtype, *data): def error(self, n, errtype, *data):
@ -109,7 +120,11 @@ class GameServer:
def readMessages(self): def readMessages(self):
m = [] m = []
while not self.messages.empty(): while not self.messages.empty():
m.append(self.messages.get()) try:
message = self.messages.get_nowait()
except queue.Empty:
return m
m.append(message)
return m return m

View File

@ -1,12 +1,38 @@
import os import os
import socket import socket
import sys import sys
import threading
import struct import struct
import selectors
from asciifarm.common.tcommunicate import send, receive from asciifarm.common.tcommunicate import send, receive
class _BytesBuffer:
def __init__(self):
self.buff = bytearray()
self.msglen = None
def addBytes(self, data):
self.buff.extend(data)
def readMessages(self):
messages = []
while True:
if self.msglen is None:
if len(self.buff) < 4:
break
header = self.buff[:4]
self.msglen = int.from_bytes(header, byteorder="big")
self.buff = self.buff[4:]
elif len(self.buff) >= self.msglen:
messages.append(self.buff[:self.msglen])
self.buff = self.buff[self.msglen:]
self.msglen = None
else:
break
return messages
# Class to open a TCP Socket # Class to open a TCP Socket
# will execute callback functions on new connections, closing connections and received messages # will execute callback functions on new connections, closing connections and received messages
# also provides a send function # also provides a send function
@ -27,9 +53,10 @@ class Server:
self.onConnection = onConnection self.onConnection = onConnection
self.onMessage = onMessage self.onMessage = onMessage
self.onConnectionClose = onConnectionClose self.onConnectionClose = onConnectionClose
self.sel = selectors.DefaultSelector()
def start(self, address): def listen(self, address):
print("starting {} socket server on address {}".format(self.socketType, address)) print("starting {} socket server on address {}".format(self.socketType, address))
try: try:
self.sock.bind(address) self.sock.bind(address)
@ -42,32 +69,52 @@ class Server:
self.sock.listen() self.sock.listen()
self.listener = threading.Thread(target=self._listen, daemon=True) self.sock.setblocking(False)
self.listener.start()
self.sel.register(self.sock, selectors.EVENT_READ, "ACCEPT")
def _listen(self): self.connections = {}
self.connections = set()
print("listening") print("listening")
while True: while True:
connection, client_address = self.sock.accept() events = self.sel.select()
listener = threading.Thread(target=self._listenCon, args=(connection,), daemon=True) for key, mask in events:
listener.start() if key.data == "ACCEPT":
sock = key.fileobj
connection, client_address = sock.accept()
connection.setblocking(False)
self.sel.register(connection, selectors.EVENT_READ, "RECEIVE")
self.connections[connection] = _BytesBuffer()
self.onConnection(connection)
elif key.data == "RECEIVE":
connection = key.fileobj
data = connection.recv(4096)
if data:
buff = self.connections[connection]
buff.addBytes(data)
for message in buff.readMessages():
self.onMessage(connection, message)
else:
del self.connections[connection]
self.onConnectionClose(connection)
#listener = threading.Thread(target=self._listenCon, args=(connection,), daemon=True)
#listener.start()
def _listenCon(self, connection): #def _listenCon(self, connection):
self.connections.add(connection) #self.connections.add(connection)
self.onConnection(connection) #self.onConnection(connection)
data = receive(connection) #data = receive(connection)
while data: #while data:
self.onMessage(connection, data) #self.onMessage(connection, data)
try: #try:
data = receive(connection) #data = receive(connection)
except socket.error: #except socket.error:
break #break
if not len(data): #if not len(data):
break #break
self.connections.discard(connection) #self.connections.discard(connection)
self.onConnectionClose(connection) #self.onConnectionClose(connection)
def getUsername(self, connection): def getUsername(self, connection):
@ -83,9 +130,11 @@ class Server:
def send(self, connection, msg): def send(self, connection, msg):
try: try:
send(connection, msg) length = len(msg)
header = length.to_bytes(4, byteorder="big")
connection.sendall(header + msg)
except Exception: except Exception:
self.connections.discard(connection) del self.connections[connection]
self.onConnectionClose(connection) self.onConnectionClose(connection)
print("failed to send to client") print("failed to send to client")

View File

@ -37,7 +37,7 @@ For example, the whole message could look like this:
## Client to server messages ## Client to server messages
Currently there are two types of messages that the client can send to the server: 'name' and 'input' Currently there are three types of messages that the client can send to the server: 'name', 'input' and 'chat'
### 'name' messages ### 'name' messages
@ -47,7 +47,7 @@ This message must be sent before any other communication.
Example message: `["name", "troido"]` Example message: `["name", "troido"]`
If a player with that name did not exist yet, it will be created and the connection will be connecte that player. If a player with that name did not exist yet, it will be created and the connection will be connected that player.
If a player with that name does exist, but it no other connection is currenty connected to that player, the connection will be connected to that player too. If a player with that name does exist, but it no other connection is currenty connected to that player, the connection will be connected to that player too.
@ -64,10 +64,18 @@ The first item in the list is the type of command, later items are arguments
Example message: `["input", ["move", "east"]]` Example message: `["input", ["move", "east"]]`
### 'chat' messages
'chat' messages are used to send messages to all connections, without going through the world first.
The value of this message is the string to send.
This string may only contain printable characters.
Example message: `["chat", "hello, other asciifarm players"]`
## Server to client messages ## Server to client messages
There are 5 types of messages that the server can send to the client: 'field', 'inventory', 'health', 'ground' and 'error' There are several types of messages that the server can send to the client: 'field', 'inventory', 'health', 'ground' and 'error'
Currently, 'chanchedcells' and 'info' messages are always sent in the same dictionary. Currently, 'chanchedcells' and 'info' messages are always sent in the same dictionary.
If there is a 'field' message, it will also be sent in that dictionary. If there is a 'field' message, it will also be sent in that dictionary.
@ -75,7 +83,9 @@ If there is a 'field' message, it will also be sent in that dictionary.
### 'error' messages ### 'error' messages
'error' messages are sent when a player tries to connect with a name that is already connected or when the input is wrong. 'error' messages are sent when a player tries to connect with a name that is already connected or when the input is wrong.
Wrong input is not always guaranteed to give an error message. The first value is a string to give the type of error. Wrong input is not always guaranteed to give an error message.
The first value is a string to give the type of error.
The message may have more data to explain the error.
Example message: `["error", "nametaken"]` Example message: `["error", "nametaken"]`
@ -90,7 +100,7 @@ It has the following properties:
- field: a 1 dimensional array (length: width*height) of integers representing the sprites in all cells of the room. - field: a 1 dimensional array (length: width*height) of integers representing the sprites in all cells of the room.
- mapping: an array or dictionary of spritenames. The values in field are the indices for the mapping. - mapping: an array or dictionary of spritenames. The values in field are the indices for the mapping.
Only one sprite per cell is sent: the one with the larges height. Only one sprite per cell is sent: the one with the largest height.
Example message: Example message: