博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
[转帖]译文:如何使用SocketAsyncEventArgs类(How to use the SocketAsyncEventArgs class)
阅读量:5266 次
发布时间:2019-06-14

本文共 10939 字,大约阅读时间需要 36 分钟。

原文链接:

引言

我一直在探寻一个高性能的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
(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
(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
(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对象的属性提供。最后,需要使用事件处理程序回调完成方法。

转载于:https://www.cnblogs.com/tilv37/p/5016231.html

你可能感兴趣的文章
UE4之数组
查看>>
超分辨率的国内外研究现状
查看>>
PPP 转义字符 编码 和 解码
查看>>
ios开发UI篇—Kvc简单介绍
查看>>
finereport的count函数实现两列比较后进行累加
查看>>
Centos查看端口占用情况
查看>>
oracle 中的日期函数
查看>>
EditTable-V1.0--续集
查看>>
JDBC连接mysql 数据库
查看>>
微信公众号开发 ----------- 接口测试号
查看>>
Java多线程系列--“JUC线程池”05之 线程池原理(四)
查看>>
PHP 数据库连接 (Mysql Mysqli PDO)
查看>>
通俗易懂的讲解iphone视图控制器的生命周期
查看>>
codeforces2015ICL,Finals,Div.1#J Ceizenpok’s formula 扩展Lucas定理 扩展CRT
查看>>
goto 的用法
查看>>
冒泡排序
查看>>
linux command curl and sha256sum implement download verification package
查看>>
LinkedHashMap 源码分析
查看>>
分享一个超棒的jQuery的单页面滚动导航设计插件 - jQuery one page nav
查看>>
【简报】帮助你免费制作单页面个人网站
查看>>