Пользователь
0,0
рейтинг
2 июня 2011 в 11:54

Разработка → Создание приложений реального времени с помощью Server-Sent Events

Буквально недавно стало известно, что Firefox 6 получит SSE (уже есть в Opera 10.6+, Chrome, WebKit 5+, iOS Safari 4+, Opera Mobile 10+) так, что поддержка более половины всех браузеров (охват аудитории пользователей) уже не за горами. Настало время присмотреться к этой технологии. SSE предложил Ian Hickson более 7 лет назад, но только год назад она стала появляться в браузерах. У нас же есть WebSockets зачем нам ещё один какой-то протокол?! Но во всем есть свои плюсы и минусы, давайте посмотрим чем же SSE может быть полезен.

Идея SSE проста — клиент подписывается на события сервера и как только происходит событие — клиент сразу же получает уведомление и некоторые данные, связанные с этим событием. Чтобы понять полезность протокола SSE необходимо сравнить его с привычными методами получения событий, вкратце объясню их суть:

Polling


Самый простой, но самый не эффективный, метод: клиент раз в несколько секунд опрашивает сервер на наличие событий. Даже если ничего нет, то клиент всеравно делает запрос — а мало ли что придет.
Плюсы:
— Просто
— Данные могут быть пожаты
Минусы:
— Очень много лишних запросов
— События всегда приходят с опозданием
— Серверу приходится хранить события пока клиент не заберет их или пока они не устареют

Long Polling


Улучшенный вариант предыдущего метода. Клиент отправляет запрос на сервер, сервер держит открытым соединение пока не придут какие-нибудь данные или клиент не отключится самостоятельно. Как только данные пришли — отправляется ответ и соединение закрывается и открывается следующее и так далее.
Плюсы по сравнению с Polling:
— Минимальное количество запросов
— Высокая временная точность событий
— Сервер хранит события только на время реконнекта
Минусы по сравнению с Polling:
— Более сложная схема

WebSockets


Это бинарный дуплексный протокол, позволяющий клиенту и серверу общаться на равных. Этот протокол можно применять для игр, чатов и всех тех приложений где вам нужны предельно точные события близкие к реальному времени.
Плюсы по сравнению с Long Polling:
— Поднимается одно соединение
— Предельно высокая временная точность событий
— Управление сетевыми сбоями контролирует браузер
Минусы по сравнению с Long Polling:
— HTTP не совместимый протокол, нужен свой сервер, усложняется отладка

Так почему же стоит применять SSE, раз у нас есть такой прекрасный протокол WebSockets?! Во-первых не каждому веб-приложению необходима двусторонняя связь — подойдет и SSE. Во-вторых SSE — HTTP совместимый протокол и вы можете реализовать рассылку событий на любом веб-сервере.

Протокол Server-Sent Events


Клиент отправляет запрос на сервер, сервер в ответ отправляет следующий заголовок:
Content-Type: text/event-stream

И не закрывает соединение (на php можно создать бесконечный цикл, как сделать на node.js будет объеснено в примере статьи). Вот и все — SSE работает! Чтобы отправить клиенту какие-то данные сервер просто пишет в сокет строку следующего формата:
data: My message\n\n

Если необходимо отправить несколько строк данных, то формат будет следующим:
data: {\n
data: "msg": "hello world",\n
data: "id": 12345\n
data: }\n\n  

Вот, впринципе, и вся база протокола. Кроме этого сервер может отправлять id сообщения это нужно на случай если соединение было разорвано. Если соединение было сброшено, то клиент при попытке подключения отправит специальный заголовок (Last-Event-ID), чтобы восстановить утраченные события:
id: 12345\n
data: GOOG\n
data: 556\n\n

Время переподключения (retry) в случае ошибок:
retry: 10000\n
data: hello world\n\n

Поле id и retry не обязательны.

На клиенте все будет выглядеть следующим образом:
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);

Все предельно просто. Давайте построим приложение на основе протокола SSE. Как водится, это будет чат.

Multipart XMLHTTPRequest


Ещё называют multipart streaming (Поддерживает только Firefox). Очень похожий на SSE протокол.
Его заголовок имеет фомат:
Content-type: multipart/x-mixed-replace;boundary=smthing

А части отсылаются в таком формате:
Content-type: text/html\r\n\r\n
--smthing\n
Message\n
--smthing\n


В клиенте создается обычный XHR, но перед отправкой запроса необходимо поставить флаг req.multipart = true;
Правда похоже на SSE? Подробнее

Есть ещё один протокол, который можно привести к SSE:

XMLHTTPRequest: Interactive


Для использования его необходима поддержка браузером специального readyState с кодом 3 (interactive) — этот статус сообщает о том, что часть данных пришла, но соединение ещё не закрыто. Для jQuery есть одноименный плагин, использующий readyState с кодом 3. И как всегда не все браузеры поддерживают readyState с кодом 3.

Пример: Чат на Server-Sent Events


Мы будем принимать поток событий по SSE: уход в оффлайн, приход в онлайн, сообщение. Т.к. по SSE нельзя отправлять сообщение, то мы будем отправлять их по HTTP.

Схема работы такая:
— При входе в чат запрашивается имя
— Клиент подключается к серверу чата. Создается поток событий.
— При подключении клиента чат рассылает всем событие: %username% online
— При отключении клиента чат рассылает всем событие: %username% offline
— Клиент может отправлять сообщение по HTTP «POST /message» Cервер принимает это сообщение и рассылает всем клиентам принятое сообщение по SSE

Разберем код клиента. Для того, чтобы у некоторых браузеров не было бесконечной загрузки мы вместо $.ready выполняем setTimeout:
setTimeout(function () { // Ставлю именно таймаут, а не $.ready иначе у вебкитов будет бесконечная загрузка
}, 50);

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

// Пытаемся сохранить имя
if (window.localStorage) {
    window.localStorage['name'] = name;
}

Создаем EventSource и передаем ему имя пользователя (теперь пользователь онлайн) и слушаем необходимые события:
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);

Не буду рассматривать метод renderMessage и разметку страницы. Весь код клиента можно посмотреть тут: index.html

На стороне сервера у нас будет Node.js. Тут все сложнее, но основная сложность в мультикасте сообщений от одного пользователя ко всем, а не в построении коммуникации по SSE.

Подключаем необходимые модули

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, включающий в себя следующие объекты:
1. Статика. Индексная страница, мы просто шлем статику:
    'GET /': function (request, response) {
        // Шлем правильные заголовки
        response.writeHead(200, {'Content-Type': 'text/html; charset=UTF-8'});
        response.write(indexFile);
        response.end();
    }

2. Поднятие SSE соедиения:
    '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. Сообщение от клиента:
    '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. Роут по умолчанию — Страница 404:
    $: function (request, response) {
        response.writeHead(404);
        response.end();
    }

Менеджер клиентов — Clients

При добавлении нового клиента (add) менеджер рассылает все сообщение о том, что клиент пришел:
    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);
    }

При удалении закрывает соединение и рассылает всем, что клиент оффлайн:
    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);
    }

Для отправки сообщений клиентам используется private метод _send:
    _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); // Отсылаем буфер
        });
    }

Метод _send используют public методы broadcast и unicast для рассылки сообщения всем и одному клиенту соответственно.

Создаем и включаем сервер

// Создаем сервер
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'); 

Исходный код server.js

Наш чат на SSE готов. Запускаем сервер:
$ node server.js

Открываем один из браузеров: Firefox 6, Opera 10.6+, Chrome, WebKit 5+, iOS Safari 4+, Opera Mobile 10+. Переходим на http://localhost/ и чатим!

Заключение


SSE — хорошая технология, которая должна вытеснить Long Poling она проста и не менее эффективна, чем WebSockets. Сейчас SSE поддерживают Opera 10.6+ (Opera 9 поддерживает старый стандарт SSE), Chrome, Safari 5+. Firefox поддерживает Multipart XMLHTTPRequest, для которого можно написать обертку и использовать как интерфейс SSE.

Ссылки


1. Онлайн пример SSE чата можно посмотреть вот тут: sse-chat.nodester.com
Это несколько урезанная версия чата из-за особенностей проксирования Nodester (нет сообщения о количестве пользователей онлайн и нет сообщений о выходе из чата, может быть частый реконнект)
2. Исходник примера: github.com/azproduction/event-source-chat
3. Ещё один туториал по SSE
4. Спецификация

PS Похоже, что чат накрыл хабраэффект, но возможно что-то с nodester(у него часто такое бывает). Если вам интересен результат, то скачайте исходник с GitHub.

