C#编写基于.Net IOCP的高性能服务器

无论什么平台,编写支持高并发性的网络服务器,瓶颈往往出在I/O上,目前最高效的是采用Asynchronous I/O模型,Linux平台提供了epoll,Windows平台提供了I/O Completion Port(IO完成端口,即IOCP)。

Windows自winsock2开始就提供了IOCP支持,可以通过C++直接调用API,但对于基于.Net的C#开发,是在.Net Framework2.0开始才引入的,在2.0版本下,最高效的网络服务器是通过异步Socket的一些列Beginxxx,Endxxx方法实现的, 底层就是基于IOCP的。

当.Net Framework升级到2.0 sp1之后,.Net Socket又提供了一种更高效的一些列xxxAsync方法,对底层IOCP实现性能有不少改进,.Net Framework升级到3.5之后更是成熟稳定,微软也开始大力推广。

在实际应用中,证明C#编写基于.Net IOCP的高性能服务器可以支持10000个以上的TCP长连接。但在具体实现过程中需要注意几个问题:
1.SocketAsyncEventArgs和Buffer最好预先分配,并能回收重复利用。
2.一个Socket的Send和Receive最好分别对应一个SocketAsyncEventArgs,因为当一个 SocketAsyncEventArgs被ReceiveAsync调用挂起后,在调用SendAsync时就会出异常。同样不要对一个 SocketAsyncEventArgs在一个异步操作被挂起时再次调用。

参考文章:

http://msdn.microsoft.com/zh-cn/magazine/cc163356.aspx

http://msdn.microsoft.com/zh-cn/library/system.net.sockets.socketasynceventargs.aspx

http://www.cnblogs.com/keyindex/archive/2010/12/15/1901875.html




MSDN 杂志 >  主页 >  所有期刊 >  2007 >  九月 >   网络: 与 .NET Framework 3.5 接轨
网络
与 .NET Framework 3.5 接轨
Mariya Atanasova and Larry Cleeton and Mike Flasko and Amit Paka

本文讨论:
  • Socket 类性能
  • 国际化 URL
  • System.Net.PeerToPeer 命名空间
本文使用了以下技术:
.NET Framework
在 Microsoft ® .NET Framework 中,System.Net 命名空间公开了 HTTP 和 SMTP 等多种网络协议的功能。即将发布的 .NET Framework 3.5(将随以前代号为“Orcas”的 Visual Studio ® 2008 一起交付)包含了针对这些核心网络层的多种增强性能和功能。在本文中,我们将了解一下 System.Net 团队提供的三个关键更改:
  • 高性能 Socket API
  • 对 URI 的国际化资源标识符支持
  • 对等 (P2P) 命名空间
即将发布的 .NET Framework 3.5 将推出对 Framework 本身所做的更改。本文所描述的功能在 Visual Studio 2008 测试版 1 中提供,该测试版可从 MSDN ® 下载。

Socket 类性能
在 .NET Framework 2.0 版本中,System.Net.Sockets 命名空间提供了一个几乎拥有 Windows ® WinSock Win32 ® API 的所有功能的 Socket 类。该功能所属的类包含为托管代码开发人员设计的各种方法和属性。在 Socket 上,有一组包括 Send 和 Receive 在内的同步方法,具备针对各种情况的参数重载。这些同步方法不仅易于使用,而且非常适合于使用套接字的简单网络任务。Socket 上还有一组基于异步编程模型 (APM) 的异步方法,APM 在 .NET Framework 中非常普遍(有关详细信息,请参阅 msdn.microsoft.com/msdnmag/issues/07/03/ConcurrentAffairs)。这些异步方法让 Socket 类的异步使用相对简单,而且还提供了一种方法来处理许多套接字,或处理在许多套接字上进行的多个发送和接收操作。
2.0 版本的 Socket 类适合多种需要使用网络套接字的客户端应用程序,以及一些服务器和服务类型的应用程序。遗憾的是,一些服务应用程序方案不适用于 2.0 版本的 Socket 类,却和直接使用 Windows WinSock API 的本机语言相容。2.0 版本的 Socket 类的主要问题是它不仅在分配必要的基础对象以便在大量套接字上同时保持 I/O 操作时需要占用过多的 CPU 循环,而且在执行单个套接字 I/O 操作时也同样如此。
凭借 .NET Framework 3.5,公共语言运行时 (CLR) 便可以更有效地同时管理大量的 Overlapped 对象。CLR 中的 Overlapped 对象可以有效封装用于管理异步 I/O 操作的本机 Windows OVERLAPPED 结构。每个进行中的 Socket 异步 I/O 操作中都有一个 Overlapped 对象实例。现在可以拥有 6 万个甚至更多的连接套接字,并同时在每个套接字上保持一个挂起的异步接收 I/O 操作。
2.0 版本的 Socket 类使用 Windows I/O 完成端口来完成异步 I/O 操作。这使应用程序可以轻易地扩展到大量的打开的套接字。.NET Framework 实现了 System.Threading.ThreadPool 类,该类提供可读取完成端口并完成异步 I/O 操作的完成线程。在开发即将发布的 3.5 版本的 .NET Framework 的过程中,我们将大量的精力放在了消除代码路径中的开销上,包括读取完成端口和调用应用程序的完成代理或在 IAsyncResult 对象中发出 I/O 完成事件对象信号之间。
.NET Framework 中的 APM 也称为 Begin/End 模式。这是因为会调用 Begin 方法来启动异步操作,然后返回一个 IAsyncResult 对象。可以选择将一个代理作为参数提供给 Begin 方法,异步操作完成时会调用该方法。或者,一个线程可以等待 IAsyncResult.AsyncWaitHandle。当回调被调用或发出等待信号时,就会调用 End 方法来获取异步操作的结果。这种模式很灵活,使用相对简单,在 .NET Framework 中非常常见。
但是,您必须注意,如果进行大量异步套接字操作,是要付出代价的。针对每次操作,都必须创建一个 IAsyncResult 对象,而且该对象不能被重复使用。由于大量使用对象分配和垃圾收集,这会影响性能。为了解决这个问题,新版本提供了另一个使用套接字上执行异步 I/O 的方法模式。这种新模式并不要求为每个套接字操作分配操作上下文对象。
我们没有创建全新的模式,而只是采用现有模式并做了一个基本更改。现在,在 Socket 类中有了一些方法,它们使用基于事件的完成模型的变体。在 2.0 版本中,您可以使用下列代码在某个套接字上启动异步发送操作:
复制代码
void OnSendCompletion(IAsyncResult ar) { }

IAsyncResult ar = socket.BeginSend(buffer, 0, buffer.Length, 
    SocketFlags.None, OnSendCompletion, state);
在新版本中,您还可以实现:
复制代码
void OnSendCompletion(object src, SocketAsyncEventArgs sae) { }

SocketAsyncEventArgs sae = new SocketAsyncEventArgs();
sae.Completed += OnSendCompletion;
sae.SetBuffer(buffer, 0, buffer.Length);
socket.SendAsync(sae);
这里有一些明显的差别。封装操作上下文的是一个 SocketAsyncEventArgs 对象,而不是 IAsyncResult 对象。该应用程序创建并管理(甚至可以重复使用)SocketAsyncEventArgs 对象。套接字操作的所有参数都由 SocketAsyncEventArgs 对象的属性和方法指定。完成状态也由 SocketAsyncEventArgs 对象的属性提供。最后,需要使用事件处理程序回调完成方法。
所有这些更改都显著改进了 .NET Framework 3.5 中 System.Net.Sockets 类的性能和可伸缩性。现有应用程序可以自动实现其中的两项改进。第三项改进,即新型 Socket 方法,只能通过修改应用程序而得到使用,但这些方法都能为基于套接字的高要求应用程序提供更理想的可伸缩性。

国际化资源标识符支持
Web 地址通常使用由一组非常有限的字符组成的通用资源标识符 (URI) 来表示。一般来说,这些地址中只能包含英文字母表中的大、小写字母、数字 0 到 9 以及少量其他包括逗号和连字符在内的 ASCII 符号。
对于世界上使用非拉丁字母字符集(如日文和希伯莱文)的地区来说,这种语法不是很方便。设想一下诸如 www.BaldwinMuseumOfScience.com 的地址,如果您讲英语,这个地址便很容易理解和记忆。但是,如果您不会说英语,则这个 URL 看上去跟符号的随机排列没什么差别。如果您只会说英语,您能记住用中文写的一长串地址吗?
国际化资源标识符(或 IRI)支持非 ASCII 字符,或者更准确的说是 Unicode/ISO 10646 字符。这意味着域名可以包含 Unicode 字符,即可以有这样的 URL:http://微軟香港.com。
我们已将现有的 System.Uri 类扩展为根据 RFC 3987 提供 IRI 支持(请参见 faqs.org/rfcs/rfc3987.html)。对于当前的用户来说,除非他们特意选择启用 IRI 功能,否则不会看到 .NET Framework 2.0 的行为有任何变化。原因是我们要确保 3.5 版本与以前版本的应用程序兼容。
如果选择采用,您必须做两项更改。首先,将下列元素添加到 machine.config 文件:
复制代码
<section name="uri" type="System.Configuration.UriSection, System, 
                          Version=2.0.0.0, Culture=neutral, 
                          PublicKeyToken=b77a5c561934e089" />
然后,指定是否应将国际化域名 (IDN) 分析应用到域名中,以及是否应该应用 IRI 分析规则。这可以在整个计算机范围的 machine.config 或单个应用程序的 app.config 中进行,如:
复制代码
<configuration>
   <uri>
      <idn enabled="All" />
      <iriParsing enabled="true" />
   </uri>
</configuration>
启用 IDN 可以将域名中的所有 Unicode 标签转换成其 Punicode 等同项。Punicode 名称只含有 ASCII 字符,而且总是以前缀“xn--”开头。这是因为 Internet 上目前部署的大多数 DNS 服务器仅支持 ASCII 字符。启用 IDN 只会影响 Uri.DnsSafeHost 属性的值。对于微軟香港.com 来说,它包含 xn--g5tu63aivy37i.com,而 Uri.Host 将包含 Unicode 字符。
根据您所使用的 DNS 服务器,在 idn 元素的已启用属性中,有三种可能的 IDN 值供您使用:
  • “All”会将 IDN 名称 (Punicode) 用于所有域名。
  • “AllExceptIntranet”会将 IDN 名称用于所有外部域名,而将 Unicode 名称用于所有内部域名。仅当 Intranet DNS 服务器支持 Unicode 名称时,这种情况才适用。
  • “None”是默认值,它和 .NET Framework 2.0 的行为相符。
启用 IRI 分析 (iriParsing enabled = "true") 后,系统会根据 RFC 3987 中的最新 IRI 规则进行规范化和字符检查。当默认值为 false 时,则会根据 RFC 2396(请参见 faqs.org/rfcs/rfc2396.html)进行规范化和字符检查。要了解有关通用资源标识符和 Uri 类的更多信息,请参阅在线文档,地址为 msdn2.microsoft.com/system.uri

System.Net.PeerToPeer 命名空间
作为新增到 .NET Framework 3.5 的一个令人兴奋的新命名空间,System.Net.PeerToPeer 命名空间位于 System.Net.dll 程序集中,它提供了轻松创建对等 (P2P) 应用程序所需的核心构建基块。该命名空间是根据典型的 P2P 应用程序的三个阶段而设计的,即:发现、连接和通信。发现阶段负责动态定位对等点及其相关的网络位置。连接阶段负责在对等点之间建立网络连接。而通信阶段则负责在对等点之间来回传输数据。
System.Net.PeerToPeer 命名空间中的功能提供了很多推动发现和连接阶段的选项。同时,诸如 Socket、HTTPWebRequest 和 Window Communication Foundation Peer Channel 之类的配套技术可以为通信阶段提供解决方案。
在对等点之间可以进行通信之前,它们必须能够发现彼此并根据名称或其他类型的标识符解析它们的网络位置(地址、协议和端口)。由于瞬时连接、DNS 中缺少地址记录以及动态 IP 地址等原因,对等点发现彼此并解析位置的方法很复杂。.NET Framework 3.5 中支持的对等名称解析协议 (PNRP) 功能不仅有助于发现,还可以通过无服务器的名称解析将任何资源解析为一组网络终结点,从而实现对等点之间的通信。PNRP 承担两项核心任务:发布供其他对等点解析的对等名;解析另一个对等点发布的对等名,并检索相关元数据(要了解有关 PNRP 协议如何工作的更多信息,请参阅 microsoft.com/p2p)。
在 System.Net.PeerToPeer 命名空间中,一个对等名代表一个通信终结点,该终结点可以是您希望与元数据相关联的任何对象(如计算机、服务或应用程序)。对等名有两种形式:安全的和不安全的。安全的对等名由一个公/私钥对支持,当使用 PNRP 进行注册时,不会受到欺骗。每个对等名字符串都有一个 Authority 节,后面跟有一个句点,然后是一个 Classifier 节。Authority 节是由计算机根据对等名的类型(安全的或不安全的)生成的;而 classifier 则是用户定义的字符串。
对于一个安全的对等名来说,Framework 会自动创建一个由 40 个字符组成的十六进制字符串作为 Authority。此十六进制字符串是由和对等名关联的公钥组成的哈希 (Hash),用于确保此类对等名注册不会受到欺骗。下面是创建安全对等名的方法:
复制代码
PeerName p = new PeerName("My PeerName", PeerNameType.Secured);
对于不安全的对等名,Authority 组件总是为字符 0。不安全的对等名只是一个字符串,不提供安全保障:
复制代码
PeerName p = new PeerName("My PeerName", PeerNameType.UnSecured);
创建对等名之后,接下来要通过实例化 PeerNameRegistration 对象将此名称与相关元数据关联起来。一个对等名通常与一个 IP 地址相关联,但就像您将看到的一样,一个对等名也可以与一个注解字符串和二进制数据 blob 相关联。在下列代码段中,通过将一个 PeerEndPoint 实例添加到注册终结点集合,可以使一个 IP 地址与对等名产生显式关联:
复制代码
PeerName peerName = new PeerName("My PeerName", PeerNameType.Secured);

PeerNameRegistration pnReg = new PeerNameRegistration();
pnReg.PeerName = peerName;
pnReg.EndPointCollection.Add(new IPEndPoint(
    IPAddress.Parse("<ip address to associate with the name>"), 5000)); 

pnReg.Comment = "up to 39 unicode char comment";
pnReg.Data = System.Text.Encoding.UTF8.GetBytes(
    "A data blob up to 4K associated with the name");
虽然这是对 PeerNameRegistration 类的合法使用,但是我们发现,常见的情形是将所有分配给本地计算机的地址与对等名相关联。要做到这一点,您只要确保 PeerNameRegistration.UseAutoEndPointSelection 的属性设置为 true,并且不要对 PeerNameRegistration.EndPointCollection 进行任何添加即可。
现在所有相关元数据都通过 PeerNameRegistration 对象被分配给该对等名,最后一步是将该对等名发布到群中,以便其他对等点可以解析该名称。在 PNRP 中,群只是一组参与 PNRP 的计算机,它定义众多名称的发布或解析范围。在发布一个名称时,您需要确定要将名称发布到哪个群(或范围)。
PNRP 目前使用两种群:链接-本地群和全局群。一个对等名若发布到链接-本地群就意味着只有同一链接上的其他对等点可以解析该名称。而发布到全局群上的对等名则允许 Internet 上的任何人解析该名称。要将对等名发布到全局群,您只需通过 Cloud.Global 枚举将该全局群分配到 PeerNameRegsitration 对象的 Cloud 属性,然后对注册对象调用 Start 方法即可。一旦 start 方法调用完成,该名称就发布了,而且可以被远程对等点解析。 图 1 显示了用于创建和发布对等名的代码。
复制代码
using System;
using System.Collections.Generic;
using System.Text;
using System.Net.PeerToPeer;

namespace CreateAndPublish
{
    class Program
    {
        static void Main(string[] args)
        {
            // Creates a secured PeerName.
            PeerName peerName = new PeerName(
                "MyWebServer", PeerNameType.Secured);

            PeerNameRegistration pnReg = new PeerNameRegistration();
            pnReg.PeerName = peerName;
            pnReg.Port = 80;

//Starting the registration means the name is published 
//for other peers to resolve.
            pnReg.Start();
            
            Console.WriteLine("Registration of Peer Name: {0} complete.", 
                peerName.ToString());

            Console.WriteLine("Press any key to stop the registration " +
                             "and close the program");
            Console.ReadKey();

            pnReg.Stop();
        }
    }
}

您已了解了如何创建并发布对等名,现在您需要了解如何解析对等名。首先,您要设置 PeerNameResolver 类的实例,然后使用该实例来同步(参见 图 2)或异步解析名称。如果您正在异步解析名称,务必要注意相同的 PeerNameResolver 可以用来解析多个对等名。也就是说,在第一个操作完成之前,您就可以开始对不同对等名启动多个异步解析操作,这样就不必为每个并行的解析操作都实例化一个新的解析程序对象。
复制代码
using System;
using System.Collections.Generic;
using System.Text;
using System.Net.PeerToPeer;
using System.Net;

namespace SyncResolve
{
    class Program
    {
        // The application accepts the peer name to resolve as the first
        // and only command line parameter.
        static void Main(string[] args)
        {
            // Create a resolver object to resolve a peername.
            PeerNameResolver resolver = new PeerNameResolver();
            PeerName peerName = new PeerName(args[0]);
            // Resolve the PeerName - this is a network operation and will
            // block until the resolve request is completed.
            PeerNameRecordCollection results = resolver.Resolve(peerName);

            // Show the data returned by the resolve operation.
            Console.WriteLine("Records from resolution of PeerName: {0}", 
                peerName);
            Console.WriteLine();
            int count = 1;
            foreach (PeerNameRecord record in results)
            {
                Console.WriteLine("Record #{0} results...", count);

                Console.WriteLine("Comment:");
                if (record.Comment != null)
                {
                    Console.WriteLine(record.Comment);
                }                

                Console.WriteLine("Data:");
                if (record.Data != null)
                {
                    //Assumes the data is an ASCII formatted string
                    Console.WriteLine(
                        System.Text.Encoding.ASCII.GetString(record.Data));
                }

                Console.WriteLine("Endpoints:");
                foreach (IPEndPoint endpoint in record.EndPointCollection)
                {
                    Console.WriteLine("\t Endpoint:{0}", endpoint);
                    Console.WriteLine();
                }

                count++;
            }

            Console.ReadKey();
        }
    }
}

此时,您可以使用 图 1图 2 中所示的代码来尝试创建和解析对等名。但要注意,当您尝试使用 PNRP 功能的时候,不能从发布对等名的进程中解析该名称。还应指出,本文介绍的 PNRP API 使用 Windows PNRP 基础结构,并受 Windows XP(安装了 support.microsoft.com/kb/920342 提供的 PNRP 更新)、Windows Vista ® 和 Windows Server 2008 支持。
.NET Framework 3.5 还推出了 System.Net.PeerToPeer.Collaboration 命名空间。协作可在两种环境中进行:第一种是“网络邻居”,适用于已登录到协作基础结构并位于相同子网的用户。(有关此主题的更多详细信息,请参阅 microsoft.com/technet/network/p2p/pnm.mspx。)另一种环境称为“联系人”,适用于那些已添加到 Windows 通讯簿的人员(可以在“联系人”文件夹的“用户”目录中找到它)。在该“联系人”环境中,用户不必位于相同子网上。
这两种环境都允许用户共享应用程序和对象、发送邀请并获取涉及其他用户的事件通知。比如,一名销售人员在机场等班机时需要和她的同事“见面”。理想情形是,这名销售人员应该能打开她的笔记本电脑,启动公司的主要业务线 (LOB) 应用程序,然后让此应用程序动态查找她的同事,而不论他们是和她一起坐在机场还是仍在办公室里。该应用程序首先必须登录协作基础结构,然后指定登录范围。在本例中,PeerScope.All 既包括 Internet 也包括“网络邻居”范围:
复制代码
PeerCollaboration.Signin(PeerScope.All);
登录后,该应用程序便使用以下代码开始查找该销售员附近的所有人员:
复制代码
PeerNearMeCollection peers = PeerCollaboration.GetPeersNearMe();
对与调用方位于相同子网的每个对等点,PeerNearMeCollection 都包含 PeerNearMe 类的一个实例。这样,该应用程序现在有一个所有“网络邻居”(以 PeerNearMe 实例形式存在)的清单。一个 PeerNearMe 实例包括一个用于指定网络位置或远程对等点的 IPEndPoint(IP 地址 + 端口)的属性。
要查找存储在这名销售员通讯簿中的所有联系人,该应用程序必须用下列代码行取得所有联系人:
复制代码
ContactManager contactManager = PeerCollaboration.ContactManager();
PeerContactCollection contacts = contactManager.GetContacts();
ContactManager 代表 Windows 通讯簿,该通讯簿是 System.Net.PeerToPeer.Collaboration 基础结构所使用的联系人存储库。
现在已经生成所有对等点(由 PeerNearMe 和 PeerContact 组成)的清单,该销售员就可以选择和哪些同事交互并迅速给每个人发去邀请。在 System.Net.PeerToPeer.Collaboration 环境中的邀请是请求远程对等点启动某个特定应用程序的一种机制。要在两台对等计算机之间建立网络连接,其中一个对等点需要主动侦听传入的数据。该机制允许一个对等点告知另一方需要运行哪一个应用程序。
就拿上述例子中的销售员来说,她想通过使用公司 LOB 应用程序和另一名同事进行交互。她需要确保她的同事也在运行 LOB 应用程序。因此,她向该同事发出邀请。当该同事的系统收到邀请后,会在该用户面前出现一个对话框,声明某个特定人员希望他们启动一个特定的应用程序(参见 图 3)。对话框提供了“接受”或“拒绝”邀请的选项。如果接收方单击“接受”按钮,邀请中提到的应用程序会在接收方的电脑中自动启动(本例中为 LOB 应用程序),并且假设该应用程序已经安装好。
图 3  从对等点启动应用程序的邀请 (单击该图像获得较大视图)
现在双方各自的系统中都有所需的应用程序在运行,并做好了协作准备。此时,LOB 应用程序使用另一个网络技术(如 Windows Communication Foundation Peer Channel、套接字或 HTTP)进行通信。
我们已经对邀请做了说明,现在让我们看一下用 System.Net.PeerToPeer.Collaboration 命名空间启用邀请所需的详细信息以及相关代码。就 P2P 基础结构来说,应用程序是指您计算机上的任意可执行文件。为了支持邀请,在邀请方和被邀请方的机器上都必须使用相同的 GUID 向对等协作基础结构注册了该应用程序。以下代码显示了如何创建和注册基于本地计算机上安装的可执行文件的应用程序:
复制代码
PeerApplication application = new PeerApplication(
    appGuid, "Collaboration Application", bytes, pathToApp, 
    arguments, PeerScope.Internet);
