ラベル Raider の投稿を表示しています。 すべての投稿を表示
ラベル Raider の投稿を表示しています。 すべての投稿を表示

2014年3月12日水曜日

P2P探訪 WebRTC用、SDP交換サーバーを作ってみた。〜WebSocketで転送する〜

続きです。
WebSocketを用いて、SDPデータを転送する機能を実現しました。


[コネクションを維持して任意のタイミングで通信]

通常、ブラウザーでサイトにアクセスする場合、ブラウザーは必要なデータをダウンロードすると、コネクションを切ります。

コネクションが切れてしまうと、サーバーからクライアントへメッセージを送信する事ができません。
なので、以前は、ホームページの表示完了後、少し時間がたってからサーバーから何かしらのPush通信を受けると行った事が出来ませんでした。

しかし、WebSocketが誕生しこの状況はいっぺんします。
WebSocketを使った場合、サーバーとクライアントのコネクションはクローズされず、維持し続けます。これによって、サーバーから、クライアントへメッセージを送信する事ができるようになりました。

WebSocketを使えば、任意のタイミングで、サーバーとブラウザーが通信できます。
この機能を利用すれば、容易にSDP交換サーバーの実現することができるでしょう。

[SDP交換サーバーの役目]


SDP交換サーバーの役目は、(a)Peerの存在を他のPeerへ伝える事。そして、(b)SDP情報を、Peer同士で交換できるようにする事です。

今回作成した、SDP交換サーバーでは、以下の機能を実装しました。
[1]  Peer全体にメッセージを送信する。
[2] 指定したUUIDのPeerへメッセージを送信する。


あたらに加わったPeerが、「[1]の機能」を用いて、p2pネットワーク全体にUUIDを送信すれば、「(a)の目的」を果たす事ができます。
また、P2P接続を確立したい場合、「[2]の機能」を用いて、他のPeerへ自身のSDPを送る事ができるでしょう。


[実装]

