SocketAsyncEventArgs

本文介绍如何使用SocketAsyncEventArgs类实现非阻塞I/O(NIO),以提高服务器性能和吞吐量。通过具体代码示例展示了如何在.NET框架中使用此类进行异步Socket编程。

SocketAsyncEventArgs

SocketAsyncEventArgs是.net提供的关于异步socket类,封装了IOCP的使用,可以用它方便的实现NIO(non-blocking IO)

NIO对于提升某些场景下Server性能和吞吐量有很大益处,准备在服务框架中使用它来编写简易rpc的部分

微软官方的demo代理有所缺少:

http://msdn.microsoft.com/en-us/library/system.net.sockets.socketasynceventargs.aspx

还有篇老外写的补充了缺少的代码:

http://www.codeproject.com/KB/IP/socketasynceventargssampl.aspx

不过例子还是感觉复杂了点,我只是需要知道SocketAsyncEventArgs本身如何使用而已,于是自行简化了一下:

   1:  using System;
   2:  using System.Net;
   3:  using System.Net.Sockets;
   4:  using System.Text;
   5:   
   6:  namespace SocketAsyncServer
   7:  {
   8:      public static class Program
   9:      {
  10:          public static void Main(String[] args)
  11:          {
  12:              IPAddress[] addressList = Dns.GetHostEntry(Environment.MachineName).AddressList;
  13:              new TcpListener().Listen(new IPEndPoint(addressList[addressList.Length - 1], 9900));
  14:   
  15:              Console.ReadKey();
  16:          }
  17:      }
  18:   
  19:      public class TcpListener
  20:      {
  21:          private SocketAsyncEventArgs Args;
  22:          private Socket ListenerSocket;
  23:          private StringBuilder buffers;
  24:          public TcpListener() { }
  25:          public void Listen(EndPoint e)
  26:          {
  27:              //buffer
  28:              buffers = new StringBuilder();
  29:              //socket
  30:              ListenerSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
  31:              ListenerSocket.Bind(e);
  32:              ListenerSocket.Listen(10);
  33:              //异步socket事件
  34:              Args = new SocketAsyncEventArgs();
  35:              Args.Completed += new EventHandler<SocketAsyncEventArgs>(ProcessAccept);
  36:              BeginAccept(Args);
  37:              Console.WriteLine("server run at {0}", e.ToString());
  38:          }
  39:   
  40:          //开始接受
  41:          void BeginAccept(SocketAsyncEventArgs e)
  42:          {
  43:              e.AcceptSocket = null;
  44:              if (!ListenerSocket.AcceptAsync(e))
  45:                  ProcessAccept(ListenerSocket, e);
  46:          }
  47:          //接受完毕 开始接收和发送
  48:          void ProcessAccept(object sender, SocketAsyncEventArgs e)
  49:          {
  50:              Socket s = e.AcceptSocket;
  51:              e.AcceptSocket = null;
  52:   
  53:              int bufferSize = 10;//1000 * 1024;
  54:              var args = new SocketAsyncEventArgs();
  55:              args.Completed += new EventHandler<SocketAsyncEventArgs>(OnIOCompleted);
  56:              args.SetBuffer(new byte[bufferSize], 0, bufferSize);
  57:              args.AcceptSocket = s;
  58:              if (!s.ReceiveAsync(args))
  59:                  this.ProcessReceive(args);
  60:   
  61:              BeginAccept(e);
  62:          }
  63:   
  64:          //IOCP回调
  65:          void OnIOCompleted(object sender, SocketAsyncEventArgs e)
  66:          {
  67:              switch (e.LastOperation)
  68:              {
  69:                  case SocketAsyncOperation.Receive:
  70:                      this.ProcessReceive(e);
  71:                      break;
  72:                  case SocketAsyncOperation.Send:
  73:                      this.ProcessSend(e);
  74:                      break;
  75:                  default:
  76:                      throw new ArgumentException("The last operation completed on the socket was not a receive or send");
  77:              }
  78:          }
  79:          
  80:          //接收完毕
  81:          void ProcessReceive(SocketAsyncEventArgs e)
  82:          {
  83:              if (e.BytesTransferred > 0)
  84:              {
  85:                  if (e.SocketError == SocketError.Success)
  86:                  {
  87:                      //读取
  88:                      var data=Encoding.ASCII.GetString(e.Buffer, e.Offset, e.BytesTransferred);
  89:                      buffers.Append(data);
  90:                      Console.WriteLine("Received:{0}", data);
  91:   
  92:                      if (e.AcceptSocket.Available == 0)
  93:                      {
  94:                          //读取完毕
  95:                          Console.WriteLine("Receive Complete.Data:{0}", buffers.ToString());
  96:                          //重置
  97:                          buffers = new StringBuilder();
  98:                          //发送反馈
  99:                          Byte[] sendBuffer = Encoding.ASCII.GetBytes("result from server");
 100:                          e.SetBuffer(sendBuffer, 0, sendBuffer.Length);
 101:                          if (!e.AcceptSocket.SendAsync(e))
 102:                          {
 103:                     
 104:                              this.ProcessSend(e);
 105:                          }
 106:                      }
 107:                      else if (!e.AcceptSocket.ReceiveAsync(e))
 108:                      {
 109:                          this.ProcessReceive(e);
 110:                      }
 111:                  }
 112:                  else
 113:                  {
 114:                      //this.ProcessError(e);
 115:                  }
 116:              }
 117:              else
 118:              {
 119:                  //this.CloseClientSocket(e);
 120:              }
 121:          }
 122:          //发送完毕
 123:          void ProcessSend(SocketAsyncEventArgs e)
 124:          {
 125:              if (e.SocketError == SocketError.Success)
 126:              {
 127:   
 128:                  if (!e.AcceptSocket.ReceiveAsync(e))
 129:                  {
 130:                      this.ProcessReceive(e);
 131:                  }
 132:              }
 133:              else
 134:              {
 135:   
 136:              }
 137:          }
 138:      }
 139:       
 140:  } 

