Java + Windows の Socket 通信

Java + WindowsにてSocket通信を行うAPの開発が必要となったので、実験メモ。

要件

  1. サーバとクライアントは同一サーバで、windowsである
  2. プログラムは、Javaで開発する
サーバ側要件
  1. 待ち行列を制御したい
  2. タイムアウト済みのクライアントとの通信は、即時に無効を検知したい。
  3. 待ち行列の制御をAPで行いたくないので、待ち行列バックログだけで管理するためにサーバ側はシングルスレッドで処理したい
クライアント側要件
  1. 一定時間待ち行列上に存在した場合は、クライアントは依頼を送信せずに、処理を諦めたい
  2. クライアントにて異常と判断した場合は、可能な限りサーバ側にて処理が行われないようにしたい
  3. サーバに依頼した処理が仕掛ってしまった場合は、処理の中断はあきらめるものの、サーバ側に諦めたことを伝えたい

普通はほとんどありえない要件ですが、今回ばかりは特別な事情ということで…

プロトコル(1)

  1. 接続: クライアント#connect -> サーバ#accept
  2. 要求: クライアント#send -> サーバ#recv
  3. 結果: クライアント#recv <- サーバ#send
  4. 切断: クライアント#close , サーバ#close

なお、recvのタイムアウトを5秒とする。以降、xx秒経過を(xx)と表現。

検討/実験(1−A)

backlogに滞留しているケース。

<クライアント側の状況>

  1. 接続(00): クライアント#connect -> OK
  2. 要求(00): クライアント#send -> OK
  3. 結果(05): クライアント#recv -> Timeoutエラー
  4. 切断(05): クライアント#close -> OK

これでは、送信した依頼はまだ実行されずにタイムアウトしたのか、それともサーバ側の処理時間が長くてタイムアウトしたのかが分からない。
というのも、syn ⇒ syn/ack ⇒ ackの流れは、ServerSocket#bind(listen)すると、OS側で自動で行われるため、クライアント側のconnectタイムアウトでは、サーバ側のaccept前でのタイムアウトか、accept後でのタイムアウトかを判定できない。
しかも、send/flushは成功している以上、サーバ側に依頼を受け取って貰えたように見える。
だが、処理結果は受信できなかったため、依頼は失敗したとして、クライアント側の処理を継続する必要がある。

<サーバ側の状況>

  1. 接続(10): サーバ#accept -> OK
  2. 要求(10): サーバ#recv -> OK
  3. 結果(10): サーバ#send -> OK
  4. 切断(10): サーバ#close -> OK

クライアントからのacceptに成功し、依頼を受信したので、処理を実行し、結果を応答できた。すばらしい。

<結果>
クライアントは処理を失敗として扱い、さらに、サーバ側で処理してしまったかどうかが判断できない。サーバ側は処理を成功として扱った。結果、クライアントとサーバ間では、状態の不整合が発生する。これでは、よろしくない。

プロトコル(2)

  1. 接続: クライアント#connect -> サーバ#accept
  2. 確認: クライアント#recv <- サーバ#send
  3. 要求: クライアント#send -> サーバ#recv
  4. 結果: クライアント#recv <- サーバ#send
  5. 切断: クライアント#close , サーバ#close
検討/実験(2−A)

backlogに滞留しているケース。

<クライアント側の状況>

  1. 接続(00): クライアント#connect -> OK
  2. 確認(05): クライアント#recv -> Timeoutエラー
  3. 要求(--): クライアント#send -> skip
  4. 結果(--): クライアント#recv -> skip
  5. 切断(05): クライアント#close -> OK

とりあえず、要求を送信していないので、サーバ側で処理していないということは確実に判定できる。すばらしい。

<サーバ側の状況>

  1. 接続(10): サーバ#accept -> OK
  2. 確認(10): サーバ#send -> OK
  3. 要求(10): サーバ#recv -> 即時エラー
  4. 結果(--): サーバ#send -> skip
  5. 切断(10): サーバ#close -> OK

要求を受信できなかったので、とりあえず、クライアント側は諦めたと考えてよいだろう。すばらしい。

<結果>
クライアント側も、サーバ側も、お互いに処理を実行していないと判断できるため、状態の不整合は発生しない。すばらしい。

