Tuesday, July 1, 2008

.NET Sockets in Two Directions with Multiple Client Support (C# Source Code Included)

  • This post contains generic code that's ready for use.
  • Full solution is available at the end of the post.

Preface

This post will walk you through the implementation of a simple client-server application that establishes two way communication via .NET sockets while using infrastructure package that extracts the low level sockets API from the application.

image

The basic package can be added with an extra layer which will allow the transport of typed messages, please refer to ‘Sending Typed (Serialized) Messages’ for in detail review and case study.

If you don’t need multiple client support, please refer to ".NET Sockets - Single Client"

Implementation

Server-Side
Server Terminal

ServerTerminal opens TCP port, waits for clients connection, accepts multiple connections, listen to clients messages (bytes array) and allow to broadcast messages (bytes array) to all connected client.

Every client that connect to the server is wrapped-up in ConnectedClient object and added to clients collection. 

public class ServerTerminal
{
    public event TCPTerminal_MessageRecivedDel MessageRecived;
    public event TCPTerminal_ConnectDel ClientConnect;
    public event TCPTerminal_DisconnectDel ClientDisconnect;

    private Socket m_socket;
    private bool m_Closed;

    private Dictionary<long, ConnectedClient> m_clients = 
        new Dictionary<long, ConnectedClient>();
    
    public void StartListen(int port)
    {
        IPEndPoint ipLocal = new IPEndPoint(IPAddress.Any, port);

        m_socket = new Socket(AddressFamily.InterNetwork, 
            SocketType.Stream, ProtocolType.Tcp);
        
        try
        {
            m_socket.Bind(ipLocal);
        }
        catch(Exception ex)
        {
            Debug.Fail(ex.ToString(),
                string.Format("Can't connect to port {0}!", port));
            
            return;
        }

        m_socket.Listen(4);
        
        // Assign delegate that will be invoked when client connect.
        m_socket.BeginAccept(new AsyncCallback(OnClientConnection), null);
    }

    private void OnClientConnection(IAsyncResult asyn)
    {
        if (m_Closed)
        {
            return;
        }

        try
        {
            Socket clientSocket = m_socket.EndAccept(asyn);

            RaiseClientConnected(clientSocket);
            
            ConnectedClient connectedClient = new ConnectedClient(clientSocket);

            connectedClient.MessageRecived += OnMessageRecived;
            connectedClient.Disconnected += OnClientDisconnection;

            connectedClient.StartListen();

            long key = clientSocket.Handle.ToInt64();
            if (m_clients.ContainsKey(key))
            {
                Debug.Fail(string.Format(
                    "Client with handle key '{0}' already exist!", key));
            }

            m_clients[key] = connectedClient;
            
            // Assign delegate that will be invoked when next client connect.
            m_socket.BeginAccept(new AsyncCallback(OnClientConnection), null);
        }
        catch (ObjectDisposedException odex)
        {
            Debug.Fail(odex.ToString(),
                "OnClientConnection: Socket has been closed");
        }
        catch (Exception sex)
        {
            Debug.Fail(sex.ToString(), 
                "OnClientConnection: Socket failed");
        }
    }

    private void OnClientDisconnection(Socket socket)
    {
        RaiseClientDisconnected(socket);

        long key = socket.Handle.ToInt64();
        if (m_clients.ContainsKey(key))
        {
            m_clients.Remove(key);
        }
        else
        {
            Debug.Fail(string.Format(
                "Unknown client '{0}' has been disconnected!", key));
        }
    }
  public void DistributeMessage(byte[] buffer)
  {
      try
      {
          foreach (ConnectedClient connectedClient in m_clients.Values)
          {
              connectedClient.Send(buffer);
          }
      }
      catch (SocketException se)
      {
          Debug.Fail(se.ToString(), string.Format(
             "Buffer could not be sent"));
      }
 }
    public void Close()
    {
        try
        {
            if (m_socket != null)
            {
                m_Closed = true;

                // Close the clients
                foreach (ConnectedClient connectedClient in m_clients.Values)
                {
                    connectedClient.Stop();
                }

                m_socket.Close();

                m_socket = null;
            }
        }
        catch (ObjectDisposedException odex)
        {
            Debug.Fail(odex.ToString(), "Stop failed");
        }
    }

    private void OnMessageRecived(Socket socket, byte[] buffer)
    {
        if (MessageRecived != null)
        {
            MessageRecived(socket, buffer);
        }
    }

    private void RaiseClientConnected(Socket socket)
    {
        if (ClientConnect != null)
        {
            ClientConnect(socket);
        }
    }

    private void RaiseClientDisconnected(Socket socket)
    {
        if (ClientDisconnect != null)
        {
            ClientDisconnect(socket);
        }
    }
}
ConnectedClient

This class is instantiated for each client that connect to the server. It utilizes the SocketListener class (will be reviewed shortly) which listen and delegate the messages coming from the client.

public class ConnectedClient
{
    // Hold reference to client socket to allow sending messages to client
    private Socket m_clientSocket;
    SocketListener m_listener;

    public ConnectedClient(Socket clientSocket)
    {
        m_clientSocket = clientSocket;
        m_listener = new SocketListener();
    }

    // Register directly to SocketListener event
    public event TCPTerminal_MessageRecivedDel MessageRecived
    {
        add
        {
            m_listener.MessageRecived += value;
        }
        remove
        {
            m_listener.MessageRecived -= value;
        }
    }

    // Register directly to SocketListener event
    public event TCPTerminal_DisconnectDel Disconnected
    {
        add
        {
            m_listener.Disconnected += value;
        }
        remove
        {
            m_listener.Disconnected -= value;
        }
    }

    public void StartListen()
    {
        m_listener.StartReciving(m_clientSocket);
    }

    public void Send(byte[] buffer)
    {
        if (m_clientSocket == null)
        {
            throw new Exception("Can't send data. ConnectedClient is Closed!");
        }
        m_clientSocket.Send(buffer);
        
    }

    public void Stop()
    {
        m_listener.StopListening();
        m_clientSocket = null;
    }
}
Server Host (Console)

The server host instantiate the ServerTerminal, register to the appropriate events and call StartListening. As a result, multiple clients can connect to its port and start sending/receiving messages.

m_ServerTerminal = new ServerTerminal();

m_ServerTerminal.MessageRecived += m_Terminal_MessageRecived;
m_ServerTerminal.ClientConnect += m_Terminal_ClientConnected;
m_ServerTerminal.ClientDisconnect += m_Terminal_ClientDisConnected;

m_ServerTerminal.StartListen(alPort);
Both-Sides
Socket Listener

SocketListener allows both ServerTerminal and ClientTetminal to listen to messages coming a socket. When a message arrives – the SocketListener figures out whether it represents new data or whether it represents 'connection dropped' message. In case the message represents new data it raises the MessageReceived event and waits for the next message. In case the message indicate that the connection has been dropped - it raises the Disconnected event and exits.

public class SocketListener
{
    private const int BufferLength = 1000;
    AsyncCallback pfnWorkerCallBack;
    Socket m_socWorker;

    public event TCPTerminal_MessageRecivedDel MessageRecived;
    public event TCPTerminal_DisconnectDel Disconnected;

    public void StartReciving(Socket socket)
    {
        m_socWorker = socket;
        WaitForData(socket);
    }

    private void WaitForData(System.Net.Sockets.Socket soc)
    {
        try
        {
            if (pfnWorkerCallBack == null)
            {
                pfnWorkerCallBack = new AsyncCallback(OnDataReceived);
            }
            
            CSocketPacket theSocPkt = new CSocketPacket(BufferLength);
            theSocPkt.thisSocket = soc;

         // Start waiting asynchronously for single data packet
         soc.BeginReceive(
                theSocPkt.dataBuffer,
                0,
                theSocPkt.dataBuffer.Length,
                SocketFlags.None,
                pfnWorkerCallBack,
                theSocPkt);
        }
        catch (SocketException sex)
        {
            Debug.Fail(sex.ToString(), "WaitForData: Socket failed");
        }

    }

    private void OnDataReceived(IAsyncResult asyn)
    {
        CSocketPacket theSockId = (CSocketPacket)asyn.AsyncState;
        Socket socket = theSockId.thisSocket;

        if (!socket.Connected)
        {
            return;
        }

        try
        {
            int iRx;
            try
            {
                iRx = socket.EndReceive(asyn);
            }
            catch (SocketException)
            {
                Debug.Write("Client has been closed and cannot answer.");

                OnConnectionDroped(socket);
                return;
            }

            if (iRx == 0)
            {
                Debug.Write("Client socket has been closed.");

                OnConnectionDroped(socket);
                return;
            }

            RaiseMessageRecived(theSockId.dataBuffer);
       // Wait for the next package
            WaitForData(m_socWorker);
        }
        catch (Exception ex)
        {
            Debug.Fail(ex.ToString(), "OnClientConnection: Socket failed");
        }
    }

    public void StopListening()
    {
        if (m_socWorker != null)
        {
            m_socWorker.Close();
            m_socWorker = null;
        }
    }

    private void RaiseMessageRecived(byte[] buffer)
    {
        if (MessageRecived != null)
        {
            MessageRecived(m_socWorker, buffer);
        }
    }

    private void OnDisconnection(Socket socket)
    {
        if (Disconnected != null)
        {
            Disconnected(socket);
        }
    }

    private void OnConnectionDroped(Socket socket)
    {
        m_socWorker = null;
        OnDisconnection(socket);
    }
}

public class CSocketPacket
{
    public System.Net.Sockets.Socket thisSocket;
    public byte[] dataBuffer;

    public CSocketPacket(int buffeLength)
    {
        dataBuffer = new byte[buffeLength];
    }
}
Client-Side
Client Terminal

ClientTerminal connects to TCP port, sends messages (bytes array) to the server and listens to server messages (bytes array).

public class ClientTerminal
{
    Socket m_socClient;
    private SocketListener m_listener;

    public event TCPTerminal_MessageRecivedDel MessageRecived;
    public event TCPTerminal_ConnectDel Connected;
    public event TCPTerminal_DisconnectDel Disconncted;

    public void Connect(IPAddress remoteIPAddress, int alPort)
    {
        m_socClient = new Socket(AddressFamily.InterNetwork, 
            SocketType.Stream, ProtocolType.Tcp);
        
        IPEndPoint remoteEndPoint = new IPEndPoint(remoteIPAddress, alPort);
        
        m_socClient.Connect(remoteEndPoint);

        OnServerConnection();
    }

    public void SendMessage(byte[] buffer)
    {
        if (m_socClient == null)
        {
            return;
        }
        m_socClient.Send(buffer);

    }

    public void StartListen()
    {
        if (m_socClient == null)
        {
            return;
        }

        if (m_listener != null)
        {
            return;
        }

        m_listener = new SocketListener();
        m_listener.Disconnected += OnServerConnectionDroped;
        m_listener.MessageRecived += OnMessageRecvied;
        
        m_listener.StartReciving(m_socClient);
    }

    public void Close()
    {
        if (m_socClient == null)
        {
            return;
        }

        if (m_listener != null)
        {
            m_listener.StopListening();
        }

        m_socClient.Close();
        m_listener = null;
        m_socClient = null;
    }

    private void OnServerConnection()
    {
        if (Connected != null)
        {
            Connected(m_socClient);
        }
    }

    private void OnMessageRecvied(Socket socket, byte[] buffer)
    {
        if (MessageRecived != null)
        {
            MessageRecived(socket, buffer);
        }
    }

    private void OnServerConnectionDroped(Socket socket)
    {
        Close();
        RaiseServerDisconnected(socket);
    }

    private void RaiseServerDisconnected(Socket socket)
    {
        if (Disconncted != null)
        {
            Disconncted(socket);
        }
    }
}
Client Host (Console)

Client host should instantiate ClientTerminal and call 'Connect' with server-name/IP-address and port. After that call - m_terminal can be used to send/receive messages to/from the server.

m_ClientTerminal = new ClientTerminal();

m_ClientTerminal.Connected += m_TerminalClient_Connected;
m_ClientTerminal.Disconncted += m_TerminalClient_ConnectionDroped;
m_ClientTerminal.MessageRecived += m_TerminalClient_MessageRecived;

m_ClientTerminal.Connect(remoteIPAddress, alPort);

 

Sample project

Download from here

6 comments:

  1. Hi,

    how do I make sure that all the bytes of my message has arrived correclty, or that there were more than 1 message that arrived.

    ReplyDelete
  2. thank you for this nice code

    ReplyDelete
  3. This a very nice code. Helped a lot. On request, Can you provide the same implementation for UDP protocol and SocketType.Dgram used. If you cannot post and still can send me to help me out this is my add - jc_mca atTHErate yahoo.com. Thanks in advance.

    ReplyDelete
  4. Adding support for UDP is not so simple, since UDP is connection less etc.

    The following link may help, the post there includes implementation of UDP communication with support for Multicast.
    http://aviadezra.blogspot.com/2009/07/multicast-igmp-c-code-sample-net.html

    Good luck.

    ReplyDelete
  5. Hi Aviade,, a wonderful post i have come through after 8 yrs,thanks for the demonstration and a helpful work for taking in pain to develop.

    I have a novice question, can the server support more than 1000 connections, assuming am hosting this on Win 2012 Server ?

    Why i am asking this is, am developing a server based s/w which will be connecting ~1500 clients, thought of developing a Client Manager which will connect with the server.

    Idea is to develop a client manager which will block until the server starts listening.

    Any other ideas are also appreciated.

    Thanks,
    Ronald

    ReplyDelete