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")
|
||||
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']
|
||||
|
|
|
@ -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"))
|
||||
|
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
@ -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")
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
||||
|
|
Loading…
Reference in New Issue