Статьи

Как использовать SPDY с Jetty

SPDY — это новый протокол, предложенный Google в качестве нового протокола для Интернета. SPDY совместим с HTTP, но пытается уменьшить загрузку веб-страниц, используя сжатие, мультиплексирование и расстановку приоритетов. Чтобы быть более точным, цели для ускорения: ( http://dev.chromium.org/spdy/spdy-whitepaper ). Проект SPDY определяет и реализует протокол прикладного уровня для Интернета, что значительно снижает задержки.

Цели высокого уровня для SPDY:

  • Чтобы сократить время загрузки страницы на 50%. Наши предварительные результаты приблизились к этой цели (см. Ниже).
  • Чтобы минимизировать сложность развертывания. SPDY использует TCP в качестве базового транспортного уровня, поэтому не требует никаких изменений в существующей сетевой инфраструктуре.
  • Чтобы избежать необходимости каких-либо изменений в содержании со стороны авторов сайта. Единственные изменения, необходимые для поддержки SPDY, относятся к клиентскому пользовательскому агенту и приложениям веб-сервера.
  • Собрать воедино единомышленников, заинтересованных в изучении протоколов как способа решения проблемы латентности. Мы надеемся разработать этот новый протокол в сотрудничестве с сообществом разработчиков программного обеспечения с открытым исходным кодом.

Некоторые конкретные технические цели:

  • Чтобы разрешить много одновременных HTTP-запросов для одного сеанса TCP.
  • Для уменьшения пропускной способности, используемой в настоящее время HTTP, путем сжатия заголовков и устранения ненужных заголовков.
  • Определить протокол, который легко реализовать и эффективно использовать на сервере. Мы надеемся уменьшить сложность HTTP, сократив крайние случаи и определив легко анализируемые форматы сообщений.
  • Чтобы сделать SSL базовым транспортным протоколом, для большей безопасности и совместимости с существующей сетевой инфраструктурой. Хотя SSL и вводит штраф за задержку, мы считаем, что долгосрочное будущее Интернета зависит от безопасного сетевого подключения. Кроме того, использование SSL необходимо, чтобы гарантировать, что связь через существующие прокси не нарушена.
  • Чтобы сервер мог инициировать связь с клиентом и передавать данные клиенту, когда это возможно.

Настройка Maven

В этой статье мы не будем слишком подробно разбираться в технической реализации этого протокола, но мы покажем вам, как вы можете начать использовать и экспериментировать с SPDY самостоятельно. Для этого мы будем использовать Jetty, которая имеет реализацию SPDY, доступную в ее последней версии ( http://wiki.eclipse.org/Jetty/Feature/SPDY ).

Итак, начнем. Для этого примера мы позволим Maven обрабатывать зависимости. И мы будем использовать следующий POM.

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
 <modelVersion>4.0.0</modelVersion>
 <groupId>smartjava.jetty.spdy</groupId>
 <artifactId>SPDY-Example</artifactId>
 <version>0.0.1-SNAPSHOT</version>
 <dependencies>
  <dependency>
   <groupId>org.eclipse.jetty.aggregate</groupId>
   <artifactId>jetty-all-server</artifactId>
   <version>8.1.2.v20120308</version>
   <type>jar</type>
   <scope>compile</scope>
   <exclusions>
    <exclusion>
     <artifactId>mail</artifactId>
     <groupId>javax.mail</groupId>
    </exclusion>
   </exclusions>
  </dependency>
  <dependency>
   <groupId>org.eclipse.jetty.spdy</groupId>
   <artifactId>spdy-jetty</artifactId>
   <version>8.1.2.v20120308</version>
  </dependency>
  <dependency>
   <groupId>org.eclipse.jetty.spdy</groupId>
   <artifactId>spdy-core</artifactId>
   <version>8.1.2.v20120308</version>
  </dependency>
  <dependency>
   <groupId>org.eclipse.jetty.spdy</groupId>
   <artifactId>spdy-jetty-http</artifactId>
   <version>8.1.2.v20120308</version>
  </dependency>
  <dependency>
   <groupId>org.eclipse.jetty.npn</groupId>
   <artifactId>npn-api</artifactId>
   <version>8.1.2.v20120308</version>
                        <scope>provided</scope>
  </dependency>
 </dependencies>
</project>

Расширение NTP TLS

С этим POM загружаются правильные библиотеки, поэтому мы можем начать использовать определенные классы SPDY в Jetty. Прежде чем мы действительно сможем использовать SPDY, нам также необходимо настроить Java для использования расширения протокола TLS: TLS Next Protocol Negotiation или NPN для краткости. Подробную информацию об этом расширении можно найти в техническом примечании по Google ( http://technotes.googlecode.com/git/nextprotoneg.html ), но вкратце это сводится к этой проблеме. Что делать, если мы хотим использовать протокол, отличный от HTTP, когда мы устанавливаем соединение с сервером через TLS. Мы не знаем, поддерживает ли сервер этот протокол, и, поскольку SPDY сфокусирован на скорости, мы не хотим дополнительной задержки при прохождении туда-обратно. Несмотря на то, что существует несколько различных решений, большинство из них страдают от непредсказуемости, дополнительных циклических переходов или поломок существующих прокси-серверов (для получения дополнительной информации см. Http://www.ietf.org/proceedings/80/slides/tls-1.pdf ).
Предложенное Google решение использует механизм расширения TLS для определения используемого протокола. Это называется «Переговоры по следующему протоколу» или сокращенно NPN. С этим расширением следующие шаги предпринимаются во время рукопожатия TLS:

  1. Клиент показывает поддержку этого расширения
  2. Сервер отвечает с этой поддержкой и включает список поддерживаемых протоколов
  3. Клиент отправляет протокол, который он хочет использовать, который не должен быть предоставлен сервером.

Это приводит к следующему рукопожатию TLS:

Клиентский сервер

ClientHello (расширение NP) ——–>
ServerHello (расширение NP и список протоколов)
Сертификат *
ServerKeyExchange *
CertificateRequest *
<——– ServerHelloDone
Сертификат *
ClientKeyExchange
CertificateVerify *
[ChangeCipherSpec] NextProtocol
Закончено ——–>
[ChangeCipherSpec] <——– Завершено
Данные приложения <——-> Данные приложения

Для получения дополнительной информации о рукопожатиях TLS / SSL посмотрите мою предыдущую статью о том, как анализировать ошибки Java SSL: http://www.smartjava.org/content/how-analyze-java-ssl-errors .
Поэтому нам нужен NPN, чтобы быстро определить протокол, который мы хотим использовать. Поскольку это не стандартный TLS, нам нужно настроить Java для использования NPN. Стандартная Java (пока) не поддерживает NPN, поэтому мы не можем запустить SPDY на стандартной JVM. Чтобы решить эту проблему, Jetty создала реализацию NPN, которую можно использовать вместе с OpenJDK 7 (для получения более подробной информации см. Http://wiki.eclipse.org/Jetty/Feature/NPN ). Вы можете скачать эту реализацию здесь: http://repo2.maven.org/maven2/org/mortbay/jetty/npn/npn-boot/, и вы должны добавить ее в путь загрузки вашего класса как таковой:

1
java -Xbootclasspath/p:<path_to_npn_boot_jar> ...

Завершение HTTP-запроса в SPDY

Теперь вы можете начать использовать SPDY от Jetty. Jetty поддерживает эту функцию двумя различными способами. Вы можете использовать его для прозрачного преобразования из SPDY в HTTP и обратно или использовать его для непосредственного общения с SPDY. Давайте создадим простую конфигурацию сервера, на которой размещается некоторый статический контент, используя соединение с поддержкой SPDY. Для этого мы будем использовать следующую конфигурацию Jetty:

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
import org.eclipse.jetty.server.Connector;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.handler.ContextHandler;
import org.eclipse.jetty.server.handler.ResourceHandler;
import org.eclipse.jetty.spdy.http.HTTPSPDYServerConnector;
import org.eclipse.jetty.util.ssl.SslContextFactory;
  
  
public class SPDYServerLauncher {
  
 public static void main(String[] args) throws Exception {
  
  // the server to start
  Server server = new Server();
  
  // the ssl context to use
  SslContextFactory sslFactory = new SslContextFactory();
  sslFactory.setKeyStorePath("src/main/resources/spdy.keystore");
  sslFactory.setKeyStorePassword("secret");
  sslFactory.setProtocol("TLSv1");
  
  // simple connector to add to serve content using spdy
  Connector connector = new HTTPSPDYServerConnector(sslFactory);
  connector.setPort(8443);
  
  // add connector to the server
  server.addConnector(connector);
  
  // add a handler to serve content
  ContextHandler handler = new ContextHandler();
  handler.setContextPath("/content");
  handler.setResourceBase("src/main/resources/webcontent");
  handler.setHandler(new ResourceHandler());
  
  server.setHandler(handler);
  
  server.start();
  server.join();
 }
}

Поскольку Jetty также имеет очень гибкий язык конфигурации XML, вы можете сделать то же самое, используя следующую конфигурацию XML.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<Configure id="Server" class="org.eclipse.jetty.server.Server">
  
    <New id="sslContextFactory" class="org.eclipse.jetty.util.ssl.SslContextFactory">
        <Set name="keyStorePath">src/main/resources/spdy.keystore</Set>
        <Set name="keyStorePassword">secret</Set>
        <Set name="protocol">TLSv1</Set>
    </New>
  
    <Call name="addConnector">
        <Arg>
            <New class="org.eclipse.jetty.spdy.http.HTTPSPDYServerConnector">
                <Arg>
                    <Ref id="sslContextFactory" />
                </Arg>
                <Set name="Port">8443</Set>
            </New>
        </Arg>
    </Call>
  
   // Use standard XML configuration for the other handlers and other
  // stuff you want to add
  
</Configure>

Как вы можете видеть в этом списке, мы указываем контекст SSL. Это необходимо, поскольку SPDY работает над TLS. Когда мы запустим эту конфигурацию, Jetty начнет прослушивать порт 8443 для соединений SPDY. Не все браузеры пока поддерживают SPDY, я протестировал этот пример с использованием новейшего браузера Chrome. Если вы перейдете по адресу https: // localhost: 8443 / dummy.html (файл, который я создал для тестирования), вы увидите содержимое этого файла, так же, как вы запрашивали его с помощью HTTPS. Так что здесь происходит? Давайте сначала посмотрим на представление сеанса SPDY, которое предоставляет Chrome, чтобы определить, действительно ли мы используем SPDY. Если вы переходите по следующему URL: chrome: // net-internals / # events & q = type: SPDY_SESSION% 20is: active. Вы увидите что-то вроде следующего рисунка.

SPDY в Chrome

В этом представлении вы можете увидеть все текущие сеансы SPDY. Если все настроено правильно, вы также можете увидеть сеанс SPDY, подключенный к localhost. Дополнительная проверка, чтобы увидеть, все ли работает как задумано, чтобы включить отладку расширения NPN. Вы можете сделать это, добавив следующую строку в код Java, который вы используете для запуска сервера:

NextProtoNego. debug = true ;

Используйте протокол SPDY напрямую

Теперь, когда у нас работает HTTP через SPDY, давайте рассмотрим другой вариант, который предоставляет Jetty, который позволяет нам напрямую отправлять и получать сообщения SPDY. Для этого примера мы просто создадим клиент, который отправляет сообщение на сервер каждые 5 секунд. Сервер отправляет ответы подключенному клиенту с количеством полученных сообщений каждую секунду. Сначала мы создаем код сервера.

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
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
  
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.spdy.SPDYServerConnector;
import org.eclipse.jetty.spdy.api.DataInfo;
import org.eclipse.jetty.spdy.api.ReplyInfo;
import org.eclipse.jetty.spdy.api.Stream;
import org.eclipse.jetty.spdy.api.StreamFrameListener;
import org.eclipse.jetty.spdy.api.StringDataInfo;
import org.eclipse.jetty.spdy.api.SynInfo;
import org.eclipse.jetty.spdy.api.server.ServerSessionFrameListener;
  
public class SPDYListener {
  
 public static void main(String[] args) throws Exception {
  
  // Frame listener that handles the communication over speedy 
  ServerSessionFrameListener frameListener = new ServerSessionFrameListener.Adapter() {
  
   /**
    * As soon as we receive a syninfo we return the handler for the stream on
    * this session
    */
   @Override
   public StreamFrameListener onSyn(final Stream stream, SynInfo synInfo) {
  
    // Send a reply to this message
    stream.reply(new ReplyInfo(false));
  
    // and start a timer that sends a request to this stream every 5 seconds
    ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor();
    Runnable periodicTask = new Runnable() {
      private int i = 0;
         public void run() {
          // send a request and don't close the stream
             stream.data(new StringDataInfo("Data from the server " + i++, false));
         }
     };
    executor.scheduleAtFixedRate(periodicTask, 0, 1, TimeUnit.SECONDS);
  
    // Next create an adapter to further handle the client input from specific stream.
    return new StreamFrameListener.Adapter() {
  
     /**
      * We're only interested in the data, not the headers in this
      * example
      */
     public void onData(Stream stream, DataInfo dataInfo) {
      String clientData = dataInfo.asString("UTF-8", true);
      System.out.println("Received the following client data: " + clientData);
     }
    };
   }
  };
  
  // Wire up and start the connector
  org.eclipse.jetty.server.Server server = new Server();
  SPDYServerConnector connector = new SPDYServerConnector(frameListener);
  connector.setPort(8181);
  
  server.addConnector(connector);
  server.start();
  server.join();
 }
}

И код клиента выглядит так:

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
import java.net.InetSocketAddress;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
  
import org.eclipse.jetty.spdy.SPDYClient;
import org.eclipse.jetty.spdy.api.DataInfo;
import org.eclipse.jetty.spdy.api.SPDY;
import org.eclipse.jetty.spdy.api.Session;
import org.eclipse.jetty.spdy.api.Stream;
import org.eclipse.jetty.spdy.api.StreamFrameListener;
import org.eclipse.jetty.spdy.api.StringDataInfo;
import org.eclipse.jetty.spdy.api.SynInfo;
  
/**
 * Calls the server every couple of seconds.
 *
 * @author jos
 */
public class SPDYCaller {
  
 public static void main(String[] args) throws Exception {
  
  // this listener receives data from the server. It then prints out the data
  StreamFrameListener streamListener = new StreamFrameListener.Adapter() {
  
      public void onData(Stream stream, DataInfo dataInfo)  {
          // Data received from server
          String content = dataInfo.asString("UTF-8", true);
          System.out.println("SPDY content: " + content);
      }
  };
  
  // Create client
  SPDYClient.Factory clientFactory = new SPDYClient.Factory();
  clientFactory.start();
  SPDYClient client = clientFactory.newSPDYClient(SPDY.V2);
  
  // Create a session to the server running on localhost port 8181
  Session session = client.connect(new InetSocketAddress("localhost", 8181), null).get(5, TimeUnit.SECONDS);
  
  // Start a new session, and configure the stream listener
  final Stream stream = session.syn(new SynInfo(false), streamListener).get(5, TimeUnit.SECONDS);
  
  //start a timer that sends a request to this stream every second
  ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor();
  Runnable periodicTask = new Runnable() {
    private int i = 0;
  
       public void run() {
        // send a request, don't close the stream
        stream.data(new StringDataInfo("Data from the client " + i++, false));
       }
   };
  executor.scheduleAtFixedRate(periodicTask, 0, 1, TimeUnit.SECONDS);
 }
}

Это показывает следующий вывод на клиенте и на сервере:

клиент:
..
SPDY контент: данные с сервера 3
SPDY контент: данные с сервера 4
SPDY контент: данные с сервера 5
SPDY контент: данные с сервера 6
..

сервер:

Получены следующие данные клиента: Данные от клиента 2
Получены следующие данные клиента: Данные от клиента 3
Получены следующие данные клиента: Данные от клиента 4
Получены следующие данные клиента: Данные от клиента 5

Сам код должен быть понятен из встроенных комментариев. Единственное, что нужно помнить, когда вы хотите отправить более одного сообщения данных через поток, это убедиться, что для второго параметра конструктора StringDataInfo установлено значение false. Если установлено значение true, поток будет закрыт после отправки данных.

1
stream.data(new StringDataInfo("Data from the client " + i++, false));

Это просто показывает простой пример использования протокола SPDY напрямую. Дополнительную информацию и примеры можно найти на вики Jetty и в документации SPDY API .

Ссылка: Как использовать SPDY с Jetty от нашего партнера JCG Йоса Дирксена в блоге Smart Java .