Статьи

Tomcat, WebSockets, HTML5, jWebSockets, JSR-340, JSON и другие

На недавней экскурсии по неблокирующим серверам я столкнулся с Comet, технологиями push-серверов и затем с веб-сокетами. Я опоздал на вечеринку Comet, но думаю, что пришел на вечеринку Web Sockets как раз вовремя. Окончательный стандарт все еще разрабатывается, и на момент написания статьи единственным браузером, поддерживающим его по умолчанию, является Chrome. Итак, чтобы начать, я взглянул на то, что было вокруг. Первоначально я не собирался писать статью об этом в блоге, я просто хотел узнать об этой новой вещи.

Моим единственным требованием было, чтобы реализация сервера была основана на Java. Причина проста — весь мой стек технологий основан на Java EE, и я хочу иметь возможность интегрировать все свои существующие компоненты в любой новый сервер без необходимости в интеграционной шине. Мне нравится, когда я могу перенести JAR-файл из одного проекта в другой и сразу же начать работу.

Поэтому я быстро взглянул на jWebSockets, Grizzly (Glassfish), Jetty и смолу Caucho. Все похоже, но все несколько иначе. Самым отличным был jWebSockets, потому что они пошли на то, чтобы построить собственный сервер, вместо того, чтобы основывать свое решение на существующем сервере. Когда я писал в блоге о node.js, у меня была довольно долгая болтовня о подобных вещах, поэтому я не буду начинать снова. Но у jWebSockets есть реальная проблема в ближайшие годы.JSR-340 говорит о поддержке веб-технологий, основанных на HTML5, и говорят, что этот JSR будет частью Servlet 3.1 и будет включен в Java EE 7 в конце следующего года. Если это произойдет, у jWebScokets возникнет проблема, связанная с тем, что реализация их сервера станет устаревшей. И поскольку их сервер не предлагает ничего другого от Java EE, он может потерять большую часть своей доли рынка. Не поймите меня неправильно, они все еще вводят новшества на стороне клиента, имеют мост, позволяющий любому браузеру использовать веб-сокеты, и создают такие вещи, как клиенты веб-сокетов Java SE. Но серверная сторона не выглядит такой горячей — просто взгляните на огромный конфигурационный файл, который вам нужно предоставить, чтобы заставить ваши плагины работать.

Во всяком случае, я не был в восторге от каких-либо решений, которые я видел. Некоторые из них основаны на сервлетах (что хорошо), и все полагаются на то, что вы создаете новый экземпляр слушателя, обработчика, плагина или того, что они по выбору называют. Но что меня поразило, так это то, что вы теряете преимущества работы внутри контейнера. Под этим я подразумеваю, что если вы думаете о сервлете или EJB (а я говорю о Java EE 6 и более поздних версиях), то компоненты, которые вы пишете, имеют то преимущество, что контейнер может позаботиться о сквозных проблемах, таких как транзакции, безопасность и параллелизм, а также предоставляют вам внедренные ресурсы, так что вам остается только написать бизнес-код. Поэтому причина, по которой я не был в восторге от любого из перечисленных выше решений, заключалась в том, что то, что я чувствовал как очевидное, было упущено. Конечно,внутренний класс внутри сервлета может использовать ресурсы из сервлета, но ничто из того, что я нашел в сети, не предполагает, что это должен быть нормальный способ программирования решения для веб-сокетов. И я не уверен, что ссылка во внутреннем классе на ресурс из сервлета не может устареть между полученными сообщениями. Я бы предпочел, чтобы контейнер всегда вводил свежие ресурсы непосредственно перед вызовом моего метода обработки событий, как это происходит в жизненном цикле сервлета или экземпляра EJB. Просто подумайте о времени ожидания соединения с базой данных между сообщениями? Если контейнер гарантировал, что соединение всегда было свежим, тогда разработчику приложения не нужно было бы беспокоиться о технических деталях.И я не уверен, что ссылка во внутреннем классе на ресурс из сервлета не может устареть между полученными сообщениями. Я бы предпочел, чтобы контейнер всегда вводил свежие ресурсы непосредственно перед вызовом моего метода обработки событий, как это происходит в жизненном цикле сервлета или экземпляра EJB. Просто подумайте о времени ожидания соединения с базой данных между сообщениями? Если контейнер гарантировал, что соединение всегда было свежим, тогда разработчику приложения не нужно было бы беспокоиться о технических деталях.И я не уверен, что ссылка во внутреннем классе на ресурс из сервлета не может устареть между полученными сообщениями. Я бы предпочел, чтобы контейнер всегда вводил свежие ресурсы непосредственно перед вызовом моего метода обработки событий, как это происходит в жизненном цикле сервлета или экземпляра EJB. Просто подумайте о времени ожидания соединения с базой данных между сообщениями? Если контейнер гарантировал, что соединение всегда было свежим, тогда разработчику приложения не нужно было бы беспокоиться о технических деталях.Просто подумайте о времени ожидания соединения с базой данных между сообщениями? Если контейнер гарантировал, что соединение всегда было свежим, тогда разработчику приложения не нужно было бы беспокоиться о технических деталях.Просто подумайте о времени ожидания соединения с базой данных между сообщениями? Если контейнер гарантировал, что соединение всегда было свежим, тогда разработчику приложения не нужно было бы беспокоиться о технических деталях.

HttpServlet имеет методы для обработки GET, POST и др. Почему компонент, обрабатывающий веб-сокет, не должен быть WebSocketServlet и иметь методы для обработки событий для рукопожатия, открытия, обмена сообщениями, закрытия и обработки ошибок? Я бы выиграл, потому что сервлет мог бы вводить ресурсы в него до вызова методов обработчика событий, и контейнер мог запускать любые требуемые транзакции или проверять безопасность и т. Д.

Поэтому, даже не подозревая, что это на самом деле будет довольно сложно реализовать Я решил разобрать Tomcat 7 и встроить в него веб-сокеты таким образом, чтобы это отвечало моим требованиям.

В результате получилось (как мне кажется,) решение довольно сексуального сервлета. Пожалуйста, уделите несколько минут изучению кода и особенно комментариев ниже, потому что это суть этой статьи. 

/**
 * a demo of what a web socket servlet could look like
 */
@WebServlet(urlPatterns={"/TestServlet"})
@ServletSecurity(@HttpConstraint(rolesAllowed = "registered"))
public class TestServlet extends WebSocketsServlet<CommandContainer> {

  @WebServiceRef
  private PricingSystem pricingSystem;
  
  @EJB
  private SalesLog salesLog;
  
  @Override
  protected void doJSONMessage(
      WebSocketsServletRequest<CommandContainer> request,
      WebSocketsServletResponse response) throws IOException {
    
    //see how generics is helping me here, 
    //when i grab the request payload?
    //CommandContainer is a class defined in the app!
    CommandContainer c = request.getDataObject();
    
    if("CLOSE".equals(c.getCommand())){
    
      //closing connection to client
      response.close();

      //handle updates to say my model or whatever      
      actualClose(EventSubType.SESSION_END, request.getSession());
      
      return;

    }else if("PRICE".equals(c.getCommand())){

      //hey wow - a call to an injected web service!
      BigDecimal price = pricingSystem.getPrice(c.getData());
      c.setResult(price.toString());

    }else if("BUY".equals(c.getCommand())){
      
      //data is e.g. GOOG,[email protected] - equally I could have 
      //put it in the model
      int idx = c.getData().indexOf(',');
      String shareCode = c.getData().substring(0, idx);
      idx = c.getData().indexOf('@');
      String numShares = c.getData().substring(shareCode.length()+1, idx);
      String price = c.getData().substring(idx+1, c.getData().length()-1);

      //awesome - a call to an injected EJB!
      salesLog.logPurchase(shareCode, 
                           new BigDecimal(price), 
                           Integer.parseInt(numShares));
      
      //and again cool - I can access the logged in user
      //in a familiar way (security)
      c.setResult("sold " + numShares + " of " + shareCode + 
                  " for " + request.getUserPrincipal().getName());

    }else{
      
      c.setResult("unknown command " + c.getCommand());
      
    }

    log("handled command " + c.getCommand() + " on session " + 
                 request.getSession().getId() + ": " + c.getResult());
  
    //the response takes care of the outputstream, framing, 
    //marshalling, etc - my life is really easy now!
    response.sendJSONResponse(c);
  }

  @Override
  protected void doError(EventSubType eventSubType, 
                         WebSocketsSession session, 
                         Exception e) {

    System.out.println("error: " + eventSubType + 
                       " on session " + session.getId() + 
                       ".  ex was: " + e);
  }

  @Override
  public void doClose(EventSubType eventSubType, 
                      WebSocketsSession session) {

    actualClose(eventSubType, session);
  }

  /** 
   * this is a method where we might do stuff like
   * tidy up our model, free resources, etc.
   */  
  private void actualClose(EventSubType eventSubType,
                           WebSocketsSession session) {
  
    System.out.println("closed session " + 
                       session.getId() + ": " + eventSubType);
  }

}


Для меня это то, как должен выглядеть серверный веб-сокет. Это действительно похоже на sevlet, EJB или веб-сервис — вещи, с которыми мы теперь знакомы. Кривая обучения мини.

Но заставить Tomcat использовать эту штуку было немного сложнее. Прежде всего, мой подход состоял в том, чтобы просто написать новый коннектор и настроить его в файле server.xml. Я дал ему номер порта и Tomcat охотно создал экземпляр моего нового класса:

  <!-- NIO WebSockets connector -->    
    <Connector port="8082" protocol="org.apache.coyote.http11.WebSocketsNioProtocol" 
               connectionTimeout="20000" 
             />


Конечно, я немного обманул здесь и сунул свой класс в пакет Койота. Чтобы заставить это работать, я разархивировал исходный код Tomcat 7.0.14 и сказал Eclipse, где его найти. Я установил папку сборки и добавил ее в classpath в пакете catalina.bat перед любым другим JAR-файлом в classpath, и я сделал это как раз в конце пакета, где выполняется вызов процесса Java. Затем я запустил свой модифицированный Tomcat, просто запустив пакет catalina, используя удаленную отладку, чтобы облегчить жизнь.

Я изначально основывал свой неблокирующий соединитель на неблокирующем сервере, который я написал и расширил для моих прошлых нескольких статей в блоге. Моя идея заключалась в том, чтобы перехватывать любые поступившие запросы на рукопожатие через веб-сокет и быстро возвращать самодельный ответ. Затем я бы вызвал запрошенный сервлет и запустил его, используя асинхронную поддержку Servlet 3.0, чтобы контейнер оставался открытым. Сообщения, которые клиент затем отправил после рукопожатия, будут обрабатываться этим сервлетом.

Идея не вполне сработала, потому что сервлет использовал асинхронную поддержку, а мой коннектор получил обратные вызовы от Tomcat, чтобы я мог что-то делать с потоками, обрабатывать события и изменения состояния. Я немного растерялся, потому что не достаточно хорошо понимал детали реализации Tomcat, и через некоторое время я сдался, потому что стало ясно, что мне придется создавать довольно много вещей, которые уже существуют в коннекторах HTTP. который поставляется с Tomcat. Я пришел к выводу, что вместо того, чтобы заново изобретать колесо, я бы сделал лучше, создав собственные версии (или даже подклассы) Coyote Http11NioProcessor и Http11NioProtocol, которые Tomcat использует для реализации коннекторов HTTP. Хотя это несколько сложно, преимущества их специализации, во-первых, в том, что я не изобретаю колесо,и, во-вторых, что я получу HTTPS (или WSS, как это происходит с веб-сокетами) бесплатно (теоретически, я еще не тестировал его, но он просто инкапсулирован, поэтому он должен работать почти без проблем).

Хотя было просто скопировать два соответствующих класса, следующей головной болью было заставить браузер принять ответ на рукопожатие. Я не посылал ответ на рукопожатие в виде строки, созданной самим собой, как это было с моим собственным неблокирующим разъемом. Вместо этого я как можно чаще использовал технологию сервлетов и создавал ответ, устанавливая на нем соответствующие заголовки и нажимая 16-байтовый ключ ответа на выходной поток ответа. В отличие от обычного ответа HTTP, браузер ожидает, что любые ответы на рукопожатие будут придерживаться довольно строгой спецификации. Такие вещи, как простой заголовок даты в ответе (который Tomcat любезно отправляет по умолчанию) заставляют браузер закрывать веб-сокет. Поэтому, немного поиграв в методе WebSocketsNioProcessor # prepareResponse (), вскоре заголовок ответа выглядел идеально.и браузер начал подыгрывать.

Но следующую проблему было не так просто решить. Обработка сообщений вообще не работала. Соединения быстро закрывались, и я не мог понять, что происходит.

После нескольких головных болей я вспомнил, как Comet реализован в Tomcat. Я постепенно осознал, что мне нужно просто прокрутить инфраструктуру Comet в Tomcat. В Tomcat вы создаете обработчик Comet, просто внедряя интерфейс CometProcessor в свой сервлет. Во время первоначального запроса контейнер проверяет, реализует ли интерфейс сервлет, который будет обслуживать запрос, и, если это так, запрос помечается как кометный запрос. Это означает, что контейнер сохраняет соединение открытым и пересылает будущие события (например, байты / сообщения) соответствующему методу-обработчику, который реализует ваш сервлет. Я заставил свой базовый сервлет (WebSocketsServlet) реализовать интерфейс CometProcessor и добавил метод события (событие CometEvent), используя ключевое слово final, чтобы разработчики приложений не моглия никогда не думал о том, чтобы переопределить это, то есть они были защищены от возможности узнать о комете. Затем этот сервлет базового класса инкапсулировал события веб-сокетов, скрывая тот факт, что я обманул с помощью Comet. Метод события из процессора Comet просто передает событие соответствующему методу doXXX в моем базовом сервлете, который прикладные программисты будут наследовать. Это почти то, что делает базовый класс HttpServlet в своем методе обслуживания.Это почти то, что делает базовый класс HttpServlet в своем методе обслуживания.Это почти то, что делает базовый класс HttpServlet в своем методе обслуживания.

Итак, мой WebSocketsServlet является Web-сокетом, эквивалентным HttpServlet, и поэтому я поместил его в пакет javax.servlet.websockets — т.е. чтобы он был предоставлен как часть JSR, которая позволяет указать, каким образом контейнеры Java EE следует обращаться с веб-сокетами.

WebSocketsServlet также имеет дело с объектами WebSocketServletRequest, WebSocketServletResponse и WebSocketSession, а не с эквивалентами HTTP. Каждый из них происходит от соответствующего объекта ServletXXX, а не от объекта HttpServletXXX, потому что при обработке фрейма веб-сокета не имеет смысла что-либо делать, например устанавливать заголовки в ответе. Заголовки имеют отношение только к рукопожатию, а не к чему-либо, о чем разработчик приложения должен заботиться. Опять же, эти интерфейсы также находятся в пакете javax.servlet.websockets, потому что они являются аналогичными классами, принадлежащими спецификации, а не просто Tomcat. К сожалению, в классе ServletRequest есть вещи, которые слишком специфичны для HTTP, такие как getParameter (String), а также другие вещи, которые я предпочел бы скрыть для веб-сокетов,как getInputStream (). Чтобы сделать WebSocketsServletRequest действительно хорошо вписывающимся в пакет javax, я думаю, что ServletRequest, возможно, нужно разбить на супер и подкласс. Возможно ли это, учитывая миллиарды строк Java-кода, я не знаю …

Такие вещи, как обтекание строки ответа (байты) в кадре веб-сокета (т. Е. Ведущий 0x00 и конечный байт 0xFF), также прозрачно обрабатываются реализацией WebSocketsServletResponse. На самом деле я пошел еще дальше. После прослушивания двух интересных видео (с Джеромом Дочезом, архитектором Glassfish — Будущее Java EE и Джером обсуждают ранние планы для Java EE 7Я был вдохновлен некоторыми вещами, о которых он говорил. Он говорил о нежелании возиться с разбором строк и получении контейнера для обработки XML и JSON. Итак … Я добавил это и к Tomcat. В коде вверху вы заметите, что метод обработки, который вызывается для обработки событий сообщений, называется doJSONMessage (WebSocketsServletRequest, WebSocketsServletResponse), а не просто doMessage. Причина в том, что клиент веб-сокетов (запущенный в браузере) позволяет вам отправить заголовок, содержащий протокол, который он будет использовать. Этот протокол является свободным текстом, который может выбрать приложение. В моей реализации я использовал XML, JSON, TEXT и BIN (двоичный код), хотя XML и BIN реализованы не полностью. Поэтому, когда клиент создает веб-сокет следующим образом:

new WebSocket("ws://localhost:8085/nio-websockets/TestServlet", "JSON"); 

этот параметр JSON используется контейнером, чтобы решить, какой именно метод-обработчик в сервлете он будет вызывать. Это также улучшается, потому что вместо того, чтобы вручную обрабатывать строку JSON, контейнер делает магию, и из WebSocketsServletRequest, который передается в метод обработчика сообщений, я могу получить объект, используя метод #getDataObject (). И благодаря обобщению он даже передает этот объект мне без необходимости использовать приведение — сервлет приложения, который реализует базовый сервлет WebSocketsServlet, указывает, какой тип объекта данных он ожидает.

Как работает эта магия, я слышал, вы спрашиваете? Ну, это не так сложно. Базовый сервлет получает байтовый массив из события Comet. Он просматривает исходный заголовок запроса и понимает, что должен обрабатывать этот запрос как объект JSON. Затем он использует XStream, который знает XML, а также JSON, чтобы демонтировать входящий запрос. Последнее, что требуется от магии, — это то, что ей нужно знать, в каком классе говорят «контейнерные» объекты в строковых картах JSON — то есть в какой класс следует использовать данные JSON? Что ж, XStream позволяет вам определять такие псевдонимы. Я подумал, где еще это произойдет, и JAX-Binding сделает это. Поэтому вместо аннотации @XmlType для JAXB я создаю аннотацию javax.json.bind.annotation.JsonType, которая применяется к объектам данных, например так:

@JsonType(name="container")
public class CommandContainer {

  private String command;
  private String result;
  private String data;
.
.
.


Здесь атрибут «name» аннотации говорит, что этот класс сопоставлен с объектами JSON, чье имя — «контейнер». Так что JSON вот так: 

var data = {"container":
               {"command": "BUY", 
                "data": "GOOG,[email protected]", 
                "result": "sold"}
           };

просто демаршалируется в экземпляр класса CommandContainer, так что Java-приложение может использовать объект немедленно, вместо того, чтобы разбираться со строками и демаршировать себя. Контейнер Tomcat анализирует аннотации во время запуска, и я просто вставил туда дополнительный код, чтобы записать эти сопоставления, чтобы, когда базовый сервлет устанавливает необработанные данные в запросе, реализация запроса (которая является объектом Coyote, а не объектом javax) ) может использовать что-то вроде XStream и эти сопоставления для демаршаллинга. Я должен был следить за загрузчиком классов здесь — к счастью, XStream позволяет вам установить загрузчик классов, потому что, если вы его не установите, вы застряли с тем, который не знает веб-приложение.Я просто взял загрузчик классов из контекста сервлета (который я получил из запроса HttpServletRequest, который контейнер фактически передает процессору Comet в событии Comet), и у него есть загрузчик классов, который знает классы webapp. Независимо от того, находятся ли такие аннотированные классы данных JSON в папке web-inf / classes или в jars, контейнер может с ними справиться.

Так что еще осталось? Ну, безопасность для начинающих. Chrome очень низок, и поскольку путь веб-сокетов находится (в данном случае) на том же хосте, что и тот, который обслуживает HTML, содержащий Javascript, открывающий сокет, он отправляет JSESSIONID на сервер во время рукопожатия веб-сокетов. Это действительно здорово, потому что до тех пор, пока вам приходилось входить в систему, чтобы получить HTML, все запросы веб-сокетов выполняются в одном сеансе, поэтому разработчики приложений имеют полный доступ к инфраструктуре безопасности, предоставляемой Java EE! Следовательно, я могу пометить свой сервлет как требующий, чтобы пользователь был в определенной роли, чтобы использовать его, а также проверить свои роли программно в моих методах-обработчиках, используя объект запроса. Если бы браузер не был так добр,Мой план состоял в том, чтобы отправить идентификатор сеанса на сервер в рукопожатии в качестве параметра запроса в строке запроса запрошенного URL, поместив идентификатор сеанса в URL с помощью JSP.

Небольшая проблема с этим решением безопасности заключается в том, что, если пользователь не вошел в систему, браузер не сможет правильно обработать код возврата в рукопожатии. По крайней мере, не в Chrome. В Chrome соединение просто закрывается, и консоль ошибок Javascript показывает слегка загадочное сообщение, указывающее, что был возвращен код 403. Чтобы каждый браузер не реализовывал свой собственный способ решения таких проблем, в спецификации веб-сокетов IETF должно быть четко указано, что такие проблемы следует отправлять как событие в метод обратного вызова клиента onerror. Давайте скрестим пальцы, эй?

Ресурсы, такие как EJB, Entity Manager (JPA) и веб-сервисы, также легко используются — потому что в Java EE 6 я могу внедрить их все, используя контейнер. Хотя Tomcat не поддерживает EJB / WebServices как таковые, я все же вставил некоторые в приведенный выше пример, чтобы проиллюстрировать, как такой сервлет может выглядеть на полноценном сервере приложений Java EE. Чтобы заставить его работать, я взломал Tomcat, чтобы найти эти аннотации, и если ничего не было найдено в дереве JNDI, он просто внедрил новый экземпляр класса — хорошо, это обман, но отлично подходит для доказательства концепции ?

Обратите внимание, что хотя приведенный выше сервлет может не являться типичным приложением, которому требуются веб-сокеты (потому что это базовое приложение для запросов / ответов), он демонстрирует, как я хотел бы видеть веб-сокеты, добавленные в Java EE. Совсем не сложно написать такой сервлет, который бы обрабатывал приложение чата или, например, приложение, в котором пользователь рисует на своем экране, а все остальные участники видят обновление чертежа на своих экранах в реальном времени. Я мог бы продолжить играть с чем-то в этом роде, но в этой статье уже обсуждаются некоторые проблемы, с которыми сталкиваются веб-сокеты.

Несколько недель назад я написал в блоге о том, как node.js должен предоставить вещи, которые я описал в этой статье, чтобы стать зрелым. Java EE сталкивается с теми же проблемами, чтобы оставаться зрелым в будущем, поскольку в него интегрированы новые технологии.

Ну, вот и все. Что вы думаете?

Патчи для Tomcat 7.0.14 доступны здесь .
Веб-приложение, содержащее показанный выше сервлет, можно скачать здесь .

Другие полезные ссылки:

От http://blog.maxant.co.uk/pebble/2011/06/21/1308690720000.html