Большинство людей уже слышали о 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 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.
Справка: прозрачное использование SPDY и HTTP с использованием Netty от нашего партнера по JCG Йоса Дирксена из блога Smart Java .