SockJS フォールバック

パブリックインターネット上では、Upgrade ヘッダーを渡すように構成されていないか、アイドル状態にあると思われる長時間の接続を閉じるため、コントロール外の制限プロキシが WebSocket 相互作用を妨げる可能性があります。

この問題の解決策は WebSocket エミュレーションです。つまり、最初に WebSocket を使用してから、WebSocket 相互作用をエミュレートし、同じアプリケーションレベルの API を公開する HTTP ベースの手法にフォールバックしようとします。

サーブレットスタックでは、Spring Framework は SockJS プロトコルの両方のサーバー(およびクライアント)サポートを提供します。

概要

SockJS のゴールは、アプリケーションが WebSocket API を使用できるようにすることですが、実行時に必要に応じて、アプリケーションコードを変更せずに非 WebSocket の代替にフォールバックすることです。

SockJS の構成:

SockJS は、ブラウザーで使用するために設計されています。さまざまなバージョンのブラウザーをサポートするために、さまざまな手法を使用します。SockJS トランスポート型とブラウザーの完全なリストについては、SockJS クライアント [GitHub] (英語) ページを参照してください。トランスポートは、WebSocket、HTTP ストリーミング、HTTP ロングポーリングの 3 つの一般的なカテゴリに分類されます。これらのカテゴリの概要については、このブログ投稿 (英語) を参照してください。

SockJS クライアントは、GET /info を送信してサーバーから基本情報を取得することから始めます。その後、使用するトランスポートを決定する必要があります。可能であれば、WebSocket が使用されます。そうでない場合、ほとんどのブラウザーには、少なくとも 1 つの HTTP ストリーミングオプションがあります。そうでない場合は、HTTP(ロング)ポーリングが使用されます。

すべてのトランスポートリクエストの URL 構造は次のとおりです。

https://host:port/myApp/myEndpoint/{server-id}/{session-id}/{transport}

内容:

  • {server-id} は、クラスター内のリクエストのルーティングに役立ちますが、それ以外では使用されません。

  • {session-id} は、SockJS セッションに属する HTTP リクエストを関連付けます。

  • {transport} は、トランスポート型(たとえば、websocketxhr-streaming など)を示します。

WebSocket トランスポートは、WebSocket ハンドシェイクを行うために 1 つの HTTP リクエストのみを必要とします。その後のすべてのメッセージは、そのソケットで交換されます。

HTTP トランスポートにはより多くのリクエストが必要です。たとえば、Ajax/XHR ストリーミングは、サーバーからクライアントへのメッセージに対する 1 つの長時間実行リクエストと、クライアントからサーバーへのメッセージに対する追加の HTTP POST リクエストに依存しています。ロングポーリングは似ていますが、各サーバーからクライアントへの送信後に現在のリクエストが終了する点が異なります。

SockJS は最小限のメッセージフレーミングを追加します。例: サーバーは最初に文字 o (「オープン」フレーム)を送信し、メッセージは a["message1","message2"] (JSON エンコード配列)として送信され、文字 h (「ハートビート」フレーム)はメッセージが 25 秒間流れない場合(デフォルト)、セッションを閉じるための文字 c (「閉じる」フレーム)。

詳細については、ブラウザーで例を実行し、HTTP リクエストを確認してください。SockJS クライアントでは、トランスポートのリストを修正できるため、各トランスポートを一度に 1 つずつ見ることができます。SockJS クライアントは、ブラウザーコンソールで役立つメッセージを有効にするデバッグフラグも提供します。サーバー側では、org.springframework.web.socket の TRACE ロギングを有効にできます。さらに詳細については、SockJS プロトコルのナレーション付きテスト (英語) を参照してください。

SockJS を有効にする

次の例に示すように、Java 構成を介して SockJS を有効にできます。

@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {

	@Override
	public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
		registry.addHandler(myHandler(), "/myHandler").withSockJS();
	}

	@Bean
	public WebSocketHandler myHandler() {
		return new MyHandler();
	}

}

次の例は、前述の例に相当する XML 構成を示しています。

<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xmlns:websocket="http://www.springframework.org/schema/websocket"
	xsi:schemaLocation="
		http://www.springframework.org/schema/beans
		https://www.springframework.org/schema/beans/spring-beans.xsd
		http://www.springframework.org/schema/websocket
		https://www.springframework.org/schema/websocket/spring-websocket.xsd">

	<websocket:handlers>
		<websocket:mapping path="/myHandler" handler="myHandler"/>
		<websocket:sockjs/>
	</websocket:handlers>

	<bean id="myHandler" class="org.springframework.samples.MyHandler"/>

</beans>

上記の例は Spring MVC アプリケーションで使用するためのものであり、DispatcherServlet の構成に含める必要があります。ただし、Spring の WebSocket および SockJS のサポートは Spring MVC に依存しません。SockJsHttpRequestHandler (Javadoc) の助けを借りて、他の HTTP サービス環境に統合するのは比較的簡単です。

ブラウザー側では、アプリケーションは sockjs-client [GitHub] (英語) (バージョン 1.0.x)を使用できます。W3C WebSocket API をエミュレートし、サーバーと通信して、実行するブラウザーに応じて最適なトランスポートオプションを選択します。sockjs-client [GitHub] (英語) ページと、ブラウザーでサポートされているトランスポート型のリストを参照してください。また、クライアントは、含めるトランスポートを指定するなど、いくつかの構成オプションも提供します。

IE 8 および 9

Internet Explorer 8 および 9 は引き続き使用されます。それらは、SockJS を持つ主な理由です。このセクションでは、これらのブラウザーでの実行に関する重要な考慮事項について説明します。

SockJS クライアントは、Microsoft の XDomainRequest (英語) を使用して、IE 8 および 9 で Ajax/XHR ストリーミングをサポートしています。これはドメイン間で機能しますが、Cookie の送信をサポートしません。多くの場合、Cookie は Java アプリケーションに不可欠です。ただし、SockJS クライアントは(Java だけでなく)多くのサーバー型で使用できるため、Cookie が重要かどうかを知る必要があります。その場合、SockJS クライアントはストリーミングに Ajax/XHR を好みます。それ以外の場合は、iframe ベースの手法に依存します。

SockJS クライアントからの最初の /info リクエストは、クライアントのトランスポートの選択に影響を与える可能性のある情報のリクエストです。それらの詳細の 1 つは、サーバーアプリケーションが Cookie に依存するかどうかです(たとえば、認証目的またはスティッキーセッションでのクラスタリング)。Spring の SockJS サポートには、sessionCookieNeeded というプロパティが含まれています。ほとんどの Java アプリケーションは JSESSIONID Cookie に依存しているため、デフォルトで有効になっています。アプリケーションで必要ない場合は、このオプションをオフにできます。SockJS クライアントは IE 8 および 9 で xdr-streaming を選択する必要があります。

iframe ベースのトランスポートを使用する場合は、HTTP レスポンスヘッダー X-Frame-Options を DENYSAMEORIGINALLOW-FROM <origin> に設定することで、特定のページでの IFrame の使用をブロックするようにブラウザーに指示できることに注意してください。これは、クリックジャッキング [OWASP] (英語) を防ぐために使用されます。

Spring Security 3.2+ は、すべてのレスポンスで X-Frame-Options を設定するためのサポートを提供します。デフォルトでは、Spring Security Java 構成はそれを DENY に設定します。3.2 では、Spring Security XML 名前空間はデフォルトでそのヘッダーを設定しませんが、そのように構成できます。将来的には、デフォルトで設定される可能性があります。

X-Frame-Options ヘッダーの設定方法の詳細については、Spring Security ドキュメントのデフォルトのセキュリティヘッダーを参照してください。追加の背景については、gh-2718 [GitHub] (英語) も参照してください。

アプリケーションが X-Frame-Options レスポンスヘッダーを追加し(必要な場合)、iframe ベースのトランスポートに依存している場合、ヘッダー値を SAMEORIGIN または ALLOW-FROM <origin> に設定する必要があります。Spring SockJS サポートは、iframe からロードされるため、SockJS クライアントの場所を知る必要もあります。デフォルトでは、iframe は CDN の場所から SockJS クライアントをダウンロードするように設定されています。このオプションを構成して、アプリケーションと同じオリジンからの URL を使用することをお勧めします。

次の例は、Java 構成でこれを行う方法を示しています。

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

	@Override
	public void registerStompEndpoints(StompEndpointRegistry registry) {
		registry.addEndpoint("/portfolio").withSockJS()
				.setClientLibraryUrl("http://localhost:8080/myapp/js/sockjs-client.js");
	}

	// ...

}

XML 名前空間は、<websocket:sockjs> 要素を通じて同様のオプションを提供します。

初期開発時に、ブラウザーが SockJS リクエスト(iframe など)をキャッシュしないようにする SockJS クライアント devel モードを有効にします。有効にする方法の詳細については、SockJS クライアント [GitHub] (英語) ページを参照してください。

ハートビート

SockJS プロトコルでは、サーバーがハートビートメッセージを送信して、プロキシが接続がハングしていると判断するのを防ぐ必要があります。Spring SockJS 構成には heartbeatTime というプロパティがあり、これを使用して周波数をカスタマイズできます。デフォルトでは、その接続で他のメッセージが送信されていないと仮定して、25 秒後にハートビートが送信されます。この 25 秒の値は、パブリックインターネットアプリケーションの次の IETF の推奨事項 (英語) と一致しています。

WebSocket および SockJS で STOMP を使用する場合、STOMP クライアントとサーバーが交換されるハートビートをネゴシエートすると、SockJS ハートビートは無効になります。

Spring SockJS サポートでは、ハートビートタスクをスケジュールするように TaskScheduler を構成することもできます。タスクスケジューラはスレッドプールによってサポートされており、デフォルト設定は使用可能なプロセッサーの数に基づいています。特定のニーズに応じて設定をカスタマイズすることを検討してください。

クライアント切断

HTTP ストリーミングおよび HTTP ロングポーリング SockJS トランスポートでは、接続が通常より長く開いたままである必要があります。これらの手法の概要については、このブログ投稿 (英語) を参照してください。

サーブレットコンテナーでは、これは Servlet 3 非同期サポートを通じて行われます。これにより、サーブレットコンテナースレッドを終了し、リクエストを処理し、別のスレッドからのレスポンスへの書き込みを継続できます。

特定の課題は、サーブレット API が、なくなったクライアントに通知を提供しないことです。eclipse-ee4j/servlet-api#44 [GitHub] (英語) を参照してください。ただし、サーブレットコンテナーは、レスポンスへの後続の書き込み試行で例外を発生させます。Spring の SockJS サービスはサーバー送信ハートビート(デフォルトでは 25 秒ごと)をサポートしているため、通常はその期間内(またはメッセージがより頻繁に送信される場合はそれ以前)にクライアントの切断が検出されます。

その結果、クライアントが切断されたためにネットワーク I/O エラーが発生する可能性があり、不要なスタックトレースでログがいっぱいになる可能性があります。Spring は、クライアントの切断(各サーバーに固有)を表すネットワーク障害を特定し、専用のログカテゴリ DISCONNECTED_CLIENT_LOG_CATEGORY (AbstractSockJsSession で定義)を使用して最小限のメッセージを記録するように最善を尽くします。スタックトレースを表示する必要がある場合は、そのログカテゴリを TRACE に設定できます。

SockJS と CORS

クロスオリジンリクエストを許可する場合(許可されたオリジンを参照)、SockJS プロトコルは XHR ストリーミングおよびポーリングトランスポートでのクロスドメインサポートに CORS を使用します。CORS ヘッダーは、レスポンスに CORS ヘッダーの存在が検出されない限り、自動的に追加されます。そのため、アプリケーションがすでに(たとえば、サーブレットフィルターを介して)CORS サポートを提供するように構成されている場合、Spring の SockJsService はこのパートをスキップします。

Spring の SockJsService で suppressCors プロパティを設定することにより、これらの CORS ヘッダーの追加を無効にすることもできます。

