翻译:使用.net3.5的缓存池和SocketAsyncEventArgs类创建socket服务器

来源:互联网 发布:二维坐标旋转矩阵 编辑:程序博客网 时间:2024/04/28 00:48

 在.NET 3.5里System.Net.Sockets空间下有一组增强功能的类,提供可供专用的高性能套接字应用程序使用的可选异步模式,SocketAsyncEventArgs 类就是这一组增强功能的一部分。该类专为需要高性能的网络服务器应用程序而设计。应用程序可以完全使用增强的异步模式,也可以仅仅在目标热点区域(例如,在接收大量数据时)使用此模式。以下是关于此类的介绍(摘自MSDN)
http://msdn.microsoft.com/zh-cn/library/system.net.sockets.socketasynceventargs.aspx

其实主要是改进了异步模式,让一些类可以重用,可能用的对象池的原理,不像以前的异步传输模式,每个数据来了,new一个新的iasyncresult,这样可能会引起GC线程CPU很高。下面是我找的一篇介绍.net 3.5里增强的socket api使用的文章,我翻译了一下,大家了解一下,貌似性能增强了不少。当然大家肯定说还不如看原文呢,但怎么说也是俺花了好几天,问了好多人才翻译出来的,大家没事赏个脸看看也没啥坏处,对吧。原文链接如下(未经作者同意翻译的,我不知道怎么联系作者,也不会用英语和作者交流,如果作者会中文,我就会问问他是否让我翻译)
http://www.flawlesscode.com/post/2008/01/Socket-Server-with-Net35-using-SocketAsyncEventArgs.aspx

In a previous post I talked about the System.Net.Sockets enhancements in .NET 3.5, and if you haven't read it I suggest you do before tucking in to this as some of that code is important to understand what's happening here.
在前面的帖子里,我谈到了System.Net.Sockets在.NET 3.5里的增强,如果你还没有读过,我建议你深入本帖之前先读一下,因为那些代码对理解本帖很重要。

Before I start, in essence this is just a result of my experimentation and while it seems it does a pretty good job,
在开始之前(我有必要提一下),虽然它看起来不错,但这本质上只是我实验的一个结果,。

I'm not going to claim it's bullerproof or that it's a good example of how to write a socket server all it does is demonstrate the techniques of working with the new classes and methods.
我不想声称这是bullerproof的,或者是一个很好的关于如何编写socket服务器的例子,所有这些用于演示使用新的类和方法工作的技术。

The sample solution you can see on the right there contains three projects.
你可以在右边看到示例解决方案里有三个项目。

FlawlessCode contains all the classes we'll need to build ourselves a socket server.
FlawlessCode项目包含了创建Socket服务器所需的所有的类。

TestLoadGenerator is a console application which generates load for us by connecting lots of sockets to our server and sending it random data.
TestLoadGenerator项目用于向我们的Socket服务器产生大量的socket连接负载,并向服务器发送随机数据。
TestSocketServer is a small socket server implementation using the classes in FlawlessCode.
TestSocketServer项目是一个使用FlawlessCode项目实现的一个简单的Socket服务端。

TcpSocketListener

We'll begin by looking at the FlawlessCode project and in particular, the TcpSocketListener.
我们将从FlawlessCode项目开始,特别是TcpSocketListener类。
It should be fairly obvious from the name what this class is meant to achieve, it sits in a loop listening for socket connections and lets us know when one arrives.
从名称上很容易看出这个类的功能,它在一个循环里监听socket连接,并在连接到达时通知我们。
The public interface is very simple and looks like this:
公共的接口非常简单,如下

 

public void Start();
public void Stop();
public event EventHandler<SocketEventArgs> SocketConnected;

 

The only thing we'll take a closer look at here is the internal loop which accepts the client sockets.
这里唯一需要我们注意的一件事就是内部循环,在内部循环中接收客户端的socket连接。

