热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

译文:如何使用SocketAsyncEventArgs类(HowtousetheSocketAsyncEventArgsclass)

转载自:http:blog.csdn.nethulihuiarticledetails3244520原文:HowtousetheSocketAsyncE
转载自: http://blog.csdn.net/hulihui/article/details/3244520

 

  • 原文:How to use the SocketAsyncEventArgs class. by Marcos Hidalgo Nunes
  •  Download client - 4.09 KB
  •  Download server - 7.5 KB

 

引言

 

我一直在探寻一个高性能的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/zhaox583132460/p/3383265.html


推荐阅读
  • 如何查询zone下的表的信息
    本文介绍了如何通过TcaplusDB知识库查询zone下的表的信息。包括请求地址、GET请求参数说明、返回参数说明等内容。通过curl方法发起请求,并提供了请求示例。 ... [详细]
  • 本文介绍了C#中生成随机数的三种方法,并分析了其中存在的问题。首先介绍了使用Random类生成随机数的默认方法,但在高并发情况下可能会出现重复的情况。接着通过循环生成了一系列随机数,进一步突显了这个问题。文章指出,随机数生成在任何编程语言中都是必备的功能,但Random类生成的随机数并不可靠。最后,提出了需要寻找其他可靠的随机数生成方法的建议。 ... [详细]
  • 本文介绍了如何使用JSONObiect和Gson相关方法实现json数据与kotlin对象的相互转换。首先解释了JSON的概念和数据格式,然后详细介绍了相关API,包括JSONObject和Gson的使用方法。接着讲解了如何将json格式的字符串转换为kotlin对象或List,以及如何将kotlin对象转换为json字符串。最后提到了使用Map封装json对象的特殊情况。文章还对JSON和XML进行了比较,指出了JSON的优势和缺点。 ... [详细]
  • 图像因存在错误而无法显示 ... [详细]
  • Android实战——jsoup实现网络爬虫,糗事百科项目的起步
    本文介绍了Android实战中使用jsoup实现网络爬虫的方法,以糗事百科项目为例。对于初学者来说,数据源的缺乏是做项目的最大烦恼之一。本文讲述了如何使用网络爬虫获取数据,并以糗事百科作为练手项目。同时,提到了使用jsoup需要结合前端基础知识,以及如果学过JS的话可以更轻松地使用该框架。 ... [详细]
  • 本文介绍了数据库的存储结构及其重要性,强调了关系数据库范例中将逻辑存储与物理存储分开的必要性。通过逻辑结构和物理结构的分离,可以实现对物理存储的重新组织和数据库的迁移,而应用程序不会察觉到任何更改。文章还展示了Oracle数据库的逻辑结构和物理结构,并介绍了表空间的概念和作用。 ... [详细]
  • 本文分享了一个关于在C#中使用异步代码的问题,作者在控制台中运行时代码正常工作,但在Windows窗体中却无法正常工作。作者尝试搜索局域网上的主机,但在窗体中计数器没有减少。文章提供了相关的代码和解决思路。 ... [详细]
  • 开发笔记:加密&json&StringIO模块&BytesIO模块
    篇首语:本文由编程笔记#小编为大家整理,主要介绍了加密&json&StringIO模块&BytesIO模块相关的知识,希望对你有一定的参考价值。一、加密加密 ... [详细]
  • Metasploit攻击渗透实践
    本文介绍了Metasploit攻击渗透实践的内容和要求,包括主动攻击、针对浏览器和客户端的攻击,以及成功应用辅助模块的实践过程。其中涉及使用Hydra在不知道密码的情况下攻击metsploit2靶机获取密码,以及攻击浏览器中的tomcat服务的具体步骤。同时还讲解了爆破密码的方法和设置攻击目标主机的相关参数。 ... [详细]
  • http:my.oschina.netleejun2005blog136820刚看到群里又有同学在说HTTP协议下的Get请求参数长度是有大小限制的,最大不能超过XX ... [详细]
  • 本文介绍了作者在开发过程中遇到的问题,即播放框架内容安全策略设置不起作用的错误。作者通过使用编译时依赖注入的方式解决了这个问题,并分享了解决方案。文章详细描述了问题的出现情况、错误输出内容以及解决方案的具体步骤。如果你也遇到了类似的问题,本文可能对你有一定的参考价值。 ... [详细]
  • Java中包装类的设计原因以及操作方法
    本文主要介绍了Java中设计包装类的原因以及操作方法。在Java中,除了对象类型,还有八大基本类型,为了将基本类型转换成对象,Java引入了包装类。文章通过介绍包装类的定义和实现,解答了为什么需要包装类的问题,并提供了简单易用的操作方法。通过本文的学习,读者可以更好地理解和应用Java中的包装类。 ... [详细]
  • 本文介绍了一个React Native新手在尝试将数据发布到服务器时遇到的问题,以及他的React Native代码和服务器端代码。他使用fetch方法将数据发送到服务器,但无法在服务器端读取/获取发布的数据。 ... [详细]
  • 本文分析了Wince程序内存和存储内存的分布及作用。Wince内存包括系统内存、对象存储和程序内存,其中系统内存占用了一部分SDRAM,而剩下的30M为程序内存和存储内存。对象存储是嵌入式wince操作系统中的一个新概念,常用于消费电子设备中。此外,文章还介绍了主电源和后备电池在操作系统中的作用。 ... [详细]
  • 基于移动平台的会展导游系统APP设计与实现的技术介绍与需求分析
    本文介绍了基于移动平台的会展导游系统APP的设计与实现过程。首先,对会展经济和移动互联网的概念进行了简要介绍,并阐述了将会展引入移动互联网的意义。接着,对基础技术进行了介绍,包括百度云开发环境、安卓系统和近场通讯技术。然后,进行了用户需求分析和系统需求分析,并提出了系统界面运行流畅和第三方授权等需求。最后,对系统的概要设计进行了详细阐述,包括系统前端设计和交互与原型设计。本文对基于移动平台的会展导游系统APP的设计与实现提供了技术支持和需求分析。 ... [详细]
author-avatar
我是王灿_246
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有