您的位置:首页 > 其它

利用 Windows Vista 和 WCF 中强大的 P2P 通信功能

2006-11-07 13:33 323 查看
欢迎来到MSDN>Windows开发

点对点技术

利用WindowsVista和WCF中强大的P2P通信功能

发布日期:2006-10-17|更新日期:2006-10-17
JustinSmith

本文基于WindowsVista和.NETFramework3.0的预发布版本撰写而成。文中包含的所有信息均有变更可能。

本文将介绍以下内容:


点对点网络基本原理


WindowsVista和WindowsCommunicationFoundation中的P2P支持


利用PeerChannel进行开发

本文涉及以下技术:

WindowsVista、.NETFramework3.0

代码下载位置:

P2P2006_10.exe(668KB)

提要栏:


在WindowsXP中安装PNRP


真实环境中PeopleNearMe的示例


本页内容

P2P基本原理:网格网络

对等名称解析协议

PeerChannel

PeopleNearMe

结束语
在我们多数人考虑点对点(P2P)应用程序时,自然而然就会想到即时消息传送应用程序、简单文件共享程序和游戏。一般情况下,在我们考虑分布式应用程序设计时,已经习惯于默认选择客户端/服务器模型,几乎很少会想到P2P模型,哪怕是一闪而过的念头,尤其是对于业务应用程序更是如此。我们为什么都关注于客户端/服务器模型,其主要原因很简单:P2P应用程序的开发过程一惯都成本高昂且耗时过长。

从传统意义来说,P2P应用程序开发所面临的难题包括:需要开发用于消息交换的专用协议;必须查找并连接隐藏在“网络地址转换”(NAT)或防火墙背后的某应用程序的实例;需要支持在广域网(WAN)中定位各应用程序所需的惯常基础结构。尽管上述难题可以克服,但已造成大量阻碍,这样就导致我们很多人都从未想过P2P应用程序所能提供的杰出的协作功能。

这些阻碍将随着WindowsVista™和附带的.NETFramework3.0版本的发布而明显改善。在将WindowsVista的增强功能与“对等名称解析协议”(PNRP)、PeopleNearMe(PNM)和Windows®CommunicationFoundation中引进的PeerChannel功能相结合的情况下,我们又向P2P应用程序跨进了一大步。我个人期望在发布WindowsVista之后,P2P的舞台会更加绚丽多彩。

WindowsVista中的P2P开发是一个极其广泛的主题,没有任何一篇文章可以将其阐述完整。因此,我不会去尝试完成这样一个不可能的任务,而是会介绍WindowsVista中一些不同的P2P技术,并为您对P2P开发工作提供背景资料。

除了假定您已基本了解Windows窗体之外,我还假设您已略微熟悉WindowsCommunicationFoundation应用程序的编写。如果情况并非如此,您可能希望通过阅读WindowsSDK或wcf.netfx3.com/content/resources.aspx中的部分内容开始起步。

P2P基本原理:网格网络

在深入探讨具体的P2P技术之前,研究P2P应用程序的一些基本原理很重要。对初学者而言,P2P应用程序是一个与该应用程序的其他实例直接相连的应用程序。在P2P语言中,该应用程序的每个实例都叫做一个节点。通常将这些节点连接和命名后的组合称为网格。因此,推动P2P应用程序开发的技术经常被称作网格技术。PNRP、PeerChannel(在WindowsCommunicationFoundation中)和PNM都是WindowsVista中网格技术的例子。

网格技术所有WindowsVista网格技术所生成的网格在拓扑结构上都大致相同。通常来讲,网格拓扑是网格中各节点间连接模式的抽象体现。为了解释清楚,请在脑海中设想一个网格。我敢断定,您所设想的网格在某种程度上与图1中所示的网格相似。


图1全连接网格
图1所示网格中四个节点中的每个节点都与网格中的其他所有节点相连。换言之,如果网格中有N个节点,则每个节点都保持N-1个连接。我们将达到此标准的网格视为全连接网格。全连接网格很少被用作适合的方法;要了解原因,让我们注意一下各节点间的连接。

