Introduction
A socket is an end point in network communication, it is uniquely identified by an ip address and a port number. TCP/IP is the most common protocol used for inter-process communication. In many cases it is also the most efficient way to send data between client and server programs. The parties communicating via TCP socket can live on different machines on the network, but they can also live on the same machine or even within the same process. As we know, communications to most web servers and most relational databases are implemented using TCP socket.
However, socket programming is never easy. Without the help of the .NET framework, we typically have to work with some low level C/C++ functions or use some third party development tools. With .NET, we can use the framework class Socket
which shields us from some low level details in socket programming. If all we care about is TCP socket, then we can use the TcpListner
class and the TcpClient
class. However, in order to write a client-server system that is efficient, scalable, and easy to maintain, we still have a lot to learn and a lot to do.
Let's first examine some of the issues. Suppose we are developing a socket server program that allows multiple clients to connect concurrently. First of all, our server has to be multithreaded and thread-safe, otherwise one bad connection could make other connections stop working. Secondly, we have to consider efficiency and scalability. If our server has to handle, say, more than one hundred concurrent clients, then it won't be efficient to use a separate thread for each client connection, because most of the time the connections will be idle. Thirdly, we need to make sure that some crashed or deadlocked clients do not prevent the server from working with other active clients. Furthermore, we may need to have some built-in capability to restrict the total number of clients and to reject untrusted clients.
The Socket
class in the .NET framework allows you to perform asynchronous socket operations. Basically, you call a member function (such as BeginReceive and BeginSend) to start the socket operation asynchronously. The member function will not block, you can either wait for the socket operation to complete or specify a delegate (function pointer) of your callback function so that it will be invoked automatically by a thread from the .NET thread pool when the socket operation is complete. However, knowing the Socket
class alone is not enough to write a good socket server program. The Socket
class is not thread-safe at the instance level. It is not easy to figure out when/where/how to make these asynchronous calls, etc. The .NET built-in thread pool is static (i.e. there is only one instance) which makes it a shared global resource, this may cause some problems under certain stress conditions.
What we need is an easy to use tool that performs reasonably well and also hides most of the complexities of socket programming. Many people have written articles and code to achieve this goal, I am just one of them.
In this article, I am going to introduce a simple .NET TCP socket component which contains a server class and a client class. My socket component is still based on the .NET Socket
class, however, I am using my own thread pool class , there is no call to the asynchronous members of the Socket
class in my code.
To make my socket server more useful, I have implemented a java client and a com client. You can use the java client to talk to the socket server from other platforms. The com client makes it possible to talk to the socket server from old VB and C/C++ programs.
The Server class
XYNetServer
is the name of our socket server class. Instances of this class are thread-safe which means you can use the same instance from multiple threads with no problem. Here is how it works.
To create an instance of the server class, the user needs to specify the ip address and the port number (to bind the server), as well as the minimal and the maximal numbers of worker threads used by the server. When the StartServer
method is called, the server instance will start listening for client connection requests on the specified ip and port. The user can call the SetConnectionFilter
method to specify a function to handle the connection requests, all connection requests will be accepted if no connection filter is set.
The server will receive two types of messages from the clients, binary messages and string messages. Call the methods SetBinaryInputHandler
and SetStringInputHandler
to specify functions to handle incoming messages. To send an outgoing message, call the SendBinaryData
method or the SendStringData
method.
Call the SetExceptionHandler
method to specify a function to handle server errors, otherwise all errors will be ignored.
Here are the signatures of the constructor, the StartServer
method, and the StopServer
method.
public XYNetServer(string sAddress, int nPort, int nMinThreadCount,
int nMaxThreadCount);
public bool StartServer();
public void StopServer();
The sAddress
parameter allows you to specify which ip address the server will use in case there are multiple ip addresses assigned to the machine. If it is the empty string, the server will be using the default ip address of the machine. The sAddress
and nPort
pair uniquely identifies the internal server socket. After the server is started, the number of worker threads used by the server will vary between nMinThreadCount
and nMaxThreadCount
, the exact number depends on the load of the server at the time. If the server is not busy, some worker threads will be terminated but at least nMinThreadCount
of them will remain active.
The StartServer
method tells the server to begin accepting clients and processing incoming data, etc., the StopServer
method will shutdown the server completely (including all threads in the internal thread pool). When StartServer
returns false
, the server is not started properly, in which case you can call GetLastException
to retrieve an exception object.
When our socket server is first started, it will use two dedicated threads, one to accept client connections, the other to scan for incoming data. If data from a client arrives or some error occurs, the server will use threads from the internal thread pool to process the data or to handle the error. Please note that the parameters nMinThreadCount
and nMaxThreadCount
specified in the constructor do not include these two dedicated threads.
User Specified Delegates (Event Handlers)
Our socket server makes use of delegates specified by the user to handle connection request, notify the arrival of incoming data, and process the incoming data. It also supports custom error handling. Here are the four kinds of delegates to be used with our socket server.
public delegate void ExceptionHandlerDelegate(Exception oBug);
This is the user defined error handler, it is set by the SetExceptionHandler
method. The oBug
parameter is an exception object representing the server error. Within the implementation of the error handler you can, for example, call the GetThreadCount
method to retrieve the number of active threads in the internal thread pool. If the number is equal to the maximum allowed, then you may conclude that the server is deadlocked (or stuck) on some resource, in which case you may choose to restart the server application.
To protect the server, a client connection will be closed automatically when a socket error occurs with the connection. Please note that the error handler may be invoked concurrently by different threads from the internal thread pool, therefore the code has to be thread-safe.
public delegate void ConnectionFilterDelegate(string sRemoteAddress,
int nRemotePort, Socket sock);
This handler is invoked by a dedicated thread to process connection requests, it is set by the SetConnectionFilter
method. The sRemoteAddress
and nRemotePort
pair uniquely identifies a client connection. To reject a connection request, you simply call the Shutdown
method and then the Close
method on the sock
parameter, otherwise connection to the client will be established.
There are several things you can do within this handler. You can write the ip address sRemoteAddress
to a log file so that you have a record of who is trying to connect to your server. You can also call the GetClientCount
method to retrieve the total number of connected clients, if the total reaches 100 (or another preferred limit), then reject the connection request.
Another example is that you can limit connections based on ip address by rejecting all requests except those from a list of trusted ip addresses. If you store all the client ip addresses in a shared storage, you can implement code to restrict the number of connections from each individual client machine.
Since this handler is invoked by a dedicated thread, you need to make sure that the code won't block so that new connection requests can be handled promptly. On the other hand, you can call Thread.Sleep
within this handler so that the server does not accept too many clients too quickly (to counter some denial of service attacks).
It is also possible to implement some kind of client authentication from this handler using the sock
parameter. All you have to do is read user name and password from the sock
object and do some checking before deciding to accept this client or not. Of course, the client has to send its user name and password (more details later). But you need to be careful not to block the processing of other connection requests.
public delegate void BinaryInputHandlerDelegate(string sRemoteAddress,
int nRemotePort, byte[] pData);
You need to call the SetBinaryInputHandler
method to set this handler. When a binary message from a client is received, this handler will be invoked automatically by a thread from the internal thread pool. Since it can be executed concurrently by multiple threads, the implementation of this handler has to be thread-safe. The sRemoteAddress
and nRemotePort
pair identifies the client that sent the current message, the message data is in the byte array pData
.
If you want to send a reply message to the client, you can call the SendBinaryData
method or the SendStringData
method within this handler passing sRemoteAddress
, nRemotePort
, and the data you want to send as parameters.
public delegate void StringInputHandlerDelegate(string sRemoteAddress,
int nRemotePort, string sData);
You need to call the SetStringInputHandler
method to set this handler. When a string message from a client is received, this handler will be invoked automatically by a thread from the internal thread pool. Since it can be executed concurrently by multiple threads, the implementation of this handler has to be thread-safe. The sRemoteAddress
and nRemotePort
pair identifies the client that sent the current message, the message data is in the string parameter sData
.
If you want to send a reply message to the client, you can call the SendBinaryData
method or the SendStringData
method within this handler passing sRemoteAddress
, nRemotePort
, and the data you want to send as parameters.
By the way, these four event handlers can only be set once for each instance of the socket server.
Messages, Maximal Data Size, And Timeout
The following methods are used to send messages to clients.
public bool SendBinaryData(string sRemoteAddress, int nRemotePort,
byte[] pData);
public bool SendStringData(string sRemoteAddress, int nRemotePort,
string sData);
Again, the nRemoteAddress
and nRemotePort
pair uniquely identifies the client. These methods can fail (if the return value is false
). Typically, they fail because the client is not connected, or the socket connection is broken, or the socket is currently in use (for processing incoming data). The same functions exist in our client class except that the client versions take only one parameter (the data).
The data sent to a client can be a byte array or a string. The SetMaxDataSize
method can be called to restrict the sizes of incoming messages. If a client is sending a message that is too large, the connection will be closed to protect the server. The default maximal data size is 4 megabytes for both server and client. Please note that the data size of a string message is twice the length of the (unicode) string.
Timeout can occur while receiving a message. The socket connection to the corresponding party will be closed when that happens. Call the SetReadTimout
method to specify the timeout value. The default timeout value is 30 seconds for both server and client.
The Client Class
Our socket client class is XYNetClient
. It is a lot simpler than the socket server described above. The client is also thread-safe at the instance level. To create an instance of the client we need to specify the server address and the server port number in the constructor. The server address can be either the name of the server machine (if it can be resolved successfully) or the ip address used by the server instance. Call the Connect
method to establish a connection to the server instance. The user needs to call the Reset
method explicitly for each client instance to close a connection. As I mentioned before, the SendBinaryData
method and the SendStringData
method can be used to send messages to the connected socket server instance.
The ReceiveData
method is used to receive messages from the server. If successful, it will return an array of two objects. If the message from the server is a binary message, the first object in the returned array will be the byte array sent by the server and the second will be null
. If the message from the server is a string message, the first object in the returned array will be null
and the second will be the string sent by the server. If the method fails, the return value will be null
. Failure can be caused by broken socket connection, timeout, etc. Please not that this method will block (until timed out) if there is no message sent from the server.
The SetMaxDataSize
method and the SetReadTimeout
method are similar to the ones in the server class.
As we have seen, the return value of a method in the client class indicates success or failure. In case of failure, an exception object can be retrieved using the GetLastException
method. An important feature of the client class is that whenever an error occurs while sending data to or receiving data from the server, the connection will be refreshed, i.e. Connect
will be called automatically.
The Connect
method is virtual so that you can implement your own client authentication. What you have to do is, derive a class from XYNetClient
, override the Connect
method. In your override, call the base class version of Connect
first, if successful call the protected method GetSocket
to retrieve the internal socket object, then use the retrieved socket object to send user name and password to the server, and receive authorization code from the server if necessary.
Getting Event Notifications For The Client
Please note that the client object is thread-safe, however, if it is accessed from multiple threads concurrently, all threads except one will block. You can have event notifications for the client, that is, you can connect a client to a server and define a procedure to handle incoming messages. Here sample code to achieve this using the ThreadHelper class described in another article of mine.
class MyClass
{
delegate void myDelegate(XYNetClient oClient);
static void ProcessIncomingMessage(XYNetClient, oClient)
{
while(true)
{
object[] pData = oClient.ReceiveData();
if(pData!=null) MyMessageHandler(pData[0], pData[1]);
}
}
static void MyMessageHandler(byte[] pBinaryMsg, string sStringMsg)
{
}
static void Main(string[] args)
{
XYNetClient oClient = new XYNetClient("myServer", 3000);
if(oClient.Connect())
{
ThreadHelper oHelper = new ThreadHelper();
oHelper.SetMethod(new myDelegate(ProcessIncomingMessage));
oHelper.SetParameter(oClient);
oHelper.StartThread();
}
}
}
The ProcessIncomingMessage method will be executed in a separate thread in the above code, it will call the MyMessageHandler method whenever the client receives a message from the server. You can write code to process incoming messages in the MyMessageHandler method. When the ProcessIncomingMessage method is invoked, the client object will be locked almost all the time and no other thread will be able to use it.
Performance, Sample Code, And Limitations
It is hard to evaluate the performance of a software component without using it in various real applications first. If execution speed is the only measure, then a socket server implemented in low level C code will probably beat any other implementation. Anyway, better execution speed is not my goal. However, I did test my component by instantiating a server instance on one machine and using it to handle 2000 concurrent client connections from other machines. So far, I have not noticed any serious problem.
Although I talked about ways to make the socket server secure, it does not mean that the component can be used in unprotected environment against all kinds of abuses and attacks.
To summarize, here are the steps you need to go through when using the XYNetServer
class.
- Add the included source or dlls to your project.
- Implement the four event handlers as described above.
- Create a server instance in your application, set event handlers, and then call the
StartServer
method.
- Call the
StopServer
method at the end of your application.
Here is what you need to do to use the XYNetClient
class.
- Add the included source or dlls to your project.
- Create a client instance in your application, then call the
Connect
method. The server has to be running before you can do this, of course.
- Call other methods of
XYNetClient
to send and receive messages in your code.
- Call the
Reset
method to terminate the client connection when you are done.
Note that it is possible to use multiple server or client objects in the same program, it is even possible to have multiple client objects connecting to a server object within the same process.
I have included a C# test application. The same executable can be run as a server and as a client. Here is how to run the test application. First, start the server process with the following command.
XYNetSocketTest 3000
The above command will start the server on port 3000. You can start one or more client processes with the following command.
XYNetSocketTest MyServer.MyCompany.Com 3000
The string "MyServer.MyCompany.Com" in the above command can be replaced by the actual ip address of the server machine. The above command can also be run on different machines on the network. If there is a firewall between the client machine and the server machine, then you need to configure the firewall first to allow socket communication on the specified port.
The server process will run for 60 seconds and then shutdown itself, it will reply to any message received. The client process will create 100 socket connections to the server, send a binary message and a string message for each connection to the server, it will also read reply messages from the server.
For you convenience, here is a simplified version of the test application. It creates sever and clients in the same process and loops forever.
class Test
{
private static XYNetServer myServer = null;
private static void ExceptionHandler(Exception oBug)
{
System.Console.Out.WriteLine("Error: " + oBug.Message);
}
private static void ConnectionFilter(string sRemoteAddress,
int nRemotePort, Socket sock)
{
System.Console.Out.WriteLine("Accept " + sRemoteAddress + ":" +
nRemotePort.ToString());
}
private static void StringInputHandler(string sRemoteAddress,
int nRemotePort, string sData)
{
System.Console.Out.WriteLine("Server: " + sRemoteAddress + ":" +
nRemotePort.ToString());
System.Console.Out.WriteLine(" " + sData);
myServer.SendStringData(sRemoteAddress, nRemotePort, "Reply: " + sData);
}
[STAThread]
static void Main(string[] args)
{
try
{
myServer = new XYNetServer("", 2560, 5, 10);
myServer.SetConnectionFilter(
new ConnectionFilterDelegate(Test.ConnectionFilter));
myServer.SetExceptionHandler(
new ExceptionHandlerDelegate(Test.ExceptionHandler));
myServer.SetStringInputHandler(
new StringInputHandlerDelegate(Test.StringInputHandler));
if(myServer.StartServer()==false) throw myServer.GetLastException();
const int nSize = 10;
const int nPause = 10;
XYNetClient[] pClients = new XYNetClient[nSize];
for(int i=0;i<nSize;i++)
{
pClients[i] = new XYNetClient("", 2560);
if(pClients[i].Connect()==false)
throw pClients[i].GetLastException();
Thread.Sleep(nPause);
}
while(true)
{
for(int i=0;i<nSize;i++)
{
if(pClients[i].SendStringData("This is test " +
i.ToString())==false)
throw pClients[i].GetLastException();
Thread.Sleep(nPause);
}
for(int i=0;i<nSize;i++)
{
Object[] pData = pClients[i].ReceiveData();
if(pData==null) throw pClients[i].GetLastException();
string sData = (string)(pData[1]);
System.Console.Out.WriteLine("Client: reply from server");
System.Console.Out.WriteLine(" " + sData);
Thread.Sleep(nPause);
}
}
}
catch(Exception oBug)
{
System.Console.Out.WriteLine("Error Type: " + oBug.GetType().Name);
System.Console.Out.WriteLine("Error Message: " + oBug.Message);
System.Console.Out.WriteLine("Error Source: " + oBug.Source);
System.Console.Out.WriteLine("Error StackTrace: " + oBug.StackTrace);
}
}
}
This component is intended to be self-contained which means you have to use it in both of your client and server applications. A drawback is, it is not possible to use other socket programs to communicate with this component directly.
Reaching outside of the .NET world
I have always thought that a software component is not that useful if you have to redesign or rewrite a significant portion of your existing code. As promised in the last version of this article, I have implemented a java class XYNetJavaClient
that can act as a client to the XYNetSocket
component. You can use the java class from your java programs. There is also a com dll XYNetComClient.dll
which can be used in old VB and C/C++ programs.
The java and com client components have almost the same methods as the .NET client class. For example, Connect
, SendStringData
, SendBinaryData
, and ReceiveData
methods are implemented and work (almost) the same way as the .NET client. For your convenience, I have included test programs that work with the .NET test server described above.
To start the java test program, you issue the following command
java XYNetJavaClient MyServer.MyCompany.com 3000
To test the com client, you can use the VB script file XYNetClientTest
.vbs.
Thank you for reading my articles on Code Project and using my tools.
Recent Updates
- 14 Oct 2005:Updated source code
- 24 Mar 2004: Modified the client class implementation. Updated the article to include event notification for a client.
- 28 Jan 2004: Added java and com client components.
- 4 Dec 2003: Added simple sample code to the article. Modified the socket server so that you can recycle an instance (shutdown and start again).