Here you can see the first usage of the new SocketAsyncEventArgs and we're calling AcceptAsync, in our callback we check the SocketError property to see if we had any errors.
你可以看到首次使用一个新的SocketAsyncEventArgs类,我们调用AcceptAsync方法,在回调里检查SocketError属性以便知道是否发生错误。


 

private void ListenForConnection(SocketAsyncEventArgs args)
{
    args.AcceptSocket 
= null;
 

    listenerSocket.InvokeAsyncMethod(
        
new SocketAsyncMethod(listenerSocket.AcceptAsync)
        , SocketAccepted, args);
}

private void SocketAccepted(object sender, SocketAsyncEventArgs e)
{
    SocketError error 
= e.SocketError;
    
if (e.SocketError == SocketError.OperationAborted)
        
return//Server was stopped 

    
if (e.SocketError == SocketError.Success)
    
{
        Socket handler 
= e.AcceptSocket;
        OnSocketConnected(handler);
    }

    
lock (this)
    
{
        ListenForConnection(e);
    }

}


ServerConnection

Next we're going to take a look at the ServerConnection class, this class encapsulates the concept of a connected client.
下面我们将仔细看看ServerConnection类,它封装一个已连接的客户端。
Depending on what you wanted to do with your server you may decide to extend this class, rewrite it or maybe completely replace it with something derived from NetworkStream.
根据你对自己服务器的要求不同,可以选择扩展这个类,重写或者干脆用从NetworkStream派生出来的类替换掉它

For our purposes today, this class when created will begin listening for data from the network, it has two public methods, one to disconnect the client and one to send data synchronously back to the client.
仅就我们今天的用途而言,这个类在被创建以后就开始监听网络上的数据,它有两个方法,一个方法用来断开客户端连接,另一个用来以同步的方式向客户端发送数据。
ServerConnection also fires two callbacks, one when data is received and one when the client is disconnected. Here is a rundown of the interesting parts:
ServerConnection类也可以出发两个回调,一个是接受数据时,一个是当客户端连接断开时,这是一个有趣的经过裁剪的片段:

public void Disconnect()
{
    
lock (this)
    
{
        CloseConnection(eventArgs);
    }

}

public void SendData(Byte[] data, Int32 offset, Int32 count)
{
    
lock (this)
    
{
        State state 
= eventArgs.UserToken as State;
        Socket socket 
= state.socket;
        
if (socket.Connected)
            socket.Send(data, offset, count, SocketFlags.None);
    }

}

private void ListenForData(SocketAsyncEventArgs args)
{
    
lock (this)
    
{
        Socket socket 
= (args.UserToken as State).socket;
        
if (socket.Connected)
        
{
            socket.InvokeAsyncMethod(socket.ReceiveAsync,
                ReceivedCompleted, args);
        }

    }

}

private void ReceivedCompleted(Object sender,
    SocketAsyncEventArgs args)
{
    
if (args.BytesTransferred == 0)
    
{
        CloseConnection(args); 
//Graceful disconnect
        return;
    }

    
if (args.SocketError != SocketError.Success)
    
{
        CloseConnection(args); 
//NOT graceful disconnect
        return;
    }

    State state 
= args.UserToken as State;
    Byte[] data 
= new Byte[args.BytesTransferred];
    Array.Copy(args.Buffer, args.Offset, data, 
0, data.Length);
    OnDataReceived(data, args.RemoteEndPoint 
as IPEndPoint,
        state.dataReceived);
    ListenForData(args);
}

private void CloseConnection(SocketAsyncEventArgs args)
{
    State state 
= args.UserToken as State;
    Socket socket 
= state.socket;
    
try
    
{
        socket.Shutdown(SocketShutdown.Both);
    }

    
catch { } // throws if client process has already closed
    socket.Close();
    socket 
= null;
    args.Completed 
-= ReceivedCompleted; //MUST Remember This!
    OnDisconnected(args, state.disconnectedCallback);
}

Taking it from the top, we can see the public Disconnect method, this simply calls our internal CloseConnection method which shuts down the socket and fires our disconnected callback.
在代码的顶部,我们可以看到一个Disconnect的public方法,他简单的调用了内部的CloseConnection方法来关闭socket并引发disconnected回调。

An interesting point to note here is that when this class is instanciated we subscribe to the SocketAsyncEventArgs.
值得注意的一点是,当这个类实例化以后,我们向SocketAsyncEventArgs类订阅消息。


Completed event, when a client disconnects we need to remember to unhook this event because when we're reusing objects and pooling resources like this it's a bad idea to leave these references hanging around.
完成事件,当客户端断开时,我们要记着拆除,因为用资源池的方式重用对象的时候,让这些(无关的)引用就那么留在外面是个糟糕的主意,

Moving down we have the public SendData method, nothing interesting here really, just a standard synchrounous call.
接下来我们有一个SendData的public方法,这里没什么特别的,就是一个标准的同步调用。

Next we get to the internal loop which listens for data from the client, notice how we check SocketAsyncEventArgs.BytesTransferred, if this is zero, the client has closed the connection and disconnected gracefully.
再下面我们有一个内部的循环来监听客户端的数据,注意下我们如何检查SocketAsyncEventArgs.BytesTransferred属性,当它是0是,是客户端已经关闭了连接并正常的断开。

We check the value of SocketError here also to make sure there was no error anywhere, after that we make a copy of the bytes we received and inform any interested parties we have new data.
我们检查SocketError的值以确保没有发生任何错误,然后为接收到的所有字节建立一个副本,并通知有关各方我们收到了新的数据。

BufferPool and SocketArgsPool

These two classes help us with pooling our resources and are not really very interesting, they're also almost identical to the MSDN examples so you can either look there or just check out the code.
这两个类帮助我们池化资源,也没有什么非常特别的地方,他们和MSDN的例子几乎相同,所以你可以看看它们或者简单的只获取代码。

BufferPool: http://msdn2.microsoft.com/en-us/library/bb517542.aspx

SocketArgsPool: http://msdn2.microsoft.com/en-us/library/bb551675.aspx

TestSocketServer

Now that we've sen to main functionality in the FlawlessCode project we're going to look at a simple socket server implementation using these classes.
现在,我们已经看到了FlawlessCode项目的主要功能,我们将看到一个使用这些类实现的一个简单的Socket服务器。

TcpSocketListener socketListener = new TcpSocketListener(IPAddress.Any, 12345, 10);
socketListener.SocketConnected += socketListener_SocketConnected;
socketListener.Start();

Fairly straight forward, we fire up out listener on port 12345 and give the listening socket an allowed connection backlog of 10.
前面的代码非常直接,我们在12345端口上建立了监听器,并得到了一个已经监听的socket,它允许10个请求队列。

 

static void socketListener_SocketConnected(object sender, SocketEventArgs e)
{
    SocketAsyncEventArgs args 
= socketArgsPool.CheckOut();
    bufferManager.CheckOut(args); 

    ServerConnection connection 
= new ServerConnection(e.Socket, args,
        
new DataReceivedCallback(DataReceived),
        
new DisconnectedCallback(Disconnected));
}


When a client connects we get an SocketAsyncEventArgs and some free buffer space for our client and then we create an instance of ServerConnection.
在客户端连接进来的时候我们会得到一个SocketAsyncEventArgs和一些为客户端准备的空闲的缓冲区,然后我们创建一个ServerConnection的实例。

Note that we are passing delegates into the constructor, this is because the ServerConnection begins listening for data immediately and we have to have the callbacks hooked up before hand.
注意一点我们需要向构造函数里传入委托,这是因为ServerConnection会立刻监听数据,我们在这之前必须准备好回调函数。

If we let the call to the constructor complete and the we hooked to standard events we may have already missed the first batch of data!
如果我们已经调用了构造函数并且连接了标准的事件,我们可能已经错过了首批数据。

 

static void DataReceived(ServerConnection sender, DataEventArgs e)
{
    
//Do whatever we want here
}

