python

WebSocket RPC или как написать живое WEB приложение для браузера

  • пятница, 23 января 2015 г. в 02:10:40
http://habrahabr.ru/post/248507/



В статье речь пойдет о технологии WebSocket. Точнее не о самой технологии, а о том, как ее можно использовать. Я давно слежу за ней. Еще когда в 2011 году один мой коллега прислал мне ссылку на стандарт, пробежав глазами, я как-то расстроился. Выглядело настолько круто, и я думал, что в момент, когда это появится в популярных браузерах, я уже буду планировать, на что потратить свою пенсию. Но все оказалось не так, и как гласит caniuse.com WebSocket не поддерживается только в Opera Mini (надо бы провести голосование, как давно кто-либо видел Opera Mini).

Кто трогал WebSocketы руками, тот наверняка знает, что работать с API тяжело. В Javascript API достаточно низкоуровневый (принять сообщение — отправить сообщение), и придется разрабатывать алгоритм, как этими сообщениями обмениваться. Поэтому и была предпринята попытка упростить работу с вебсокетами.

Так и появился WSRPC. Для нетерпеливых вот простое демо.


Идея



Основная идея в том, чтобы дать разработчику простой API на Javascript вроде:

var url = window.location.protocol==="https:"?"wss://":"ws://" + window.location.host + '/ws/';
RPC = WSRPC(url, 5000);

// Инициализируем объект
RPC.call('test').then(function (data) {
    // посылаем аргументы как *args
    RPC.call('test.serverSideFunction', [1,2,3]).then(function (data) {
        console.log("Server return", data)
    });

    // Объект как аргументы **kwargs
    RPC.call('test.serverSideFunction', {size: 1, id: 2, lolwat: 3}).then(function (data) {
        console.log("Server return", data)
    });
});

// Если с сервера придет вызов 'whoAreYou', вызовем следующую функцию
// ответим на сервер то, что после return
RPC.addRoute('whoAreYou', function (data) {
    return window.navigator.userAgent;
});

RPC.connect();


И на python:
import tornado.web
import tornado.httpserver
import tornado.ioloop
import time
from wsrpc import WebSocketRoute, WebSocket, wsrpc_static

class ExampleClassBasedRoute(WebSocketRoute):
    def init(self, **kwargs):
        return self.socket.call('whoAreYou', callback=self._handle_user_agent)

    def _handle_user_agent(self, ua):
        print ua

    def serverSideFunction(self, *args, **kwargs):
        return args, kwargs

WebSocket.ROUTES['test'] = ExampleClassBasedRoute
WebSocket.ROUTES['getTime'] = lambda: time.time()

if __name__ == "__main__":
    http_server = tornado.httpserver.HTTPServer(tornado.web.Application((
        # Генерирует url со статикой q.min.js и wsrpc.min.js
        # (подключать в том же порядке)
        wsrpc_static(r'/js/(.*)'),
        (r"/ws/", WebSocket),
        (r'/(.*)', tornado.web.StaticFileHandler, {
             'path': os.path.join(project_root, 'static'),
             'default_filename': 'index.html'
        }),
    ))
    http_server.listen(options.port, address=options.listen)
    WebSocket.cleapup_worker()
    tornado.ioloop.IOLoop.instance().start()


Особенности


Поясню некоторые моменты того, как это работает.

JavaScript

Браузер инициализирует новый объект RPC, после этого мы вызываем методы, но WebSocket еще не соединился. Не беда, вызовы стали в очередь, которую мы разгребаем при удачном соединении, или отвергаем все обещания (promises), очищая очередь при следующем неудачном соединении. Библиотека все время пытается соединиться с сервером (на события соединения и отсоединения тоже можно подписаться RPC.addEventListener(«onconnect», func)). Но пока мы не запустили RPC.connect(), мы мирно складываем вызовы в очередь внутри RPC.

После соединения сериализуем в JSON наши параметры и отправляем на сервер сообщение вида:
{"serial":3,"call":"test","arguments": null}

На что сервер отвечает:
{"data": {}, "serial": 3, "type": "callback"}
где serial это номер вызова.
После получения ответа библиотка на JS разрешает обещание (resolve promise), и мы вызываем то, что за then. После этого делаем еще один вызов и так далее…
Замечу также, что между вызовом и ответом на него, может пройти сколько угодно времени.

Python

На Python регистрируются вызовы в объекте WebSocket. Атрибут класса (class-property) ROUTES это словарь (dict), который хранит ассоциацию того, как называется вызов, и какая функция или класс его обслуживает.

Если указана функция, она просто вызывается, и ее результат передается клиенту.

Когда мы указываем класс, и клиент хоть раз вызывает его, мы создаем экземпляр этого класса и храним его вместе с соединением до самого его разрыва. Это очень удобно, можно сделать statefull соединение с браузером.

Доступ к методам осуществляется через точку. Если метод называется с подчеркивания (_hidden), то доступ из Javascript к нему не получить.

Еще от клиента к серверу, и от сервера к клиенту пробрасываются исключения. Когда я это реализовал, а был просто ошарашен. Увидеть Javascript traceback в питонячих логах — гарантированный когнтивный диссонанс. Ну, а про питонячьи Exceptions в JS я молчу.

Итог


Использую этот модуль на нескольких проектах. Везде работает как надо, основные баги вычистил.

Вместо заключения


Спасибо моим коллегам и друзъям за то, что помогали находить ошибки и иногда присылали патчи. Ну, и тебе, читатель. Если ты это читаешь, с учетом сухости статьи, тогда тебе уж точно интересна эта тема.

upd 1: Добавил WebSocket.cleapup_worker() в примеры