上述代码run起来之后,打开cmd用telnet测试下即可

telnet 127.0.0.1 9900

顺便推荐一下园子兄弟写的一个框架实现了nio的rpc

http://shuttler.codeplex.com/

http://www.cnblogs.com/overred/archive/2009/12/20/Shuttler_Net_2.html

转自:http://www.cnblogs.com/wsky/archive/2011/04/06/2007201.html

==============================================

转自:http://blog.youkuaiyun.com/hulihui/article/details/3244520

引言

我一直在探寻一个高性能的Socket客户端代码。以前,我使用Socket类写了一些基于传统异步编程模型的代码(BeginSend、BeginReceive,等等)。但它没有满足我所要的性能需求。终于,我找到了基于事件的异步操作新模式(参见2007年9月MSDN杂志上的“ 连接.NET框架3.5 ”)(部分内容见文后的 翻译附注 ——译者注)。

背景

由于减少了阻塞线程,高性能I/O限制应用中广泛使用异步编程模型(AMP,Asynchronous Programming Model)。.NET Framework第一个版本就实现了APM,现在使用诸如lambda表达式等新的技术C#3.0一直在改进其性能。针对Socket编程,不仅性能上提升了不少,而且新APM模型发布了一个更简易的编程方法,该方法使用SocketAsyncEventArgs类来保持I/O操作之间的上下文(见文后的 翻译附注 ——译者注),从而降低对象分配和垃圾收集工作。

在.NET 2.0 SP1上可以使用SocketAsyncEventArgs类,本文的代码就是用Microsoft Visual Studio .NET 2005编写的。

使用代码

