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")
return
if error == "invalidname":
self.close("error: "+ msg[2])
self.close("Invalid name error: "+ msg[2:])
return
self.log(error)
self.log(" ".join(msg[1:]))
if msgType == 'field':
field = msg[1]
fieldWidth = field['width']

View File

@ -17,7 +17,7 @@ class Game:
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)
roomLoader = roomloader.RoomLoader(worldFile, os.path.join(saveDir, "rooms"))

View File

@ -4,16 +4,21 @@
import json
import queue
import string
import threading
from . import view
from . import socketserver as server
from . import player
import re
nameRegex = re.compile("(~|\w)\w*")
class GameServer:
def __init__(self, game, socketType):
def __init__(self, socketType):
self.serv = server.Server(socketType, self.newConnection, self.receive, self.close)
@ -21,12 +26,13 @@ class GameServer:
self.players = {}
self.game = game
self.messages = queue.Queue()
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):
@ -43,49 +49,54 @@ class GameServer:
def receive(self, n, data):
try:
data = json.loads(data.decode('utf-8'))
if isinstance(data[0], str):
data = [data]
for msg in data:
msgType = msg[0]
if msgType == "name":
name = msg[1]
if name in self.players:
self.error(n, "nametaken", "another connection to this player already exists")
return
if len(name) < 1:
self.error(n, "invalidname", "name needs at least one character")
return
if name[0] not in string.ascii_letters + string.digits:
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")
try:
data = json.loads(data.decode('utf-8'))
except json.JSONDecodeError:
self.error(n, "invalidmessage", "Invalid JSON")
return
if not isinstance(data, list) or len(data) != 2:
self.error(n, "invalidmessage", "Message must be a json list of length 2")
return
msg = data
msgType = msg[0]
if msgType == "name":
name = msg[1]
if len(name) < 1:
self.error(n, "invalidname", "name needs at least one character")
return
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]
message = name + ": " + msg[1]
print(message)
self.broadcast(message, "chat")
if len(bytes(name, "utf-8")) > 256:
self.error(n, "invalidname", "name may not be longer than 256 utf8 bytes")
return
if nameRegex.match(name) is None:
self.error(n, "invalidname", "Name must match the regex: {}".format(nameRegex.pattern))
return
if name[0] == "~" and 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 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:
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):
@ -109,7 +120,11 @@ class GameServer:
def readMessages(self):
m = []
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

View File

@ -1,12 +1,38 @@
import os
import socket
import sys
import threading
import struct
import selectors
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
# will execute callback functions on new connections, closing connections and received messages
# also provides a send function
@ -27,9 +53,10 @@ class Server:
self.onConnection = onConnection
self.onMessage = onMessage
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))
try:
self.sock.bind(address)
@ -42,32 +69,52 @@ class Server:
self.sock.listen()
self.listener = threading.Thread(target=self._listen, daemon=True)
self.listener.start()
def _listen(self):
self.connections = set()
self.sock.setblocking(False)
self.sel.register(self.sock, selectors.EVENT_READ, "ACCEPT")
self.connections = {}
print("listening")
while True:
connection, client_address = self.sock.accept()
listener = threading.Thread(target=self._listenCon, args=(connection,), daemon=True)
listener.start()
events = self.sel.select()
for key, mask in events:
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):
self.connections.add(connection)
self.onConnection(connection)
data = receive(connection)
while data:
self.onMessage(connection, data)
try:
data = receive(connection)
except socket.error:
break
if not len(data):
break
self.connections.discard(connection)
self.onConnectionClose(connection)
#def _listenCon(self, connection):
#self.connections.add(connection)
#self.onConnection(connection)
#data = receive(connection)
#while data:
#self.onMessage(connection, data)
#try:
#data = receive(connection)
#except socket.error:
#break
#if not len(data):
#break
#self.connections.discard(connection)
#self.onConnectionClose(connection)
def getUsername(self, connection):
@ -83,9 +130,11 @@ class Server:
def send(self, connection, msg):
try:
send(connection, msg)
length = len(msg)
header = length.to_bytes(4, byteorder="big")
connection.sendall(header + msg)
except Exception:
self.connections.discard(connection)
del self.connections[connection]
self.onConnectionClose(connection)
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
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
@ -47,7 +47,7 @@ This message must be sent before any other communication.
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.
@ -64,10 +64,18 @@ The first item in the list is the type of command, later items are arguments
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
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.
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 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"]`
@ -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.
- 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: