Pull to refresh

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

Reading time 7 min
Views 2.5K
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». В случае возврата иного, хаб должен пытаться повторно выполнять запрос в течении разумного времени, пока не получит код успеха.

Ссылки:

Tags:
Hubs:
+20
Comments 20
Comments Comments 20

Articles