Статьи

Netty: прозрачное использование SPDY и HTTP

Большинство людей уже слышали о SPDY, протоколе от Google, предложенном в качестве замены устаревшего протокола HTTP. Веб-серверы — это браузеры, которые медленно внедряют этот протокол, и поддержка растет. В недавней статье я уже писал о том, как работает SPDY и как включить поддержку SPDY в Jetty. Поскольку в течение нескольких месяцев Netty (родом из JBoss) также поддерживает SPDY . Так как Netty часто используется для высокопроизводительных протокольных серверов, SPDY является логичным подходом. В этой статье я покажу вам, как вы можете создать базовый сервер на базе Netty, который выполняет согласование протоколов между SPDY и HTTP. Он использовал пример HTTPRequestHandler из примера отслеживания Netty для использования и создания некоторого содержимого HTTP.
Чтобы все заработало, нам нужно сделать следующее:
  • Включите NPN в Java, чтобы определить протокол для использования.
  • Определите, на основе согласованного протокола, использовать ли HTTP или SPDY.
  • Убедитесь, что правильные заголовки SPDY отправлены обратно с HTTP.
SPDY использует расширение TLS, чтобы определить протокол для использования в связи. Это называется NPN. Я написал более полное объяснение и показал сообщения, включенные в статью о том, как использовать SPDY на Jetty , поэтому для получения дополнительной информации посмотрите эту статью. По сути, это расширение заключается в том, что во время обмена TLS сервер и клиент также обмениваются протоколами транспортного уровня, которые они поддерживают. В случае SPDY сервер может поддерживать как протокол SPDY, так и протокол HTTP. Клиентская реализация может затем определить, какой протокол использовать.
Поскольку это не то, что доступно в стандартной реализации Java, нам нужно расширить функциональность Java TLS с помощью NPN.
Включить поддержку NPN в Java
До сих пор я нашел два варианта, которые можно использовать для добавления поддержки NPN в Java. Один из них — по адресу https://github.com/benmmurphy/ssl_npn, у которого также есть базовый пример SPDY / Netty в своем репо, где он использует свою собственную реализацию. Другой вариант, который я буду использовать, — это поддержка NPN, предоставляемая Jetty . Jetty предоставляет простой в использовании API, который можно использовать для добавления поддержки NPN в контексты Java SSL. Еще раз, в статье о Jetty вы можете найти больше информации об этом. Чтобы настроить NPN для Netty, нам нужно сделать следующее:
  1. Добавить библиотеку NPN в bootpath
  2. Подключите контекст SSL к NPN Api
Добавить библиотеку NPN в стенд
Обо всем по порядку. Загрузите загрузочный jar-файл NPN с http://repo2.maven.org/maven2/org/mortbay/jetty/npn/npn-boot/8.1.2.v2012… и убедитесь, что при запуске сервера вы запускаете его следующим образом :
1
java -Xbootclasspath/p:<path_to_npn_boot_jar>
С этим фрагментом кода Java SSL поддерживает NPN. Однако нам все еще нужен доступ к результатам этих переговоров. Нам нужно знать, используем ли мы HTTP или SPDY, поскольку это определяет, как мы обрабатываем полученные данные. Для этого Jetty предоставляет API. Для этого и для необходимых библиотек Netty мы добавляем в pom следующие зависимости, так как я использую maven.
01
02
03
04
05
06
07
08
09
10
11
<dependency>
 <groupId>io.netty</groupId>
 <artifactId>netty</artifactId>
 <version>3.4.1.Final</version>
</dependency>
 
<dependency>
 <groupId>org.eclipse.jetty.npn</groupId>
 <artifactId>npn-api</artifactId>
 <version>8.1.2.v20120308</version>
</dependency>
Подключите контекст SSL к API NPN
Теперь, когда у нас включен NPN и правильный API добавлен в проект, мы можем настроить обработчик SSL Netty. Настройка обработчиков в Netty выполняется в PipelineFactory. Для нашего сервера я создал следующую PipelineFactory:
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
package smartjava.netty.spdy;
import static org.jboss.netty.channel.Channels.pipeline;
  
import java.io.FileInputStream;
import java.security.KeyStore;
  
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLEngine;
  
import org.eclipse.jetty.npn.NextProtoNego;
import org.jboss.netty.channel.ChannelPipeline;
import org.jboss.netty.channel.ChannelPipelineFactory;
import org.jboss.netty.handler.ssl.SslHandler;
  
public class SPDYPipelineFactory implements ChannelPipelineFactory {
  
 private SSLContext context;
  
 public SPDYPipelineFactory() {
  try {
   KeyStore keystore = KeyStore.getInstance("JKS");
   keystore.load(new FileInputStream("src/main/resources/server.jks"),
     "secret".toCharArray());
  
   KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509");
   kmf.init(keystore, "secret".toCharArray());
  
   context = SSLContext.getInstance("TLS");
   context.init(kmf.getKeyManagers(), null, null);
  
  } catch (Exception e) {
   e.printStackTrace();
  }
 }
  
 public ChannelPipeline getPipeline() throws Exception {
  // Create a default pipeline implementation.
  ChannelPipeline pipeline = pipeline();
  
  // Uncomment the following line if you want HTTPS
  SSLEngine engine = context.createSSLEngine();
  engine.setUseClientMode(false);
  
  NextProtoNego.put(engine, new SimpleServerProvider());
  NextProtoNego.debug = true;
  
  pipeline.addLast("ssl", new SslHandler(engine));
  pipeline.addLast("pipeLineSelector", new HttpOrSpdyHandler());
  
  return pipeline;
 }
}
В конструкторе из этого класса мы устанавливаем базовый контекст SSL. Хранилище ключей и ключ, которые мы используем, я создал с помощью java keytool, это обычная конфигурация SSL. Когда мы получаем запрос, вызывается операция getPipeline, чтобы определить, как обработать запрос. Здесь мы используем класс NextProtoNego, предоставляемый Jetty-NPN-API, для подключения нашего SSL-соединения к реализации NPN. В этой операции мы передаем провайдера, который используется в качестве обратного вызова и конфигурации для нашего сервера. Мы также устанавливаем для NextProtoNego.debug значение true. Это выводит некоторую информацию об отладке, которая делает отладку проще. Код для SimpleServerProvider очень прост:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
public class SimpleServerProvider implements ServerProvider {
  
 private String selectedProtocol = null;
  
 public void unsupported() {
  //if unsupported, default to http/1.1
  selectedProtocol = "http/1.1";
 }
  
 public List<String> protocols() {
     return Arrays.asList("spdy/2","http/1.1");
 }
  
 public void protocolSelected(String protocol) {
  selectedProtocol = protocol;
 }
  
 public String getSelectedProtocol() {
  return selectedProtocol;
 }
}
Этот код говорит сам за себя.
  • Неподдерживаемая операция вызывается, когда клиент не поддерживает NPN. В этом случае мы используем HTTP по умолчанию.
  • Операция protocol () возвращает протоколы, которые поддерживает сервер
  • Операция protocolSelected вызывается, когда протокол и клиент согласовали протокол.
GetSelectedProtocol — это метод, который мы будем использовать для получения выбранного протокола из другого обработчика в конвейере Netty.
Определите, на основе согласованного протокола, использовать ли HTTP или SPDY
Теперь нам нужно настроить Netty таким образом, чтобы он запускал определенный конвейер для запроса HTTPS и конвейер для запросов SPDY. Для этого давайте вернемся к небольшой части конвейера.
1
2
pipeline.addLast("ssl", new SslHandler(engine));
pipeline.addLast("pipeLineSelector", new HttpOrSpdyHandler());
Первая часть этого конвейера — это SslHandler, настроенный с поддержкой NPN. Следующий обработчик, который будет вызван, — HttpOrSpdyHandler. Этот обработчик на основе протокола определяет, какой конвейер использовать. Код для этого обработчика приведен ниже:
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
public class HttpOrSpdyHandler implements  ChannelUpstreamHandler {
  
 public void handleUpstream(ChannelHandlerContext ctx, ChannelEvent e)
   throws Exception {
  
  // determine protocol type
  SslHandler handler = ctx.getPipeline().get(SslHandler.class);
  SimpleServerProvider provider = (SimpleServerProvider) NextProtoNego.get(handler.getEngine());
  
  if ("spdy/2".equals(provider.getSelectedProtocol())) {
   ChannelPipeline pipeline = ctx.getPipeline();
  
         pipeline.addLast("decoder", new SpdyFrameDecoder());
         pipeline.addLast("spdy_encoder", new SpdyFrameEncoder());
         pipeline.addLast("spdy_session_handler", new SpdySessionHandler(true));
         pipeline.addLast("spdy_http_encoder", new SpdyHttpEncoder());
  
               // Max size of SPDY messages set to 1MB
         pipeline.addLast("spdy_http_decoder", new SpdyHttpDecoder(1024*1024));
         pipeline.addLast("handler", new HttpRequestHandler());
  
         // remove this handler, and process the requests as spdy
         pipeline.remove(this);
   ctx.sendUpstream(e);
  else if ("http/1.1".equals(provider.getSelectedProtocol())) {
   ChannelPipeline pipeline = ctx.getPipeline();
   pipeline.addLast("decoder", new HttpRequestDecoder());
   pipeline.addLast("http_encoder", new HttpResponseEncoder());
   pipeline.addLast("handler", new HttpRequestHandler());
  
   // remove this handler, and process the requests as http
   pipeline.remove(this);
   ctx.sendUpstream(e);
  } else {
   // we're still in protocol negotiation, no need for any handlers
   // at this point.
  }
 }
}
Используя API NPN и наш текущий контекст SSL, мы получаем SimpleServerProvider, который мы добавили ранее. Мы проверяем, установлен ли selectedProtocol, и если да, то настраиваем цепочку для обработки. Мы работаем с тремя вариантами в этом классе:
  1. Протокол отсутствует : возможно, протокол еще не согласован. В этом случае мы не делаем ничего особенного, а просто обрабатываем это нормально.
  2. Существует протокол http : мы настроили цепочку обработчиков для обработки HTTP-запросов.
  3. Существует протокол spdy : мы создали цепочку обработчиков для обработки запросов SPDY.
С этой цепочкой все сообщения, которые мы в конечном итоге получаем HttpRequestHandler, являются HTTP-запросами. Мы можем нормально обработать этот HTTP-запрос и вернуть HTTP-ответ. Различные конфигурации конвейера будут обрабатывать все это правильно.
Убедитесь, что правильные заголовки SPDY отправлены обратно с HTTP
Последний шаг, который нам нужно сделать, это этот тест. Мы протестируем это с последней версией Chrome, чтобы проверить, работает ли SPDY, и мы будем использовать wget для проверки обычных http-запросов. Я упоминал, что HttpRequestHandler, последний обработчик в цепочке, выполняет нашу обработку HTTP. Я использовал http://netty.io/docs/stable/xref/org/jboss/netty/example/http/snoop/Http… как HTTPRequestHandler, так как тот приятно возвращает информацию о HTTP-запросе, без меня делать что-либо. Если вы запустите это без изменений, вы столкнетесь с проблемой. Чтобы соотнести ответ HTTP с правильным сеансом SPDY, нам нужно скопировать заголовок из входящего запроса в ответ: заголовок «X-SPDY-Stream-ID». Я добавил следующее в HttpSnoopServerHandler, чтобы удостовериться, что эти заголовки скопированы (это должно было быть сделано в отдельном обработчике).
01
02
03
04
05
06
07
08
09
10
private final static String SPDY_STREAM_ID = = "X-SPDY-Stream-ID";
private final static String SPDY_STREAM_PRIO = "X-SPDY-Stream-Priority";
 
// in the writeResponse method add
 
if (request.containsHeader(SPDY_STREAM_ID)) {
    response.addHeader(SPDY_STREAM_ID,request.getHeader(SPDY_STREAM_ID));
    // optional header for prio
    response.addHeader(SPDY_STREAM_PRIO,0);
}
Теперь все, что осталось — это сервер с главным для запуска, и мы можем протестировать нашу реализацию SPDY.
01
02
03
04
05
06
07
08
09
10
11
12
13
public class SPDYServer {
  
 public static void main(String[] args) {
  // bootstrap is used to configure and setup the server
  ServerBootstrap bootstrap = new ServerBootstrap(
    new NioServerSocketChannelFactory(
      Executors.newCachedThreadPool(),
      Executors.newCachedThreadPool()));
  
  bootstrap.setPipelineFactory(new SPDYPipelineFactory());
  bootstrap.bind(new InetSocketAddress(8443));
 }
}
Запустите сервер, запустите Chrome и посмотрим, все ли работает. Откройте https: // localhost: 8443 / thisIsATest url, и вы должны получить результат, который выглядит примерно так:
На выходе сервера вы можете увидеть некоторые записи отладки NPN:
1
2
3
4
5
[S] NPN received for 68ce4f39[SSLEngine[hostname=null port=-1] SSL_NULL_WITH_NULL_NULL]
[S] NPN protocols [spdy/2, http/1.1] sent to client for 68ce4f39[SSLEngine[hostname=null port=-1] SSL_NULL_WITH_NULL_NULL]
[S] NPN received for 4b24e48f[SSLEngine[hostname=null port=-1] SSL_NULL_WITH_NULL_NULL]
[S] NPN protocols [spdy/2, http/1.1] sent to client for 4b24e48f[SSLEngine[hostname=null port=-1] SSL_NULL_WITH_NULL_NULL]
[S] NPN selected 'spdy/2' for 4b24e48f[SSLEngine[hostname=null port=-1] SSL_NULL_WITH_NULL_NULL]
Дополнительная проверка — просмотр открытых сессий SPDY в браузере Chrome с использованием следующего URL: chrome: // net-internals / # spdy
Теперь давайте проверим, работает ли старый добрый HTTP. Из командной строки сделайте следующее:
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
jos@Joss-MacBook-Pro.local:~$ wget --no-check-certificate https://localhost:8443/thisIsATest
--2012-04-27 16:29:09--  https://localhost:8443/thisIsATest
Resolving localhost... ::1, 127.0.0.1, fe80::1
Connecting to localhost|::1|:8443... connected.
WARNING: cannot verify localhost's certificate, issued by `/C=NL/ST=NB/L=Waalwijk/O=smartjava/OU=smartjava/CN=localhost':
  Self-signed certificate encountered.
HTTP request sent, awaiting response... 200 OK
Length: 285
Saving to: `thisIsATest'
  
100%[==================================================================================>] 285         --.-K/s   in 0s     
  
2012-04-27 16:29:09 (136 MB/s) - `thisIsATest' saved [285/285]
  
jos@Joss-MacBook-Pro.local:~$ cat thisIsATest
WELCOME TO THE WILD WILD WEB SERVER
===================================
VERSION: HTTP/1.1
HOSTNAME: localhost:8443
REQUEST_URI: /thisIsATest
  
HEADER: User-Agent = Wget/1.13.4 (darwin11.2.0)
HEADER: Accept = */*
HEADER: Host = localhost:8443
HEADER: Connection = Keep-Alive
  
jos@Joss-MacBook-Pro.local:~$
И это работает! Wget использует стандартный HTTPS, и мы получаем результат, а Chrome использует SPDY и представляет результат из того же обработчика. Через пару дней я также опубликую статью о том, как включить SPDY для Play Framework 2.0, поскольку их веб-сервер также основан на Netty.