第4章 P2P应用编程.docx
《第4章 P2P应用编程.docx》由会员分享,可在线阅读,更多相关《第4章 P2P应用编程.docx(18页珍藏版)》请在冰豆网上搜索。
第4章P2P应用编程
第4章P2P应用编程
近年来,P2P的发展非常迅速。
采用P2P方式实现的软件也越来越多,涉及到通信、互动游戏、媒体播放等多种网络应用。
目前人们普遍认为,P2P在加强网络上人的交流、文件交换、深度搜索、分布计算以及协同工作等方面大有前途。
4.1P2P基础知识
P2P是Peer-to-Peer的缩写,也叫对等互联或点对点技术。
与TCP、UDP不同,P2P并不是一种新的协议,而是利用现有网络协议实现网络数据或资源信息共享的技术,它使用的不一定是TCP协议,也可能是UDP协议或者其他协议。
使用P2P技术,可以让一台计算机与另一台计算机直接交换数据,通过Internet直接使用对方的文件,而不必像传统C/S模式全部通过服务器处理。
P2P的特点主要有:
1.对等模式
与传统的服务器/客户端模式不同,使用P2P技术实现的每个计算机节点既是客户端,也是服务器,其功能的提供是对等的,人们可以直接连接到安装了相同的P2P软件的其他用户的计算机上进行数据交互,而不需要提供专门的服务器。
2.分布式网络数据存储结构
相对于目前流行的C/S、B/S的“集中式”网络数据存储结构,P2P最大的特点在于“分散”。
网络中所有的计算、存储和网络连接能力都分布在非集中式网络的“对等伙伴”上。
例如,以前客户端下载文件都是从服务器上下载,而P2P技术则改变了以服务器为中心的状态,每个节点可以各下载一部分,然后互相从对方或者其他节点下载。
采用这种方式,大量用户同时下载不但不会形成服务器网络带宽瓶颈,造成网络堵塞,反而加快了下载速度。
这种方法被认为最能发挥互联网的优势。
分布式计算是继“服务器/客户端”结构后新兴的网络应用模式。
在传统的“服务器/客户端”应用系统中,客户端与服务器有明确的分界,常常发生客户端能力过剩,服务器能力不足或网路堵塞的现象。
P2P系统中的使用者能同时扮演客户端和服务器的多重角色,使两个使用者之间能不通过服务器而直接进行信息分享,以构建具有自主、开放、异质、延展等特性的分布式网际网络应用系统。
从计算模式上看,P2P更加符合分布式计算的理念。
其所倡导的计算能力边缘化、计算资源共享等思想,刚好与网格技术不谋而合。
通过P2P技术,人们可以在不改变原有基础设施的基础上,实现对底层计算资源的控制和调用。
P2P的设计模式可以分为两大类:
一种是单纯型P2P架构,没有专用的服务器;另一种是混合型P2P架构,即单纯型和专用服务器相结合的架构。
单纯型P2P架构没有中央服务器,各个节点之间直接交互信息。
这种方式的优点是使用方便,任何一台计算机只要安装了同一个P2P应用软件,就可以和其他安装这个软件的计算机直接通信。
而正是由于没有中央服务器参与协调,这种方式的使用范围就比较有限了。
原因很简单,一台计算机要和另一台计算机连接,必须要知道对方的IP地址和监听端口,否则就无法向对方发送信息。
而这个工作只能通过人来处理,即通过软件提供的手工操作功能将对方的IP地址和端口加入到搜索范围内,无法利用计算机自动搜索扩展。
从原理上说,互联网最基本的协议TCP/IP并没有客户机和服务器的概念,所有的设备都是通讯的平等的一端。
起初,所有的互联网上的系统都同时具有服务器和客户机的功能。
当然,后来发展的软件的确采用了客户机/服务器的结构:
浏览器和Web服务器,邮件客户端和邮件服务器。
但是,对于服务器来说,它们之间仍然是对等联网的。
以E-mail为例,互联网上并没有一个巨大的、唯一的邮件服务器来处理所有的E-mail,而是对等联网的邮件服务器相互协作把E-mail传送到相应的服务器上去。
混合型P2P架构则是将P2P和客户/服务器模式相结合,此时的中央服务器仅起到促成各节点协调和扩展的功能。
安装了P2P软件的各个计算机开始全部和索引服务器连接,以便告知自己监听的IP地址和端口,然后再通过索引服务器告知其他与自己连接的计算机,每一台计算机的连接和断开连接都通过服务器通知网络上有联系的计算机。
这样就减轻了每台计算机搜索其他计算机的负担,扩展也比较方便,而真正的信息交互则仍然通过点对点直接完成。
但带来的缺点就是服务器必须正常工作才能搜索到其他计算机。
1999年,Napster首先发掘了P2P在文件共享方面的潜力,推出面向全球互联网用户的MP3自由下载服务。
仅1年间,其注册会员就达到300万。
可以说,Napster唤醒了深藏在互联网背后的对等联网。
实际上,文件共享功能在局域网中是再平常不过的事情。
但是Napster的成功促使人们认识到把这种“对等联网”拓展到整个互联网范围的可能性。
事实上,网络上现有的许多服务都可以归入P2P的行列。
即时通信系统例如ICQ、YahooPager、微软的MSNMessenger以及国内的OICQ、POPO等都是最流行的P2P应用。
目前比较流行的下载类P2P软件的典型应属BT(BitTorrent),它采用一种结构化网络结构,利用分布式哈希表(DHT)技术,使每个独立节点都不需要维护整个网络信息,只在节点中存储其临近的后继节点信息,就可以有效地找到其他目标节点。
近年来悄然兴起的互联网视频直播软件也是P2P的应用之一。
这类软件使用网状结构,支持多种格式的流媒体文件,节点间动态查找就近连接。
其工作原理是创建系统内部的虚拟组播技术,利用用户的剩余处理能力和带宽为下游客户提供客户端代理,每一个授权用户都可以将收到的媒体流进行复制或分裂成多个流发送到其他用户,网络中每个用户都可以是媒体的使用者同时也是媒体的分发者。
用虚拟组播技术媒体流可以不断的被增加和复制,并一个接一个地传递到网络的其他用户,从而在网络边界形成一个媒体流的虚拟交叉网络。
当用户需要播放某一频道节目时,组播系统就会将它加入到相应组播组中,使用户迅速得到服务。
利用P2P的虚拟组播技术实现了在线用户越多,视频播放越流畅的特性。
由此可见,现有Internet网络架构下点播式的视频业务,使用P2P技术非常合适。
随着计算机技术的发展和宽带网的进一步普及,P2P应用的优势越来越明显。
它可以充分利用互联网的边缘资源,即用户的计算能力、存储能力和带宽,甚至每个计算机硬盘的内容。
由于P2P网络的非中心化和自发组织的体系结构特性,使其具有非常好的健壮性。
也正是P2P网络的灵活性和易操作性提升了互联网用户的共享和参与热情,从而带动互联网的进一步发展。
4.2P2P应用举例
本节首先通过单纯型P2P架构实现一个简单的聊天程序,然后介绍通过P2P技术设计实际应用程序的思路。
利用这个程序,可以在网络中向已知计算机发送、接收字符串信息。
程序同时既作为服务器端,又作为客户端。
任何一台计算机只要安装了这个程序,就可以通过Internet与加入到好友列表中的任何一个好友即时聊天。
【例4-1】使用P2P技术设计一个简易聊天程序,要求不使用专用的主服务器,只要将好友添加到好友列表中,就能检测到好友是否在线,并相互发送聊天信息。
(1)创建一个名为P2PExample的Windows应用程序,将Form1.cs换名为FormP2P.cs,然后在该设计窗体内设计如图4-1所示的界面。
这是完成聊天功能的主界面。
由于不存在主服务器,所以添加好友时,需要提供好友所用计算机的IP地址和端口号。
为了方便起见,程序中自动生成并显示出本机当前所用的IP地址和端口,如果程序运行时IP地址没有显示出来,就无法和别的计算机连接。
(2)添加命名空间引用:
usingSystem.IO;
usingSystem.Net.Sockets;
usingSystem.Net;
usingSystem.Threading;
(3)在构造函数上方添加字段声明,并在构造函数中添加代码:
privateThreadmyThread;
privateTcpListenertcpListener;
privateIPAddressmyIPAddress;
privateintmyPort;
privateSystem.Diagnostics.StopwatchsecondWatch;
publicFormP2P()
{
InitializeComponent();
secondWatch=newSystem.Diagnostics.Stopwatch();
ColumnHeaderipColumn=newColumnHeader();
ipColumn.Text="IP地址";
ipColumn.Width=136;
ColumnHeaderportColumn=newColumnHeader();
portColumn.Text="端口号";
ColumnHeaderonlineColumn=newColumnHeader();
onlineColumn.Text="是否在线";
onlineColumn.Width=71;
listViewMyFriend.View=View.Details;
listViewMyFriend.Columns.AddRange(
newColumnHeader[]{ipColumn,portColumn,onlineColumn});
}
(4)添加Load事件代码:
privatevoidFormP2P_Load(objectsender,EventArgse)
{
//启动秒表
secondWatch.Start();
timerSecond.Enabled=true;
buttonStartTimer.Enabled=false;
buttonStopTimer.Enabled=true;
//使用代理指定在线程上执行的方法
ThreadStartmyThreadStartDelegate=newThreadStart(Listening);
//创建一个用于监听的线程对象,通过代理执行线程中的方法
myThread=newThread(myThreadStartDelegate);
//启动线程
myThread.Start();
}
(5)添加线程执行的方法:
//该方法是通过代理调用执行的
privatevoidListening()
{
Socketsocket=null;
//获取本机第一个可用IP地址
myIPAddress=(IPAddress)Dns.GetHostAddresses(Dns.GetHostName()).GetValue(0);
Randomr=newRandom();
while(true)
{
try
{
//随机产生一个0-65535之间的端口号
myPort=r.Next(65535);
//创建TcpListener对象,在本机的IP和port端口监听连接到该IP和端口的请求
tcpListener=newTcpListener(myIPAddress,myPort);
tcpListener.Start();
//显示IP地址和端口
ShowLocalIpAndPort();
//在信息窗口中显示成功信息
ShowMyMessage(string.Format("尝试用端口{0}监听成功",myPort));
ShowMyMessage(string.Format(
"[{0}]{1:
h点m分s秒}开始在{2}端口监听与本机的连接",
myIPAddress,DateTime.Now,myPort));
break;
}
catch
{
//继续while循环,以便随机找下一个可用端口号,同时显示失败信息
ShowMyMessage(string.Format("尝试用端口{0}监听失败",myPort));
}
}
while(true)
{
try
{
//使用阻塞方式接收客户端连接,根据连接信息创建TcpClient对象
//注意:
AcceptSocket接收到新的连接请求才会继续执行其后的语句
socket=tcpListener.AcceptSocket();
//如果往下执行,说明已经根据客户端连接请求创建了套接字
//使用创建的套接字接收客户端发送的信息
NetworkStreamstream=newNetworkStream(socket);
StreamReadersr=newStreamReader(stream);
stringreceiveMessage=sr.ReadLine();
inti1=receiveMessage.IndexOf(",");
inti2=receiveMessage.IndexOf(",",i1+1);
inti3=receiveMessage.IndexOf(",",i2+1);
stringipString=receiveMessage.Substring(0,i1);
stringportString=receiveMessage.Substring(i1+1,i2-i1-1);
stringmessageTypeString=receiveMessage.Substring(i2+1,i3-i2-1);
stringmessageString=receiveMessage.Substring(i3+1);
ShowMyMessage(ipString,portString,messageTypeString,messageString);
}
catch
{
//通过停止TcpListener使AcceptSocket()出现异常
//在异常处理中关闭套接字并终止线程
if(socket!
=null)
{
if(socket.Connected)
{
socket.Shutdown(SocketShutdown.Receive);
}
socket.Close();
}
myThread.Abort();
}
}
}
在第一个while循环中,首先随机生成一个端口号,然后使用本机第一个IP地址和产生的端口进行监听,如果不出现异常,说明该IP地址和端口号可用,退出while循环;否则继续随机产生下一个端口,直到找到可用的端口为止。
在第二个while循环中,使用AcceptSocket方法接收到该端口的连接请求,在接收到连接请求之前,该方法一直处于阻塞方式,一旦接收到新的连接请求,就创建一个对应的套接字对象,然后就可以利用这个套接字对象创建网络流对象,再利用StreamReader从网络流中一直读取字符,直到遇到回车换行为止。
注意,这里使用回车换行作为每条信息之间的分隔符。
在介绍TCP协议时,读者已经知道TCP协议是没有消息边界的,如果不考虑这个问题,就无法保证发送的每条信息和接收的每条信息一一对应,例如可能会出现发送的几条信息一块接收的情况。
考虑下面几条语句:
byte[]buffer=newbyte[socket.ReceiveBufferSize];
inti=socket.Receive(buffer,buffer.Length,SocketFlags.None);
stringreceiveMessage=System.Text.Encoding.UTF8.GetString(buffer,0,i);
如果不仔细考虑,读者可能会认为这几条语句和使用StreamReader对象从网络流读取方式完成的功能相同,其实这样使用会出现问题的。
由于边界问题和网络中传输的影响,使用socket.Receive方法接收到的不一定刚好是发送的一条信息,可能是一部分,也可能是几条信息都在缓冲区内,而这段代码中只接收了一次,自然无法保证和发送的一条信息完全对应。
另外,如果在一台机器上调试,由于没有网络传输的影响,自然也就无法发现这段代码中存在的问题。
即使在一个局域网中调试,网络传输的影响很小,也很难发现边界问题。
而使用StreamReader对象的ReadLine方法,则可以一直读取直到遇到回车换行为止,从而保证与发送的以回车换行结尾的每条信息对应。
接收到一条信息以后,根据发送时在字符串中插入的逗号分隔符,将其分开,分别得到对方的IP地址、端口号、信息类型以及信息。
然后调用SendMyMessage方法分别处理。
还有一点要注意,不能在第二个循环中试图用一个布尔型变量作为是否退出循环的判断标准,这是因为程序执行到AcceptSocket方法时会处于阻塞方式,无法保证及时响应该布尔值的变化,也就失去了判断的意义。
(6)添加代理及相应的方法:
delegatevoidShowMessageDelegate1(stringstr);
privatevoidShowMyMessage(stringstr)
{
//比较调用的线程和创建的线程是否同一个线程
//如果不是,结果为true
if(this.listBoxMessage.InvokeRequired==true)
{
//如果结果为true,则自动通过代理执行else中语句的功能(注意:
是else,不是if)
//这里只需要传入参数str即可
//但是执行的功能会始终与else中的指定的功能相同,区别仅是通过代理完成
ShowMessageDelegate1messageDelegate=newShowMessageDelegate1(ShowMyMessage);
this.Invoke(messageDelegate,newobject[]{str});
}
else
{
//在这里指定如果是同一个线程需要完成什么功能
//如果是不同线程,系统会自动通过代理实现这里指定的功能
listBoxMessage.Items.Add(str);
}
}
delegatevoidShowMessageDelegate2(stringipString,stringportString,stringmessageTypeString,stringmessageString);
privatevoidShowMyMessage(stringipString,stringportString,stringmessageTypeString,stringmessageString)
{
if(this.listBoxMessage.InvokeRequired==true)
{
ShowMessageDelegate2messageDelegate=newShowMessageDelegate2(ShowMyMessage);
this.Invoke(messageDelegate,newobject[]{ipString,portString,messageTypeString,messageString});
}
else
{
//messageType的含义为:
//前缀:
第i次连接
//前缀:
检查接收者是否在线
//前缀:
聊天信息
intmyfriendIndex=CheckMyFriend(ipString);
switch(messageTypeString)
{
case"connect":
if(myfriendIndex==-1)
{
ListViewItemmyFriendItem=newListViewItem(
newstring[]{ipString,portString,"是"});
listViewMyFriend.Items.Add(myFriendItem);
}
listBoxMessage.Items.Add(string.Format("[{0}:
{1}]说:
{2}",
ipString,portString,messageString));
break;
case"check":
if(myfriendIndex==-1)
{
ListViewItemmyFriendItem=newListViewItem(
newstring[]{ipString,portString,"是"});
listViewMyFriend.Items.Add(myFriendItem);
}
//不需要显示
break;
case"message":
listBoxMessage.Items.Add(string.Format("[{0}:
{1}]说:
{2}",
ipString,portString,messageString));
break;
default:
listBoxMessage.Items.Add(string.Format("什么意思呀:
“{0}”",messageString));
break;
}
}
}
delegatevoidShowIpAndPortDelegate();
privatevoidShowLocalIpAndPort()
{
if(this.listBoxMessage.InvokeRequired)
{
ShowIpAndPortDelegatemessageDelegate=
newShowIpAndPortDelegate(ShowLocalIpAndPort);
this.Invoke(messageDelegate);
}
else
{
textBoxLocalIp.Text=myIPAddress.ToString();
textBoxLocalPort.Text=myPort.ToString();
}
}
使用代理的目的是为了解决一个线程无法在托管模式下直接调用另一个线程的控件问题。
当然,如果通过非托管模式,也可以直接调用,但可能会引起死锁等问题。
(7)添加代码,检查好友是否在好友列表中:
privateintCheckMyFriend(stringremoteIpString)
{
//在listViewMyFriend中检查指定的ip是否存在
ListViewItemitem=listViewMyFriend.FindItemWithText(remoteIpString);
if(item==null)
{
return-1;
}
else
{
returnitem.Index;
}
}
代码中的FindItemWithText方法检查ListView中的每一项中是否包含指定的字符串,如果是,则返回该项,否则返回null。
由于好