$30 off During Our Annual Pro Sale. View Details »

Keep your users up-to-date in real-time with WebSockets!

Keep your users up-to-date in real-time with WebSockets!

Live document collaboration, playing cooperative and competetive games, updating sports scores, booking seats. Stateless and belated nature of HTTP requests is not a perfect match for these and other similar use cases. WebSockets offer immediate delivery of messages in two-way communication between the client and the server. Instead of periodic polling for new messages, they are pushed to the receiver over TCP/IP connection. Implementing WebSockets is not limited to technologies like node.js but has also been possible in PHP for quite some time with impressive results. In this talk, I introduced this technology and told the audience how to successfully adopt it in their PHP applications while avoiding problems and pitfalls.

https://joind.in/event/phpday-2016/keep-your-users-up-to-date-in-realtime-with-websockets

Ondřej Mirtes

May 14, 2016
Tweet

More Decks by Ondřej Mirtes

Other Decks in Programming

Transcript

  1. Keep your users
    up-to-date in real-time
    with WebSockets!
    Ondřej Mirtes
    phpDay, May 14th 2016

    View Slide

  2. ...is boring!
    There's a much broader set of technologies you can use to your advantage, your users' comfort
    and to lower your monthly hosting bill. Just don't be afraid to try them out.

    View Slide

  3. I love using modern technologies that fit the solved problem better.

    View Slide

  4. Inspected webpage is doing the same AJAX request over and over again checking if there's
    something new to show to the user.

    View Slide

  5. Periodical updates
    Event occurs User is notified about it
    time

    View Slide

  6. ½ interval delay

    on average!
    User waits a really long time to see the update. This approach also causes unnecessary server
    strain - building and checking state from clean slate in every request.

    View Slide

  7. Event occurs
    Sending to server
    Server processing
    Sending event to client
    User is notified about it
    Push
    time
    With push principle, we leave waiting completely out of the picture, everything happens
    instantly. User is notified immediately. This whole timeline usually happens in tens of ms.

    View Slide

  8. WebSockets
    TCP/IP connection, not HTTP (Connection upgrade handshake at the beginning)
    Not request-response, but a two-way stream of messages
    I encourage adopting optimistic UI

    View Slide

  9. youtu.be/XrvleVBa6aE
    WebSockets is not only a web technology, you can use it in native mobile apps too.
    Video of a prototype I made shows communication between web and a native app, React.PHP
    server forwards messages behind the scenes.

    View Slide

  10. Booking theater seats
    I'd like to show a few cool things I made with WebSockets.

    View Slide

  11. Papertrail iOS app & React.PHP backend

    View Slide

  12. Notifications about other users editing a page

    View Slide

  13. WebSockets in PHP
    Long-running process

    listening on a port

    View Slide

  14. PHP is ready for long-running processes. Memory leaks do not happen because of language's
    fault anymore. But it certainly requires more discipline and thinking about memory allocation.

    View Slide

  15. WebSockets in PHP
    Asynchronous processing
    PHP is single-threaded, but still capable of asynchronous processing. Waiting happens in the OS.
    Suitable for HTTP requests, SQL queries, filesystem access…

    View Slide

  16. WebSockets in PHP
    Event loop
    Handling Events
    Waiting

    for Events
    Core of async programming. Possible with vanilla PHP using stream_select function.

    Use abstraction libraries like React.PHP for nicer code.

    View Slide

  17. WebSockets in PHP
    Beware of blocking functions!
    Request 1
    R1 - blocking the thread
    Request 2 Request 3
    R2 R3
    Responses

    View Slide

  18. WebSockets in PHP
    Non-blocking – start processing ASAP
    Request 1 Request 2 Request 3
    Event loop controls the flow. PHP process does something useful at all times and does not block.

    View Slide

  19. How asynchronous code looks
    $db
    ->query('SELECT url FROM foo')
    ->then(function ($result) {

    return $httpClient->get($result['url']);
    })
    ->then(function ($response) {
    $webSocketClient->send($response->body);
    });
    echo "processing...\n";
    Using promises. The echo is run first because the callbacks are called in the next loop run (tick)
    at the earliest.

    View Slide

  20. WebSockets in PHP
    composer require cboden/ratchet

    View Slide

  21. Initial setup
    $loop = React\EventLoop\Factory::create();
    $server = new \React\Socket\Server($loop);

    $server->listen(8080, '0.0.0.0');
    new \Ratchet\Server\IoServer(

    new \Ratchet\Http\HttpServer(

    new \Ratchet\WebSocket\WsServer($app)

    ),

    $server

    );

    $loop->run();
    Outside your main app's MVC and router, usually inside Symfony Console.
    $loop->run() blocks – nothing after it gets executed until $loop->stop() is called.

    View Slide

  22. $app – \Ratchet\MessageComponentInterface
    function onOpen(ConnectionInterface $conn);
    function onClose(ConnectionInterface $conn);
    function onError(
    ConnectionInterface $conn,
    \Exception $e
    );
    function onMessage(ConnectionInterface $from, $msg);
    Entrypoint to the WebSockets app. Something like a controller – don't make a fat one.
    When client connects, I don't know anything about him, what is he interested in etc.
    The first message should usually be about what the client subscribes to.

    View Slide

  23. Simple chat app
    private $clients;
    function __construct() {
    $this->clients = new \SplObjectStorage();
    }
    function onOpen(ConnectionInterface $conn) {
    $this->clients->attach($conn);
    }
    function onClose(ConnectionInterface $conn) {
    $this->clients->detach($conn);
    }

    View Slide

  24. Simple chat app
    function onMessage(
    ConnectionInterface $from,
    $msg
    ) {
    foreach ($this->clients as $client) {
    if ($client !== $from) {
    $client->send($msg);
    }
    }
    }
    Avoid sending the message back to the original author.

    View Slide

  25. Everything should be asynchronous
    function onMessage(
    ConnectionInterface $from,
    $msg
    ) {
    $this->db->query('...')->then(
    function ($result) use ($from) {
    $from->send($result['foo']);
    }
    );
    }
    Stuff dependent on incoming messages should be async in order not to block the server.
    Async is more complex – anything can happen between the request is sent and the response is
    received. The client could have already been disconnected.

    View Slide

  26. Debugging
    /** \Monolog\Logger */
    private $logger;
    function onOpen(ConnectionInterface $conn) {
    $ua = $connection
    ->WebSocket
    ->request
    ->getHeader('User-Agent');
    $this->logger->addDebug('Connected: ' . $ua);
    }
    Use Monolog even for debug output – easy to switch to logging somewhere else

    just by configuration.

    View Slide

  27. Timers
    $timer = $loop->addTimer(5, function () {
    echo "5 seconds passed!\n";
    });
    $loop->cancelTimer($timer);
    $loop->addPeriodicTimer(5, function () {
    echo "Every 5 seconds\n";
    });
    Non-blocking alternative to sleep(). Useful in WebSockets for expiration or tracking inactivity.
    Can occur a little bit later – after "waiting for events" loop phase.

    View Slide

  28. One process to rule them all
    $_SESSION
    One process handling all requests – global variables like $_SESSION not usable

    View Slide

  29. One process to rule them all
    Client Browser
    WebSockets Server HTTP Web Server
    Session Storage
    You need to abstract session storage, session data are connection-specific
    Solved by Ratchet SessionProvider connected to a Symfony session handler

    View Slide

  30. One process to rule them all
    Stateless API approach
    Or do not use sessions at all and authorize user via a token in an HTTP header

    View Slide

  31. Inter-process commucation
    PHP-FPM
    CLI
    WebSockets Server
    Client Browsers
    When something happens in the classic big web app and you need to notify the WebSockets
    clients about it, you can do it through a message queue.

    View Slide

  32. Deployment
    Use Supervisord to keep the process alive
    Restart the process when deploying a new version
    Implement pcntl_signal to handle kill signals

    View Slide

  33. Nginx as a proxy
    Client Browser
    HTTP or HTTPS
    80 or 443
    HTTP
    any port
    Ratchet
    Exotic ports are blocked in corporate environments, you want to provide access to the WS server
    through 80 or 443. Ratchet does not support HTTPS, can be solved with Nginx proxy.

    View Slide

  34. Nginx as a proxy
    upstream websockets_ratchet {
    server localhost:8080;
    }
    Name of the upstream will be used in proxy configuration.

    View Slide

  35. Nginx as a proxy
    location /heartbeat-websocket {
    proxy_pass http://websockets_ratchet;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";
    proxy_set_header Host $host;
    proxy_read_timeout 86400;
    proxy_redirect off;
    proxy_buffering off;
    }
    It's important to turn the buffering off otherwise it doesn't work as well.

    View Slide

  36. Ratchet 1 Ratchet 2 Ratchet 3 Ratchet 4
    Loadbalancer
    Client Browser
    Horizontal scaling by adding Ratchet servers. They have to communicate between each other.

    View Slide

  37. caniuse.com/#feat=websockets

    View Slide

  38. Client-side support detection
    'WebSocket' in window
    It's better to detect certain features like this instead of a browser vendor and version, too
    difficult to maintain the matrix of supporting browsers.

    View Slide

  39. WebSockets JavaScript API

    var ws = new WebSocket('ws://localhost:8080');

    var ws = new WebSocket('wss://localhost:8080');
    Constructor causes the client to connect. You can connect to any server, no cross-origin
    restriction forced by the browser. wss is secure variant, on HTTPS you can connect to wss only.

    View Slide

  40. WebSockets JavaScript API
    ws.onopen = function () {}
    ws.onerror = function (error) {}
    ws.onclose = function () {}
    ws.onmessage = function (event) {
    alert(event.data);
    }
    ws.send(JSON.stringify({'foo': 'bar'}));

    View Slide

  41. Reconnecting
    var openConnection = function () {
    var ws = new WebSocket(...);
    ws.onclose = function () {
    setTimeout(function () {
    openConnection();
    }, 2000);
    };
    };
    User can go offline and come back online at any time. Improve this implementation

    by presenting that the connection is offline and prolonging the interval between attempts.

    View Slide

  42. Content Security Policy
    connect-src ws://example.com
    CSP is used to harden the security of your app – essentially whitelisting what the browser

    can do. self does not work because it matches the scheme of the website - ws is not http.

    View Slide

  43. WebSockets PHP Client

    wat?
    Until now we've talked about the web browser as the WebSockets client.
    But PHP can be a client too!

    View Slide

  44. WebSockets PHP client
    composer require ratchet/pawl

    View Slide

  45. WebSockets PHP client
    Functional tests
    Verify end-to-end that your WebSockets server works in PHPUnit

    View Slide

  46. WebSockets PHP client
    Server monitoring
    Use it to provide HTTP endpoint for services like Pingdom or Uptime Robot that do not support
    WebSockets directly. Return HTTP 200 if you can connect to the server, 500 if the server is down.

    View Slide

  47. WebSockets PHP client
    Ratchet
    WebSockets
    AJAX
    Long-polling server
    WebSockets
    If you need to support older browsers, you can write another React.PHP server to provide

    AJAX longpolling to your WebSockets app. This server will persist WebSockets connections
    across HTTP requests.

    View Slide

  48. Future where the reload button

    does not exist


    Looking foward to what

    you build with it!

    View Slide

  49. @OndrejMirtes
    feedback:
    https://joind.in/talk/ab95e

    View Slide