PeerCollaboration.RegisterApplication(
    application, PeerApplicationRegistrationType.AllUsers);
通过邀请启动的已注册应用程序可以查询是哪个联系人或终结点使用 PeerCollboration.ApplicationLaunchInfo 发送的邀请。这允许刚刚启动的应用程序知道它是通过一个远程对等点发来的邀请启动的,这样它就知道应重新连接到发送该邀请的对等点。发送邀请的方式可以是对 GetPeersNearMe 返回的对等对象调用 Invite,或者对从 GetContacts 返回的联系人调用 Invite,如下所示:
复制代码
PeerInvitationResponse pir = peerNearMe.Invite(
    app, "Hello World", data);

PeerInvitationResponse pir = contact.Invite(
    app, "Hello World", data);
比如,这名销售员和一名同事在机场进行通信,并打算在两人分开后继续。她可以在自己的计算机上将那名同事添加为联系人。这是通过使用 peer.AddToContactManager 从 GetPeersNearMe 调用返回的对等类实现的。一旦这位销售员和她的同事相互将对方添加为联系人,他们就可以在任何地方查找对方,并进行交互。
.NET Framework 3.5 中的协作 API 还能让您共享数据。比如,如果该公司的所有销售人员都希望以电子方式共享其名片,通过使用对等对象便可实现这一愿望。对等对象只是一些可以被远程对等点查看的数据 blob(如图像文件)。各种对象都可以根据范围进行共享,指定“网络邻居”以允许位于相同子网上的用户都能查看这些对象,或在整个 Internet 范围共享以便任何地方的用户都能查看这些对象。比如,要创建一个对象并在“网络邻居”范围共享,您只需使用以下代码:
复制代码
PeerObject object = new PeerObject(
    objectGuid, bytes, PeerScope.NearMe);
PeerCollaboration.SetObject(object);
状态和更改通知也是 P2P 协作的一个重要方面。比如说,我们的销售员一看到在机场的同事 Steve 登录而且有空,就希望与他聊天。在 PeerNearMe 类中提供了一个称为 PeerNearMeChanged 的事件,用于更新位于同一子网的对等点。对等点的任何更改都将触发对关联代理的调用,并发送更改信息。这样,当 Steve 进入(或离开)网络时该销售员都会得到通知。所需代码如下所示:
复制代码
PeerNearMe.PeerNearMeChanged += PeerNearMeChangedCallback;
...
void PeerNearMeChangedCallback(
    object sender, PeerNearMeChangedEventArgs args)
{
        // Check which PeerNearMe has changed and what the change was
        // from the args parameter.
}
为异地联系人提供的更改通知还需要若干其他步骤:该过程要求相互信任,这表示您必须选择监视该联系人,而该联系人也必须选择允许您监视。所以,首先您选择通过对 Contact 调用 Subscribe 方法监视该联系人。然后,您想要监视的对等点必须添加您为联系人,并允许您监视 — 通过将您的 Contact 上的 SubscribeAllowed 属性设置为 SubscriptionType.Allowed。在这些步骤完成后,您可以对您监视的对等点的名称、对象、应用程序等的特定更改进行跟踪。
例如,假设我们的销售员一看到客户联系人上线就希望和他聊天。下列代码将允许这名销售员取得该客户必要的状态更改:
复制代码
custContact.PresenceChanged += ContactPresenceChangedCallback;
...
void ContactPresenceChangedCallback(
    object sender, PresenceChangedEventArgs args)
{
    if (args.PeerPresenceInfo.PresenceStatus ==  
        PeerPresenceStatus.Online)
    {
        // Start chatting with the customer
    }
}
注意,更改信息既可以针对特定对象,也可以针对特定应用程序。向 PeerObject 类中的 ObjectChanged 事件附加一个代理,便可提供对该对象所做更改以及已更改对象来源的信息,例如来自联系人或 PeerNearMe。同样,向 PeerApplication 中的 ApplicationChanged 事件添加一个代理,便可提供与该应用程序的任何更改有关的信息。
例如,若要监视联系人名单中某联系人公开的一系列应用程序中的某个特定应用程序是否更改,必须向 PeerApplication 的 ApplicationChanged 事件添加一个代理:
复制代码
peerApplication.ApplicationChanged += AppChangedCallback;
...
void AppChangedCallback(
    object sender, ApplicationChangedEventArgs args) 
{
    // Check what the change was and which contact and endpoint it
    // originated from the args parameter.
}
现在让我们设想这名销售员马上要做其他事情,因此必须更改她的状态,以便让对等点知道她当下没有时间。通过修改和 PeerCollaboration.LocalPresence 属性关联的数据便可实现这一目的,如下所示:
复制代码
PeerCollaboration.LocalPresenceInfo = new PeerPresenceInfo(
    PeerPresenceStatus.Away, "Talking with Customer");

结束语
本文仅简单介绍了 .NET Framework 3.5 新增的一些核心网络功能。如果您已经迫不及待想体验一番,可以到 msdn2.microsoft.com/aa700831 下载最新 CTP 版本,尝试一下这些新功能。请访问 Windows Network Developer Platform 团队博客 blogs.msdn.com/wndp,随时了解有关 System.Net 以及 Windows 网络技术的最新新闻。






NET Framework 4
2(共 2)对本文的评价是有帮助 - 评价此主题

表示异步套接字操作。

System.Object
   System.EventArgs
     System.Net.Sockets.SocketAsyncEventArgs

命名空间:   System.Net.Sockets
程序集:  System(在 System.dll 中)
C#
C++
F#
VB
public class SocketAsyncEventArgs : EventArgs, 
	IDisposable

SocketAsyncEventArgs 类型公开以下成员。

 名称说明
公共方法SocketAsyncEventArgs创建一个空的 SocketAsyncEventArgs 实例。
页首
 名称说明