网格中的节点基本上都使用现有的常用传输方式进行通信。与所有的现代操作系统一样,WindowsVista利用TCP/IP和UDP进行网络通信。如果TCP/IP是全连接网格所选的传输方式,那么由N个节点组成的全连接网格中的每个节点都必须创建或接收N-1个套接字。随着N值的增加,此模型显然变得不可行。例如,如果假定一个N=1000的情况,那么每个节点都将需要保持999个套接字,这简直就行不通。

要解决可伸缩性和WAN连接性的问题,您必须诉诸于部分连接的网格,如图2中所示。顾名思义,部分连接网格中的节点只与网格中的其他少量节点相连。就P2P来说,这些相邻的节点被称作邻居。通常来说,部分连接网格对每个节点的资源需求更少,从而大幅提高了网格的可伸缩性。从理论上讲,部分连接网格的规模可以扩大到包括全球所有计算机上的所有应用程序。


图2部分连接网格
加入网格节点加入网格的方式取决于所使用的网格技术,但通常来讲,预期节点必须用网格名来解析网格中已有的一个或多个节点的物理地址。如果您假设一个部分连接网格,网格名的解析结果就是网格中可用物理地址的一个子集。在接收到网格中一个或多个物理节点的物理地址后,预期节点必须立即连接到这些地址中的一个、部分或全部地址。连接到网格之后,新添加的节点必须立即让自己准备好响应来自其他预期节点的后续的网格名解析请求。

网格名解析是一个复杂的主题。这种复杂性在很大程度上是因为,在许多情况下,网格名解析要依赖于一个或更多的其他网格。为清楚说明问题,请想一下美国邮政局使用的网格。再具体点说,假定我需要向我的朋友Rusty邮寄一个包裹。要邮寄这个包裹,我可能需要去一个邮局。如果我不知道距离我最近的邮局地点,我会到Internet上查找最近邮局的地址。从抽象意义上说,要“连接”到美国邮政局的网格需要我先访问所有网格中最大的网格(即Internet)才能解析最近的节点的地址。换言之,您可以使用一个网格去解析包含在另一个网格中的地址。我将在本文的PNRP部分详细讨论这个概念。

与其他节点通信一旦节点连接到某网格,它就可以通过以下两种方式之一与其他节点进行通信:网格扩散(也称多方消息传送),或定向消息传送。顾名思义,网格扩散是试图将消息发送到网格中的所有节点。一般而言,网格中的某节点可通过将消息发送到它的所有邻居而将消息传播到其他所有节点。在接收到消息后,初始发送节点的邻居负责将该消息转发给它的邻居,以此类推。相比之下,定向消息传送指的是试图将消息直接发送到网格中的某特定节点。在部分连接网格中,初始发送节点可能未与目标接收节点相连。如果真是这样,则初始发送节点必须将消息发送给它的一个或多个邻居。其中某个邻居可能会与目标接收节点相连。如果是这样,该邻居就会将消息转发给目标接收节点。如果不是这样,则该邻居会推测它的哪个邻居自身可能会与目标接收节点相连。

网格很少处于静态。在多数P2P应用程序中,节点可能会频繁加入和离开网格,可能是由于网络连接变动引起,也可能是由于在使用即时消息传送应用程序时,用户启动和终止应用程序引起。除了网格中的自然变动之外,多数网格技术都具备一些维护自身的机制。通常而言,网格维护的目标就是修复或调整网格以使其更高效或更稳健地运行。要注意到每个网格技术实现网格维护的方式都不同,这一点很重要。

返回页首

对等名称解析协议

顾名思义,PNRP旨在基于网格名等要素来解析物理地址。PNRP可供带有AdvancedNetworkingPack的WindowsXPServicePack1(SP1)以及WindowsXPSP2和WindowsXPProfessionalx64Edition使用。WindowsVista也将附带PNRP版本2。从最简单的层次看,PNRP本身就是一个采用Windows服务形式的P2P应用程序,并且PNRP节点的网格专用于发现加入到其他网格中的节点的物理地址。(有关此方面的详细信息,请参见提要栏中的“在WindowsXP上安装PNRP”。)

PNRP和IPv6PNRP构建于Internet协议版本6(IPv6)之上。由于IPv6对于多数开发人员而言还相当陌生,因此在讨论PNRP机制之前,非常有必要提及IPv6的至少一个重要方面。在IPv6中,地址是一个128位的值(这可能产生约3.4×1038种地址组合)。这种大小的IPv6地址池使IPv6的最重要功能之一-端对端寻址变为可能,即使这些地址被分割为多个子网并隐藏在NAT之后。有关IPv6以及使其能够在IPv4基础结构中使用的技术的详细信息,请访问microsoft.com/technet/itsolutions/network/ipv6/introipv6.mspx

