Rewrite server using quart instead of flask

This commit is contained in:
Paul Brinkmeier 2023-01-19 02:03:44 +01:00
parent d9eb2d7b05
commit 09d3d8684c
6 changed files with 164 additions and 175 deletions

View File

@ -1,171 +0,0 @@
import json
import simple_websocket
from collections import OrderedDict
from flask import Flask
from flask_sock import Sock
from queue import Queue
from random import Random
from threading import Thread, Lock
class BoardGenerator:
def __init__(self, seed):
self.dice = [
'aeaneg',
'wngeeh',
'ahspco',
'lnhnrz',
'aspffk',
'tstiyd',
'objoab',
'owtoat',
'iotmuc',
'erttyl',
'ryvdel',
'toessi',
'lreixd',
'terwhv',
'eiunes',
'nuihmq'
]
self.rng = Random(seed)
def generate_board(self):
shuffled_dice = self.rng.sample(self.dice, k=len(self.dice))
roll_results = [self.rng.choice(die) for die in shuffled_dice]
return [roll_results[4 * i : 4 * (i+1)] for i in range(0, 4)]
class Client:
def __init__(self, sock, client_id):
self.sock = sock
self.data = { 'id': client_id, 'name': '' }
class GlebbyState:
def __init__(self):
self.clients_lock = Lock()
# We want to preserve the order that clients arrived in,
# e.g. for whose turn it is
self.clients = OrderedDict()
self.next_client_id_lock = Lock()
self.next_client_id = 0
self.incoming_messages = Queue()
self.board_generator = BoardGenerator(42)
self.board = None
# domain stuff
def handle_message_from(self, client_id, payload):
print(f"{client_id}: {payload}")
if payload['type'] == 'set-name':
self.clients[client_id].data['name'] = payload['name']
self.broadcast(client_id, {
'type': 'set-name',
'name': payload['name']
})
elif payload['type'] == 'chat':
self.broadcast(client_id, {
'type': 'chat',
'message': payload['message']
})
elif payload['type'] == 'roll':
self.board = self.board_generator.generate_board()
self.broadcast(client_id, {
'type': 'roll',
'board': self.board
})
else:
print("Unhandled!")
def get_state_dto(self, client_id):
return {
'yourId': client_id,
'players': [
self.clients[other_client_id].data
for other_client_id in self.clients
],
'board': self.board
}
# message receiving and sending
def put_incoming_message(self, client_id, payload):
self.incoming_messages.put({
'from': client_id,
'payload': json.loads(payload)
})
def send_to(self, client_id_from, client_id_to, payload):
self.clients[client_id_to].sock.send(json.dumps({
'from': client_id_from,
'payload': payload
}))
def broadcast(self, client_id_from, payload):
with self.clients_lock:
for client_id_to in self.clients:
self.send_to(client_id_from, client_id_to, payload)
# client management
def _get_next_client_id(self):
with self.next_client_id_lock:
client_id = self.next_client_id
self.next_client_id += 1
return client_id
# TODO: Instead of using clients_lock, synchronize clients throught incoming_messages
# Make add_client and remove_client simply push events into the queue
def add_client(self, sock):
client_id = self._get_next_client_id()
self.broadcast(client_id, {'type': 'join'})
with self.clients_lock:
self.clients[client_id] = Client(sock, client_id)
self.send_to(None, client_id, {
'type': 'init',
'state': self.get_state_dto(client_id)
})
return client_id
def remove_client(self, client_id):
with self.clients_lock:
del self.clients[client_id]
self.broadcast(client_id, {'type': 'leave'})
# event thread stuff
def start_event_thread(self):
event_thread = Thread(target=lambda: self._event_thread())
event_thread.daemon = True
event_thread.start()
def _event_thread(self):
while True:
msg = self.incoming_messages.get()
self.handle_message_from(msg['from'], msg['payload'])
# Initialization
state = GlebbyState()
state.start_event_thread()
# TODO: Examine Quart (basically an asyncio version of flask)
app = Flask(__name__, static_url_path='')
sock = Sock(app)
@app.route('/')
def index():
return app.send_static_file('index.html')
@sock.route('/glebby')
def echo(sock):
client_id = state.add_client(sock)
try:
while True:
data = sock.receive()
state.put_incoming_message(client_id, data)
except simple_websocket.ConnectionClosed:
state.remove_client(client_id)

View File

@ -0,0 +1,36 @@
import asyncio
from quart import Quart, websocket
from glebby.model import Model
model = Model()
app = Quart(__name__, static_url_path='')
async def send_outgoing(queue):
while True:
data = await queue.get()
await websocket.send(data)
@app.websocket('/glebby')
async def ws():
global model
# For each connected client, we create a queue to store its outgoing messages.
# We pass this queue to the model so that it can communicate with the clients.
outgoing_queue = asyncio.Queue()
client_id = await model.add_client(outgoing_queue)
try:
# In order to get the queued messages through the websocket, we need to create a task
# since the current one will be busy receiving incoming messages below.
outgoing_task = asyncio.create_task(send_outgoing(outgoing_queue))
while True:
data = await websocket.receive()
await model.handle_incoming(client_id, data)
# When a client disconnects, we shut down the message pumping task and wait for it to finish
except asyncio.CancelledError:
await model.remove_client(client_id)
outgoing_task.cancel()
await outgoing_task

View File

@ -0,0 +1,28 @@
from random import Random
class BoardGenerator:
def __init__(self, seed):
self.dice = [
'aeaneg',
'wngeeh',
'ahspco',
'lnhnrz',
'aspffk',
'tstiyd',
'objoab',
'owtoat',
'iotmuc',
'erttyl',
'ryvdel',
'toessi',
'lreixd',
'terwhv',
'eiunes',
'nuihmq'
]
self.rng = Random(seed)
def generate_board(self):
shuffled_dice = self.rng.sample(self.dice, k=len(self.dice))
roll_results = [self.rng.choice(die) for die in shuffled_dice]
return [roll_results[4 * i : 4 * (i+1)] for i in range(0, 4)]

View File

@ -0,0 +1,86 @@
import itertools
import json
from collections import OrderedDict
from glebby.board import BoardGenerator
class Client:
def __init__(self, client_id, outgoing_queue):
self.outgoing_queue = outgoing_queue
self.data = { 'id': client_id, 'name': '' }
async def put_outgoing_message(self, message):
await self.outgoing_queue.put(message)
class Model:
def __init__(self):
self.clients = OrderedDict()
self.id_generator = itertools.count(start=0, step=1)
self.board_generator = BoardGenerator(None)
self.board = None
async def add_client(self, outgoing_queue):
client_id = next(self.id_generator)
await self.broadcast(client_id, { 'type': 'join' })
self.clients[client_id] = Client(client_id, outgoing_queue)
print(f'<{client_id}|join>')
await self.send_to(None, client_id, {
'type': 'init',
'state': self.get_state_dto(client_id)
})
return client_id
async def remove_client(self, client_id):
del self.clients[client_id]
await self.broadcast(client_id, { 'type': 'leave' })
print(f'<{client_id}|leave>')
async def send_to(self, client_id_from, client_id_to, payload):
await self.clients[client_id_to].put_outgoing_message(json.dumps({
'from': client_id_from,
'payload': payload
}))
async def broadcast(self, client_id_from, payload):
for client_id_to in self.clients:
await self.send_to(client_id_from, client_id_to, payload)
async def handle_incoming(self, client_id, message):
print(f'<{client_id}|data> {message}')
payload = json.loads(message)
if payload['type'] == 'set-name':
self.clients[client_id].data['name'] = payload['name']
await self.broadcast(client_id, {
'type': 'set-name',
'name': payload['name']
})
elif payload['type'] == 'chat':
await self.broadcast(client_id, {
'type': 'chat',
'message': payload['message']
})
elif payload['type'] == 'roll':
self.board = self.board_generator.generate_board()
await self.broadcast(client_id, {
'type': 'roll',
'board': self.board
})
else:
print("Unhandled!")
def get_state_dto(self, client_id):
return {
'yourId': client_id,
'players': [
client.data
for client in self.clients.values()
],
'board': self.board
}

View File

@ -1,12 +1,18 @@
aiofiles==22.1.0
blinker==1.5
click==8.1.3 click==8.1.3
Flask==2.2.2
flask-sock==0.6.0
h11==0.14.0 h11==0.14.0
h2==4.1.0
hpack==4.0.0
hypercorn==0.14.3
hyperframe==6.0.1
importlib-metadata==6.0.0 importlib-metadata==6.0.0
itsdangerous==2.1.2 itsdangerous==2.1.2
Jinja2==3.1.2 Jinja2==3.1.2
MarkupSafe==2.1.1 MarkupSafe==2.1.2
simple-websocket==0.9.0 priority==2.0.0
quart==0.18.3
toml==0.10.2
Werkzeug==2.2.2 Werkzeug==2.2.2
wsproto==1.2.0 wsproto==1.2.0
zipp==3.11.0 zipp==3.11.0

4
glebby-server/run.py Normal file
View File

@ -0,0 +1,4 @@
import glebby
# For development use only
glebby.app.run()