史萊姆論壇

返回   史萊姆論壇 > 教學文件資料庫 > 網路軟硬體架設技術文件
忘記密碼?
註冊帳號 論壇說明 標記討論區已讀

歡迎您來到『史萊姆論壇』 ^___^

您目前正以訪客的身份瀏覽本論壇,訪客所擁有的權限將受到限制,您可以瀏覽本論壇大部份的版區與文章,但您將無法參與任何討論或是使用私人訊息與其他會員交流。若您希望擁有完整的使用權限,請註冊成為我們的一份子,註冊的程序十分簡單、快速,而且最重要的是--註冊是完全免費的!

請點擊這裡:『註冊成為我們的一份子!』

Google 提供的廣告


 
 
主題工具 顯示模式
舊 2005-06-15, 05:20 AM   #1
psac
榮譽會員
 
psac 的頭像
榮譽勳章
UID - 3662
在線等級: 級別:30 | 在線時長:1048小時 | 升級還需:37小時級別:30 | 在線時長:1048小時 | 升級還需:37小時級別:30 | 在線時長:1048小時 | 升級還需:37小時級別:30 | 在線時長:1048小時 | 升級還需:37小時級別:30 | 在線時長:1048小時 | 升級還需:37小時
註冊日期: 2002-12-07
住址: 木柵市立動物園
文章: 17381
現金: 5253 金幣
資產: 33853 金幣
預設 P2P之UDP穿透NAT的原理與實現

P2P之UDP穿透NAT的原理與實現





論壇上經常有對P2P原理的討論,但是討論歸討論,很少有實質的東西產生(來源碼)。哈哈,在這裡我就用自己實現的一個來源碼來說明UDP穿越NAT的原理。

首先先介紹一些基本概念:
NAT(Network Address Translators),網路位址轉換:網路位址轉換是在IP位址日益缺乏的情況下產生的,它的主要目的就是為了能夠位址重用。NAT分為兩大類,基本的NAT和NAPT(Network Address/Port Translator)。
最開始NAT是執行在路由器上的一個功能模組。

最先提出的是基本的NAT,它的產生關於如下事實:一個私有網路(域)中的節點中只有很少的節點需要與外網連接(哈哈,這是在上世紀90年代中期提出的)。那麼這個子網中其實只有少數的節點需要全球唯一的IP位址,其他的節點的IP位址應該是可以重用的。
因此,基本的NAT實現的功能很簡單,在子網內使用一個保留的IP子網段,這些IP對外是不可見的。子網內只有少數一些IP位址可以對應到真正全球唯一的IP位址。如果這些節點需要訪問外部網路,那麼基本NAT就負責將這個節點的子網內IP轉化為一個全球唯一的IP然後傳送出去。(基本的NAT會改變IP包中的原IP位址,但是不會改變IP包中的連接阜)
關於基本的NAT可以參看RFC 1631

另外一種NAT叫做NAPT,從名稱上我們也可以看得出,NAPT不但會改變經過這個NAT設備的IP資料報的IP位址,還會改變IP資料報的TCP/UDP連接阜。基本NAT的設備可能我們見的不多(哈哈,我沒有見到過),NAPT才是我們真正討論的主角。看下圖:
Server S1
18.181.0.31:1235
|
^ Session 1 (A-S1) ^ |
| 18.181.0.31:1235 | |
v 155.99.25.11:62000 v |
|
NAT
155.99.25.11
|
^ Session 1 (A-S1) ^ |
| 18.181.0.31:1235 | |
v 10.0.0.1:1234 v |
|
Client A
10.0.0.1:1234
有一個私有網路10.*.*.*,Client A是其中的一台電腦,這個網路的網路閘道(一個NAT設備)的外網IP是155.99.25.11(應該還有一個局內網的IP位址,比如10.0.0.10)。如果Client A中的某個行程(這個行程新增了一個UDP Socket,這個Socket綁定1234連接阜)想訪問外網主機18.181.0.31的1235連接阜,那麼當資料包通過NAT時會發生什麼事情呢?
首先NAT會改變這個資料包的原IP位址,改為155.99.25.11。

接著NAT會為這個傳輸新增一個Session(Session是一個抽像的概念,如果是TCP,也許Session是由一個SYN包開始,以一個FIN包結束。而UDP呢,以這個IP的這個連接阜的第一個UDP開始,結束呢,哈哈,也許是幾分鍾,也許是幾小時,這要看具體的實現了)並且給這個Session分配一個連接阜,比如62000,然後改變這個資料包的源連接阜為62000。所以本來是(10.0.0.1:1234->18.181.0.31:1235)的資料包到了網際網路上變為了(155.99.25.11:62000->18.181.0.31:1235)。
一旦NAT新增了一個Session後,NAT會記住62000連接阜對應的是10.0.0.1的1234連接阜,以後從18.181.0.31傳送到62000連接阜的資料會被NAT自動的轉發到10.0.0.1上。(注意:這裡是說18.181.0.31傳送到62000連接阜的資料會被轉發,其他的IP傳送到這個連接阜的資料將被NAT拋棄)這樣Client A就與Server S1建立以了一個連接。