从SocketAsyncEventArgs类开始,我学习了 MSDN 上的样例程序,但该文缺少一些内容:AsyncUserToken类。我认为这个类应该公开一个Socket属性,它对应执行I/O操作的Socket。一段时间后,我认识到这个类不是必要的,因为属性UserToken是一个Object,它可以接受任何东西。下面的修改方法中直接使用一个Socket实例当作UserToken。
 //  处理Socket侦听者接收。 
 private void ProcessAccept(SocketAsyncEventArgs e)
 {
     if (e.BytesTransferred > 0)
     {
         Interlocked.Increment(ref numConnectedSockets);
         Console.WriteLine( "Client connection accepted. "
                 "There are {0} clients connected to the server",
                 numConnectedSockets);
     }

     //  获取接受的客户端连接,赋给ReadEventArg对象的UserToken。 
     SocketAsyncEventArgs readEventArgs = readWritePool.Pop();
     readEventArgs.UserToken = e.AcceptSocket;

     //  一旦客户端连接,提交一个连接接收。 
     Boolean willRaiseEvent = e.AcceptSocket.ReceiveAsync(readEventArgs);
     if (!willRaiseEvent)
     {
         ProcessReceive(readEventArgs);
     }

     //  接受下一个连接请求。 
     StartAccept(e);
 }

 //  当一个异步接收操作完成时调用该方法。 
 //  如果远程主机关闭了连接,该Socket也关闭。 
 //  如果收到数据,则回返到客户端。 
 private void ProcessReceive(SocketAsyncEventArgs e)
 {
     //  检查远程主机是否关闭了连接。 
     if (e.BytesTransferred > 0)
     {
         if (e.SocketError == SocketError.Success)
         {
             Socket s = e.UserToken as Socket;

             Int32 bytesTransferred = e.BytesTransferred;

             //  从侦听者获取接收到的消息。 
             String received = Encoding.ASCII.GetString(e.Buffer,
                               e.Offset, bytesTransferred);

             //  增加服务器接收的总字节数。
             Interlocked.Add(ref totalBytesRead, bytesTransferred);
             Console.WriteLine("Received: /"{0}/". The server has read" + 
                               " a total of {1} bytes.", received,
                               totalBytesRead);

             //  格式化数据后发回客户端。 
             Byte [] sendBuffer =
               Encoding.ASCII.GetBytes("Returning "  + received);

             //  设置传回客户端的缓冲区。 
             e.SetBuffer(sendBuffer, 0, sendBuffer.Length);
             Boolean  willRaiseEvent = s.SendAsync(e);
             if (!willRaiseEvent)
             {
                 ProcessSend(e);
             }
         }
         else
         {
             CloseClientSocket(e);
         }
     }
 }

 //  当异步发送操作完成时调用该方法。 
 //  当Socket读客户端的任何附加数据时,该方法启动另一个接收操作。 
 private void ProcessSend(SocketAsyncEventArgs e)
 {
     if (e.SocketError == SocketError.Success)
     {
         //  完成回发数据到客户端。 
         Socket s = e.UserToken as Socket;
         //  读取从发送客户端发送的下一个数据块。 
         Boolean willRaiseEvent = s.ReceiveAsync(e);
         if (!willRaiseEvent)
         {
             ProcessReceive(e);
         }
     }
     else
     {
         CloseClientSocket(e);
     }
 }
我修改了如何操作侦听者收到消息的代码——不是简单地回发给客户端(参见ProcessReceive方法)。在样例程序中,我使用属性Buffer、Offset与BytesTransfered来接收消息,SetBuffer方法把修改后的消息回返给客户端。

为了控制侦听者生存期时间,使用了一个Mutex类的实例。基于原Init方法的Start方法创建Mutex对象,相应的Stop方法释放Mutex对象。这些方法适用于实现作为Windows服务的Socket服务器。
 //  启动服务器并开始侦听传入连接请求。 
 internal void Start(Object data)
 {
     Int32 port = (Int32)data;

     //  获取主机相关信息。 
     IPAddress[] addressList = 
            Dns.GetHostEntry(Environment.MachineName).AddressList;
     //  获取侦听者所需的端点(endpoint)。 
     IPEndPoint localEndPoint = 
            new IPEndPoint(addressList[addressList.Length - 1], port);

     //  创建侦听传入连接的Socket。 
     this.listenSocket = new Socket(localEndPoint.AddressFamily,
                         SocketType.Stream, ProtocolType.Tcp);

     if (localEndPoint.AddressFamily == AddressFamily.InterNetworkV6)
     {
         //  设置Socket侦听者的双模式(IPv4与IPv6)。 
         //  27等价于IPV6_V6ONLY Socket
         //  Winsock片段中的如下选项, 
         //  根据 Creating IP Agnostic Applications - Part 2 (Dual Mode Sockets) 
         //  创建IP的不可知应用——第2部分(双模式 Sockets)
         
         this.listenSocket.SetSocketOption(SocketOptionLevel.IPv6,
                                          (SocketOptionName)27, false);
         this.listenSocket.Bind(new IPEndPoint(IPAddress.IPv6Any,
                                localEndPoint.Port));
     }
     else
     {
         //  Socket与本地端点关联。 
         this.listenSocket.Bind(localEndPoint);
     }

     //  启动侦听队列最大等待数为100个连接的服务器。
     this.listenSocket.Listen(100);

     //  提交一个侦听Socket的接收任务。 
     this.StartAccept(null);

     mutex.WaitOne();
 }

 //  停止服务器。 
 internal void Stop()
 {
     mutex.ReleaseMutex();
 }
