« Arduinoメールチェッカー(その2) | トップページ | ArduinoでNTPを使用する »

Arduinoイーサーネットシールドの接続処理

Arduinoのイーサネットシールド(Ethernet Shield)にて、リセット後1回目のclient.connect()がtimeoutする問題があると書いたのですが、調べてみると、タイムアウトの発生条件は、「コネクションを切断した直後にリセットを行って、同一宛先に再接続した場合でした」。以下、分かったことを示します。

2009/6/7追記:hamayanさんからいただいたコメントに基づき確認方法を見直しました。

調査用のスケッチ

以下に示す調査用のスケッチを作成して、PC上のWeb Serverにアクセスしてみます。EthernetライブラリをIDE-0016に変更しました。

#include <Ethernet.h>

byte mac[] = { xx, xx, xx, xx, xx, xx };
byte ip[] = { 192, 168, 0, 110 };
byte server[] = { 192, 168, 0, 10 };

Client client(server, 80);

void setup()
{
  Ethernet.begin(mac, ip);
  Serial.begin(115200);
 
  delay(1000);
}

void loop()
{
  getpage();
  for(;;)
   ; //無限ループ
}

void getpage()
{
  Serial.println("connecting...");
  Serial.print("client.status = ");
  Serial.println(client.status(),HEX);
 
  if (client.connect()) {
    Serial.println("connected");
    Serial.print("client.status = ");
    client.write("GET /index.html HTTP/1.0\r\n\r\n");
    Serial.println(client.status(),HEX);
  } else {
    Serial.println("connection failed");
    Serial.print("client.status = ");
    Serial.println(client.status(),HEX);
    Serial.println();
    return;
  }

  while(true)
  {
    if (client.available()) {
      char c = client.read();
//      Serial.print(c);
    }
 
    if (!client.connected()) {
      Serial.println("disconnecting.");
      Serial.print("client.status = ");
      Serial.println(client.status(),HEX);
      client.stop();
      Serial.println("client.stop");
      Serial.print("client.status = ");
      Serial.println(client.status(),HEX);
      Serial.println();
      return;
    }
  }
}

スケッチアップロードの一回目の起動は、以下のように正しく動作します。各statusの意味を、「w5100.h」から抜粋しました。

connecting...
client.status = 0 → SOCK_CLOSED
connected
client.status = 17 → SOCK_ESTABLISHED
disconnecting.
client.status = 1C → SOCK_CLOSE_WAIT
client.stop
client.status = 0

一回目の動作が終了直後にリセットをかけて再接続すると、以下のように接続に失敗(タイムアウト)します。

connecting...
client.status = 0
connection failed
client.status = 0

LEDの点滅を見ていると、サーバーにパケットを投げているように見えます。WireSharkを使用してPC側でパケットキャプチャーを行うと、以下のように、Ethernet ShieldはTCP-SYNを投げてコネクション接続の要求を行っているのですが、PC(サーバー)側が応答していないことが分かりました。

Capture_2

なぜこうなるのかですが;

Ethernet Shieldからのアクセスが終わった直後に、netstatコマンドを使用してネットワークの接続状態を調べると、以下のようにEthernet Shieldとの接続は「TIME_WAIT」状態になっています。

C:>netstat -n
アクティブな接続
  プロトコル  ローカル アドレス          外部アドレス        状態
  TCP    127.0.0.1:27015        127.0.0.1:49178        ESTABLISHED
  TCP    127.0.0.1:49178        127.0.0.1:27015        ESTABLISHED
  TCP    192.168.0.10:80        192.168.0.110:1025     TIME_WAIT

TIME_WAITの意味は以下です(RFC793より):
 対向側のTCPがFIN-ACKを受信するまでの十分な時間を確保するための待ち時間。

Windows側がTIME_WAIT状態中にArduinoをリセットした場合、Ethernet Shieldは前回アクセスと同一のソース・ポートを使ってTCP-SYNを投げるため、Windowsが応答しないようです。TIME_WAIT状態が終了するまで待ってリセットを行うと接続ができます。

冒頭に示したスケッチでは、client.connect()は1回しか実行しませんが、タイムアウトした際に再度client.connect()を行う処理を追加すると、TIME_WAIT中であっても、Ethernet Sheildがソース・ポートを1026に変えてTCP-SYNを投げるため2回目のclient.connect()は成功します。即ち、client.connect()をリトライすると成功することになります。

従って、これまでEthernetライブラリのバグではと思っていた事象は、WindowsのTCPセッション管理との兼ね合いで発生していることになります。

Linuxだとどうなるか

Linuxの場合、TIME_WAIT期間中にリセットによる再接続を行ってもタイムアウトせずに接続できました。リセット後のTCP-SYNで一度TCP-RSTが入るのですが、ポート番号は1025のままでもLinuxは新規のコネクションを受け入れるようです。

シーケンス図でまとめると

パケットキャプチャーで分かった動作シーケンスをWindows/Linuxそれぞれまとめると、以下のようになります。Windows/Linux共、Web ServerをApache2.2にして動作条件を統一しました。