検討/実験(2−B)

処理遅延が発生したケース。

<クライアント側の状況>

  1. 接続(00): クライアント#connect -> OK
  2. 確認(00): クライアント#recv -> OK
  3. 要求(00): クライアント#send -> OK
  4. 結果(05): クライアント#recv -> Timeoutエラー
  5. 切断(05): クライアント#close -> OK

要求は送信できたが、結果受信をタイムアウトしてしまった。

<サーバ側の状況>

  1. 接続(00): サーバ#accept -> OK
  2. 確認(00): サーバ#send -> OK
  3. 要求(00): サーバ#recv -> OK
  4. 結果(10): サーバ#send -> OK
  5. 切断(10): サーバ#close -> OK

要求を受信できなかったし、結果を返すこともできたので、きっと、クライアントは正常に処理できたのだろう。すばらしい。

<結果>
クライアントは処理を失敗として扱い、サーバ側は処理を成功として暑かった。結果、クライアントとサーバ間では、状態の不整合が発生する。これでは、よろしくない。

プロトコル(3)

  1. 設定: クライアント#Linger(true, 0s)
  2. 接続: クライアント#connect -> サーバ#accept
  3. 確認: クライアント#recv <- サーバ#send
  4. 要求: クライアント#send -> サーバ#recv
  5. 結果: クライアント#recv <- サーバ#send
  6. 切断: クライアント#close , サーバ#close
検討/実験(3−A)

backlogに滞留しているケース。

<クライアント側の状況>

  1. 設定(00): クライアント#Linger(true, 0) -> OK
  2. 接続(00): クライアント#connect -> OK
  3. 確認(05): クライアント#recv -> Timeoutエラー
  4. 要求(--): クライアント#send -> skip
  5. 結果(--): クライアント#recv -> skip
  6. 切断(05): クライアント#close -> OK

とりあえず、要求を送信していないので、サーバ側で処理していないということは確実に判定できる。すばらしい。

<サーバ側の状況>

  1. 接続(10): サーバ#accept -> OK
  2. 確認(10): サーバ#send -> 即時エラー
  3. 要求(10): サーバ#recv -> skip
  4. 結果(--): サーバ#send -> skip
  5. 切断(10): サーバ#close -> OK

確認の送信に失敗したので、とりあえず、クライアント側は諦めたと考えてよいだろう。すばらしい。

<結果>
クライアント側も、サーバ側も、お互いに処理を実行していないと判断できるため、状態の不整合は発生しない。すばらしい。

検討/実験(3−B)

処理遅延が発生したケース。

<クライアント側の状況>

  1. 設定(00): クライアント#Linger(true, 0) -> OK
  2. 接続(00): クライアント#connect -> OK
  3. 確認(00): クライアント#recv -> OK
  4. 要求(00): クライアント#send -> OK
  5. 結果(05): クライアント#recv -> Timeoutエラー
  6. 切断(05): クライアント#close -> OK

要求は送信できたが、結果受信をタイムアウトしてしまった。

<サーバ側の状況>

  1. 接続(00): サーバ#accept -> OK
  2. 確認(00): サーバ#send -> OK
  3. 要求(00): サーバ#recv -> OK
  4. 結果(10): サーバ#send -> 即時エラー
  5. 切断(10): サーバ#close -> OK

結果の送信に失敗したので、とりあえず、クライアント側は諦めたと考えてよいだろう。だが、処理は行ってしまった。

<結果>
クライアント側はサーバ側にて処理が行われたかどうかは分からないものの、サーバ側もクライアントに結果を通知できなかったため、お互いが依頼をなかったことに(ロールバック)できれば、状態に相違は発生しない。まあ、良いかな。

まとめ

今回の条件である、同一サーバ内でのソケットを利用したプロセス間通信であり、サーバがwindowsであり、APがJavaで書かれているようなケースにおいては、リスクを最小限にするために、SoLinger(true,0)をしつつ、サーバからの接続確認の送信から始めるようにすれば、そこそこのプロトコルができそうである。
ただ、絶妙なタイミングでの障害の場合の検証は行えていないのと、検証そのものが難しいので、とりあえず、今回はこの設計で頑張ってみましょう。