现在,我们有了一个Socket服务器,下一步使用SocketAsyncEventArgs类建立一个Socket客户端。虽然MSDN说这个类特别设计给网络服务器应用,但也没有限制在客户端代码中使用APM。下面给出了SocketClient类的样例代码:
 using System;
 using System.Net;
 using System.Net.Sockets; 
 using System.Text;
 using System.Threading;

 namespace SocketAsyncClient
 {
     //  实现Socket客户端的连接逻辑。 
     internal sealed class SocketClient: IDisposable
     {
         //  Socket操作常数。
         private const Int32 ReceiveOperation = 1, SendOperation = 0;

         //  用于发送/接收消息的Socket。 
         private Socket clientSocket;

         //  Socket连接标志。 
         private Boolean connected = false;

         //  侦听者端点。 
         private IPEndPoint hostEndPoint;

         //  触发连接。 
         private static AutoResetEvent autoConnectEvent =
                               new AutoResetEvent(false);

         //  触发发送/接收操作。 
         private static AutoResetEvent[]
                 autoSendReceiveEvents = new AutoResetEvent[]
         {
             new AutoResetEvent(false),
             new AutoResetEvent(false)
         };

         //  创建一个未初始化的客户端实例。 
         //  启动传送/接收处理将调用Connect方法,然后是SendReceive方法。 
         internal SocketClient(String hostName, Int32 port)
         {
             //  获取主机有关的信息。 
             IPHostEntry host = Dns.GetHostEntry(hostName);

             //  主机地址。 
             IPAddress[] addressList = host.AddressList;

             //  实例化端点和Socket。 
             hostEndPoint = new IPEndPoint(addressList[addressList.Length - 1], port);
             clientSocket = new Socket(hostEndPoint.AddressFamily,
                                SocketType.Stream, ProtocolType.Tcp);

         //  连接主机。 
         internal void Connect()
         {
             SocketAsyncEventArgs connectArgs = new SocketAsyncEventArgs();

             connectArgs.UserToken = clientSocket;
             connectArgs.RemoteEndPoint = hostEndPoint;
             connectArgs.Completed += 
                new EventHandler<socketasynceventargs>(OnConnect);

             clientSocket.ConnectAsync(connectArgs);
             autoConnectEvent.WaitOne();

             SocketError errorCode = connectArgs.SocketError;
             if (errorCode != SocketError.Success)
             {
                 throw new SocketException((Int32)errorCode);
             } 
         }

         ///  与主机断开连接。 
         internal void isconnect()
         {
             clientSocket.Disconnect(false);
         }

         //  连接操作的回调方法
         private void OnConnect(object sender, SocketAsyncEventArgs e)
         {
             //  发出连接完成信号。 
             autoConnectEvent.Set();

             //  设置Socket已连接标志。 
             connected = (e.SocketError == SocketError.Success);
         }

         //  接收操作的回调方法
         private void OnReceive(object sender, SocketAsyncEventArgs e)
         {
             //  发出接收完成信号。 
             autoSendReceiveEvents[SendOperation].Set();
         }

         //  发送操作的回调方法
         private void OnSend(object sender, SocketAsyncEventArgs e)
         {
             //  发出发送完成信号。 
             autoSendReceiveEvents[ReceiveOperation].Set();

             if (e.SocketError == SocketError.Success)
             {
                 if (e.LastOperation == SocketAsyncOperation.Send)
                 {
                     //  准备接收。 
                     Socket s = e.UserToken as Socket;

                     byte [] receiveBuffer = new byte [255];
                     e.SetBuffer(receiveBuffer, 0, receiveBuffer.Length);
                     e.Completed += new EventHandler<socketasynceventargs>(OnReceive);
                     s.ReceiveAsync(e);
                 }
             }
             else
             {
                 ProcessError(e);
             }
         }

         //  失败时关闭Socket,根据SocketError抛出异常。 
         private void ProcessError(SocketAsyncEventArgs e)
         {
             Socket s = e.UserToken as Socket;
             if (s.Connected)
             {
                 //  关闭与客户端关联的Socket
                 try
                 {
                     s.Shutdown(SocketShutdown.Both);
                 } 
                 catch (Exception)
                 {
                     //  如果客户端处理已经关闭,抛出异常 
                 }
                 finally
                 {
                     if (s.Connected)
                     {
                         s.Close();
                     }
                 }
             }

             //  抛出SocketException 
             throw new SocketException((Int32)e.SocketError);
         }

         //  与主机交换消息。 
         internal String SendReceive(String message)
         { 
             if (connected)
             {
                 //  创建一个发送缓冲区。 
                 Byte [] sendBuffer = Encoding.ASCII.GetBytes(message);

                 //  准备发送/接收操作的参数。 
                 SocketAsyncEventArgs completeArgs = new SocketAsyncEventArgs();
                 completeArgs.SetBuffer(sendBuffer, 0, sendBuffer.Length);
                 completeArgs.UserToken = clientSocket;
                 completeArgs.RemoteEndPoint = hostEndPoint;
                 completeArgs.Completed +=
                   new EventHandler<socketasynceventargs>(OnSend);

                 //  开始异步发送。 
                 clientSocket.SendAsync(completeArgs);

                 //  等待发送/接收完成。 
                 AutoResetEvent.WaitAll(autoSendReceiveEvents);

                 //  从SocketAsyncEventArgs缓冲区返回数据。 
                 return Encoding.ASCII.GetString(completeArgs.Buffer,
                        completeArgs.Offset, completeArgs.BytesTransferred);
             }
             else
             {
                 throw new SocketException((Int32)SocketError.NotConnected);
             }
         }

         #region IDisposable Members

         // 释放SocketClient实例。 
         public void Dispose()
         {
             autoConnectEvent.Close();
             autoSendReceiveEvents[SendOperation].Close();
             autoSendReceiveEvents[ReceiveOperation].Close();
             if (clientSocket.Connected)
             {
                 clientSocket.Close();
             }
         }

         #endregion
     }
 }

兴趣点

我有服务器群场景下的Socket服务器运行的经验。这种场景中,不能使用主机地址列表的第一项,而要使用最后一项,在前面的Start方法中可以看到这一点。另一个技巧就是如何为IP6地址族设置双模式,这对于那些想在Windows Vista和Windows Server 2008上运行Socket服务器是有帮助的,它们默认IP6。

本文的两个程序都使用命令行参数运行。如果服务器和客户端均运行在一个Windows域之外的机器上,客户端代码必须替换“localhost”为主机名而不是机器名。

历史

  • 15 January, 2008 - 提交初版。

翻译附注

作为IOCP关键类SocketAsyncEventArgs的补充知识,摘抄2007年9月MSDN杂志上的“ 连接.NET框架3.5 ”的部分内容如下:

.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对象的属性提供。最后,需要使用事件处理程序回调完成方法。



最近有项目要做一个高性能网络服务器,决定下功夫搞定完成端口(IOCP),最终花了一个星期终于把它弄清楚了,并用C++写了一个版本,效率很不错。 但,从项目的总体需求来考虑,最终决定上.net平台,因此又花了一天一夜弄出了一个C#版,在这与大家分享。 一些心得体会: 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秒。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值