IP Messenger で自分が二人表示される問題が解決(更新あり)
- (2009/01/12 追記)
- 問題を取り違えており、修正できていませんでした。原因と修正方法は別にありました。間違っているところを訂正します。
少し前に書いていた問題が修正できたので報告。報告していた内容と少し理由が違い、Mac OS X に限らない通常のソケットプログラミングの問題でした。報告していたとおり、起動時の IP アドレスを記憶したままでユーザ管理をしていたことが問題でした。
「自分自身が二人表示されてしまう」という現象が発生するのは、無線 LAN で接続された状態で起動した状態から、有線 LAN を接続した場合です。
無線 → 有線の順であれば、有線接続時に無線 LAN 側が切断されていたもいなくても、同じように現象は発生するようです。
現象発生後、有線 LAN を切断すれば一人に戻ります。
また、状況とタイミングによっては、他の PC / Mac の IP Messenger にも二人に見える可能性があります。
原因。
IP Messenger は、通信に UDP/IP のパケットを利用します。
UDP パケットを受信するためには、ソケットがサーバとして受信待ちをして待機しておく必要があり、プログラミング上は bind() システムコールで、ソケットに IP アドレスとポート番号を設定します。このへんの作法は、C 言語であれば、UNIX でも Linux でも Mac OS X でも Windows でも同じ API を使用します。
これまでの実装では、サーバソケットに bind する IP アドレスを INADDR_ANY ( 0.0.0.0 ) という特別な値(マシンのすべての IP アドレスの該当ポート番号宛パケットを受信する)に設定していました。メッセージは送信、受信ともこのソケットを利用します。送信時は、その時点で一番優先されるネットワークの IP アドレスで送信され、受信時は全てのネットワーク(今回の場合なら有線 LAN と無線 LAN 両方)のパケットを受信します。
- int sockUDP;
- struct sockaddr_in addr;
- short portNo = 2425;
- sockUDP = socket(AF_INET, SOCK_DGRAM, 0);
- memset(&addr, 0, sizeof(addr));
- addr.sin_family = AF_INET;
- addr.sin_addr.s_addr = htonl(INADDR_ANY);
- addr.sin_port = htons(portNo);
- bind(sockUDP, (struct sockaddr*)&addr, sizeof(addr));
IP Messenger のユーザ自動認識の仕組みは、以下の方法によって実現されています。
- 起動直後またはユーザリストを更新する IP Messenger クライアントは、ブロードキャストアドレスへの UDP パケットの送信により、LAN のサブネット内のすべてのアドレスに ENTRY パケットを送信する。
- ENTRY パケットを(サーバソケットで)受信した他の IP Messenger クライアントは、受信者のユーザ情報を添えて ENTRY 応答を返す。
- ENTRY 応答を受けた ENTRY 送信元は、応答を受信するごとにユーザリストにユーザを追加する。
最初の ENTRY はすべてのアドレスに送信され、同じポート番号で起動している IP Messenger クライアントにのみ届き、それぞれに応答を返してきます。それらをすべて集めると、ユーザリストができるというわけです。
起動済みで応答を返してくれた他の IP Messenger クライアントは、既に同じ事を実施してユーザリストを持っていますので、受信した ENTRY パケットの情報をもとにユーザを 1 名追加すれば、新たな参加者と同じ最新のリストになります。
今回の問題は、サーバソケットから無線 LAN と有線 LAN の両方の ENTRY パケットを受信してしまい、それぞれに ENTRY 応答を返すことにより発生します。異なるアドレスから受信した ENTRY 応答を別々のユーザとして扱うのは、IP Messenger のプロトコル上は正しい挙動です。つまり、応答を受けた側が二人表示するのは正しく、1 台の Mac なのに異なる IP アドレスで複数の応答を返してしまう部分がイクナイ
起動時やリスト更新時に、ユーザリストに自分が現れないケースを回避するため、起動時に作成した自分自身のユーザ情報をリストに強制的に追加してしまっていたことにより発生していました。
INADDR_ANY に bind されたソケットを利用していますので、有線無線の両方のネットワークでパケットを受信しても、返す応答はその時点で優先(有線ではなく…)されるネットワークの IP を使って送信されます。つまり、同じ IP アドレスで 2 通返ってしまうだけのことです。同じ IP アドレスで同じユーザであれば、ユーザリストに追加されるのは 1 人です。
自分が現れないケースの原因はわからないまま遙か昔に暫定対処したものでしたが、その強制追加ユーザと、ネットワーク環境が変わって異なる IP アドレスから送られたユーザが別のユーザとしてリストに表示されていました。
修正は、有線 LAN と無線 LAN が両方接続されて二つの IP アドレスを持っていても、どちらかの IP アドレス宛のパケットにだけ応答を返すようにするべきです。
ソケットプログラミングといえばなんといってもこれ、白本。基本です。IP Messenger 作り始める時にも結構お世話になりました(私のは第1版の古いやつですけど…)。
[tmkm-amazon]4894712059[/tmkm-amazon]
修正方法。
。 今回の修正内容は、v0.9.2 リリースに含まれる予定です。 SystemConfiguration.Framework を利用することにより、有線 LAN と 無線 LAN の切替や、ネットワークの消失と復帰なども把握できるようになり、より IP Messenger を(ネットワーク接続環境が変わっても)シームレスに利用出来るようになりますが、その修正には内部構造の少々まとまった変更が必要になり、現在作業中です。 それらの修正と合わせて、近いうちに v0.9.2 をリリースしたいと思っています。 http://bts.ishwt.net/roadmap_page.phpサーバソケットの bind アドレスを INADDR_ANY から自分の IP アドレスにすれば、この問題は解決します。
起動時やユーザリスト更新時に、起動時点の情報で用意した自分自身を追加することをやめれば、この問題は解決します。ソケットは INADDR_ANY に bind したままで問題ありませんでした
しかし、課題がひとつ。従来の実装では、bind 済みのソケットから接続されている IP アドレスを得ていたので、bind 前には IP アドレスがわからない、というニワトリ卵問題がありました。IP アドレスを得てからソケットを作りなおして bind しなおす等の方法がないわけではありませんが、もっと別の(スマートな)方法できちんと解決したいところです。そこで、それとは別に、IP アドレスの取得方法を変えました。いままでは ioctrl という UNIX システムコールにより IP アドレスを得ていましたが、SystemConfiguration.Framework という Mac OS X 独自の方法を利用して取得する方式に変更しました。
SystemConfiguration.Framework を用いることで bind 前に IP アドレスを得ることができるようになり、起動時点で最も優先されている IP アドレス(システム環境設定のネットワークの設定で一番上にあるネットワークの IP アドレス)にのみ bind するようになりました。
今回利用している SystemConfiguration.Framework の仕組みは、Mac OS X 10.3 から導入されたものが含まれており、以前の開発時点では存在しなかった仕組みです(と軽くイイワケw)。SystemConfiguration.Framework の使い方については、また別のエントリで紹介したいと思っています。v0.9.2。
リリースに向けた状況はこちら。