哈哈,上面的基礎知識可能很多人都知道了,那麼下面是關鍵的部分了。
看看下面的情況:
Server S1 Server S2
18.181.0.31:1235 138.76.29.7:1235
| |
| |
+----------------------+----------------------+
|
^ Session 1 (A-S1) ^ | ^ Session 2 (A-S2) ^
| 18.181.0.31:1235 | | | 138.76.29.7:1235 |
v 155.99.25.11:62000 v | v 155.99.25.11:62000 v
|
Cone NAT
155.99.25.11
|
^ Session 1 (A-S1) ^ | ^ Session 2 (A-S2) ^
| 18.181.0.31:1235 | | | 138.76.29.7:1235 |
v 10.0.0.1:1234 v | v 10.0.0.1:1234 v
|
Client A
10.0.0.1:1234
接上面的例子,如果Client A的原來那個Socket(綁定了1234連接阜的那個UDP Socket)又接著向另外一個Server S2傳送了一個UDP包,那麼這個UDP包在通過NAT時會怎麼樣呢?
__________________
http://bbsimg.qianlong.com/upload/01/08/29/68/1082968_1136014649812.gif
psac 目前離線  
送花文章: 3, 收花文章: 1625 篇, 收花: 3196 次
舊 2005-06-15, 05:22 AM   #2 (permalink)
榮譽會員
 
psac 的頭像
榮譽勳章
UID - 3662
在線等級: 級別:30 | 在線時長:1048小時 | 升級還需:37小時級別:30 | 在線時長:1048小時 | 升級還需:37小時級別:30 | 在線時長:1048小時 | 升級還需:37小時級別:30 | 在線時長:1048小時 | 升級還需:37小時級別:30 | 在線時長:1048小時 | 升級還需:37小時
註冊日期: 2002-12-07
住址: 木柵市立動物園
文章: 17381
現金: 5253 金幣
資產: 33853 金幣
預設

這時可能會有兩種情況發生,一種是NAT再次新增一個Session,並且再次為這個Session分配一個連接阜號(比如:62001)。

另外一種是NAT再次新增一個Session,但是不會新分配一個連接阜號,而是用原來分配的連接阜號62000。前一種NAT叫做Symmetric NAT,後一種叫做Cone NAT。我們期望我們的NAT是第二種,哈哈,如果你的NAT剛好是第一種,那麼很可能會有很多P2P軟體失靈。(可以慶幸的是,現在絕大多數的NAT屬於後者,即Cone NAT)

好了,我們看到,通過NAT,子網內的電腦向外連結是很容易的(NAT相當於透明的,子網內的和外網的電腦不用知道NAT的情況)。



但是如果外部的電腦想訪問子網內的電腦就比較困難了(而這正是P2P所需要的)。
那麼我們如果想從外部傳送一個資料報給局內網的電腦有什麼辦法呢?首先,我們必須在局內網的NAT上打上一個「洞」(也就是前面我們說的在NAT上建立一個Session),這個洞不能由外部來打,只能由局內網內的主機來打。


而且這個洞是有方向的,比如從內部某台主機(比如:192.168.0.10)向外部的某個IP(比如:219.237.60.1)傳送一個UDP包,那麼就在這個局內網的NAT設備上打了一個方向為219.237.60.1的「洞」,(這就是稱為UDP Hole Punching的技術)以後219.237.60.1就可以通過這個洞與局內網的192.168.0.10聯係了。(但是其他的IP不能利用這個洞)。

哈哈,現在該輪到我們的正題P2P了。


有了上面的理論,實現兩個局內網的主機通訊就差最後一步了:那就是雞生蛋還是蛋生雞的問題了,兩邊都無法主動發出連接請求,誰也不知道誰的公共外網位址,那我們如何來打這個洞呢?我們需要一個中間人來聯係這兩個局內網主機。
現在我們來看看一個P2P軟體的流程,以下圖為例:

Server S (219.237.60.1)
|
|
+----------------------+----------------------+
| |
NAT A (外網IP:202.187.45.3) NAT B (外網IP:187.34.1.56)
| (局內網IP:192.168.0.1) | (局內網IP:192.168.0.1)
| |
Client A (192.168.0.20:4000) Client B (192.168.0.10:40000)

首先,Client A登入伺服器,NAT A為這次的Session分配了一個連接阜60000,那麼Server S收到的Client A的位址是202.187.45.3:60000,這就是Client A的外網位址了。同樣,Client B登入Server S,NAT B給此次Session分配的連接阜是40000,那麼Server S收到的B的位址是187.34.1.56:40000。
psac 目前離線  
送花文章: 3, 收花文章: 1625 篇, 收花: 3196 次
舊 2005-06-15, 05:23 AM   #3 (permalink)
榮譽會員
 
