《通信网络程序设计》课件第11章_第1页
《通信网络程序设计》课件第11章_第2页
《通信网络程序设计》课件第11章_第3页
《通信网络程序设计》课件第11章_第4页
《通信网络程序设计》课件第11章_第5页
已阅读5页,还剩106页未读 继续免费阅读

下载本文档

版权说明:本文档由用户提供并上传,收益归属内容提供方,若内容存在侵权,请进行举报或认领

文档简介

第11章P2P技术11.1P2P技术概述11.2NAT穿越11.3P2P编程小结

由于传统的C/S模式对服务器过于依赖,导致负载分布不平衡,以及性价比和扩展性都存在一定的问题,因此目前一种新的通信模式非常盛行,这就是P2P(Peer-to-Peer)技术。P2P技术通过客户端进行互联的方法,将通信任务的压力分散开来,可以大大优化文件下载、流媒体、即时通信、语音通信等多种网络应用,是网络通信程序方面的程序员必须掌握的一项关键技术。

本章首先介绍P2P的概念、分类和原理,然后讲解了将P2P技术运用于当前网络需解决的NAT的穿越问题,重点介绍了两种NAT穿越技术的打洞方法,继而进行了基于TCP打洞的P2P编程实例。掌握本章学习内容需紧密结合对TCP/IP协议的IP层工作机理的深入理解。

P2P正式步入发展在20世纪90年代末期,始于美国波士顿大学一年级新生肖恩·范宁编写的Napster音乐共享程序,现已彻底统治了当今的互联网。据统计,互联网中大约50%~90%的总流量都来自于P2P,具有非同一般的意义。11.1P2P技术概述11.1.1概念

P2P是英文“对等”的简称,又称为“点对点”。“对等”技术是一种网络新技术,依赖于网络中参与者的计算能力和带宽,而不是把依赖都聚集在较少的几台服务器上。简单地说,P2P直接将人们联系起来,让人们通过互联网直接交互,这使得网络上用户的沟通变得更容易,共享和交互也变得更直接,真正地消除了网络连接的中间环节。也就是说,P2P就是人们可以直接连接到其他用户的计算机,并交换文件,而不是像过去那样连接到服务器再浏览与下载(如图11-1所示)。同时,P2P也改变了互联网现在以P2P工作组大网站为中心的状态,重返“非中心化”状态,并把权力交还给了用户。

P2P不是一种新的协议,而是利用现有的网络协议实现网络数据或资源信息共享的技术,它使用的可能是TCP、UDP或其他协议。P2P技术与C/S模式相比具有许多优点。首先,P2P能够解决C/S模式中存在的服务器与客户机计算任务分配不均的问题,使负载均衡,降低服务器的压力。其次,P2P具有高性价比,能够利用闲置的计算能力或存储空间,达到高性能计算和海量存储的目的。另外,P2P可扩展性良好,由于不存在服务器端瓶颈,其扩展性大大加强。同时,也应当注意P2P带来了版权与安全性等问题。图11-1C/S模式与P2P模式结构比较目前,P2P有许多耳熟能详的著名应用,如文件下载的BitTorrent、迅雷、电驴;流媒体播放器PPLive;即时通信软件ICQ、MSN;语音网络通信软件Skype等。11.1.2原理

P2P的设计模式总体可以分为两类,即单纯型P2P和混合型P2P架构,其原理稍有不同。

单纯型P2P架构没有中央服务器,各个节点之间直接交互信息,如图11-1(b)所示,其优点是使用方便,任何一个安装了P2P应用软件的计算机,都可以与其他安装这个软件的计算机进行P2P通信。它的缺点是没有中央服务器参与协调,使用范围比较有限。

混合型P2P架构将P2P和C/S模式相结合。这种结构在单纯型的基础上引入中央服务器,这里的中央服务器不同于C/S模式中的服务器,仅起到促成各节点协调和扩展的功能。工作时安装了P2P软件的各个计算机首先登录中央服务器连接,告知服务器自己监听的IP地址和端口,然后由中央服务器建立索引,进而告知其他登录的计算机。登录后的各台计算机根据中央服务器提供的信息,并在中央服务器的帮助下与其他计算机建立P2P连接,实现数据共享或通信。每台计算机的连接和断开都要通过中央服务器通知所有登录的计算机。这种架构的优点是实现了文件查询和文件传输的分离,有效地节省了中央服务器的带宽消耗,减少了系统的文件传输延时;缺点是增加了对服务器的依赖性,中央服务器的瘫痪容易导致整个网络的崩溃。

