Статьи

Длинный опрос кота с весны

«О, миссис миссис», как сказал бы комик Фрэнки Хауэрд , но достаточно британской инсинуации и двойного намерения, потому что Long Polling Tomcat — это не какое-то сексуальное отклонение от могущественного соседа, это техника (или скорее хак), которая была разработано в результате отказа Стива Джобса от поддержки Adobe Flash Player на iPhone и iPad . Дело в том, что использование Flash Player как части веб-приложения было действительно хорошим способом поддержки парадигмы публикации и подписки, поскольку он мог обслуживать те сценарии, которые требуют актуальных обновлений, таких как живые цены на акции, обновления новостей и изменения в Коэффициенты ставок, в то время как прямой HTTP с его парадигмой запрос / ответ является хорошим способом поддержки статических страниц. Большое количество компаний приложило много усилий для разработки приложений, использующих Flash, для предоставления своим пользователям данных в реальном времени. Когда Apple объявила, что iOS не будет поддерживать Adobe Flash, они остались без поддержки и без решения для iPhone, и чтобы вернуться на мобильный рынок, я предположил, что многие из них прошли долгий опрос.

Итак, что такое длинный опрос ? Ну, это не высокий парень из Варшавы , идея состоит в том, чтобы имитировать шаблон публикации и подписки. Сценарий выглядит так:

  1. Браузер запрашивает некоторые данные с сервера.
  2. Сервер не имеет этих данных и позволяет запросу зависать.
  3. Через некоторое время данные ответа становятся доступны, и сервер завершает запрос.
  4. Как только браузер получает данные, он отображает их и затем быстро запрашивает обновление.
  5. Теперь поток возвращается к точке 2.

Я подозреваю, что ребята в Spring не слишком заинтересованы в термине «длительный опрос», так как они более формально называют эту технику асинхронной обработкой запросов.

Взглянув на мой поток обработки длинных опросов или асинхронных запросов выше, вы, вероятно, догадались, что произойдет с вашим сервером. Каждый раз, когда вы заставляете сервер ждать получения данных, вы связываете некоторые из его ценных ресурсов. Если ваш веб-сайт популярен и подвергается большой нагрузке, то количество ресурсов, потребляемых в ожидании обновлений ракет, и, как следствие, ваш сервер могут закончиться и дать сбой.

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

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

С течением времени телекомпания хочет заменить старый Teletype веб-приложением, которое отображает обновления матчей в реальном времени.

Снимок экрана 2013-08-19 в 18.04.01

В этом новом сценарии президент телекомпании нанимает наших друзей в Agile Cowboys inc, чтобы разобраться в новостях. Чтобы упростить задачу, он дает им исходный код для классов Message , Match и MatchReporter , которые повторно используются в новом проекте. Генеральный директор Agile Cowboys нанимает на работу нескольких новых разработчиков: специалиста по JavaScript, JQuery, CSS и HTML для работы над интерфейсом и парня по Java для Spring MVC Webapp.

Специалист внешнего интерфейса предлагает следующий код опроса JavaScript:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
var allow = true;
var startUrl;
var pollUrl;
 
