TornadIO2¶
This is implementation of the Socket.IO realtime transport library on top of the Tornado framework.
TornadIO2 is compatible with 0.7+ version of the Socket.IO and implements most of the features found in original Socket.IO server software.
Topics¶
Migrating from previous TornadIO version¶
TornadIO2 has some incompatible API changes.
1. Instead of having one router handler, TornadIO2 exposes transports as first-class Tornado handlers. This saves some memory per active connection, because instead of having two handlers per request (router and transport), you will now have only one. This change affects how TornadIO2 is initialized and plugged into your Tornado application:
ChatServer = tornadio2.router.TornadioRouter(ChatConnection)
# Fill your routes here
routes = [(r"/", IndexHandler)]
# Extend list of routes with Tornadio2 URLs
routes.extend(ChatServer.urls)
application = tornado.web.Application(routes)
or alternative approach:
ChatServer = tornadio2.router.TornadioRouter(ChatConnection)
application = tornado.web.Application(ChatServer.apply_routes([(r"/", IndexHandler)]))
2. SocketConnection.on_open was changed to accept single request
parameter. This parameter
is instance of the ConnectionInfo class which contains some helper methods like
get_argument(), get_cookie(), etc. Also, if you return False
from your on_open
handler,
TornadIO2 will reject connection.
Example:
class MyConnection(SocketConnection):
def on_open(self, request):
self.user_id = request.get_argument('id')
if not self.user_id:
return False
This variable is also available for multiplexed connections and will contain query string parameters from the socket.io endpoint connection request.
3. There’s major behavioral change in exception handling. If something blows up and is not handled, whole connection is closed (including any running multiplexed connections). In previous TornadIO version it was silently dropping currently open transport connection and expecting for socket.io to reconnect.
4. Persistent connections are not dropped immediately - there’s a chance that person might reconnect with same session id and we will want to pick it up.
5. Socket.IO 0.7 dropped support for xhr-multipart transport, so you can safely remove it from your configuration file.
Unicode¶
TornadIO2 supports unicode for all transports. When you send something, it will be automatically converted to the unicode (assuming that it is not unicode already).
Few rules:
send
has following logic in place:- If message is object or dictionary, it will be json encoded into unicode string
- If message is unicode, it will be sent as is
- If message is non-unicode, not an object and not a dictionary, it will be converted to string and converted to unicode using utf-8 encoding. If your string is in some other encoding (multi-byte, etc), it is up for you to handle encoding and pass your data in unicode (or utf-8 encoded).
emit
has similar logic:- It will convert event name into unicode string (if it is not). It is expected that event name will only use latin characters
- All
emit
arguments will be json encoded into unicode string
3. All incoming messages will be automatically converted to unicode strings. You can expect to receive unicode strings in your message handler and events.
Multiplexed connections¶
Starting from socket.io 0.7, there’s new concept of multiplexed connections: you can have multiple “virtual” connections working through one transport connection. TornadIO2 supports this transparently, but you have to tell TornadIO how to route multiplexed connection requests. To accomplish this, you can either use built-in routing mechanism or implement your own.
To use built-in routing, declare and initialize __endpoints__
dictionary in
your main connection class:
class ChatConnection(SocketConnection):
def on_message(self, msg):
pass
class PingConnection(SocketConnection):
def on_message(self, msg):
pass
class MyRouterConnection(SocketConnection):
__endpoints__ = {'/chat': ChatConnection,
'/ping': PingConnection}
def on_message(self, msg):
pass
MyRouter = tornadio2.router.TornadioRouter(MyRouterConnection)
On client side, create two connections:
var chat = io.connect('http://myserver/chat'),
ping = io.connect('http://myserver/ping');
chat.send('Hey Chat');
ping.send('Hey Ping');
So, you have to have three connection classes for 2 virtual connections - that’s how socket.io works. If you want, you can send some messages to MyRouterConnection as well, if you will connect like this:
var conn = io.connect('http://myserver'),
chat = io.connect('http://myserver/chat'),
ping = io.connect('http://myserver/ping');
conn.send('Hey Connection!')
Events¶
Instead of having “just” messages, socket.io 0.7 introduced new concept of events. Event is just a name and collection of parameters.
TornadIO2 provides easy-to-use syntax sugar which emulates RPC calls from the client to your python code. Check following example:
class MyConnection(SocketConnection):
@event('hello')
def test(self, name):
print 'Hello %s' % name
self.emit('thanks', name=name)
self.emit('hello', name, 'foobar')
In your client code, to call this event, do something like:
sock.emit('hello', {name: 'Joes'});
You can also use positional parameters. For previous example, you can also do something like:
sock.emit('hello', 'Joes')
To handle event on client side, use following code:
sock.on('thanks', function(data) {
alert(data.name);
});
sock.on('hello', function(name, dummy) {
alert('Hey ' + name + ' ' + dummy);
});
However, take care - if method signature does not match (missing parameters, extra parameters, etc), your connection will blow up and self destruct.
event
decorator can be used without parameter and it will use event handler name
in this case:
class MyConnection(SocketConnection):
@event
def hello(self, name):
print 'Hello %s' % name
If you don’t like this event handling approach, just override on_event
in your
socket connection class and handle them by yourself:
class MyConnection(SocketConnection):
def on_event(self, name, *args, **kwargs):
if name == 'hello':
print 'Hello %s' % (kwargs['name'])
self.emit('thanks', name=kwargs['name'])
There’s also some magic involved in event message parsing to make it easier to work with events.
If you send data from client using following code:
sock.emit('test', {a: 10, b: 10});
TornadIO2 will unpack dictionary into kwargs
parameters and pass it to the
on_event
handler. However, if you pass more than one parameter, Tornadio2 won’t
unpack them into kwargs
and will just pass parameters as args
. For example, this
code will lead to args
being passed to on_event
handler:
sock.emit('test', 1, 2, 3, {a: 10, b: 10});
Acknowledgments¶
New feature of the socket.io 0.7+. When you send message to the client, you now have way to get notified when client receives the message. To use this, pass a callback function when sending a message:
class MyConnection(SocketConnection):
def on_message(self, msg):
self.send(msg, self.my_callback)
def my_callback(self, msg, ack_data):
print 'Got ack for my message: %s' % message
ack_data
contains acknowledgment data sent from the client, if any.
To send event with acknowledgement, use SocketConnection.emit_ack
method:
class MyConnection(SocketConnection):
def on_message(self, msg):
self.emit_ack(self.my_callback, 'hello')
def my_callback(self, event, ack_data):
print 'Got ack for my message: %s' % msg
If you want to send reverse confirmation with a message, just return value you want to send from your event handler:
class MyConnection(SocketConnection):
@event('test')
def test(self):
return 'Joes'
and then, in your javascript code, you can do something like:
sock.emit('test', function(data) {
console.log(data); // data will be 'Joes'
});
If you want to return multiple arguments, return them as tuple:
class MyConnection(SocketConnection):
@event('test')
def test(self):
return 'Joes', 'Mike', 'Mary'
On client-side, you can access them by doing something like:
sock.emit('test', function(name1, name2, name3) {
console.log(name1, name2, name3); // data will be 'Joes'
});
Generator-based asynchronous interface¶
tornadio2.gen
module is a wrapper around tornado.gen
API. You might want to take a
look at Tornado documentation for this module, which can be found here.
While you can use tornado.gen
API without any problems, sometimes you may want to handle your messages in
order they were received. If you will decorate your functions with tornado.gen.engine
, your code will work asynchronously -
second message might get handled before first message got processed.
To prevent this situation, TornadIO2 provides helpful decorator: tornadio2.gen.sync_engine
. sync_engine
will queue incoming
calls if there’s another instance of the function running. As a result, it will call your method synchronously without
blocking the io_loop. This decorator only works with class methods, don’t try to use it for functions - it requires self
to properly function.
Lets check following example:
from tornadio2 import gen
class MyConnection(SocketConnection):
@gen.sync_engine
def on_message(self, query):
http_client = AsyncHTTPClient()
response = yield gen.Task(http_client.fetch, 'http://google.com?q=' + query)
self.send(response.body)
If client will quickly send two messages, it will work “synchronously” - on_message
won’t be called for second message
till handling of first message is finished.
However, if you will change decorator to gen.engine
, message handling will be asynchronous and might be out of order:
from tornadio2 import gen
class MyConnection(SocketConnection):
@gen.engine
def on_message(self, query):
http_client = AsyncHTTPClient()
response = yield gen.Task(http_client.fetch, 'http://google.com?q=' + query)
self.send(response.body)
If client will quickly send two messages, server will send response as soon as response is ready and if it takes longer to handle first message, response for second message will be sent first.
As a nice feature, you can also decorate your event handlers or even wrap main on_event
method, so
all events can be synchronous when using asynchronous calls.
tornadio2.gen
API will only work with the yield
based methods (methods that produce generators). If you implement your
asynchronous code using explicit callbacks, it is up for you how to synchronize their execution order.
TBD: performance considerations, python iterator performance.
Statistics¶
TornadIO2 captures some counters:
Name | Description |
---|---|
Sessions | |
max_sessions | Maximum number of sessions |
active_sessions | Number of currently active sessions |
Connections | |
max_connections | Maximum number of connections |
active_connections | Number of currently active connections |
connections_ps | Number of opened connections per second |
Packets | |
packets_sent_ps | Packets sent per second |
packets_recv_ps | Packets received per second |
Stats are captured by the router object and can be accessed
through the stats
property:
MyRouter = tornadio2.TornadioRouter(MyConnection)
print MyRouter.stats.dump()
For more information, check stats module API or stats
example.
Deployment¶
Going big¶
So, you’ve finished writting your application and want to share it with rest of the world, so you started thinking about scalability, deployment options, etc.
Most of the Tornado servers are deployed behind the nginx, which also used to serve static content. Unfortunately, older versions of the nginx did not support HTTP 1.1 and as a result, proxying of the websocket connections did not work. However, starting from nginx 1.1 there’s support of HTTP 1.1 protocol and websocket proxying works. You can get more information here.
Alternative solution is to use HAProxy. Sample HAProxy configuration file can be found here. You can hide your application and TornadIO instances behind one HAProxy instance running on one port to avoid cross-domain AJAX calls, which ensures greater compatibility.
However, HAProxy does not work on Windows, so if you plan to deploy your solution on Windows platform, you might want to take look into MLB.
Scalability¶
Scalability is completely different beast. It is up for you, as a developer, to design scalable architecture of the application.
For example, if you need to have one large virtual server out of your multiple physical processes (or even servers), you have to come up with some kind of the synchronization mechanism. This can be either common meeting point (and also point of failure), like memcached, redis, etc. Or you might want to use some transporting mechanism to communicate between servers, for example something AMQP based, ZeroMQ or just plain sockets with your custom protocol.
Performance¶
Unfortunately, TornadIO2 was not properly benchmarked and this is something that will be accomplished in the future.
Bugs and Workarounds¶
There are some known bugs in socket.io (valid for socket.io-client 0.8.6). I consider them “show-stoppers”, but you can work around them with some degree of luck.
Connect after disconnect¶
Unfortunately, disconnection is bugged in socket.io client. If you close socket connection,
io.connect()
to the same endpoint will silently fail. If you try to forcibly connect
associated socket, it will work, but you have to make sure that you’re not setting up
callbacks again, as you will end up having your callbacks called twice.
Link: https://github.com/LearnBoost/socket.io-client/issues/251
For now, if your main connection was closed, you have few options:
var conn = io.connect(addr, {'force new connection': true});
conn.on('message', function(msg) { alert('msg') });
or alternative approach:
io.j = [];
io.sockets = [];
var conn = io.connect(addr);
conn.on('message', function(msg) { alert('msg') });
or separate reconnection from initial connection:
var conn = io.connect(addr);
conn.on('disconnect', function(msg) { conn.socket.reconnect(); });
If you use first approach, you will lose multiplexing for good.
If you use second approach, apart of it being quite hackish, it will clean up existing
sockets, so socket.io will have to create new one and will use it to connect to endpoints.
Also, instead of clearing io.sockets
, you can remove socket which matches your URL.
If you use third approach, make sure you’re not setting up events again.
On a side note, if you can avoid using disconnect()
for socket, do so.
Query string parameters¶
Current implementation of socket.io client stores query string parameters on session level. So, if you have multiplexed connections and want to pass parameters to them - it is not possible.
See related bug report: https://github.com/LearnBoost/socket.io-client/issues/331
So, you can not expect query string parametes to be passed to your virtual connections and
will have to structure your JS code, so first io.connect()
will include anything you
want to pass to the server.
Windows and anti-virus software¶
It is known that some anti-virus software (I’m lookin at you, Avast) is messing up with websockets protocol if it is going through the port 80. Avast proxies all traffic through local proxy which does some on-the-fly traffic analysis and their proxy does not support websockets - all messages sent from server are queued in their proxy, connection is kept alive, etc.
Unfortunately, socket.io does not support this situation and won’t fallback to other protocol.
There are few workarounds: 1. Disable websockets for everyone (duh) 2. Run TornadIO2 (or your proxy/load balancer) on two different ports and have simple logic
on client side to switch to another port if connection fails
Socket.IO developers are aware of the problem and next socket.io version will provide official workaround.
Here’s more detailed article on the matter: https://github.com/LearnBoost/socket.io/wiki/Socket.IO-and-firewall-software.
API¶
tornadio2.conn
¶
Connection¶
tornadio2.conn¶
Tornadio connection implementation.
-
class
tornadio2.conn.
SocketConnection
(session, endpoint=None)[source]¶ Subclass this class and define at least on_message() method to make a Socket.IO connection handler.
To support socket.io connection multiplexing, define _endpoints_ dictionary on class level, where key is endpoint name and value is connection class:
class MyConnection(SocketConnection): __endpoints__ = {'/clock'=ClockConnection, '/game'=GameConnection}
ClockConnection
andGameConnection
should derive from theSocketConnection
class as well.SocketConnection
has usefulevent
decorator. Wrap method with it:class MyConnection(SocketConnection): @event('test') def test(self, msg): print msg
and then, when client will emit ‘test’ event, you should see ‘Hello World’ printed:
sock.emit('test', {msg:'Hello World'});
-
SocketConnection.
on_open
(request)[source]¶ Default on_open() handler.
Override when you need to do some initialization or request validation. If you return False, connection will be rejected.
You can also throw Tornado HTTPError to close connection.
- request
ConnectionInfo
object which contains caller IP address, query string parameters and cookies associated with this request.
For example:
class MyConnection(SocketConnection): def on_open(self, request): self.user_id = request.get_argument('id', None) if not self.user_id: return False
-
SocketConnection.
on_message
(message)[source]¶ Default on_message handler. Must be overridden in your application
-
SocketConnection.
on_event
(name, args=[], kwargs={})[source]¶ Default on_event handler.
By default, it uses decorator-based approach to handle events, but you can override it to implement custom event handling.
- name
- Event name
- args
- Event args
- kwargs
- Event kwargs
There’s small magic around event handling. If you send exactly one parameter from the client side and it is dict, then you will receive parameters in dict in kwargs. In all other cases you will have args list.
For example, if you emit event like this on client-side:
sock.emit('test', {msg='Hello World'})
you will have following parameter values in your on_event callback:
name = 'test' args = [] kwargs = {msg: 'Hello World'}
However, if you emit event like this:
sock.emit('test', 'a', 'b', {msg='Hello World'})
you will have following parameter values:
name = 'test' args = ['a', 'b', {msg: 'Hello World'}] kwargs = {}
-
SocketConnection.
send
(message, callback=None, force_json=False)[source]¶ Send message to the client.
- message
- Message to send.
- callback
- Optional callback. If passed, callback will be called when client received sent message and sent acknowledgment back.
- force_json
- Optional argument. If set to True (and message is a string) then the message type will be JSON (Type 4 in socket_io protocol). This is what you want, when you send already json encoded strings.
tornadio2.flashserver
¶
tornadio2.flashserver¶
Flash Socket policy server implementation. Merged with minor modifications from the SocketTornad.IO project.
tornadio2.gen
¶
tornadio2.gen¶
Generator-based interface to make it easier to work in an asynchronous environment.
Wrapper¶
tornadio2.gen.
sync_engine
(func)[source]¶Queued version of the
tornado.gen.engine
.Prevents calling of the wrapped function if there is already one instance of the function running asynchronously. Function will be called synchronously without blocking io_loop.
This decorator can only be used on class methods, as it requires
self
to make sure that calls are scheduled on instance level (connection) instead of class level (method).
tornadio2.periodic
¶
tornadio.flashserver¶
This module implements customized PeriodicCallback from tornado with support of the sliding window.
-
class
tornadio2.periodic.
Callback
(callback, callback_time, io_loop)[source]¶ Custom implementation of the Tornado.Callback with support of callback timeout delays.
tornadio2.persistent
¶
tornadio2.polling
¶
tornadio2.polling¶
This module implements socket.io polling transports.
-
class
tornadio2.polling.
TornadioPollingHandlerBase
(application, request, **kwargs)[source]¶ Polling handler base
Request¶
Callbacks¶
Output¶
Sessions¶
-
TornadioPollingHandlerBase.
_get_session
(session_id)[source]¶ Get session if exists and checks if session is closed.
-
class
tornadio2.polling.
TornadioXHRPollingHandler
(application, request, **kwargs)[source]¶ xhr-polling transport implementation
tornadio2.preflight
¶
tornadio2.preflight¶
Transport protocol router and main entry point for all socket.io clients.
tornadio2.proto
¶
tornadio2.proto¶
Socket.IO protocol related functions
Packets¶
-
tornadio2.proto.
disconnect
(endpoint=None)[source]¶ Generate disconnect packet.
- endpoint
- Optional endpoint name
-
tornadio2.proto.
connect
(endpoint=None)[source]¶ Generate connect packet.
- endpoint
- Optional endpoint name
-
tornadio2.proto.
message
(endpoint, msg, message_id=None, force_json=False)[source]¶ Generate message packet.
- endpoint
- Optional endpoint name
- msg
- Message to encode. If message is ascii/unicode string, will send message packet. If object or dictionary, will json encode and send as is.
- message_id
- Optional message id for ACK
- force json
- Disregard msg type and send the message with JSON type. Usefull for already JSON encoded strings.
-
tornadio2.proto.
event
(endpoint, name, message_id, *args, **kwargs)[source]¶ Generate event message.
- endpoint
- Optional endpoint name
- name
- Event name
- message_id
- Optional message id for ACK
- args
- Optional event arguments.
- kwargs
- Optional event arguments. Will be encoded as dictionary.
-
tornadio2.proto.
ack
(endpoint, message_id, ack_response=None)[source]¶ Generate ACK packet.
- endpoint
- Optional endpoint name
- message_id
- Message id to acknowledge
- ack_response
- Acknowledgment response data (will be json serialized)
JSON¶
tornadio2.router
¶
tornadio2.router¶
Transport protocol router and main entry point for all socket.io clients.
-
class
tornadio2.router.
TornadioRouter
(connection, user_settings={}, namespace='socket.io', io_loop=None)[source]¶ TornadIO2 router implementation
Public¶
-
TornadioRouter.
__init__
(connection, user_settings={}, namespace='socket.io', io_loop=None)[source]¶ Constructor.
- connection
- SocketConnection class instance
- user_settings
- Settings
- namespace
- Router namespace, defaulted to ‘socket.io’
- io_loop
- IOLoop instance, optional.
-
TornadioRouter.
urls
¶ List of the URLs to be added to the Tornado application
tornadio2.server
¶
tornadio2.server¶
Implements handy wrapper to start FlashSocket server (if FlashSocket protocol is enabled). Shamesly borrowed from the SocketTornad.IO project.
-
class
tornadio2.server.
SocketServer
(application, no_keep_alive=False, io_loop=None, xheaders=False, ssl_options=None, auto_start=True)[source]¶ HTTP Server which does some configuration and automatic setup of Socket.IO based on configuration. Starts the IOLoop and listening automatically in contrast to the Tornado default behavior. If FlashSocket is enabled, starts up the policy server also.
-
__init__
(application, no_keep_alive=False, io_loop=None, xheaders=False, ssl_options=None, auto_start=True)[source]¶ Initializes the server with the given request callback.
If you use pre-forking/start() instead of the listen() method to start your server, you should not pass an IOLoop instance to this constructor. Each pre-forked child process will create its own IOLoop instance after the forking process.
- application
- Tornado application
- no_keep_alive
- Support keep alive for HTTP connections or not
- io_loop
- Optional io_loop instance.
- xheaders
- Extra headers
- ssl_options
- Tornado SSL options
- auto_start
- Set auto_start to False in order to have opportunities to work with server object and/or perform some actions after server is already created but before ioloop will start. Attention: if you use auto_start param set to False you should start ioloop manually
-
tornadio2.session
¶
Session¶
tornadio2.session¶
Active TornadIO2 connection session.
-
class
tornadio2.session.
Session
(conn, server, request, expiry=None)[source]¶ Socket.IO session implementation.
Session has some publicly accessible properties:
- server
- Server association. Server contains io_loop instance, settings, etc.
- remote_ip
- Remote IP
- is_closed
- Check if session is closed or not.
-
Session.
close
(endpoint=None)[source]¶ Close session or endpoint connection.
- endpoint
- If endpoint is passed, will close open endpoint connection. Otherwise will close whole socket.
-
Session.
is_closed
¶ Check if session was closed
-
class
tornadio2.session.
ConnectionInfo
(ip, arguments, cookies)[source]¶ Connection information object.
Will be passed to the
on_open
handler of your connection class.Has few properties:
- ip
- Caller IP address
- cookies
- Collection of cookies
- arguments
- Collection of the query string arguments
Return single cookie by its name
tornadio2.sessioncontainer
¶
tornadio2.sessioncontainer¶
Simple heapq-based session implementation with sliding expiration window support.
-
class
tornadio2.sessioncontainer.
SessionBase
(session_id=None, expiry=None)[source]¶ Represents one session object stored in the session container. Derive from this object to store additional data.
-
__init__
(session_id=None, expiry=None)[source]¶ Constructor.
session_id
- Optional session id. If not provided, will generate new session id.
expiry
- Expiration time. If not provided, will never expire.
-