公共属性AcceptSocket获取或设置要使用的套接字或创建用于接受与异步套接字方法的连接的套接字。
公共属性Buffer获取要用于异步套接字方法的数据缓冲区。
公共属性BufferList获取或设置一个要用于异步套接字方法的数据缓冲区数组。
公共属性BytesTransferred获取在套接字操作中传输的字节数。
公共属性ConnectByNameError获取在使用 DnsEndPoint 时发生连接失败所引发的异常。
公共属性ConnectSocket成功完成 ConnectAsync 方法后创建和连接的Socket 对象。
公共属性Count获取可在异步操作中发送或接收的最大数据量(以字节为单位)。
公共属性DisconnectReuseSocket获取或设置一个值,该值指定套接字能否在断开操作之后重用。
公共属性LastOperation获取最近使用此上下文对象执行的套接字操作类型。
公共属性Offset获取 Buffer 属性引用的数据缓冲区的偏移量(以字节为单位)。
公共属性ReceiveMessageFromPacketInfo获取接收的数据包的 IP 地址和接口。
公共属性RemoteEndPoint获取或设置异步操作的远程 IP 终结点。
公共属性SendPacketsElements获取或设置要为 Socket.SendPacketsAsync 方法使用的异步操作发送的缓冲区数组。
公共属性SendPacketsFlagsSocket.SendPacketsAsync 方法使用的异步操作获取或设置TransmitFileOptions 值的按位组合。
公共属性SendPacketsSendSize获取或设置发送操作中使用的数据块的大小(以字节为单位)。
公共属性SocketError获取或设置异步套接字操作的结果。
公共属性SocketFlags获取异步套接字操作的结果或设置异步操作的行为。
公共属性UserToken获取或设置与此异步套接字操作关联的用户或应用程序对象。
页首
 名称说明
公共方法Dispose释放由 SocketAsyncEventArgs 实例使用的非托管资源,并可选择释放托管资源。
公共方法Equals(Object)确定指定的 Object 是否等于当前的Object (继承自Object。)
受保护的方法FinalizeSocketAsyncEventArgs 类使用的可用资源。 (重写Object.Finalize()。)
公共方法GetHashCode用作特定类型的哈希函数。 (继承自 Object。)
公共方法GetType获取当前实例的 Type (继承自Object。)
受保护的方法MemberwiseClone创建当前 Object 的浅表副本。 (继承自Object。)
受保护的方法OnCompleted表示异步操作完成时调用的方法。
公共方法SetBuffer(Int32, Int32)设置要用于异步套接字方法的数据缓冲区。
公共方法SetBuffer(Byte[], Int32, Int32)设置要用于异步套接字方法的数据缓冲区。
公共方法ToString返回表示当前对象的字符串。 (继承自 Object。)
页首
 名称说明
公共事件Completed用于完成异步操作的事件。
页首

System.Net.Sockets.Socket 类有一组增强功能,提供可供专用的高性能套接字应用程序使用的可选异步模式,SocketAsyncEventArgs 类就是这一组增强功能的一部分。 该类专为需要高性能的网络服务器应用程序而设计。应用程序可以完全使用增强的异步模式,也可以仅仅在目标热点区域(例如,在接收大量数据时)使用此模式。

这些增强功能的主要特点是可以避免在异步套接字 I/O 量非常大时发生重复的对象分配和同步。当前由 System.Net.Sockets.Socket 类实现的开始/结束设计模式要求为每个异步套接字操作分配一个System.IAsyncResult 对象。

在新的 System.Net.Sockets.Socket 类增强功能中,异步套接字操作由分配的可重用SocketAsyncEventArgs 对象描述并由应用程序维护。高性能套接字应用程序非常清楚地知道必须保持的重叠的套接字操作的量。 应用程序可以根据自身需要创建任意多的 SocketAsyncEventArgs 对象。例如,如果服务器应用程序需要总是有 15 个未完成的套接字接收操作来支持传入客户端连接率,那么可以为此分配 15 个可重用的SocketAsyncEventArgs 对象。

使用此类执行异步套接字操作的模式包含以下步骤:

  1. 分配一个新的 SocketAsyncEventArgs 上下文对象,或者从应用程序池中获取一个空闲的此类对象。

  2. 将该上下文对象的属性设置为要执行的操作(例如,完成回调方法、数据缓冲区、缓冲区偏移量以及要传输的最大数据量)。

  3. 调用适当的套接字方法 (xxxAsync) 以启动异步操作。

  4. 如果异步套接字方法 (xxxAsync) 返回 true,则在回调中查询上下文属性来获取完成状态。

  5. 如果异步套接字方法 (xxxAsync) 返回 false,则说明操作是同步完成的。可以查询上下文属性来获取操作结果。

  6. 将该上下文重用于另一个操作,将它放回到应用程序池中,或者将它丢弃。

新的异步套接字操作上下文对象的生命周期由应用程序代码引用和异步 I/O 引用决定。在对异步套接字操作上下文对象的引用作为一个参数提交给某个异步套接字操作方法之后,应用程序不必保留该引用。在完成回调返回之前将一直引用该对象。 但是,应用程序保留对上下文的引用是有好处的,这样该引用就可以重用于将来的异步套接字操作。

下面的代码示例实现使用 SocketAsyncEventArgs 类的套接字服务器的连接逻辑。接受连接之后,从客户端读取的所有数据都将发回客户端。 客户端模式的读取和回送会一直继续到客户端断开连接。 此示例使用的 BufferManager 类显示在SetBuffer(Byte[], Int32, Int32) 方法的代码示例中。此示例使用的 SocketAsyncEventArgsPool 类显示在 SocketAsyncEventArgs 构造函数的代码示例中。

