如何在Jetty中使用SPDY

SPDY是Google提出的一種新協議,是針對網絡的新協議。 SPDY與HTTP兼容,但嘗試通過壓縮,多路複用和優先級降低網頁負載。準確地說,快速的目標是:( http://dev.chromium.org/spdy/spdy-whitepaper )。 SPDY項目爲Web定義並實現了一個應用程序層協議,可大大減少延遲。

SPDY的高級目標是:

  • 旨在將頁面加載時間減少50%。 我們的初步結果已經接近這個目標(見下文)。
  • 以最小化部署複雜性。 SPDY使用TCP作爲基礎傳輸層,因此不需要更改現有的網絡基礎結構。
  • 爲了避免網站作者對內容進行任何更改。 支持SPDY的唯一更改是在客戶端用戶代理和Web服務器應用程序中。
  • 將對探索協議感興趣的志趣相投的各方聚集在一起,以解決延遲問題。 我們希望與開源社區和行業專家合作開發此新協議

一些特定的技術目標是:

  • 允許多個併發HTTP請求在單個TCP會話中運行。
  • 通過壓縮頭並消除不必要的頭來減少HTTP當前使用的帶寬。
  • 定義易於實施且服務器效率高的協議。 我們希望通過減少邊緣情況並定義易於解析的消息格式來降低HTTP的複雜性。
  • 使SSL成爲基礎傳輸協議,以提高安全性和與現有網絡基礎結構的兼容性。 儘管SSL確實會帶來延遲損失,但我們認爲網絡的長期發展取決於安全的網絡連接。 另外,必須使用SSL以確保跨現有代理的通信不中斷。
  • 使服務器能夠啓動與客戶端的通信並將數據推送到客戶端。

安裝專家

在本文中,我們不會過多地研究此協議的技術實現,但是我們將向您展示如何開始自己使用SPDY並進行試驗。 爲此,我們將使用在最新版本( http://wiki.eclipse.org/Jetty/Feature/SPDY )中提供SPDY實現的Jetty。

因此,讓我們開始吧。 對於此示例,我們將讓Maven處理依賴關係。 我們將使用以下POM。

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
 <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,將加載正確的庫,因此我們可以開始在Jetty中使用特定的SPDY類。 但是,在真正使用SPDY之前,我們還需要配置Java以使用TLS協議的擴展:TLS下一協議協商或簡稱NPN。 可以在googles技術說明( http://technotes.googlecode.com/git/nextprotoneg.html )上找到此擴展程序的詳細信息,但總而言之,可以歸結爲這個問題。 通過TLS與服務器建立連接時,如果我們想使用不同於HTTP的協議該怎麼辦? 我們不知道服務器是否支持該協議,並且由於SPDY專注於速度,因此我們不希望增加往返行程的延遲。 即使有幾種不同的解決方案,但大多數解決方案都具有不可預測性,額外的往返次數或破壞現有代理的影響(有關更多信息,請參閱( http://www.ietf.org/proceedings/80/slides/tls-1.pdf )。
Google提出的解決方案是使用TLS的擴展機制來確定要使用的協議。 這稱爲「下一協議協商」或簡稱NPN。 使用此擴展,在TLS握手期間將執行以下步驟:

  1. 客戶端顯示對此擴展程序的支持
  2. 服務器響應此支持幷包括支持的協議列表
  3. 客戶端發送他要使用的協議,而不必由服務器提供。

這導致以下TLS握手:

客戶端服務器

ClientHello(NP擴展)——–>
ServerHello(NP擴展和協議列表)
證書*
ServerKeyExchange *
證書申請*
<——– ServerHelloDone
證書*
ClientKeyExchange
證書驗證*
[ChangeCipherSpec] NextProtocol
成品——–>
[ChangeCipherSpec] <——–已完成
應用數據<——->應用數據

有關TLS / SSL握手的更多信息,請參閱我以前的文章,該文章有關如何分析Java SSL錯誤: http : //www.smartjava.org/content/how-analyze-java-ssl-errors
因此,我們需要NPN來快速確定我們要使用的協議。 由於這不是標準的TLS,因此我們需要配置Java以使用NPN。 標準Java還不支持NPN,因此我們不能在標準JVM上運行SPDY。 爲了解決這個問題,Jetty創建了一個可以與OpenJDK 7一起使用的NPN實現(有關更多詳細信息,請參見http://wiki.eclipse.org/Jetty/Feature/NPN )。 您可以從此處下載此實現: http : //repo2.maven.org/maven2/org/mortbay/jetty/npn/npn-boot/ ,您必須像這樣將其添加到啓動類路徑中:

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

在SPDY中包裝HTTP請求

現在,您可以開始使用Jetty的SPDY。 Jetty以兩種不同方式支持此功能。 您可以使用它來將SPDY透明地轉換爲HTTP,然後再次返回,也可以使用它直接對話SPDY。 讓我們創建一個簡單的服務器配置,使用啓用SPDY的連接託管一些靜態內容。 爲此,我們將使用以下Jetty配置:

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配置執行相同的操作。

<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請求該文件一樣。 那麼這裏發生了什麼? 首先,讓我們看一下Chrome提供的SPDY會話視圖,以確定我們是否真的在使用SPDY。 如果您導航到以下網址:chrome:// net-internals /#events&q = type:SPDY_SESSION%20is:active。 您會看到類似下圖的內容。

Chrome中的SPDY

在此視圖中,您可以看到所有當前的SPDY會話。 如果一切配置正確,您還可以看到連接到本地主機的SPDY會話。 進行額外檢查以查看是否一切正常,以啓用NPN擴展的調試功能。 您可以通過在用於啓動服務器的Java代碼中添加以下行來做到這一點:

NextProtoNego。 調試 = true ;

直接使用SPDY協議

現在我們已經可以使用HTTP over SPDY了,讓我們看一下Jetty提供的另一個選項,它允許我們直接發送和接收SPDY消息。 對於此示例,我們將創建一個每5秒向服務器發送一條消息的客戶端。 服務器以每秒接收的消息數發送響應到連接的客戶端。 首先,我們創建服務器代碼。

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();
 }
}

客戶端代碼如下所示:

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,則在發送數據後將關閉流。

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

這僅顯示了一個簡單的用例,您可以直接使用SPDY協議。 可以在Jetty WikiSPDY API文檔中找到更多信息和示例。

參考: Smart Java博客上來自JCG合作伙伴 Jos Dirksen的如何將SPDY與Jetty結合使用

翻譯自: https://www.javacodegeeks.com/2012/04/how-to-use-spdy-with-jetty.html