16

UDPホールパンチングの実装方法についてよく読んでいますが、何らかの理由で機能させることができません。

udp ホール パンチングが何であるかをよく知らない人のために、私自身の定義を以下に示します。

目標は、サーバーの助けを借りて 2 つのクライアント (クライアント A とクライアント B) 間でデータを転送できるようにすることです。したがって、クライアント A はサーバーに接続し、その情報を送信します。クライアント B も同じことを行います。サーバーには、クライアント A がクライアント B にデータを送信したり、クライアント A からクライアント B にデータを送信したりできるように、必要な情報があります。したがって、サーバーはその情報を両方のクライアントに提供します。両方のクライアントが互いに関する情報を取得すると、サーバーの助けを借りずにそれらのクライアント間でデータの送受信を開始できます。

私の目標は、今説明したこと (udp ホール パンチング) を実行できるようにすることです。その前に、サーバーからクライアントに接続できると便利だと思います。そのために、クライアントに関する情報をサーバーに送信する予定です。サーバーがその情報を受信したら、最初からクライアントに接続しようとします。それを実行できるようになったら、実際の udp ホール パンチングの実装を開始するために必要なものがすべて揃っているはずです。

設定方法は次のとおりです。

ここに画像の説明を入力

上のルーターにはサーバーがあり、下のルーターは LAN ポートに接続されています。下のルーター (NAT) は、WAN ポートを介して上のルーターに接続されています。クライアント コンピュータは、下のルーターの LAN ポートの 1 つに接続されています。

したがって、その接続では、クライアントはサーバーを見ることができますが、サーバーはクライアントを見ることができません。

したがって、擬似コードで行ったアルゴリズムは次のとおりです。

  • クライアントがサーバーに接続します。
  • クライアントは、NAT でいくつかのポートを開くためにいくつかの UDP パッケージをサーバーに送信します
  • クライアントがリッスンしているポートでサーバーに情報を送信します。
  • サーバーがその情報を受信したら、最初からクライアントに接続しようとします。

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

サーバ:

static void Main()
{     
    /* Part 1 receive data from client */
    UdpClient listener = new UdpClient(11000);
    IPEndPoint groupEP = new IPEndPoint(IPAddress.Any, 11000);
    string received_data;
    byte[] receive_byte_array = listener.Receive(ref groupEP);       
    received_data = Encoding.ASCII.GetString(receive_byte_array, 0, receive_byte_array.Length);

    // get info
    var ip = groupEP.Address.ToString();
    var port = groupEP.Port;

    /* Part 2 atempt to connect to client from scratch */
    // now atempt to send data to client from scratch once we have the info       
    Socket sendSocket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
    IPEndPoint endPointClient = new IPEndPoint(IPAddress.Parse(ip), port);
    sendSocket.SendTo(Encoding.ASCII.GetBytes("Hello"), endPointClient);
}

クライアント:

static void Main(string[] args)
{
    /* Part 1 send info to server */
    Socket sending_socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram,  ProtocolType.Udp);
    IPAddress send_to_address = IPAddress.Parse("192.168.0.132");
    IPEndPoint sending_end_point = new IPEndPoint(send_to_address, 11000);
    sending_socket.SendTo(Encoding.ASCII.GetBytes("Test"), sending_end_point);

    // get info
    var port = sending_socket.LocalEndPoint.ToString().Split(':')[1];

    /* Part 2 receive data from server */
    IPEndPoint groupEP = new IPEndPoint(IPAddress.Any, int.Parse(port));
    byte[] buffer = new byte[1024];
    sending_socket.Receive(buffer);
} 

何らかの理由で数回機能しました!クライアントが回線でデータを正常に受信すると機能します。sending_socket.Receive(buffer);

注意事項:listner 2 番目の部分のサーバーで、新しい変数を作成する代わりに インスタンス変数を使用し、sendSocketその変数を介してバイトを送信した場合、クライアントは送信されるデータを受信できます。サーバーの 2 番目の部分は 2 番目のクライアント B によって実装されることを思い出してください。そのため、変数を最初から再度初期化しています...


編集:

ここでは、同じ問題を別の方法で見ることができます。同じオブジェクトを使用する代わりに新しいオブジェクトを初期化すると、クライアントは応答を受け取りません。

タイプ UdpClient のオブジェクトがあります。そのオブジェクトでデータを他のピアに送信できます。同じプロパティを持つ同じタイプの別のオブジェクトを作成してデータを送信しようとすると、機能しません! いくつかの変数を初期化するのに欠けている可能性があります。リフレクションでプライベート変数を設定できるので、問題はありません。とにかくここにサーバーコードがあります:

public static void Main()
{
    // wait for client to send data
    UdpClient listener = new UdpClient(11000);
    IPEndPoint groupEP = new IPEndPoint(IPAddress.Any, 11000);        
    byte[] receive_byte_array = listener.Receive(ref groupEP);

    // connect so that we are able to send data back
    listener.Connect(groupEP);

    byte[] dataToSend = new byte[] { 1, 2, 3, 4, 5 };

    // now let's atempt to reply back

    // this part does not work!
    UdpClient newClient = CopyUdpClient(listener, groupEP);
    newClient.Send(dataToSend, dataToSend.Length);

    // this part works!
    listener.Send(dataToSend, dataToSend.Length);
}

static UdpClient CopyUdpClient(UdpClient client, IPEndPoint groupEP)
{
    var ip = groupEP.Address.ToString();
    var port = groupEP.Port;
    var newUdpClient = new UdpClient(ip, port);
    return newUdpClient;
}

クライアント コードは基本的にサーバーにデータを送信し、応答を待ちます。

    string ipOfServer = "192.168.0.132";
    int portServerIsListeningOn = 11000;

    // send data to server
    Socket sending_socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
    IPAddress send_to_address = IPAddress.Parse(ipOfServer);
    IPEndPoint sending_end_point = new IPEndPoint(send_to_address, portServerIsListeningOn);
    sending_socket.SendTo(Encoding.ASCII.GetBytes("Test"), sending_end_point);

    // get info
    var port = sending_socket.LocalEndPoint.ToString().Split(':')[1];

    // now wait for server to send data back
    IPEndPoint groupEP = new IPEndPoint(IPAddress.Any, int.Parse(port));
    byte[] buffer = new byte[1024];
    sending_socket.Receive(buffer); // <----- keeps waiting in here :(

クライアントはルーター (NAT) の背後にあることに注意してください。それ以外の場合、この問題は発生しません。udpClient をコピーする理由は、その変数を別のコンピューターに送信して、他のコンピューターがクライアントにデータを送信できるようにするためです。

だから私の質問は 、元のオブジェクトlistenerがデータを送信できるのにできnewClientないのはなぜですか? sending_socket.Receive(buffer);サーバーが行を実行した後でも、クライアントは行で待機し続けます: newClient.Send(dataToSend, dataToSend.Length);。リスナーがデータを送信しても newClient を送信しない場合、クライアントはデータを正常に受信します。両方の変数の宛先 IP とポートが同じ場合、これはなぜですか? 変数はどのように異なりますか?

注: サーバーとクライアントが同じネットワーク上にある場合、コピーは機能し、変数newClientはクライアントにデータを送信できます。この問題をシミュレートするには、クライアントが NAT (ルーター) の背後にある必要があります。このようなネットワークの例は、2 つのルーターで構成されている場合があります。それらをルーター X とルーター Y と呼びましょう。S とクライアント C のサーバー呼び出しも必要です。これにより、S は X の LAN ポートの 1 つに接続できます。C は Y の LAN ポートの 1 つに接続できます。最後に、Y の WAN ポートを X の LAN ポートの 1 つに接続します。

4

4 に答える 4

14

うーん、ここでいくつかのことを混乱させていると思います。一つには、それは実際にはUDPホールパンチングと呼ばれています。これがどのように機能するかを説明しようと思います。

NATルーターは通常、内部プライベートネットワークから外部インターネットにパケットを転送するときにポートマッピングを実行します。

NATの背後にあるマシンでUDPソケットを作成し、データグラムを外部IP/ポートに送信したとします。そのデータグラムを運ぶIPパケットが送信側マシンを離れるとき、そのIPヘッダーの送信元アドレスフィールドはローカルのグローバルにルーティングできないプライベートIPアドレス(など192.168.1.15)に設定され、UDPヘッダーの送信元ポートフィールドは割り当てられたポートに設定されますソケット(バインディングを介して明示的に、またはエフェメラルポートからOSによって暗黙的に選択されます)。この送信元ポート番号に電話しますP1

次に、NATルーターがそのパケットを外部ネットワークに送信すると、送信元IPアドレスを自身の外部IPアドレスに上書きし(そうでない場合、パケットを元に戻す方法はありません)、多くの場合、送信元UDPポートを他の値に上書きします(おそらくプライベートネットワーク上の他のホストが同じ送信元ポートを使用しているため、あいまいさが生じます)。元の送信元ポートとその新しいポート番号(ラベルを付けましょうP2)の間のマッピングは、リターンパケットと一致するようにルーターに保持されます。このマッピングは、ターゲットIPアドレスターゲットUDPポートに固有の場合もあります。

これで、ルーターに「穴を開ける」ことができました。ルーターからポートに返送されたUDPパケットはP2、UDPポートで内部マシンに転送されますP1。繰り返しますが、NATの実装によっては、これを元のターゲットIPアドレスとターゲットUDPポートからのパケットのみに制限することができます。

クライアント間通信の場合、NATルーターが同じ内部送信元ポートを同じ外部送信元ポートにマップすることを期待して、サーバーを介して一方の外部IP/ポートを他方に通知する必要があります。次に、クライアントはそれらを使用して相互にパケットを送信します。

お役に立てれば。

于 2012-08-30T19:02:16.050 に答える
3

ついに答えが見つかりました!これは、クライアントとサーバーだけの実装です。私の次の試みは、3 台のコンピューターを使用することです。とにかくこれが役立つことを願っています:

サーバーコード:

class Program
{
    static byte[] dataToSend = new byte[] { 1, 2, 3, 4, 5 };

    // get the ip and port number where the client will be listening on
    static IPEndPoint GetClientInfo()
    {
        // wait for client to send data
        using (UdpClient listener = new UdpClient(11000))
        {
            IPEndPoint groupEP = new IPEndPoint(IPAddress.Any, 11000);
            byte[] receive_byte_array = listener.Receive(ref groupEP);

            return groupEP;
        }
    }

    static void Main(string[] args)
    {
        var info = GetClientInfo(); // get client info

        /* NOW THAT WE HAVE THE INFO FROM THE CLIENT WE ARE GONG TO SEND
           DATA TO IT FROM SCRATCH!. NOTE THE CLIENT IS BEHIND A NAT AND
           WE WILL STILL BE ABLE TO SEND PACKAGES TO IT
        */

        // create a new client. this client will be created on a 
        // different computer when I do readl udp punch holing
        UdpClient newClient = ConstructUdpClient(info);

        // send data
        newClient.Send(dataToSend, dataToSend.Length);            
    }

    // Construct a socket with the info received from the client
    static UdpClient ConstructUdpClient(IPEndPoint clientInfo)
    {          
        var ip = clientInfo.Address.ToString();
        var port = clientInfo.Port;

        // this is the part I was missing!!!!
        // the local end point must match. this should be the ip this computer is listening on
        // and also the port            
        UdpClient client = new UdpClient(new IPEndPoint( IPAddress.Any, 11000));

        // lastly we are missing to set the end points. (ip and port client is listening on)

        // the connect method sets the remote endpoints
        client.Connect(ip, port);

        return client;
    }
}

クライアントコード:

string ipOfServer = "192.168.0.139";
int portServerIsListeningOn = 11000;

// send data to server
Socket sending_socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
IPAddress send_to_address = IPAddress.Parse(ipOfServer);
IPEndPoint sending_end_point = new IPEndPoint(send_to_address, portServerIsListeningOn);
sending_socket.SendTo(Encoding.ASCII.GetBytes("Test"), sending_end_point);

// get info
var port = sending_socket.LocalEndPoint.ToString().Split(':')[1];

// now wait for server to send data back
IPEndPoint groupEP = new IPEndPoint(IPAddress.Any, int.Parse(port));
byte[] buffer = new byte[1024];
sending_socket.Receive(buffer); // <----- we can receive data now!!!!!
于 2012-09-01T17:12:14.510 に答える
1

Have you considered using UPnP on the client to configure NAT traversal to allow incoming packets on a particular port? The client would then only need to communicate the inbound IP and port to the server, and wait for the server to send packets. http://en.wikipedia.org/wiki/Universal_Plug_and_Play

于 2012-08-30T22:30:29.460 に答える
0

初めてサーバーに接続できるようです。接続が成功したら、毎回接続を閉じて切断する必要があります。このサンプル コードを確認して ください http://codeidol.com/csharp/csharp-network/Connectionless-Sockets/A -Simple-UDP-Application/

于 2012-08-29T05:38:31.523 に答える