SockJS では、次のヘッダーと値が必要です。

  • Access-Control-Allow-OriginOrigin リクエストヘッダーの値から初期化されます。

  • Access-Control-Allow-Credentials: 常に true に設定します。

  • Access-Control-Request-Headers: 同等のリクエストヘッダーの値から初期化されます。

  • Access-Control-Allow-Methods: トランスポートがサポートする HTTP メソッド(TransportType 列挙型を参照)。

  • Access-Control-Max-Age: 31536000 (1 年に設定)。

正確な実装については、AbstractSockJsService の addCorsHeaders およびソースコードの TransportType 列挙を参照してください。

または、CORS 設定で許可されている場合、SockJS エンドポイントプレフィックスを持つ URL を除外し、Spring の SockJsService で処理できるようにすることを検討してください。

SockJsClient

Spring は、ブラウザーを使用せずにリモート SockJS エンドポイントに接続するための SockJS Java クライアントを提供します。これは、パブリックネットワークを介した 2 つのサーバー間の双方向通信が必要な場合(つまり、ネットワークプロキシが WebSocket プロトコルの使用を排除できる場合)に特に役立ちます。SockJS Java クライアントは、テスト目的(たとえば、多数の同時ユーザーをシミュレートする)にも非常に役立ちます。

SockJS Java クライアントは、websocketxhr-streamingxhr-polling トランスポートをサポートします。残りのものは、ブラウザーでの使用にのみ意味があります。

WebSocketTransport を以下で構成できます。

  • JSR-356 ランタイムの StandardWebSocketClient

  • Jetty 9 + ネイティブ WebSocket API を使用した JettyWebSocketClient

  • Spring の WebSocketClient の実装。

定義上、XhrTransport は xhr-streaming と xhr-polling の両方をサポートします。これは、クライアントの観点からは、サーバーへの接続に使用される URL 以外に違いはないためです。現在、2 つの実装があります。

  • RestTemplateXhrTransport は、HTTP リクエストに Spring の RestTemplate を使用します。

  • JettyXhrTransport は、HTTP リクエストに Jetty の HttpClient を使用します。

次の例は、SockJS クライアントを作成し、SockJS エンドポイントに接続する方法を示しています。

List<Transport> transports = new ArrayList<>(2);
transports.add(new WebSocketTransport(new StandardWebSocketClient()));
transports.add(new RestTemplateXhrTransport());

SockJsClient sockJsClient = new SockJsClient(transports);
sockJsClient.doHandshake(new MyWebSocketHandler(), "ws://example.com:8080/sockjs");
SockJS は、メッセージに JSON 形式の配列を使用します。デフォルトでは、Jackson 2 が使用され、クラスパス上にある必要があります。または、SockJsMessageCodec のカスタム実装を構成して、SockJsClient で構成することもできます。

SockJsClient を使用して多数の同時ユーザーをシミュレートするには、基礎となる HTTP クライアント(XHR トランスポート用)を構成して、十分な数の接続とスレッドを許可する必要があります。次の例は、Jetty でこれを行う方法を示しています。

HttpClient jettyHttpClient = new HttpClient();
jettyHttpClient.setMaxConnectionsPerDestination(1000);
jettyHttpClient.setExecutor(new QueuedThreadPool(1000));

次の例は、カスタマイズを検討する必要があるサーバー側の SockJS 関連のプロパティ(詳細については javadoc を参照)を示しています。

@Configuration
public class WebSocketConfig extends WebSocketMessageBrokerConfigurationSupport {

	@Override
	public void registerStompEndpoints(StompEndpointRegistry registry) {
		registry.addEndpoint("/sockjs").withSockJS()
			.setStreamBytesLimit(512 * 1024) (1)
			.setHttpMessageCacheSize(1000) (2)
			.setDisconnectDelay(30 * 1000); (3)
	}

	// ...
}
1streamBytesLimit プロパティを 512KB に設定します(デフォルトは 128KB — 128 * 1024 です)。
2httpMessageCacheSize プロパティを 1,000 に設定します(デフォルトは 100 です)。
3disconnectDelay プロパティを 30 プロパティ秒に設定します(デフォルトは 5 秒 — 5 * 1000 です)。