static void Disconnected(ServerConnection sender, SocketAsyncEventArgs e)
{
    bufferManager.CheckIn(e);
    socketArgsPool.CheckIn(e);
}


Here we do whatever processing is necessary when a client sends us data.
我们可以在客户端发送数据时做任何我们想做的事情。

When a client disconnects we just check our buffer space and SocketAsyncEventArgs back into their respective pools to fight another day.
当客户端断开时,我们仅仅检出缓存区并且让SocketAsyncEventArgs返回他们各自的池里,以便以后再用。

TestLoadGenerator

I'm not going to go into how the load generation works for now, the code is all very straight forward if you've managed to follow the post this far I would imagine.
我现在不想讲load generation是如何工作的,如果你已经一直看完本帖,我想你会发现代码非常的简单。

One thing to note is that if you want to test this code and open thousands of connections you need to tweak a registry setting or windows wont give you enough ports.
需要注意的一点那是,如果你想测试这些代码,并打开成千上万个连接的话,你需要调整注册表设置以便windows给你足够的端口。

You will need to add the DWORD MaxUserPort to HKLM/SYSTEM/CurrentControlSet/Services/Tcpip/Parameters and give it a high enough value that windows won't run out of ports (reboot required, sorry)! Here is a quick examaple of how the load generation classes are used:
你需要在HKLM/SYSTEM/CurrentControlSet/Services/Tcpip/Parameters下添加一个MaxUserPort的DWORD的值,并设置成一个足够高的值,否则windows不会打开那么多端口(需要重启,切记),这是使用这些类产生负载的一个简单示例:

static void Main(string[] args)
{
    LoadGenerator generator 
= new LoadGenerator(15000);
    generator.BytesPerDelivery 
= 2048;
    generator.DeliveriesPerSecond 
= 2;
    generator.SocketCount 
= 15000;
    generator.SocketDelay 
= 5;
    generator.SocketsPerDelivery 
= 3;
    generator.Start(IPAddress.Parse(
"127.0.0.1"), 12345);
    Console.ReadLine();
    generator.Stop();
}

Pretty easy to use, right?
相当容易使用,不是吗?
We create an instance of the LoadGenerator class, telling it we'd like 15,000 connections maximum.
我们创建一个LoadGenerator类的实例,告诉它我们最大要压倒15000个连接。

Then we set some properties saying that we'd like each connected socket to deliver 2K of data twice per second.
然后我们设置一些属性告诉它我们希望每个socket连接上每秒发送两个大小为2k的数据包。(这句翻译的可能不准确)

We'd like 15,000 sockets and we'd like them to connect 5ms apart and that we want on average 3% of sockets to send data in each delivery. Then we just aim and fire! Check it out:
我们希望15000个socket在5毫秒内连接,并且有每个迭代里有百分之3的socket发送数据。然后我们启动它,我们看看效果(这句翻译的可能不准确):
图(我的机器,服务端初始化5000SocketArgsPool,和一个大小为5000BufferPool,每个Buffer大小为1024,启动后大约占500M内存,我的机器是双核32位。客户端初始化5000个到服务端的连接,每个连接每秒发2个包,每个包的大小为2048 bytes,跑了半小时,GC、线程及锁争用截图如下):

When this was taken, the server executable was using 109MB or RAM and 1% CPU on my desktop machine so I think at 15,000 connections we've got some pretty damned good performance out of this thing!
当它执行起来,在我的台式机服务器使用了109M的内存及1%的CPU,所以我想15000个连接达到如此好的性能是相当漂亮的。

Obviously when we start implementing the server logic and actually processing each packet this will go up, but for a bare socket server, I'm pretty pleased.
当然,当我们开始实现自己的服务端逻辑时候我们会处理更多的包,但对一个简单的socket来说,我觉得非常好了。(后半句翻译的可能不准确)


相关链接
OutOfMemoryException and Pinning
http://blogs.msdn.com/yunjin/archive/2004/01/27/63642.aspx