UPD Добавил Multipart XMLHTTPRequest, XMLHTTPRequest: Interactive спасибо за дополнение yui_room9
Mikhail Davydov @azproduction
карма
449,5
рейтинг 0,0
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Спецпроект

Самое читаемое Разработка

Комментарии (55)

  • +8
    в данном случае преймуществ по сравненению с long polling почти нет:
    1. long polling поддерживается всеми браузерами, в отличии от SSE
    2. и в случае SSE и в случае long polling нужен evented сервер
    • +2
      Соглашусь, что поддержка браузерами — слабая сторона. Но SSE более удобный/простой интерфейс работы, в частности с ошибками.
    • 0
      Long Polling требует реконнекта после получения сообщения от сервера, например в том же чате на 10к юзеров это будет не очень хорошо и быстро.
      • 0
        Если у вас есть keep-alive (а он у вас есть), то tcp реконнекта не будет.
  • +1
    Время, как всегда, покажет, что лучше. Мне лично больше импонирует WebSocket.
    • 0
      А я, честно говоря, не понял отличия от WebSockets… Тоже самое они делают.

      К тому же, с WebSockets аналогичный чат можно реализовать даже меньшим количеством кода. И код этот гораздо проще будет, сравните.
      • 0
        1. SSE работает в сиплекс режиме.
        2. поверх HTTP.
        3. Насколько мне известно, драфт вебсокетов сейчас потенциально небезопасен, поэтому в некоторых браузерах он вырублен по дефолту.
        • 0
          уже и драфт исправили и включат сокеты :)
          • 0
            Драфт действительно исправили уже?
          • 0
            Кстати, а Mozilla что-нибудь по этому поводу уже говорила? Когда добавит поддержку WebSockets в релиз?
            • 0
              В Firefox 6 обратно включат вебсокеты, починенные.
              Хотя, тот же «потенциально небезопасен» можно повторить в Flash / Java и никто на это не жалуется, а тут производители браузеров завелись.
  • 0
    Пример не работает (@Chat: connection error). Хабраэффект?
    • 0
      Уже даже и не грузится.
  • 0
    Хм, а как получилось уронить ноду если туда стучится всего максимум сотня человек( это вообще мало )
    • 0
      nodesterbeta
    • 0
      Похоже на то, что фронтенд нодстера глючит. Приложение нормально запускается:
      $ nodester app info sse-chat
      nodester info Gathering information about: sse-chat
      nodester info sse-chat on port 10299 running: true (pid: 18353)


      $ nodester app restart sse-chat
      nodester info Attemping to restart app: sse-chat
      nodester info app restarted.


      $ nodester app logs sse-chat
      nodester info Showing logs for: sse-chat
      Munging require paths..
      Globallizing Buffer
      Reading file...
      Nodester wrapped script starting (18369) at Thu, 02 Jun 2011 09:39:33 GMT
      [INFO] Nodester listening on port: 10299
      Online: Thu Jun 02 2011 09:39:33 GMT+0000 (UTC)


      Пишу багрепорт
      • 0
        Переходите на nodejitsu.com/
        • 0
          Попробовал бы, но жду инвайт :)
          • +1
            Напишите в хабраящик email — возможно, удастся ускорить процесс ;)
  • 0
    > WebSockets
    > Это двоичный дуплексный протокол

    Двунаправленный же, бо bidirectional.
    • 0
      дуплексный он какбэ подразумевает двунаправленность
      • +1
        Ок, либо двунаправленный, либо (полно)дуплексный, но никак не двоичный дуплексный.
        • +1
          да, конечно. «двоичный» тут вообще не понятно к чему
    • 0
      двоичный в смысле — бинарный, я так понял.
      • 0
        Он был бы двоичным aka бинарным, если бы позволял пересылать только сырые байты.
        А так — строки, блобы и типизированные массивы к вашим услугам.
        • –1
          что значит только сырые байты?
          мы не можем представить строки в виде этих сырых байт?
          • 0
            > что значит только сырые байты?

            byte[] b; // Где-то в Java-программе

            > мы не можем представить строки в виде этих сырых байт?

            Можем. Но удовольствие работы с такими строками мы оставим ниндзям клана Ассемблера.
  • 0
    PubSub для клиент-сервера? Хм. А почему бы и нет?
  • 0
    А можно вопрос использовал технологию Long Polling, встретился с проблемой, что некоторые броузеры (сейчас уже точно не помню, кажется Chrome) на все время действия запроса показывал индикатор загрузки…

    Тестировали ли вы Long Polling в разных броузерах? Как результаты с индикатором загрузки ??
    Помогает ли посылка заголовка Content-Type: text/event-stream бороться с этим ??

    P.S. Руками и ногами за WebSockets, но увы не могу себе временно позволить этого счастья. :(
    • +1
      почему? работает везде уже давно. где нативно, где через флеш. давно можно забыть, библиотеки делают это прозрачно для разработчика
      • +1
        Через flash — это костыль… FF и Opera до сих пор не держат WebSockets не говоря уже о мобильных браузерах…
        • 0
          100%. Плюс танцы с бубном вокруг веб серверов. А мне необходимо приложение работающее во всех браузерах и деплоящееся на все веб серврера.
          • 0
            socket.io — и точить ничего не надо, но нативный сервер только под Node.js
            • 0
              А в socet.io, как и в Socket.IO-node нет поддержки sse получается?
        • 0
          «Опера» держит, но выключено по-умолчанию. Из-за проблем с безопасностью протокола, которые недавно, вроде, исправили (см. комменты выше).
    • +1
      Необходимо немного подождать, а только потом отправлять запрос. Первичная загрузка пройдет и запрос будет отправлен тихо.
      Для того, чтобы у некоторых браузеров не было бесконечной загрузки мы вместо $.ready выполняем setTimeout:
      setTimeout(function () { 
      // Ставлю именно таймаут, а не $.ready иначе у вебкитов будет бесконечная загрузка
      
      // код
      
      }, 50);
    • 0
      Бесконечная загрузка у хвалёных JSONP.
      Обынчый XMLHttpRequest (в т.ч. Long Polling) открывает соединение без полос загрузки.
      • 0
        Увы при одном условии это не так — если в Chrome или Safari в методe jQuery.ready (или просто в скрипте в теле страницы) выполнить длинный запрос: SSE, XHR или загрузить большую картинку через new Image, то пока ресурс не закроет соединение «загрузка...» не пропадет. Как лечить — комент выше.
  • 0
    Радует.
    Поднимет стабильность у javascript-фреймворков для подобных соединений (например, now.js), используя альтернативные теххнолгии для современных брузеров.
  • +1
    SSE есть и в Опере 9, но реализованы они там ещё по старону драфту: my.opera.com/WebApplications/blog/show.dml/438711
    • 0
      *старому
  • 0
    Думаю в минусы «Long Polling», ещё надо добавить «Много открытых соединений».
    • 0
      Соединений столько же, как и при sse/ws. Реконнектов — да, больше.
    • 0
      Думаю можно добавить «Много висящих процессов/тредов» в некоторых реализациях.
  • 0
    Вот как работает чат через прокси:
    @ChatBot: Maxim online
    @ChatBot: Hello, Maxim
    @Chat: connection error
    @ChatBot: Maxim online
    @ChatBot: Hello, Maxim
    @Chat: connection error
    @ChatBot: Maxim online
    @ChatBot: Hello, Maxim
    @Chat: connection error
    @ChatBot: Maxim online
    @ChatBot: Hello, Maxim
    @Chat: connection error
    @ChatBot: Maxim online
    @ChatBot: Hello, Maxim
    @Chat: connection error
    @ChatBot: Maxim online
    @ChatBot: Hello, Maxim
    @Chat: connection error
    Maxim: Hi
    @ChatBot: Maxim online
    @ChatBot: Hello, Maxim
    Welcome to EventSource Chat!
    • 0
      Зашел написал «Hi» и перешел на другую вкладку, а потом смотрю — там уже тьма вот таких hello/online/connect (:
    • 0
      Для sse необходимо держать постоянное соединение. connection error — значит сервер закрыл соединение или «кончился интернет». Постоянный реконнект — это поведение браузера.
      • 0
        А я думаю, что это наш корпоративный прокси так меня унижал (:
  • 0
    Эх, Михаил почему то так и не дописал в статью.

    В данный момент уже можно использовать SSE почти во всех браузерах, за исключением IE.
    Метод грязный но рабочий, суть метода проста, мы отсылаем обычный XHR запрос, на сервере отдаём заголовок event-stream и не рвём соединение, вот и всё мы получаем постоянное соединение с сервером.

    Ниже код JS+php

    //JS side of dirt SSE
    //timeout to load indicator skip
    setTimeout(function(){
    var transport = (window.XMLHttpRequest) ? new XMLHttpRequest() : new ActiveXObject('Microsoft.XMLHTTP');
    transport.open('GET', 'test.php', true);
    transport.onreadystatechange=parse;
    transport.send();
    },500);

    function parse(){
    //make element with id = response inside html or use console.log
    document.getElementById('response').innerHTML = this.responseText;
    }


    <?php
    //php side of SSE
    set_time_limit(0);
    header("Content-type: text/event-stream");

    while(true){
    echo time().'
    ';

    //ob_flush and flush is needed, without it not be output
    ob_flush();
    flush();
    sleep(1);
    }
    ?>
    • 0
      Это и есть XMLHTTPRequest: Interactive, только сильно упрощенный
      • 0
        Не совсем.

        Вся соль именно в заголовке, в том же хроме, если делать без заголовка то отдачи не будет.
        Насколько я понимаю это просто впиленный draft SSE на будующее, когда ещё только планировали то заголовок впилили а обёртку нет.
        Поэтому так и работает.
        • 0
          В том же хроме давно есть SSE. Но заголовок «Content-type» в XMLHttpRequest ни на что не влияет (даже если там будет Content-type: стихи/пушкина), кроме появления свойства responseXML (Content-type: text/xml). Функция parse же вызывается несколько раз из-за того, что проходят куски данных (php функция flush) и срабатывает переключение на readyState 3 несколько раз.

          В пруф я хочу вам показать пример:
          Зайдите на страницу javascript.ru/ajax/comet/xmlhttprequest-interactive, в консоли фаербага запустите вот этот код (он идентичен вашему):
          var request = new XMLHttpRequest()
          request.open('GET', 'http://javascript.ru/server_push/long_digits.php?r=0.8904660024932128', true);
          request.onreadystatechange = function(){
              console.log('readyState: %d, responseText: %s',request.readyState, request.responseText);
          };
          request.send();

          На выходе будет что-то такое:
          readyState: 1, responseText:
          readyState: 2, responseText:
          readyState: 3, responseText: 1 2
          readyState: 3, responseText: 1 2 3
          readyState: 3, responseText: 1 2 3 4
          readyState: 3, responseText: 1 2 3 4 5
          readyState: 3, responseText: 1 2 3 4 5 6
          readyState: 3, responseText: 1 2 3 4 5 6 7
          readyState: 3, responseText: 1 2 3 4 5 6 7 8
          readyState: 3, responseText: 1 2 3 4 5 6 7 8 9
          readyState: 3, responseText: 1 2 3 4 5 6 7 8 9 10
          readyState: 4, responseText: 1 2 3 4 5 6 7 8 9 10


          Это и называется «XMLHTTPRequest: Interactive». Если же выдачу «причесать», то мы получим ваш эмулятор SSE. Так что я ничего не упустил ;)

          Подробнее про readyState и о том как он срабатывает: www.quirksmode.org/blog/archives/2005/09/xmlhttp_notes_r_2.html
          • 0
            В хромиуме линукс ваш код выше выдаёт сразу

            readyState: 2, responseText:
            readyState: 3, responseText: 1 2 3 4 5 6 7 8 9 10
            readyState: 4, responseText: 1 2 3 4 5 6 7 8 9 10

            Именно про это я и пытался пояснить, может быть не очень подробно расписал :) извините тогда.

            Версия Chromium 11.0.696.68 (84545)

            Также и мой код выше если убрать заголовок не будет срабатывать кусками, а просто тупо повиснет.
            А с заголовком отдаёт кусками, это скорее всего из за разной реализации readyState или из за глюка какого, именно этот глюк и побудил меня тогда искать вариант SSE потомучто Interactive не работал.
  • 0
    Я не знаком с Node.JS. Такой вопрос: я правильно понимаю, что вызов client.response.write() внутри Clients._send() не вызывает запись в сокет, а складирует переданную строку во внутреннюю очередь на запись? Второй вопрос: именно поэтому нам нужен evented сервер, как писали в комментариях выше, чтобы в произвольный момент можно было добавлять новые куски данных на запись, не блокируясь?

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.