簡單的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.打不通的通過伺服器做中轉。