C#
// Implements the connection logic for the socket server.  
// After accepting a connection, all data read from the client 
// is sent back to the client. The read and echo back to the client pattern 
// is continued until the client disconnects.
class Server
{
    private int m_numConnections;   // the maximum number of connections the sample is designed to handle simultaneously 
    private int m_receiveBufferSize;// buffer size to use for each socket I/O operation 
    BufferManager m_bufferManager;  // represents a large reusable set of buffers for all socket operations
    const int opsToPreAlloc = 2;    // read, write (don't alloc buffer space for accepts)
    Socket listenSocket;            // the socket used to listen for incoming connection requests
    // pool of reusable SocketAsyncEventArgs objects for write, read and accept socket operations
    SocketAsyncEventArgsPool m_readWritePool;
    int m_totalBytesRead;           // counter of the total # bytes received by the server
    int m_numConnectedSockets;      // the total number of clients connected to the server 
    Semaphore m_maxNumberAcceptedClients;

    // Create an uninitialized server instance.  
    // To start the server listening for connection requests
    // call the Init method followed by Start method 
    //
    // <param name="numConnections">the maximum number of connections the sample is designed to handle simultaneously</param>
    // <param name="receiveBufferSize">buffer size to use for each socket I/O operation</param>
    public Server(int numConnections, int receiveBufferSize)
    {
        m_totalBytesRead = 0;
        m_numConnectedSockets = 0;
        m_numConnections = numConnections;
        m_receiveBufferSize = receiveBufferSize;
        // allocate buffers such that the maximum number of sockets can have one outstanding read and 
        //write posted to the socket simultaneously  
        m_bufferManager = new BufferManager(receiveBufferSize * numConnections * opsToPreAlloc,
            receiveBufferSize);

        m_readWritePool = new SocketAsyncEventArgsPool(numConnections);
        m_maxNumberAcceptedClients = new Semaphore(numConnections, numConnections); 
    }

    // Initializes the server by preallocating reusable buffers and 
    // context objects.  These objects do not need to be preallocated 
    // or reused, but it is done this way to illustrate how the API can 
    // easily be used to create reusable objects to increase server performance.
    //
    public void Init()
    {
        // Allocates one large byte buffer which all I/O operations use a piece of.  This gaurds 
        // against memory fragmentation
        m_bufferManager.InitBuffer();

        // preallocate pool of SocketAsyncEventArgs objects
        SocketAsyncEventArgs readWriteEventArg;

        for (int i = 0; i < m_numConnections; i++)
        {
            //Pre-allocate a set of reusable SocketAsyncEventArgs
            readWriteEventArg = new SocketAsyncEventArgs();
            readWriteEventArg.Completed += new EventHandler<SocketAsyncEventArgs>(IO_Completed);
            readWriteEventArg.UserToken = new AsyncUserToken();

            // assign a byte buffer from the buffer pool to the SocketAsyncEventArg object
            m_bufferManager.SetBuffer(readWriteEventArg);

            // add SocketAsyncEventArg to the pool
            m_readWritePool.Push(readWriteEventArg);
        }

    }

    // Starts the server such that it is listening for 
    // incoming connection requests.    
    //
    // <param name="localEndPoint">The endpoint which the server will listening 
    // for connection requests on</param>
    public void Start(IPEndPoint localEndPoint)
    {
        // create the socket which listens for incoming connections
        listenSocket = new Socket(localEndPoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
        listenSocket.Bind(localEndPoint);
        // start the server with a listen backlog of 100 connections
        listenSocket.Listen(100);

        // post accepts on the listening socket
        StartAccept(null);            

        //Console.WriteLine("{0} connected sockets with one outstanding receive posted to each....press any key", m_outstandingReadCount);
        Console.WriteLine("Press any key to terminate the server process....");
        Console.ReadKey();
    }


    // Begins an operation to accept a connection request from the client 
    //
    // <param name="acceptEventArg">The context object to use when issuing 
    // the accept operation on the server's listening socket</param>
    public void StartAccept(SocketAsyncEventArgs acceptEventArg)
    {
        if (acceptEventArg == null)
        {
            acceptEventArg = new SocketAsyncEventArgs();
            acceptEventArg.Completed += new EventHandler<SocketAsyncEventArgs>(AcceptEventArg_Completed);
        }
        else
        {
            // socket must be cleared since the context object is being reused
            acceptEventArg.AcceptSocket = null;
        }

        m_maxNumberAcceptedClients.WaitOne();
        bool willRaiseEvent = listenSocket.AcceptAsync(acceptEventArg);
        if (!willRaiseEvent)
        {
            ProcessAccept(acceptEventArg);
        }
    }

    // This method is the callback method associated with Socket.AcceptAsync 
    // operations and is invoked when an accept operation is complete
    //
    void AcceptEventArg_Completed(object sender, SocketAsyncEventArgs e)
    {
        ProcessAccept(e);
    }

    private void ProcessAccept(SocketAsyncEventArgs e)
    {
        Interlocked.Increment(ref m_numConnectedSockets);
        Console.WriteLine("Client connection accepted. There are {0} clients connected to the server",
            m_numConnectedSockets);

        // Get the socket for the accepted client connection and put it into the 
        //ReadEventArg object user token
        SocketAsyncEventArgs readEventArgs = m_readWritePool.Pop();
        ((AsyncUserToken)readEventArgs.UserToken).Socket = e.AcceptSocket;

        // As soon as the client is connected, post a receive to the connection
        bool willRaiseEvent = e.AcceptSocket.ReceiveAsync(readEventArgs);
        if(!willRaiseEvent){
            ProcessReceive(readEventArgs);
        }

        // Accept the next connection request
        StartAccept(e);
    }

    // This method is called whenever a receive or send operation is completed on a socket 
    //
    // <param name="e">SocketAsyncEventArg associated with the completed receive operation</param>
    void IO_Completed(object sender, SocketAsyncEventArgs e)
    {
        // determine which type of operation just completed and call the associated handler
        switch (e.LastOperation)
        {
            case SocketAsyncOperation.Receive:
                ProcessReceive(e);
                break;
            case SocketAsyncOperation.Send:
                ProcessSend(e);
                break;
            default:
                throw new ArgumentException("The last operation completed on the socket was not a receive or send");
        }       

    }

    // This method is invoked when an asynchronous receive operation completes. 
    // If the remote host closed the connection, then the socket is closed.  
    // If data was received then the data is echoed back to the client.
    //
    private void ProcessReceive(SocketAsyncEventArgs e)
    {
        // check if the remote host closed the connection
        AsyncUserToken token = (AsyncUserToken)e.UserToken;
        if (e.BytesTransferred > 0 && e.SocketError == SocketError.Success)
        {
            //increment the count of the total bytes receive by the server
            Interlocked.Add(ref m_totalBytesRead, e.BytesTransferred);
            Console.WriteLine("The server has read a total of {0} bytes", m_totalBytesRead);

            //echo the data received back to the client
            e.SetBuffer(e.Offset, e.BytesTransferred);
            bool willRaiseEvent = token.Socket.SendAsync(e);
            if (!willRaiseEvent)
            {
                ProcessSend(e);
            }

        }
        else
        {
            CloseClientSocket(e);
        }
    }

    // This method is invoked when an asynchronous send operation completes.  
    // The method issues another receive on the socket to read any additional 
    // data sent from the client
    //
    // <param name="e"></param>
    private void ProcessSend(SocketAsyncEventArgs e)
    {
        if (e.SocketError == SocketError.Success)
        {
            // done echoing data back to the client
            AsyncUserToken token = (AsyncUserToken)e.UserToken;
            // read the next block of data send from the client
            bool willRaiseEvent = token.Socket.ReceiveAsync(e);
            if (!willRaiseEvent)
            {
                ProcessReceive(e);
            }
        }
        else
        {
            CloseClientSocket(e);
        }
    }

    private void CloseClientSocket(SocketAsyncEventArgs e)
    {
        AsyncUserToken token = e.UserToken as AsyncUserToken;

        // close the socket associated with the client
        try
        {
            token.Socket.Shutdown(SocketShutdown.Send);
        }
        // throws if client process has already closed
        catch (Exception) { }
        token.Socket.Close();

        // decrement the counter keeping track of the total number of clients connected to the server
        Interlocked.Decrement(ref m_numConnectedSockets);
        m_maxNumberAcceptedClients.Release();
        Console.WriteLine("A client has been disconnected from the server. There are {0} clients connected to the server", m_numConnectedSockets);

        // Free the SocketAsyncEventArg so they can be reused by another client
        m_readWritePool.Push(e);
    }

}    


.NET Framework
受以下版本支持:4、3.5 SP1、3.0 SP1、2.0 SP1
.NET Framework Client Profile
受以下版本支持:4、3.5 SP1

Windows 7, Windows Vista SP1 或更高版本, Windows XP SP3, Windows Server 2008(不支持服务器核心), Windows Server 2008 R2(支持 SP1 或更高版本的服务器核心), Windows Server 2003 SP2

.NET Framework 并不是对每个平台的所有版本都提供支持。有关支持的版本的列表,请参见.NET Framework 系统要求
此类型的任何公共 static(在 Visual Basic 中为 Shared) 成员都是线程安全的。但不保证所有实例成员都是线程安全的。


