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

Підсумок

Використовую цей модуль на декількох проектах. Скрізь працює як треба, основні баги вичистив.

Замість висновку

Спасибі моїм колегам і друзям за те, що допомагали знаходити помилки і іноді надсилали патчі. Ну, і тобі, читачу. Якщо ти це читаєш, з урахуванням сухості статті, тоді тобі точно цікава ця тема.

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.