WebSocket RPC или как написать живое WEB приложение для браузера
В статье речь пойдет о технологии 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) 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 я молчу.
Итог Использую этот модуль на нескольких проектах. Везде работает как надо, основные баги вычистил.Вместо заключения Спасибо моим коллегам и друзъям за то, что помогали находить ошибки и иногда присылали патчи. Ну, и тебе, читатель. Если ты это читаешь, с учетом сухости статьи, тогда тебе уж точно интересна эта тема.