  • 0
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
最近有项目要做一个高性能网络服务器,去网络上搜到到的都是C++版本而且是英文或者简单的DEMO,所以自己动手写了C# 的DEMO。 网络上只写接收到的数据,没有说怎么处理缓冲区数据,本DEMO简单的介绍如何处理接收到的数据。简单易用,希望对大家有用. 1、在C#中,不用去面对完成端口的操作系统内核对象,Microsoft已经为我们提供了SocketAsyncEventArgs类,它封装了IOCP的使用。请参考:http://msdn.microsoft.com/zh-cn/library/system.net.sockets.socketasynceventargs.aspx?cs-save-lang=1&cs-lang=cpp#code-snippet-1。 2、我的SocketAsyncEventArgsPool类使用List对象来存储对客户端来通信的SocketAsyncEventArgs对象,它相当于直接使用内核对象时的IoContext。我这样设计比用堆栈来实现的好处理是,我可以在SocketAsyncEventArgsPool池中找到任何一个与服务器连接的客户,主动向它发信息。而用堆栈来实现的话,要主动给客户发信息,则还要设计一个结构来存储已连接上服务器的客户。 3、对每一个客户端不管还发送还是接收,我使用同一个SocketAsyncEventArgs对象,对每一个客户端来说,通信是同步进行的,也就是说服务器高度保证同一个客户连接上要么在投递发送请求,并等待;或者是在投递接收请求,等待中。本例只做echo服务器,还未考虑由服务器主动向客户发送信息。 4、SocketAsyncEventArgs的UserToken被直接设定为被接受的客户端Socket。 5、没有使用BufferManager 类,因为我在初始化时给每一个SocketAsyncEventArgsPool中的对象分配一个缓冲区,发送时使用Arrary.Copy来进行字符拷贝,不去改变缓冲区的位置,只改变使用的长度,因此在下次投递接收请求时恢复缓冲区长度就可以了!如果要主动给客户发信息的话,可以new一个SocketAsyncEventArgs对象,或者在初始化中建立几个来专门用于主动发送信息,因为这种需求一般是进行信息群发,建立一个对象可以用于很多次信息发送,总体来看,这种花销不大,还减去了字符拷贝和消耗。 6、测试结果:(在我的笔记本上时行的,我的本本是T420 I7 8G内存) 100客户 100,000(十万次)不间断的发送接收数据(发送和接收之间没有Sleep,就一个一循环,不断的发送与接收) 耗时3004.6325 秒完成 总共 10,000,000 一千万次访问 平均每分完成 199,691.6 次发送与接收 平均每秒完成 3,328.2 次发送与接收 整个运行过程中,内存消耗在开始两三分种后就保持稳定不再增涨。 看了一下对每个客户端的延迟最多不超过2秒。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值