Большинство людей уже слышали о 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, нам нужно сделать следующее:
- Добавить библиотеку NPN в bootpath
- Подключите контекст 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, и если да, то настраиваем цепочку для обработки. Мы работаем с тремя вариантами в этом классе:
- Протокол отсутствует : возможно, протокол еще не согласован. В этом случае мы не делаем ничего особенного, а просто обрабатываем это нормально.
- Существует протокол http : мы настроили цепочку обработчиков для обработки HTTP-запросов.
- Существует протокол 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 addif (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/thisIsATestResolving localhost... ::1, 127.0.0.1, fe80::1Connecting 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 OKLength: 285Saving 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.1HOSTNAME: localhost:8443REQUEST_URI: /thisIsATest HEADER: User-Agent = Wget/1.13.4 (darwin11.2.0)HEADER: Accept = */*HEADER: Host = localhost:8443HEADER: Connection = Keep-Alive jos@Joss-MacBook-Pro.local:~$ |
И это работает! Wget использует стандартный HTTPS, и мы получаем результат, а Chrome использует SPDY и представляет результат из того же обработчика. Через пару дней я также опубликую статью о том, как включить SPDY для Play Framework 2.0, поскольку их веб-сервер также основан на Netty.
Справка: прозрачное использование SPDY и HTTP с использованием Netty от нашего партнера по JCG Йоса Дирксена из блога Smart Java .