Tcpsequence_20090607

Windows/Linuxそれぞれの動作概要を以下に示します(数字は、図のまる付き数字に対応します)。

■Windows (Vista)

  1. Src Port 1025で一回目の接続
  2. 切断。Server側からTCP-FINを送信している
  3. Client (Ethernet Shield)もTCP-FINを送信する
  4. Windowsの接続状態がTIME_WAITになる
    → TCPの教科書だと、FINは双方向で送信すること、TIME_WAIT状態は先にFINを送信したActive close側の状態遷移であるため、不思議な状態遷移をしているように見えます
  5. Windows側でTIME_WAIT状態になっているSrc Port 1025を使って再接続
  6. Windowsが応答せずタイムアウト
  7. Src Port番号を変えて再接続すると成功する

■Linux

  1. Src Port 1025で一回目の接続
  2. 切断。Server側からTCP-FINを送信している
  3. Client (Ethernet Shield)もTCP-FINを送信する
  4. Linuxの接続状態がTIME_WAITになる
    → 双方向でTCP-FINを送信しており、一般的な終了シーケンスになっている
  5. Linux側でTIME_WAIT状態になっているSrc Port 1025を使って再接続
  6. LinuxがSYN-ACKを返さない(ACKフラグのみを返す)
  7. TCP-RST後の再接続でコネクションが確立する
    → clinet.connect()のタイムアウトは発生しない

WindowsとLinuxで使用したWebサーバーが異なりますが、TCP-FINシーケンスの差分はカーネルの実装によるものなのか、もしくはサーバーアプリのSocket操作によるものなのかは分かりませんでした。
当初、ServerアプリがAbyssの際は、Server(Windows)がTCP-SYNを送信しないように見えたのですが、ApacheではTCP-FINの送信がキャプチャーできるようになりました。一回目の接続・解放シーケンスは、Windows/Linux共同様になりました。

メールチェッカーでも同様に、一回目のチェック終了直後にリセットを行うとタイムアウトが発生します。当方が使用するメールサーバーも (niftyですが)Windows的な動作になっているのでしょうか。BBルーターのNATで同一ポートの再接続が蹴られるということはないと思うのですが。

ということで、client.connect()のタイムアウトに関しては、リトライで救うしかないということが分かりました。

« Arduinoメールチェッカー(その2) | トップページ | ArduinoでNTPを使用する »

Arduino」カテゴリの記事

コメント

Windows相手に組み込み機器のネットワークをデバックしていると、この現象は発生しますね。

TCPの状態遷移でいけばTIME_WAITに入るのはCLOSING経由とFIN_WAIT経由の2つあります。
キャプチャしたWireSharkのステータスからは判らないのですが、多分Windows側もFINを投げているのだと思います。HTTP1.0ならKEEP ALIVEせずにセッションを一旦閉じに入りますから。

TIME_WAITに入ればこちらから投げたACK応答を相手が受け取れずに居る可能性を考慮して2MSL時間待つのが普通ですので、Winodws側の動作はおかしくないと思います。
むしろlinuxの方が。

hamayanさん、
コメントありがとうございました。
TCPセッション終了の詳細動作はよく分かっていませんでした。何分、私の知識はLayer-3 に(Routing)偏っておりまして(^-^;;
記事の修正版にも記載しましたが、ApacheだとPC(Windows)からのFINがキャプチャーできました。ご指摘の通り、FIN-WAIT経由で、本来の状態遷移を経由してTIME_WAITになっているということですね。
どちらかというと、Linuxカーネルの挙動(TIME_WAIT中のポートを使用したTCP-SYNに対して、SYNフラグを立てずに応答する)が変なのですね。
大変勉強になりました。

Web server に TIME_WAIT が溜まる問題を調べていて参考にさせて頂きました。
結果 arduino-1.0.1\libraries\Ethernet\EthernetClient.cpp にフローミスがあることが分かりましたので、こちらにも報告しておきます。
EthernetClient.cpp 中 EthernetClient::stop() メソッド実装で 無条件に disconnect(_sock) が呼ばれているのが原因です。
サーバーが CLOSE_WAIT 状態の時にconnected() が 受信データ0の場合 falseになるため クライアントが終了処理として stop() をすぐ呼ぶとサーバーのCLOSE_WAIT中にdisconnect() のパケットを受けてしまい TIME_WAIT 遷移してしまう・・・のがもんd内になっています。 disconnect でなくclose のみ呼んでいれば正常にクライアントとして切断フローが行われ TIME_WAIT がたまらないのは確認できました。
//disconnect(_sock);
if( status() != SnSR::CLOSE_WAIT ) {
disconnect(_sock);
}
私はライブラリを直接、こんな感じで対処しました。

TATUOさん、
コメントありがとうございました。
なるほど、client (Arduino)側のstop処理に問題があったのですか。

この記事へのコメントは終了しました。

« Arduinoメールチェッカー(その2) | トップページ | ArduinoでNTPを使用する »

2018年10月
  1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30 31      
無料ブログはココログ