improved communication handling on the server
This commit is contained in:
parent
10bdb33255
commit
84d899aa45
|
@ -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']
|
||||||
|
|
|
@ -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"))
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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")
|
||||||
|
|
||||||
|
|
|
@ -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:
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue