Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / desktop / Win32

EpServerEngine.cs - A lightweight asynchronous IOCP TCP Template Server-Client Framework using C# and Socket

4.94/5 (19 votes)
1 Jul 2015MIT8 min read 45.1K   1.1K  
How to create a server-client network quickly using IOCP TCP template server-client framework, EpServerEngine.cs (C#).

Table of contents

  1. Introduction 
  2. Background
    1. Optional
  3. Purpose of the Template Server-Client Framework 
  4. Framework Classes
    1. Server side
    2. Client side
    3. General
  5. Conclusion
  6. Reference 

Introduction  

After writing the EpServerEngine framework with C++, I thought it would be much easier to build prototype if there is template server-client framework with C#. I would recommend this for building quick prototypes or testing packet communications. Moreover, take this as a guideline for building your own template server-client framework. 

Background  

For detailed information related to "Socket", please refer to MSDN by Microsoft. Microsoft provides easy guides on how to create a client and a server using System.Net.Sockets. I highly recommend you to follow this tutorial using Microsoft Visual Studio.

Optional  

  1. Utility Library used in EpServerEngine.cs
  2. The original idea used in EpServerEngine.cs 

Purpose of the Template Server-Client Framework  

As a software developer, many people will agree that writing the same code over and over again is a very lame job to do. It is even worse when if the code is just a bunch of nasty initializers. This template server-client framework will do all those jobs for you, and you only have to design and implement your packet structure and packet processing routine for the server and client, and leave the rest of the jobs to the framework (initialization/packet waiting/creating the worker threads/mutual exclusion for the network, etc.).

Framework Classes  

Server side  

IocpTcpServer class  

This is a IOCP TCP server class which is responsible for the following:

  • Starts the server 
  • Stops the server  
  • Sets up the port to use   
  • Broadcast the messages to all clients that are connected.  
  • Disconnects all the clients that are connected (for emergency use)   

In addition to the above functionalities, it internally also creates a listening thread, which accepts the clients, and creates a socket for each clients connected, in order to communicate with the clients that are connected.

What do I have to do to create my own server? 

That is very simple.

C#
using EpServerEngine.cs;

// MyServer.cs

public class MyServer
{
   ...
   INetworkServer m_server = new IocpTcpServer();
   ...
};
  1. Create your Server class (ex. MyServer)  
  2. Create a member variable with new IocpTcpServer object.
  3. Done!   
How to start/stop the server 

You can very easily start/stop your server as below:

C#
using EpServerEngine.cs;

// MyServer.cs

public class MyServer
{
   ...
   INetworkServer m_server = new IocpTcpServeR();
   ... 
   void StartServer(string portString)
   {
      ServerOps ops = new ServerOps(callbackObj, portString);
      m_server.StartServer(ops);
   }
   ...  
   void StopServer()
   {
      if(m_server.IsServerStarted()) // check if server is started 
      {  
         m_server.StopServer();  // Disconnect all clients connected and stop listening 
      }
   }  
   ...
};       
What is this ServerOps class and callbackObj?  

IocpTcpServer requires a ServerOps object to start the server.

ServerOps takes in two parameters, callback object(which inherits INetworkServerCallback) for the server and port number as string for server listening.

IocpTcpServer will call the callback methods on three situations with the info.

  • OnServerStarted [ void OnServerStarted(INetworkServer server, StartStatus status) ]
    • OnServerStarted will be called when either server is started successfully or failed to start.
    •  You can check the status with StartStatus whether server started successfully or not, and try to start the server again if it failed.
  • OnAccept [ INetworkSocketCallback OnAccept(INetworkServer server, IPInfo ipInfo) ]
    • OnAccept has two parameters, server and ipInfo. server refers to the server instance that accepted the client for the rest of callback. ipInfo refers to the IP information of the client trying to connect.
    • You can check the ipInfo and reject the connection by returning null, or you can return the callback object for socket connected to this client to accept. (this will be explained in detail later.)
  • OnServerStopped [ void OnServerStopped(INetworkServer server) ]
    • OnServerStopped will be called when the given server is completely stopped.

So you can make your callback object for the server as your own server class, MyServer.cs.

C#
using EpServerEngine.cs;

// MyServer.cs

public class MyServer: INetworkServerCallback
{
   ...
   INetworkServer m_server = new IocpTcpServer();
   ...
   void StartServer(string portString)
   {
      ServerOps ops=new ServerOps(this, portString);
      m_server.StartServer(ops);
   }
   ...
   public void OnServerStarted(INetworkServer server, StartStatus status)
   {
      if(status==StartStatus.SUCCESS || status == StartStatus.FAIL_ALREADY_STARTED)
      {
         // Server is started
      }
      else
      {
         // Server failed to start
      }
   }

   public INetworkSocketCallback OnAccept(INetworkServer server, IPInfo ipInfo)
   {
      // reject client with ip 10.231.44.124, otherwise accept
      if (ipInfo.GetIPAddress().Equals("10.231.44.124"))
      {
         return null;
      }
      return new ServerSocketCallback(); // Socket callback object explained later
   }
       
   public void OnServerStopped(INetworkServer server)
   {
      // Server is stopped
   }
   ... 
}

Or you can also create a separate class and pass an instance of that class to the ServerOps as below:

C#
using EpServerEngine.cs;

// ServerCallbackObj.cs

public class ServerCallbackObj: INetworkServerCallback
{
   ...
   public void OnServerStarted(INetworkServer server, StartStatus status)
   {
      if(status==StartStatus.SUCCESS || status == StartStatus.FAIL_ALREADY_STARTED)
      {
         // Server is started
      }
      else
      {
         // Server failed to start
      }
   }

   public INetworkSocketCallback OnAccept(INetworkServer server, IPInfo ipInfo)
   {
      // reject client with ip 10.231.44.124, otherwise accept
      if (ipInfo.GetIPAddress().Equals("10.231.44.124"))
      {
         return null;
      }
      return new ServerSocketCallback(); // Socket callback object explained later
   }
       
   public void OnServerStopped(INetworkServer server)
   {
      // Server is stopped
   }
   ... 
};

// MyServer.cs

public class MyServer{

   ...
   void StartServer(string portString)
   {
      INetworkServerCallback callbackObj = new ServerCallbackObj();
      ServerOps ops = new ServerOps(callbackObj, portString);
      m_server.StartServer(ops);
   }
   ...
}  

For more detail, please refer to the EpServerEngine.cs' Wiki page, here.

IocpTcpSocket class  

The socket is automatically created by IocpTcpServer instance when client is accepted with socket callback object. This IocpTcpSocket class is responsible for the following:

  • Send/Receive messages to/from client  
  • Disconnect client

You can also check the connection status or IP information of client with the socket.

How do I create this socket callback class? 

This is very simple job as creating the server above.

C#
using EpServerEngine.cs;

// MyServerSocketCallback.cs

class MyServerSocketCallback: INetworkSocketCallback
{
   void OnNewConnection(INetworkSocket socket)
   {
      // the client is connected on given socket
   }

   void OnReceived(INetworkSocket socket, Packet receivedPacket)
   {
      // the data is received from client with given socket within receivedPacket
      byte[] receivedPacketBytes=receivedPacket.GetPacket();
   }

   void OnSent(INetworkSocket socket, SendStatus status)
   {
      // the data is sent to client with given status
      if(status==SendStatus.SUCCESS)
      {
         // succesfully sent
      }
      else
      {
         // failed to send data to client
      }
   }
   
   void OnDisconnect(INetworkSocket socket)
   {
      // client with given socket is disconnected
   }

};
  1. Create your socket callback class (ex. MyServerSocketCallback)  
  2. Make your class to inherit INetworkSocketCallback  
  3.  Implement four callback functions (OnNewConnection, OnReceived, OnSent, OnDisconnect)
  4. Done! 
What are these four callbacks?  

IocpTcpSocket will call the callback methods on four situations with the info.

  • OnNewConnection [ void OnNewConnection(INetworkSocket socket) ]
    • OnNewConnection will be called when the client is accepted from the server.
  • OnReceived [ void OnReceived(INetworkSocket socket, Packet receivedPacket) ]
    • OnReceived has two parameters, socket and receivedPacket. socket refers to the socket instance that connected to the client for the rest of callback. receivedPacket referes to the packet class which contains data sent by client.
  • OnSent [ void OnSent(INetworkSocket socket, SendStatus status) ]
    • OnSent will be called when either the data is sent to client successfully or not.
    • You can check the status with SendStatus on whether the data is successfully sent or not, and try to send the data again if it failed.
  • OnDisconnect [ void OnDisconnect(INetworkSocket socket) ]
    • OnDisconnect will be called when the client is disconnected from the server
How to use this server socket callback class? 

return the instance of this socket callback class on "OnAccept" call within your server callback class, which inherits INetworkServerCallback

So how do I send data to the client?  

With the instance of INetworkSocket, you can simply send the data to the client as below:

C#
...
// socket is an instance of INetworkSocket
String helloString="hello";
byte[] packet = new byte[helloString.Length*sizeof(char)];
System.Buffer.BlockCopy(helloString.ToCharArray(), 0, packet, 0, packet.Length);
socket.Send(new Packet(packet, packet.Count(), false));
...

Or you can use the instance of INetworkServer to broadcast the data to all the clients connected as below:

C#
...
// socket is an instance of INetworkSocket
String helloString="hello";
byte[] packet = new byte[helloString.Length*sizeof(char)];
System.Buffer.BlockCopy(helloString.ToCharArray(), 0, packet, 0, packet.Length);
server.Broadcast(new Packet(packet, packet.Count(), false));
...

You can use Stream or Serializer and make your data into byte array (byte[]) to incorporate into Packet class, then send the packet with Send/Broadcast method.

For more detail, please refer to the EpServerEngine.cs's Wiki page, here.

Client side  

IocpTcpClient class  

This is IOCP TCP client class which is responsible for the following:

  • Connect to a server
  • Disconnect from a server 
  • Send/Receive the messages to/from a server. 

What do I have to do to create my own client?   

C#
using EpServerEngine.cs;

// MyClient.cs

public class MyClient
{
   ...
   INetworkClient m_client = new IocpTcpClient();
   ...
};
  1. Create your Client class (e.g., MyClient
  2. Create a member variable with new IocpTcpClient object.
  3. Done!   
How to connect/disconnect to/from the server  

You can very easily connect/disconnect to/from your server as below:

C#
using EpServerEngine.cs;

// MyClient.cs

public class MyClient
{
   ...
   INetworkClient m_client = new IocpTcpClient();
   ...
   void ConnectToServer(String hostname, String port)
   {
      ClientOps ops= new ClientOps(callbackObj, hostname,port);
      m_client.Connect(ops);
   }

   void DisconnectFromServer()
   {
      if(m_client.IsConnectionAlive()) // check if connected to server
      { 
         m_client.Disconnect();        // disconnect from the server
      }
   }
   ... 
};   
What is this ClientOps class and callbackObj?  

IocpTcpClient requires a ClientOps object to connect to the server similart to ServerOps.

ClientOps takes in three parameters, callback object(which inherits INetworkClientCallback) for the client, hostname as string, and port number as string for server connection.

IocpTcpClient will call the callback methods on four situations with the info.

  • OnConnected [ void OnConnected(INetworkClient client, ConnectStatus status) ]
    • OnConnected will be called when either client is connected to the server successfully or failed to connect.
    •  You can check the status with ConnectStatus whether client connected to the server successfully  or not, and try to connect to the server again if it failed.
  • OnReceived [ void OnReceived(INetworkClient client, Packet receivedPacket) ]
    • OnReceived has two parameters, client and receivedPacket. client refers to the client instance that connected to the server for the rest of callback. receivedPacket refers to the packet class which contains data sent by client.
  • OnSent [ void OnSent(INetworkClient client, SendStatus status) ]
    • OnSent will be called when either the data is sent to server successfully or not.
    • You can check the status with SendStatus whether the data is successfully sent or not, and try to send the data again if it failed.
  • OnDisconnect [ void OnDisconnect(INetworkClient client) ]
    • OnDisconnect will be called when the client is disconnected from the server.

So you can make your callback object for the client as your own client class, MyClient.cs:

C#
using EpServerEngine.cs;

// MyClient.cs

public class MyClient: INetworkClientCallback
{
   ...
   INetworkClient m_client = new IocpTcpClient();
   ...
   void ConnectToServer(string hostname, string portString)
   {
      ClientOps ops=new ClientOps(this, hostname, portString);
      m_server.Connect(ops);
   }
   ...
   public void OnConnected(INetworkClient client, ConnectStatus status)
   {
      if(status==ConnectStatus.SUCCESS || status == ConnectStatus.FAIL_ALREADY_CONNECTED)
      {
         // Client is connected to the server
      }
      else
      {
         // failed to connect
      }
   }
 
  void OnReceived(INetworkClient client, Packet receivedPacket)
   {
      // the data is received from server within receivedPacket
      byte[] receivedPacketBytes=receivedPacket.GetPacket();
   }

   void OnSent(INetworkClient client, SendStatus status)
   {
      // the data is sent to server with given status
      if(status==SendStatus.SUCCESS)
      {
         // succesfully sent
      }
      else
      {
         // failed to send data to the server
      }
   }
   
   void OnDisconnect(INetworkClient client)
   {
      // client is disconnected from the server
   }
   ... 
}
Or you can also create separate class and pass an instance of that class to ClientOps class as below:
C#
using EpServerEngine.cs;

// ClientCallbackObj.cs

public class ClientCallbackObj: INetworkClientCallback
{
   ...
   public void OnConnected(INetworkClient client, ConnectStatus status)
   {
      if(status==ConnectStatus.SUCCESS || status == ConnectStatus.FAIL_ALREADY_CONNECTED)
      {
         // Client is connected to the server
      }
      else
      {
         // failed to connect
      }
   }
   
   void OnReceived(INetworkClient client, Packet receivedPacket)
   {
      // the data is received from server within receivedPacket
      byte[] receivedPacketBytes=receivedPacket.GetPacket();
   }

   void OnSent(INetworkClient client, SendStatus status)
   {
      // the data is sent to server with given status
      if(status==SendStatus.SUCCESS)
      {
         // succesfully sent
      }
      else
      {
         // failed to send data to the server
      }
   }
   
   void OnDisconnect(INetworkClient client)
   {
      // client is disconnected from the server
   }
   ... 
};

// MyClient.cs

public class MyClient{

   ...
   void ConnectToServer(string hostname, string portString)
   {
      INetworkClientCallback callbackObj = new ClientCallbackObj();
      ClientOps ops = new ClientOps(callbackObj, hostname, portString);
      m_client.Connect(ops);
   }
   ...
}  
So how do I send data to the server?  

With the instance of INetworkClient, you can simply send the data to the server as below:

C#
...
// Whether client is INetworkClient or IocpTcpClient
String helloString="hello";
byte[] packet = new byte[helloString.Length*sizeof(char)];
System.Buffer.BlockCopy(helloString.ToCharArray(), 0, packet, 0, packet.Length);
client.Send(new Packet(packet, packet.Count(), false));
...

You can use Stream or Serializer and make your data into byte array (byte[]) to incorporate into Packet class, then send the packet with Send method.

For more detail, please refer to the EpServerEngine.cs's Wiki page, here.

General  

PacketSerializer class  

This is a packet serializer class which is responsible for the following:

  • Serialize the instance of a serializable class to byte array.
  • Deserialize the byte array to given class.
C#
// Serializable Class example

[Serializable]
public class SomeData: ISerializable
{
   public int packetCode;
   public long data;
   public SomeData()
   {}
   public SomeData(int iPacketcode, long iData)
   {
      packetCode= iPacketcode;
      data= iData;
   }
   public void GetObjectData(SerializationInfo info, StreamingContext context)
   {
      info.AddValue("packetCode", packetCode, typeof(int));
      info.AddValue("data", data,typeof(long));
   }

   public SomeData(SerializationInfo info, StreamingContext context)
   {
      packetCode = (int)info.GetValue("packetCode", typeof(int));
      data = (long)info.GetValue("data",typeof(long));
   }
};

...

// Class Serializing example
PacketSerializer<SomeData> serializer =new PacketSerializer<SomeData>(new SomeData(1,10));
byte[] byetarr=serializer.GetPacketRaw();
...

// Class Deserialize example
void OnReceived(INetworkSocket socekt, Packet receivedPacket)
{
   PacketSerializer<SomeData> serializer = new PacketSerializer<SomeData>(receivedPacket.GetPacket(), 0, receivedPacketByteSize());
   SomeData someData = serializer.GetPacket();
   Debug.Log(someData.packetCode+" " + someData.data);
}

For more detail, please refer to the EpServerEngine.cs's Wiki page, here

Packet class  

 This is a packet class which is responsible for the following:

  • Basic unit of data to communicate between the server and the client for EpServerEngine.cs. 
How to use this packet class? 
  • PacketSerializer
  • MemoryStream
  • etc.

For example, if you define the packet structure as above, you can create Packet object as below to send it to the server/client.

C#
// PacketSerializer
PacketSerializer<SomeData> serializer = new PacketSerializer<SomeData>(new SomeData(14));
byte[] packet = serializer.GetPacketRaw();
Packet sendPacket= new Packet(packet             // byte array of packet
                              , packet.Count()   // byte size of the data
                              , false);          // flag whether to allocate the memory
                                                 //   for the data within Packet Object or not.

////////////////////////////
// OR
////////////////////////////

// MemoryStream
byte[] packet = new byte[12];
MemoryStream stream = new MemoryStream(packet);
stream.Write(BitConverter.GetBytes((int)PacketCode.SOME_CODE), 0, 4);
stream.Write(BitConverter.GetBytes((long)14),0,8);

Packet sendPacket = new Packet(packet, packet.Count(), false);
  • Note that since copying memory for packet is critical on the performance in Server development, you can choose whether to allocate the memory for the Packet object, or just hold the reference to the data byte array within the Packet object according to the situation.  

For more detail, please refer to the EpServerEngine.cs's Wiki page, here

Conclusion  

As I said in Introduction, this might not be the best framework out there. However, since using this framework will allow you to build the server and client very quickly, I believe this can be very helpful for developing prototype server/client applications, or testing packet communications, since this enhances you to start from server/client communication design by letting you ignore all the nasty initialization and handling of IOCP Sockets and threads. Hope this helps for your server/client developments. 

Reference  

History  

  • 06.12.2015: - Minor update on Article
  • 11.11.2014: - Title changed.
  • 11.10.2014: - Typo fixed.
  • 10.27.2014: - Source updated.
  • 10.25.2014: - Submitted the article.  

License

This article, along with any associated source code and files, is licensed under The MIT License