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

译文:如何使用SocketAsyncEventArgs类(How to use the SocketAsyncEventArgs class)的更多相关文章

  1. [转帖]译文:如何使用SocketAsyncEventArgs类(How to use the SocketAsyncEventArgs class)

    原文链接:http://norke.blog.163.com/blog/static/276572082011828104315941/ 引言 我一直在探寻一个高性能的Socket客户端代码.以前,我 ...

  2. c# SocketAsyncEventArgs类的使用 IOCP服务器

    要编写高性能的Socket服务器,为每个接收的Socket分配独立的处理线程的做法是不可取的,当连接数量很庞大时,服务器根本无法应付.要响应庞大的连接数量,需要使用IOCP(完成端口)来撤换并处理响应 ...

  3. C# SocketAsyncEventArgs类

    Namespace:System.Net.Sockets Assemblies:System.Net.Sockets.dll, System.dll, netstandard.dll (Represe ...

  4. 03、Windows Phone 套接字(Socket)实战之WP客户端设计

    因为 PC 端和 WP 端进行通信时,采用的自定义的协议,所以也需要定义 DataType 类来判断 通信数据的类型,并且把数据的描述信息(head) 和数据的实际内容(body)进行拼接和反转,所以 ...

  5. 转 C#高性能Socket服务器SocketAsyncEventArgs的实现(IOCP)

    原创性申明 本文作者:小竹zz  博客地址:http://blog.csdn.net/zhujunxxxxx/article/details/43573879转载请注明出处引言 我一直在探寻一个高性能 ...

  6. SocketAsyncEventArgs

    SocketAsyncEventArgs是.net提供的关于异步socket类,封装了IOCP的使用,可以用它方便的实现NIO(non-blocking IO) NIO对于提升某些场景下Server性 ...

  7. C#高性能Socket服务器SocketAsyncEventArgs的实现(IOCP)

    网址:http://blog.csdn.net/zhujunxxxxx/article/details/43573879 引言 我一直在探寻一个高性能的Socket客户端代码.以前,我使用Socket ...

  8. 高性能TcpServer(C#) - 2.创建高性能Socket服务器SocketAsyncEventArgs的实现(IOCP)

    高性能TcpServer(C#) - 1.网络通信协议 高性能TcpServer(C#) - 2.创建高性能Socket服务器SocketAsyncEventArgs的实现(IOCP) 高性能TcpS ...

  9. .netcore使用SocketAsyncEventArgs Pool需要注意!

    在.net中做网络通讯往往都会用到SocketAsyncEventArgs,为了得到更好的性能配合Pool复用SocketAsyncEventArgs可以得到一个更好的效果,但在dotnet core ...

随机推荐

  1. [LeetCode#218] The Skyline Problem

    Problem: A city's skyline is the outer contour of the silhouette formed by all the buildings in that ...

  2. FindBugs

    FindBugs是一个能静态分析源代码中可能会出现Bug的Eclipse插件工具. 可以从http://sourceforge.net/project/showfiles.php?group_id=9 ...

  3. javascript循环

    <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/ ...

  4. Datetime中yyyy-MM-dd-hh-mm-ss的格式

    namespace yyyy_MM_dd_hh_mm{    class Program    {        static void Main(string[] args)        { wh ...

  5. bzoj1034

    贪心 尽可能让最强的赢,最弱的赢,都不行则最弱打最强 感性的想,我肯定要尽可能的赢,而且赢的要对等 实在不能赢就拿最小的拼,所谓的田忌赛马策略 由于总分一定,己方最差即己方最好时对方的分数 ..] o ...

  6. linux命令 cp 递归复制 带权限复制

    cp -r 递归复制源目录下所有文件及子目录 到 目标目录或文件 cp -p 把源文件或目录下的所具有的权限一同复制 到 目标目录或文件

  7. php--常用的时间处理函数

    天地四方曰宇,往古来今曰宙 时间是世界的重要组成部分,不论花开花落,还是云卷云舒都有它的影子. 但它源起何处?又将去向何方?没人知道答案,也不需要答案,我们需要的只是一个相对的起点来标识时间,现今世界 ...

  8. Kia's Calculation(HDU 4267)

    Problem Description Doctor Ghee is teaching Kia how to calculate the sum of two integers. But Kia is ...

  9. Mac下go语言goclipse插件安装部署

    Try using this URL as a Eclipse Software Site:https://raw.githubusercontent.com/GoClipse/goclipse.gi ...

  10. 【解决】Internet访问看似正常(无叹号受限)却打不开网页

    嘛╮(╯▽╰)╭ 可能是前几天中了一等奖败了人品 .. 今天果断受点小挫折 事情是这样的:昨晚电脑在不插电的情况下打了一小时“剑灵”,有点烫,电量剩30%,关机睡觉,今早发现上不去网页了! 桌面右下角 ...