`
talin2010
  • 浏览: 501431 次
  • 性别: Icon_minigender_1
  • 来自: 河北
社区版块
存档分类
最新评论

Use .NET SocketAsyncEventArgs to write socket server code in C#

阅读更多

Use .NET SocketAsyncEventArgs to write socket server code in C#
Is your email address OK?You are signed up for our newsletters but your email address is either unconfirmed, or has not been reconfirmed in a long time. Please clickhereto have a confirmation email sent so we can confirm your email address and start sending you newsletters again. Alternatively, you canupdate your subscriptions.

Download SocketAsyncServerAndClient.zip - 200.2 KB



Introduction

Writing fast, scalable socket server code for TCP/IP is not easy. In order to help you write scalable, high performance socket server code, Microsoft created theSocketAsyncEventArgsclass. A proven way to write scalable, high performance socket code for TCP/IP in Windows can be seen in this article onI/O completion ports. Also, here's a link to a Microsoft page onI/O Completion Ports.SocketAsyncEventArgsuses I/O Completion ports via the asynchronous methods in the .NET Socket class.SocketAsyncEventArgsobject allows us to access the accepting socket with all the functionality of theSocketAsyncEventArgsclass, like working asynchronously, raising the Completed event, setting buffer space, etc.

But there are other issues that need to be addressed in order to create dependable socket server code. One issue is buffers. Buffers in TCP are unmanaged, that is, not controlled by the .NET Framework, but by Windows system. So the buffer gets "pinned" to one place in memory, thereby causing memory fragmentation, since the .NET GarbageCollector will not be able to collect that space. This situation is improved by putting all the buffers together in one block of memory, and just reusing that same space over and over. The example in theBufferManagercode on Microsoft's page for theSetBuffermethod shows how to build it.

Background

You may have started your research into this topic at Microsoft's main page for theSocketAsyncEventArgsclass. The example code on that page got me started. But it was also confusing. It seems that they have removed some example code about theUserToken property. Also, some of the method names were a bit confusing, as were some of the variable names. Their reason for using a Semaphore was not explained really. While their code for building theBufferManagerwas good, the way that they dealt with theSetBuffermethod in their ProcessReceive method in example code for theSocketAsyncEventArgsclass will pretty much work in only the narrowest of examples. If you send a 10 byte string, and then a 20 byte string, it won't work. Their code sets the buffer to be whatever size you send on the first message from the client. So after the first message it would just send back the first 10 bytes. So, we need a better example of how to get the data and use it. Plus, in the explanation of the example code they said "For example, if a server application needs to have 15 socket accept operations outstanding at all times to support incoming client connection rates, it can allocate 15 reusableSocketAsyncEventArgsobjects for that purpose." But then their example only included reusableSocketAsyncEventArgsobjects for receive/send, not for accept. TheSocketAsyncEventArgsobject for the accept operation would wait until the receive/send finished to do another accept op. Instead we can use a pool, as they mentioned in their explanation, and post accept operations faster.

So, this article was done in an attempt to make it clearer how to use theSocketAsyncEventArgsclass. The code in this article was developed on Visual Studio 2008 using .NET 3.5. Pay special attention to the code related to buffers, as buffer-related stuff seems to be an area where people have more difficulty. This article assumes some knowledge of delegates and event handling in Windows.

Regarding theSocketAsyncEventArgsclass, Microsoft's website says it requires "Platforms: Windows 7, Windows Vista, Windows XP SP3, Windows Server 2008, Windows Server 2003. (The) .NET Framework Supported in: 4, 3.5 SP1, 3.0 SP1, 2.0 SP1. (The) .NET Framework Client Profile Supported in: 4, 3.5 SP1."

TCP Protocol and Socket Basics

If you have experience with socket server code, you can skip this section. For those new to socket programming, there are four main steps in using a socket server with TCP. (It's often described as six parts, but I like to put the first three together into one.)

1)Listenfor connection requests
In order to listen you need to
(a) create a socket.
(b) bind that socket to a port,
(c) listen with that socket.
A client (not a server) can initiate a connection request, by sending a SYN packet. The client doesnotlisten for incoming connections. It always initiates connections by using the Connect or ConnectAsync method. When a client initiates a connection, then the Windows TCP/IP subsystem of the server will respond with SYN, ACK. After the client machine's Windows TCP/IP subsystem responds back with an ACK packet, the connection is established. Windows will handle this TCP/IP stuff for you. In other words, SYN, ACK, PSH, packets, and similar parts of TCP/IP donothave to be coded by you. Very nice. The server's listening socket can maintain a queue of connection requests waiting to be accepted. This queue is called the "backlog". The listening socket passes the connection info to another socket via an "accept" operation, and then gets the next incoming connection in the backlog queue, or if there is none, waits till there is a new connection from a client.
2)Acceptconnection requests
In order to have multiple connections on the same port, the listening socket must pass off the connection info to another socket, which accepts it. The accepting socket isnotbound to the port. You post an accept operation to pass the connection from the listening socket to the accepting socket. The accept operation can be posted before the incoming connection is established, so that the listening socket immediately passes off the new connection info to the accepting socket. The client doesnotneed to perform an accept operation.
3)Receive/Sendvia the connection
After the accept operation has completed you can now receive or send data with that connection. (The sameSocketAsyncEventArgsobject that did the accept operation could also do the receiving or sending, if we post a receive or send on it and have buffer space for it.) In the design of the code below, theSocketAsyncEventArgswhich did the accept operation passes the connection info over to anotherSocketAsyncEventArgsobject to do receiving/sending. (We could also split the receiving and sending into 2 separateSocketAsyncEventArgsobjects, if we wish. One reason to do that might be a need for different buffer sizes for send vs. receive. We would just need to have the connection info in both theSocketAsyncEventArgsobject that sends and theSocketAsyncEventArgsthat receives. And they might need a reference to each other also.)
4)Closethe connection
Either client or server can initiate an operation to close the connection. Usually the client would initiate that. Again, the lower level TCP/IP of the disconnect is handled by Windows operating system. The connection can be closed using the Close method, which destroys the Socket and cleans up its managed and unmanaged resources. You can also close by using the Disconnect or DisconnectAsync method, which allows reuse of the Socket.

With TCP there is no guarantee that one send operation on the client will be equal to one receive operation on the server. One send operation on the client might be equal to one, two or more receive operations on the server. And the same is true going back to client from server. So you must have some way of determining where a TCP message begins and/or ends. Three possible ways to handle TCP messages are:
1) Prefix every message with an integer that tells the length of the message.
2) All messages be fixed length. And both client and server must know the length before the message is sent.
3) Append every message with a delimiter to show where it ends. And both client and server must know what the delimiter is before the message is sent.

Also, your communications protocol must include whether there will be a response (send operation) from the server back to the client after each received message or not. Will that response be after one complete received TCP message, or can it be after more than one message? If it is after one message, the code is simpler probably.

Okay, so let's think about the possible situations that might occur with the data that the server receives in one receive operation with our communication protocol.

1) On the first receive op, receive less bytes than the length of the prefix.
2) After having received part of the prefix on a previous receive op or ops, then receive another part of the prefix, but not all of it.
3) After having received part of the prefix on a previous receive op or ops, then receive the rest of the prefix, but nothing more.
4) After having received part of the prefix on a previous receive op or ops, we then receive the rest of it, plus part of the message.
5) After having received part of the prefix on a previous receive op or ops, we then receive the rest of it, plus all of the message.
6) Receive exactly the number of bytes that are in the prefix, but nothing more.
7) After having received exactly the number of bytes that are in the prefix on a previous receive op or ops, we then receive part of the message.
8) After having received exactly the number of bytes that are in the prefix on a previous receive op or ops, we then receive all of the message.
9) Receive the number of bytes for the prefix plus part of the message, but NOT all of the message.
10) After having received the prefix and part of the message on a previous receive op or ops, we then receive another part of the message, but not all of it.
11) After having received the prefix and part of the message on a previous receive op or ops, we then receive all the rest of the message.
12) Receive the number of bytes for the prefix plus all of the message on the first receive op. This is actually the most common thing that will happen. But all of the above things can happen and do happen. If both client and server have buffer sizes larger than the messages, then the situations above may not happen when running both client and server on the same machine, or even on a LAN. But TCP is unpredictable over the Internet. So your code needs to allow for all of those possibilities.

Let's look at some code

Accept operations. In this app the socket which does the accept can be accessed thru aSocketAsyncEventArgsobject, in itsAcceptSocketproperty. On Microsoft'sAcceptSocketpage it says, "If not supplied (set to null) before calling theSocket.AcceptAsync method, a new socket will be created automatically." That's what is done in the code below. We just allow a new Socket object to be created for every new connection. (There is the option of reusing sockets in .NET and having a pool of Socket objects, but so far I have not found it to yield a significant advantage.) We can have a pool of theseSocketAsyncEventArgsobjects, each one containing a separate Socket object, to deal with accept operations. In this pool you do not need one object for each connection the server is maintaining, because after the accept operation completes, a reference to the socket is handed off to anotherSocketAsyncEventArgsobject pretty fast. It doesnotseem to help to put a lot ofSocketAsyncEventArgsobjects in the pool. Again, repeating for clarity, the socket which does the accept operation is inSocketAsyncEventArgs.AcceptSocketproperty of theSocketAsyncEventArgsobjects that comes out of the pool ofSocketAsyncEventArgsobjects that we create for accept operations.

Receive/Send operations. In this app the receive and send operations are handled viaSocketAsyncEventArgsobjects that come out of a pool ofSocketAsyncEventArgsobjects that we create forreceive/sendoperations. This is NOT the same pool as we just examined regarding accept operations. To improve performance we have a pool of these objects which do receive and send operations. The number ofSocketAsyncEventArgsobjects in the pool for receive/send operations should probably be equal to the maximum number of concurrent connections allowed.

What is our communication protocol in this code?
1) One message from client will correspond with one message from the server.
2) After a connection is made the client will send a message first, and then post a receive op to wait for the response from the server. And for each message that the client sends the server will respond with a message to the client. Then it will post another receive op and wait for the next message from the client. In our code the server will make a few changes to the data before responding to the client, so that we do more than just echo data sent by the client, which is what the Microsoft example does. That approach should help you see what happens with the buffers better than just echoing would.
3) We will prefix every message with an integer that tells the length of the message.

Okay, let's look at the server code. Sometimes in code comments I abbreviate "SocketAsyncEventArgs" as "SAEA".

Collapse
class Program
    {
        //You would want a buffer size larger than 25 probably, unless you know the
        //data will almost always be less than 25. It is just 25 in our test app.
        public static readonly Int32 testBufferSize = 25;
                        
        //This is the maximum number of asynchronous accept operations that can be 
        //posted simultaneously. This determines the size of the pool of 
        //SocketAsyncEventArgs objects that do accept operations. Note that this
        //is NOT the same as the maximum # of connections.
        public static readonly Int32 maxSimultaneousAcceptOps = 4;

        //The size of the queue of incoming connections for the listen socket.
        public static readonly Int32 backlog = 100;

        //For the BufferManager
        public static readonly Int32 opsToPreAlloc = 2;    // 1 for receive, 1 for send
        
        //This variable determines the number of 
        //SocketAsyncEventArg objects put in the pool of objects for receive/send.
        //The value of this variable also affects the Semaphore.
        //This app uses a Semaphore to ensure that the max # of connections
        //value does not get exceeded.
        //Max # of connections to a socket can be limited by the Windows Operating System
        //also.
        public static readonly Int32 maxNumberOfConnections = 100;

        //If this is true, then after a message is received, 
        //the server will send a message back to the client.
        //In our example app, this must be true. The code will not handle false.
        public static bool respondToClient = true;
        
        //If this port # will not work for you, it's okay to change it.
        public static readonly Int32 port = 4444;

        //This number must be the same as the value on the client.
        //Tells what size the message prefix will be. Don't change this unless
        //you change the code, because 4 is the length of 32 bit integer, which
        //is what we are using as prefix.
        public static readonly Int32 receivePrefixLength = 4;
        public static readonly Int32 sendPrefixLength = 4;                
        
        public static Int32 mainTransMissionId = 10001;
        //object that will be used to lock the mainTransMissionId
        public static object lockerForTid = new object();

        public static List<DataHolder> listOfDataHolders;
        //object that will be used to lock the listOfDataHolders
        public static object lockerForList = new object();

        public static Int32 mainSessionId = 1000000001;
        //object that will be used to lock the mainSessionId
        public static object lockerForSesId = new object();        
                
        //If you make this a positive value, it will simulate some delay on the
        //receive/send SAEA object after doing a receive operation.
        //That would be where you would do some work on the received data, 
        //before responding to the client.
        //This is in milliseconds. So a value of 1000 = 1 second delay.
        public static readonly Int32 msDelayAfterGettingMessage = -1;

        static void Main(String[] args)
        {
            try
            {
                //Just displaying the IP addresses of this machine.
                IPAddress[] addressList = 
                     Dns.GetHostEntry(Environment.MachineName).AddressList;
                
                // Get endpoint for the listener.
                IPEndPoint localEndPoint = 
                     new IPEndPoint(addressList[addressList.Length - 1], port);
                                
                //This object holds a lot of settings that we pass from Main method
                //to the SocketListener. In a real app, you might want to read
                //these settings from a database or windows registry settings that
                //you would create.
                SocketListenerSettings theSocketListenerSettings = 
                     new SocketListenerSettings(maxNumberOfConnections, backlog, 
                     maxSimultaneousAcceptOps, receivePrefixLength, 
                     testBufferSize, sendPrefixLength, opsToPreAlloc, localEndPoint);
                
                //instantiate the SocketListener.
                SocketListener socketListener = 
                     new SocketListener(theSocketListenerSettings);
                                
                string closeString = "z";
                string closeCheck = "";
                while (closeCheck != closeString)
                {
                    Console.WriteLine("type '" + closeString 
                     + "' and press Enter to close the program.");
                    closeCheck = Console.ReadLine();                    
                }
            }
            catch (IndexOutOfRangeException)
            {
                Console.WriteLine("port invalid");
            }
            catch (FormatException)
            {
                Console.WriteLine("port invalid");
            }
            catch (Exception ex)
            {
                Console.WriteLine("Error: " + ex.Message);
            }            
        }
    }


The primary class is SocketListener.

Collapse
class SocketListener
    {        
        //Buffers for sockets are unmanaged by .NET. 
        //So memory used for buffers gets "pinned", which makes the
        //.NET garbage collector work around it, fragmenting the memory. 
        //Circumvent this problem by putting all buffers together 
        //in one block in memory. Then we will assign a part of that space 
        //to each SocketAsyncEventArgs object, and
        //reuse that buffer space each time we reuse the SocketAsyncEventArgs object.
        //Create a large reusable set of buffers for all socket operations.
        BufferManager theBufferManager;

        // the socket used to listen for incoming connection requests
        Socket listenSocket; 

        //A Semaphore has two parameters, the initial number of available slots
        // and the maximum number of slots. We'll make them the same. 
        //This Semaphore is used to keep from going over max connection #.         
        //(It is not about controlling threading really here.)   
        Semaphore theMaxConnectionsEnforcer;       
        
        //an object that we pass in and which has all the settings the listener needs 
        SocketListenerSettings socketListenerSettings;
        
        // pool of reusable SocketAsyncEventArgs objects for accept operations
        SocketAsyncEventArgsPool poolOfAcceptEventArgs;
        // pool of reusable SocketAsyncEventArgs objects for 
        //receive and send socket operations
        SocketAsyncEventArgsPool poolOfRecSendEventArgs;
        
        //_______________________________________________________________________________
        // Constructor.
        public SocketListener(SocketListenerSettings theSocketListenerSettings)        
        {
            this.socketListenerSettings = theSocketListenerSettings;
                        
            //Allocate memory for buffers. We are using a separate buffer space for
            //receive and send, instead of sharing the buffer space, like the Microsoft
            //example does.
            this.theBufferManager = 
                    new BufferManager(this.socketListenerSettings.BufferSize * 
                    this.socketListenerSettings.MaxConnections * 
                    this.socketListenerSettings.OpsToPreAllocate,
                    this.socketListenerSettings.BufferSize * 
                    this.socketListenerSettings.OpsToPreAllocate);
            
            this.poolOfRecSendEventArgs = 
                    new SocketAsyncEventArgsPool(this.socketListenerSettings.MaxConnections);
            this.poolOfAcceptEventArgs = 
                    new SocketAsyncEventArgsPool(this.socketListenerSettings.MaxAcceptOps);                    
            this.theMaxConnectionsEnforcer = 
                    new Semaphore(this.socketListenerSettings.MaxConnections + 1, 
                    this.socketListenerSettings.MaxConnections + 1);

            //Microsoft's example called these from Main method, which you 
            //can easily do if you wish.
            Init();
            StartListen();
        }

        //____________________________________________________________________________
        // initializes the server by preallocating reusable buffers and 
        // context objects (SocketAsyncEventArgs objects).  
        //It is NOT mandatory that you preallocate them or reuse them. But, but it is 
        //done this way to illustrate how the API can 
        // easily be used to create reusable objects to increase server performance.
        
        internal void Init()
        {   
            // Allocate one large byte buffer block, which all I/O operations will 
            //use a piece of. This gaurds against memory fragmentation.
            this.theBufferManager.InitBuffer();

            // preallocate pool of SocketAsyncEventArgs objects for accept operations           
            for (Int32 i = 0; i < this.socketListenerSettings.MaxAcceptOps; i++)
            {
                // add SocketAsyncEventArg to the pool
                this.poolOfAcceptEventArgs.Push(CreateNewSaeaForAccept(poolOfAcceptEventArgs));
            }           

            //The pool that we built ABOVE is for SocketAsyncEventArgs objects that do
            // accept operations. 
            //Now we will build a separate pool for SAEAs objects 
            //that do receive/send operations. One reason to separate them is that accept
            //operations do NOT need a buffer, but receive/send operations do. 
            //ReceiveAsync and SendAsync require
            //a parameter for buffer size in SocketAsyncEventArgs.Buffer.
            // So, create pool of SAEA objects for receive/send operations.
            SocketAsyncEventArgs eventArgObjectForPool;

            for (Int32 i = 0; i < this.socketListenerSettings.MaxConnections; i++)
            {
                //Allocate the SocketAsyncEventArgs object for this loop, 
                //to go in its place in the stack which will be the pool
                //for receive/send operation context objects.
                eventArgObjectForPool = new SocketAsyncEventArgs();

                // assign a byte buffer from the buffer block to 
                //this particular SocketAsyncEventArg object
                this.theBufferManager.SetBuffer(eventArgObjectForPool);

                //Attach the SocketAsyncEventArgs object
                //to its event handler. Since this SocketAsyncEventArgs object is 
                //used for both receive and send operations, whenever either of those 
                //completes, the IO_Completed method will be called.
                eventArgObjectForPool.Completed += 
                    new EventHandler<SocketAsyncEventArgs>(IO_Completed);
                
                //We can store data in the UserToken property of SAEA object.
                DataHoldingUserToken theTempReceiveSendUserToken = 
                    new DataHoldingUserToken(eventArgObjectForPool,
                   eventArgObjectForPool.Offset, eventArgObjectForPool.Offset + 
                   this.socketListenerSettings.BufferSize, 
                   this.socketListenerSettings.ReceivePrefixLength, 
                   this.socketListenerSettings.SendPrefixLength, i + 1);

                //We'll have an object that we call DataHolder, that we can remove from
                //the UserToken when we are finished with it. So, we can hang on to the
                //DataHolder, pass it to an app, serialize it, or whatever.
                theTempReceiveSendUserToken.CreateNewDataHolder();
                                
                eventArgObjectForPool.UserToken = theTempReceiveSendUserToken;

                // add this SocketAsyncEventArg object to the pool.
                this.poolOfRecSendEventArgs.Push(eventArgObjectForPool);
            }
        }

        //____________________________________________________________________________
        // This method is called when we need to create a new SAEA object to do
        //accept operations. The reason to put it in a separate method is so that
        //we can easily add more objects to the pool if we need to.
        //You can do that if you do NOT use a buffer in the SAEA object that does
        //font-style: italic; color: 

  


  
分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics