mkdir tornado-chat
cd tornado-chat
установка в систему нужных команд (установщик python pip и virtualenv)
sudo apt-get install python3-pip virtualenv
установка виртуального окружения в проект
virtualenv -p python3 venv
pip install tornado
Если нужно перечислить все библиотеки в одном текстовом файле то запуск следующий.
pip install -r requirements.txt
# tornado-server.py
import tornado.ioloop
import tornado.web
class MainHandler(tornado.web.RequestHandler):
def get(self):
self.write("Hello, world")
def make_app():
return tornado.web.Application([
(r"/", MainHandler),
])
if __name__ == "__main__":
app = make_app()
app.listen(8888)
tornado.ioloop.IOLoop.current().start()
python tornado-server.py
Если нужно запускать короче (без python) то в начале файла пишем
#!/usr/bin/env python
....
import tornado.ioloop
import tornado.web
.....
Отмечаем как исполняемый и запускаем.
chmod +x tornado-server.py
./tornado-server.py
Видим следующую картину.
...
import tornado.web
loader = tornado.template.Loader(".")
...
<html>
<head>
</head>
<body>
<h1> {{ myvar }} </h1>
</body>
</html>
class MainHandler(tornado.web.RequestHandler):
def get(self):
self.render("index.html", myvar='Hello world')
# tornado-server.py
from tornado import autoreload
...
if __name__ == "__main__":
print('Starting server on 8888 port')
autoreload.start()
autoreload.watch('.')
autoreload.watch('index.html')
После этих изменений и перегрузки сервера (останавливать по ctrl+c) сервер будет автоматически перегружаться после каждого изменения кода сервера (tornado-server.py) и шаблона (index.html).
Определим и передадим словарь в шаблон.
# tornado-server.py
users = [
{
'name': 'Dima',
'uid': 1
},
{
'name': 'Vovan',
'uid': 2
},
]
...
class MainHandler(tornado.web.RequestHandler):
def get(self):
self.render("index.html", myvar='Hello world', users=users)
Шаблон index.html.
<ul>
{% for user in users %}
<li> {{ user['name'] }} </li>
{% end %}
</ul>
Добавим форму в шаблон.
<form method="POST" action="/submit">
<input id="chat_message" type="text" name="message" />
<input id="submit_button" type="submit" value="Submit" />
</form>
После сабмита ловим 404.
class FormHandler(tornado.web.RequestHandler):
def post(self):
print(self.get_argument('message'))
self.render("index.html", myvar='Hello world', users=users)
Прицепим этот класс к роутингу с url заканчивающимся на /submit.
def make_app():
return tornado.web.Application([
(r"/", MainHandler),
...
(r"/submit", FormHandler),
...
])
После сабмита видим что переменная из формы попала на сервер и вывелась в консоле.
Определим новый класс - наследник от WebSocketHandler.
from tornado.websocket import WebSocketHandler
...
class WebsocketHandler(tornado.websocket.WebSocketHandler):
def open(self):
print('Open connection')
def on_message(self, message):
print('got message')
def on_close(self):
print('close connection')
Тут мы должны ОБЯЗАТЕЛЬНО переопределить 3 метода.
def make_app():
return tornado.web.Application([
(r"/", MainHandler),
(r"/submit", FormHandler),
(r"/websocket", WebsocketHandler),
])
<script>
$( document ).ready(function() {
var ws = new WebSocket("ws://localhost:8888/websocket");
});
</script>
Видим что подключение установлено.
$( document ).ready(function() {
var ws = new WebSocket("ws://localhost:8888/websocket");
ws.onopen = function() {
message = {'action': 'connect', 'message': 'new connection'}
ws.send(message);
};
...
По параметру action мы будем определять тип сообщения для принятия решения о дальнейших действиях как на стороне сервера так и на клиенте.
def on_message(self, message):
print('got message')
print(message)
...
Сообщение приходит после обновления страницы при каждом соединении с веб-сокетом. Однако в виде js-объекта а нам необходимо распарсить этот объект из json в словарь питона.
Для этого нам нужно на клиенте передавать строку, пропуская данные через ф-цию JSON.stringify() в шаблоне.
def on_message(self, message):
print('got message')
print(JSON.stringify(message))
Далее преобразовать на стороне сервера.
import json
.....
def on_message(self, message):
print('got message')
json_message = json.loads(message)
print(json_message)
Передадим произвольную строку назад в браузер после принятия сообщения из него.
Воспользуемся методом write_message(string)
def on_message(self, message):
print('got message')
json_message = json.loads(message)
print(json_message)
self.write_message('message from socket server')
Теперь необходимо обработать событие поступления сообщения с сервера на стороне JS.
За это отвечает метод on_message().
ws.onmessage = function (evt) {
alert(evt);
}
Как видно в evt поступает сложный объект события.
Чтобы выудить из него полезную нагрузку (наше строковое сообщение) используем следующую конструкцию:
jdata = JSON.parse(evt.data);
Свойство data и будет содержать наше сообщение.
Передадим вместо строки словарь питона.
self.write_message({'action': 'onmessage', 'message': message })
Как видим в параметре message зашла строка с экранированными спец символами (результат работы JSON.stringify которая преобразует объект в строку) , которую можно легко преобразовать обратно в json объект на стороне клиента ф-цией JSON.parse(evt.data).
Организуем на сервере бесконечный цикл с задержкой в 1 сек и отправкой таймстампа.
import datetime
import time
.....
def open(self):
print('Open connection')
while True:
time.sleep(1)
self.write_message({'action': 'ping', 'message': str(datetime.datetime.now())})
Так как каждое соединение с сокет сервером порождает отдельный экземпляр объекта соединения внутри Tornado (self), нам необходимо определить место хранения этих объектов внутри сервера для того чтоб иметь возможность:
извлекать произвольный объект соединения для передачи сообщения адресно
циклично отправлять широковещательное сообщение всем активным клиентам
создавать и удалять эти соединенbz из списка при коннекте и разрыве соответственно
Для этих целей более подходит словарь {key1: Object1, key2: Object2} чем список [Object1, Object2] т.к. позволяет извлекать и удалять соединение по ключу.
В качестве ключа будем использовать md5 хешик из таймстампа для уникальности.
Импортируем библиотеку для md5.
import hashlib
Сгенерируем хэш.
def open(self):
print('Open connection')
sign = hashlib.md5(str(datetime.datetime.now()).encode('utf-8')).hexdigest()
self.client_id = sign # сохраним в текущем экземпляре.
Определим пустой контейнер (словарь) для хранения текущих (активных) сокет соединений.
ws_clients = {}
class WebsocketHandler(tornado.websocket.WebSocketHandler):
...
Добавим текущее соединение в словарь при подлючении клиента и выведем их в консоль.
def open(self):
print('Open connection')
sign = hashlib.md5(str(datetime.datetime.now()).encode('utf-8')).hexdigest()
ws_clients[sign] = self
print(ws_clients)
...
Осталось удалить соединение при дисконнекте.
def on_close(self):
print('close connection')
del ws_clients[self.client_id]
Обратите внимание на то что переменная ws_clients объявлена ЗА классом и является глобальной. Поэтому мы легко получаем к ней доступ из любого метода любого соединения (без self).
Таким образо теперь при обрыве соединения (закрытие вкладки браузера, самого браузера или обновления страницы) соединение будет удалено из списка (словаря), либо добавлено в случае установления соединения.