PNRP示例PNRP功能的原型、结构、错误代码均在WindowsSDK的p2p.h头文件中定义。如果某应用程序想要向PNRP注册一个网格名,它必须用非托管代码通过WindowsAPI实现,或用托管代码通过公共语言运行库(CLR)的P/Invoke工具实现。目前,在WindowsAPI的PNRP部分的.NETFramework中未包含托管包装。但您可以通过使用netsh命令行实用程序来访问PRNP。通过netsh,您可以按以下所示向PNRP注册一个新的PNRP名:


c:/temp>netsh

netsh>p2ppnrppeer

netshp2ppnrppeer>add0.justinsmith

Ok.

命令行参数0.justinsmith即是P2P名。当此命令执行时,PNRP基础结构会生成一个PNRPID,将该PNRPID与P2P名相关联,并为该PNRPID分配一个IPv6和IPv4地址。如果您访问另一台已安装PNRP并已将其启动的计算机,则可以用以下netsh命令解析网格名0.justinsmith:


netshp2ppnrppeer>resolve0.justinsmith

Resolvestarted...

Found:Comment:gonzo

Addresses:[0000:0000:0000:0000:0000:0000:0000:0001]:8350udp

192.168.42.100:8350tcp

现在需要解释一下该解析命令的输出。首先,Comment字段代表注册了0.justinsmith的计算机名称(我在Muppets的基础上为我的计算机命名)。该字段从netsh自动填充,并且不能用作解析进程的一部分。其次,注意一下分配给该节点的IPv6和IPv4地址。这是netsh和允许通过IPv4网络传送IPv6通信量的Teredo转移技术共同具备的一个功能。诚然,我只是浅显地阐述了PNRP,但我已经表明了PNRP使我可以用P2P名来解析IP地址。有关PNRP的详细信息,请访问microsoft.com/technet/prodtechnol/winxppro/deploy/p2pintro.mspx

返回页首

PeerChannel

WindowsCommunicationFoundation的主要优点之一就是,它为许多不同类型的分布式应用程序提供了一个通用的编程模型。例如,编写一个基于TCP/IP通过二进制编码消息进行通信的分布式应用程序所需的代码与编写一个基于HTTP通过符合WS-*的可互操作的消息进行通信的分布式应用程序所需的代码惊人地相似。WindowsCommunicationFoundation的一个鲜为人知的功能就是它支持使用这同一个通用编程模型来构建P2P应用程序。由于WindowsCommunicationFoundation对P2P应用程序的支持,可能会有人将其视为一种网格技术,但实际上,只有PeerChannelWindowsCommunicationFoundation模块才专用于构建P2P应用程序。因此,PeerChannel这个术语通常用于指代WindowsCommunicationFoundation的P2P功能。无论WindowsCommunicationFoundation中的PeerChannel指代的是什么,它实际上消除了通常与P2P应用程序开发相伴的所有复杂性,并且在我看来,它是P2P应用程序开发领域的一个创新性突破。

PeerChannel网格PeerChannel网格是专为消息扩散而设计。但PeerChannel包含了可将消息传播到网格的一部分而不是整个网格的机制。因此,更准确地说,PeerChannel网格是专为多方消息传送而设计。

PeerChannel网格的结构由每个节点所连接的邻居数量所控制。为此,PeerChannel网格会主动维护网格的结构。这种维护的作用就是使网格性能稳健并且分布均匀。更具体点说,网格中的节点会设法将连接的邻居数保持在两到七个之间。这些阈值可使对本地节点的资源需求与保持网格稳健性之间取得平衡。

如果某节点在进入网格时带有三个邻居,然后其中两个邻居离开网格,则该节点将开始一个维护周期以试图获取新的邻居连接。同样,如果一个节点连接的邻居少于七个,则它将接受新连接,直到它具有七个邻居连接时为止。当一个PeerChannel节点连有三个邻居时,则认为它处于理想的连接状态,但一个节点将接受多达七个邻居,以便低于邻居数最低阈值的节点可以迅速获得新的邻居。应注意的是,您的应用程序代码不能更改这些阈值或对网格的维护实施任何控制,这一点很重要。这些细节问题完全由PeerChannel基础结构逐个节点地处理。

PeerChannel提供了PNRP解析程序和自定义解析程序,以供用作预期节点发现网格中已有节点地址的方法。无论选择哪个解析方法,主旨都是一样的:将网格名传递到解析程序并接收网格中其他节点的IP地址列表。一旦解析进程生成地址列表,预期PeerChannel节点就并发连接到每个地址。当PeerChannel网格中已有的某节点收到其中一个连接请求时,它可以接受或拒绝该连接。如果接受该连接,则现有节点会向新连接的节点发送一条欢迎消息,消息中除了其他内容之外,还包含网格中其他节点的地址列表。如果拒绝该连接,则现有节点会向预期节点发送一条拒绝消息,消息中包含拒绝理由以及网格中其他节点的地址列表。

这里的重要一点是,网格名解析(通过PNRP解析程序或自定义解析程序执行)不是向PeerChannel中预期节点返回地址列表的唯一方式。与将网格名解析作为预期节点获取地址的唯一方法相比,此特性可使节点更快速地进入理想的连接状态。此外,这一特性使网格中的节点可对节点拥有的邻居数(这一因素会进而影响网格的稳健性)进行控制。

PeerChannel网格内的通信被调整为尽量减少重复性的消息传递。当网格中的某节点向该网格发送消息时,实际上是在向其邻居发送消息。当收到消息后,每个邻居都会检验该消息,然后将其转发给自己的邻居。如果一个PeerChannel节点从某邻居收到消息,它不会将此消息转发回该邻居。此外,如果一个PeerChannel节点经常从某邻居那里收到先前已收到并处理过的消息,则与该邻居的连接可能会在下一个维护周期中终止。这些功能将通过每个节点上的本地高速缓存来实现。在内部,PeerChannel网格中的每个节点都会对WS-Addressing消息ID的值和传送消息的邻居的标识符进行缓存处理。节点在决定向哪些邻居传送该消息时会检查这个高速缓存。将这些功能相结合后,会将网格调整为以最低的重复率和网络带宽消耗来向网格中的各节点传送消息。

如前所述,PeerChannel节点还可以将消息发送给网格中节点的子集。这可以通过向消息分配一个跳跃计数来实现,这实际上是跟踪转发消息所经由的节点数的方式。不要将这种机制与定向消息传送相混淆,定向消息传送是将消息传送目标锁定在某个特定节点。更确切地说,跳跃计数是模糊界定接收消息的节点数的方式。例如,如果一个PeerChannel节点(节点A)有三个邻居且在跳跃计数为1的情况下向网格发送一个消息,则该消息将被传送到三个节点。同样,如果节点A的每个邻居也都有三个唯一邻居且节点A在跳跃计数为2的情况下向网格发送一个消息,则该消息将被传送到九个节点。但如果节点A的任一邻居有共用的邻居,则此数字将相应减少。

从物理上说,跳跃计数在消息中被表示为标头块中的一个整数。当某节点收到带有跳跃计数的消息时,它会检查跳跃计数的值。如果该值大于零,节点会单调递减此跳跃计数,然后将带有递减后的跳跃计数值的消息转发给相应邻居。如果所接收消息中包含的跳跃计数为0,则不转发该消息。另需注意的重要一点是,跳跃计数标头块被排除在消息签名之外,因此更改这个值不会影响到应用于消息的数字签名的完整性,而且会防止产生与重复生成数字签名并将其序列化到消息相应部分中所关联的开销。

PeerChannel示例让我们用PeerChannel和Windows窗体来构建一个简单的P2P应用程序,称之为PictureViewer。顾名思义,该应用程序的用途是允许网格中的所有节点可以查看同一张图片。从高层次来说,构建此应用程序所需的步骤如下所示:

1.
定义基本的Windows窗体样板代码。
2.
向窗体添加控件。
3.
定义必需的WindowsCommunicationFoundation服务合约。
4.
编写连接到网格和从网格接收消息所需的WindowsCommunicationFoundation代码。
5.
编写向网格中其他节点发送消息所需的代码。
图3显示了完成后的应用程序。步骤1和2是开发任何Windows窗体应用程序时所必需的步骤,因此我将不在这里对其说明。对于任何WindowsCommunicationFoundation应用程序,开发过程的第一步是定义服务合约。PeerChannel要使用的服务合约类似于其他WindowsCommunicationFoundation合约,只不过PeerChannel需要所有的OperationContractAttribute批注都将IsOneWay实例属性设置为true。此属性规定消息的接收方不应发送回复。如果想要接收方发送回复,可以将服务合约定义为双向合约,但每个OperationContractAttribute批注仍必须将IsOneWay实例属性设置为true。就此例而言,我不会创建一个双向合约(WindowsSDK中有几个双向合约的示例)。我要使用的合约如下所示:


[ServiceContract]

interfaceIPictureViewer{

[OperationContract(IsOneWay=true)]

voidSharePicture(Streamstream);

}


图3PictureViewerP2P应用程序
请注意,SharePicture接口方法用OperationContractAttribute属性加以批注,并且IsOneWay实例属性被设置为true。SharePicture操作将System.IO.Stream视为一个参数,因为此操作将被用于向网格中其他节点传送图片的字节。

在定义了我们的服务合约后,现在就该添加WindowsCommunicationFoundation代码,该代码会将我们的应用程序连接到PeerChannel网格并且被动等待来自网格的消息。首先,在窗体中实现新定义的服务合约。然后,定义类型ServiceHost的字段。所接收的消息将被发送到frmPictureViewer类型的单个实例。要表明此功能,我必须将正确的ServiceBehavior分配给frmPictureViewer类型。这两个步骤如图4所示。

接下来,我必须对ServiceHost进行实例化,添加端点并开始侦听外来的消息。由于我正在构建Windows窗体应用程序,因此实现此操作的逻辑位置就是窗体的构造函数,如图5所示。

此时,我已经完成了要连接到网格并侦听消息所需的所有步骤。与标准的WindowsCommunicationFoundation代码相比,不同之处只在于Uri的方案(net.p2p)、所使用的绑定(NetPeerTcpBinding)以及所增添的基于密码的安全性。请注意,我已选择将网格密码直接置于代码中,这一点很重要。如果想要使网格密码保密,请不要在您当前所使用的应用程序中如此操作。

只要调用了ServiceHost.Open,我们的应用程序就会尝试通过PNRP来解析网格名(pictureView)。此时,我可以通过运行netsh命令以列出注册的对等名称来验证我们的PeerChannel应用程序是否正在使用PNRP。如果PNRP可以将网格名解析为一个或多个IP地址,则我们的应用程序将尝试连接到这些节点。如果不是这样,则该节点将成为网格中的第一个节点。如前所述,现有节点将通过发送欢迎消息或拒绝消息来接受或拒绝连接。这里的重要一点是,这种情况有可能在对ServiceHost.Open的调用返回后发生。

将消息发送到其他节点在共享图片之前,我必须首先加载图片。实现此操作所需的代码是Windows窗体基本代码:首先,对OpenFileDialog进行实例化,获得一个Stream,将该Stream转换为Image,然后通过PictureBox.Image属性引用Image。等一下,这不是SharePicture方法所执行的操作吗?事实上,确实如此。从本质上说,要将图像加载到PictureBox中,我只需要调用SharePicture方法,将从OpenFileDialog.OpenFile返回的Stream作为一个参数传递。

要将包含图片的消息发送到网格中的其他节点,我必须编写几行代码,但是此代码与您在其他任何WindowsCommunicationFoundation应用程序中编写的代码几乎相同。起初,我需要在类型ChannelFactory<IPictureViewer>和IPictureViewer的窗体中定义一些字段。接下来,我需要在窗体的构造函数中将这些变量实例化。这些步骤如图6所示。

请注意,所使用的网格密码和证书(用于创建消息的数字签名)与设置ServiceHost时所用的必须是同一个。除此之外,此代码要与非PeerChannelWindowsCommunicationFoundation应用程序中所需的代码相同。

既然我已经构建了自己的发送基础结构,我就可以使用它向网格中的其他节点发送消息。为此,我只需为共享按钮编写一个事件处理程序即可,如下所示:


privatevoidbtnShare_Click(objectsender,EventArgse)

{

using(MemoryStreamstream=newMemoryStream())

{

Imageimage=pbView.Image;

image.Save(stream,ImageFormat.Jpeg);//将图像存储到stream中

stream.Position=0;//复位位置

channel.SharePicture(stream);//向网格发送消息

}

}

简言之,PeerChannel大大简化了P2P应用程序的开发。PictureViewer的完全功能版约有150行源代码,其中大部分代码专用于Windows窗体基础结构。这个功能完全的应用程序包含了一个跳跃计数实现,可以从MSDN®杂志网站下载。

返回页首

PeopleNearMe

PNM是集成在WindowsVista之中的一种网格技术,它允许邻近的设备组和人员组相互发现、连接、邀请并进行协作。PNM特别适用于这样一些任务:在咖啡店与邻座其他几个人一起玩游戏;与同事共享您的桌面;甚至连接到会议室中的投影仪,等等。PNM提供的这些功能如此强大,我们有理由假设,一旦它被发布,开发人员社区就将会找到新的、具有独创性的方式来利用此技术。重要的是,要注意PNM是一项完全自选的网格技术,在默认情况下是关闭状态。

除其他应用程序之外,PNM体系结构包含了一个称为p2phost.exe的P2P应用程序。此进程运行时,将通过连接到其他计算机上p2phost.exe的实例来创建网格。通常而言,此网格的用途是定向消息传送。更确切地说,PNM用于解析本地节点并与这些本地节点的子集进行通信。PNMAPI作为WindowsAPI的一部分提供,并且多半程度上侧重于配置p2phost.exe的行为。

总的来说,PNMAPI的主要类别包括函数、结构、事件和错误代码,通过这些类别可以向PNM注册应用程序、邀请其他人加入协作会话、启动已注册的应用程序、创建持久性合约并邀请不再属于本地的联系人。提要栏中的“真实环境中PeopleNearMe的示例”将例示此过程。请注意,不支持应用程序使用PNM进行通信。就PictureViewer而言,这表示在提要栏中说明的Tom和Harry的PictureViewer实例之间传递的消息仍由PeerChannel来处理。

返回页首

结束语

P2P应用程序开发是一个涉及范围非常广泛的主题,并且对于多数开发人员而言还相当陌生。随着WindowsVista和.NETFramework3.0的发布,P2P应用程序的传统开发门槛将明显降低。我相信,技术的进步(如PNRP、IPv6)加上更具生产力的新型平台的问世(如PeerChannel和PNM)将在P2P应用程序开发领域开创一个新时代。最终,应用程序将更具协作性,并提供我们起初只能想像的一些功能。

JustinSmithJustinSmith在Wintellect担任培训师和顾问。他目前正在为MicrosoftPress撰写《AppliedWindowsCommunicationFoundationProgramming》(WindowsCommunicationFoundation实用编程),计划在2007年1月出版,他还是Wintellect的“MasteringDistributedApplications”(掌握分布式应用程序)课程的作者。您可以通过justins@wintellect.com发送电子邮件与Justin联系。

本文摘自2006年10月发行的MSDN杂志

转到原英文页面

©2006MicrosoftCorporation版权所有。保留所有权利。使用规定。



返回页首

varframesValid=false;
if(window.name=="MNPMainFrame")
{varmenuFrame=parent.frames["MNPMenuFrame"];if(menuFrame){framesValid=true;}}
if(!framesValid)location.replace("/china/MSDN/library/Windev/WindowsVista/PeerToPeer.mspx?mfr=true");

elsedocument.write('

');

top.document.title=self.document.title;


适合打印机打印的版本通过电子邮件发送此页面添加到收藏夹

FavUrl="http://www.microsoft.com/china/MSDN/library/Windev/WindowsVista/PeerToPeer.mspx";FavTitle="利用WindowsVista和WCF中强大的P2P通信功能";Rate=0.0;
DialogWidth=306;lc="zh-chs";DialogHeight=290;
Host="www.microsoft.com";HostUrl="/china/MSDN/library/Windev/WindowsVista/PeerToPeer.mspx";


个人信息中心MSDN中文速递邮件联系我们||
©2006MicrosoftCorporation.版权所有.保留所有权利商标隐私权声明||

varmsviFooter2;if(document.getElementById){msviFooter2=document.getElementById("msviFooter2");msviFooter2.style.filter="";}


内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: