Asynchronous Socket Utility Classes – Part IIAuthor: William KennedyDate Added: 31st Mar 2003 Type: Tutorial Rating: This is a printable version of "Asynchronous Socket Utility Classes – Part II". For the complete online version, please visit http://www.devarticles.com/content.php?articleId=493 Page 1: IntroductionA socket is a connection oriented communication channel that is established between two computers. In the OSI network model, http://www2.rad.com/networks/1994/osi/layers.htm, sockets are implemented at the session layer. This means that sockets can be used with any transport protocol like TCP or UDP. Session layer protocols, like sockets, require that a computer must establish a connection to any computer it wished to communicate with. Once a connection is established, messages can be passed between both computers. A socket connection can be thought of as a virtual pipe between two computers. Each pipe provides an exclusive communication channel. If multiple computers connect to the same computer, each will have their own pipe. It is easy to identify which computer sent a message because each message flows through that computers pipe. This means an application must be looking for messages on every pipe that exists. The most common way to do this is to spawn a thread for each pipe. Asynchronous sockets provide a way to have a pool of threads process messages for all established pipes, instead of having a thread per pipe. This solution is scalable and improves performance. The .NET framework provides nice low-level classes to help us add asynchronous socket support into our applications. However, the implementation still lacks an organizational structure that is easy to use. For more information on socket programming look at the follow sites: http://msdn.microsoft.com/library/default.asp?url=/library/en-us/cpguide/html/cpconnon-blockingserversocketexample.asp When adding asynchronous socket support into our applications it can seem initially confusing, and at times overwhelming. We have to understand how the asynchronous socket framework is implemented. We need to understand threading and how it is applied. Most importantly we need to have some understanding of how to integrate this into our application. To help us through these issues, I have built two classes. These classes provide reusable interfaces, which cleanly and quickly add asynchronous socket support into our applications. Both of these classes take care of the low-level details that allow us to only have to worry about coding the business problem at hand. Low level details such as implementing the asynchronous socket thread pool, identifying which messages came from which computers, and handling any error conditions that occur in a consistent way. These classes will save us hours of time developing and debugging our asynchronous socket applications. In this article I will build the server socket class and sample application to test it. System Requirements A basic understanding of C# is required to follow through the examples and the classes. Basic concepts of type, properties, threading, synchronization, and delegates are required. It is important that you have completed part I of this article. In that article we build the CSocketServer class. Defining The Problem High-level class needs to be developed that abstract the .NET framework classes used to develop asynchronous socket applications. This new class needs to abstract the details behind asynchronous socket programming. This class must be easy to use and reliable. Any classes that are developed need to be reusable so they can be shared among many different types of applications. Defining The Solution A single class will be developed. We will develop a class that abstracts the intricacies of building asynchronous socket servers. This class will allow us to be ignorant of the details and yet allow us to access the underlining framework if we need to. Page 2: Component Design and CodingCSocketServer Now we will build a class that provides socket server support. Open the UtilSocket.cs file created during Part I of this article and add the follow class just after the CSocketClient class inside the namespace. } // End of CSocketClient Delegate Method Types Section We need to add four delegate method types. The first three delegate method types are identical to the delegate method types we added to the CSocketClient class and perform the same function. The new delegate method type is for the Accept Handler. We will call the method referenced by a property of this type when a client socket connection request is accepted. /// <summary> DelType: Called when a message is extracted from the socket </summary> Private Properties Section We need to add eleven private properties In the CSocketClient class, we used the TcpClient class to provide the support we needed to establish socket connections to a server. In this class we will create a property using the TcpListener class. This property will provide the support we need to listen for and accept those client connections requests coming from the TcpClient class or similar technologies. private TcpListener m_pTcpListener; The CSocketServer class will be responsible for listening and accepting client socket connection requests via the TcpListener property. We need a thread to perform this responsibility. This property is a thread object to manage the Accept thread. private Thread m_pAcceptThread; Once a connection request is accepted the Accept thread, the thread will instantiate a CSocketClient object. The new CSocketClient object will be used to encapsulate the new socket connection. The CSocketServer will maintain an array of the CSocketClient objects it instantiates and will be responsible for the lifetime of these objects. private CSocketClient[] m_pSocketClientList; The application developer will decide the maximum number of client socket connections that will be accepted. This property maintains this number. It is used later in the constructor to instantiate the SocketClientList property array. private Int32 m_iMaxClientConnections; Since the Accept thread is instantiating CSocketClient objects, it must pass the required arguments to its constructor. This property is maintained by the CSocketServer class so the Accept thread can pass this value to the constructor of the CSocketClient objects it creates. private Int32 m_iSizeOfRawBuffer; The following delegate method type properties are maintained because they need to be passed to the constructor of the CSocketClient objects created by the Accept thread. The Accept Handler property is used by the Accept thread and not passed to the instantiated CSocketClient object. When the Accept thread accepts a client socket connection request and instantiates a CSocketClient object, the Accept thread will call the Accept Handler method. This is when the application developer can perform any application specific actions required when a client connection is accepted. private MESSAGE_HANDLER m_pfnMessageHandler; private ACCEPT_HANDLER m_pfnAcceptHandler; There will be a need for synchronization because of the SocketClientList array. Different threads will need to add and remove CSocketClient object references from this array at possibly the same time. This object property will be used in a Monitor object to provide critical section support to the required member methods. private Object m_pSocketListCS; The last private property is the dispose flag property. The finalize method will use this flag to determine if the class has already been disposed or not. private Boolean m_bDisposeFlag; Public Properties Section We need to add three public properties. This property maintains the ipaddress of a local nic card used to listen for client socket connections. If you want to listen on multiple nic cards you will need to instantiate multiple CSocketServer objects, one for each card. This property can store a host name or an ipaddress. If this value is a host name it must be resolvable by a DNS lookup. private String m_strIpAddress; This property maintains the port the server is to listen for client socket connections on. You must make sure the port is currently not in use. To manually check if a port is in use, run the “netstat –a” command from the command line. This property is for the application developer. If the application developer would like to store some state associated with the socket server, he can use this property. Constructor, Finalize, and Dispose Section In the constructor, all we need to do is instantiate the object for the critical section property and set the dispose property flag to false. //******************************************************************** The finalize method verifies that the object instance has not been disposed already. If the application developer remembers properly to call dispose, there is nothing for finalize to do. This finalize function only exists as a safe guard. The dispose method sets the dispose flag to true so the finalize function does not try to dispose the object instance again. All that needs to be done is to make sure the Accept thread is terminated. The Stop method performs that action as you will see later in the public methods section. Private Methods Section We need to add two private methods. The LocateSocketIndex method is used to locate an empty slot in the SocketClientList property array. This is used by the Accept thread to save a reference to a newly instantiated CSocketClient object. Next, we wait to enter the Monitor object. We only want one thread accessing the SocketClientList property array at a time. In the call to Monitor.Enter, we pass the SocketList object property to protect the SocketClientList property array. Now we safely search the SocketClientList property array for an empty slot. Once we find an empty slot, we break out of the loop. After the loop, we call Monitor.Exit passing the SocketList object property again and return the value of iSocket. Notice how the calls to Monitor.Enter and Monitor.Exit are outside the try and catch statements. This is done to make sure that if we successfully enter the critical section we absolutely exit from it. If you do not call Monitor.Exit after passing through Monitor.Enter, no other threads will ever get through the critical section again. Those threads waiting to pass through the Monitor.Enter call will block forever. It is important to always make sure Monitor.Exit is called and only called once. I have found this to be the most efficient and safest way to guarantee critical sections get released properly. //******************************************************************** The AcceptThread method is spawned as a worker thread. It performs most of the work for the CSocketServer class. The job of the AcceptThread method is to listen for incoming client socket connection requests and accept those requests. Once a client socket connection request is accepted by the call to the TcpListener AcceptSocket method, the Accept thread instantiates a CSocketClient object. The new CSocketClient object exists to encapsulate the accepted socket connection. We then store a reference to the CSocketClient object in the SocketClientList property array. Once these activities are complete, then we call the Accept Handler. This allows the application developer to be notified a client socket connection request has been accepted. In both of the catch statements, we call the Error Handler. This notifies the application developer to be notified about an error accepting a socket client connection. In the catch statement that catches the System.Net.Sockets.SocketException object, we check for error code 10004. This error code means that a socket blocking call was cancelled. We get this error when the TcpListener property is stopped. In this case, we do not call the Error Handler since this is a known error that is unavoidable. In both catch statements, we close the socket client connection if we accepted the connection before the error. //******************************************************************** Public Methods Section We need to add one public method. The RemoveSocket method is used to remove a reference of a CSocketClient object from the SocketClientList property array. A reference to a CSocketClient object is passed to the RemoveSocket method. We first need to check to make sure the reference to the passed CSocketClient object is valid. We do this by comparing the reference passed into RemoveSocket with the reference located in the SocketClientList property array. We determine which slot to look at in the SocketClientList array by using the slot the passed CSocketClient object says it’s located in. If these references match, we de-reference the object from the SocketClientList property array. You may have noticed that the CSocketClient class currently does not have a public property called GetSocketListIndex. We will be adding the public property to the CSocketClient class later. Again the critical section is used to protect the SocketClientList property array. The calls to Monitor.Enter and Monitor.Exit are outside the try and catch statements. //******************************************************************** CSocketClient Updates Now we will add some new properties and methods to the CSocketClient class to support the CSocketServer class. Private Properties Section We need to add one private property. This property maintains a reference back to the owning CSocketServer object. We need this reference to call the CSocketServer RemoveSocket method from the CSocketClient Dispose method. private CSocketServer m_pSocketServer; Public Properties Section We need to add two public properties. This property maintains the index where this object is located in the CSocketServer SocketClientList array. private Int32 m_iSocketListIndex; This property maintains a reference to the socket object created by the TcpListener property when the CSocketServer Accept thread accepts a client socket connection request. This property will be passed to the NetworkStream property instead of the TcpClient property when the CSocketServer class instantiates a CSocketClient object. private Socket m_pClientSocket; Constructor, Finalize, and Dispose Section We need to add a new constructor method to support the CSocketServer class. This overloaded constructor method takes 10 arguments. The XML documentation for the constructor describes the arguments. In the constructor, we are setting the class properties that should not change during the lifetime of an object instance of this class. In this version of the constructor method, we need to do all of the things we did as in the first version plus a few new things. First, we need to save the reference to the CSocketServer object that created this CSocketClient object. Next, we save the reference to the socket for the accepted connection and the index where this object can be located in the CSocketServer SocketClientList array. Now we need to save the ipaddress and port of the client that connected to us. In the CSocketClient Connect method we made the following call. GetNetworkStream = GetTcpClient.GetStream(); This associated the new socket connection we established with the NetworkStream property so we could process socket messages asynchronously. The Connect method will never be called in this context because the connection is already established. In this case, we need to associate the socket being passed in with the NetworkStream property. GetNetworkStream = new NetworkStream(GetClientSocket); This statement above is used here in the constructor to associate the socket being passed in with the NetworkStream property. The last thing to add is the socket options. Since the Connect method will never be called, we need to set the socket options here. We set all the same options we did in connect. //******************************************************************** We need to make one change to the Dispose method. We need to add the call to the CSocketServer RemoveSocket method. // Remove the socket from the list This is added after the catch statement to guarantee the call is made. This will remove the reference to this object from the CSocketServer SocketClientList array. //******************************************************************** The last change is to the Disconnect method. Here we need to add the following code. if (GetClientSocket != null) This code will close the socket and remove the reference this class has to the memory so garbage collection can clean up. //******************************************************************** Page 3: Sample Application UpdateNow it is time to test our CSocketServer class. Open the class1.cs file and go to the bottom. Add a new method called MessageHandlerServer. This method is called when we receive a message from a socket client connection. In this method we will send back to the client a HTTP HTML command. //******************************************************************** Add the AcceptHandler next. This is allow us to be notified when a connection is established to our server. //******************************************************************** The TestServer method will be used to start the server. We will listen for connections on port 9000 using the network card configured under our machine name. In the Main method, comment out the call to TestClient and add the call to TestServer. Now we are ready to start our server and test it. //******************************************************************* // Test the CSocketClient class Start the server and launch your browser. Our server is listening for connections on our local machine on port 9000. You need the machine name or ipaddress of our local machine. In the address bar type the following: http://machinename:9000 . This will have the browser connect to our server on port 9000 and our server will send the browser back the HTML statement that will display Hello World on the browser. Page 4: ReviewSo now we have the ability to add socket client and server support to our applications easily and quickly. In the client sample application, all we needed to do was to instantiate a CSocketClient object. Then we can decide which server we want to connect to and code the handler functions appropriately. In the server sample application, all we needed to do was to instantiate a CSocketServer object. Then we can decide which ipaddress and port to listen for socket client requests and code the handler functions appropriately. For more great programming articles, please visit http://www.devarticles.com. If you have an opinion or question about this article, then please post it at the devArticles developer forum, which is located at http://www.devarticles.com/forum |
本文地址:http://com.8s8s.com/it/it45719.htm