Реалізація Websockets за допомогою php (бібліотеки Ratchet) і веб-серверу Tornado

05.06.2015
Implementing Websockets using php (Ratchet library) or Tornado web server
Автор:

Вебсокети (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

3 votes, Рейтинг: 4.3

Також по темі

1

20 порад, як покращити юзабіліті сайту електронної комерції і отримувати від нього вищий прибуток.

2

У нашому блозі ми розповідаємо про те, як правильно налаштувати стандарти відображення сайту на Wordpress для різних мов.

3

Компанію InternetDevels було включено до списку найкращих розробників на Drupal за версією Clutch.co

4

InternetDevels відкриває новий офіс у Львові!

5

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

Subscribe to our blog updates