Add server

This is a first try at connecting multiple clients using a server.
The commit includes a lot of debugging messages.
I will hopefully clean up the server and some of the client code.
This commit is contained in:
Joscha 2017-04-13 18:10:37 +00:00
parent 63410fd99e
commit 320dd16889
6 changed files with 236 additions and 26 deletions

View file

@ -2,6 +2,8 @@ import threading
import time import time
from utils import CHUNK_WIDTH, CHUNK_HEIGHT, Position from utils import CHUNK_WIDTH, CHUNK_HEIGHT, Position
import sys
class ChunkDiff(): class ChunkDiff():
""" """
Represents differences between two chunks (changes to be made to a chunk). Represents differences between two chunks (changes to be made to a chunk).
@ -13,10 +15,16 @@ class ChunkDiff():
def __init__(self): def __init__(self):
self._chars = {} self._chars = {}
def __str__(self):
return "cd" + str(self._chars)
def __repr__(self):
return "cd" + repr(self._chars)
@classmethod @classmethod
def from_dict(cls, d): def from_dict(cls, d):
diff = cls() diff = cls()
diff._chars = d diff._chars = {int(i): v for i, v in d.items()}
return diff return diff
#self._chars = d.copy() #self._chars = d.copy()
@ -99,6 +107,9 @@ class Chunk():
def get_changes(self): def get_changes(self):
return self._modifications return self._modifications
def as_diff(self):
return self._content.combine(self._modifications)
def touch(self, now=None): def touch(self, now=None):
self.last_modified = now or time.time() self.last_modified = now or time.time()
@ -111,7 +122,7 @@ class Chunk():
#y += 1 #y += 1
def lines(self): def lines(self):
return self._content.combine(self._modifications).lines() return self.as_diff().lines()
def modified(self): def modified(self):
return not self._modifications.empty() return not self._modifications.empty()
@ -150,14 +161,17 @@ class ChunkPool():
def apply_changes(self, changes): def apply_changes(self, changes):
for change in changes: for change in changes:
pos = Position(change[0][0], change[0][1]) #pos = Position(change[0][0], change[0][1])
pos = change[0]
diff = change[1] diff = change[1]
chunk = self.get(pos) chunk = self.get(pos)
if not chunk: if not chunk:
chunk = self.create(pos) chunk = self.create(pos)
sys.stderr.write(f"Previous at {pos}: {chunk._content}\n")
chunk.commit_diff(diff) chunk.commit_diff(diff)
sys.stderr.write(f"Afterwrd at {pos}: {chunk._content}\n")
def commit_changes(self): def commit_changes(self):
changes = [] changes = []
@ -177,6 +191,7 @@ class ChunkPool():
def load_list(self, coords): def load_list(self, coords):
for pos in coords: for pos in coords:
if pos not in self._chunks:
self.load(pos) self.load(pos)
def unload(self, pos): def unload(self, pos):
@ -188,7 +203,7 @@ class ChunkPool():
self.unload(pos) self.unload(pos)
def clean_up(self, except_for=[], condition=lambda pos, chunk: True): def clean_up(self, except_for=[], condition=lambda pos, chunk: True):
# old list comprehension which became too long: ## old list comprehension which became too long:
#coords = [pos for pos, chunk in self._chunks.items() if not pos in except_for and condition(chunk)] #coords = [pos for pos, chunk in self._chunks.items() if not pos in except_for and condition(chunk)]
#self.save_changes() # needs to be accounted for by the user #self.save_changes() # needs to be accounted for by the user

View file