function Poll() {
 
  this.start = function start(start, poll) {
 
    startUrl = start;
    pollUrl = poll;
 
    if (request) {
      request.abort(); // abort any pending request
    }
 
    // fire off the request to MatchUpdateController
    var request = $.ajax({
      url : startUrl,
      type : "get",
    });
 
    // This is jQuery 1.8+
    // callback handler that will be called on success
    request.done(function(reply) {
 
      console.log("Game on..." + reply);
      setInterval(function() {
        if (allow === true) {
          allow = false;
          getUpdate();
        }
      }, 500);
    });
 
    // callback handler that will be called on failure
    request.fail(function(jqXHR, textStatus, errorThrown) {
      // log the error to the console
      console.log("Start - the following error occured: " + textStatus, errorThrown);
    });
 
  };
 
  function getUpdate() {
 
    console.log("Okay let's go...");
 
    if (request) {
      request.abort();  // abort any pending request
    }
 
    // fire off the request to MatchUpdateController
    var request = $.ajax({
      url : pollUrl,
      type : "get",
    });
 
    // This is jQuery 1.8+
    // callback handler that will be called on success
    request.done(function(message) {
      console.log("Received a message");
 
      var update = getUpdate(message);
      $(update).insertAfter('#first_row');
    });
 
    function getUpdate(message) {
 
      var update = "<div class='span-4  prepend-2'>"
            + "<p class='update'>Time:</p>"
            + "</div>"
            + "<div class='span-3 border'>"
            + "<p id='time' class='update'>"
            + message.matchTime
            + "</p>"
            + "</div>"
            + "<div class='span-13 append-2 last' id='update-div'>"
            + "<p id='message' class='update'>"
            + message.messageText
            + "</p>"
            + "</div>";
      return update;
    };
 
    // callback handler that will be called on failure
    request.fail(function(jqXHR, textStatus, errorThrown) {
      // log the error to the console
      console.log("Polling - the following error occured: " + textStatus, errorThrown);
    });
 
    // callback handler that will be called regardless if the request failed or succeeded
    request.always(function() {
      allow = true;
    });
  }; 
};

Класс с именем Poll имеет один метод start() , который принимает два аргумента. Первый из них используется браузером для подписки на фид данных обновлений матчей, а второй — это URL-адрес, который используется для опроса сервера на наличие обновлений. Этот код вызывается из функции JQuery ready(…) .

1
2
3
4
5
6
7
8
$(document).ready(function() {
 
  var startUrl = "matchupdate/subscribe";
  var pollUrl = "matchupdate/simple";
 
  var poll = new Poll();
  poll.start(startUrl,pollUrl);
 });

Когда вызывается метод start() он отправляет серверу Ajax-запрос на подписку на обновления совпадений. Когда сервер отвечает простым «ОК», обработчик request.done(…) запускает таймер 1/2 секунды, вызывая setInterval(…) с анонимной функцией в качестве аргумента. Эта функция использует простой флаг ‘ allow ‘, который, если true, позволяет getUpdate() метод getUpdate() . Флаг allow затем устанавливается в false, чтобы гарантировать отсутствие проблем с повторным входом.

Функция getUpdate(…) выполняет еще один Ajax-вызов на сервер, используя второй аргумент URL, описанный выше. На этот раз обработчик request.done(…) захватывает обновление соответствия, преобразует его в HTML и вставляет его после div ‘ first_row ‘, чтобы отобразить его на экране.

Возвращаясь к сценарию, генеральный директор Agile Cowboys Inc хочет произвести впечатление на свою новую девушку, поэтому он покупает ей Porsche 911 . Теперь он не может заплатить за это, используя свои собственные деньги, поскольку его жена узнает, что происходит, поэтому он платит за это частичкой денег от сделки телекомпании. Это означает, что он может позволить себе только обучающемуся выпускнику писать код на стороне сервера. Этот выпускник может быть неопытным, но он повторно MatchReporter классы Message , Match и MatchReporter для предоставления обновлений соответствия. Помните, что Queue и Match вводятся в MatchReporter . Когда MatchReporter.start() метод MatchReporter.start() , он загружает совпадение и считывает сообщения обновления, где проверяет их временные метки и добавляет их в очередь в соответствующий момент. Если вы хотите увидеть код для MatchReporter , Match т. Д., Взгляните на оригинальный блог .

Затем выпускник создает простой контроллер обновления весеннего матча

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
@Controller()
public class SimpleMatchUpdateController {
 
  private static final Logger logger = LoggerFactory.getLogger(SimpleMatchUpdateController.class);
 
  @Autowired
  private SimpleMatchUpdateService updateService;
 
  @RequestMapping(value = "/matchupdate/subscribe" + "", method = RequestMethod.GET)
  @ResponseBody
  public String start() {
    updateService.subscribe();
    return "OK";
  }
 
  /**
   * Get hold of the latest match report - when it arrives But in the process
   * hold on to server resources
   */
  @RequestMapping(value = "/matchupdate/simple", method = RequestMethod.GET)
  @ResponseBody
  public Message getUpdate() {
 
    Message message = updateService.getUpdate();
    logger.info("Got the next update in a really bad way: {}", message.getMessageText());
    return message;
  }
}

