Реализация PubSubHubbub-подписики в Java-приложении на App Engine

    PubSubHubbubРазбираясь с обозначенной в заголовке темой, попутно обнаружил, что в рунете она раскрыта довольно слабо, хотя с момента представления данного протокола прошло уже много времени. Хочу слегка заполнить этот небольшой пробел, поделившись опытом.
    Напомню кратко, что PubSubHubbub (PuSH) — это протокол, предложенный Google и призванный сделать более эффективным процесс доставки данных по каналам типа RSS от издателей к подписчикам. Центральное место в схеме, обеспечивающей работу протокола, отводится независимым хабам, выполняющим роль посредников между непосредственными источниками данных и конечными их получателями. При этом, хаб оповещает всех зарегистрированных у него подписчиков канала о поступлении новых данных сразу после их появления, одновременно передавая новую порцию данных.
    Таким образом, если вы создаете приложение, занимающееся обработкой фидов в формате RSS или Atom, то можете заметно облегчить себе жизнь, возложив «черную» работу на хаб. Конкретные плюсы такой схемы:
    • возможность «интеграции» множества внешних каналов в единый поток данных общего формата, поступающий на вход приложения: хаб может позаботиться об этом;
    • отсутствие необходимости отделения новых данных от старых: хаб доставит только новые;
    • не нужно постоянно отслеживать канал на предмет новых данных: хаб сам сообщит когда надо;
    • минимальное время с момента публикации до момента оповещения вашего приложения.

    Другими словами, вы можете получить оперативную доставку данных, заметно сэкономив как на объеме входящего трафика, так и на процессорном времени приложения. Для приложений на App Engine, ограниченных квотами, эти моменты могут оказаться принципиальными. Кроме того, вы сэкономите свое время, поскольку придется написать меньший объем несложного кода.
    Ниже приведены минимально необходимые фрагменты кода на Java, которые были успешно мною опробованы на одном из хабов. Кода совсем немного и он несложный.


    Итак, речь идет о приложении-подписчике (subscriber), которое будет принимать данные от хаба (hub). В соответствии с протоколом, сценарий взаимодействия подписчика с хабом включает следующее:
    1. хабу направляется запрос о подписке с адресом канала и адресом подписчика;
    2. хаб проверяет канал и направляет запрос в адрес подписчика о подтверждении подписки;
    3. подписчик подтверждает подписку;
    4. хаб извещает подписчика и доставляет ему новые данные по мере их появления в канале;
    5. через определенное время хаб повторно запрашивает подписчика о подтверждении подписки.

    Этот сценарий означает, что наше минимальное приложение должно реализовать сервлет, способный:
    1. подтвердить подписку в ответ на запрос хаба;
    2. принять очередную посылку с порцией новых данных.

    Кроме того, оно может иметь функцию, реализующую собственно процедуру запроса подписки.

    Запрос подписки


    Поскольку хабы, которые я пробовал позволяют запросить подписку «вручную», воспользовавшись соответствующим веб-интерфейсом сервиса, данная процедура не обязательна в рамках приложения.
    При запросе подписки, необходимо сообщить хабу значения четырех обязательных параметров:
    1. URL подписчика (hub.callback): адрес сервлета приложения, по которому с ним будет взаимодействовать хаб;
    2. тип запроса (hub.mode): желаемое действие, а именно подписка, либо отказ от нее (subscribe / unsubscribe);
    3. URL подписываемого канала (hub.topic): адрес канала, сообщения которого вы желаете принимать;
    4. способ подтверждения запроса (hub.verify): сообщает хабу о необходимости либо необязательности незамедлительного (синхронного) запроса о подтверждении подписки (sync / async).

    Кроме того, хаб может поддерживать необязательные параметры, такие как:
    • время подписки (hub.lease_seconds): длительность в секундах, определяющая как долго мы желаем получать сообщения канала;
    • секретная строка (hub.secret): передается, если требуется проверка подлинности принимаемых подписчиком сообщений (хаб на ее основе будет генерироать HMAC-код для передаваемого контента и подписывать им свои сообщеия);
    • верификационная последовательность символов (hub.verify_token): если задана, то будет передана параметром в запросе подтверждения, чтобы приложение-подписчик могло убедиться, что оно подтверждает не случайную подписку.

    Если вас устраивает «ручной» режим подписки, то можете переходить к следующему разделу.
    Однако, может быть, что от приложения требуется способность самостоятельно осуществлять подписку. Вот пример функции, реализующий данную процедуру:

    import java.net.URL;
    import java.net.URLEncoder;
    import java.net.HttpURLConnection;
    import java.io.OutputStreamWriter;
    import com.google.appengine.repackaged.com.google.common.util.Base64;

    //..

    public static void pshbSubscribe(String callback, String mode, String topic, String verify) throws IOException  {

      callback = URLEncoder.encode(«hub.callback», «UTF-8») + "=" + URLEncoder.encode(callback, «UTF-8»);
      mode = URLEncoder.encode(«hub.mode», «UTF-8») + "=" + URLEncoder.encode(mode, «UTF-8»);
      topic = URLEncoder.encode(«hub.topic», «UTF-8») + "=" + URLEncoder.encode(topic, «UTF-8»);
      verify = URLEncoder.encode(«hub.verify», «UTF-8») + "=" + URLEncoder.encode(verify, «UTF-8»);
      String body = callback + "&" + mode + "&" + topic + "&" + verify;

      URL url = new URL(«myhub.com/hubbub»);
      HttpURLConnection connection = (HttpURLConnection) url.openConnection();
      connection.setDoOutput(true);
      connection.setRequestMethod(«POST»);
      connection.setRequestProperty(«Content-Type», «application/x-www-form-urlencoded»);
         
      connection.setRequestProperty(«Authorization»,
        «Basic „ + Base64.encode((“myname:mypwd»).getBytes()));

      OutputStreamWriter writer = new OutputStreamWriter(connection.getOutputStream());
      writer.write(body);
      writer.close();
     
      if (connection.getResponseCode() != HttpURLConnection.HTTP_NO_CONTENT )  {
        // error
        //..  
      }
    }

    * This source code was highlighted with Source Code Highlighter.

    В соответствии с протоколом, запрос на подписку представляет собой POST-запрос по адресу, предоставляемому хабом ("myhub.com/hubbub") в стандартном виде, используемом для передачи значений форм (где "Content-Type" есть "application/x-www-form-urlencoded"). В теле сообщения передаются выше озвученные параметры.
    Хаб, на котором я тестировал код, требует предварительной регистрации и запрос на подписку с аутентификацией (HTTP Basic Authentication). Отсюда возникает «Authorization» с именем и паролем ("myname:mypwd") пользователя хаба. Насколько я понимаю, это особенность конкретного хаба.
    В случае успешной подписки хаб должен вернуть 204 («No Content»), либо 202 («Accepted») в случае асинхронной верификации (если hub.verify имел значение «async»).
    Таким образом, пример запроса подписки может выглядеть так:

    pshbSubscribe(«myapp.appspot.com/subscribe», «subscribe», «habrahabr.ru/rss/blogs/java», «sync»);

    Первый параметр — адрес сервлета приложения. Далее рассмотрим работу этого сервлета.

    Подтверждение подписки


    После получения запроса подписки хаб должен затребовать подтверждение, отправив GET-запрос по полученному адресу. В нашем примере это "myapp.appspot.com/subscribe". По этому адресу приложением должен быть реализован сервлет, отвечающий на данный запрос:

    import javax.servlet.http.*;
    //..

    @SuppressWarnings(«serial»)
    public class SubscribeServlet extends HttpServlet  {
    //..

    public void doGet(HttpServletRequest req, HttpServletResponse resp)
          throws IOException  {

      resp.setContentType(«text/plain»);
      resp.setStatus(200);

      if (req.getParameter(«hub.mode») != null)
      {
        resp.getOutputStream().print(req.getParameter(«hub.challenge»));
        resp.getOutputStream().flush();
      }
    }
    //..


    * This source code was highlighted with Source Code Highlighter.

    В запросе хаб передает несколько параметров, смысл которых тот же, что и в запросе на подписку:
    • hub.mode: тип запроса (subscribe / unsubscribe);
    • hub.topic: URL подписываемого канала;
    • hub.verify_token: верификационная последовательность символов (присутствует, если передавался при запросе).

    Если значения параметров устраивают (соответствуют запросу), то чтобы подтвердить подписку (или отказ от нее), нужно в ответ вернуть код 2xx, а в тело ответа поместить значение еще одного параметра: hub.challenge.
    Если мы не хотим подтверждать запрос, следует вернуть 404 («Not Found»).
    Если хабу вернуть другие коды (3xx, 4xx, 5xx), то он решит, что у нас проблемы и верификация не пройдена.
    В случае, если содержимое тела ответа будет отличаться от значения hub.challenge, хаб также будет считать, что верификация не пройдена.
    Если используется асинхронный способ запроса, то в случае неудачи (возврат 3xx, 4xx, 5xx либо несоответствие содержимого ответа параметру hub.challenge) хаб должен пытаться требовать подтверждения повторно.

    Прием данных от хаба


    Когда хаб обнаружит, что у него есть новые данные для подписчика, он выполнит POST-запрос по уже известному ему адресу, предоставленному подписчиком. В теле запроса он передаст эти данные в формате RSS или Atom ("Content-Type" будет "application/rss+xml " либо "application/atom+xml"). Для обработки запроса наш сервлет будет иметь функцию:

    public void doPost(HttpServletRequest req, HttpServletResponse resp)
           throws IOException  {

      SyndFeedInput input = new SyndFeedInput();
      SyndFeed feed = input.build(new XmlReader(req.getInputStream()));

       @SuppressWarnings(«unchecked»)
      List<SyndEntry> entriesList = feed.getEntries();

      for (SyndEntry entry: entriesList)
      {
        String title = entry.getTitle();
        String author = entry.getAuthor();
        URL url = new URL(entry.getLink());

         @SuppressWarnings(«unchecked»)
        List<SyndContent> contentsList = entry.getContents();
        //..

      }
      //..

      resp.setStatus(204);
    }


    * This source code was highlighted with Source Code Highlighter.

    В этом примере для разбора данных используются классы библиотеки Rome, предназначенной для работы с фидами (SyndFeedInput, SyndFeed, SyndEntry,… ). Пример аналогичного кода, примененного для решения конкретной задачи (пересылка данных, получаемых от хаба через XMPP), можно посмотреть тут.
    Если во время подписки был определен параметр hub.secret, то запрос придет с параметром "X-Hub-Signature", со значением вида "sha1=signature", где 'signature' — есть сгенерированный для содержимого тела запроса HMAC-код (SHA1 signature). Чтобы убедиться в подлинности сообщения, приложение должно само вычислить HMAC-код для тела запроса, используя известную ему hub.secret. Если результат совпадет с 'signature', то сообщение подлинное. Подробнее тут.
    Если сообщение успешно принято, необходимо вернуть код 2xx, независимо от результатов проверки «X-Hub-Signature». В случае возврата иного, хаб должен пытаться повторно выполнять запрос в течении разумного времени, пока не получит код успеха.

    Ссылки:

    Similar posts

    Ads
    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More

    Comments 20

      +1
      А что за приложение на App Engine Вы пишете для rss?
        +1
        На данный момент какого-либо цельного приложения я не имею. То, что есть — разрозненные черновые экперименты «для внутреннего пользования» и баловство. Сам недавно только начал пробовать App Engine.
        Мысли, которые возникают в связи с технологией, например: дешевый способ способ реализации тематических сервисов, предоставляющих реал-тайм информацию с гибкими возможностями поиска и индивидуальной подстройки под пользователя.
        Близкое но чуть другое: создание альтернатив ридеру, в большей степени приспосабливающихся к пользователю. Тут как возможности фильтрации, так и расширения контента на основе использования обратной связи и анализа поведения. (правда я особо не изучал,- может что-то такое уже есть)
        0
        недавно разбирался, на русском языке ничего не нашел(
          +1
          Идея оформить текстик появилась как раз после нашего общения. Хочется нескромно спросить, помогли ли подсказки: заработало что-нибудь?
            0
            Да, спасибо очень помогло, разобрался таки с протоколом
          0
          как появится карма, переноси в тематический блог
            +1
            PubSubHubbub-подписики — это просто lol :)
              0
              На feedburner похоже. Я не ошибаюсь?
                0
                В каком смысле похоже?
                PubSubHubbub — это протокол. FeedBurner — сервис, который, кстати, на данный момент поддерживает данный протокол. То есть с точки зрения протокола, FeedBurner — подписчик и издатель. Например, если у вас блог на Blogger (который также поддерживает PubSubHubbub) и он подключен к FeedBurner, то теоретически как только вы написали пост, Blogger пингует подписчиков и FeedBurner тут же получает сообщение. На выходе, в свою очередь, FeedBurner пингует уже его подписчиков. Например, Twitter (при включенной опции) получит оповещение и сообщение быстренько появится там. (на деле возможно, что между этими конкретными сервисами существует более хитрая связь — я не знаю). При этом хабом FeedBurner не является.
                  0
                  Из Feedburner можно много куда рассылку отстроить. twitter, buzz, freendfeeds,…
                  И много откуда.
                  Поясните, почему он не является хабом.
                    0
                    Потому что у него нельзя запросить подписку с помощью этого протокола и получать новые порции данных. С другой стороны, почти нет сомнений, что если вы у гугловского хаба запросите подписку на фид, выдаваемый FeedBurner-ом, то последний успешно будет оповещать хаб, а хаб ваше приложение. Кстати, если глянуть в таблицу, то получается, что FeedBurner и подписчиком не является. Понятно, что он умел и продолжает уметь «кушать» фиды и без всякого хаббаба. Просто на мой взгляд ему ничего не мешает быть подписанным через тот же гугл-хаб на те фиды, хозяева которых поддерживают протокол как издатели.
                    Если по смыслу, то можно увидеть общее межу хабом и FeedBurner в том, что они посредники-преобразователи. Только для хаба основная задача — распространение, а для FeedBurner — модификация.
                0
                ну что же вы язык-то позорите!!!

                слабо в отдельную функцию вынести?

                private static string par(String parameter, String value) {
                return URLEncoder.encode( parameter, «UTF-8») + "=" + URLEncoder.encode( value, «UTF-8»)
                }
                  +1
                  Я старался, чтобы пример был максимально понятен и прозрачен с первого взгляда. Плодить функции не хотелось.
                  Насчет позорности спорить не буду )
                  0
                  странно, что на примерах rss. изначально протокол создавался для Googel Wave.
                    0
                    Протокол основан на использовании RSS/Atom. Поэтому не думаю, что странно.
                    Вот что писано на главной страничке, ему посвященной:
                    «Turn your feeds into real-time streams
                    Easily turn existing Atom and RSS feeds into real-time streams.
                    PubSubHubbub is a simple, open, server-to-server publish/subscribe protocol as an extension to Atom and RSS. Parties speaking the PubSubHubbub protocol can get near-instant notifications via WebHook callbacks when a feed they are interested in is updated.»

                    Если он родился по ходу работы над вэйвом, то противоречия тут нет. Применяться может кучей способов, например, тот же Buzz, как известно его использует.
                    И вообще Google вроде помышляет о том, чтобы массово сподвигнуть сайты к поддержке издания контента с его помощью, чтобы им было легче индексировать свежие страницы.
                    0
                    по-моему, интереснее было бы увидеть на GAE реализацию издателя, а не подписчика
                      +1
                      Вот если интересно есть java-пример.
                      По идее для GAE проблем не должно быть сделать. Там собственно только хаб оповещать: посылать POST с двумя параметрами.
                      +1
                      HMAC код (hub.secret): код идентификации сообщений для случая, когда принимаемый контент требует авторизации;

                      Ну вы и написали, «контент требует авторизации». Подписчик требует подписывания доставляемого контента. И да, лучше все-таки использовать этот параметр. А то я узнаю ваш секретный callback url и такого туда наPOSTчу, ого-го.
                        0
                        Да уж. Спасибо за замечание. Поправил формулировку про hub.secret, попытавшись сделать корректной. И добавил пояснение насчет параметра «X-Hub-Signature» в описании приема данных.
                        0
                        Я не совсем понимаю что именно и как нужно отправить. Вот ваше сообщение.
                        Если значения параметров устраивают (соответствуют запросу), то чтобы подтвердить подписку (или отказ от нее), нужно в ответ вернуть код 2xx, а в тело ответа поместить значение еще одного параметра: hub.challenge.

                        Он отсылает GET, но Connection пишет Close.
                        Что надо в ответ.
                        HTTP/1.1 200 OK

                        <hub_challenge?>

                        Only users with full accounts can post comments. Log in, please.