P2P技术实现必须解决一大难题,就是NAT穿越的问题。当前许多计算机都隐藏在私有网络中,通常因安全原因,外网的计算机无法知道内网计算机上的P2P进程编号(含IP地址和端口号,见1.3.2节),更不允许直接连接内网的计算机。由前面介绍的P2P原理可知,这对P2P的实现是致命的。因此必须解决穿越进入私有网络的问题,其关键在于外部网络进程如何穿越网络地址转换(NetworkAddressTranslators,NAT)设备,与内网计算机上的进程建立全相关。11.2NAT穿越11.2.1NAT概念

NAT是在IP地址日益缺乏的情况下产生的,它的主要目的就是为了能够地址重用,其产生基于如下事实:一个私有网络(域)中的节点中只有很少的节点需要与外网连接,那么这个子网中其实只有少数的节点需要全球唯一的IP地址,其他节点的IP地址应该是可以重用的。

1.原理

NAT的核心功能就是进行地址转换。通常NAT设备有两个NIC,一个接入Internet,一个接入LAN(因此其拥有两个IP地址)。进行NAT转换可以分为数据向外和数据向内两种情况。一方面通过正确的地址转换保障从私有内网(简称内网)传出的数据可以路由到达外网终端上的网络进程,另一方面还要保障从外网终端上的网络进程返回的数据可以安全通过NAT到达内网终端上的网络进程,其转换过程如图11-2所示。图11-2NAT转换过程为了对各种地址加以区分,通常使用以下四种地址进行区分。

内部本地地址(InsideLocalIPaddress,IL)是指内网主机地址,图11-2中主机Tom的IL地址就是,Jerry的IL地址就是。IL地址在外部网络中是无法路由的或路由会将数据导向错误的主机。

内部全局地址(InsideGlobalIPaddress,IG)代表内部IP到外部网络可路由的合法IP,图11-2中NAT的IG地址就是1,这是从外部看子网,整个子网所呈现的地址,因此它代表整个内向的地址。外部本地地址(OutsideLocalIPaddress,OL)是内网主机所知的一台连接外部网络的主机IP,如图11-2中NAT的OL地址就是8,这是从内网主机向外网看去,整个外网所呈现的地址,因此它代表整个外围的地址。

外部全局地址(OutsideGlobalIPaddress,OG)是外部网络主机的合法IP,如图11-2中所示的服务器地址为1。以图11-2中的网络结构为例,进行NAT地址转换的两种情况描述如下:

(1)数据传出:假设终端Tom(IL地址为)上网络进程(端口号为1234)的一个数据包要透过NAT传出到外网WWW服务器(OG地址为1)的Web服务程序上(端口号为80)。在进行转换之前,NAT设备查找路由并验证数据包是否合乎规则。如通过验证,则将该数据包的源(Sur)地址()替换为NAT的IG地址(1),再随机安排一个NAT的端口号(如1222)替换Tom的端口号1234,记录该对应关系到地址转换表中(如表11-1中的记录1),从而这张转换表就将这台计算机的不可路由的IP地址及其端口号与路由器(这里是NAT)的IP地址绑定起来了。接下来,就可以发送数据包到外网,等待响应。表11-1NAT地址转换表

(2)数据传入:当一个数据包从目的服务器(1)发送回来时,假设它的目的(Des)主机和端口号组合为1:1222,根据NAT地址转换表的记录1就可以确定目的计算机的IL地址和端口号,其组合为:1234,则替换该数据包的Des地址和端口号,然后发送至那台计算机。可见,NAT地址表中是否有对应的记录是外网数据能否进入内网的关键,利用地址转换的工作原理可以实现NAT的穿越。

注意,在进行IP地址和端口号替换后,需要重新计算数据包头校验和,否则数据包会被丢弃。

2.分类

NAT的分类方法很多,按照发展过程可分为基本NAT和NAPT(NetworkAddress/PortTranslator)两类。

最开始,NAT是运行在路由器上的一个功能模块,并且首先出现的是基本NAT。基本NAT实现的功能很简单。在子网内使用一个保留的IP子网段,这些IP对外是不可见的,而子网内只有少数一些IP地址可以对应到真正全球唯一的IP地址。如果这些节点需要访问外部网络,那么基本NAT就负责将这个节点的子网内IP转化为一个全球唯一的IP并发送出去(基本的NAT会改变IP包中的原IP地址,但是不会改变IP包中的端口)。

NAPT比NAT稍微复杂一些,它不但会改变经过这个NAT设备的IP数据包的IP地址,还会改变IP数据包的TCP/UDP端口。目前NAPT是主流的NAT技术,前面对图11-2的说明也是基于NAPT的。

3.转发

显然,根据前面对NAT工作原理的介绍,当地址转换表中不存在一个外网的地址时,则以这个地址为Des地址的数据包将会被丢弃,这就给P2P技术带来致命问题。如图11-3所示的结构中,客户A想要直接建立与客户B的连接就会因为NAT的阻隔而失败。

为了解决这一问题,可以采用NAT转发的方法加以解决。也就是客户A、B以服务器S为中间人,转发数据。A与S、B与S分别建立会话,由S对接收到的数据进行地址替换,实现间接的A、B之间的通信,如图11-3所示。显然这样会大大增加S的负担,在实际的网络中几乎不可行。

图11-3NAT转发

4.反向连接

还有一种解决外网计算机与内网计算机的连接方法就是反向连接。这种方法的应用条件比较特殊,即两台需要连接的计算机只有一台在NAT后面,如图11-4所示。

图11-4NAT反向连接进行反向连接的过程分以下三步:

(1)客户B向中央服务器S发送与客户A的连接请求;

(2)服务器S中继该连接请求到客户A;

(3)客户A发起连接,与客户B建立连接。

反向连接利用服务器S与客户A之间已经建立的合法通信(地址转换关系已经在客户A登录时就已经插入NAT的地址转换表),以及客户A可以自由连接一台具有OG地址的主机来实现P2P。但是由于反向连接的条件过于特殊,因此使用不是非常广泛。

5.NAT穿越

目前解决NAT穿越主要是巧妙地利用TCP/IP的TCP和UDP协议,使用称为“打洞”(holepunching)的技术。具体方法是:应用程序向NAT外的服务器发送请求连接其他应用程序的消息,收到请求消息后产生响应消息,通知连接方和被连接方对方的IL、IG地址及对应端口信息。利用得到的地址和端口信息,UDP和TCP各自采用不同的方法在NAT上建立地址转换记录,从而实现对方连接的数据包可以穿越NAT而进入内网。这时就可以建立不同NAT后计算机上应用程序的连接了。

UDP打洞(见11.2.2节)与TCP打洞(见11.2.3节)稍有区别。上述这种NAT穿越方法最大的优点是无需现有NAT设备做任何改动,而且可以适用于多级NAT的网络结构,是当前NAT穿越采用的主流技术。11.2.2UDP打洞

UDP打洞利用UPD协议,在中央服务器的协调下实现NAT穿越。

假设在如图11-5所示的网络结构上实现UDP穿越。这个结构中存在两台计算机客户A和客户B,它们都拥有自己的私有IP地址,并且都处在不同的NAT之后。相同的P2P程序分别运行于A和B上。在公网上存在一台中央服务器S,并且对它们都开放了UDP端口1234。A和B首先分别与S建立通信会话,这时NATA把它自己的UDP端口62000分配给A与S的会话,NATB也把自己的UDP端口31000分配给B与S的会话。在建立通话的过程中,S通过数据包的源地址和内容已经了解了A和B的IL地址和各自的NATIG地址及对应的端口号。

图11-5UDP打洞过程

A希望与B建立端对端的连接,其UDP打洞分为以下三步:

(1)

A开始发送一个UDP信息到S,并提出连接B的请求;

(2)S收到请求后,向A和B分别发出对方的IL地址和各自的NATIG地址以及对应的端口号,即A得到B的IL地址及端口是:4321,B的IG地址及端口是:31000;B得到A的IL地址及端口是:4321,A的IG地址及端口是1:62000。

