ESock replacement
Highway is a wrapper around ws4py that is needed for routing and exchanging routes automatically. Currently only the server exchanges routes with the client. This behaviour has to be replicated the other way around too. Not all that stable.
This commit is contained in:
parent
56ad9ba376
commit
a583d87f6a
1 changed files with 214 additions and 0 deletions
214
Shared/Highway.py
Normal file
214
Shared/Highway.py
Normal file
|
@ -0,0 +1,214 @@
|
|||
import Logging
|
||||
import gzip
|
||||
import Routing
|
||||
import struct
|
||||
import json
|
||||
from threading import Lock
|
||||
|
||||
from ws4py.websocket import WebSocket
|
||||
from ws4py.client.threadedclient import WebSocketClient
|
||||
|
||||
INDEXED_DICT = 5
|
||||
|
||||
DATA_TYPES = {str : 0, dict : 1,
|
||||
list : 1, bytes : 2,
|
||||
int : 3, float : 4, INDEXED_DICT : 5}
|
||||
|
||||
|
||||
def reverse_dict(dict):
|
||||
return {v : k for k, v in dict.items()}
|
||||
|
||||
|
||||
REVERSE_DATA_TYPES = reverse_dict(DATA_TYPES)
|
||||
|
||||
INVALID_ROUTE = 1
|
||||
INVALID_METADATA_LAYOUT = 2
|
||||
INVALID_DATA_TYPE = 3
|
||||
|
||||
META_ROUTE = "meta"
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
class ConvertFailedError(ValueError):
|
||||
def __init__(self):
|
||||
super(ValueError, self).__init__("conversion failed (invalid data type supplied)")
|
||||
|
||||
|
||||
class Metadata:
|
||||
def __init__(self, data_type, m_route):
|
||||
self.data_type = REVERSE_DATA_TYPES[data_type]
|
||||
self.m_route = m_route
|
||||
|
||||
|
||||
|
||||
def create_metadata(data_type, converted_route, indexed_dict=False):
|
||||
return struct.pack("bh",
|
||||
DATA_TYPES[data_type] if not indexed_dict else DATA_TYPES[INDEXED_DICT],
|
||||
converted_route)
|
||||
|
||||
|
||||
def prepare_data(data):
|
||||
original_data_type = type(data)
|
||||
if original_data_type in DATA_TYPES:
|
||||
data_type = DATA_TYPES[original_data_type]
|
||||
else:
|
||||
data_type = DATA_TYPES[bytes]
|
||||
if original_data_type is str:
|
||||
data = data.encode()
|
||||
elif original_data_type is dict or original_data_type is list:
|
||||
data = json.dumps(data, separators=(',',':')).encode()
|
||||
return data, original_data_type
|
||||
|
||||
|
||||
def pack_message(data, exchange_route, compression_level,
|
||||
debug=False, indexed_dict=False):
|
||||
data, original_data_type = prepare_data(data)
|
||||
data = gzip.compress(data, compression_level)
|
||||
"""
|
||||
if debug:
|
||||
data_repr = str(data).replace("\n", " ")
|
||||
if len(data_repr) > 80:
|
||||
data_repr = data_repr[:80] + "..."
|
||||
Logging.info("Packaged '%s' on route '%s': %s" %
|
||||
(original_data_type.__name__, route, data_repr))
|
||||
"""
|
||||
return create_metadata(original_data_type, exchange_route,
|
||||
indexed_dict=indexed_dict) + data
|
||||
|
||||
|
||||
|
||||
def parse_metadata(message):
|
||||
metadata = struct.unpack("bh", message[:4])
|
||||
return Metadata(metadata[0], metadata[1])
|
||||
|
||||
|
||||
|
||||
|
||||
def parse_message(data, data_type):
|
||||
data = convert_data(gzip.decompress(data[4:]), data_type)
|
||||
return data
|
||||
|
||||
|
||||
def convert_data(data, data_type, debug=False):
|
||||
try:
|
||||
if data_type == str:
|
||||
data = data.decode()
|
||||
elif data_type == int:
|
||||
data = int(data.decode())
|
||||
elif data_type == float:
|
||||
data = float(data.decode())
|
||||
elif data_type in (dict, list):
|
||||
data = json.loads(data.decode())
|
||||
elif data_type == INDEXED_DICT:
|
||||
data = json.loads(data.decode())
|
||||
indexed_data = {}
|
||||
for key in data:
|
||||
indexed_data[int(key)] = data[key]
|
||||
data = indexed_data
|
||||
except Exception:
|
||||
return None
|
||||
return data
|
||||
|
||||
|
||||
class Shared:
|
||||
def setup(self, routes, compression_level, debug=False):
|
||||
self.routes = routes
|
||||
self.compression_level = compression_level
|
||||
self.address, self.port = self.peer_address
|
||||
self.debug = debug
|
||||
|
||||
|
||||
def opened(self):
|
||||
self.routes = Routing.create_routes(self.routes, self)
|
||||
|
||||
|
||||
def received_message(self, message):
|
||||
message = message.data
|
||||
metadata = parse_metadata(message)
|
||||
try:
|
||||
route = self.routes[self.exchange_routes[metadata.m_route]]
|
||||
except KeyError:
|
||||
self.send(Handler.INVALID_ROUTE, META_ROUTE)
|
||||
Logging.error("Received message with non-existing route '%d' from '%s:%d'" % (
|
||||
metadata.m_route, self.address, self.port))
|
||||
return
|
||||
data = convert_data(message, metadata.data_type)
|
||||
if self.debug:
|
||||
data_repr = str(data).replace("\n", " ")
|
||||
if len(data_repr) > 80:
|
||||
data_repr = data_repr[:80] + "..."
|
||||
Logging.info("Received '%s' on route '%s': %s (%s:%d)" % (
|
||||
type(data).__name__, route, data_repr, self.address,
|
||||
self.port))
|
||||
self.routes[route].run(data, self)
|
||||
|
||||
|
||||
def patched_send(self, data, route, indexed_dict=False):
|
||||
self.raw_send(pack_message(data, self.reverse_exchange_routes[route],
|
||||
self.compression_level, debug=self.debug,
|
||||
indexed_dict=indexed_dict), binary=True)
|
||||
if self.debug:
|
||||
data_repr = str(data).replace("\n", " ")
|
||||
if len(data_repr) > 80:
|
||||
data_repr = data_repr[:80] + "..."
|
||||
Logging.info("Sent '%s' on route '%s': %s (%s:%d)" % (
|
||||
type(data).__name__, route, data_repr, self.address,
|
||||
self.port))
|
||||
|
||||
|
||||
|
||||
|
||||
class Server(Shared, WebSocket):
|
||||
def setup(self, routes, compression_level, debug=False):
|
||||
super().setup(routes, compression_level, debug=debug)
|
||||
# Send replacement can't be done in the parent setup method because
|
||||
# both client and server use a method from a different module.
|
||||
self.raw_send = self.send
|
||||
self.send = self.patched_send
|
||||
|
||||
|
||||
|
||||
def opened(self):
|
||||
super().opened()
|
||||
self.exchange_routes = Routing.create_exchange_map(self.routes)
|
||||
self.reverse_exchange_routes = reverse_dict(self.exchange_routes)
|
||||
self.patched_send(self.exchange_routes, META_ROUTE, indexed_dict=True)
|
||||
Routing.launch_routes(self.routes, self)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
class Client(WebSocketClient, Shared):
|
||||
def setup(self, routes, compression_level, debug=False):
|
||||
super().setup(routes, compression_level, debug=debug)
|
||||
# Send replacement can't be done in the parent setup method because
|
||||
# both client and server use a method from a different module.
|
||||
self.metadata_lock = Lock()
|
||||
self.raw_send = self.send
|
||||
self.send = self.patched_send
|
||||
self._received_message = self.received_message
|
||||
self.received_message = self.receive_routes
|
||||
|
||||
|
||||
def receive_routes(self, message):
|
||||
self.metadata_lock.acquire()
|
||||
message = message.data
|
||||
metadata = parse_metadata(message)
|
||||
if metadata.m_route != -1:
|
||||
Logging.error("Invalid route on first message (not meta)")
|
||||
self.close()
|
||||
return
|
||||
data = parse_message(message, metadata.data_type)
|
||||
if Routing.validate_exchange_map(data):
|
||||
self.exchange_routes = data
|
||||
self.reverse_exchange_routes = reverse_dict(self.exchange_routes)
|
||||
self.received_message = self._received_message
|
||||
Logging.info("Routes successfully exchanged: %s" % str(self.exchange_routes))
|
||||
else:
|
||||
Logging.error("Invalid exchange map.")
|
||||
self.close()
|
||||
return
|
||||
self.metadata_lock.release()
|
Reference in a new issue