psac 的頭像
榮譽勳章
UID - 3662
在線等級: 級別:30 | 在線時長:1048小時 | 升級還需:37小時級別:30 | 在線時長:1048小時 | 升級還需:37小時級別:30 | 在線時長:1048小時 | 升級還需:37小時級別:30 | 在線時長:1048小時 | 升級還需:37小時級別:30 | 在線時長:1048小時 | 升級還需:37小時
註冊日期: 2002-12-07
住址: 木柵市立動物園
文章: 17381
現金: 5253 金幣
資產: 33853 金幣
預設

此時,Client A與Client B都可以與Server S通信了。如果Client A此時想直接傳送訊息給Client B,那麼他可以從Server S那兒獲得B的公共外網位址187.34.1.56:40000,是不是Client A向這個位址傳送訊息Client B就能收到了呢?

答案是不行,因為如果這樣傳送訊息,NAT B會將這個訊息丟棄(因為這樣的訊息是不請自來的,為了安全,大多數NAT都會執行丟棄動作)。現在我們需要的是在NAT B上打一個方向為202.187.45.3(即Client A的外網位址)的洞,那麼Client A傳送到187.34.1.56:40000的訊息,Client B就能收到了。這個打洞指令由誰來發呢,哈哈,當然是Server S。


總結一下這個程序:如果Client A想向Client B傳送訊息,那麼Client A傳送指令給Server S,請求Server S指令Client B向Client A方向打洞。

哈哈,是不是很繞口,不過沒關係,想一想就很清楚了,何況還有來源碼呢(侯老師說過:在來源碼面前沒有秘密 8)),然後Client A就可以通過Client B的外網位址與Client B通信了。

注意:以上程序只適合於Cone NAT的情況,如果是Symmetric NAT,那麼當Client B向Client A打洞的連接阜已經重新分配了,Client B將無法知道這個連接阜(如果Symmetric NAT的連接阜是順序分配的,那麼我們或許可以猜測這個連接阜號,可是由於可能導致失敗的因素太多,我們不推薦這種猜測連接阜的方法)。

下面是一個模擬P2P聊天的程序的來源碼,程序很簡單,P2PServer執行在一個擁有公共外網IP的電腦上,P2PClient執行在兩個不同的NAT後(注意,如果兩個客戶端執行在一個NAT後,本程序很可能不能執行正常,這取決於你的NAT是否支持loopback translation,詳見http://midcom-p2p.sourceforge.net/draft-ford-midcom-p2p-01.txt,當然,此問題可以通過雙方先嘗試連接對方的局內網IP來解決,但是這個程式碼只是為了驗證原理,並沒有處理這些問題),後登入的電腦可以獲得先登入電腦的用戶名,後登入的電腦通過send username message的格式來傳送消息。

如果傳送成功,說明你已取得了直接與對方連接的成功。
程序現在支持三個指令:send , getu , exit

send格式:send username message
功能:傳送訊息給username

getu格式:getu
功能:獲得當前伺服器用戶列表

exit格式:exit
功能:註銷與伺服器的連接(伺服器不會自動監測客戶是否吊線)

程式碼很短,相信很容易懂,如果有什麼問題,可以給我發郵件zhouhuis22@sina.com 或者在CSDN上傳送短消息。同時,歡迎轉發此文,但希望保留作者版權8-)。

最後感謝CSDN網友 PiggyXP 和 Seilfer的測試說明
psac 目前離線  
送花文章: 3, 收花文章: 1625 篇, 收花: 3196 次
舊 2005-06-15, 05:24 AM   #4 (permalink)
榮譽會員
 
psac 的頭像
榮譽勳章
UID - 3662
在線等級: 級別:30 | 在線時長:1048小時 | 升級還需:37小時級別:30 | 在線時長:1048小時 | 升級還需:37小時級別:30 | 在線時長:1048小時 | 升級還需:37小時級別:30 | 在線時長:1048小時 | 升級還需:37小時級別:30 | 在線時長:1048小時 | 升級還需:37小時
註冊日期: 2002-12-07
住址: 木柵市立動物園
文章: 17381
現金: 5253 金幣
資產: 33853 金幣
預設

P2PServer.c

/* P2P 程序服務端
*
* 檔案名:P2PServer.c
*
* 日期:2004-5-21
*
* 作者:shootingstars(zhouhuis22@sina.com)
*
*/
#pragma comment(lib, "ws2_32.lib")

#include "windows.h"
#include "..\proto.h"
#include "..\Exception.h"

UserList ClientList;