(3)在得到对方的地址后,A和B发出两个UDP连接,一个连接对方的IG地址和端口号,一个直接指向对方的IL地址和端口号的连接。这时A向B的IG地址(:31000)发送的信息导致NATA增加一条1:62000与:31000的地址转换记录,同样,B向A的IG地址(1:62000)发送的信息导致NATA增加一条:31000与1:62000的地址转换记录。此时会有两种情况发生:一种是A连接B的IG地址和端口号先于B连接A的IG地址和端口号到达NATB,但由于NATB上的地址转换记录还没有建立,因此A连接B的IG地址数据包会被丢弃;另一种是A连接B的IG地址和端口号晚于B连接A的IG地址和端口号到达NATB,这时NATB上的地址转换记录已经建立,因此A连接B的IG地址数据包会被转发给B(依据地址映射上的对应关系)。无论哪一种情况发生,最终总是有一个NAT被穿越,那么A和B之间就可以直接进行通信而无需S的协助了。

之所以在步骤(3)中还要发送一个直接指向对方的IL地址和端口号的连接,就是考虑到A与B同处于一个NAT,则这个连接一定会比由NAT“发卡”式转发要有更高的效率。11.2.3TCP打洞

TCP打洞利用TCP协议,在中央服务器的协调下实现NAT穿越。同样采用11.2.2节的网络结构实现TCP打洞,A希望与B建立端对端的连接,分为三个步骤,如图11-6所示。图11-6TCP打洞过程

(1)客户A和B分别登录服务器S,A可以通过S得到B的IL和IG地址,B也可以通过S得到A的IL和IG地址,此时A与B相互发送的消息会被NAT所丢弃。

(2)为了完成TCP打洞,穿越NAT,A会打开一个侦听套接字,接收来自B的信息,再打开一个套接字并向B的IL地址发一个SYN包(两个套接字使用端口复用);B打开一个侦听套接字,接收来自A的信息,也可以再打开一个套接字向A的公网地址发送SYN包(两个套接字同样使用端口复用)。

(3)双方经过三次握手(见6.3.4节)建立TCP直连。

在TCP打洞的过程中可能因操作系统的工作机制不同而导致截然不同的连接实现。

假设A的第一个SYN包到B的公网地址后被B的NAT丢弃,但是B的第一个SYN包到A的公网地址后通过A,在A再次发送一个SYN包,根据操作系统的差异可能发生以下两种情况。

情况一:A连接B的套接字注意到这个到达的SYN包与刚发向B的SYN包所对应的会话匹配。A的TCP栈因此联系这个SYN包到刚刚A试图连接到B的公共地址的那个套接字上。因此Connect同步成功,而侦听的那个套接字上什么也没有发生。由于收到的SYN包不包含A先前发送的SYN的ACK,A的TCP栈重发给B的公共地址一个带SYN-ACK的包,ISN部分只是原来ISN的重复(使用相同的序号),一旦B收到A的SYN-ACK包,它会响应A的SYN包的ACK,则TCP会话完成三次握手而进入连接状态。

情况二:与情况一不同,A的活动状态的Listen套接字注意到B发出的SYN包,由于该包看起来像是一个对A的连接尝试,A的TCP栈会使用Accept()函数,返回值为这个新的TCP会话产生一个新的流套接字。与情况一类似,A用一个SYN-ACK包进行响应,则这个TCP连接的启动过程就像是一个平常的C/S风格,TCP连接顺利建立。由于A先前对B的connect()使用了当前的源地址到目的地址的组合,而这个组合又被上面的套接字再次使用,则A的这个Connect()必定失败,产生一个典型的“地址已使用”错误。但是,应用仍然会工作在前面已建立连接的流套接字,这个错误会被忽略,TCP连接还是被建立起来了。

前一种情况通常出现在基于BSD的操作系统,后一种多出现在Linux和Windows操作系统上。

如前所述,TCP打洞的一个重要条件就是要将两个不同的套接字绑定到一个端口上,因此必须采用端口复用技术。由于传统的标准伯克利(Berkeley)网卡要么用来连接主动建立对外连接,要么使用Listen()和Accept()被动建立来自外部的连接,不能提供类似UDP那样的同一端口既可以向外连接又能接受来自外部的连接,即仅允许建立一对一的响应,应用程序在将一个套接字绑定到本地一个端口以后,任何试图将第二个套接字绑定到该端口的操作都会失败。而为了实现TCP打洞就必须使用一个本地TCP端口来监听来自外部的TCP连接,同时建立多个向外连接的TCP连接,这就需要使用setsockopt()函数进行端口复用设置,实现代码如下:

Boolval;

Socketsock;

Setsockopt(sock,SOL_SOCKET,SO_REUSEADDR,(char*)&val,sizeof(val));

Winsock2对端口复用技术提供很好的支持,这也是其特色之一。

基于混合型P2P架构和UDP协议的基本P2P连接功能,总体软件可分为P2P协议、服务端和客户端三部分(这与传统的C/S意义完全不同)。程序设计前有如下约定:11.3P2P编程

①P2PServer运行在一个拥有公网IP的计算机上,P2PClient运行在两个不同的NAT后;②后登录的计算机可以获得先登录计算机的用户名,后登录的计算机通过“sendusernamemessage”的格式来发送消息,如果发送成功,说明已取得了直接与对方连接的成功;③程序使用了三个命令,即send、getu、exit,其中,send代表发送信息给用户,getu代表获得当前服务器用户列表,exit代表客户端注销与服务器的连接。11.3.1P2P协议程序

P2P软件必须商定统一的协议,否则各个端点将无法协调。协议需要规定命令、信息协议、消息类型、消息格式。本节协议将这些信息分为客户端与服务器间的通信信息协议和客户端间的通信信息协议两类实现。

//protocol.h

#pragmaonce

#include<list>

//定义iMessageType的值

#defineLOGIN 1

#defineLOGOUT 2

#defineP2PTRANS 3

#defineGETALLUSER 4

#defineSERVER_PORT60000 //服务器端口

//======================================

//客户端与服务器间通信信息协议

//======================================

structstLoginMessage //客户端登录服务器发送的消息

{

charuserName[10];

charpassword[10];

};

structstLogoutMessage //客户端注销时发送的消息

{

charuserName[10];

};

structstP2PTranslate //客户端向服务器请求另外一个Client向UDP发送打洞消息

{

charuserName[10];

};

structstMessage //客户端向服务器发送的消息格式

{

intiMessageType;

union_message

{

stLoginMessageloginmember;

stLogoutMessagelogoutmember;

stP2PTranslatetranslatemessage;

}message;

};

structstUserListNode //客户节点信息

{

charuserName[10];

unsignedintip;

unsignedshortport;

};

structstServerToClient //服务端向客户端发送的消息

{

intiMessageType;

union_message

{

stUserListNodeuser;

}message;

};

//======================================

//客户端间通信信息协议

//======================================

#defineP2PMESSAGE 100 //发送消息

#defineP2PMESSAGEACK 101 //收到消息的应答

#defineP2PSOMEONEWANTTOCALLYOU 102 //服务器向客户端发送的消息,希望它发送

//UDP打洞包

#defineP2PTRASH 103 //客户端发送的打洞包,接收端应该

//忽略此消息

structstP2PMessage //客户端之间发送消息格式

{

intiMessageType; //信息类型

intiStringLen; //地址信息

unsignedshortPort; //端口信息

};

usingnamespacestd;

typedeflist<stUserListNode*>UserList;

在上述协议文件中,使用了UserList来记录用户的信息。客户端与服务器端均需遵循上述协议,具体方法是通过#include"protocol.h"引入这个协议头文件,并在通信过程中遵循它规定的信息格式来实现。11.3.2服务器端程序

服务器端程序主要完成的任务包括三项:检测在线用户情况、维护用户列表、转达客户方打洞的请求。主要工作由main()函数的一个for循环完成,分别对四种客户请求进行不同的响应。

//P2PServer.cpp

#include"windows.h"

#include"protocol.h"

#include“winsock2.h”

#pragmacomment(lib,"ws2_32.lib")

UserListClientList;

DWORDStartSock(){//同6.3.2节,略}

stUserListNodeGetUser(char*username)//检索所需用户信息

{

for(UserList::iteratorUserIterator=ClientList.begin();UserIterator!=ClientList.end();++UserIterator)

if(strcmp(((*UserIterator)->userName),username)==0)return*(*UserIterator);

stUserListNodeNULLnode={"absent",0,0};//如果未找到则返回一个空节点信息

returnNULLnode;

}

intmain(intargc,char*argv[])

{

StartSock();

SOCKETPrimaryUDP;

PrimaryUDP=socket(AF_INET,SOCK_DGRAM,0);

sockaddr_inlocal;

local.sin_family=AF_INET;

local.sin_port=htons(SERVER_PORT);

local.sin_addr.s_addr=htonl(INADDR_ANY);

intnResult=bind(PrimaryUDP,(sockaddr*)&local,sizeof(sockaddr));

if(nResult==SOCKET_ERROR)

{

printf("binderror!\n");

return0;

}

sockaddr_insender;

stMessagerecvbuf;

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

for(;;)//主循环

{

intdwSender=sizeof(sockaddr_in);

intret=recvfrom(PrimaryUDP,(char*)&recvbuf,sizeof(stMessage),0,

(sockaddr*)&sender,&dwSender);

if(ret<=0)

{

printf("recverror");

continue;

}

else

{

intmessageType=recvbuf.iMessageType;

switch(messageType){

caseLOGIN: //将这个用户的信息记录到用户列表中

{

printf("hasauserlogin:%s\n",recvbuf.message.loginmember.userName);

stUserListNode*currentuser=newstUserListNode();

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);

//发送已经登录的客户信息

intnodecount=(int)ClientList.size();

sendto(PrimaryUDP,(constchar*)&nodecount,sizeof(int),0,

(constsockaddr*)&sender,sizeof(sender));

for(UserList::iteratorUserIterator=ClientList.begin();

UserIterator!=ClientList.end();++UserIterator)

{

sendto(PrimaryUDP,(constchar*)(*UserIterator),sizeof(stUserListNode),

0,(constsockaddr*)&sender,sizeof(sender));

}

break;

}

caseLOGOUT: //将此客户信息删除

{

printf("hasauserlogout:%s\n",recvbuf.message.logoutmember.userName);

UserList::iteratorremoveiterator=NULL;

for(UserList::iteratorUserIterator=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;

}

caseP2PTRANS://某个客户希望服务端向另外一个客户发送一个打洞消息

{

printf("%swantstop2p%s\n",inet_ntoa(sender.sin_addr),

recvbuf.message.translatemessage.userName);

stUserListNodenode=GetUser(recvbuf.message.translatemessage.userName);

sockaddr_inremote;

remote.sin_family=AF_INET;

remote.sin_port=htons(node.port);

remote.sin_addr.s_addr=htonl(node.ip);

in_addrtmp;

tmp.S_un.S_addr=htonl(node.ip);

printf("theaddressis%s,andportis%d\n",inet_ntoa(tmp),node.port);

stP2PMessagetransMessage;

transMessage.iMessageType=P2PSOMEONEWANTTOCALLYOU;

transMessage.iStringLen=ntohl(sender.sin_addr.S_un.S_addr);

transMessage.Port=ntohs(sender.sin_port);

sendto(PrimaryUDP,(constchar*)&transMessage,sizeof(transMessage),0,

(constsockaddr*)&remote,sizeof(remote));

break;

}

caseGETALLUSER: //获取服务器上所有用户的信息

{

intcommand=GETALLUSER;

sendto(PrimaryUDP,(constchar*)&command,sizeof(int),0,

(constsockaddr*)&sender,sizeof(sender));

intnodecount=(int)ClientList.size();

sendto(PrimaryUDP,(constchar*)&nodecount,sizeof(int),0,

(constsockaddr*)&sender,sizeof(sender));

for(UserList::iteratorUserIterator=ClientList.begin();

UserIterator!=ClientList.end();++UserIterator)

{

sendto(PrimaryUDP,(constchar*)(*UserIterator),sizeof(stUserListNode),

0,(constsockaddr*)&sender,sizeof(sender));

}

break;

}

}

}

}

return0;

}

程序中的GetUser()函数通过字符串比较来检索已经登录的用户。11.3.3客户端程序

客户端程序完成P2P连接的工作过程是:首先登录服务器,获得已经登录服务器的用户列表,然后选择一个用户对其发送消息,之后根据用户的指令进行其他操作。发送消息给某个用户的流程是:直接向某个用户的外网IP发送消息,如果此前没有联系过,那么此消息将无法发送,发送端等待超时;超时后,发送端将发送一个请求信息到服务端,要求服务端发送给该客户端一个请求,请求它给本机发送打洞消息,从而实现NAT的打洞。

//P2PClient.cpp

#include"windows.h"

#include"protocol.h"

#include<iostream>

#include<winsock2.h>

usingnamespacestd;

#pragmacomment(lib,"ws2_32.lib")

#defineCOMMANDMAXC256

#defineMAXRETRY5

UserListClientList;

SOCKETPrimaryUDP;

charUserName[10];

charServerIP[20];

boolRecvedACK;

DWORDStartSock(){//同6.3.2节,略}

stUserListNodeGetUser(char*username)

{

for(UserList::iteratorUserIterator=ClientList.begin();UserIterator!=ClientList.end();++UserIterator)

if(strcmp(((*UserIterator)->userName),username)==0)return*(*UserIterator);

stUserListNodeNULLnode={"absent",0,0};//如果未找到则返回一个空节点信息

returnNULLnode;

}

voidConnectToServer(SOCKETsock,char*username,char*serverip)

{

sockaddr_inremote;

remote.sin_addr.S_un.S_addr=inet_addr(serverip);

remote.sin_family=AF_INET;

remote.sin_port=htons(SERVER_PORT);

stMessagesendbuf;

sendbuf.iMessageType=LOGIN;

strncpy(sendbuf.message.loginmember.userName,username,10);

sendto(sock,(constchar*)&sendbuf,sizeof(sendbuf),0,(constsockaddr*)&remote,sizeof(remote));

intusercount;

intfromlen=sizeof(remote);

intiread=recvfrom(sock,(char*)&usercount,sizeof(int),0,(sockaddr*)&remote,&fromlen);

if(iread<=0){printf("binderror!\n");}

//登录到服务器端后,接收服务器端发送来的已经登录的用户的信息

cout<<"Have"<<usercount<<"usersloginedserver:"<<endl;

for(inti=0;i<usercount;i++)

{

stUserListNode*node=newstUserListNode;

recvfrom(sock,(char*)node,sizeof(stUserListNode),0,(sockaddr*)&remote,&fromlen);

ClientList.push_back(node);

cout<<"Username:"<<node->userName<<endl;

in_addrtmp;

tmp.S_un.S_addr=htonl(node->ip);

cout<<"UserIP:"<<inet_ntoa(tmp)<<endl;

cout<<"UserPort:"<<node->port<<endl;

cout<<""<<endl;

}

}

voidOutputUsage()//屏幕打印使用方法简介

{

cout<<"Youcaninputyoucommand:\n"<<"CommandType:\"send\",\"exit\",\"getu\"\n"

<<"Example:sendUsernameMessage\n"<<"exit\n"<<"getu\n"<<endl;

}

boolSendMessageTo(char*UserName,char*Message)

{

charrealmessage[256];

unsignedintUserIP;

unsignedshortUserPort;

boolFindUser=false;

for(UserList::iteratorUserIterator=ClientList.begin();UserIterator!=ClientList.end();++UserIterator)

{

if(strcmp(((*UserIterator)->userName),UserName)==0)

{

UserIP=(*UserIterator)->ip;

UserPort=(*UserIterator)->port;

FindUser=true;

}

}

if(!FindUser)returnfalse;

strcpy(realmessage,Message);

for(inti=0;i<MAXRETRY;i++)

{

RecvedACK=false;

sockaddr_inremote;

remote.sin_addr.S_un.S_addr=htonl(UserIP);

remote.sin_family=AF_INET;

remote.sin_port=htons(UserPort);

stP2PMessageMessageHead;

MessageHead.iMessageType=P2PMESSAGE;

MessageHead.iStringLen=(int)strlen(realmessage)+1;

intisend=sendto(PrimaryUDP,(constchar*)&MessageHead,sizeof(MessageHead),0,

(constsockaddr*)&remote,sizeof(remote));

isend=sendto(PrimaryUDP,(constchar*)&realmessage,MessageHead.iStringLen,0,

(constsockaddr*)&remote,sizeof(remote));

for(intj=0;j<10;j++) //等待接收线程时将此标记修改

{

if(RecvedACK)returntrue;

elseSleep(300);

}

//若未接收到目标主机回应,则目标主机端口映射未打开,发送请求信息给服务器,

//要其告诉目标主机打开映射端口(UDP打洞)

sockaddr_inserver;

server.sin_addr.S_un.S_addr=inet_addr(ServerIP);

server.sin_family=AF_INET;

server.sin_port=htons(SERVER_PORT);

stMessagetransMessage;

transMessage.iMessageType=P2PTRANS;

strcpy(transMessage.message.translatemessage.userName,UserName);

sendto(PrimaryUDP,(constchar*)&transMessage,sizeof(transMessage),0,

(constsockaddr*)&server,sizeof(server));

Sleep(100);//等待对方先发送信息

}

returnfalse;

}

voidParseCommand(char*CommandLine) //解析命令获取当前服务器的所有用户

{

if(strlen(CommandLine)<4)return;

charCommand[10];

strncpy(Command,CommandLine,4);

Command[4]='\0';

if(strcmp(Command,"exit")==0)

{

stMessagesendbuf;

sendbuf.iMessageType=LOGOUT;

strncpy(sendbuf.message.logoutmember.userName,UserName,10);

sockaddr_inserver;

server.sin_addr.S_un.S_addr=inet_addr(ServerIP);

server.sin_family=AF_INET;

server.sin_port=htons(SERVER_PORT);

sendto(PrimaryUDP,(constchar*)&sendbuf,sizeof(sendbuf),0,

(constsockaddr*)&server,sizeof(server));

shutdown(PrimaryUDP,2);

closesocket(PrimaryUDP);

//exit(0);亦可在此处结束进程

}

elseif(strcmp(Command,"send")==0)

{

charsendname[20];

charmessage[COMMANDMAXC];

inti;

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("SendOK!\n");

elseprintf("SendFailure!\n");

}

elseif(strcmp(Command,"getu")==0)

{

intcommand=GETALLUSER;

sockaddr_inserver;

server.sin_addr.S_un.S_addr=inet_addr(ServerIP);

server.sin_family=AF_INET;

server.sin_port=htons(SERVER_PORT);

sendto(PrimaryUDP,(constchar*)&command,sizeof(command),0,

(constsockaddr*)&server,sizeof(server));

}

}

DWORDWINAPIRecvThreadProc(LPVOIDlpParameter) //接收消息线程

{

sockaddr_inremote;

intsinlen=sizeof(remote);

stP2PMessagerecvbuf;

for(;;)

{

intiread=recvfrom(PrimaryUDP,(char*)&recvbuf,sizeof(recvbuf),0,

(sockaddr*)&remote,&sinlen);

if(iread<=0)

{

printf("recverror\n");

continue;

}

switch(recvbuf.iMessageType)

{

caseP2PMESSAGE: //接收到P2P的消息

{

char*comemessage=newchar[recvbuf.iStringLen];

intiread1=recvfrom(PrimaryUDP,comemessage,256,0,(sockaddr*)

&remote,&sinlen);

comemessage[iread1-1]='\0';

if(iread1<=0)printf("receiveerror!\n");

else

{

printf("RecvaMessage:%s\n",comemessage);

stP2PMessagesendbuf;

sendbuf.iMessageType=P2PMESSAGEACK;

sendto(PrimaryUDP,(constchar*)&sendbuf,sizeof(sendbuf),0,

(constsockaddr*)&remote,sizeof(remote));

}

delete[]comemessage;

break;

}

caseP2PSOMEONEWANTTOCALLYOU://接收到打洞命令,向指定的IP地址打洞

{

printf("Recvp2someonewanttocallyoudata\n");

sockaddr_inremote;

remote.sin_addr.S_un.S_addr=htonl(recvbuf.iStringLen);

remote.sin_family=AF_INET;

remote.sin_port=htons(recvbuf.Port);

stP2PMessagemessage; //UDP打洞

message.iMessageType=P2PTRASH;

sendto(PrimaryUDP,(constchar*)&message,sizeof(message),0,

温馨提示

  • 1. 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。图纸软件为CAD,CAXA,PROE,UG,SolidWorks等.压缩文件请下载最新的WinRAR软件解压。
  • 2. 本站的文档不包含任何第三方提供的附件图纸等,如果需要附件,请联系上传者。文件的所有权益归上传用户所有。
  • 3. 本站RAR压缩包中若带图纸,网页内容里面会有图纸预览,若没有图纸预览就没有图纸。
  • 4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
  • 5. 人人文库网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对用户上传分享的文档内容本身不做任何修改或编辑,并不能对任何下载内容负责。
  • 6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
  • 7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。

评论

0/150

提交评论