@ -1,10 +1,15 @@
import curses import curses
import json
import os import os
import string import string
import sys import sys
import threading import threading
import websocket
from websocket import WebSocketException as WSException
from maps import Map, ChunkMap from maps import Map, ChunkMap
from chunks import ChunkDiff from chunks import ChunkDiff
from utils import Position
from clientchunkpool import ClientChunkPool from clientchunkpool import ClientChunkPool
class Client(): class Client():
@ -12,7 +17,7 @@ class Client():
self.stopping = False self.stopping = False
self.chunkmap_active = False self.chunkmap_active = False
self.address = address self.address = f"ws://{address}/"
self._drawevent = threading.Event() self._drawevent = threading.Event()
self.pool = ClientChunkPool(self) self.pool = ClientChunkPool(self)
#self.map_ = Map(sizex, sizey, self.pool) #self.map_ = Map(sizex, sizey, self.pool)
@ -21,10 +26,28 @@ class Client():
#self.sock = socket.Socket(...) #self.sock = socket.Socket(...)
def launch(self, stdscr): def launch(self, stdscr):
# connect to server
try:
self._ws = websocket.create_connection(
self.address,
enable_multithread=True
)
except ConnectionRefusedError:
sys.stderr.write(f"Could not connect to server: {self.address!r}\n")
return
# create map etc.
sizey, sizex = stdscr.getmaxyx() sizey, sizex = stdscr.getmaxyx()
self.map_ = Map(sizex, sizey, self.pool, self) self.map_ = Map(sizex, sizey, self.pool, self)
self.chunkmap = ChunkMap(self.map_) self.chunkmap = ChunkMap(self.map_)
# start connection thread
self.connectionthread = threading.Thread(
target=self.connection_thread,
name="connectionthread"
)
self.connectionthread.start()
# start input thread # start input thread
self.inputthread = threading.Thread( self.inputthread = threading.Thread(
target=self.input_thread, target=self.input_thread,
@ -34,6 +57,7 @@ class Client():
) )
self.inputthread.start() self.inputthread.start()
# update screen until stopped
while not self.stopping: while not self.stopping:
self._drawevent.wait() self._drawevent.wait()
self._drawevent.clear() self._drawevent.clear()
@ -94,26 +118,60 @@ class Client():
else: sys.stderr.write(repr(i) + "\n") else: sys.stderr.write(repr(i) + "\n")
def connection_thread(self):
while True:
try:
j = self._ws.recv()
self.handle_json(json.loads(j))
except (WSException, ConnectionResetError, OSError):
#self.stop()
return
def handle_json(self, message):
sys.stderr.write(f"message: {message}\n")
if message["type"] == "apply-changes":
changes = []
for chunk in message["data"]:
pos = Position(chunk[0][0], chunk[0][1])
change = ChunkDiff.from_dict(chunk[1])
changes.append((pos, change))
sys.stderr.write(f"Changes to apply: {changes}\n")
self.map_.apply_changes(changes)
def stop(self): def stop(self):
sys.stderr.write("Stopping!\n")
self.stopping = True self.stopping = True
self._ws.close()
self.redraw() self.redraw()
def request_chunks(self, coords): def request_chunks(self, coords):
def execute(): #sys.stderr.write(f"requested chunks: {coords}\n")
changes = [(pos, ChunkDiff()) for pos in coords] message = {"type": "request-chunks", "data": coords}
with self.pool as pool: self._ws.send(json.dumps(message))
pool.apply_changes(changes)
tx = threading.Timer(1, execute) #def execute():
tx.start() #changes = [(pos, ChunkDiff()) for pos in coords]
#with self.pool as pool:
#pool.apply_changes(changes)
#tx = threading.Timer(1, execute)
#tx.start()
def unload_chunks(self, coords):
#sys.stderr.write(f"unloading chunks: {coords}\n")
message = {"type": "unload-chunks", "data": coords}
self._ws.send(json.dumps(message))
def send_changes(self, changes): def send_changes(self, changes):
pass #sys.stderr.write(f"sending changes: {changes}\n")
message = {"type": "save-changes", "data": changes}
self._ws.send(json.dumps(message))
def main(argv): def main(argv):
if len(argv) != 2: if len(argv) != 2:
print("Usage:") print("Usage:")
print(" {} address".format(argv[0])) print(f" {argv[0]} address")
return return
os.environ.setdefault('ESCDELAY', '25') # only a 25 millisecond delay os.environ.setdefault('ESCDELAY', '25') # only a 25 millisecond delay

View file

@ -1,5 +1,8 @@
import threading
from chunks import ChunkPool from chunks import ChunkPool
import sys
class ClientChunkPool(ChunkPool): class ClientChunkPool(ChunkPool):
""" """
A ChunkPool that requests/loads chunks from a client. A ChunkPool that requests/loads chunks from a client.
@ -9,6 +12,10 @@ class ClientChunkPool(ChunkPool):
super().__init__() super().__init__()
self._client = client self._client = client
self._save_thread = None
def set(self, pos, chunk):
super().set(pos, chunk)
#def commit_changes(self): #def commit_changes(self):
#changes = [] #changes = []
@ -24,12 +31,44 @@ class ClientChunkPool(ChunkPool):
self._client.redraw() self._client.redraw()
def save_changes_delayed(self):
sys.stderr.write("Pre-HEHEHE\n")
if not self._save_thread:
def threadf():
sys.stderr.write("HEHEHE\n")
self.save_changes()
self._save_thread = None
self._save_thread = threading.Timer(.25, threadf)
self._save_thread.start()
def save_changes(self): def save_changes(self):
changes = self.commit_changes() changes = self.commit_changes()
self._client.send_changes(changes) dchanges = []
for pos, change in changes:
dchange = change.to_dict()
if dchange:
dchanges.append((pos, dchange))
if dchanges:
self._client.send_changes(dchanges)
def load(self, pos): def load(self, pos):
raise Exception raise Exception
def load_list(self, coords): def load_list(self, coords):
coords = [pos for pos in coords if pos not in self._chunks]
if coords:
self._client.request_chunks(coords) self._client.request_chunks(coords)
#def unload(self, pos):
#raise Exception
def unload_list(self, coords):
if coords:
#self.save_changes()
self._client.unload_chunks(coords)
super().unload_list(coords)
# What needs to happen differently from the default implementation:
# loading -> only ask server when necessary
# unloading -> send message to server
# unloading -> commit changes when anything is actually unloaded

