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