1. 程式人生 > >簡單的p2p-demo,udp打洞

簡單的p2p-demo,udp打洞

什麼是p2p:

peer-to-peer,簡單來說,就是兩個使用者可以直接進行網路通訊。

為什麼我們需要p2p:

1.大多數的網路狀態都是使用者A和使用者B互相通訊,需要一箇中間伺服器來做訊息的中轉。如果可以使用者對使用者直接通訊,那麼可以減輕伺服器壓力。

2.一定程度上消除了網路延遲。

3.很多現實場景需要P2P,例如網路電話。

對網路有一定了解的話就會知道,一般使用者是沒有自己的公網ip的,使用者和伺服器通訊,都是使用者主動向伺服器傳送訊息(反向連線)並開啟一個可以讓伺服器進來的通道。伺服器才知道使用者的ip,並且可以向用戶傳送訊息。

如果A通過伺服器知道了B的Ip,A和B之間也不一定是能夠通訊的。這裡就涉及如下的內容:

NAT(Network Address Translation,網路地址轉換)是1994年提出的。當在專用網內部的一些主機本來已經分配到了本地IP地址(即僅在本專用網內使用的專用地址),但現在又想和因特網上的主機通訊(並不需要加密)時,可使用NAT方法。
這種方法需要在專用網連線到因特網的路由器上安裝NAT軟體。裝有NAT軟體的路由器叫做NAT路由器,它至少有一個有效的外部全球IP地址。這樣,所有使用本地地址的主機在和外界通訊時,都要在NAT路由器上將其本地地址轉換成全球IP地址,才能和因特網連線。
另外,這種通過使用少量的公有IP 地址代表較多的私有IP 地址的方式,將有助於減緩可用的IP地址空間的枯竭。在RFC 1632中有對NAT的說明。

而NAT又有3種:

靜態轉換是指將內部網路的私有IP地址轉換為公有IP地址,IP地址對是一對一的,是一成不變的,某個私有IP地址只轉換為某個公有IP地址。藉助於靜態轉換,可以實現外部網路對內部網路中某些特定裝置(如伺服器)的訪問。


動態轉換是指將內部網路的私有IP地址轉換為公用IP地址時,IP地址是不確定的,是隨機的,所有被授權訪問上Internet的私有IP地址可隨機轉換為任何指定的合法IP地址。也就是說,只要指定哪些內部地址可以進行轉換,以及用哪些合法地址作為外部地址時,就可以進行動態轉換。動態轉換可以使用多個合法外部地址集。當ISP提供的合法IP地址略少於網路內部的計算機數量時。可以採用動態轉換的方式。


埠多路複用(Port address Translation,PAT)是指改變外出資料包的源埠並進行埠轉換,即埠地址轉換(PAT,Port Address Translation).採用埠多路複用方式。內部網路的所有主機均可共享一個合法外部IP地址實現對Internet的訪問,從而可以最大限度地節約IP地址資源。同時,又可隱藏網路內部的所有主機,有效避免來自internet的攻擊。因此,目前網路中應用最多的就是埠多路複用方式。

引用自(http://baike.baidu.com/link?url=b3s1JVUyBy_UucgNiCXLcMcVUHWJjbstQBjOJEoeaqWvKN_taY-TsyVpOm-asMKwAJAINdKi1HQ0EUTSWadK6_)

也就是說,其中一種NAT的ip對映方式是動態的。(這種NAT很難打通)

對於其他幾種可以打通的NAT。我們可以搭建一箇中間伺服器,用於打洞。這樣可以實現使用者和使用者之間的直接通訊。流程如下:


1.A連線伺服器,伺服器得到A的ip。

2.B連線伺服器,伺服器得到B的ip。

3.A告訴伺服器A試圖對B傳送訊息,伺服器告訴B,讓B訪問A的ip。這時打開了一個B向A的通道。

4.伺服器告訴A打洞完成,A向B傳送訊息。

5.B告訴伺服器B試圖對A傳送訊息,伺服器告訴A,讓A訪問B的ip。這時打開了一個A向B的通道。

6.伺服器告訴B打洞完成,B向A傳送訊息。

這樣就實現了A和B的互通訊息。

如下一個簡單的DEMO,直接上程式碼:

Server:


//
// async_udp_echo_server.cpp
// ~~~~~~~~~~~~~~~~~~~~~~~~~
//
// Copyright (c) 2003-2015 Christopher M. Kohlhoff (chris at kohlhoff dot com)
//
// Distributed under the Boost Software License, Version 1.0. (See accompanying
// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
//

#include <cstdlib>
#include <iostream>
#include <sstream>
#include <boost/asio.hpp>
#include <boost/thread/thread.hpp>

using boost::asio::ip::udp;

class Server
{

private:
    const std::string LOGIN = "login"; 
    const std::string LOGIN_SUCESS = "login_sucess"; 
    const std::string LOGIN_ERROR = "login_error";
 
    const std::string LOGOUT = "logout"; 
    const std::string LOGOUT_ERROR = "logout_error";
 
    const std::string GETURL_P2S_REQUEST = "getUrlsReq"; 
    const std::string GETURL_S2P_RESPONSE = "getUrlsRep";
 
    const std::string P2PMSG = "p2pMsg"; 
    const std::string P2SMSG = "p2sMsg"; 
    const std::string S2PMSG = "s2pMgs";

    const std::string TRANSLATE_P2S_REQUEST = "p2sTranslate";
    const std::string TRANSLATE_S2P_REQUEST = "s2pTranslate";
    const std::string ACK_P2P = "ack";

public:

    Server(boost::asio::io_service& io_service, short port);
   
    void UserLogin(const std::string& userName,const udp::endpoint& netPoint);
    void UserLogout(const std::string& userName);
    void PaserCommand(const std::string& cmd,const udp::endpoint& netPoint);
    void UserGetUrl(const udp::endpoint& netPoint);
    void UserTransfer(const std::string& userName,const udp::endpoint& netPoint);
    ~Server();
    
    void do_receive();
    void do_send(std::size_t length);

private:
    struct User{
        std::string userName;
        udp::endpoint netPoint;
        User(const User &user){
            this->userName = user.userName;
            this->netPoint = user.netPoint;
        }
        User(std::string userName,udp::endpoint netPoint):userName(userName),netPoint(netPoint)
        {
        } 
    }; 

    std::vector<User> clientList;
    udp::socket server;
    udp::endpoint remotePoint;

    enum { max_length = 1024 };
    char data_[max_length];
    int msgLength;
};




//
// async_udp_echo_server.cpp
// ~~~~~~~~~~~~~~~~~~~~~~~~~
//
// Copyright (c) 2003-2015 Christopher M. Kohlhoff (chris at kohlhoff dot com)
//
// Distributed under the Boost Software License, Version 1.0. (See accompanying
// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
//

#include "server.h"
#include <cstdlib>
#include <iostream>
#include <sstream>
#include <boost/asio.hpp>
#include <stdarg.h>
#include <boost/algorithm/string.hpp>
#include <boost/tokenizer.hpp>
#include <boost/format.hpp>
#include <stdlib.h>
#include <sstream>
using boost::asio::ip::udp;
using namespace std;

string merage(std::string string_array[],unsigned int len){
    if(string_array == NULL)
        return "";
    std::string msg = "";
    for(unsigned int i = 0;i < len;++i){
        if(i == len-1)
        {
            msg += string_array[i];
        }    
        else
        {
            msg += string_array[i];
            msg += " ";
        }
    }
    return msg;
}

void Server::do_send(std::size_t length)
{
    server.async_send_to(
        boost::asio::buffer(data_, length), remotePoint,
        [this](boost::system::error_code,std::size_t)
            {
                do_receive();
            }
        );
}

Server::Server(boost::asio::io_service& io_service,short port):server(io_service,udp::endpoint(udp::v4(),port)),msgLength(-1)
{
    do_receive();
}

void Server::do_receive()
{
    server.async_receive_from(
        boost::asio::buffer(data_,max_length),remotePoint,
        [this](boost::system::error_code ec,std::size_t bytes_recvd){
        
        data_[bytes_recvd] = '\0';
        cout<<"do_recieve: "<<data_<<endl;
        PaserCommand(string(data_),remotePoint);
        
        if(!ec && bytes_recvd > 0)
        {
            do_send(msgLength);
        }
        else
        {
            do_receive();
        }
    });
}

Server::~Server()
{
}

void Server::UserLogin(const std::string& userName,const udp::endpoint& netPoint)
{
    clientList.push_back(User(userName,netPoint));
}

void Server::UserTransfer(const std::string& userName,const udp::endpoint& netPoint)
{
    cout<<"userName:  "<<userName<<endl;
    int index = -1;
    for(unsigned int i = 0;i < clientList.size();++i)
    {
        if(clientList[i].userName.compare(userName) == 0)
        { 
            index = i;
            break;
        }
    }
    if(index != -1){
        stringstream stream;
        stream<<remotePoint.port();
        string string_port;
        stream>>string_port;
       // join({}, " ")
        std::string string_array[] = {
            TRANSLATE_S2P_REQUEST,
            remotePoint.address().to_string(),
            string_port
        };
        string msg = merage(string_array,3);
        memcpy(data_,msg.c_str(),msg.size());
        data_[msg.size()] = '\0';   
        msgLength = msg.size()+1;
        cout<<string_array[1]<<endl;
        cout<<string_array[2]<<":"<<string_port<<endl;
        cout<<"to"<<clientList[index].netPoint<<endl;
        remotePoint = clientList[index].netPoint;
    }
    else
    {
        cout<<"no this user"<<endl;
    }
}

void Server::UserLogout(const std::string& userName)
{
    for(auto it = clientList.begin();it != clientList.end();it++)
    {
        if((*it).userName.compare(userName) == 0)
        {
            clientList.erase(it);
        }
    }
}

void Server::UserGetUrl(const udp::endpoint& netPoint)
{
    std::string msg = GETURL_S2P_RESPONSE;
    for(unsigned int i = 0;i < clientList.size();++i)
    {
        stringstream stream;
        stream<<clientList[i].netPoint.port();
        string string_port;
        stream>>string_port;
        string string_array[] = {
            msg,
            clientList[i].userName,
            clientList[i].netPoint.address().to_string(),
            string_port
        };
        msg = merage(string_array,4);
        memcpy(data_,msg.c_str(),msg.size());
        data_[msg.size()] = '\0';
        msgLength = msg.size()+1;
    }
}

void Server::PaserCommand(const std::string &cmd,const udp::endpoint& netPoint)
{
    cout<<endl;
    cout<<"cmd: "<<cmd<<endl;
    std::vector<std::string> vec_string;
    boost::split(vec_string,cmd,boost::is_any_of(" "));

    if(vec_string.size() > 0)
    {
        if(vec_string[0].compare(LOGIN) == 0)
        {
            UserLogin(vec_string[1],netPoint);
        }
        else if(vec_string[0].compare(LOGOUT) == 0)
        {
            UserLogout(vec_string[1]);
        }
        else if(vec_string[0].compare(GETURL_P2S_REQUEST) == 0)
        {
            UserGetUrl(netPoint);
        }
        else if(vec_string[0].compare(TRANSLATE_P2S_REQUEST) == 0)
        {
            UserTransfer(vec_string[2],netPoint);
        }
        else
        {
            cout<<"error command"<<endl;
        }
    }
}
//
// async_udp_echo_server.cpp
// ~~~~~~~~~~~~~~~~~~~~~~~~~
//
// Copyright (c) 2003-2015 Christopher M. Kohlhoff (chris at kohlhoff dot com)
//
// Distributed under the Boost Software License, Version 1.0. (See accompanying
// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
//

#include <cstdlib>
#include <iostream>
#include <sstream>
#include <boost/asio.hpp>
#include "server.h"

using boost::asio::ip::udp;

int main(int argc, char* argv[])
{
    try
    {
        boost::asio::io_service io_service;
        Server server(io_service, 2280);
        io_service.run();
    }
    catch (std::exception& e)
    {
        std::cerr << "Exception: " << e.what() << "\n";
    }

    return 0;
}



Client: (C#)
using System;
using System.Collections.Generic;
using System.Text;
using System.Net;
using System.Net.Sockets;
using System.Threading;


namespace P2P.P2PClient
{
    public class Client : IDisposable
    {
        private const int MAXRETRY = 10;
        private UdpClient client;
        private IPEndPoint hostPoint;
        private IPEndPoint remotePoint;
        private P2P.WellKnown.UserCollection userList;
        private string myName;
        private bool ReceivedACK;
        private Thread listenThread;

        public Client(string serverIP)
        {
            ReceivedACK = false;
            remotePoint = new IPEndPoint(IPAddress.Any, 0);
            hostPoint = new IPEndPoint(IPAddress.Parse(serverIP), P2P.WellKnown.P2PConsts.SRV_PORT);
            client = new UdpClient();
            userList = new P2P.WellKnown.UserCollection();
            listenThread = new Thread(new ThreadStart(Run));
        }

        public void Start()
        {
            if (this.listenThread.ThreadState == ThreadState.Unstarted)
            {
                this.listenThread.Start();
            }
        }

        public void ConnectToServer(string userName)
        {
            myName = userName;
            string msg = P2P.WellKnown.P2PConsts.MerageCmd(P2P.WellKnown.P2PConsts.LOGIN, userName);
            byte[] byteArray = System.Text.Encoding.Default.GetBytes(msg);
            client.Send(byteArray, byteArray.Length, hostPoint);
            RequestGetUrls();
        }

        private bool SendMessageTo(string toUserName, string message)
        {
            P2P.WellKnown.User toUser = userList.Find(toUserName);
            if (toUser == null)
            {
                return false;
            }
            for (int i = 0; i < MAXRETRY; i++)
            {
                string p2pmsg = P2P.WellKnown.P2PConsts.MerageCmd(P2P.WellKnown.P2PConsts.P2PMSG,message);
                byte[] p2pByteArray = System.Text.Encoding.Default.GetBytes(p2pmsg);
                client.Send(p2pByteArray,p2pByteArray.Length,toUser.NetPoint);
                // 等待接收執行緒將標記修改
                for (int j = 0; j < 10; j++)
                {
                    if (this.ReceivedACK)
                    {
                        this.ReceivedACK = false;
                        return true;
                    }
                    else
                    {
                        Thread.Sleep(300);
                    }
                }
                //UDP打洞
                string msg = P2P.WellKnown.P2PConsts.MerageCmd(P2P.WellKnown.P2PConsts.TRANSLATE_P2S_REQUEST,myName,toUserName);
                byte[] byteArray = System.Text.Encoding.Default.GetBytes(msg);
                client.Send(byteArray, byteArray.Length,hostPoint);
                // 等待對方先發送資訊
                Thread.Sleep(100);
            }
            return false;
        }
        private void DisplayUsers(P2P.WellKnown.UserCollection users)
        {
            foreach (P2P.WellKnown.User user in users)
            {
                Console.WriteLine("Username: {0}, IP:{1}, Port:{2}", user.UserName, user.NetPoint.Address.ToString(), user.NetPoint.Port);
            }
        }

        private void RecieveUserListMsg(string[] args)
        {
            if (args.Length < 4)
                return;
            userList.Clear();
            for (int i = 1; i < args.Length; i+=3)
            {
                userList.Add(new P2P.WellKnown.User(
                    args[i], 
                    new IPEndPoint(IPAddress.Parse(args[i+1]), 
                        int.Parse(args[i+2])))
                        );
            }
            this.DisplayUsers(userList); 
        }

        private void RecieveTransferMsg(IPEndPoint remotePoint)
        {
            string msg = P2P.WellKnown.P2PConsts.MerageCmd(P2P.WellKnown.P2PConsts.ACK_P2P);
            byte[] byteArray = System.Text.Encoding.Default.GetBytes(msg);
            client.Send(byteArray,byteArray.Length,remotePoint);

            for (int i = 1; i < 11; i++)
            {
                Console.WriteLine(remotePoint.Port + i);
                client.Send(byteArray, byteArray.Length, new IPEndPoint(remotePoint.Address,remotePoint.Port + i));
            }
        }

        private void RecieveP2PMsg()
        {
            //回覆
            string msg = P2P.WellKnown.P2PConsts.MerageCmd(P2P.WellKnown.P2PConsts.ACK_P2P);
            byte[] byteArray = System.Text.Encoding.Default.GetBytes(msg);
            client.Send(byteArray, byteArray.Length,remotePoint);
        }

        private void RequestGetUrls() 
        {
            string msg = P2P.WellKnown.P2PConsts.MerageCmd(P2P.WellKnown.P2PConsts.GETURL_P2S_REQUEST, myName);
            byte[] byteArray = System.Text.Encoding.Default.GetBytes(msg);
            client.Send(byteArray, byteArray.Length, hostPoint);
        }

        private void PaserResponseCommand(String cmdstring) 
        {
            cmdstring = cmdstring.Trim();
            string[] args = cmdstring.Split(new char[] { ' ' });
            Console.WriteLine(cmdstring);
            if (args.Length > 0)
            {
                if (string.Compare(args[0], P2P.WellKnown.P2PConsts.GETURL_S2P_RESPONSE, true) == 0)
                {
                    RecieveUserListMsg(args);
                }
                else if (string.Compare(args[0], P2P.WellKnown.P2PConsts.TRANSLATE_S2P_REQUEST, true) == 0)
                {
                    RecieveTransferMsg(new IPEndPoint(IPAddress.Parse(args[1]), int.Parse(args[2])));
                }
                else if (string.Compare(args[0], P2P.WellKnown.P2PConsts.P2PMSG, true) == 0)
                {
                        RecieveP2PMsg();
                }
                else if (string.Compare(args[0], P2P.WellKnown.P2PConsts.ACK_P2P, true) == 0)
                {
                    Console.WriteLine("Receive ACK");
                    this.ReceivedACK = true;
                }
            }
        }

        private void Run()
        {
            byte[] buffer;
            while (true)
            {
                Console.WriteLine("run!");
       
                buffer = client.Receive(ref remotePoint);
                String str = System.Text.Encoding.Default.GetString(buffer);
                PaserResponseCommand(str);
                Thread.Sleep(100);
            }
        }


        public void PaserCommand(string cmdstring)
        {
            cmdstring = cmdstring.Trim();
            string[] args = cmdstring.Split(new char[] { ' ' });

            if (args.Length > 0)
            {
                if (string.Compare(args[0], P2P.WellKnown.P2PConsts.LOGOUT, true) == 0)
                {
                    string msg = P2P.WellKnown.P2PConsts.MerageCmd(P2P.WellKnown.P2PConsts.LOGOUT, myName);
                    byte[] byteArray = System.Text.Encoding.Default.GetBytes(msg);
                    client.Send(byteArray, byteArray.Length, hostPoint);
                    Dispose();
                    System.Environment.Exit(0);
                }
                else if (string.Compare(args[0], P2P.WellKnown.P2PConsts.P2PMSG, true) == 0)
                {
                    if (args.Length > 2)
                    {
                        string toUserName = args[1];
                        string message = "";

                        for (int i = 2; i < args.Length; i++)
                        {
                            if (args[i] == "") message += " ";
                            else message += args[i];
                        }
                        if (this.SendMessageTo(toUserName, message))
                        {
                            Console.WriteLine("Send OK!");
                        }
                        else
                            Console.WriteLine("Send Failed!");
                    }
                }
                else if (string.Compare(args[0], P2P.WellKnown.P2PConsts.GETURL_P2S_REQUEST, true) == 0)
                {
                    RequestGetUrls();
                }
                else
                {
                    Console.WriteLine("Unknown command {0}", cmdstring);
                }
            }
        }


        #region IDisposable 成員

        public void Dispose()
        {
            try
            {
                this.listenThread.Abort();
                this.client.Close();
            }
            catch
            { }
        }

        #endregion

    }
}


using System;
using System.Collections.Generic;
using System.Text;
using System.Threading;
namespace P2P.P2PClient
{
    class Program
    {
        static void Main(string[] args)
        {
            Client client = new Client("xxx.xxx.xxx.xxx"); 
            client.ConnectToServer("xxx.xxx.xxx.xxx");

            client.Start();

            while (true)
            {
                string str = Console.ReadLine();
                client.PaserCommand(str);
            }
        }
    }
}


伺服器的程式碼是對boost中得demo的簡單修改。

1.如果要真正實現一個自己的TURN伺服器的話。還需要考慮洞的生存時間,需要發一些包用來維護洞的存在。

2.打不通的通過伺服器做中轉。