void InitWinSock()
{
WSADATA wsaData;

if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
{
printf("Windows sockets 2.2 startup");
throw Exception("");
}
else{
printf("Using %s (Status: %s)\n",
wsaData.szDescription, wsaData.szSystemStatus);
printf("with API versions %d.%d to %d.%d\n\n",
LOBYTE(wsaData.wVersion), HIBYTE(wsaData.wVersion),
LOBYTE(wsaData.wHighVersion), HIBYTE(wsaData.wHighVersion));

}
}

SOCKET mksock(int type)
{
SOCKET sock = socket(AF_INET, type, 0);
if (sock < 0)
{
printf("create socket error");
throw Exception("");
}
return sock;
}

stUserListNode GetUser(char *username)
{
for(UserList::iterator UserIterator=ClientList.begin();
UserIterator!=ClientList.end();
++UserIterator)
{
if( strcmp( ((*UserIterator)->userName), username) == 0 )
return *(*UserIterator);
}
throw Exception("not find this user");
}

int main(int argc, char* argv[])
{
try{
InitWinSock();

SOCKET PrimaryUDP;
PrimaryUDP = mksock(SOCK_DGRAM);

sockaddr_in local;
local.sin_family=AF_INET;
local.sin_port= htons(SERVER_PORT);
local.sin_addr.s_addr = htonl(INADDR_ANY);
int nResult=bind(PrimaryUDP,(sockaddr*)&local,sizeof(sockaddr));
if(nResult==SOCKET_ERROR)
throw Exception("bind error");

sockaddr_in sender;
stMessage recvbuf;
memset(&recvbuf,0,sizeof(stMessage));

// 開始主循環.
// 主循環負責下面幾件事情:
// 一:讀取客戶端登入和登出消息,記錄客戶列表
// 二:轉發客戶p2p請求
for(;
{
int dwSender = sizeof(sender);
int ret = recvfrom(PrimaryUDP, (char *)&recvbuf, sizeof(stMessage), 0, (sockaddr *)&sender, &dwSender);
if(ret <= 0)
{
printf("recv error");
continue;
}
else
{
psac 目前離線  
送花文章: 3, 收花文章: 1625 篇, 收花: 3196 次
舊 2005-06-15, 05:26 AM   #5 (permalink)
榮譽會員
 
psac 的頭像
榮譽勳章
UID - 3662
在線等級: 級別:30 | 在線時長:1048小時 | 升級還需:37小時級別:30 | 在線時長:1048小時 | 升級還需:37小時級別:30 | 在線時長:1048小時 | 升級還需:37小時級別:30 | 在線時長:1048小時 | 升級還需:37小時級別:30 | 在線時長:1048小時 | 升級還需:37小時
註冊日期: 2002-12-07
住址: 木柵市立動物園
文章: 17381
現金: 5253 金幣
資產: 33853 金幣
預設

int messageType = recvbuf.iMessageType;
switch(messageType){
case LOGIN:
{
// 將這個用戶的訊息記錄到用戶列表中
printf("has a user login : %s\n", recvbuf.message.loginmember.userName);
stUserListNode *currentuser = new stUserListNode();
strcpy(currentuser->userName, recvbuf.message.loginmember.userName);
currentuser->ip = ntohl(sender.sin_addr.S_un.S_addr);
currentuser->port = ntohs(sender.sin_port);

ClientList.push_back(currentuser);

// 傳送已經登入的客戶訊息
int nodecount = (int)ClientList.size();
sendto(PrimaryUDP, (const char*)&nodecount, sizeof(int), 0, (const sockaddr*)&sender, sizeof(sender));
for(UserList::iterator UserIterator=ClientList.begin();
UserIterator!=ClientList.end();
++UserIterator)
{
sendto(PrimaryUDP, (const char*)(*UserIterator), sizeof(stUserListNode), 0, (const sockaddr*)&sender, sizeof(sender));
}

break;
}
case LOGOUT:
{
// 將此客戶訊息移除
printf("has a user logout : %s\n", recvbuf.message.logoutmember.userName);
UserList::iterator removeiterator = NULL;
for(UserList::iterator UserIterator=ClientList.begin();
UserIterator!=ClientList.end();
++UserIterator)
{
if( strcmp( ((*UserIterator)->userName), recvbuf.message.logoutmember.userName) == 0 )
{
removeiterator = UserIterator;
break;
}
}
if(removeiterator != NULL)
ClientList.remove(*removeiterator);
break;
}
case P2PTRANS:
{
// 某個客戶希望服務端向另外一個客戶傳送一個打洞消息
printf("%s wants to p2p %s\n",inet_ntoa(sender.sin_addr),recvbuf.message.translatemessage.userName);
stUserListNode node = GetUser(recvbuf.message.translatemessage.userName);
sockaddr_in remote;
remote.sin_family=AF_INET;
remote.sin_port= htons(node.port);
remote.sin_addr.s_addr = htonl(node.ip);

in_addr tmp;
tmp.S_un.S_addr = htonl(node.ip);
printf("the address is %s,and port is %d\n",inet_ntoa(tmp), node.port);

stP2PMessage transMessage;
transMessage.iMessageType = P2PSOMEONEWANTTOCALLYOU;
transMessage.iStringLen = ntohl(sender.sin_addr.S_un.S_addr);
transMessage.Port = ntohs(sender.sin_port);

sendto(PrimaryUDP,(const char*)&transMessage, sizeof(transMessage), 0, (const sockaddr *)&remote, sizeof(remote));

break;
}

case GETALLUSER:
{
int command = GETALLUSER;
sendto(PrimaryUDP, (const char*)&command, sizeof(int), 0, (const sockaddr*)&sender, sizeof(sender));

int nodecount = (int)ClientList.size();
sendto(PrimaryUDP, (const char*)&nodecount, sizeof(int), 0, (const sockaddr*)&sender, sizeof(sender));

for(UserList::iterator UserIterator=ClientList.begin();
UserIterator!=ClientList.end();
++UserIterator)
{
sendto(PrimaryUDP, (const char*)(*UserIterator), sizeof(stUserListNode), 0, (const sockaddr*)&sender, sizeof(sender));
}
break;
}
}
}
}

}
catch(Exception &e)
{
printf(e.GetMessage());
return 1;
}

return 0;
}


/* P2P 程序客戶端
*
* 檔案名:P2PClient.c
*
* 日期:2004-5-21
*
* 作者:shootingstars(zhouhuis22@sina.com)
*
*/

#pragma comment(lib,"ws2_32.lib")

#include "windows.h"
#include "..\proto.h"
#include "..\Exception.h"
#include <iostream>
using namespace std;

UserList ClientList;



#define COMMANDMAXC 256
#define MAXRETRY 5

SOCKET PrimaryUDP;
char UserName[10];
char ServerIP[20];

bool RecvedACK;

void InitWinSock()
{
WSADATA wsaData;

if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
{
printf("Windows sockets 2.2 startup");
throw Exception("");
}
else{
printf("Using %s (Status: %s)\n",
wsaData.szDescription, wsaData.szSystemStatus);
printf("with API versions %d.%d to %d.%d\n\n",
LOBYTE(wsaData.wVersion), HIBYTE(wsaData.wVersion),
LOBYTE(wsaData.wHighVersion), HIBYTE(wsaData.wHighVersion));
}
}

SOCKET mksock(int type)
{
SOCKET sock = socket(AF_INET, type, 0);
if (sock < 0)
{
printf("create socket error");
throw Exception("");
}
return sock;
}

stUserListNode GetUser(char *username)
{
for(UserList::iterator UserIterator=ClientList.begin();
UserIterator!=ClientList.end();
++UserIterator)
{
if( strcmp( ((*UserIterator)->userName), username) == 0 )
return *(*UserIterator);
}
throw Exception("not find this user");
}

void BindSock(SOCKET sock)
{
sockaddr_in sin;
sin.sin_addr.S_un.S_addr = INADDR_ANY;
sin.sin_family = AF_INET;
sin.sin_port = 0;

if (bind(sock, (struct sockaddr*)&sin, sizeof(sin)) < 0)
throw Exception("bind error");
}

void ConnectToServer(SOCKET sock,char *username, char *serverip)
{
sockaddr_in remote;
remote.sin_addr.S_un.S_addr = inet_addr(serverip);
remote.sin_family = AF_INET;
remote.sin_port = htons(SERVER_PORT);

stMessage sendbuf;
sendbuf.iMessageType = LOGIN;
strncpy(sendbuf.message.loginmember.userName, username, 10);

sendto(sock, (const char*)&sendbuf, sizeof(sendbuf), 0, (const sockaddr*)&remote,sizeof(remote));

int usercount;
int fromlen = sizeof(remote);
int iread = recvfrom(sock, (char *)&usercount, sizeof(int), 0, (sockaddr *)&remote, &fromlen);
if(iread<=0)
{
throw Exception("Login error\n");
}
psac 目前離線  
送花文章: 3, 收花文章: 1625 篇, 收花: 3196 次
舊 2005-06-15, 05:27 AM   #6 (permalink)
榮譽會員
 
psac 的頭像
榮譽勳章
UID - 3662
在線等級: 級別:30 | 在線時長:1048小時 | 升級還需:37小時級別:30 | 在線時長:1048小時 | 升級還需:37小時級別:30 | 在線時長:1048小時 | 升級還需:37小時級別:30 | 在線時長:1048小時 | 升級還需:37小時級別:30 | 在線時長:1048小時 | 升級還需:37小時
註冊日期: 2002-12-07
住址: 木柵市立動物園
文章: 17381
現金: 5253 金幣
資產: 33853 金幣
預設

// 登入到服務端後,接收服務端發來的已經登入的用戶的訊息
cout<<"Have "<<usercount<<" users logined server:"<<endl;
for(int i = 0;i<usercount;i++)
{
stUserListNode *node = new stUserListNode;
recvfrom(sock, (char*)node, sizeof(stUserListNode), 0, (sockaddr *)&remote, &fromlen);
ClientList.push_back(node);
cout<<"Username:"<<node->userName<<endl;
in_addr tmp;
tmp.S_un.S_addr = htonl(node->ip);
cout<<"UserIP:"<<inet_ntoa(tmp)<<endl;
cout<<"UserPort:"<<node->port<<endl;
cout<<""<<endl;
}
}

void OutputUsage()
{
cout<<"You can input you command:\n"
<<"Command Type:\"send\",\"exit\",\"getu\"\n"
<<"Example : send Username Message\n"
<<" exit\n"
<<" getu\n"
<<endl;
}

/* 這是主要的函數:傳送一個消息給某個用戶(C)
*流程:直接向某個用戶的外網IP傳送消息,如果此前沒有聯係過
* 那麼此消息將無法傳送,傳送端等待超時。
* 超時後,傳送端將傳送一個請求訊息到服務端,
* 要求服務端傳送給客戶C一個請求,請求C給本地機傳送打洞消息
* 以上流程將重複MAXRETRY次
*/
bool SendMessageTo(char *UserName, char *Message)
{
char realmessage[256];
unsigned int UserIP;
unsigned short UserPort;
bool FindUser = false;
for(UserList::iterator UserIterator=ClientList.begin();
UserIterator!=ClientList.end();
++UserIterator)
{
if( strcmp( ((*UserIterator)->userName), UserName) == 0 )
{
UserIP = (*UserIterator)->ip;
UserPort = (*UserIterator)->port;
FindUser = true;
}
}

if(!FindUser)
return false;

strcpy(realmessage, Message);
for(int i=0;i<MAXRETRY;i++)
{
RecvedACK = false;

sockaddr_in remote;
remote.sin_addr.S_un.S_addr = htonl(UserIP);
remote.sin_family = AF_INET;
remote.sin_port = htons(UserPort);
stP2PMessage MessageHead;
MessageHead.iMessageType = P2PMESSAGE;
MessageHead.iStringLen = (int)strlen(realmessage)+1;
int isend = sendto(PrimaryUDP, (const char *)&MessageHead, sizeof(MessageHead), 0, (const sockaddr*)&remote, sizeof(remote));
isend = sendto(PrimaryUDP, (const char *)&realmessage, MessageHead.iStringLen, 0, (const sockaddr*)&remote, sizeof(remote));

// 等待接收執行緒將此標記修改
for(int j=0;j<10;j++)
{
if(RecvedACK)
return true;
else
Sleep(300);
}

// 沒有接收到目標主機的回應,認為目標主機的連接阜映射沒有
// 開啟,那麼傳送請求訊息給伺服器,要伺服器告訴目標主機
// 開啟映射連接阜(UDP打洞)
psac 目前離線  
送花文章: 3, 收花文章: 1625 篇, 收花: 3196 次
舊 2005-06-15, 05:28 AM   #7 (permalink)
榮譽會員
 
psac 的頭像
榮譽勳章
UID - 3662
在線等級: 級別:30 | 在線時長:1048小時 | 升級還需:37小時級別:30 | 在線時長:1048小時 | 升級還需:37小時級別:30 | 在線時長:1048小時 | 升級還需:37小時級別:30 | 在線時長:1048小時 | 升級還需:37小時級別:30 | 在線時長:1048小時 | 升級還需:37小時
註冊日期: 2002-12-07
住址: 木柵市立動物園
文章: 17381
現金: 5253 金幣
資產: 33853 金幣
預設

sockaddr_in server;
server.sin_addr.S_un.S_addr = inet_addr(ServerIP);
server.sin_family = AF_INET;
server.sin_port = htons(SERVER_PORT);

stMessage transMessage;
transMessage.iMessageType = P2PTRANS;
strcpy(transMessage.message.translatemessage.userName, UserName);

sendto(PrimaryUDP, (const char*)&transMessage, sizeof(transMessage), 0, (const sockaddr*)&server, sizeof(server));
Sleep(100);// 等待對方先傳送訊息。
}
return false;
}


// 解析指令,暫時只有exit和send指令
// 新增getu指令,獲取當前伺服器的所有用戶
void ParseCommand(char * CommandLine)
{
if(strlen(CommandLine)<4)
return;
char Command[10];
strncpy(Command, CommandLine, 4);
Command[4]='\0';

if(strcmp(Command,"exit")==0)
{
stMessage sendbuf;
sendbuf.iMessageType = LOGOUT;
strncpy(sendbuf.message.logoutmember.userName, UserName, 10);
sockaddr_in server;
server.sin_addr.S_un.S_addr = inet_addr(ServerIP);
server.sin_family = AF_INET;
server.sin_port = htons(SERVER_PORT);

sendto(PrimaryUDP,(const char*)&sendbuf, sizeof(sendbuf), 0, (const sockaddr *)&server, sizeof(server));
shutdown(PrimaryUDP, 2);
closesocket(PrimaryUDP);
exit(0);
}
else if(strcmp(Command,"send")==0)
{
char sendname[20];
char message[COMMANDMAXC];
int i;
for(i=5;;i++)
{
if(CommandLine[i]!=' ')
sendname[i-5]=CommandLine[i];
else
{
sendname[i-5]='\0';
break;
}
}
strcpy(message, &(CommandLine[i+1]));
if(SendMessageTo(sendname, message))
printf("Send OK!\n");
else
printf("Send Failure!\n");
}
else if(strcmp(Command,"getu")==0)
{
int command = GETALLUSER;
sockaddr_in server;
server.sin_addr.S_un.S_addr = inet_addr(ServerIP);
server.sin_family = AF_INET;
server.sin_port = htons(SERVER_PORT);

sendto(PrimaryUDP,(const char*)&command, sizeof(command), 0, (const sockaddr *)&server, sizeof(server));
}
}

// 接受消息執行緒
DWORD WINAPI RecvThreadProc(LPVOID lpParameter)
{
sockaddr_in remote;
int sinlen = sizeof(remote);
stP2PMessage recvbuf;
for(;
{
int iread = recvfrom(PrimaryUDP, (char *)&recvbuf, sizeof(recvbuf), 0, (sockaddr *)&remote, &sinlen);
if(iread<=0)
{
printf("recv error\n");
continue;
}
switch(recvbuf.iMessageType)
{
case P2PMESSAGE:
{
// 接收到P2P的消息
char *comemessage= new char[recvbuf.iStringLen];
int iread1 = recvfrom(PrimaryUDP, comemessage, 256, 0, (sockaddr *)&remote, &sinlen);
comemessage[iread1-1] = '\0';
if(iread1<=0)
throw Exception("Recv Message Error\n");
else
{
printf("Recv a Message:%s\n",comemessage);

stP2PMessage sendbuf;
sendbuf.iMessageType = P2PMESSAGEACK;
sendto(PrimaryUDP, (const char*)&sendbuf, sizeof(sendbuf), 0, (const sockaddr*)&remote, sizeof(remote));
}

delete []comemessage;
break;

}
case P2PSOMEONEWANTTOCALLYOU:
{
// 接收到打洞指令,向指定的IP位址打洞
printf("Recv p2someonewanttocallyou data\n");
sockaddr_in remote;
remote.sin_addr.S_un.S_addr = htonl(recvbuf.iStringLen);
remote.sin_family = AF_INET;
remote.sin_port = htons(recvbuf.Port);

// UDP hole punching
stP2PMessage message;
message.iMessageType = P2PTRASH;
sendto(PrimaryUDP, (const char *)&message, sizeof(message), 0, (const sockaddr*)&remote, sizeof(remote));
psac 目前離線  
送花文章: 3, 收花文章: 1625 篇, 收花: 3196 次
舊 2005-06-15, 05:35 AM   #8 (permalink)
榮譽會員
 
psac 的頭像
榮譽勳章
UID - 3662
在線等級: 級別:30 | 在線時長:1048小時 | 升級還需:37小時級別:30 | 在線時長:1048小時 | 升級還需:37小時級別:30 | 在線時長:1048小時 | 升級還需:37小時級別:30 | 在線時長:1048小時 | 升級還需:37小時級別:30 | 在線時長:1048小時 | 升級還需:37小時
註冊日期: 2002-12-07
住址: 木柵市立動物園
文章: 17381
現金: 5253 金幣
資產: 33853 金幣
預設

break;
}
case P2PMESSAGEACK:
{
// 傳送消息的回應
RecvedACK = true;
break;
}
case P2PTRASH:
{
// 對方傳送的打洞消息,忽略掉。
//do nothing ...
printf("Recv p2ptrash data\n");
break;
}
case GETALLUSER:
{
int usercount;
int fromlen = sizeof(remote);
int iread = recvfrom(PrimaryUDP, (char *)&usercount, sizeof(int), 0, (sockaddr *)&remote, &fromlen);
if(iread<=0)
{
throw Exception("Login error\n");
}

ClientList.clear();

cout<<"Have "<<usercount<<" users logined server:"<<endl;
for(int i = 0;i<usercount;i++)
{
stUserListNode *node = new stUserListNode;
recvfrom(PrimaryUDP, (char*)node, sizeof(stUserListNode), 0, (sockaddr *)&remote, &fromlen);
ClientList.push_back(node);
cout<<"Username:"<<node->userName<<endl;
in_addr tmp;
tmp.S_un.S_addr = htonl(node->ip);
cout<<"UserIP:"<<inet_ntoa(tmp)<<endl;
cout<<"UserPort:"<<node->port<<endl;
cout<<""<<endl;
}
break;
}
}
}
}


int main(int argc, char* argv[])
{
try
{
InitWinSock();

PrimaryUDP = mksock(SOCK_DGRAM);
BindSock(PrimaryUDP);

cout<<"Please input server ip:";
cin>>ServerIP;

cout<<"Please input your name:";
cin>>UserName;

ConnectToServer(PrimaryUDP, UserName, ServerIP);

HANDLE threadhandle = CreateThread(NULL, 0, RecvThreadProc, NULL, NULL, NULL);
CloseHandle(threadhandle);
OutputUsage();

for(;
{
char Command[COMMANDMAXC];
gets(Command);
ParseCommand(Command);
}
}
catch(Exception &e)
{
printf(e.GetMessage());
return 1;
}
return 0;
}


/* 異常類
*
* 檔案名:Exception.h
*
* 日期:2004.5.5
*
* 作者:shootingstars(zhouhuis22@sina.com)
*/

#ifndef __HZH_Exception__
#define __HZH_Exception__

#define EXCEPTION_MESSAGE_MAXLEN 256
#include "string.h"

class Exception
{
private:
char m_ExceptionMessage[EXCEPTION_MESSAGE_MAXLEN];
public:
Exception(char *msg)
{
strncpy(m_ExceptionMessage, msg, EXCEPTION_MESSAGE_MAXLEN);
}

char *GetMessage()
{
return m_ExceptionMessage;
}
};

#endif


/* P2P 程序傳輸傳輸協定
*
* 日期:2004-5-21
*
* 作者:shootingstars(zhouhuis22@sina.com)
*
*/

#pragma once
#include <list>

// 定義iMessageType的值
#define LOGIN 1
#define LOGOUT 2
#define P2PTRANS 3
#define GETALLUSER 4

// 伺服器連接阜
#define SERVER_PORT 2280

// Client登入時向伺服器傳送的消息
struct stLoginMessage
{
char userName[10];
char password[10];
};

// Client註銷時傳送的消息
struct stLogoutMessage
{
char userName[10];
};

// Client向伺服器請求另外一個Client(userName)向自己方向傳送UDP打洞消息
struct stP2PTranslate
{
char userName[10];
};

// Client向伺服器傳送的消息格式
struct stMessage
{
int iMessageType;
union _message
{
stLoginMessage loginmember;
stLogoutMessage logoutmember;
stP2PTranslate translatemessage;
}message;
};

// 客戶節點訊息
struct stUserListNode
{
char userName[10];
unsigned int ip;
unsigned short port;
};

// Server向Client傳送的消息
struct stServerToClient
{
int iMessageType;
union _message
{
stUserListNode user;
}message;

};

//======================================
// 下面的傳輸協定用於客戶端之間的通信
//======================================
#define P2PMESSAGE 100 // 傳送消息
#define P2PMESSAGEACK 101 // 收到消息的回應
#define P2PSOMEONEWANTTOCALLYOU 102 // 伺服器向客戶端傳送的消息
// 希望此客戶端傳送一個UDP打洞包
#define P2PTRASH 103 // 客戶端傳送的打洞包,接收端應該忽略此消息

// 客戶端之間傳送消息格式
struct stP2PMessage
{
int iMessageType;
int iStringLen; // or IP address
unsigned short Port;
};

using namespace std;
typedef list<stUserListNode *> UserList;




源碼下載:http://www.lihuasoft.net/source/show.php?id=3644
psac 目前離線  
送花文章: 3, 收花文章: 1625 篇, 收花: 3196 次
舊 2005-06-20, 10:10 AM   #9 (permalink)
lew
註冊會員
 
lew 的頭像
榮譽勳章
UID - 16444
在線等級: 級別:9 | 在線時長:122小時 | 升級還需:18小時級別:9 | 在線時長:122小時 | 升級還需:18小時級別:9 | 在線時長:122小時 | 升級還需:18小時級別:9 | 在線時長:122小時 | 升級還需:18小時
註冊日期: 2002-12-23
VIP期限: 2009-05
住址: WOMAN'S HOME
文章: 294
精華: 0
現金: 5909 金幣
資產: 10909 金幣
預設

COOL !!!
lew 目前離線  
送花文章: 135, 收花文章: 14 篇, 收花: 70 次
 


主題工具
顯示模式

發表規則
不可以發文
不可以回覆主題
不可以上傳附加檔案
不可以編輯您的文章

論壇啟用 BB 語法
論壇啟用 表情符號
論壇啟用 [IMG] 語法
論壇禁用 HTML 語法
Trackbacks are 禁用
Pingbacks are 禁用
Refbacks are 禁用


所有時間均為台北時間。現在的時間是 03:14 AM


Powered by vBulletin® 版本 3.6.8
版權所有 ©2000 - 2019, Jelsoft Enterprises Ltd.


SEO by vBSEO 3.6.1