Вебсокети (Websockets) - це протокол, що забезпечує двосторонній канал зв’язку між браузером та веб-сервером. Його відмінність від звичного способу передачі даних полягає у тому, що з’єднання не закривається після отримання відповіді, а тримається відкритим. Таким чином, йде обмін інформацією у реальному часі, зокрема, можна отримувати від сервера сповіщення без додаткових запитів.
Коли і де доречно використовувати вебсокети:
- Соціальні мережі, зокрема чати та сповіщення про дії інших користувачів у режимі онлайн (хто що лайкнув, прокоментував, запостив і т.д.)
- Фінансові сповіщення, або інші типи сповіщень, де важлива актуальність інформації.
- При сумісній роботі над документами, коли важливо знати, які саме зміни внесла інша людина на даний час.
- Додатки, для яких важливі дані про актуальне місцезнаходження.
- Онлайн-освіта, де важливим критерієм є онлайн-контакт студента і вчителя, або студентів між собою.
- Онлайн-ігри, де важливою є онлайн-взаємодія гравців.
Найчастіше вебсокети реалізують за допомогою Node.js і веб-серверу Tornado. Це пов’язано з тим, що php розвивалась як мова програмування для інтернет-сторінок, для яких принятий формат запит-відповідь.
Проте бібліотека Ratchet дозволяє обійти ці обмеження і реалізувати серверну підтримку Вебсокетів за допомогою php.
Отже, у цій статті ми розглянемо обидва способи реалізації вебсокетів:
- з допомогою Node.js та веб-серверу Tornado;
- з допомогою php (бібліотеки Ratchet) та js.
Ви зможете порівняти їх і обрати той, що підійде вам найкраще. У цій статті ми розглянемо реалізацію найпростішого чату (без аутентифікації). Починаємо!
Вебсокети з допомогою Tornado та js
Tornado - веб-сервер та фреймворк, написаний на Python, який легко розширюється та не блокується під час запитів. Його створили для використання у проекті FriendFeed. Цю компанію придбав Facebook у 2009 році, після чого було відкрито вихідні коди Tornado [1].
Структура додатку:
.
├── app.py - серверна частина
├── index.html
├── requirements.txt
── static
── main.js - front-end
back-end (Tornado):
1. Створюємо нову директорію, де розмістимо проект:
mkdir tornado-websocket-example && cd tornado-websocket-example
2. Створюємо файл requirements.txt з наступним вмістом:
tornado==4.0.2
3. Встановлюємо tornado за допомогою pip:
pip install -r requirements.txt
4. Створюємо файл app.py у корені проекту:
from tornado import websocket, web, ioloop import json cl = [] class IndexHandler(web.RequestHandler): def get(self): self.render("index.html") class SocketHandler(websocket.WebSocketHandler): def check_origin(self, origin): return True def open(self): if self not in cl: cl.append(self) def on_close(self): if self in cl: cl.remove(self) class ApiHandler(web.RequestHandler): @web.asynchronous def get(self, *args): self.finish() @web.asynchronous def post(self, *args): self.finish() message = self.get_argument("m") data = {"message": message} data = json.dumps(data) for c in cl: c.write_message(data) app = web.Application([ (r'/', IndexHandler), (r'/ws', SocketHandler), (r'/api', ApiHandler), (r"/static/(.*)", web.StaticFileHandler, {"path":r"./static/"}), ]) if __name__ == '__main__': app.listen(8080) ioloop.IOLoop.instance().start()
Цей файл - це серце нашої серверної частини.
Короткі пояснення щодо коду
cl = [] - масив під’єднаних користувачів.
app = web.Application([ - прописуємо роутинг.
(r'/', IndexHandler), - прописуємо шлях для рендеру index.html.
(r'/ws', SocketHandler), - прописуємо шлях для під’єднання вебсокетів.
(r'/api', ApiHandler), - прописуємо шлях для надсилання повідомлень.
self.render("index.html") - вказуємо веб-серверу, який файл потрібно рендерити.
cl.append(self) - при під’єднанні нового користувача додаємо його у масив під’єднаних користувачів.
cl.remove (self) - при від’єднанні користувача видаляємо його з масиву під’єднаних користувачів.
def get (self, * args): - тут прописуємо обробку get запитів (ми їх ігноруємо)
def post (self, * args): - тут прописуємо обробку post запитів (розсилаємо повідомлення всіх користувачам)
app.listen (8080) - тут прописуємо порт
front-end (WebSocket):
Створюємо файл index.html у корені папки проектуt
Chat with websockets <!DOCTYPE html> <html> <head> <title>Chat with websockets</title> <link href="http://netdna.bootstrapcdn.com/twitter-bootstrap/2.3.1/css/bootstrap-combined.no-icons.min.css" rel="stylesheet"> <script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.8.2/jquery.min.js"></script> <script type="text/javascript" src="static/main.js"></script> </head> <body> <div class="container"> <h1>Chat with websockets</h1> <hr> WebSocket status : <span id="message"></span> <hr> <div id="chat"> <div class="viewport"></div> <form class="row row-chat"> <div class="input-group"> <input type="text" class="form-control" placeholder="Type your message" /> <span class="input-group-btn"> <button type="submit" class="btn btn-primary">Send</button> </span> </div> </form> </div> </body> </html>
Для зручності підключаємо jQuery та Bootstrap.
Створюємо файл main.js (розміщуємо у папці static) - це серце нашої клієнтської частини:
$( document ).ready(function() {
function wrap_message(msg, $element) {
var html = '<div class="bubble bubble">'+ msg +'</div>';
html = $.trim(html);
$element.html($element.html() + html);
}
var ws = new WebSocket('ws://localhost:8080/ws');
var $message = $('#message');
ws.onopen = function(){
$message.attr("class", 'label label-success');
$message.text('open');
};
ws.onmessage = function(ev){
$message.attr("class", 'label label-info');
$message.hide();
$message.fadeIn("slow");
$message.text('received message');
var json = JSON.parse(ev.data);
wrap_message(json.message, $('.viewport'));
};
ws.onclose = function(ev){
$message.attr("class", 'label label-important');
$message.text('closed');
};
ws.onerror = function(ev){
$message.attr("class", 'label label-warning');
$message.text('error occurred');
};
$('.input-group-btn .btn-primary').click(function(event) {
$.get("api?m=" + $(this).parent().parent().find('input').val());
$.post( "/api", { m: $(this).parent().parent().find('input').val() } );
$(this).parent().parent().find('input').val('');
return false;
});
});
Коротке пояснення щодо коду:
var ws = new WebSocket('ws://localhost:8080') - створення екземляра вебсокету, зверніть увагу, що порт на front-end та back-end має збігатися.
ws.onopen = function() - тут прописується дії при відкритті каналу, у даному прикладі ми просто інформуємо користувача, що WebSocket відкритий.
ws.onmessage = function(ev) - тут прописується дії при отриманні повідомлення, у даному прикладі ми виводимо повідомлення користувачу..
ws.onclose = function(ev) - тут прописується дії при закритті каналу, у даному прикладі ми просто інформуємо користувача, що WebSocket закритий.
Висновок
Запускаємо сервер
python app.py
Відкриваємо http://localhost:8080/ у кількох браузерах, якщо все гаразд - ви можете переписуватись між різними браузерами..
Ми розглянули найпростіший приклад реалізії вебсокетів за веб-серверу Tornado Архів з кодом додатку додається. Лінк на репозиторій. У прикладі була реалізована варіація на тему Hello World
Websockets з допомогою php (бібліотека Ratchet) та js
Структура додатку:
.
├── chat-server.php - файл для запуску сервера
├── composer.json
├── composer.lock
├── index.html
├── src
│ └── MyApp
│ └── Chat.php - серверна частина
└── static
└── main.js - front-end
back-end (Ratchet):
1. Створюємо нову директорію, де розмістимо проект:
mkdir ratchet-websocket-example && cd ratchet-websocket-example
2. Встановлюємо бібліотеку Ratchet за допомогою composer:
php ~/composer.phar require cboden/ratchet && php ~/composer.phar install
3. Створюємо файл chat-server.php:
run();
Цей файл відповідатиме за запуск серверної частини нашого чату.
4. Створюємо папку src і у ній створюємо папку MyApp:
mkdir -p src/MyApp
5. Створюємо файл Chat.php:
clients = new \SplObjectStorage; } public function onOpen(ConnectionInterface $conn) { // Store the new connection to send messages to later $this->clients->attach($conn); echo "New connection! ({$conn->resourceId})\n"; } public function onMessage(ConnectionInterface $from, $msg) { $numRecv = count($this->clients)/* - 1*/; echo sprintf('Connection %d sending message "%s" to %d other connection%s' . "\n" , $from->resourceId, $msg, $numRecv, $numRecv == 1 ? '' : 's'); foreach ($this->clients as $client) { //if ($from !== $client) { // if you don’t want sent message to sender too - uncomment this line // The sender is not the receiver, send to each client connected $client->send(json_encode(array('message' => $msg))); //} // if you don’t want sent message to sender too - uncomment this line } } public function onClose(ConnectionInterface $conn) { // The connection is closed, remove it, as we can no longer send it messages $this->clients->detach($conn); echo "Connection {$conn->resourceId} has disconnected\n"; } public function onError(ConnectionInterface $conn, \Exception $e) { echo "An error has occurred: {$e->getMessage()}\n"; $conn->close(); } }
Цей файл - це серце нашої серверної частини.
Коротке пояснення щодо коду
public function onOpen(ConnectionInterface $conn) - тут прописується дії при відкритті нового каналу, у даному прикладі ми просто виводимо повідомлення у консолі. Тут для прикладу можна обмежити під’єднання користувачів.
public function onMessage(ConnectionInterface $from, $msg) - тут прописується дії при отриманні повідомлення, тут ми показуємо сповіщення у консолі і розсилаємо це повідомлення всім під’єднаним користувачам. У цій частині можна реалізувати вибіркове відправлення повідомлення.
public function onClose(ConnectionInterface $conn) - тут прописується дії при закритті каналу, у прикладі ми просто виводимо сповіщення у консолі.
//if ($from !== $client) { - розкоментуйте цю перевірку, якщо не хочете відсилати повідомлення його автору.
Потенційні проблеми
Спробуйте запустити файл chat-server.php:
php chat-server.php
якщо отримаєте помилку типу “PHP Fatal error: Class 'MyApp\Chat' not found”, ймовірна причина у невірному розміщенні файлу Chat.php. У файлі chat-server.php є стрічка:
require __DIR__ . '/vendor/autoload.php';
яка відповідає за автозавантаження класів. Для того щоб автозавантаження було вдале, місцезнаходження файлу Chat.php має відповідати стандартам PSR-0. У нашому випадку він має знаходитись тут src/MyApp/Chat.php відносно папки додатку.
front-end (WebSocket):
Створюємо файл index.html у корені папки проекту
<!DOCTYPE html> <html> <head> <title>Chat with websockets</title> <link href="//netdna.bootstrapcdn.com/twitter-bootstrap/2.3.1/css/bootstrap-combined.no-icons.min.css" rel="stylesheet"> <link href="/static/main.css" rel="stylesheet"> <script type="text/javascript" src="//ajax.googleapis.com/ajax/libs/jquery/1.8.2/jquery.min.js"></script> <script type="text/javascript" src="/static/main.js"></script> </head> <body> <div class="container"> <h1>Chat with websockets</h1> <hr> WebSocket status : <span id="message"></span> <hr> <div id="chat"> <div class="viewport"></div> <form class="row row-chat"> <div class="input-group"> <input type="text" class="form-control" placeholder="Type your message" /> <span class="input-group-btn"> <button type="submit" class="btn btn-primary">Send</button> </span> </div> </form> </div> </body> </html>
Для зручності підключаємо jQuery та Bootstrap.
Створюємо файл main.js (розміщуємо у папці static) - це серце нашої клієнтської частини:
$( document ).ready(function() { function wrap_message(msg, $element) { var html = '<div class="bubble bubble">'+ msg +'</div>'; html = $.trim(html); $element.html($element.html() + html); } var ws = new WebSocket('ws://localhost:8080/ws'); var $message = $('#message'); ws.onopen = function(){ $message.attr("class", 'label label-success'); $message.text('open'); }; ws.onmessage = function(ev){ $message.attr("class", 'label label-info'); $message.hide(); $message.fadeIn("slow"); $message.text('received message'); var json = JSON.parse(ev.data); wrap_message(json.message, $('.viewport')); }; ws.onclose = function(ev){ $message.attr("class", 'label label-important'); $message.text('closed'); }; ws.onerror = function(ev){ $message.attr("class", 'label label-warning'); $message.text('error occurred'); }; $('.input-group-btn .btn-primary').click(function(event) { $.get("api?m=" + $(this).parent().parent().find('input').val()); $.post( "/api", { m: $(this).parent().parent().find('input').val() } ); $(this).parent().parent().find('input').val(''); return false; }); });
Коротке пояснення щодо коду
var ws = new WebSocket('ws://localhost:8080') - створюючи екземляр вебсокету, зверніть увагу, що порт на front-end та back-end має збігатися.
ws.onopen = function() - тут прописується дії при відкритті каналу, у даному прикладі ми просто інформуємо користувача що WebSocket відкритий.
ws.onmessage = function(ev) - тут прописується дії при отриманні повідомлення, у даному прикладі ми виводимо повідомлення користувачу.
ws.onclose = function(ev) - тут прописується дії при закритті каналу, у даному прикладі ми просто інформуємо користувача, що WebSocket закритий.
Висновок
Запускаємо сервер
php chat-server.php
Відкриваємо файл index.html у різних браузерах, якщо все гаразд - ви можете переписуватись між різними браузерами.
Ми розглянули найпростіший приклад з реалізії вебсокетів за допомогою бібліотеки Ratchet. У прикладі була реалізована варіація на тему Hello World.
Demo на сайті socketo.me