Creating Real-Time Applications with Server-Sent Events

    Just recently it became known that Firefox 6 will receive SSE ( already available in Opera 10.6+, Chrome, WebKit 5+, iOS Safari 4+, Opera Mobile 10+) so that support for more than half of all browsers (reaching the audience of users) is no longer in favor mountains. It's time to take a closer look at this technology. SSE was offered by Ian Hickson more than 7 years ago, but only a year ago it began to appear in browsers. We have WebSockets, why do we need another protocol ?! But everything has its pros and cons, let's see what SSE can be useful for.

    The idea of ​​SSE is simple - the client subscribes to server events and as soon as the event occurs, the client immediately receives a notification and some data related to this event. To understand the usefulness of the SSE protocol, you need to compare it with the usual methods for receiving events, briefly explain their essence:

    Polling


    The simplest, but not the most effective, method: the client once every few seconds polls the server for events. Even if there is nothing, then the client all the same makes a request - but you never know what will come.
    Pros:
    - Simple
    - Data can be shipped
    Cons:
    - A lot of unnecessary requests
    - Events always arrive late
    - The server has to store events until the client picks them up or until they become outdated

    Long polling


    An improved version of the previous method. The client sends a request to the server, the server keeps the connection open until some data arrives or the client disconnects on its own. As soon as the data has arrived, a response is sent and the connection is closed and the next opens and so on.
    Pros compared to Polling:
    - Minimum number of requests
    - High temporal accuracy of events
    - The server stores events only during reconnect
    Cons compared to Polling:
    - More complex scheme

    Websockets


    This is a binary duplex protocol that allows the client and server to communicate on equal terms. This protocol can be used for games, chats and all those applications where you need extremely accurate events close to real time.
    Pros compared to Long Polling:
    - One connection rises
    - Extremely high temporary accuracy of events
    - Network failure management controls the browser
    Cons compared to Long Polling:
    - HTTP is not a compatible protocol, you need your own server, debugging is complicated

    So why it is worth using SSE, since we have such a beautiful WebSockets protocol ?! First, not every web application needs two-way communication - SSE is also suitable. Secondly, SSE is an HTTP compatible protocol and you can implement event broadcasting on any web server.

    Server-Sent Events Protocol


    The client sends a request to the server, the server sends the following header in response:
    Content-Type: text/event-stream
    

    And it doesn’t close the connection (on php you can create an endless loop, how to do on node.js will be explained in the example article). That's it - SSE works! To send some data to the client, the server simply writes a line of the following format to the socket:
    data: My message\n\n
    

    If you need to send several rows of data, the format will be as follows:
    data: {\n
    data: "msg": "hello world",\n
    data: "id": 12345\n
    data: }\n\n  
    

    Here, in principle, and the entire base of the protocol. In addition, the server can send message id this is necessary in case the connection was disconnected. If the connection was dropped, then the client, when trying to connect, will send a special header (Last-Event-ID) to restore the lost events:
    id: 12345\n
    data: GOOG\n
    data: 556\n\n
    

    Retry time in case of errors:
    retry: 10000\n
    data: hello world\n\n
    

    The id and retry fields are optional.

    On the client, everything will look like this:
    var source = new EventSource('http://localhost/stream.php');
    source.addEventListener('message', function(e) {
      // Пришли какие-то данные
      console.log(e.data);
    }, false);
    source.addEventListener('open', function(e) {
      // Соединение было открыто
    }, false);
    source.addEventListener('error', function(e) {
      if (e.eventPhase == EventSource.CLOSED) {
        // Соединение закрыто
      }
    }, false);
    

    Everything is extremely simple. Let's build an application based on the SSE protocol. As usual, this will be a chat.

    Multipart XMLHTTPRequest


    Also called multipart streaming (only supports Firefox). Very similar to SSE protocol.
    Its header has a format:
    Content-type: multipart/x-mixed-replace;boundary=smthing
    

    And the parts are sent in this format:
    Content-type: text/html\r\n\r\n
    --smthing\n
    Message\n
    --smthing\n
    


    A regular XHR is created in the client, but before sending the request, you must set the flag. Does req.multipart = true;
    it look like SSE? More

    There is another protocol that can lead to SSE:

    XMLHTTPRequest: Interactive


    To use it, the browser must support the special readyState with code 3 (interactive) - this status indicates that part of the data has arrived, but the connection has not yet been closed. There is a plugin of the same name for jQuery using readyState with code 3. And as always, not all browsers support readyState with code 3.

    Example: Chat on Server-Sent Events


    We will accept the stream of events on SSE: going offline, coming online, message. Because SSE cannot send a message, then we will send them via HTTP.

    The working scheme is as follows:
    - When entering the chat, a name
    is requested - The client connects to the chat server. An event stream is created.
    - When a client connects, the chat sends an event to everyone:% username% online
    - When a client disconnects, the chat sends an event to everyone:% username% offline
    - The client can send a message via HTTP “POST / message” The server receives this message and sends an accepted message via SSE to all clients

    Let's analyze the client code. To ensure that some browsers do not have endless downloads, instead of $ .ready, we execute setTimeout:
    setTimeout(function () { // Ставлю именно таймаут, а не $.ready иначе у вебкитов будет бесконечная загрузка
    }, 50);
    

    Request username:
    // Получаем имя из localStorage и запрашиваем новое
    var name = (prompt('Name:', window.localStorage ? window.localStorage['name'] || '' : '') || 'anonymous').substr(0, 20);
    // Пытаемся сохранить имя
    if (window.localStorage) {
        window.localStorage['name'] = name;
    }
    

    We create an EventSource and pass it the username (now the user is online) and listen to the necessary events:
    var eventSrc = new EventSource("/event?name=" + name);
    // Слушаем событие EventSource - "message"
    eventSrc.addEventListener("message", function(event) {
        var data = JSON.parse(event.data);
        // Отрисовываем пришедшее с сервера сообщение
        renderMessage(data);
    }, false);
    // Слушаем событие EventSource - "error"
    eventSrc.addEventListener("error", function(event) {
        // Сообщаем о проблеме с подключением
        renderMessage({
            isbot: true,
            message: 'connection error',
            name: '@Chat'
        });
    }, false);
    

    I will not consider the renderMessage method and page layout. All client code can be viewed here: index.html

    On the server side, we will have Node.js. Everything is more complicated here, but the main difficulty in multicast messages from one user to all, and not in building communication over SSE.

    We connect the necessary modules

    var http = require('http'),
        fs = require('fs'),
        qs = require('querystring'),
        parse = require('url').parse;
    // Кэшируем статику (index.html мы будет отдавать с помошью Node.js)
    var indexFile = fs.readFileSync('index.html'); // Buffer
    

    Routes

    We create a list of Routes routes, which includes the following objects:
    1. Statics. Index page, we just helmet statics:
        'GET /': function (request, response) {
            // Шлем правильные заголовки
            response.writeHead(200, {'Content-Type': 'text/html; charset=UTF-8'});
            response.write(indexFile);
            response.end();
        }
    

    2. Raising the SSE connection:
        'GET /event': function (request, response) {
            var url = parse(request.url, true);
            var name = (url.query.name || 'anonymous').substr(0, 20);
            var clientId = Clients.generateClientId();
            // Шлем спец заголовок для EventSource
            response.writeHead(200, {'Content-Type': 'text/event-stream'});
            // Выставляем больший таймаут на сокет, иначе сокет запроется через 2 минуты
            request.socket.setTimeout(1000 * 60 * 60); // 1 Час
            // Если соединение упало - удаляем этого клиента
            request.on('close', function () {
                Clients.remove(clientId);
            });
            // Добавляем клиента в список
            Clients.add(clientId, response, name);
        }
    

    3. Message from the client:
        'POST /message': function (request, response) {
            var data = '';
            // Пришел кусочек тела POST
            request.on('data', function (chunk) {
                data += chunk;
            });
            // Все кусочки POST тела собраны
            request.on('end', function () {
                // Парсим тело
                data = qs.parse(data);
                // Рассылаем всем сообщение
                Clients.broadcast(data.message, data.name, false);
                response.writeHead(200);
                response.end();
            });
        }
    

    4. Default Route - Page 404:
        $: function (request, response) {
            response.writeHead(404);
            response.end();
        }
    

    Customer Manager - Clients

    When adding a new client (add), the manager sends out all the message that the client has arrived:
        add: function (clientId, response, name) {
            this._clients[clientId] = {response: response, name: name || 'anonymous'};
            this.count++;
            // Рассылаем сообщения от имени бота
            this.unicast(clientId, 'Hello, ' + name + '! Online ' + this.count, '@ChatBot', true);
            this.broadcast(name + ' online', '@ChatBot', true);
        }
    

    When deleting, it closes the connection and sends out to everyone that the client is offline:
        remove: function (clientId) {
            // Если клиента нет, то ничего не делаем
            var client = this._clients[clientId];
            if (!client) {
                return;
            }
            // Закрываем соединение
            client.response.end();
            // Удаляем клиента
            delete this._clients[clientId];
            this.count--;
            // Сообщаем всем оставшимся, что он вышел
            // Рассылаем сообщения от имени бота
            this.broadcast(client.name + ' offline', '@ChatBot', true);
        }
    

    The private _send method is used to send messages to clients:
        _send: function (clients, message, name, isbot) {
            if (!message || !name) {
                return;
            }
            // Подготавливаем сообщение
            var data = JSON.stringify({
                message: message.substr(0, 1000),
                name: (name || 'anonymous').substr(0, 20),
                isbot: isbot || false
            });
            // Создаем новый буфер, чтобы при большом количестве клиентов
            // Отдача была более быстрой из-за особенностей архитектуры Node.js
            data = new Buffer("data: " + data + "\n\n", 'utf8'); // Формат сообщение SSE
            // Рассылаем всем
            clients.forEach(function (client) {
                client.response.write(data); // Отсылаем буфер
            });
        }
    

    The _send method uses the public broadcast and unicast methods to send messages to all and one client, respectively.

    We create and turn on the server

    // Создаем сервер
    var httpServer = http.createServer(function (request, response) {
        var key = request.method + ' ' + parse(request.url).pathname;
        // Если роута нет, то отдаем по умолчанию Routes.$ - 404
        (Routes[key] || Routes.$)(request, response);
    });
    // Включаем сервер
    httpServer.listen(80);
    console.log('Online'); 
    

    Source code server.js

    Our chat on SSE is ready. We start the server:
    $ node server.js
    

    Open one of the browsers: Firefox 6, Opera 10.6+, Chrome, WebKit 5+, iOS Safari 4+, Opera Mobile 10+. We pass on http://localhost/and chat!

    Conclusion


    SSE is a good technology that should displace Long Poling. It is simple and no less effective than WebSockets. Now SSEs support Opera 10.6+ (Opera 9 supports the old SSE standard), Chrome, Safari 5+. Firefox supports Multipart XMLHTTPRequest, for which you can write a wrapper and use it as an SSE interface.

    References


    1. An online example of SSE chat can be viewed here: sse-chat.nodester.com
    This is a slightly stripped down version of the chat due to the peculiarities of proxying Nodester (there is no message about the number of users online and there are no messages about leaving the chat, there may be a frequent reconnect)
    2 Example source : github.com/azproduction/event-source-chat
    3. Another tutorial on SSE
    4. PS specification It seems that the chat has covered the habra effect, but something with nodester is possible (it often happens). If you are interested in the result, then download the source from GitHub. UPD Added by Multipart XMLHTTPRequest, XMLHTTPRequest: Interactive thanks for the addition of yui_room9




    Also popular now: