DUICUO

NettyとWebSocketをベースとしたJD.comの宅配サービス

JD.com Merchant Center システムでは、販売業者から、Web プラットフォーム上で自動印刷を実装することが要望されており、手動で監視してクリックして印刷する必要がなく、レシートを直接印刷することで人件費を節約できます。

解決

この問題に関する 2 つの推論:

  • AJAX を使用してサーバーをポーリングし、最新の注文を取得できます。これはプルと呼ばれます。
  • これはプッシュのような設計を使用して実現できます。

私たちは 2 つのアプローチの利点と欠点を評価しました。

  • AJAX方式は実装が簡単で、サーバーから定期的にデータを取得するだけで済みます。しかし、無効なポーリング操作の回数も増加し、サーバー側での無効なクエリの回数も意図せず増加してしまいます。
  • プッシュ方式は実装がやや複雑で、サーバーとPC間の接続を維持する必要があります。そのため、持続的な接続を確立する必要があり、最終的にプッシュ効果が得られます。

議論の結果、私たちは 2 番目のオプションを選択しました。注文センターによって生成された新しい注文は、メッセージ キュー (MQ) を介して Web クライアントにプッシュされ、最終的にユーザー エクスペリエンスが向上します。

ソリューションの紹介

長時間接続ソリューションの選択については、多くの投稿を参考にした結果、最終的にWebSocketプロトコルを使用して長時間接続を実装することにしました。IMやサーバーサイドプッシュ通知などの同様のシナリオでも、このプロトコルが使用されています。

次に、主流の WebSocket フレームワークである Netty、Tomcat、SocketIO を比較します。

  • Tomcat などの WebSocket をサポートするコンテナーをベースとした開発は簡単ですが、高い同時実行性のサポートはあまり良くなく、接続が切断されやすく、コンテナーへの依存性もあります。
  • netty-socketIOはnetty4のラッパーで、nettyと同様の効率性を提供します。ユーザーフレンドリーなAPIを備えたクロスプラットフォームソリューションです。JD.comのログブックもログ転送にsocketIOを使用しているため、検討中の選択肢となっています。
  • Nettyは業界で主流のNIOフレームワークです。NettyはJava NIOをカプセル化することで、開発者がビジネスロジックに集中し、開発コストを削減できるようにします。

多くの有名なRPCフレームワークは、トランスポート層としてNettyを使用しています。NettyはユーザーフレンドリーなAPI、強力な機能、そして多くのコーデックプロトコルを内蔵しており、WebSocketプロトコルの実装に非常に便利です。

これらのフレームワークを並べて比較してみましょう。

そのため、選択の面では、依然としてsocketIOとnettyに焦点を当てました。スケーラビリティと柔軟性を考慮しつつ、nettyはHTTP機能を提供できることも考慮しました。

最終的に、Nettyを使用することにしました。もちろん、SocketIOは多くの機能をカプセル化しており、非常に強力です。それに比べて、Nettyはより軽量であるため、私たちにとってより適しています。

Nettyの特徴

Nettyは非同期性と非ブロッキング性を特徴としています。従来のI/Oはストリーム指向ですが、NIOはバッファ指向であるため、非ブロッキングです。

Netty のスレッド モデルを図に示します。

このモデルは、一般的にReactorモデルと呼ばれています。ボススレッドは、クライアントからのリクエストを受信するために使用される独立したNIOスレッドプールであり、デフォルトのスレッドプールサイズは1です。ワーカースレッドプールは、特定の読み取りおよび書き込み操作を処理するために使用され、デフォルトのスレッドプールサイズはCPU数の2倍です。

上記のモデルでは、ExecutionHandlerに特に注意する必要があります。ExecutionHandlerはワーカースレッドで実行されるため、IOや計算といった時間のかかる操作はスレッドプールで実行するのが最適です。そうしないと、Netty全体のスループットに影響を及ぼします。

これらの点を理解した上で、次の図に示すように、独自のビジネスニーズに基づいたプロセスを設計しました。

  • ステップ 1: Web クライアントはサーバーに登録を要求し、登録が成功すると永続的な接続を維持します。
  • ステップ 2: サーバーはメッセージを MQ に送信します。
  • ステップ 3: Netty は受信したメッセージを Web クライアントにプッシュします。
  • ステップ4:Webクライアントを使用して印刷コントロールを呼び出して印刷します。印刷コントロールは事前にインストールされている必要があります(印刷コントロールはPCにインストールされたドライバーで、JSを使用して呼び出されます)。

JS 呼び出しが成功した場合、コントロールは印刷情報を印刷キューに格納します。失敗した場合は、手順 4 を繰り返します。

もちろん、現在の構造はスタンドアロン版に過ぎず、本番環境の要件を満たしていません。将来の構造は、次の図に示すように進化する可能性があります。

サーバーとNettyの間にルーティング層を構築します。ルーティング層の主な役割は以下のとおりです。

  • クラスターの活性情報を収集する
  • 着陸地点、つまりどのマシンに着陸したかを記録します。
  • メッセージの受信と配信

これら3つの機能により、情報配信戦略を簡単に指定できます。ルーティングにはHTTPプロトコルを使用するため、Nettyは短いHTTP接続を受信できる必要があります。そのため、Netty全体としては、長い接続と短い接続の両方に対応している必要があります。

以下はコードの一部です。

Nettyの起動クラスはSpringを使用してNettyを起動します。Nettyの起動はメインスレッドをブロックするため、バックグラウンドスレッドで起動する必要があります。起動パラメータは以下のとおりです。