SimpleMatchUpdateController содержит два очень простых метода. Первый, start() , просто вызывает SimpleMatchUpdateService чтобы подписаться на обновления совпадений, а второй, getUpdate() , запрашивает SimpleMatchUpdateService для следующего обновления совпадений. Глядя на это, вы, вероятно, можете догадаться, что вся работа выполняется с помощью SimpleMatchUpdateService .

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
@Service("SimpleService")
public class SimpleMatchUpdateService {
 
  @Autowired
  @Qualifier("theQueue")
  private LinkedBlockingQueue<Message> queue;
 
  @Autowired
  @Qualifier("BillSkyes")
  private MatchReporter matchReporter;
 
  /**
   * Subscribe to a match
   */
  public void subscribe() {
    matchReporter.start();
  }
 
  /**
   *
   * Get hold of the latest match update
   */
  public Message getUpdate() {
 
    try {
      Message message = queue.take();
      return message;
    } catch (InterruptedException e) {
      throw new UpdateException("Cannot get latest update. " + e.getMessage(), e);
    }
  }
 
}

SimpleMatchUpdateService также содержит два метода. Первый, subscribe() , сообщает MatchReporter начать помещать обновления в очередь. Второй getUpdate() удаляет следующее обновление из Queue и возвращает его в браузер в виде JSON для отображения.

Все идет нормально; однако в этом случае очередь реализуется экземпляром LinkedBlockingQueue . Это означает, что если нет доступных обновлений, когда браузер делает свой запрос, поток запросов заблокирует метод queue.take() , связывая ценные ресурсы сервера. Когда доступно обновление, queue.take() возвращается и отправляет Message в браузер. Для неопытного стажера все кажется хорошо, и кодекс начинает действовать. В следующую субботу начинается футбольное премьерство (футбол, если вы в США), один из самых оживленных выходных в спортивном календаре, и очень большое количество пользователей хотят получить самую свежую информацию о большой игре. Конечно, на сервере не хватает ресурсов, он не может справиться с нагрузкой и постоянно падает. Президент телекомпании не слишком доволен этим и вызывает генерального директора Agile Cowboys в свой офис. Он ясно дает понять, что кровь будет течь, если эта проблема не будет решена. Генеральный директор Agile Cowboys осознает свою ошибку и после ссоры со своей девушкой забирает Porsche. Затем он отправляет электронное письмо консультанту по Java / Spring и предлагает ему Porsche, если он придет и исправит код. Консультант Spring не может отклонить такое предложение и принимает. Это главным образом потому, что он знает, что спецификация Servlet 3 решает эту проблему, позволяя ServletRequest в асинхронный режим. Это освобождает ресурсы сервера, но сохраняет ServletResponse открытым, позволяя другому стороннему потоку завершить обработку. Он также знает, что ребята в Spring придумали новую технику в Spring 3.2 под названием «Отложенный результат», которая разработана для этих ситуаций. Тем временем бывшая подруга генерального директора Agile Cowboys, все еще расстроенная потерей своего Porsche, отправляет по электронной почте своей жене электронные письма, рассказывающие ей о деле ее мужа…

Поскольку этот блог превращается в эпизод из Далласа, я думаю, что его время закончиться. Так будет ли код исправлен вовремя? Будет ли консультант по Java / Spring тратить слишком много времени на управление своим новым Porsche? Сможет ли генеральный директор простить свою девушку? Будет ли его жена развестись с ним? Ответы на эти вопросы и дополнительную информацию о технике Spring DeferredResult найти в следующий раз…

Возможно, вы заметили, что в примере кода есть еще одна ОГРОМНАЯ дыра, а именно тот факт, что может быть только один подписчик. Поскольку это всего лишь пример кода, и я говорю о длительных опросах, а не о внедрении публикации и подписки, проблема скорее не в теме. Я могу (или не могу) исправить это позже.

Код, сопровождающий этот блог, доступен на Github по адресу: https://github.com/roghughe/captaindebug/tree/master/long-poll

Ссылка: Длинный опрос Tomcat с Spring от нашего партнера JCG Роджера Хьюза в блоге Captain Debug’s Blog .