nodejsを使って実現しました。
https://github.com/kyorohiro/HelloWebRTC/blob/master/signalServer/signalserver.njs


    // httpサーバーを立ち上げる
    this.mHttpServer = HTTP.createServer(function (req, res) {
          ...
    }
    // websocketサーバーを立ち上げる
    this.mWsserverSocket = new WSServer({httpServer: this.mHttpServer});
    this.mWsserverSocket.on('request', function(req){
          ....
          ....
       // 受け取ったメッセージを解析して、転送する。  
       if(messageType === "unicast") {
       var v = {}
            v["_contentType"]    = contentType;
            v["_content"] = content;
            v["_to"]      = to;
            v["_from"]    = from;
           //[1] Peer全体にメッセージを送信する。
            _own.uniMessage(to, JSON.stringify(v));
       } else if(messageType =="broadcast") {
             var v = {}
             v["_contentType"] = contentType;
             v["_content"]     = content;
             v["_from"]        = from;
             //[2] 指定したUUIDのPeerへメッセージを送信する。
             _own.broadcastMessage(JSON.stringify(v));
       } 
    });


[次回]

続きを書きます。クライアント側の解説をもう少しします。それが終わったら、各Peerにもこの機能を持せたサンプルを提供します。
余裕があれば、それを土台に主だったDHTを、WebRTC上で実現できるか検証してみます。




2014年3月11日火曜日

P2P探訪 WebRTC用、SDP交換サーバーを作ってみた。〜 UUIDでPeerを識別する 〜

WebRTCを用いて、P2P通信をする場合には、SDPというデータを交換する必要がある事を説明しました。
ただし、このSDPを交換する部分はWebRTCでは提供されていません。自作する必要があるでしょう。※有り物を拝借するでも良いです。

試しに、WebSocketを使ってSignalServer(SDP交換機)を作成してみました。紹介します。


[UUIDでPeerを識別する]

多数のPeerの交換器としてサーバーを動作させたい場合、各Peerを識別する方法が必要です。
特にP2Pシステムでは、統制を取らずに一意の識別子の作成をする事が望ましいです。
※今は交換サーバーが一つを想定しているので、本来考慮する事ないかもしれません。

今回は乱数を使用して実現しました。128bitの値を乱数で生成します。0〜2**128の値衝突する確率は、1/2**64ととても少ないのです。
※ アレと思った方は、「誕生日攻撃」ググると良い

// ref http://note19.com/2007/05/27/javascript-guid-generator/
function s4() {
 return (((1+Math.random())*0x10000)|0).toString(16).substring(1);
}
function createUUID() {
 return s4()+s4()+"-"+s4()+"-"+s4()+"-"+s4()+"-"+s4()+s4()+s4();
}

※rfc4122では、最初の6bitは予約されています。


[次回]

続きを書く..。その続きは、Peerに交換機の機能を付けたサンプルを紹介する予定。


※ 成果物は以下
https://github.com/kyorohiro/HelloWebRTC/tree/master/signalServer






2014年3月1日土曜日

P2P探訪 StunでNat越え その7 WebRTCでShakehand

前回、Stunの実例として、WebRTCを利用してSDP(自身のアドレスとポート)を取得しました。せっかくなので、WebRTCを使って、Peer同士でメッセージのやり取してみましょう。


WebRTCを用いて、お互いのPeerが接続してメッセージを送る方法は簡単です。
1. 自分のSDPと相手のSDPを取得する。
2. 取得したSDPを設定する。
3. メッセージを送信する。※今回はテキスト

とするだけです。

[解説]

以前説明した通り、Peerどうしが接続するためには、接続相手のアドレスを知る必要があります。このアドレスを知りたい場合には、Stunサーバーを利用するのでした。

また、接続される側も、接続してくるPeerのアドレスがわかっていれば、「UDPパンチ」などを用いて、接続できる可能性をあげる事ができるのは、ご存知のことでしょう。以前解説したとおりです。

WebRTCも同様の手法を取っています。「接続される側」、「接続する側」のアドレスを前もって、WebRTCの知らせる事で、Peer同士が接続できるようになります。

※注意点
WebRTCでは、「接続される側」、「接続する側」のアドレスを、Peerに知らせる方法は提供されていません。
独自に実装する必要があります。WebSocketを利用する方法流行っているみたいです!!



[接続の処理の流れ]

実際の処理の流れを見てみましょう。接続が完了すれば、メッセージを送信できるようになります。

○自分のSDPを取得/設定
#接続を要求する側
O-1. RTCPeerConnectionを生成する。
O-2. RTCPeerConnection#createOffer()をコールする。
O-3. RTCPeerConnection#setLocalDescription()をコールして設定する

#接続を受け入れる側
A-1. RTCPeerConnectionを生成する
A-2. RTCPeerConnection#createAnswer()をコールする
A-3. RTCPeerConnection#setLocalDescription()をコールして設定する
※A-2の操作は先に接続要求してきている、SDPを設定しておく必要があります

○相手のSDPを設定する

#接続を要求する側/#接続を受け入れる側
A/O-1. RTCPeerConnection#setRemoteDescription()をコールする。

[サンプルコード]

○サンプルコード

WebRTCを利用して、テキストを送る事ができます。
※通常は、SDPデータを交換するのに、WebSocketを使います。しかし、今回のサンプルでは、WebSocket用のサーバーを用意する必要はありません。その部分は手動で行うように作りました。


[次回]

....次回は、WebRTCを使って、何かします。たぶん、Torrentの考え方を応用したサンプルを紹介します。




2014年2月25日火曜日

P2P探訪 StunでNat越え その6 WebRTCでSDPを取得

P2Pの起爆剤になりそうなものとして、もっとも有力なのは、WebRTCではないでしょうか。
WebRTCを用いれば、ブラウザーで、P2Pアプリの機能を提供することができます。

もちろん、WebRTCではSTUNサーバーが利用されています。
さっそく、STUNを介して、自身のアドレスを取得してみましょう。


1. RTCPeerConnection を生成する。
2. RTCPeerConnection#createOffer()をコールする

でOKです。

以下のような値を取得できます。SDPと呼ばれています。

...
...
m=audio 55491 RTP/SAVPF 111 103 104 0 8 106 105 13 126
c=IN IP4 111.100.57.xxx
a=rtcp:55491 IN IP4 111.100.57.214
a=candidate:38022971xx 1 udp 21139371xx 192.168.0.3 55491 typ host generation 0
a=candidate:38022971xx 2 udp 21139371xx 192.168.0.3 55491 typ host generation 0
a=candidate:12749365xx 1 udp 18455016xx 111.100.57.2xx 55491 typ srflx raddr 192.168.0.3 rport 55491 generation 0
a=candidate:12749365xx 2 udp 18455016xx 111.100.57.2xx 55491 typ srflx raddr 192.168.0.3 rport 55491 generation 0
a=candidate:28878806xx 1 tcp 15099573xx 192.168.0.3 0 typ host generation 0
a=candidate:28878806xx 2 tcp 15099573xx 192.168.0.3 0 typ host generation 0

[予告]

次回は、WebRTCを使って、何かします。たぶん、Torrentの考え方を応用したサンプルを紹介します。

○ソース
https://github.com/kyorohiro/HelloWebRTC/tree/master/connectStun

2014年1月26日日曜日

P2P探訪 StunでNat越え その5

フルコーン意外は上手く通信できない

NATにもさまざまな特徴がある事が理解していただけたでしょう。
具体的に、各特徴ごとに通信可能か確認してみましょう。

フルコーン <---------> フルコーン
フルコーン <---------x 制限付き
フルコーン <---------x シンメトリック
制限付き    x---------x 制限付き

といった感じで、フルコーンNATでなければ、受信が困難な事がわかります。

UDPパンチなら一部回避できる

しかし、UDPホールパンチングというテクニックを使えばこの制限を一部回避
する事ができます。

フルコーン <---------> フルコーン
フルコーン <---------> 制限付き
制限付き    <---------> 制限付き
フルコーン <---------x シンメトリック

といった感じです。

前もってメッセージを送信してもらう

制限付きNATは、「送信した事がある相手からのメッセージを受け付ける」、「送信した事がない相手からのメッセージは受け付けない」を満たすように設計されています。

そこで、UDPパンチでは、これからメッセージを送ってくる相手へ、「あらかじめメッセージを送信しておく」ことで、この問題に対処しています。
相手にメッセージを送った実績があれば、制限付きNATが通信をフィルタリングすることはありません。相手からのメッセージは制限なく自分に届きます。

そもそも、UDPにおいては、通信に失敗したかどうかをNATは判断できません。
なので、 NATからは、受信したメッセージが「通信相手からのレスポンスなのか」、「新規のメッセージなのか」を判断するすべはありません。

例

※ d1とd2は異なる端末とします。
-----------------------------------------------
KyoroDatagramMock d1 = new KyoroDatagramMock(KyoroDatagramMock.NAT_TYPE_RESTRICTED_PORT);
KyoroDatagramMock d2 = new KyoroDatagramMock(KyoroDatagramMock.NAT_TYPE_RESTRICTED_PORT);
d1.bind(HttpObject.address("1.1.0.1", 800));
d2.bind(HttpObject.address("1.1.1.2", 801));
d1.send("...".getBytes(), d2.getMappedIp());
-----------------------------------------------

d1からd2への メッセージはd2へ届きません。 d2からd1へメッセージが送信された事がないからです。このため、不正な通信と見なされて破棄されます。



-----------------------------------------------
d2.send("....".getBytes(), d1.getMappedIp());
-----------------------------------------------
d2からd1への メッセージはd1へ届きません。 d1からd2へメッセージが送信された事がないからです。このため、不正な通信と見なされて破棄されます。


-----------------------------------------------
d1.send("abc".getBytes(), d2.getMappedIp());
-----------------------------------------------
d1からd2へのメッセージはとどきます。
以前d2からd1へメッセージを送信した事があるからです。



[まとめ/次回]

前もって、メッセージを送信してくる可能性がある相手には、UDPメッセージを送信しておく。といった事をするだけで、通信可能な端末が増える事を説明しました。
次回は、いったんNAT越えから離れて、具体的にP2Pネットワークを構成する方法について解説します。

DHTまたは、普及しているP2Pアプリプロトコルについて解説します。










2014年1月18日土曜日

P2P探訪 STUNでNat越え その4



まずは、スーパーノード候補として最有力なNATであるか調査してみましょう。
外部から見えている、「アドレスとポート」が常に一定であり。
他からのアクセスを制限していないNATのことです。(フルコーン)


○STUNでの確認方法

 どのようにして、制限があるかを確認するのでしょうか? STUNの仕組みはとても単純です。実際に通信してみて各種条件で通信ができるか試します。実際に試してみて、もしも通信できたならば、同一の条件下の端末とは通信可能といえるでしょう。


具体的には、実際に外部からメッセージを送信してもらう。そして、送信しもらったメッセージを受け取る事ができるかを確認します。

  • 接続したUDPサーバーから、メッセージを受け取れる事
  • 接続したUDPサーバーの異なるポートから、メッセージを受け取れること
  • 接続したUDPサーバーと異なるサーバーから、メッセージを受け取る事ができる事



○Bindingリクエスト

 実際にSTUNの通信内容を見ていきましょう。サーバーにレスポンスする条件(ポートとアドレス)を指定してレスポンスを返しもらいます。このレスポンスの依頼をSTUNでは、Bindingリクエストと読んでいます。

 例えば以下のような、依頼を出す事でしょう。
  1. CL はサーバーへ「受け取ったサーバーから、レスポンスを返してもらう。」依頼をだす。
  2. CL はサーバーへ「受け取ったサーバーから、ポートだけ変えてレスポンスを返してもらう。」依頼をだす。
  3. CL はサーバーへ「受け取ったサーバーから、ポートとアドレスを変えてレスポンスを返してもらう。」依頼をだす。


 3のレスポンスを受け取る事ができたならば、「フルコーン」といえます。



○ 実際に送受信するメッセージ

STUNではBindingリクエストと呼ばれるリクエストほをサーバーへサーバーへ送信します。


1. 送信先のアドレスとポートから返信するように依頼をだす。
{
0x00, 0x00,   // 最初の2byteは0
0x00, 0x01,   // Binding リクエストを意味する 0x01 
0x00, 0x08,   // Attrinuteのサイズ 8バイトを表す 0x08
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, // 16バイトのid
0x00, 0x03, // ChangeRequest 
0x00, 0x04, // attibuteのボティのサイズ 4
0x00, 0x00, 0x00, 0x00// 送信先のアドレスとポートから返信する
};

2. IPとポートを変えて返信

{
0x00, 0x00,   // 最初の2byteは0
0x00, 0x01,   // Binding リクエストを意味する 0x01 
0x00, 0x08,   // Attrinuteのサイズ 8バイトを表す 0x08
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, // 16バイトのid
0x00, 0x03, // ChangeRequest 
0x00, 0x04, // attibuteのボティのサイズ 4
0x00, 0x00, 0x00, 0x06// 送信先のアドレスとポートから返信する
};



以下のような返答をレスポンスを受け取る事ができます。

{
0x00, 0x00, // 最初の2バイトがは0
0x01, 0x01, // Bindingレスポンスを意味する。0x101 
0x00, 36, // length
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, //id
// 
0x00, 0x04// レスポンスをしてくれたサーバーのアドレスを意味する(SOURCE_ADDRESS)
0x00, 0x08, //length
0x00, 0x01, //family
0x03, (byte)(0xFF&0x20), //レスポンスしてくれたポート
127, 0, 0, 1,//レスポンスしてくれたアドレス
//
0x00, 0x01, // サーバーから見えたクライアントのアドレス(MAPPED_ADDRESS),
0x00, 0x08, //length
0x00, 0x01, //family
0x03, (byte)(0xFF&0x20), //サーバーから見えたクライアントのポート
127, 0, 0, 1,//サーバーから見えたクライアントのアドレス
//
0x00, 0x05, // STUNサーバーが持つ、異なるポートと異なるアドレス(CHANGE_ADDRESS)
0x00, 0x08, //length
0x00, 0x01, //family
0x03, (byte)(0xFF&0x20), //port
127, 0, 0, 1,


};

うけとったレスポンスを元に、自身がフルコーンなのか判定できます。判定するのは、STUNサーバーではなく。STUNクライアントであるところに注意してください。
今回の場合、このレスポンスを受け取る事ができたならば、フルコーンであると言えます。

参照検証ように作成したコード

https://github.com/kyorohiro/Hetimatan/blob/master/Hetimatan/src_nat/net/hetimatan/net/stun/HtunServer.java


まとめ/次回予告


という事で、STUNの正体は見えてきたでしょうか。
基本的な考え方は、「様々な条件で通信ができるか試してみ。どの条件で通信できたかで分類する」と行ったものである事が理解できたでしょう。

次回は、UDPパンチについて解説します。通信に細工をする事でNATをだまして、通信できるようにするテクニックです。






2014年1月13日月曜日

P2P探訪 STUNでNat越え その3

 STUNサーバーをつくりながら、NATの構成を推測する方法を解説していきます。
※ 制限のあるNAT配下で、通信をできるようにする方法については、後回しにします。
気になる方は「udp hole punching」とかで検索してください。

○もっとも厄介な制限

もっとも、厄介な制限はなんでしょうか? それは、UDPの使用に制限がかかっている場合です。
まずは、UDPの使用をできる事を確認してみましょう。


○サーバーに問い合わせて確認する

 UDPの使用に制限があるかはアプリからは判断できません。なぜならば、 制限を加えているのは、主にルータだからです。なので、実際に外部のUDPサーバーと通信してみるより方法がありません。
  外部のUDPサーバーに アクセスしてみて返答があれば、UDPが使える。返答がなければUDPが使えない。として判定できます。



○ 作った見よう

 本書では、NAT越えをじょじょ広げていき、Stunにサーバーもどきを作っていきます。ただ、UDPが使用可能かのチェックをするのに必要な最小のこ構成は、「外部に返答を返すUDPサーバーを用意する」だけです。
 早速用意してみました。

 やった事
  • Serversman で、vpsを借りる。
    stunを実現するには、ipアドレスが2つ必要です。Standardプラン以降のものを準備する必要があるでしょう。もちろん、P2Pアプリとして実現するのであれば、Entryプランを2つ取得しても良いでしょう。http://kyorohiro.blogspot.jp/2013/07/blog-post.html

  • 確認用に作成したコード
サーバーから見えているクライアントのアドレスとポートを返すだけのアプリです。    https://github.com/kyorohiro/Hetimatan/blob/master/Hetimatan/src_nat/net/hetimatan/net/stun/HtunServer.java


○ 次回

  Stunもどきの判定能力をじょじょにあげていたいと思います。次回はフルコーンNATかを判定してみる予定です。




2014年1月8日水曜日

P2P探訪 STUNでNAT越え その2

中継サーバーを間におく事で、お互いのアドレスとポート番号を特定する事ができまはた。UDPをSocketを使用して、通信してみましょう。しかし、残念ながら、多くの皆さんは通信に失敗する事でしょう。

<後述するような中継サーバー(P2PTracker)を試してみましょう>

ルータの制限

なぜだ? UDPならばできそうなものだが?

「アドレスとポートから送り先をたどれない」ならば、そもそもUDPでの通信ができないではないか?
 そもそも、UDPはTCPと違いコネクションを持ちません。UDPは通信相手もUDPパケットに含まれるアドレスとポート番号を頼りにして相手と通信をします。
 ですから、「UDPパケットを受け取ったサーバー」と「そのサーバーからパケット情報をもらったクライアント」では、差が無いように思えます。


残念ながら、ルータによって制限がかけられています。

残念ながらルータには制限がかけられている場合があります。UDPの通信の仕組みはどうあれ、「UDPで送ったパケットを返信するのは、送った先の端末から」なのです。
ルータ制作者の立場にたってみれば、それ以外を想定する必要はないでしょう。

 
例えば、通信した「相手のアドレス」意外は不正な通信の可能性が高いとみなして、パケットを破棄する。(制限付きNAT)
例えば、「通信した相手のポート番号」意外は不正な通信の可能性が高いと見なして、パケットを破棄する。
※ 制限している訳では無くて、効率の問題でそうなっているだけかも知れません。

そもそも、UDPの本来の目的を満たすだけならば、送信相手によって、アドレスとポートを変えても良いでしょう。(シンメトリックNAT)

まとめ/次回予告

UDPに制限がある事を理解して頂けたことでしょう。 次回は、「制限を突破する方法」または、「制限の種類を特定する方法」について解説して行きたいと思います。



サンプルコード

○中継サーバー

import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.Inet4Address;
import java.net.UnknownHostException;
import java.util.LinkedList;


public class P2PTracker {

    public static void main(String[] args) {
        P2PTracker tracker;
        try {
            tracker = new P2PTracker();
            tracker.run();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    private LinkedList mPeerList = new LinkedList<>();
    private DatagramSocket mSocket= null;

    public P2PTracker() throws IOException {
        mSocket = new DatagramSocket(8080);
    }

    public void run() {
        try {
            do {
                DatagramPacket packet = receive();
                requestPacket(packet.getAddress().getHostAddress(), packet.getPort());
            } while(true);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public DatagramPacket receive() throws IOException {
            byte[] buf = new byte[1024];
            DatagramPacket packet= new DatagramPacket(buf,buf.length);
            mSocket.receive(packet);
            PeerInfo info = new PeerInfo();
            info.mHost = packet.getAddress().getHostName();
            info.mPort = packet.getPort();
            if(!mPeerList.contains(info)) {
                mPeerList.addFirst(info);
                if(mPeerList.size() > 50) {
                    mPeerList.removeLast();
                }
            }
            return packet;
    }

    public void requestPacket(String host, int port) throws UnknownHostException, IOException {
        byte[] buf = getPeerInfoList().toString().getBytes();
        DatagramPacket packet= new DatagramPacket(
                buf, buf.length,
                Inet4Address.getByName(host), port);
        mSocket.send(packet);
    }

    public String getPeerInfoList() {
        StringBuilder buider = new StringBuilder();
        for(PeerInfo info : mPeerList) {
            buider.append(""+info.mHost+":"+info.mPort+",");
        }

        System.out.println("#s#"+buider.toString());
        return buider.toString();
    }

    public static class PeerInfo {
        public String mHost = "";
        public int mPort = 0;

        @Override
        public boolean equals(Object obj) {
            if(!(obj instanceof PeerInfo)) {
                return false;
            }
            PeerInfo target = (PeerInfo)obj;
            if(target.mHost.equals(mHost) && target.mPort == mPort) {
                return true;
            } else {
                return false;
            }
        }
    }
}



○クライアントの中継サーバと通信する部分

import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.Inet4Address;


public class P2PClient {

    public static void main(String[] args) {
        P2PClient client = new P2PClient("xx.xx.xx.xx");
        client.run(8081);
    }

    private String mHost = "127.0.0.1";
        public P2PClient(String host) {
        mHost = host;
    }

    public void run(int port) {
        try {
            clientAction(port);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public void clientAction(int port) throws IOException {
        DatagramSocket socket = new DatagramSocket(port);
        byte[] buf = new byte[1024];
        DatagramPacket sendPacket= new DatagramPacket(
          buf,buf.length,
          Inet4Address.getByName(mHost), 
          8080);
        socket.send(sendPacket);

        DatagramPacket receivePacket = new DatagramPacket(buf, buf.length);
        socket.receive(receivePacket);
        System.out.println("#cl#"+new String(receivePacket.getData()));
        socket.close();
    }
}


....
...
..


2014年1月7日火曜日

P2P探訪 STUNでNAT越え その1


UPnPを用いて、NAT越えできました。しかし、ルータがUPnPをサポートしていなかったり。UPnPだけでは越えられないNATがあります。

本文では、その代案として前回解説できなかった。「適当なサーバーに接続してみて、相手から見えているアドレスを返してもらう方法」について解説していきます。

TCPの限界

インターネットで公開されている情報のほとんどは、TCPという通信方法でデータをやり取りされています。ですから、インターネットで情報を公開したい場合は、TCPサーバーを立ち上げる事を考える事でしょう。
 しかし、ルータがUPnPをサポートしていない場合、TCPを用いたサーバーを運用する事は困難になります。※ 基本、無理と考えもらって問題ありません。


接続相手から教えてもらう方法はどうした?

適当なサーバーに接続してみて、相手から見えているアドレスを返してもらう事で実現できないのでしょうか。前回はできそうな事を臭わせていました。しかし、TCPにおいて、これは困難です。

実際にTCPのプログラムを書き確認して見ましょう。接続相手のホストアドレスは推測できます。しかし、ポート番号を知るすべはありません。


import java.io.IOException;
import java.net.Inet4Address;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.UnknownHostException;


public class TCPTest {

 public static void main(String[] args) {
  TCPTest test = new TCPTest();
  test.startServer();
  try {
   Thread.sleep(3000);
  } catch (InterruptedException e) {
   e.printStackTrace();
  }
  test.startClient();
 }

 private Server mServer = new Server();
 public void startServer() {
  mServer.start();
 }

 public void startClient() {
  try {
   clientAction();
  } catch (UnknownHostException e) {
   e.printStackTrace();
  } catch (IOException e) {
   e.printStackTrace();
  }
 }

 public class Server extends Thread {
  @Override
  public void run() {
   try {
    serverAction();
   } catch (IOException e) {
    e.printStackTrace();
   }
  }
 }
 public void serverAction() throws IOException {
  int port = 8080;
  ServerSocket server = new ServerSocket(port);
  Socket connection = server.accept();
  System.out.println("hostAddress="+connection.getInetAddress().getHostAddress());
  System.out.println("port="+connection.getPort());
 }

 public void clientAction() throws UnknownHostException, IOException {
  int port = 8080;
  Socket client = new Socket(Inet4Address.getByName("127.0.0.1"), port);
  client.getOutputStream().write("binding".getBytes());
 }
}


以下のような値が表示されます。
hostAddress=127.0.0.1

port=52106


サーバーがaccept()時に生成するSocketのさす「アドレスとポート」は接続先のサーバーのアドレスではありません。
なので、ポート番号は0-65535と限りがありますが、どのポートでサーバーが待ち受けているか調べるには、最悪65536回試す必要があります。



UDPを使おう

そこで、NATを越えて通信する方法として、UDPを使用する事でこの問題を解決しましょう。
UDPは、TCPと異なり、相手に接続するポート番号と、待ち受けるポート番号同じになります。このため、「相手から見えているアドレス」だけでなく、ポート番号も知る事ができるのです。



import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.Inet4Address;


public class UDPTest {

 public static void main(String[] args) {
  UDPTest test = new UDPTest();
  test.startServer();
  try {
   Thread.sleep(3000);
  } catch (InterruptedException e) {
   e.printStackTrace();
  }
  test.startClient();
 }

 public void startClient() {
  try {
   clientAction();
  } catch (IOException e) {
   e.printStackTrace();
  }
 }

 public void startServer() {
  Thread th = new Thread() {
   public void run() {
    try {
     serverAction();
    } catch (IOException e) {
     e.printStackTrace();
    }
   }
  };
  th.start();
 }
 public void clientAction() throws IOException {
  DatagramSocket socket = new DatagramSocket(8081);
  byte[] buf = new byte[1024];
  DatagramPacket packet= new DatagramPacket(
    buf,buf.length,
    Inet4Address.getByName("127.0.0.1"),
    8080
    );
  socket.send(packet);
 }

 public void serverAction() throws IOException {
  DatagramSocket socket = new DatagramSocket(8080);
  byte[] buf = new byte[1024];
  DatagramPacket packet= new DatagramPacket(buf,buf.length);
  socket.receive(packet);
  System.out.println("address="+packet.getAddress());
  System.out.println("port="+packet.getPort());
 }

}


以下のような値が表示されます。
address=/127.0.0.1

port=8081


サーバーへ接続したクライアントが待ち受けしている。アドレスとポート番号を取得できました。

まとめ

このような感じで、UDPを使う事で、アプリが外部に公開したいる「アドレス」と「ポート番号」を取得できました。UDP使えば、ご家庭のアプリをサーバーとして公開する事も可能でしょう。


次回予告
しかし、取得した「アドレス」と「ポート」には、制限があった。
素直につかえないぞ!!


-------
kyorohiro work

kyorohiro.strikingly.com





2014年1月3日金曜日

P2P探訪 UPnPでNAT越えする

 P2Pアプリは、サーバーとクライアントの両方機能をもったアプリです。基本的には、各言語のServer用のSocketでプログラムを書くことでこのサーバー部分の機能を実装できます。しかし、ご家庭の端末はそれだけでは実現できない事があります。


○  NATの弊害


 「端末から見えている自分のIP」と「通信相手から見えている自分のIP」がことなる場合があるからです。※ 異なるのが普通と事と考えてもよいでしょう。
 サーバーとしての機能を活用するためには、相手に自分のIPを伝える必要があります。そもそも、相手が自分のIPを知らないと、接続してもらえません。



○ 相手から見えているIPを知る方法

そこで、相手から見えているIPを調べて、相手に通知してあげましょう。そうすれば、サーバーとして機能を果たす事ができます。

相手から見えているIPを知る方法はいくつかあります。
* a. ルータに確認する
* b.適当なサーバーに接続してみて、相手から見えているアドレスを返してもらうぬ
などです。


ここでは、「a.ルータに確認する方法」について紹介します。


○ UPnPを使おう

ルーターとは、UPnPプロトコルを通して会話する事ができます。

1 "239.255.255.250" 1900に参加する


例えば以下のような感じ
<pre>
MulticastSocket ssdpSocket = null;
InetSocketAddress ssdpGroup = new InetSocketAddress("239.255.255.250", 1900);
InetAddress nicAddress = InetAddress.getByName(hostName);
ssdpSocket = new MulticastSocket(new InetSocketAddress(nicAddress, SSDP_PORT));
ssdpSocket.joinGroup(ssdpGroup, NetworkInterface.getByInetAddress(nicAddress));

</pre>

2. ルータを探す

UPnPは同一ネットワーク(ルータ内)のコンピュータへ、「M-SEARCH」をブルードキャスト送信する事で実現できます。

例えば以下のような感じ
<pre>
String message =
"M-SEARCH * HTTP/1.1"+"\r\b"+
"HOST:239.255.255.250:1900"+"\r\b"+
"MAN:\"ssdp:discover\""+"\r\b"+
"ST:urn:schemas-upnp-org:device:InternetGatewayDevice:1"+"\r\b"+
"MX:3"+"\r\b"+
"+"\r\b";


ssdpSocket.send(new DatagramPacket(
message.getBytes(), message.getBytes().length, ssdpGroup);

</pre>

こんなのが返る
<pre>
HTTP/1.1 200 OK
CACHE-CONTROL: max-age=120
Location: http://192.168.0.1:2869/upnp/rootdevice.xml
SERVER: IGD-HTTP/1.1 UPnP/1.0 UPnP-Device-Host/1.0
ST: urn:schemas-upnp-org:device:InternetGatewayDevice:1
EXT:
USN: uuid:79f0447f-860fbb81::urn:schemas-upnp-org:device:InternetGatewayDevice:1

</pre>

3. NAT関連のサービスを探す

http://192.168.0.1:2869/upnp/rootdevice.xmlをGetします。このXMLファイルには、そのデバイスが提供しているサービスが書かれています。このファイルをパースしてNAT関連のサービスを探しましょう。

例えば、
GET /upnp/rootdevice.xml HTTP/1.1

Host: 192.168.0.1
を送る。
</pre>

こんなのが返る
<?xml version="1.0"?>
<root xmlns="urn:schemas-upnp-org:device-1-0">
..
.
<service>
<serviceType>urn:schemas-upnp-org:service:Layer3Forwarding:1</serviceType>
<serviceId>urn:upnp-org:serviceId:L3Frwd1</serviceId>
<controlURL>/upnp/control/L3Frwd1</controlURL>
<eventSubURL>/upnp/event/L3Frwd1</eventSubURL>
<SCPDURL>/upnp/L3Frwd1.xml</SCPDURL>

</service>
...
..
<service>
<serviceType>urn:schemas-upnp-org:service:WANIPConnection:1</serviceType>
<serviceId>urn:upnp-org:serviceId:WANIPConn1</serviceId>
<controlURL>/upnp/control/WANIPConn1</controlURL>
<eventSubURL>/upnp/event/WANIPConn1</eventSubURL>
<SCPDURL>/upnp/WANIPConn1.xml</SCPDURL>
</service>
..
.
</root>

serviceTypeが、「WANIPConnection」「WANPPPConnection」のものが、NAT関連のサービスになります。


4. 通信相手から見えている自分のIPを取得する。

見つかったサービスから、IPアドレスを取得する事ができます。


こんな感じで要求を出す事ができます。
<pre>
POST /upnp/control/WANIPConn1 HTTP/1.0
SOAPACTION: "urn:schemas-upnp-org:service:WANPPPConnection:1#AddPortMapping"
Content-Length: 718
Host: 192.168.0.1

<?xml version="1.0"?>
<SOAP-ENV:Envelope xmlns:SOAP-ENV:="http://schemas.xmlsoap.org/soap/envelope/" SOAP-ENV:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
  <SOAP-ENV:Body>
    <m:AddPortMapping xmlns:m="urn:schemas-upnp-org:service:WANPPPConnection:1">
      <NewRemoteHost></NewRemoteHost>
      <NewExternalPort>8081</NewExternalPort>
      <NewProtocol>TCP</NewProtocol>
      <NewInternalPort>8081</NewInternalPort>
      <NewInternalClient>192.168.0.3</NewInternalClient>
      <NewEnabled>1</NewEnabled>
      <NewPortMappingDescription>test</NewPortMappingDescription>
      <NewLeaseDuration>3600</NewLeaseDuration>
    </m:AddPortMapping>
  </SOAP-ENV:Body>

</SOAP-ENV:Envelope>
</pre>

こんなのが返ります。

HTTP/1.1 200 OK
CONTENT-LENGTH: 423
CONTENT-TYPE: text/xml; charset="utf-8"
SERVER: IGD-HTTP/1.1 UPnP/1.0 UPnP-Device-Host/1.0
EXT:

<?xml version="1.0"?>
<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" SOAP-ENV:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
<SOAP-ENV:Body>
<m:GetExternalIPAddressResponse xmlns:m="urn:schemas-upnp-org:service:WANIPConnection:1">
<NewExternalIPAddress>xxx.xxx.xxx.xxx</NewExternalIPAddress>
</m:GetExternalIPAddressResponse>
</SOAP-ENV:Body>
</SOAP-ENV:Envelope>



5. Portをマッピングする

「通信相手から見えているポート番号」と「自分から見えているポート番号」を関連付けます。見つかったサービスへ要求を出す事で実現できます。
例えば以下のような感じ

<pre>
POST /upnp/control/WANIPConn1 HTTP/1.0
SOAPACTION: "urn:schemas-upnp-org:service:WANPPPConnection:1#AddPortMapping"
Content-Length: 718
Host: 192.168.0.1

<?xml version="1.0"?>
<SOAP-ENV:Envelope xmlns:SOAP-ENV:="http://schemas.xmlsoap.org/soap/envelope/" SOAP-ENV:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
  <SOAP-ENV:Body>
    <m:AddPortMapping xmlns:m="urn:schemas-upnp-org:service:WANPPPConnection:1">
      <NewRemoteHost></NewRemoteHost>
      <NewExternalPort>8081</NewExternalPort>
      <NewProtocol>TCP</NewProtocol>
      <NewInternalPort>8081</NewInternalPort>
      <NewInternalClient>192.168.0.3</NewInternalClient>
      <NewEnabled>1</NewEnabled>
      <NewPortMappingDescription>test</NewPortMappingDescription>
      <NewLeaseDuration>3600</NewLeaseDuration>
    </m:AddPortMapping>
  </SOAP-ENV:Body>

</SOAP-ENV:Envelope>
</pre>

成功すると、200 OKが返ります。

6. Portをマッピングを解除する

必要ななくなったら、ポートの関連づけを解除しましょう。以下のような感じででせきます。

POST /upnp/control/WANIPConn1 HTTP/1.0
SOAPACTION: "urn:schemas-upnp-org:service:WANPPPConnection:1#
DeletePortMapping"
Content-Length: xxx
Host: 192.168.0.1

<?xml version=\"1.0\"?>
<SOAP-ENV:Envelope xmlns:SOAP-ENV:=\"http://schemas.xmlsoap.org/soap/envelope/\" SOAP-ENV:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\">
 <SOAP-ENV:Body>
<m:DeletePortMapping xmlns:m=\"urn:schemas-upnp-org:service:WANPPPConnection:1\">
<NewRemoteHost></NewRemoteHost>
<NewExternalPort>8081</NewExternalPort>
<NewProtocol>TCP</NewProtocol>
</m:DeletePortMapping>
</SOAP-ENV:Body>
</SOAP-ENV:Envelope>

成功すると、200 OKが返ります。




○参考



http://upnp.org/sdcps-and-certification/



○検証用に書いたコード

https://github.com/kyorohiro/Hetimatan/tree/master/Hetimatan/src_util/net/hetimatan/net/ssdp


-------
kyorohiro work

kyorohiro.strikingly.com





2013年7月17日水曜日

P2P探訪 Raider トラッカーにアクセスしてみる

TorrentクローンをJava で作成しています。
そろそろ、Applet上で動作するデモができそうです。 試してみて解ったことが結構ありました。
そこで、学習したしことを、epub形式でまとめています。http://p.booklog.jp/users/kyorohiro


やる気スイッチが入らないので、書きかけの物をさらします。



-----------------

トラッカーにアクセスしてみる

TorrentからTrackerサーバーのアドレスを取得するできました。これでTrackerへ接続する準備ができました。さっそくTrackerにアクセスしてみましょう。

TrackerはHttpサーバーです。 「ピアID」と「Info辞書のHash」と「ポート番号」を渡すと、ピアの一覧を返してくれます。 Httpサーバーなので、「ピアID」と「Info辞書のHash」を持つURLさえ生成すれば、ブラウザからもアクセスできます。
まずは、 URLを生成して、Trackerからピアのリストを取得してみましょう。

ピアIDを準備する

Trackerへアクセスする際に、peer_idが要求されます。 peer_idは20バイトのユニーク値を持つバイト配列です。
このidは、クライアントを識別するのも活用できます。 詳しくは、http://www.bittorrent.org/beps/bep_0020.html を参照してみてください。 

例えば、以下のような感じでかける。
public static String createPeerId() {
byte[] peerId = new byte[20]; 
Random random = new Random(System.currentTimeMillis());
random.nextBytes(peerId);
System.arraycopy("-KY-".getBytes(), 0, peerId, 0, 8);
PercentEncoder encoder = new PercentEncoder();
return encoder.encode(peerId);
}
※ せっかく20バイトもデータがあのに、System.currentTimeMillis()でほぼ一意決まってしまう。スジが良くないです。 

Info辞書のHashを準備する

Trackerでは管理しているデータをユニークなID(SHA1のハッシュ)で管理しています。 Trackerは複数のTorrentファイルを管理していることがほとんどです。 なので、TrackerはこのユニークなIDをもらい、どのピアのリストを欲しがっているか判断するのです。

なので、Trackerからピア一覧をもらう場合は、このユニークなIDを用意する必要があります。 しかし、このIDはTorrentファイルには記載されていません。 Torrentファイルから生成する必要があります。
TorrentファイルのInfo辞書のSHA1を求めてください。 それがTrackerが必要としているユニークなIDです。


例えば、以下のような感じでかける。SHA1ハッシュは既存のAPIを使用すると良いでしょう。

Reader reader = <Info辞書の部分>
byte[] temp = ....
MessageDigest md = MessageDigest.getInstance("SHA");
do {
int len = reader.read(temp);
if (len < 0) {
break;
}
md.update(temp, 0, len);
} while (true);
digestLen = md.digest(buffer, 0, 20);


その他イロイロ準備する

上記のほかにも、「Port番号」、「状態」、「ダウンロードしたバイト数」、「アップロードしたバイト数」を渡す必要があります。
- 「Port番号」
他のピアからの通信の待ち受けしているポート番号です。 

- 「状態」
Trackerに状態を通知します。 「started」、 「stopped」、 「completed」の3つがあります。
既にデータを保持している場合は、「completed」、持っていない場合は、「started」、 ネットワークから抜ける場合は、「stopped」を設定します。

アドレスを生成してみよう

HttpサーバーへリクエストするURLは使用できる文字が制限されています。しかし、今回生成した値は、限りなくランダムに近い20バイトの配列です。 このため、そのままURLに使用することはできません。 
このような場合、 Percent Encoding をしたものをURLとして使用することになっています。
Percent Encoding
URLとして使用できない文字を、 「%xx」の形式にします。xxは16進数です。
例えば、以下のような感じでかけます。

private static final char[] sEncodeMap = {
'0','1','2','3','4','5','6',
'7','8','9','A','B','C','D','E','F'};
public  String encode(byte[] input, int start, int end) {
StringBuilder mBuilder = new StringBuilder();
for(int i=start;i<end;i++) {
byte atom = input[i];
mBuilder.append(''%');
mBuilder.append(sEncodeMap[(0xF0&atom)>>4]);
mBuilder.append(sEncodeMap[atom&0xF]);
}
return mBuilder.toString()
}


URLを作る
準備した値をつなげてURLを生成しましょう。
http://xxx.xxx.xx:6969/announce?info_hash=<生成した文字列>&peer_id=<ピアID>&port=6881&uploaded=0&downloaded=0&event=started

生成したURLをブラウザに入力すれば、Trackerからピアのリストをもらえます、

P2P探訪 Raider Torrentファイルを読み込んでみる(metafile)


TorrentクローンをJava で作成しています。
そろそろ、Applet上で動作するデモができそうです。 試してみて解ったことが結構ありました。
そこで、学習したしことを、epub形式でまとめています。http://p.booklog.jp/users/kyorohiro


やる気スイッチが入らないので、書きかけの物をさらします。





-------

Torrentファイルを読み込んでみる

すでにBencodingのパーサー作成済みなので、Torrentファイルの中身を解析することができるようになったと思います。
本ページでは、Torrentファイルに記載されている情報について説明します。

.Torrentはメタファイル 

Torrentファイルのデータの中身は、metainfo file structure と呼ばれています。 「メタ」というのは、データを要約したものです。
例えば、文書であれば、 「著作名」、「題名」、「ISBN番号」といったものがあげられます。 カメラで写真を撮ると、デシタルデータの場合JPEGという方式で画像されたりします。この時、 「撮影日時」、「撮影機器」、「解像度」、「GPS情報」などが記録されます。


では、Torrentでは、どのような情報が登録されているでしょうか?

MetaInfoの構造

ファイルがひとつの時
{
   announce:<benstring>, 
   info:{
     length:<beninteger>,
     name:<benstring>,
     piece length:<beninteger>,
     pieces:<benstring>
   }
}

ファイルが複数の場合
{
   announce:<benstring>, 
   info:{
    files: [
            { length:<beninteger>, path:[<benstring>,<benstring>]},
            { length:<beninteger>, path:[<benstring>,<benstring>]},
            ..
     ]
     name:<benstring>,
     piece length:<beninteger>,
     pieces:<benstring>
   }
} 

ファイル名を記録できる

ファイル名を記録できます。 図xxxの name でファイル名を表現します。 lengthでファイルサイズを表します。
例えば、 「dokamagi,mp4」という20MBのデータをを記録したい場合、以下のように記述できます。
{ 
info:{
  name:"dokamagi,mp4"
  length:20971529
 }
}

TorrentはひとつのMetaInfoに複数のファイルを含めることもできます。この場合、name と pathを用いてファイル名を表現します。

例えば、「data/manga/bb.zip」「data/movie/dokamagi.mp4」というデータを記録した場合は、以下のように記述できます。

{ 
info:{
  files[
    {length:20971529, movie, dokamagi.mp4}
    {length:2097152, manga, bb.mp4}
  ]
  name:"data"
  length:20971529
 }
}

Trackerのアドレスを記録できる

Trackerのアドレスが記録できます。 announce でアドレスを記述します。
例えば、「http://127.0.0.1:8080/announce」をTrackerのアドレスとして記録した場合は、
{
announce:"http://127.0.0.1:8080/announce"
}


データの分割情報を知ることができる

Torrentは、データを分割してダウンロード、アップロードします。  MetaFileには、データを分割するサイズ、そのSHA1ハッシュ値が記述されています。 分割するサイズは「piece length 」で記述します。 SHA1ハッシュは、「pieces」で記述します。
例えば、32kのデータを、16kbに分割する場合は
{
   announce:<benstring>, 
   info:{
     piece length:16384,
     pieces:<40バイトのデータ>
   }
}

piecesは、分割したデータごとにSHA1ハッシュをとります。上記の例では、データが2分割されるため、20×2の40バイトとなっています。

P2P探訪 Raider Torrentファイルを読み込んでみる(bencoding)

TorrentクローンをJava で作成しています。
そろそろ、Applet上で動作するデモができそうです。 試してみて解ったことが結構ありました。
そこで、学習したしことを、epub形式でまとめています。http://p.booklog.jp/users/kyorohiro


やる気スイッチが入らないので、書きかけの物をさらします。

-------------------------------------------------------------------

ダウンロードの始まり

データをダウンロードは、「へちまたん.torrent」といった、torrentを拡張子に持つファイルを取得することから始まります。

通常、インターネットを通じてWWWから取得することになると思います。
OSのイメージであれば配布しているサイトで手に入るかもしれません。 オープンソースアニメや著作権きれのデータなど、それ専用のデータを野止めているサイトを探すと良いでしょう。「オープンソース アニメ Torrent」、「著作権切れ Torrent」といったキーワードで検索してみてください。Googleで検索すれば簡単に見つかると思います。

このページでは、実際にTorrentファイルを読み込み。どのような情報が記載されているか調べてみましょう。

Bencode/Bencoding で書かれている

Torrentファイルはbencoding という形式で書かれています。  Torrentファイルを読み込むためには、bencodingを解釈できるようにならなくてなりません。 まずは、Bencodingを読み込んでみましょう。

String/Integer/List/Dictionaryを扱える

 bencodingは、 String/辞書/リスト の4つのデータ型を扱うことができます。
 そして、以下のようなフォーマットで書かれています。

  beninteger   : "i" [0-9]* "e"
  benstring    : [0-9]* ":" <bytes array/string> 
     # bytes array/string length is prev [0-9]*.
  bendiction   : "d" dictelements "e" 
  benlist      : "l" listelements "e"
  benobject    : beninteger | benstring | beniction | benlist
  listelements : benobject (benobject)*
  dictelements : benstring benobject (benstring benobject)*


ひとつひとつ見ていきましょう。
文字列
まずは文字列です。 この本ではbencodingでの文字列をbenstiringと呼ぶことにはます。 例えば、「torrent」という文字列は、こbenstringでは、「7:torrent」と書きます。  
また、benstringは  文字列だけでなく、バイト配列としても使われます。<20バイトの配列> は、「20:<20バイトの配列>」とあらわす事ができるのです。

整数
実数です。この本ではbencodingでの整数はbenintegerと呼ぶことにはます。例えば、「1024}という数字は、benintegerでは、[i1024e」と書くことができます。

リスト
リストとは、bencodingで扱えるデータ構造のひとつです。 0個以上のデータを順序ありでデータを保持できます。つまり、 文字、整数、リスト、辞書、を保持することができます。

この本ではbencodingでのリストはbenlistと呼ぶことにはます。
例えば、「512という数字と、"test"という文字列」は、benlistでは、[li512e4:teste」と書くことができます。                                                                                                                                           
辞書
辞書とはbencodingで扱えるデータ構造のひとつです。辞書データは文字列とデータを紐付けで管理することができます。
例えば RPGゲームで主人公のパラメータとして、名前、性別、レベル、得意な魔法、とか設定されているとします。辞書型は、主人公辞書は、"レベル"とキーを渡せば、主人公のレベルが返ります。 女の主人公ならば、"性別"とキーを渡せば、女性と返ります。

辞書です。この本ではbencodingでのリストはbendictと呼ぶことにはます。
例えば、bencodingで 「"レベル" 13」 「"magic" "halito"」 を持つ辞書は、bendictでは、[d5:levelei13e5:magic6:halitoe」と書くことができます。

パースしやすい構造になっている

bencodingはパースし易いつくりになっています。
先頭1文字で、どのデータ構造でできているかが判断できるようになっているからです。

例えば、 integerならば'i'、 stringならば's'、リストならば'l'、辞書ならば、'd' といった感じです。
なので、bencodingのdecoderは、以下のようなシナリオで実現できます。
1. 一文字読み込む。
2. どのデータか判別する。
3. データ形式に応じて読み込む。
4. 1に戻る。



今後の表記について

今後 bencodingでデーター構造を表はする場合は、以下の表記をしようします。
リスト
  「li512e4:teste」は、 「[ 512, test] 」と表記します。
辞書
  「d5:levelei13e5:magic6:halitoe」は「{level:13, magic:halito}」と表記します。

P2P探訪 Raider お互いの位置を知る

TorrentクローンをJava で作成しています。
そろそろ、Applet上で動作するデモができそうです。 試してみて解ったことが結構ありました。
そこで、学習したしことを、epub形式でまとめています。http://p.booklog.jp/users/kyorohiro


やる気スイッチが入らないので、書きかけの物をさらします。

-------------------------------------------------------------------

お互いの位置を知る

P2Pにてデータをダウンロードする場合、自分が欲しいデータを持っているコンピュータを探すことからはじまります。
これは、従来のシステムから考えると特殊なことです。たとえば、皆さんが日常で使用しているインターネットの世界では、欲しいデータの在り処はアドレスとい形で提供されています。Googleで検索したいならば、www.google.com、Youtubeで動画をみたいならば、www.youtube.com、ニコニコ動画で生放送をみたいならば、live.nicovideo.jp といったアドレスをブラウザに入力すると、欲しい情報を手に入れることができるでしょう。

しかし、P2Pではデータを持っているのは、インターネット上に名前を振られた、GoogleやYoutubeのようなホスト名を持つコンピュータではありません。今、皆さんがブラウザを起動しているようなコンピュータになります。
なので、GoogleやYoutubeにアクセスするように、皆さんのコンピュータにアクセスするためのアドレスを知る必要があります。

また、これはP2Pでのファイル共有としては重要な機能です。お互いのアドレスを知ることができれば、ほぼほぼデータ交換をに必要な作業は完了したと言っても良いでしょう。欲しいデータがあるコンピュータを知ることができたならば、後はデータ配信するようにお願いすれば良いのです。


Torrentでのお互いの位置を知る方法は2つある

では、Torrentではお互いの位置をどのようにして知るのでしょうか?Torrentでは2つの方法が提供されています。

Httpサーバーから教えてもらう方法

ひとつは、「GoogleやYoutubeのようなホスト名を持つコンピュータ」から教えてもらう方法。 このコンピュータのことをTrackerと呼びます。
P2Pといえる部分はTorrentクライアント同士がデータを転送する部分になります。 それだけではなく、TrackerはGoogleやYoutubeといったた通常のWWWのコンピュータと同じルールで動作しています。 
なので、ブラウザーからTrackerにアクセスすることも可能です。 ブラウザからTrackerにアクセスすると、データを配信してくれるTorrentクライアントの一覧を取得できます。 ためしに、以下の手順でURLを生成してTrackerにアクセスしてみてください。  TrackerはTorrentの一覧を返してくれます。
1. 
2. 
3. 
4. 
5. 

Torrentクライアント同で情報を交換しあう

 もうひとつは、「知り合いのTorrentクライアントから教えてもらう方法」です。 Mainline DHT と呼ばれていて、Kademliaベースの分散ハッシュテーブルが使用されています。 「Kademlia」については別の章で説明します。

当初TorrentはDHTをサポートしていませんでした。 このため、TorrentはTracker化が潰れてしまうとデータを配信したTorrentクライアントは存在するのに、データを配信できなません。 いかに、転送効率をあげようとも、Trackerが落ちてしまうという弱点を持っていたのです。
しかし、今では、DHTがサポートされ、Trackerが存在していなくとも、データの配信を可能にしました。


友達の友達は友達!!
DHTは「知り合いの知り合いを6人たどれば、世界中の誰とでもつながる」と同じ仕組みでできています。 例え、自分がメッセージを届けたい人のメールアドレスをしらなくても、 あなたの友人は知っているかも知れません。
友人にメッセージを届けたい人にメールアドレスを教えてもらうと良いでしょう。

もしも、 あなたの友人が「メッセージを届けたい人のメールアドレス」を知らなくても、その友人の友人は知っているかも知れません。現実世界では、「六次の隔たり」と呼ばれていて、6人もたどれば、世界中の誰とでもつながっているそうです。

DHTをサポートするTorrentクライアントは、 Torrentクライアントのリスト持っています。 もしも、特定のデータを持つTorrentクライアントを聞かれたら、自分のリストからもっともそのことを知っていそうなTorrentクライアントを紹介します。
これによって、少ない回数でおめあてのデータを持つクライアントを発見することでできるのです。

2013年6月3日月曜日

P2P探訪 Raider 楽観的チョーク解除ではぶられる確率

ひとつだけランダムにChokeを解除する機能が備わっています。Chokeがされ続けて1ピースもダウンロードが開始されない状況が長く発生するのかどうか検証しました。

[Choke]

Torrentでは、一度にアップロードするPeerを制限しています。だいたい、4つくらいです。4つの内1つはランダムに切り替わります。4つの内3つは戦略的に切り替えます。つまり、転送速度に時間がかからないPeerや、以前にデータをダウンロードさせてもらったPeerに優先的にデータをアップロードするのに使用します。

低回線でひとつもデータの断片を持たない場合、ランダムに切り替わるものだけが頼りになります。長時間データがアップロードされない状況に陥ることはないのでしょうか?検証しました。


[楽観的Chokeであふれる確率]

このランダムに割り振られるアップロードするPeerを選ぶことを楽観的チョーク解除と呼ぶことにします。Chokeがデータをアップロードしない。Unchokeがデータわアップロードする。という意味です。

Peerが4つある場合

以下のような組み合わせが考えられます。○がunchokeされる。 ×がchokeされる。
○○○○ 
×○○○,○×○○
○○×○,○○○× 
××○○,×○×○
×○○×,○××○
○×○×,○○×× 
○×××,×○××
××○×,×××○
×××× 

また、○となる確率は1/4、×となる確率は3/4ですから、

4つすべてのPeerからダウンロードできる確率は(1/256)3つからダウンロードできる確率は(12/256)、2つのPeerからダウンロードできる確率は(54/256)、1つからダウンロードできる確率は、(108/256)、どのPeerからもダウンロードできない確率は(81/256)となります。

同様にどのPeerからもダウンロードできない確率、

Peerが 4の場合は、 (3**4)/(4**4) = 0.31

Peerが 5の場合は、 (4**5)/(5**5) = 0.32

Peerが50の場合は、(49**50)/(50**50) = 0.36

Peerが1000000の場合は = 0.36

といった感じになります。また、30秒ごとに切り替わりますから、Peerが50であった場合、30秒後もはぶられる確率は、0.12、60秒後もはぶられる確率は、0.04となります。

Seederが存在している限り、ダウンロードがまったまできなくなる状況が続くことはなさそうです。


[PS]

簡単な2項分布の問題でした。2項分布つにいては、Googleしてください。

書き間違えてた。書き直した。

思ったよりもChokeが長く続く可能性があるのに驚いた。何か間違えているかな?

初期状態では、どれが戦略的に優先すべきPeerが不明なわけだから、(46**50)/(50**50)=0.015となる。また、((x-4)/x)**xは0.020くらいに収束する。つまり、Peer数が増えても状況はかわらない。

....ミスッた。別の記事で書き直します。



Game Boy の VGM から JavaScript への変換後に、 生成AIの補正が有効だった事について

Game Boy の VGM から JavaScript への変換後に、 生成AIの補正が有効だった事について Tetorica FM2612 Playground には GameBoy や Genesis の VGM ファイル から JavaScript へ変換する機能を持って...