Debugging alternate transports

This commit is contained in:
Roy Hyunjin Han 2013-11-03 11:59:54 -08:00
commit 018e566049
5 changed files with 400 additions and 216 deletions

View file

@ -15,6 +15,7 @@ var main = io.of('').on('connection', function(socket) {
} }
}); });
socket.on('emit', function() { socket.on('emit', function() {
console.log('hey');
socket.emit('emit_response'); socket.emit('emit_response');
}); });
socket.on('emit_with_payload', function(payload) { socket.on('emit_with_payload', function(payload) {

View file

@ -1,20 +1,21 @@
import logging import logging
import json import json
import requests import requests
import socket
import time import time
import websocket
from collections import namedtuple from collections import namedtuple
from .exceptions import SocketIOConnectionError, _TimeoutError, _PacketError
from .transports import _get_response, _negotiate_transport, TRANSPORTS
_Session = namedtuple('_Session', [
_SocketIOSession = namedtuple('_SocketIOSession', [
'id', 'id',
'heartbeat_timeout', 'heartbeat_timeout',
'server_supported_transports', 'server_supported_transports',
]) ])
_log = logging.getLogger(__name__) _log = logging.getLogger(__name__)
TRANSPORTS = 'websocket', 'xhr-polling', 'jsonp-polling'
PROTOCOL_VERSION = 1 PROTOCOL_VERSION = 1
RETRY_INTERVAL_IN_SECONDS = 1
class BaseNamespace(object): class BaseNamespace(object):
@ -157,22 +158,26 @@ class SocketIO(object):
def wait(self, seconds=None, for_callbacks=False): def wait(self, seconds=None, for_callbacks=False):
try: try:
warning_screen = _yield_warning_screen(seconds, sleep=1) warning_screen = _yield_warning_screen(seconds)
for elapsed_time in warning_screen: for elapsed_time in warning_screen:
try: try:
if for_callbacks and not self._transport.has_ack_callback: if for_callbacks and not self._transport.has_ack_callback:
break break
try: try:
self._process_packet(self._transport.recv_packet()) packet = self._transport.recv_packet().next()
self._process_packet(packet)
except _TimeoutError: except _TimeoutError:
pass pass
except _PacketError as error: except _PacketError as e:
_log.warn('[packet error] %s', error) _log.warn('[packet error] %s', e)
self.heartbeat_pacemaker.send(elapsed_time) self.heartbeat_pacemaker.send(elapsed_time)
except SocketIOConnectionError as error: except SocketIOConnectionError as e:
self.disconnect() try:
warning = Exception('[connection error] %s' % error) warning = Exception('[connection error] %s' % e)
warning_screen.throw(warning) warning_screen.throw(warning)
except StopIteration:
_log.warn(warning)
self.disconnect()
except KeyboardInterrupt: except KeyboardInterrupt:
pass pass
@ -198,29 +203,33 @@ class SocketIO(object):
return self.__transport return self.__transport
except AttributeError: except AttributeError:
pass pass
warning_screen = _yield_warning_screen(seconds=None, sleep=1) warning_screen = _yield_warning_screen(seconds=None)
for elapsed_time in warning_screen: for elapsed_time in warning_screen:
try: try:
self.__transport = self._get_transport() self.__transport = self._get_transport()
break break
except SocketIOConnectionError as error: except SocketIOConnectionError as e:
if not self.wait_for_connection: if not self.wait_for_connection:
raise raise
warning = Exception('[waiting for connection] %s' % error) try:
warning = Exception('[waiting for connection] %s' % e)
warning_screen.throw(warning) warning_screen.throw(warning)
except StopIteration:
_log.warn(warning)
return self.__transport return self.__transport
def _get_transport(self): def _get_transport(self):
self.session = _get_session(self.secure, self.base_url, **self.kw) socketIO_session = _get_socketIO_session(
self.secure, self.base_url, **self.kw)
_log.debug('[transports available] %s', ' '.join( _log.debug('[transports available] %s', ' '.join(
self.session.server_supported_transports)) socketIO_session.server_supported_transports))
# Initialize heartbeat_pacemaker # Initialize heartbeat_pacemaker
self.heartbeat_pacemaker = self._make_heartbeat_pacemaker( self.heartbeat_pacemaker = self._make_heartbeat_pacemaker(
heartbeat_interval=self.session.heartbeat_timeout - 2) heartbeat_interval=socketIO_session.heartbeat_timeout - 2)
self.heartbeat_pacemaker.next() self.heartbeat_pacemaker.next()
# Negotiate transport # Negotiate transport
transport = _negotiate_transport( transport = _negotiate_transport(
self.client_supported_transports, self.session, self.client_supported_transports, socketIO_session,
self.secure, self.base_url, **self.kw) self.secure, self.base_url, **self.kw)
# Update namespaces # Update namespaces
for namespace in self._namespace_by_path.values(): for namespace in self._namespace_by_path.values():
@ -314,143 +323,6 @@ class SocketIO(object):
return lambda *args: self._transport.ack(packet_id, *args) return lambda *args: self._transport.ack(packet_id, *args)
class SocketIOError(Exception):
pass
class _TimeoutError(Exception):
pass
class _PacketError(SocketIOError):
pass
class SocketIOConnectionError(SocketIOError):
pass
class _AbstractTransport(object):
def __init__(self):
self._packet_id = 0
self._callback_by_packet_id = {}
def disconnect(self, path=''):
if not self.connected:
return
if path:
self.send_packet(0, path)
else:
self.connection.close()
def connect(self, path):
self.send_packet(1, path)
def send_heartbeat(self):
self.send_packet(2)
def message(self, path, data, callback):
if isinstance(data, basestring):
code = 3
else:
code = 4
data = json.dumps(data, ensure_ascii=False)
self.send_packet(code, path, data, callback)
def emit(self, path, event, args, callback):
data = json.dumps(dict(name=event, args=args), ensure_ascii=False)
self.send_packet(5, path, data, callback)
def ack(self, packet_id, *args):
packet_id = packet_id.rstrip('+')
data = '%s+%s' % (
packet_id,
json.dumps(args, ensure_ascii=False),
) if args else packet_id
self.send_packet(6, data=data)
def noop(self):
self.send_packet(8)
def send_packet(self, code, path='', data='', callback=None):
packet_id = self.set_ack_callback(callback) if callback else ''
packet_parts = str(code), packet_id, path, data
packet_text = ':'.join(packet_parts)
self.send(packet_text)
_log.debug('[packet sent] %s', packet_text)
def recv_packet(self):
code, packet_id, path, data = None, None, None, None
packet_text = self.recv()
_log.debug('[packet received] %s', packet_text)
try:
packet_parts = packet_text.split(':', 3)
except AttributeError:
raise _PacketError('invalid packet (%s)' % packet_text)
packet_count = len(packet_parts)
if 4 == packet_count:
code, packet_id, path, data = packet_parts
elif 3 == packet_count:
code, packet_id, path = packet_parts
elif 1 == packet_count:
code = packet_parts[0]
return code, packet_id, path, data
def set_ack_callback(self, callback):
'Set callback to be called after server sends an acknowledgment'
self._packet_id += 1
self._callback_by_packet_id[str(self._packet_id)] = callback
return '%s+' % self._packet_id
def get_ack_callback(self, packet_id):
'Get callback to be called after server sends an acknowledgment'
callback = self._callback_by_packet_id[packet_id]
del self._callback_by_packet_id[packet_id]
return callback
@property
def has_ack_callback(self):
return True if self._callback_by_packet_id else False
class _WebsocketTransport(_AbstractTransport):
def __init__(self, session, secure, base_url, **kw):
super(_WebsocketTransport, self).__init__()
url = '%s://%s/websocket/%s' % (
'wss' if secure else 'ws',
base_url, session.id)
_log.debug('[transport selected] %s', url)
try:
self.connection = websocket.create_connection(url)
except socket.timeout as error:
raise SocketIOConnectionError(error)
except socket.error as error:
raise SocketIOConnectionError(error)
self.connection.settimeout(1)
@property
def connected(self):
return self.connection.connected
def recv(self):
try:
return self.connection.recv()
except socket.timeout:
raise _TimeoutError
except socket.error as error:
raise SocketIOConnectionError(error)
except websocket.WebSocketConnectionClosedException:
raise SocketIOConnectionError('server closed connection')
def send(self, packet_text):
try:
self.connection.send(packet_text)
except socket.error:
raise SocketIOConnectionError('could not send %s' % packet_text)
def find_callback(args, kw=None): def find_callback(args, kw=None):
'Return callback whether passed as a last argument or as a keyword' 'Return callback whether passed as a last argument or as a keyword'
if args and callable(args[-1]): if args and callable(args[-1]):
@ -461,7 +333,7 @@ def find_callback(args, kw=None):
return None, args return None, args
def _yield_warning_screen(seconds=None, sleep=0): def _yield_warning_screen(seconds=None):
last_warning = None last_warning = None
for elapsed_time in _yield_elapsed_time(seconds): for elapsed_time in _yield_elapsed_time(seconds):
try: try:
@ -471,7 +343,7 @@ def _yield_warning_screen(seconds=None, sleep=0):
if last_warning != warning: if last_warning != warning:
last_warning = warning last_warning = warning
_log.warn(warning) _log.warn(warning)
time.sleep(sleep) time.sleep(RETRY_INTERVAL_IN_SECONDS)
def _yield_elapsed_time(seconds=None): def _yield_elapsed_time(seconds=None):
@ -483,40 +355,19 @@ def _yield_elapsed_time(seconds=None):
yield time.time() - start_time yield time.time() - start_time
def _get_session(secure, base_url, **kw): def _get_socketIO_session(secure, base_url, **kw):
server_url = '%s://%s/' % ('https' if secure else 'http', base_url) server_url = '%s://%s/' % ('https' if secure else 'http', base_url)
try: try:
response = requests.get(server_url, **kw) response = _get_response(requests.get, server_url, **kw)
except requests.exceptions.ConnectionError: except _TimeoutError as e:
raise SocketIOConnectionError('could not start connection') raise SocketIOConnectionError(e)
status = response.status_code
if 200 != status:
raise SocketIOConnectionError('unexpected status code (%s)' % status)
response_parts = response.text.split(':') response_parts = response.text.split(':')
return _Session( return _SocketIOSession(
id=response_parts[0], id=response_parts[0],
heartbeat_timeout=int(response_parts[1]), heartbeat_timeout=int(response_parts[1]),
server_supported_transports=response_parts[3].split(',')) server_supported_transports=response_parts[3].split(','))
def _negotiate_transport(
client_supported_transports, session,
secure, base_url, **kw):
server_supported_transports = session.server_supported_transports
for supported_transport in client_supported_transports:
if supported_transport in server_supported_transports:
return {
'websocket': _WebsocketTransport,
# 'xhr-polling':
# 'jsonp-polling':
}[supported_transport](session, secure, base_url, **kw)
raise SocketIOError(' '.join([
'could not negotiate a transport:',
'client supports %s but' % ', '.join(client_supported_transports),
'server supports %s' % ', '.join(server_supported_transports),
]))
if __name__ == '__main__': if __name__ == '__main__':
requests_log = logging.getLogger('requests') requests_log = logging.getLogger('requests')
requests_log.setLevel(logging.WARNING) requests_log.setLevel(logging.WARNING)

View file

@ -0,0 +1,14 @@
class SocketIOError(Exception):
pass
class SocketIOConnectionError(SocketIOError):
pass
class _TimeoutError(Exception):
pass
class _PacketError(SocketIOError):
pass

View file

@ -1,7 +1,9 @@
import logging import logging
from socketIO_client import SocketIO, BaseNamespace, find_callback
from unittest import TestCase from unittest import TestCase
from . import SocketIO, BaseNamespace, find_callback
from .transports import TIMEOUT_IN_SECONDS
HOST = 'localhost' HOST = 'localhost'
PORT = 8000 PORT = 8000
@ -10,22 +12,21 @@ PAYLOAD = {'xxx': 'yyy'}
logging.basicConfig(level=logging.DEBUG) logging.basicConfig(level=logging.DEBUG)
class TestSocketIO(TestCase): class BaseMixin(TestCase):
def setUp(self): def setUp(self):
self.socketIO = SocketIO(HOST, PORT)
self.called_on_response = False self.called_on_response = False
def tearDown(self): def tearDown(self):
del self.socketIO del self.socketIO
def on_response(self, *args): def on_response(self, *args):
self.called_on_response = True
for arg in args: for arg in args:
if isinstance(arg, dict): if isinstance(arg, dict):
self.assertEqual(arg, PAYLOAD) self.assertEqual(arg, PAYLOAD)
else: else:
self.assertEqual(arg, DATA) self.assertEqual(arg, DATA)
self.called_on_response = True
def test_disconnect(self): def test_disconnect(self):
'Disconnect' 'Disconnect'
@ -42,100 +43,97 @@ class TestSocketIO(TestCase):
def test_message(self): def test_message(self):
'Message' 'Message'
self.socketIO.define(Namespace) namespace = self.socketIO.define(Namespace)
self.socketIO.message() self.socketIO.message()
self.socketIO.wait(0.1) self.socketIO.wait(self.wait_time_in_seconds)
namespace = self.socketIO.get_namespace()
self.assertEqual(namespace.response, 'message_response') self.assertEqual(namespace.response, 'message_response')
def test_message_with_data(self): def test_message_with_data(self):
'Message with data' 'Message with data'
self.socketIO.define(Namespace) namespace = self.socketIO.define(Namespace)
self.socketIO.message(DATA) self.socketIO.message(DATA)
self.socketIO.wait(0.1) self.socketIO.wait(self.wait_time_in_seconds)
namespace = self.socketIO.get_namespace()
self.assertEqual(namespace.response, DATA) self.assertEqual(namespace.response, DATA)
def test_message_with_payload(self): def test_message_with_payload(self):
'Message with payload' 'Message with payload'
self.socketIO.define(Namespace) namespace = self.socketIO.define(Namespace)
self.socketIO.message(PAYLOAD) self.socketIO.message(PAYLOAD)
self.socketIO.wait(0.1) self.socketIO.wait(self.wait_time_in_seconds)
namespace = self.socketIO.get_namespace()
self.assertEqual(namespace.response, PAYLOAD) self.assertEqual(namespace.response, PAYLOAD)
def test_message_with_callback(self): def test_message_with_callback(self):
'Message with callback' 'Message with callback'
self.socketIO.message(callback=self.on_response) self.socketIO.message(callback=self.on_response)
self.socketIO.wait_for_callbacks(seconds=0.1) self.socketIO.wait_for_callbacks(seconds=self.wait_time_in_seconds)
self.assertTrue(self.called_on_response) self.assertTrue(self.called_on_response)
def test_message_with_callback_with_data(self): def test_message_with_callback_with_data(self):
'Message with callback with data' 'Message with callback with data'
self.socketIO.message(DATA, self.on_response) self.socketIO.message(DATA, self.on_response)
self.socketIO.wait_for_callbacks(seconds=0.1) self.socketIO.wait_for_callbacks(seconds=self.wait_time_in_seconds)
self.assertTrue(self.called_on_response) self.assertTrue(self.called_on_response)
def test_emit(self): def test_emit(self):
'Emit' 'Emit'
self.socketIO.define(Namespace) namespace = self.socketIO.define(Namespace)
self.socketIO.emit('emit') self.socketIO.emit('emit')
self.socketIO.wait(0.1) self.socketIO.wait(self.wait_time_in_seconds)
self.assertEqual(self.socketIO.get_namespace().args_by_event, { self.assertEqual(namespace.args_by_event, {
'emit_response': (), 'emit_response': (),
}) })
def test_emit_with_payload(self): def test_emit_with_payload(self):
'Emit with payload' 'Emit with payload'
self.socketIO.define(Namespace) namespace = self.socketIO.define(Namespace)
self.socketIO.emit('emit_with_payload', PAYLOAD) self.socketIO.emit('emit_with_payload', PAYLOAD)
self.socketIO.wait(0.1) self.socketIO.wait(self.wait_time_in_seconds)
self.assertEqual(self.socketIO.get_namespace().args_by_event, { self.assertEqual(namespace.args_by_event, {
'emit_with_payload_response': (PAYLOAD,), 'emit_with_payload_response': (PAYLOAD,),
}) })
def test_emit_with_multiple_payloads(self): def test_emit_with_multiple_payloads(self):
'Emit with multiple payloads' 'Emit with multiple payloads'
self.socketIO.define(Namespace) namespace = self.socketIO.define(Namespace)
self.socketIO.emit('emit_with_multiple_payloads', PAYLOAD, PAYLOAD) self.socketIO.emit('emit_with_multiple_payloads', PAYLOAD, PAYLOAD)
self.socketIO.wait(0.1) self.socketIO.wait(self.wait_time_in_seconds)
self.assertEqual(self.socketIO.get_namespace().args_by_event, { self.assertEqual(namespace.args_by_event, {
'emit_with_multiple_payloads_response': (PAYLOAD, PAYLOAD), 'emit_with_multiple_payloads_response': (PAYLOAD, PAYLOAD),
}) })
def test_emit_with_callback(self): def test_emit_with_callback(self):
'Emit with callback' 'Emit with callback'
self.socketIO.emit('emit_with_callback', self.on_response) self.socketIO.emit('emit_with_callback', self.on_response)
self.socketIO.wait_for_callbacks(seconds=0.1) self.socketIO.wait_for_callbacks(seconds=self.wait_time_in_seconds)
self.assertTrue(self.called_on_response) self.assertTrue(self.called_on_response)
def test_emit_with_callback_with_payload(self): def test_emit_with_callback_with_payload(self):
'Emit with callback with payload' 'Emit with callback with payload'
self.socketIO.emit('emit_with_callback_with_payload', self.socketIO.emit('emit_with_callback_with_payload',
self.on_response) self.on_response)
self.socketIO.wait_for_callbacks(seconds=0.1) self.socketIO.wait_for_callbacks(seconds=self.wait_time_in_seconds)
self.assertTrue(self.called_on_response) self.assertTrue(self.called_on_response)
def test_emit_with_callback_with_multiple_payloads(self): def test_emit_with_callback_with_multiple_payloads(self):
'Emit with callback with multiple payloads' 'Emit with callback with multiple payloads'
self.socketIO.emit('emit_with_callback_with_multiple_payloads', self.socketIO.emit('emit_with_callback_with_multiple_payloads',
self.on_response) self.on_response)
self.socketIO.wait_for_callbacks(seconds=0.1) self.socketIO.wait_for_callbacks(seconds=self.wait_time_in_seconds)
self.assertTrue(self.called_on_response) self.assertTrue(self.called_on_response)
def test_emit_with_event(self): def test_emit_with_event(self):
'Emit to trigger an event' 'Emit to trigger an event'
self.socketIO.on('emit_with_event_response', self.on_response) self.socketIO.on('emit_with_event_response', self.on_response)
self.socketIO.emit('emit_with_event', PAYLOAD) self.socketIO.emit('emit_with_event', PAYLOAD)
self.socketIO.wait(0.1) self.socketIO.wait(self.wait_time_in_seconds)
self.assertTrue(self.called_on_response) self.assertTrue(self.called_on_response)
def test_ack(self): def test_ack(self):
'Trigger server callback' 'Trigger server callback'
self.socketIO.define(Namespace) namespace = self.socketIO.define(Namespace)
self.socketIO.emit('ack', PAYLOAD) self.socketIO.emit('ack', PAYLOAD)
self.socketIO.wait(0.1) self.socketIO.wait(self.wait_time_in_seconds)
self.assertEqual(self.socketIO.get_namespace().args_by_event, { self.assertEqual(namespace.args_by_event, {
'ack_response': (PAYLOAD,), 'ack_response': (PAYLOAD,),
'ack_callback_response': (PAYLOAD,), 'ack_callback_response': (PAYLOAD,),
}) })
@ -146,7 +144,7 @@ class TestSocketIO(TestCase):
chatNamespace = self.socketIO.define(Namespace, '/chat') chatNamespace = self.socketIO.define(Namespace, '/chat')
newsNamespace = self.socketIO.define(Namespace, '/news') newsNamespace = self.socketIO.define(Namespace, '/news')
newsNamespace.emit('emit_with_payload', PAYLOAD) newsNamespace.emit('emit_with_payload', PAYLOAD)
self.socketIO.wait(0.1) self.socketIO.wait(self.wait_time_in_seconds)
self.assertEqual(mainNamespace.args_by_event, {}) self.assertEqual(mainNamespace.args_by_event, {})
self.assertEqual(chatNamespace.args_by_event, {}) self.assertEqual(chatNamespace.args_by_event, {})
self.assertEqual(newsNamespace.args_by_event, { self.assertEqual(newsNamespace.args_by_event, {
@ -154,6 +152,30 @@ class TestSocketIO(TestCase):
}) })
class Test_WebsocketTransport(BaseMixin):
def setUp(self):
super(Test_WebsocketTransport, self).setUp()
self.socketIO = SocketIO(HOST, PORT, transports=['websocket'])
self.wait_time_in_seconds = 0.1
class Test_XHR_PollingTransport(BaseMixin):
def setUp(self):
super(Test_XHR_PollingTransport, self).setUp()
self.socketIO = SocketIO(HOST, PORT, transports=['xhr-polling'])
self.wait_time_in_seconds = TIMEOUT_IN_SECONDS + 1
class Test_JSONP_PollingTransport(BaseMixin):
def setUp(self):
super(Test_JSONP_PollingTransport, self).setUp()
self.socketIO = SocketIO(HOST, PORT, transports=['jsonp-polling'])
self.wait_time_in_seconds = TIMEOUT_IN_SECONDS + 1
class Namespace(BaseNamespace): class Namespace(BaseNamespace):
def initialize(self): def initialize(self):

View file

@ -0,0 +1,296 @@
import json
import logging
import re
import requests
import socket
import time
import websocket
from itertools import izip
from .exceptions import SocketIOError, SocketIOConnectionError, _TimeoutError
TRANSPORTS = 'websocket', 'xhr-polling', 'jsonp-polling'
BOUNDARY = u'\ufffd'.encode('utf-8')
TIMEOUT_IN_SECONDS = 2
_log = logging.getLogger(__name__)
class _AbstractTransport(object):
def __init__(self):
self._packet_id = 0
self._callback_by_packet_id = {}
def disconnect(self, path=''):
if not self.connected:
return
if path:
self.send_packet(0, path)
else:
self.close()
def connect(self, path):
self.send_packet(1, path)
def send_heartbeat(self):
self.send_packet(2)
def message(self, path, data, callback):
if isinstance(data, basestring):
code = 3
else:
code = 4
data = json.dumps(data, ensure_ascii=False)
self.send_packet(code, path, data, callback)
def emit(self, path, event, args, callback):
data = json.dumps(dict(name=event, args=args), ensure_ascii=False)
self.send_packet(5, path, data, callback)
def ack(self, packet_id, *args):
packet_id = packet_id.rstrip('+')
data = '%s+%s' % (
packet_id,
json.dumps(args, ensure_ascii=False),
) if args else packet_id
self.send_packet(6, data=data)
def noop(self):
self.send_packet(8)
def send_packet(self, code, path='', data='', callback=None):
packet_id = self.set_ack_callback(callback) if callback else ''
packet_parts = str(code), packet_id, path, data
packet_text = ':'.join(packet_parts)
self.send(packet_text)
_log.debug('[packet sent] %s', packet_text)
def recv_packet(self):
code, packet_id, path, data = None, None, None, None
for packet_text in self.recv():
_log.debug('[packet received] %s', packet_text)
try:
packet_parts = packet_text.split(':', 3)
except AttributeError:
_log.warn('[packet error] %s', packet_text)
continue
packet_count = len(packet_parts)
if 4 == packet_count:
code, packet_id, path, data = packet_parts
elif 3 == packet_count:
code, packet_id, path = packet_parts
elif 1 == packet_count:
code = packet_parts[0]
yield code, packet_id, path, data
def set_ack_callback(self, callback):
'Set callback to be called after server sends an acknowledgment'
self._packet_id += 1
self._callback_by_packet_id[str(self._packet_id)] = callback
return '%s+' % self._packet_id
def get_ack_callback(self, packet_id):
'Get callback to be called after server sends an acknowledgment'
callback = self._callback_by_packet_id[packet_id]
del self._callback_by_packet_id[packet_id]
return callback
@property
def has_ack_callback(self):
return True if self._callback_by_packet_id else False
class _WebsocketTransport(_AbstractTransport):
def __init__(self, socketIO_session, secure, base_url, **kw):
super(_WebsocketTransport, self).__init__()
url = '%s://%s/websocket/%s' % (
'wss' if secure else 'ws',
base_url, socketIO_session.id)
try:
self._connection = websocket.create_connection(url)
except socket.timeout as e:
raise SocketIOConnectionError(e)
except socket.error as e:
raise SocketIOConnectionError(e)
self._connection.settimeout(TIMEOUT_IN_SECONDS)
@property
def connected(self):
return self._connection.connected
def send(self, packet_text):
try:
self._connection.send(packet_text)
except socket.error:
raise SocketIOConnectionError('could not send %s' % packet_text)
def recv(self):
try:
yield self._connection.recv()
except socket.timeout:
raise _TimeoutError
except socket.error as e:
raise SocketIOConnectionError(e)
except websocket.WebSocketConnectionClosedException as e:
raise SocketIOConnectionError('connection closed (%s)' % e)
def close(self):
self._connection.close()
class _XHR_PollingTransport(_AbstractTransport):
def __init__(self, socketIO_session, secure, base_url, **kw):
super(_XHR_PollingTransport, self).__init__()
self._url = '%s://%s/xhr-polling/%s' % (
'https' if secure else 'http',
base_url, socketIO_session.id)
self._connected = True
self._http_session = _prepare_http_session(kw)
@property
def connected(self):
return self._connected
@property
def _params(self):
return dict(t=time.time())
def send(self, packet_text):
_get_response(
self._http_session.post,
self._url,
params=self._params,
data=packet_text,
timeout=TIMEOUT_IN_SECONDS)
def recv(self):
response = _get_response(
self._http_session.get,
self._url,
params=self._params,
timeout=TIMEOUT_IN_SECONDS)
encoded_text = response.text.encode('utf-8')
if not encoded_text.startswith(BOUNDARY):
yield encoded_text.decode('utf-8')
for packet_text in _yield_text_from_framed_data(encoded_text):
yield packet_text
def close(self):
_get_response(
self._http_session.get,
self._url,
params=dict(self._params.items() + [('disconnect', True)]))
self._connected = False
class _JSONP_PollingTransport(_AbstractTransport):
DATA_PATTERN = re.compile(r'io.j\[(\d+)\]\("(.*)"\);')
def __init__(self, socketIO_session, secure, base_url, **kw):
super(_JSONP_PollingTransport, self).__init__()
self._url = '%s://%s/jsonp-polling/%s' % (
'https' if secure else 'http',
base_url, socketIO_session.id)
self._connected = True
self._http_session = _prepare_http_session(kw)
self._jsonp_id = 0
@property
def connected(self):
return self._connected
@property
def _params(self):
return dict(t=time.time(), jsonp=self._jsonp_id)
def send(self, packet_text):
_get_response(
self._http_session.post,
self._url,
params=self._params,
data='d=%s' % requests.utils.quote(packet_text),
headers={'content-type': 'application/x-www-form-urlencoded'},
timeout=TIMEOUT_IN_SECONDS)
def recv(self):
response = _get_response(
self._http_session.get,
self._url,
params=self._params,
headers={'content-type': 'application/javascript'},
timeout=TIMEOUT_IN_SECONDS)
encoded_text = response.text.encode('utf-8')
if not encoded_text.startswith(BOUNDARY):
self._jsonp_id, encoded_data = self.DATA_PATTERN.match(
encoded_text).groups()
yield encoded_data.decode('utf-8')
for packet_text in _yield_text_from_framed_data(encoded_text):
yield packet_text
def close(self):
_get_response(
self._http_session.get,
self._url,
params=dict(self._params.items() + [('disconnect', True)]))
self._connected = False
def _negotiate_transport(
client_supported_transports, session,
secure, base_url, **kw):
server_supported_transports = session.server_supported_transports
for supported_transport in client_supported_transports:
if supported_transport in server_supported_transports:
_log.debug('[transport selected] %s', supported_transport)
return {
'websocket': _WebsocketTransport,
'xhr-polling': _XHR_PollingTransport,
'jsonp-polling': _JSONP_PollingTransport,
}[supported_transport](session, secure, base_url, **kw)
raise SocketIOError(' '.join([
'could not negotiate a transport:',
'client supports %s but' % ', '.join(client_supported_transports),
'server supports %s' % ', '.join(server_supported_transports),
]))
def _yield_text_from_framed_data(framed_data):
parts = [x.decode('utf-8') for x in framed_data.split(BOUNDARY)]
for text_length, text in izip(parts[1::2], parts[2::2]):
if text_length == str(len(text)):
yield text
warning = 'invalid declared length=%s for packet_text=%s' % (
text_length, text)
_log.warn('[packet error] %s', warning)
def _get_response(request, *args, **kw):
try:
response = request(*args, **kw)
except requests.exceptions.Timeout as e:
raise _TimeoutError(e)
except requests.exceptions.ConnectionError as e:
raise SocketIOConnectionError(e)
except requests.exceptions.SSLError as e:
raise SocketIOConnectionError('could not negotiate SSL (%s)' % e)
status = response.status_code
if 200 != status:
raise SocketIOConnectionError('unexpected status code (%s)' % status)
return response
def _prepare_http_session(kw):
http_session = requests.Session()
http_session.headers.update(kw.get('headers', {}))
http_session.auth = kw.get('auth')
http_session.proxies.update(kw.get('proxies', {}))
http_session.hooks.update(kw.get('hooks', {}))
http_session.params.update(kw.get('params', {}))
http_session.verify = kw.get('verify')
http_session.cert = kw.get('cert')
http_session.cookies.update(kw.get('cookies', {}))
return http_session