次に、ChannelInitializer、コーデック用の HttpServerCodec、WebSocket プロトコル ハンドシェイク用の WSServerProtocolHandler を記述しましょう。

私たちは、ビジネス レベルの 2 つのカスタム ハンドラー (httpRequestHandler と authorizeHandler) に重点を置いています。

httpRequestHandler の目的は、URL が有効かどうかを確認し、パラメータを受け取ることです。

httpRequestHandler メソッドは、URL に基づいてリクエストをフィルタリングし、カスタムの短い接続リクエストを作成することもできます。

authorizeHandler の目的は、データが正しいかどうかを検証することです。正しい場合、チャネルをマップに保存し、マップを通じてビジネスIDとチャネルの関係を確立します。

検証プロセスはauthorizeHandler内のchannelReadに実装されています。検証に失敗した場合、現在のチャネルは閉じられます。

検証に合格すると、情報は ctx.fireChannelRead(msg) メソッドを介して次のハンドラーに渡され、処理されます。

このプロジェクトでは、データの検証は主にパラメータを渡すことによって実行され、これは URL パラメータの渡しによって実現されます。

httpRequestHandler では、URL パラメータをチャネルの attr に設定し、次のハンドラーである authorizeHandler に渡します。

そのため、authorizeメソッドでは、get()メソッドを使用してパラメータ値を取得できます。uは暗号化されたデータであり、ここで復号する必要があります。復号に失敗した場合、検証は失敗したとみなされます。

もちろん、アプリケーション間サービスがある場合は、Cookieを使用して暗号化された文字列を読み書きすることもできます。Cookie内の情報は、具体的なビジネス要件に応じて、request.getHeaderを介して取得できます。

コード例は次のとおりです。

このマップは、サーブレット内のセッションとして理解できます。クライアントに送信する情報がある場合、map.get(key) を呼び出してそのクライアントの現在のチャネルを取得し、writeAndFlush メソッドを呼び出して情報を送信します。次の例は、MQメッセージを受信した後の処理ロジックを示しています。

では、もしチャネルが閉じられたらどうなるのか?マップ上のチャネルは無効になるのだろうか?と疑問に思う人もいるかもしれません。

実際、チャネルを維持し、間接的にこのマップを維持するために、ハートビートのようなメカニズムも必要です。

チャネルが正常に閉じられている場合は、channelInactive メソッドを使用してチャネルをリッスンできます。

アイドル時間が長時間続く場合は、プロジェクト内で追加のIdleStateHandlerを使用して処理します。userEventTriggeredメソッドをオーバーライドすることで、アイドル状態のチャネルをリッスンします。チャネルが設定したタイムアウトに達すると、Nettyはこのメソッドをコールバックします。

この時点でコア部分は処理済みです。あとは保存したチャネルを通じてクライアントに情報を送信するだけです。

最後に、Web 側では、WebSocket API をカプセル化し、接続が失われたときに自動的に再接続するメカニズムを提供する小さな JavaScript ライブラリである reconnecting-websocket を使用しました。これにより、切断と再接続の操作を完了するのに役立ちます。

遭遇した問題

テストの結果、ws の URI の後にパラメータを渡すことはできないことが判明しました。そうしないと、Netty で WebSocket プロトコル ハンドシェイクを実装するときに接続が失われます。

この問題に対処するため、WebSocketHandlerの前にHTTPHandlerフィルタが追加されました。このフィルタは、渡されたパラメータをチャネルの属性に格納し、リクエストのURIを書き換えて次のパイプラインに渡します。これにより、問題は実質的に解決されます。

読み取り/書き込みアクティビティが少ない時間帯は、ハートビートパケットを送信することで接続を維持する必要があります。しかし、ネットワークの不安定さやサーバーの再起動などにより、クライアント側で接続が失われ、注文メッセージが一時的に受信されない場合があります。そのため、クライアント側に再接続メカニズムを実装する必要があります。

この問題に対処するため、reconnecting-websocket JSフレームワークを使用しました。このフレームワークはネイティブWebSocket実装を拡張し、切断後にすぐに再接続できないという問題を効果的に防ぐ再接続メカニズムを備えています。

テスト中に、コントロールとチケットプリンターに問題が発生すると、印刷エラーが発生したり、チケットプリンターの用紙切れが発生したりすることがあります。Lodopコントロールは、コンピューターの印刷キューに印刷情報を追加できます。

用紙がなくなると、レシートプリンターからアラームが鳴ります。レシート用紙が挿入されると、プリンターはキュー内のデータを自動的に印刷します。

コントロールの呼び出し時に例外が発生することがあります。現在の解決策は、JavaScript で try-catch ブロックを使用することです。

失敗した場合は再試行されます。再試行回数はカスタマイズ可能です。再試行回数を超えた場合は、何も実行されません。この設定はあまり厳密ではなく、さらなる最適化が必要です。

要約

上記の取り組みにより、Web上での自動印刷はほぼ実現しました。長期間にわたる社内テストを経て、サーバーとクライアント間の通信は安定しています。今後は、グレースケール加盟店を対象としたユーザーエクスペリエンステストを実施します。

特定のシナリオでは、適切なテクノロジーを選択すると効率が向上しますが、そうでない場合は逆の効果が生じる可能性があります。

長期にわたる接続を選択するときは、次の 3 つの主な原則を念頭に置いてください。

  • 望ましい制御効果を実現するために、サーバーはクライアントにデータを積極的にプッシュする必要がありますか?
  • リアルタイムパフォーマンスの要件は厳しいですか?
  • クライアントがオンライン ステータスのリアルタイムの変更を監視する必要があるかどうか。