Isolate transport
This commit is contained in:
parent
96014d3d53
commit
9afe5c2f2a
3 changed files with 183 additions and 103 deletions
|
|
@ -5,9 +5,9 @@ import threading
|
||||||
import time
|
import time
|
||||||
from collections import namedtuple
|
from collections import namedtuple
|
||||||
|
|
||||||
from .compat import get_byte, get_character, get_unicode
|
from . import transports
|
||||||
|
from .compat import get_byte, get_character, get_unicode, parse_url
|
||||||
from .exceptions import ConnectionError, TimeoutError, PacketError
|
from .exceptions import ConnectionError, TimeoutError, PacketError
|
||||||
from .transports import _get_response
|
|
||||||
|
|
||||||
|
|
||||||
__version__ = '0.6.1'
|
__version__ = '0.6.1'
|
||||||
|
|
@ -183,7 +183,6 @@ class LoggingSocketIONamespace(SocketIONamespace, LoggingMixin):
|
||||||
|
|
||||||
class EngineIO(object):
|
class EngineIO(object):
|
||||||
|
|
||||||
_engineIO_protocol = 3
|
|
||||||
_engineIO_request_index = 0
|
_engineIO_request_index = 0
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
|
|
@ -191,31 +190,22 @@ class EngineIO(object):
|
||||||
host, port=None, Namespace=None,
|
host, port=None, Namespace=None,
|
||||||
wait_for_connection=True, transports=TRANSPORTS,
|
wait_for_connection=True, transports=TRANSPORTS,
|
||||||
resource='engine.io', **kw):
|
resource='engine.io', **kw):
|
||||||
self._url = 'http://%s:%s/%s/' % (host, port, resource)
|
self._is_secure, self._url = _parse_host(host, port, resource)
|
||||||
|
self._wait_for_connection = wait_for_connection
|
||||||
|
self._client_transports = transports
|
||||||
|
self._kw = kw
|
||||||
self._http_session = requests.Session()
|
self._http_session = requests.Session()
|
||||||
|
|
||||||
response = self._http_session.get(self._url, params={
|
|
||||||
'EIO': self._engineIO_protocol,
|
|
||||||
'transport': 'polling',
|
|
||||||
't': self._get_timestamp(),
|
|
||||||
})
|
|
||||||
engineIO_packets = _decode_engineIO_content(response.content)
|
|
||||||
engineIO_packet_type, engineIO_packet_data = engineIO_packets[0]
|
|
||||||
assert engineIO_packet_type == 0
|
|
||||||
value_by_name = json.loads(get_unicode(engineIO_packet_data))
|
|
||||||
# 'websocket' in value_by_name['upgrades']
|
|
||||||
self._ping_interval = value_by_name['pingInterval'] / float(1000)
|
|
||||||
self._ping_timeout = value_by_name['pingTimeout'] / float(1000)
|
|
||||||
self._session_id = value_by_name['sid']
|
|
||||||
|
|
||||||
if Namespace:
|
if Namespace:
|
||||||
self.define(Namespace)
|
self.define(Namespace)
|
||||||
|
|
||||||
self._heartbeat_thread = HeartbeatThread(
|
def __enter__(self):
|
||||||
send_heartbeat=self._ping,
|
return self
|
||||||
relax_interval_in_seconds=self._ping_interval,
|
|
||||||
hurry_interval_in_seconds=1)
|
def __exit__(self, *exception_pack):
|
||||||
self._heartbeat_thread.start()
|
self.close()
|
||||||
|
|
||||||
|
def __del__(self):
|
||||||
|
self.close()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def connected(self):
|
def connected(self):
|
||||||
|
|
@ -266,7 +256,6 @@ class EngineIO(object):
|
||||||
namespace.on_disconnect()
|
namespace.on_disconnect()
|
||||||
except PacketError:
|
except PacketError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
self._heartbeat_thread.relax()
|
self._heartbeat_thread.relax()
|
||||||
|
|
||||||
def _should_stop_waiting(self):
|
def _should_stop_waiting(self):
|
||||||
|
|
@ -274,7 +263,7 @@ class EngineIO(object):
|
||||||
return self.__transport._wants_to_disconnect
|
return self.__transport._wants_to_disconnect
|
||||||
|
|
||||||
def _process_packets(self):
|
def _process_packets(self):
|
||||||
for engineIO_packet in self._recv_packet():
|
for engineIO_packet in self._transport.recv_packet():
|
||||||
try:
|
try:
|
||||||
self._process_packet(engineIO_packet)
|
self._process_packet(engineIO_packet)
|
||||||
except PacketError as e:
|
except PacketError as e:
|
||||||
|
|
@ -332,89 +321,100 @@ class EngineIO(object):
|
||||||
|
|
||||||
def _message(self, engineIO_packet_data):
|
def _message(self, engineIO_packet_data):
|
||||||
engineIO_packet_type = 4
|
engineIO_packet_type = 4
|
||||||
response = self._http_session.post(self._url, params={
|
self._transport.send_packet(engineIO_packet_type, engineIO_packet_data)
|
||||||
'EIO': self._engineIO_protocol,
|
|
||||||
'transport': 'polling',
|
|
||||||
't': self._get_timestamp(),
|
|
||||||
'sid': self._session_id,
|
|
||||||
}, data=_encode_engineIO_content([
|
|
||||||
(engineIO_packet_type, engineIO_packet_data),
|
|
||||||
]), headers={
|
|
||||||
'content-type': 'application/octet-stream',
|
|
||||||
})
|
|
||||||
print('message()')
|
|
||||||
print(response.content)
|
|
||||||
engineIO_packets = _decode_engineIO_content(response.content)
|
|
||||||
for engineIO_packet_type, engineIO_packet_data in engineIO_packets:
|
|
||||||
socketIO_packet_type = int(get_character(engineIO_packet_data, 0))
|
|
||||||
socketIO_packet_data = engineIO_packet_data[1:]
|
|
||||||
print('engineIO_packet_type = %s' % engineIO_packet_type)
|
|
||||||
print('socketIO_packet_type = %s' % socketIO_packet_type)
|
|
||||||
print('socketIO_packet_data = %s' % socketIO_packet_data)
|
|
||||||
|
|
||||||
def _ping(self, engineIO_packet_data=''):
|
def _ping(self, engineIO_packet_data=''):
|
||||||
engineIO_packet_type = 2
|
engineIO_packet_type = 2
|
||||||
response = self._http_session.post(self._url, params={
|
self._transport.send_packet(engineIO_packet_type, engineIO_packet_data)
|
||||||
'EIO': self._engineIO_protocol,
|
|
||||||
'transport': 'polling',
|
|
||||||
't': self._get_timestamp(),
|
|
||||||
'sid': self._session_id,
|
|
||||||
}, data=_encode_engineIO_content([
|
|
||||||
(engineIO_packet_type, engineIO_packet_data),
|
|
||||||
]), headers={
|
|
||||||
'content-type': 'application/octet-stream',
|
|
||||||
})
|
|
||||||
print('ping()')
|
|
||||||
print(response.content)
|
|
||||||
engineIO_packets = _decode_engineIO_content(response.content)
|
|
||||||
for engineIO_packet_type, engineIO_packet_data in engineIO_packets:
|
|
||||||
socketIO_packet_type = int(get_character(engineIO_packet_data, 0))
|
|
||||||
socketIO_packet_data = engineIO_packet_data[1:]
|
|
||||||
print('engineIO_packet_type = %s' % engineIO_packet_type)
|
|
||||||
print('socketIO_packet_type = %s' % socketIO_packet_type)
|
|
||||||
print('socketIO_packet_data = %s' % socketIO_packet_data)
|
|
||||||
|
|
||||||
def _pong(self, engineIO_packet_data=''):
|
def _pong(self, engineIO_packet_data=''):
|
||||||
engineIO_packet_type = 3
|
engineIO_packet_type = 3
|
||||||
response = self._http_session.post(self._url, params={
|
self._transport.send_packet(engineIO_packet_type, engineIO_packet_data)
|
||||||
'EIO': self._engineIO_protocol,
|
|
||||||
'transport': 'polling',
|
|
||||||
't': self._get_timestamp(),
|
|
||||||
'sid': self._session_id,
|
|
||||||
}, data=_encode_engineIO_content([
|
|
||||||
(engineIO_packet_type, engineIO_packet_data),
|
|
||||||
]), headers={
|
|
||||||
'content-type': 'application/octet-stream',
|
|
||||||
})
|
|
||||||
print('pong()')
|
|
||||||
print(response.content)
|
|
||||||
engineIO_packets = _decode_engineIO_content(response.content)
|
|
||||||
for engineIO_packet_type, engineIO_packet_data in engineIO_packets:
|
|
||||||
socketIO_packet_type = int(get_character(engineIO_packet_data, 0))
|
|
||||||
socketIO_packet_data = engineIO_packet_data[1:]
|
|
||||||
print('engineIO_packet_type = %s' % engineIO_packet_type)
|
|
||||||
print('socketIO_packet_type = %s' % socketIO_packet_type)
|
|
||||||
print('socketIO_packet_data = %s' % socketIO_packet_data)
|
|
||||||
|
|
||||||
def _recv_packet(self):
|
@property
|
||||||
response = _get_response(
|
def _transport(self):
|
||||||
self._http_session.get,
|
try:
|
||||||
self._url,
|
if self.connected:
|
||||||
params={
|
return self.__transport
|
||||||
'EIO': self._engineIO_protocol,
|
except AttributeError:
|
||||||
'transport': 'polling',
|
pass
|
||||||
't': self._get_timestamp(),
|
self._get_engineIO_session()
|
||||||
'sid': self._session_id,
|
self._negotiate_transport()
|
||||||
},
|
self._reset_heartbeat()
|
||||||
timeout=self._ping_timeout)
|
self._connect_namespaces()
|
||||||
for engineIO_packet in _decode_engineIO_content(response.content):
|
return self.__transport
|
||||||
yield engineIO_packet
|
|
||||||
|
def _get_engineIO_session(self):
|
||||||
|
url = '%s://%s/' % ('https' if self.is_secure else 'http', self._url)
|
||||||
|
warning_screen = _yield_warning_screen()
|
||||||
|
for elapsed_time in warning_screen:
|
||||||
|
try:
|
||||||
|
engineIO_packet_type, engineIO_packet_data = next(
|
||||||
|
XHR_PollingTransport().recv_packet())
|
||||||
|
|
||||||
|
response = _get_response(self._http_session.get, url, params={
|
||||||
|
'EIO': self._engineIO_protocol,
|
||||||
|
'transport': 'polling',
|
||||||
|
't': self._get_timestamp(),
|
||||||
|
}, **self._kw)
|
||||||
|
except (TimeoutError, ConnectionError) as e:
|
||||||
|
if not self._wait_for_connection:
|
||||||
|
raise
|
||||||
|
warning = Exception('[waiting for connection] %s' % e)
|
||||||
|
warning_screen.throw(warning)
|
||||||
|
engineIO_packets = _decode_engineIO_content(response.content)
|
||||||
|
engineIO_packet_type, engineIO_packet_data = engineIO_packets[0]
|
||||||
|
assert engineIO_packet_type == 0
|
||||||
|
value_by_name = json.loads(get_unicode(engineIO_packet_data))
|
||||||
|
self._session_id = value_by_name['sid']
|
||||||
|
self._ping_interval = value_by_name['pingInterval'] / float(1000)
|
||||||
|
self._ping_timeout = value_by_name['pingTimeout'] / float(1000)
|
||||||
|
self._transport_upgrades = value_by_name['upgrades']
|
||||||
|
|
||||||
|
def _negotiate_transport(self):
|
||||||
|
self.__transport = self._get_transport('xhr-polling')
|
||||||
|
|
||||||
|
def _reset_heartbeat(self):
|
||||||
|
try:
|
||||||
|
self._heartbeat_thread.stop()
|
||||||
|
except AttributeError:
|
||||||
|
pass
|
||||||
|
self._heartbeat_thread = HeartbeatThread(
|
||||||
|
send_heartbeat=self.__transport._ping,
|
||||||
|
relax_interval_in_seconds=self._ping_interval,
|
||||||
|
hurry_interval_in_seconds=1)
|
||||||
|
self._heartbeat_thread.start()
|
||||||
|
|
||||||
|
def _connect_namespaces(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _get_transport(self, transport_name):
|
||||||
|
self._log(logging.DEBUG, '[transport chosen] %s', transport_name)
|
||||||
|
return {
|
||||||
|
'xhr-polling': transports.XHR_PollingTransport,
|
||||||
|
}[transport_name]()
|
||||||
|
|
||||||
def _log(self, level, msg, *attrs):
|
def _log(self, level, msg, *attrs):
|
||||||
_log.log(level, '%s: %s' % (self._url, msg), *attrs)
|
_log.log(level, '%s: %s' % (self._url, msg), *attrs)
|
||||||
|
|
||||||
|
|
||||||
class SocketIO(EngineIO):
|
class SocketIO(EngineIO):
|
||||||
|
"""Create a socket.io client that connects to a socket.io server
|
||||||
|
at the specified host and port.
|
||||||
|
|
||||||
|
- Define the behavior of the client by specifying a custom Namespace.
|
||||||
|
- Prefix host with https:// to use SSL.
|
||||||
|
- Set wait_for_connection=True to block until we have a connection.
|
||||||
|
- Specify desired transports=['websocket', 'xhr-polling'].
|
||||||
|
- Pass query params, headers, cookies, proxies as keyword arguments.
|
||||||
|
|
||||||
|
SocketIO(
|
||||||
|
'localhost', 8000,
|
||||||
|
params={'q': 'qqq'},
|
||||||
|
headers={'Authorization': 'Basic ' + b64encode('username:password')},
|
||||||
|
cookies={'a': 'aaa'},
|
||||||
|
proxies={'https': 'https://proxy.example.com:8080'})
|
||||||
|
"""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
|
|
@ -429,6 +429,14 @@ class SocketIO(EngineIO):
|
||||||
wait_for_connection, transports,
|
wait_for_connection, transports,
|
||||||
resource, **kw)
|
resource, **kw)
|
||||||
|
|
||||||
|
def __exit__(self, *exception_pack):
|
||||||
|
self.disconnect()
|
||||||
|
super(SocketIO, self).__exit__(*exception_pack)
|
||||||
|
|
||||||
|
def __del__(self):
|
||||||
|
self.disconnect()
|
||||||
|
super(SocketIO, self).__del__()
|
||||||
|
|
||||||
def on(self, event, callback, path=''):
|
def on(self, event, callback, path=''):
|
||||||
try:
|
try:
|
||||||
namespace = self.get_namespace(path)
|
namespace = self.get_namespace(path)
|
||||||
|
|
@ -555,6 +563,12 @@ class SocketIO(EngineIO):
|
||||||
'Return function that acknowledges the server'
|
'Return function that acknowledges the server'
|
||||||
return lambda *args: self._ack(path, ack_id, *args)
|
return lambda *args: self._ack(path, ack_id, *args)
|
||||||
|
|
||||||
|
def _connect_namespaces(self):
|
||||||
|
for path, namespace in self._namespace_by_path.items():
|
||||||
|
namespace._transport = self.__transport
|
||||||
|
if path:
|
||||||
|
self.__transport.connect(path)
|
||||||
|
|
||||||
|
|
||||||
class HeartbeatThread(threading.Thread):
|
class HeartbeatThread(threading.Thread):
|
||||||
|
|
||||||
|
|
@ -573,13 +587,19 @@ class HeartbeatThread(threading.Thread):
|
||||||
self._stop = threading.Event()
|
self._stop = threading.Event()
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
while not self._stop.is_set():
|
try:
|
||||||
self._send_heartbeat()
|
while not self._stop.is_set():
|
||||||
if self._adrenaline.is_set():
|
try:
|
||||||
interval_in_seconds = self._hurry_interval_in_seconds
|
self._send_heartbeat()
|
||||||
else:
|
except TimeoutError:
|
||||||
interval_in_seconds = self._relax_interval_in_seconds
|
pass
|
||||||
self._rest.wait(interval_in_seconds)
|
if self._adrenaline.is_set():
|
||||||
|
interval_in_seconds = self._hurry_interval_in_seconds
|
||||||
|
else:
|
||||||
|
interval_in_seconds = self._relax_interval_in_seconds
|
||||||
|
self._rest.wait(interval_in_seconds)
|
||||||
|
except ConnectionError:
|
||||||
|
pass
|
||||||
|
|
||||||
def relax(self):
|
def relax(self):
|
||||||
self._adrenaline.clear()
|
self._adrenaline.clear()
|
||||||
|
|
@ -590,6 +610,7 @@ class HeartbeatThread(threading.Thread):
|
||||||
self._rest.clear()
|
self._rest.clear()
|
||||||
|
|
||||||
def stop(self):
|
def stop(self):
|
||||||
|
self._rest.set()
|
||||||
self._stop.set()
|
self._stop.set()
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -603,6 +624,16 @@ def find_callback(args, kw=None):
|
||||||
return None, args
|
return None, args
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_host(host, port, resource):
|
||||||
|
if not host.startswith('http'):
|
||||||
|
host = 'http://' + host
|
||||||
|
url_pack = parse_url(host)
|
||||||
|
is_secure = url_pack.scheme == 'https'
|
||||||
|
port = port or url_pack.port or (443 if is_secure else 80)
|
||||||
|
url = '%s:%d%s/%s' % (url_pack.hostname, port, url_pack.path, resource)
|
||||||
|
return is_secure, url
|
||||||
|
|
||||||
|
|
||||||
def _decode_engineIO_content(content):
|
def _decode_engineIO_content(content):
|
||||||
packets = []
|
packets = []
|
||||||
content_index = 0
|
content_index = 0
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,8 @@
|
||||||
import six
|
import six
|
||||||
|
try:
|
||||||
|
from urllib.parse import urlparse as parse_url
|
||||||
|
except ImportError:
|
||||||
|
from urlparse import urlparse as parse_url
|
||||||
|
|
||||||
|
|
||||||
def get_byte(x, index):
|
def get_byte(x, index):
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,51 @@ import requests
|
||||||
from .exceptions import ConnectionError, TimeoutError
|
from .exceptions import ConnectionError, TimeoutError
|
||||||
|
|
||||||
|
|
||||||
|
ENGINEIO_PROTOCOL = 3
|
||||||
|
|
||||||
|
|
||||||
|
class AbstractTransport(object):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class XHR_PollingTransport(AbstractTransport):
|
||||||
|
|
||||||
|
# pass http session
|
||||||
|
# pass session id (can be none)
|
||||||
|
# pass timeout
|
||||||
|
|
||||||
|
def send_packet(self, engineIO_packet_type, engineIO_packet_data):
|
||||||
|
_get_response()
|
||||||
|
|
||||||
|
assert ok
|
||||||
|
|
||||||
|
response = self._http_session.post(self._url, params={
|
||||||
|
'EIO': self._engineIO_protocol,
|
||||||
|
'transport': 'polling',
|
||||||
|
't': self._get_timestamp(),
|
||||||
|
'sid': self._session_id,
|
||||||
|
}, data=_encode_engineIO_content([
|
||||||
|
(engineIO_packet_type, engineIO_packet_data),
|
||||||
|
]), headers={
|
||||||
|
'content-type': 'application/octet-stream',
|
||||||
|
})
|
||||||
|
assert response.content == 'ok'
|
||||||
|
|
||||||
|
def _recv_packet(self):
|
||||||
|
response = _get_response(
|
||||||
|
self._http_session.get,
|
||||||
|
self._url,
|
||||||
|
params={
|
||||||
|
'EIO': self._engineIO_protocol,
|
||||||
|
'transport': 'polling',
|
||||||
|
't': self._get_timestamp(),
|
||||||
|
'sid': self._session_id,
|
||||||
|
},
|
||||||
|
timeout=self._ping_timeout)
|
||||||
|
for engineIO_packet in _decode_engineIO_content(response.content):
|
||||||
|
yield engineIO_packet
|
||||||
|
|
||||||
|
|
||||||
def _get_response(request, *args, **kw):
|
def _get_response(request, *args, **kw):
|
||||||
try:
|
try:
|
||||||
response = request(*args, **kw)
|
response = request(*args, **kw)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue