...

четверг, 22 января 2015 г.

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 я молчу.


Итог




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

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




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

Recommended article: Chomsky: We Are All – Fill in the Blank.

This entry passed through the Full-Text RSS service - if this is your content and you're reading it on someone else's site, please read the FAQ at http://ift.tt/jcXqJW.


Комментариев нет:

Отправить комментарий