Asynchroniczny PHP & komunikacja czasu rzeczywistego z wykorzystaniem websocketów
Apr 11, 2017
Asynchroniczny PHP& komunikacja czasu
rzeczywistego z wykorzystaniemwebsocketów
who am iNazywam się Łukasz AdamczewskiPracuje w firmie Polcode jako Starszy Programista PHP
Szukaj mnie na @lukeadamczewski
- czyli wasz język tego nie potrafi
CPU IO CPU
czas
IO
Model typowej aplikacji
W czym problem?
Model typowej aplikacji
CPU IO CPU
czas
IO
asynchroniczna instrukcja
CPU
Model non-blocking IO
IO
IO
CPU
IO
CPU
CZA
S
różny czas wykonyw
ania
IO
CPU
oczekiwanie na koniec poprzedniego zadania
Słów kilka o czasach dostępu czyli IO vs CPU
http://norvig.com/21-days.html#answers
fetch from L1 cache memory 0.5 nanosec
fetch from L2 cache memory 7 nanosec
fetch from main memory 100 nanosec
send 2K bytes over 1Gbps network 20,000 nanosec
read 1MB sequentially from memory 0,25 ms = 250,000 ns
Read 1 MB sequentially from SSD* 1 ms
Disk seek 10 ms
Read 1 MB sequentially from disk 20 ms
send packet US to Europe and back 150 ms
* Assuming ~1GB/sec SSD
getUser(432, function (user) { console.log(user.name);}); console.log('Done');
Typowy node.js
getUser(432, function (user) { console.log(user.name);}); console.log('Done');1
Typowy node.js
getUser(432, function (user) { console.log(user.name);}); console.log('Done');1
2
Typowy node.js
Node.JS - webserver “hello world”
var http = require('http');
var server = new http.Server();
server.on('request', function (req, res) {
res.writeHead(
200,
{'Content-Type':'text/plain'}
);
res.end('Hello World');
});
server.listen(8000, '127.0.0.1');
Node.JS - webserver “hello world”
var http = require('http');
var server = new http.Server();
server.on('request', function (req, res) {
res.writeHead(
200,
{'Content-Type':'text/plain'}
);
res.end('Hello World');
});
server.listen(8000, '127.0.0.1');
oczekiwanie na zdarzenie request
kod obsługi zdarzenia wysyłający odpowiednie nagłówki do klienta HTTP
oraz wysyłający odpowiednią wiadomość
Node.JS - webserver “hello world”
var http = require('http');
var server = new http.Server();
server.on('request', function (req, res) {
res.writeHead(
200,
{'Content-Type':'text/plain'}
);
res.end('Hello World');
});
server.listen(8000, '127.0.0.1');
oczekiwanie na zdarzenie request
kod obsługi zdarzenia wysyłający odpowiednie nagłówki do klienta HTTP
oraz wysyłający odpowiednią wiadomość C10K problem
10k połączeń na jednym rdzeniu
Node.JS - webserver “hello world”
var http = require('http');
var server = new http.Server();
server.on('request', function (req, res) {
res.writeHead(
200,
{'Content-Type':'text/plain'}
);
res.end('Hello World');
});
server.listen(8000, '127.0.0.1');
oczekiwanie na zdarzenie request
kod obsługi zdarzenia wysyłający odpowiednie nagłówki do klienta HTTP
oraz wysyłający odpowiednią wiadomość C10K problem
10k połączeń na jednym rdzeniu
Zróbmy to w PHP!
bo w czym problem?
<?php
$server = stream_socket_server('tcp://127.0.0.1:8000');
while ($conn = stream_socket_accept($server, -1)) {
fwrite($conn, "HTTP/1.1 200 OK\r\n");
fwrite($conn, "Content-Length: 3\r\n\r\n");
fwrite($conn, "Hi PHPers\n");
fclose($conn);
}
$ curl 127.0.0.1:8000 -v
> GET / HTTP/1.1
> Host: 127.0.0.1:8000
< HTTP/1.1 200 OK
Hi PHPers
<?php
$server = stream_socket_server('tcp://127.0.0.1:8000');
while ($conn = stream_socket_accept($server, -1)) {
fwrite($conn, "HTTP/1.1 200 OK\r\n");
fwrite($conn, "Content-Length: 3\r\n\r\n");
fwrite($conn, "Hi PHPers\n");
fclose($conn);
}
BLOCKING
IO
<?php
$server = stream_socket_server('tcp://127.0.0.1:8000');
while ($conn = stream_socket_accept($server, -1)) {
fwrite($conn, "HTTP/1.1 200 OK\r\n");
fwrite($conn, "Content-Length: 3\r\n\r\n");
fwrite($conn, "Hi PHPers\n");
fclose($conn);
}
BLOCKING
IO
NON-BLOCKING
IO// If mode is 0, the given stream will be switched to non-blocking modebool stream_set_blocking ( resource $stream , int $mode )
int stream_select ( array &$read , array &$write , array &$except , int $tv_sec [, int $tv_usec = 0 ] )
$readable = $read ?: null;
$writable = $write ?: null;
$except = null;
if (stream_select($readable, $writable, $except, 1)) {
if ($readable) {
foreach ($readable as $stream) { /* code */ }
}
if ($writable) {
foreach ($writable as $stream) { /* code */ }
}
}
Pooling IO
tablica streamów do odczytu
tablica streamów do zapisu
streamy “faworyzowane”
Pooling IOPooling IO$readable = $read ?: null;
$writable = $write ?: null;
$except = null;
if (stream_select($readable, $writable, $except, 1)) {
if ($readable) {
foreach ($readable as $stream) { /* code */ }
}
if ($writable) {
foreach ($writable as $stream) { /* code */ }
}
}
EVENT LOOP
CZYLI MOŻNA LEPIEJ
EVENT LOOP➜ Zarządzanie streamami
➜ Ustawianie timerów jednorazowych i cyklicznych
➜ Ustawianie nextTicków i futureTicków
➜ Maksymalna czas zwłoki (timeout) może być zdefiniowany
➜ Jeżeli nie ma dalszych ticków, timerów lub streamów do obsłużenia -
event loop kończy pracę
Event Loop:Istnieje kilka implementacji z docelowymi backendami:
● StreamSelectLoop - stream_select● LibEventLoop - libevent pecl extension● LibEvLoop - libev pecl extension (najszybszy)● może kiedyś LibUV :)
Demultiplexer:oczekuje na eventy dla zasobów np. nowe połączenie i powiadamia dispatcher, po czym przechodzi do dalszego nasłuchiwania.
Dispatcher:Komponent służący do rejestracji obsługi zdarzeń. Odbiera synchronicznie event z demultiplexera i wybiera właściwy event handler, który następnie uruchamia.
Obsługa zdarzeń:Event Handler który obsługuje przekierowane do niego zdarzenie. Jest to po prostu jeden ze zdefiniowanych wcześniej callbacków.
Zasoby:Czyli tutaj mamy wszystkie streamy które chcemy obsługiwać. Mogą to być także procesy czy np. uchwyty do plików.
Hello World Again
Instalacja z wykorzystaniem composera
curl -sS https://getcomposer.org/installer | php
php composer.phar require react/react
require 'vendor/autoload.php';
$loop = React\EventLoop\Factory::create();
$socket = new React\Socket\Server($loop);
$http = new React\Http\Server($socket, $loop);
$http->on('request', function ($request, $response) {
$response->writeHead(200, ['Content-Type' =>
'text/plain']);
$response->end("Hi Phpers\n");
});
$socket->listen(8000);
$loop->run();
Ekosystem
EVENT LOOP
STREAM
TICKS
SOCKET
HTTP
TIMER
CHILD PROCESSFILESYSTEM
PROMISES
EVENT LOOP
STREAM
TICKS
SOCKET
HTTP
TIMER
CHILD PROCESSFILESYSTEM
PROMISES
Tickiumożliwiają wykonywanie określonych funkcji w ramach Event Loopa. Dzielą się na $loop-
>nextTick($callback) i $loop->futureTick($callback) .
Pierwszy jest wykonywany zawsze na początku każdej iteracji loopa, wykonywanie drugiego
jest zawsze oddelegowane jako ostatnia operacja w ramach iteracji.
Hint: zakolejkowane futureTicki nie będą wykonywane jeśli Event Loop nie ma więcej
zadań. NextTicki uzupełniają Event Loop nowymi zadaniami.
EVENT LOOP
STREAM
TICKS
SOCKET
HTTP
TIMER
CHILD PROCESSFILESYSTEM
PROMISES
Funkcja odmierzania czasu wykonywana w ramach Event Loop w kolejności zaraz po nextTicku.
● $loop->addTimer - jednorazowe wykonanie funkcji po upływie czasu (jak setTimeout)● $loop->addPeriodicTimer - wykonuje funkcje cyklicznie (jak setInterval)● $loop->cancelTimer - zatrzymje timer● $loop->isTimerActive - sprawdza stan działania timera
Hint: nie polegaj na czasie odmierzanym przez timery w 100% ponieważ operacje przetwarzane w Event Loop mogą zablokować je na jakiś czas wynikający z bieżących działań.
STREAM
SOCKET
HTTP
CHILD PROCESSFILESYSTEM
PROMISES
● Opakowuje natywny zasób stream. ● Rejestrowane w ramach Event Loop’a.● Stream do odczytu i zapisu (ReadableStream / WriteableStream)● Potkowość (ang. pipeline). WriteableStream może być ze sobą łączone, więc wyjście
jednego jest wejściem drugiego.● Dane wczytywane / zapisywane do streamów są buforowane
Klasy Funkcjonalne:
CompositeStream - łączy streamy do odczytu i zapisu łącząc obydwie funkcjonalnościThroughStream - umożliwia modyfikacje danch które stream zawiera - filtrowanie. BufferedSink - konwertuje WriteableStream do Promise
STREAM
STREAM
SOCKET
HTTP
CHILD PROCESSFILESYSTEM
PROMISES
Dla funkcji asynchronicznych umożliwia natychmiastowy zwrot wartości, a raczej pewnej zaliczki tej wartości.
Przykład - zamiana hosta na ip:
$factory = new React\Dns\Resolver\Factory();
$dns = $factory->create('8.8.8.8', $loop);
$dns->resolve('igor.io')->then(function ($ip) {
echo "Host: $ip\n";
});
PROMISE
STREAM
SOCKET
HTTP
CHILD PROCESSFILESYSTEM
PROMISES
Komponent sieciowy umożliwiający tworzenie serwerów nasłuchujących nowych połączeń
oraz przetwarzających dane połączonych klientów.
$socket = new React\Socket\Server($loop);
$socket->on('connection', function ($conn) {
$conn->on('data', function ($data, $conn) {
$conn->write($data);
});
});
$socket->listen(1337);
SOCKET
WebsocketyKomunikacja w czasie rzeczywistym
Websockety - zalety
➜ Wsparcie we wszystkich wiodących przeglądarkach (> IE8)
➜ Dwukierunkowość komunikacji➜ Niezależnie wysyłanie wiadomości➜ Protokół oparty na HTTP➜ Niewielki rozmiar pojedynczego pakietu
danych
WebsocketAPI - interfejs kliencki JavaScript
var websocket = new WebSocket('ws://localhost:8000' );
websocket.onopen = function(evt) {};
websocket.onclose = function(evt) {};
websocket.onmessage = function(evt) {};
websocket.onerror = function(evt) {};
Ratchet - WebSockets for PHP
class WS implements MessageComponentInterface {
function onOpen(ConnectionInterface $conn) {}
function onClose(ConnectionInterface $conn) {}
function onError(ConnectionInterface $conn, \Exception $e) {}
function onMessage(ConnectionInterface $from, $msg) {
$from->send($msg);
}
}
$server = IoServer::factory(
new HttpServer(new WsServer(new WS())),
3000
);
$server->run();
The Web Application Messaging Protocol
The Bigger The Better
➜ Remote Procedure Calls (RPC)➜ Publish & Subscribe➜ Autobahn.JS➜ integracja ZeroMQ➜ integracja Redis
Autobahn.JS?
// dla wersji AUTOBAHNJS_VERSION="0.7.1"var session = new ab.Session( "ws://127.0.0.1:3000", function () { // zostaliśmy połączeni }, function () { // zostaliśmy rozłączeni }, { 'skipSubprotocolCheck': true, 'maxRetries': 5, 'retryDelay': 2000 });session.subscribe("http://phpers.pl/event/message", callback);
class WAMP implements Ratchet\Wamp\WampServerInterface {
public function onPublish(ConnectionInterface $conn, $topic, $event, array
$exclude, array $eligible) {}
public function onCall(ConnectionInterface $conn, $id, $topic, array $params) {}
public function onSubscribe(ConnectionInterface $conn, $topic) {}
public function onUnSubscribe(ConnectionInterface $conn, $topic) {}
public function onOpen(ConnectionInterface $conn) {}
public function onClose(ConnectionInterface $conn) {}
public function onError(ConnectionInterface $conn, \Exception $e) {}
}
$server = \Ratchet\Server\IoServer::factory(
new HttpServer(new WsServer(new WampServer(new WAMP()))),
3000
);
$server->run();
Ratchet + WAMP
$loop = React\EventLoop\Factory::create();
$pusher = new WAMP;
$context = new React\ZMQ\Context($loop);
$pull = $context->getSocket(ZMQ::SOCKET_PULL);
$pull->bind('tcp://127.0.0.1:5555');
$pull->on('message', array($pusher, 'onQueueAdded'));
$socket = new React\Socket\Server($loop);
$socket->listen(3000, '0.0.0.0');
$webServer = new IoServer(
new HttpServer(new WsServer(new WampServer($pusher))),
$socket
);
$loop->run();
ZeroMQ + Ratchet
ZeroMQ + Ratchet
$loop = React\EventLoop\Factory::create();
$pusher = new WAMP;
$context = new React\ZMQ\Context($loop);
$pull = $context->getSocket(ZMQ::SOCKET_PULL);
$pull->bind('tcp://127.0.0.1:5555');
$pull->on('message', array($pusher, 'onQueueAdded'));
$socket = new React\Socket\Server($loop);
$socket->listen(3000, '0.0.0.0');
$webServer = new IoServer(
new HttpServer(new WsServer(new WampServer($pusher))),
$socket
);
$loop->run();
$context = new \ZMQContext(); $socket = $context->getSocket( \ZMQ::SOCKET_PUSH, 'websocket');$socket->connect('tcp://127.0.0.1:5555');$socket->send($payloadAsJSON);
ZeroMQ + Ratchet
$loop = React\EventLoop\Factory::create();
$pusher = new WAMP;
$context = new React\ZMQ\Context($loop);
$pull = $context->getSocket(ZMQ::SOCKET_PULL);
$pull->bind('tcp://127.0.0.1:5555');
$pull->on('message', array($pusher, 'onQueueAdded'));
$socket = new React\Socket\Server($loop);
$socket->listen(3000, '0.0.0.0');
$webServer = new IoServer(
new HttpServer(new WsServer(new WampServer($pusher))),
$socket
);
$loop->run();
$context = new \ZMQContext(); $socket = $context->getSocket( \ZMQ::SOCKET_PUSH, 'websocket');$socket->connect('tcp://127.0.0.1:5555');$socket->send($payloadAsJSON);
public function onQueueAdded($payload) {
$payloadData = json_decode($payload, true); // dalsze przetwarzanie }
MAO?➜ WAMP2➜ voryx/Thruway (kompatybilny z nowym
Autobahn.JS)
➜ bixuehujin/reactphp-mysql
➜ DNode
➜ STOMP
➜ AR.Drone
➜ Whois
➜ Childprocess
Koniec!Macie pytania?
Odniesienia i inspiracje
➜ http://blog.wyrihaximus.net/categories/reactphp/➜ Presentation theme - SlidesCarnival➜ Zdjęcia - Death to the Stock Photo ➜ http://www.slideshare.
net/SteveRhoades2/asynchronous-php-and-realtime-messaging
➜ https://speakerdeck.com/jmikola/async-php-with-react➜ https://speakerdeck.com/igorw/react-phpnw