1、1第11章 P2P 技 术11.1 P2P技术概述技术概述11.2 NAT穿越穿越11.3 P2P编程编程小结小结2由于传统的C/S模式对服务器过于依赖,导致负载分布不平衡,以及性价比和扩展性都存在一定的问题,因此目前一种新的通信模式非常盛行,这就是P2P(Peer-to-Peer)技术。P2P技术通过客户端进行互联的方法,将通信任务的压力分散开来,可以大大优化文件下载、流媒体、即时通信、语音通信等多种网络应用,是网络通信程序方面的程序员必须掌握的一项关键技术。本章首先介绍P2P的概念、分类和原理,然后讲解了将P2P技术运用于当前网络需解决的NAT的穿越问题,重点介绍了两种NAT穿越技术的打洞
2、方法,继而进行了基于TCP打洞的P2P编程实例。掌握本章学习内容需紧密结合对TCP/IP协议的IP层工作机理的深入理解。3P2P正式步入发展在20世纪90年代末期,始于美国波士顿大学一年级新生肖恩范宁编写的Napster音乐共享程序,现已彻底统治了当今的互联网。据统计,互联网中大约50%90%的总流量都来自于P2P,具有非同一般的意义。11.1 P2P技术概述技术概述411.1.1 11.1.1 概念概念P2P是英文“对等”的简称,又称为“点对点”。“对等”技术是一种网络新技术,依赖于网络中参与者的计算能力和带宽,而不是把依赖都聚集在较少的几台服务器上。简单地说,P2P直接将人们联系起来,让人
3、们通过互联网直接交互,这使得网络上用户的沟通变得更容易,共享和交互也变得更直接,真正地消除了网络连接的中间环节。也就是说,P2P就是人们可以直接连接到其他用户的计算机,并交换文件,而不是像过去那样连接到服务器再浏览与下载(如图11-1所示)。同时,P2P也改变了互联网现在以P2P工作组大网站为中心的状态,重返“非中心化”状态,并把权力交还给了用户。5P2P不是一种新的协议,而是利用现有的网络协议实现网络数据或资源信息共享的技术,它使用的可能是TCP、UDP或其他协议。P2P技术与C/S模式相比具有许多优点。首先,P2P能够解决C/S模式中存在的服务器与客户机计算任务分配不均的问题,使负载均衡,
4、降低服务器的压力。其次,P2P具有高性价比,能够利用闲置的计算能力或存储空间,达到高性能计算和海量存储的目的。另外,P2P可扩展性良好,由于不存在服务器端瓶颈,其扩展性大大加强。同时,也应当注意P2P带来了版权与安全性等问题。6图11-1 C/S模式与P2P模式结构比较7目前,P2P有许多耳熟能详的著名应用,如文件下载的BitTorrent、迅雷、电驴;流媒体播放器PPLive;即时通信软件ICQ、MSN;语音网络通信软件Skype等。811.1.2 11.1.2 原理原理P2P的设计模式总体可以分为两类,即单纯型P2P和混合型P2P架构,其原理稍有不同。单纯型P2P架构没有中央服务器,各个节
5、点之间直接交互信息,如图11-1(b)所示,其优点是使用方便,任何一个安装了P2P应用软件的计算机,都可以与其他安装这个软件的计算机进行P2P通信。它的缺点是没有中央服务器参与协调,使用范围比较有限。混合型P2P架构将P2P和C/S模式相结合。这种结构在单纯型的基础上引入中央服务器,这里的中央服务器不同于C/S模式中的服务器,仅起到促成各节点协调和扩展的功能。9工作时安装了P2P软件的各个计算机首先登录中央服务器连接,告知服务器自己监听的IP地址和端口,然后由中央服务器建立索引,进而告知其他登录的计算机。登录后的各台计算机根据中央服务器提供的信息,并在中央服务器的帮助下与其他计算机建立P2P连
6、接,实现数据共享或通信。每台计算机的连接和断开都要通过中央服务器通知所有登录的计算机。这种架构的优点是实现了文件查询和文件传输的分离,有效地节省了中央服务器的带宽消耗,减少了系统的文件传输延时;缺点是增加了对服务器的依赖性,中央服务器的瘫痪容易导致整个网络的崩溃。10P2P技术实现必须解决一大难题,就是NAT穿越的问题。当前许多计算机都隐藏在私有网络中,通常因安全原因,外网的计算机无法知道内网计算机上的P2P进程编号(含IP地址和端口号,见1.3.2节),更不允许直接连接内网的计算机。由前面介绍的P2P原理可知,这对P2P的实现是致命的。因此必须解决穿越进入私有网络的问题,其关键在于外部网络进
7、程如何穿越网络地址转换(Network AddressTranslators,NAT)设备,与内网计算机上的进程建立全相关。11.2 NAT穿越穿越1111.2.1 NAT11.2.1 NAT概念概念NAT是在IP地址日益缺乏的情况下产生的,它的主要目的就是为了能够地址重用,其产生基于如下事实:一个私有网络(域)中的节点中只有很少的节点需要与外网连接,那么这个子网中其实只有少数的节点需要全球唯一的IP地址,其他节点的IP地址应该是可以重用的。121 1原理原理NAT的核心功能就是进行地址转换。通常NAT设备有两个NIC,一个接入Internet,一个接入LAN(因此其拥有两个IP地址)。进行N
8、AT转换可以分为数据向外和数据向内两种情况。一方面通过正确的地址转换保障从私有内网(简称内网)传出的数据可以路由到达外网终端上的网络进程,另一方面还要保障从外网终端上的网络进程返回的数据可以安全通过NAT到达内网终端上的网络进程,其转换过程如图11-2所示。13图11-2 NAT转换过程14为了对各种地址加以区分,通常使用以下四种地址进行区分。内部本地地址(Inside Local IP address,IL)是指内网主机地址,图11-2中主机Tom的IL地址就是10.0.0.1,Jerry的IL地址就是10.0.0.2。IL地址在外部网络中是无法路由的或路由会将数据导向错误的主机。内部全局地
9、址(Inside Global IP address,IG)代表内部IP到外部网络可路由的合法IP,图11-2中NAT的IG地址就是155.99.25.11,这是从外部看子网,整个子网所呈现的地址,因此它代表整个内向的地址。15外部本地地址(Outside Local IP address,OL)是内网主机所知的一台连接外部网络的主机IP,如图11-2中NAT的OL地址就是10.0.0.88,这是从内网主机向外网看去,整个外网所呈现的地址,因此它代表整个外围的地址。外部全局地址(Outside Global IP address,OG)是外部网络主机的合法IP,如图11-2中所示的服务器地址为
10、18.181.0.31。16以图11-2中的网络结构为例,进行NAT地址转换的两种情况描述如下:(1)数据传出:假设终端Tom(IL地址为10.0.0.1)上网络进程(端口号为1234)的一个数据包要透过NAT传出到外网WWW服务器(OG地址为18.181.0.31)的Web服务程序上(端口号为80)。在进行转换之前,NAT设备查找路由并验证数据包是否合乎规则。如通过验证,则将该数据包的源(Sur)地址(10.0.0.1)替换为NAT的IG地址(155.99.25.11),再随机安排一个NAT的端口号(如1222)替换Tom的端口号1234,记录该对应关系到地址转换表中(如表11-1中的记录1
11、),17从而这张转换表就将这台计算机的不可路由的IP地址及其端口号与路由器(这里是NAT)的IP地址绑定起来了。接下来,就可以发送数据包到外网,等待响应。18表11-1 NAT地址转换表19(2)数据传入:当一个数据包从目的服务器(18.181.0.31)发送回来时,假设它的目的(Des)主机和端口号组合为155.99.25.11:1222,根据NAT地址转换表的记录1就可以确定目的计算机的IL地址和端口号,其组合为10.0.0.1:1234,则替换该数据包的Des地址和端口号,然后发送至那台计算机。可见,NAT地址表中是否有对应的记录是外网数据能否进入内网的关键,利用地址转换的工作原理可以实
12、现NAT的穿越。注意,在进行IP地址和端口号替换后,需要重新计算数据包头校验和,否则数据包会被丢弃。202 2分类分类NAT的分类方法很多,按照发展过程可分为基本NAT和NAPT(Network Address/Port Translator)两类。最开始,NAT是运行在路由器上的一个功能模块,并且首先出现的是基本NAT。基本NAT实现的功能很简单。在子网内使用一个保留的IP子网段,这些IP对外是不可见的,而子网内只有少数一些IP地址可以对应到真正全球唯一的IP地址。如果这些节点需要访问外部网络,那么基本NAT就负责将这个节点的子网内IP转化为一个全球唯一的IP并发送出去(基本的NAT会改变I
13、P包中的原IP地址,但是不会改变IP包中的端口)。21NAPT比NAT稍微复杂一些,它不但会改变经过这个NAT设备的IP数据包的IP地址,还会改变IP数据包的TCP/UDP端口。目前NAPT是主流的NAT技术,前面对图11-2的说明也是基于NAPT的。223 3转发转发显然,根据前面对NAT工作原理的介绍,当地址转换表中不存在一个外网的地址时,则以这个地址为Des地址的数据包将会被丢弃,这就给P2P技术带来致命问题。如图11-3所示的结构中,客户A想要直接建立与客户B的连接就会因为NAT的阻隔而失败。为了解决这一问题,可以采用NAT转发的方法加以解决。也就是客户A、B以服务器S为中间人,转发数
14、据。A与S、B与S分别建立会话,由S对接收到的数据进行地址替换,实现间接的A、B之间的通信,如图11-3所示。显然这样会大大增加S的负担,在实际的网络中几乎不可行。23图11-3 NAT转发244 4反向连接反向连接还有一种解决外网计算机与内网计算机的连接方法就是反向连接。这种方法的应用条件比较特殊,即两台需要连接的计算机只有一台在NAT后面,如图11-4所示。25图11-4 NAT反向连接26进行反向连接的过程分以下三步:(1)客户B向中央服务器S发送与客户A的连接请求;(2)服务器S中继该连接请求到客户A;(3)客户A发起连接,与客户B建立连接。反向连接利用服务器S与客户A之间已经建立的合
15、法通信(地址转换关系已经在客户A登录时就已经插入NAT的地址转换表),以及客户A可以自由连接一台具有OG地址的主机来实现P2P。但是由于反向连接的条件过于特殊,因此使用不是非常广泛。275NAT穿越目前解决NAT穿越主要是巧妙地利用TCP/IP的TCP和UDP协议,使用称为“打洞”(hole punching)的技术。具体方法是:应用程序向NAT外的服务器发送请求连接其他应用程序的消息,收到请求消息后产生响应消息,通知连接方和被连接方对方的IL、IG地址及对应端口信息。利用得到的地址和端口信息,UDP和TCP各自采用不同的方法在NAT上建立地址转换记录,从而实现对方连接的数据包可以穿越NAT而
16、进入内网。这时就可以建立不同NAT后计算机上应用程序的连接了。28UDP打洞(见11.2.2节)与TCP打洞(见11.2.3节)稍有区别。上述这种NAT穿越方法最大的优点是无需现有NAT设备做任何改动,而且可以适用于多级NAT的网络结构,是当前NAT穿越采用的主流技术。2911.2.2 UDP11.2.2 UDP打洞打洞UDP打洞利用UPD协议,在中央服务器的协调下实现NAT穿越。假设在如图11-5所示的网络结构上实现UDP穿越。这个结构中存在两台计算机客户A和客户B,它们都拥有自己的私有IP地址,并且都处在不同的NAT之后。相同的P2P程序分别运行于A和B上。在公网上存在一台中央服务器S,并
17、且对它们都开放了UDP端口1234。A和B首先分别与S建立通信会话,这时NAT A把它自己的UDP端口62000分配给A与S的会话,NAT B也把自己的UDP端口31000分配给B与S的会话。30在建立通话的过程中,S通过数据包的源地址和内容已经了解了A和B的IL地址和各自的NAT IG地址及对应的端口号。31图11-5 UDP打洞过程32A希望与B建立端对端的连接,其UDP打洞分为以下三步:(1)A开始发送一个UDP信息到S,并提出连接B的请求;(2)S收到请求后,向A和B分别发出对方的IL地址和各自的NAT IG地址以及对应的端口号,即A得到B的IL地址及端口是10.1.1.3:4321,
18、B的IG地址及端口是138.76.29.7:31000;B得到A的IL地址及端口是10.0.0.1:4321,A的IG地址及端口是155.99.25.11:62000。33(3)在得到对方的地址后,A和B发出两个UDP连接,一个连接对方的IG地址和端口号,一个直接指向对方的IL地址和端口号的连接。这时A向B的IG地址(138.76.29.7:31000)发送的信息导致NAT A增加一条155.99.25.11:62000与138.76.29.7:31000的地址转换记录,同样,B向A的IG地址(155.99.25.11:62000)发送的信息导致NAT A增加一条138.76.29.7:310
19、00与155.99.25.11:62000的地址转换记录。此时会有两种情况发生:一种是A连接B的IG地址和端口号先于B连接A的IG地址和端口号到达NAT B,但由于NAT B上的地址转换记录还没有建立,34因此A连接B的IG地址数据包会被丢弃;另一种是A连接B的IG地址和端口号晚于B连接A的IG地址和端口号到达NAT B,这时NAT B上的地址转换记录已经建立,因此A连接B的IG地址数据包会被转发给B(依据地址映射上的对应关系)。无论哪一种情况发生,最终总是有一个NAT被穿越,那么A和B之间就可以直接进行通信而无需S的协助了。之所以在步骤(3)中还要发送一个直接指向对方的IL地址和端口号的连接
20、,就是考虑到A与B同处于一个NAT,则这个连接一定会比由NAT“发卡”式转发要有更高的效率。3511.2.3 TCP11.2.3 TCP打洞打洞TCP打洞利用TCP协议,在中央服务器的协调下实现NAT穿越。同样采用11.2.2节的网络结构实现TCP打洞,A希望与B建立端对端的连接,分为三个步骤,如图11-6所示。36图11-6 TCP打洞过程37(1)客户A和B分别登录服务器S,A可以通过S得到B的IL和IG地址,B也可以通过S得到A的IL和IG地址,此时A与B相互发送的消息会被NAT所丢弃。(2)为了完成TCP打洞,穿越NAT,A会打开一个侦听套接字,接收来自B的信息,再打开一个套接字并向B
21、的IL地址发一个SYN包(两个套接字使用端口复用);B打开一个侦听套接字,接收来自A的信息,也可以再打开一个套接字向A的公网地址发送SYN包(两个套接字同样使用端口复用)。38(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
22、试图连接到B的公共地址的那个套接字上。39因此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会话产生一个新的流套接字。40与情况一类似,A用一个SY
23、N-ACK包进行响应,则这个TCP连接的启动过程就像是一个平常的C/S风格,TCP连接顺利建立。由于A先前对B的connect()使用了当前的源地址到目的地址的组合,而这个组合又被上面的套接字再次使用,则A的这个Connect()必定失败,产生一个典型的“地址已使用”错误。但是,应用仍然会工作在前面已建立连接的流套接字,这个错误会被忽略,TCP连接还是被建立起来了。前一种情况通常出现在基于BSD的操作系统,后一种多出现在Linux和Windows操作系统上。41如前所述,TCP打洞的一个重要条件就是要将两个不同的套接字绑定到一个端口上,因此必须采用端口复用技术。由于传统的标准伯克利(Berke
24、ley)网卡要么用来连接主动建立对外连接,要么使用Listen()和Accept()被动建立来自外部的连接,不能提供类似UDP那样的同一端口既可以向外连接又能接受来自外部的连接,即仅允许建立一对一的响应,应用程序在将一个套接字绑定到本地一个端口以后,任何试图将第二个套接字绑定到该端口的操作都会失败。42而为了实现TCP打洞就必须使用一个本地TCP端口来监听来自外部的TCP连接,同时建立多个向外连接的TCP连接,这就需要使用setsockopt()函数进行端口复用设置,实现代码如下:Bool val;Socket sock;Setsockopt(sock,SOL_SOCKET,SO_REUSEA
25、DDR,(char*)&val,sizeof(val);Winsock2对端口复用技术提供很好的支持,这也是其特色之一。43基于混合型P2P架构和UDP协议的基本P2P连接功能,总体软件可分为P2P协议、服务端和客户端三部分(这与传统的C/S意义完全不同)。程序设计前有如下约定:11.3 P2P编程编程44 P2PServer运行在一个拥有公网IP的计算机上,P2PClient运行在两个不同的NAT后;后登录的计算机可以获得先登录计算机的用户名,后登录的计算机通过“send username message”的格式来发送消息,如果发送成功,说明已取得了直接与对方连接的成功;程序使用了三个命令,
26、即send、getu、exit,其中,send代表发送信息给用户,getu代表获得当前服务器用户列表,exit代表客户端注销与服务器的连接。4511.3.1 P2P11.3.1 P2P协议程序协议程序P2P软件必须商定统一的协议,否则各个端点将无法协调。协议需要规定命令、信息协议、消息类型、消息格式。本节协议将这些信息分为客户端与服务器间的通信信息协议和客户端间的通信信息协议两类实现。/protocol.h#pragma once#include/定义iMessageType的值#define LOGIN1#define LOGOUT246#define P2PTRANS3#define GE
27、TALLUSER4#define SERVER_PORT 60000 /服务器端口/=/客户端与服务器间通信信息协议/=struct stLoginMessage/客户端登录服务器发送的消息char userName10;47char password10;struct stLogoutMessage/客户端注销时发送的消息char userName10;struct stP2PTranslate/客户端向服务器请求另外一个Client向UDP发送打洞消息48char userName10;struct stMessage/客户端向服务器发送的消息格式 int iMessageType;uni
28、on _message49 stLoginMessage loginmember;stLogoutMessage logoutmember;stP2PTranslate translatemessage;message;struct stUserListNode/客户节点信息 char userName10;unsigned int ip;50 unsigned short port;struct stServerToClient/服务端向客户端发送的消息 int iMessageType;union _message stUserListNode user;message;51;/=/客户端
29、间通信信息协议/=#define P2PMESSAGE100/发送消息#define P2PMESSAGEACK101/收到消息的应答#define P2PSOMEONEWANTTOCALLYOU102/服务器向客户端发送的消息,希望它发送/UDP打洞包52#define P2PTRASH103/客户端发送的打洞包,接收端应该/忽略此消息struct stP2PMessage/客户端之间发送消息格式int iMessageType;/信息类型int iStringLen;/地址信息 unsigned short Port;/端口信息;53using namespace std;typedef
30、list UserList;在上述协议文件中,使用了UserList来记录用户的信息。客户端与服务器端均需遵循上述协议,具体方法是通过#include protocol.h引入这个协议头文件,并在通信过程中遵循它规定的信息格式来实现。5411.3.2 11.3.2 服务器端程序服务器端程序服务器端程序主要完成的任务包括三项:检测在线用户情况、维护用户列表、转达客户方打洞的请求。主要工作由main()函数的一个for循环完成,分别对四种客户请求进行不同的响应。/P2PServer.cpp#include windows.h#include protocol.h#include“winsock2.
31、h”55#pragma comment(lib,ws2_32.lib)UserList ClientList;DWORD StartSock()/同6.3.2节,略 stUserListNode GetUser(char*username)/检索所需用户信息 for(UserList:iterator UserIterator=ClientList.begin();UserIterator!=ClientList.end();+UserIterator)56 if(strcmp(*UserIterator)-userName),username)=0)return*(*UserIterator
32、);stUserListNode NULLnode=absent,0,0;/如果未找到则返回一个空节点信息 return NULLnode;int main(int argc,char*argv)StartSock();57 SOCKET PrimaryUDP;PrimaryUDP=socket(AF_INET,SOCK_DGRAM,0);sockaddr_in local;local.sin_family=AF_INET;local.sin_port=htons(SERVER_PORT);local.sin_addr.s_addr=htonl(INADDR_ANY);int nResult=
33、bind(PrimaryUDP,(sockaddr*)&local,sizeof(sockaddr);if(nResult=SOCKET_ERROR)58 printf(bind error!n);return 0;sockaddr_in sender;stMessage recvbuf;memset(&recvbuf,0,sizeof(stMessage);for(;)/主循环 59int dwSender=sizeof(sockaddr_in);int ret=recvfrom(PrimaryUDP,(char*)&recvbuf,sizeof(stMessage),0,(sockaddr
34、*)&sender,&dwSender);if(ret userName,recvbuf.message.loginmember.userName);currentuser-ip=ntohl(sender.sin_addr.S_un.S_addr);currentuser-port=ntohs(sender.sin_port);62ClientList.push_back(currentuser);/发送已经登录的客户信息int nodecount=(int)ClientList.size();sendto(PrimaryUDP,(const char*)&nodecount,sizeof(i
35、nt),0,(const sockaddr*)&sender,sizeof(sender);for(UserList:iterator UserIterator=ClientList.begin();63UserIterator!=ClientList.end();+UserIterator)sendto(PrimaryUDP,(const char*)(*UserIterator),sizeof(stUserListNode),0,(const sockaddr*)&sender,sizeof(sender);break;case LOGOUT:/将此客户信息删除64printf(has a
36、 user logout:%sn,recvbuf.message.logoutmember.userName);UserList:iterator removeiterator=NULL;for(UserList:iterator UserIterator=ClientList.begin();UserIterator!=ClientList.end();+UserIterator)65if(strcmp(*UserIterator)-userName),recvbuf.message.logoutmember.userName)=0)removeiterator=UserIterator;b
37、reak;if(removeiterator!=NULL)ClientList.remove(*removeiterator);66break;case P2PTRANS:/某个客户希望服务端向另外一个客户发送一个打洞消息printf(%s wants to p2p%sn,inet_ntoa(sender.sin_addr),recvbuf.message.translatemessage.userName);67stUserListNode node=GetUser(recvbuf.message.translatemessage.userName);sockaddr_in remote;r
38、emote.sin_family=AF_INET;remote.sin_port=htons(node.port);remote.sin_addr.s_addr=htonl(node.ip);in_addr tmp;68tmp.S_un.S_addr=htonl(node.ip);printf(the address is%s,and port is%dn,inet_ntoa(tmp),node.port);stP2PMessage transMessage;transMessage.iMessageType=P2PSOMEONEWANTTOCALLYOU;transMessage.iStri
39、ngLen=ntohl(sender.sin_addr.S_un.S_addr);69transMessage.Port=ntohs(sender.sin_port);sendto(PrimaryUDP,(const char*)&transMessage,sizeof(transMessage),0,(const sockaddr*)&remote,sizeof(remote);break;70case GETALLUSER:/获取服务器上所有用户的信息 int command=GETALLUSER;sendto(PrimaryUDP,(const char*)&command,sizeof
40、(int),0,(const sockaddr*)&sender,sizeof(sender);int nodecount=(int)ClientList.size();71sendto(PrimaryUDP,(const char*)&nodecount,sizeof(int),0,(const sockaddr*)&sender,sizeof(sender);for(UserList:iterator UserIterator=ClientList.begin();UserIterator!=ClientList.end();+UserIterator)72sendto(PrimaryUD
41、P,(const char*)(*UserIterator),sizeof(stUserListNode),0,(const sockaddr*)&sender,sizeof(sender);break;73 return 0;程序中的GetUser()函数通过字符串比较来检索已经登录的用户。7411.3.3 11.3.3 客户端程序客户端程序客户端程序完成P2P连接的工作过程是:首先登录服务器,获得已经登录服务器的用户列表,然后选择一个用户对其发送消息,之后根据用户的指令进行其他操作。发送消息给某个用户的流程是:直接向某个用户的外网IP发送消息,如果此前没有联系过,那么此消息将无法发送,发送
42、端等待超时;超时后,发送端将发送一个请求信息到服务端,要求服务端发送给该客户端一个请求,请求它给本机发送打洞消息,从而实现NAT的打洞。75/P2PClient.cpp#include windows.h#include protocol.h#include#include using namespace std;#pragma comment(lib,ws2_32.lib)#define COMMANDMAXC 25676#define MAXRETRY 5UserList ClientList;SOCKET PrimaryUDP;char UserName10;char ServerIP2
43、0;bool RecvedACK;DWORD StartSock()/同6.3.2节,略 stUserListNode GetUser(char*username)77 for(UserList:iterator UserIterator=ClientList.begin();UserIterator!=ClientList.end();+UserIterator)if(strcmp(*UserIterator)-userName),username)=0)return*(*UserIterator);stUserListNode NULLnode=absent,0,0;/如果未找到则返回一个
44、空节点信息78 return NULLnode;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;79 remote.sin_port=htons(SERVER_PORT);stMessage sendbuf;sendbuf.iMessageType=LOGIN;strncpy(sendbuf.message.loginmember.use
45、rName,username,10);sendto(sock,(const char*)&sendbuf,sizeof(sendbuf),0,(const sockaddr*)&remote,sizeof(remote);int usercount;80 int fromlen=sizeof(remote);int iread=recvfrom(sock,(char*)&usercount,sizeof(int),0,(sockaddr*)&remote,&fromlen);if(iread=0)printf(bind error!n);/登录到服务器端后,接收服务器端发送来的已经登录的用户的
46、信息 coutHave usercount users logined server:endl;for(int i=0;iusercount;i+)81 stUserListNode*node=new stUserListNode;recvfrom(sock,(char*)node,sizeof(stUserListNode),0,(sockaddr*)&remote,&fromlen);ClientList.push_back(node);coutUsername:userNameip);82coutUserIP:inet_ntoa(tmp)endl;coutUserPort:portend
47、l;coutendl;void OutputUsage()/屏幕打印使用方法简介83 coutYou can input you command:nCommand Type:send,exit,getun Example:send Username Messagen exitn getunuserName),UserName)=0)UserIP=(*UserIterator)-ip;UserPort=(*UserIterator)-port;FindUser=true;86 if(!FindUser)return false;strcpy(realmessage,Message);for(in
48、t i=0;iMAXRETRY;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);87stP2PMessage MessageHead;MessageHead.iMessageType=P2PMESSAGE;MessageHead.iStringLen=(int)strlen(realmessage)+1;int isend=sendto(PrimaryUDP,(cons
49、t char*)&MessageHead,sizeof(MessageHead),0,(const sockaddr*)&remote,sizeof(remote);isend=sendto(PrimaryUDP,(const char*)&realmessage,MessageHead.iStringLen,0,88(const sockaddr*)&remote,sizeof(remote);for(int j=0;j10;j+)/等待接收线程时将此标记修改if(RecvedACK)return true;else Sleep(300);/若未接收到目标主机回应,则目标主机端口映射未打开,
50、发送请求信息给服务器,89/要其告诉目标主机打开映射端口(UDP打洞)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);90sendto(Primar