View file

@ -1,4 +1,4 @@
from .chunks.py import ChunkPool from chunks import ChunkPool
class ChunkDB(): class ChunkDB():
""" """

13
maps.py
View file

@ -16,10 +16,10 @@ class Map():
self.chunkpreload = 0 # preload chunks in this radius (they will count as "visible") self.chunkpreload = 0 # preload chunks in this radius (they will count as "visible")
self.chunkunload = 5 # don't unload chunks within this radius self.chunkunload = 5 # don't unload chunks within this radius
self.cursorpadding = 2 self.cursorpadding = 2
self.worldx = 0 self.worldx = -self.cursorpadding
self.worldy = 0 self.worldy = -self.cursorpadding
self.cursorx = self.cursorpadding self.cursorx = 0
self.cursory = self.cursorpadding self.cursory = 0
self.lastcurx = self.cursorx self.lastcurx = self.cursorx
self.lastcury = self.cursory self.lastcury = self.cursory
@ -137,6 +137,7 @@ class Map():
if chunk: if chunk:
chunk.set(inchunkx(self.cursorx), inchunky(self.cursory), char) chunk.set(inchunkx(self.cursorx), inchunky(self.cursory), char)
pool.save_changes_delayed()
self.move_cursor(1, 0, False) self.move_cursor(1, 0, False)
@ -145,6 +146,7 @@ class Map():
chunk = pool.get(Position(chunkx(self.cursorx-1), chunky(self.cursory))) chunk = pool.get(Position(chunkx(self.cursorx-1), chunky(self.cursory)))
if chunk: if chunk:
chunk.delete(inchunkx(self.cursorx-1), inchunky(self.cursory)) chunk.delete(inchunkx(self.cursorx-1), inchunky(self.cursory))
pool.save_changes_delayed()
self.move_cursor(-1, 0, False) self.move_cursor(-1, 0, False)
@ -208,6 +210,9 @@ class Map():
#self.load_visible() #self.load_visible()
def apply_changes(self, changes):
self.chunkpool.apply_changes(changes)
ChunkStyle = namedtuple("ChunkStyle", "string color") ChunkStyle = namedtuple("ChunkStyle", "string color")
class ChunkMap(): class ChunkMap():

View file

@ -1 +1,94 @@
# import from chunks, dbchunkpool # import from chunks, dbchunkpool
import json
from SimpleWebSocketServer import SimpleWebSocketServer, WebSocket
from utils import Position
from chunks import ChunkDiff
from dbchunkpool import DBChunkPool
pool = DBChunkPool()
clients = set()
class WotServer(WebSocket):
def handle_request_chunks(self, coords):
changes = []
with pool:
for coor in coords:
pos = Position(coor[0], coor[1])
change = pool.get(pos) or pool.create(pos)
dchange = change.as_diff().to_dict()
changes.append((pos, dchange))
self.loaded_chunks.add(pos)
message = {"type": "apply-changes", "data": changes}
print(f"Message bong sent: {json.dumps(message)}")
self.sendMessage(json.dumps(message))
def handle_unload_chunks(self, coords):
for coor in coords:
pos = Position(coor)
self.loaded_chunks.remove(pos)
def handle_save_changes(self, dchanges):
changes = []
for chunk in dchanges:
print("CHUNK!", chunk)
pos = Position(chunk[0][0], chunk[0][1])
change = ChunkDiff.from_dict(chunk[1])
changes.append((pos, change))
with pool:
pool.apply_changes(changes)
#with pool:
#for chunk in changes:
#print("changed content:", pool.get(chunk[0])._content)
for client in clients:
client.send_changes(changes)
def send_changes(self, changes):
print("NORMAL CHANGES:", changes)
dchanges = []
for chunk in changes:
pos = chunk[0]
change = chunk[1]
if pos in self.loaded_chunks:
dchanges.append((pos, change.to_dict()))
print("LOADED CHANGES:", dchanges)
if dchanges:
print("Changes!")
message = {"type": "apply-changes", "data": dchanges}
print("Changes?")
print(f"Message bang sent: {json.dumps(message)}")
self.sendMessage(json.dumps(message))
def handleMessage(self):
message = json.loads(self.data)
print(f"message arrived: {message}")
if message["type"] == "request-chunks":
self.handle_request_chunks(message["data"])
elif message["type"] == "unload-chunks":
self.handle_unload_chunks(message["data"])
elif message["type"] == "save-changes":
self.handle_save_changes(message["data"])
print("Message received and dealt with.")
#changes = []
#for chunk in message["data"]:
#pass
#self.sendMessage(self.data)
def handleConnected(self):
print(self.address, 'connected')
clients.add(self)
self.loaded_chunks = set()
def handleClose(self):
print(self.address, 'closed')
clients.remove(self)
server = SimpleWebSocketServer('', 8000, WotServer)
server.serveforever()