Click here to Skip to main content
15,867,568 members
Articles / Programming Languages / C#

Simple Client-server Interactions using C#

Rate me:
Please Sign up or sign in to vote.
4.63/5 (53 votes)
10 Apr 2012CPOL7 min read 948.9K   35.1K   183   171
Introduces a couple of cover classes to provide event-driven interaction with TCP/IP sockets and simple message protocols

Introduction

Sockets are an important part of the modern programmer's armory. To read this article, you are making extensive use of sockets – the article itself, and each image, come down a socket to your machine, and perhaps two, if you're reading this at work, behind a router. Communicating with other machines lets you spread computationally expensive tasks, share data and so on. Yet the Framework doesn't provide as good a toolkit as it does for, for example, Windows Forms (unless you are doing something 'standard' like making HTTP requests). If someone sends me some data, or connects to my server, I want an event to be thrown, just like if someone presses a button on my form.

There are also problems with networks, and specifically with TCP. Connections can be lost at any time, so an OnDisconnect event would be nice so that we can take appropriate action if this happens. (The appropriate action varies from a message box to cleaning up information stored on a server about the client who disappeared.) What I send as one message can arrive as several, or vice versa; while TCP guarantees that data arrives in the right order (if it arrives at all), it has no concept of individual messages.

I have written a small assembly which provides event-driven client-server communication over TCP (using the tools provided in System.Net.Sockets). It also specifies a simple message-based protocol so that you can keep your messages together, although you don't have to use it.

Being a Simple Client

A client is the term for a user who connects to a server, typically to request data. Your browser acts as a client while it downloads material from the Internet. For most problems, there is one server and many clients, and if you're writing an application which talks to existing servers, all you need to use is a client socket.

Creating a client socket with ordinary .NET functions is slightly messy, so I provide a cover. And once you have the socket, you need to create an instance of ClientInfo to get event-driven access to the data.

C#
// At the top of the file, you will always need
using System.Net.Sockets;
using RedCorona.Net;

class SimpleClient{
  ClientInfo client;
  void Start(){
    Socket sock = Sockets.CreateTCPSocket("www.myserver.com", 2345);
    client = new ClientInfo(sock, false); // Don't start receiving yet
    client.OnReadBytes += new ConnectionReadBytes(ReadData);
    client.BeginReceive();
  }

  void ReadData(ClientInfo ci, byte[] data, int len){
    Console.WriteLine("Received "+len+" bytes: "+
       System.Text.Encoding.UTF8.GetString(data, 0, len));
  }
}

Et voilà, you can receive data! (Assuming you have a server that sends you some, of course.) To send text data, use client.Send("text"). ClientInfo exposes the socket it is using, so to send binary data, use client.Socket.Send().

Messaged Communication

Receiving data with OnReadBytes doesn't guarantee your messages arrive in one piece, though. ClientInfo provides two ways to keep messages together, one suitable for simplistic text messaging and one suitable for arbitrary message-based communication.

Text Messages

Often, being able to pass text with a suitable end marker, often a new line, is all we need in an application. Similar things can then be done with data received like this as with command strings. To do this, assign a handler to the OnRead event:

C#
class TextClient{
  ClientInfo client;
  void Start(){
    Socket sock = Sockets.CreateTCPSocket("www.myserver.com", 2345);
    client = new ClientInfo(sock, false); // Don't start receiving yet
    client.OnRead += new ConnectionRead(ReadData);
    client.Delimiter = '\n';  // this is the default, shown for illustration
    client.BeginReceive();
  }

  void ReadData(ClientInfo ci, String text){
    Console.WriteLine("Received text message: "+text);
  }
}

Now, any data received will be interpreted as text (UTF8 text, to be precise), and ReadData() will be called whenever a newline is found. Note that if the data is not text (and is not valid UTF8), invalid characters will be replaced with '?' – so don't use this method unless you know you are always handling text.

Binary Messages

For more advanced applications, you may want to pass a byte stream (which can represent anything), but still keep the message together. This is typically done by sending the length first, and ClientInfo does the same thing. To use this protocol, you need to put the client into a messaged mode and use OnReadMessage():

C#
class MessageClient{
  ClientInfo client;
  void Start(){
    Socket sock = Sockets.CreateTCPSocket("www.myserver.com", 2345);
    client = new ClientInfo(sock, false); // Don't start receiving yet
    client.MessageMode = MessageMode.Length;
    client.OnReadMessage += new ConnectionRead(ReadData);
    client.BeginReceive();
  }

  void ReadData(ClientInfo ci, uint code, byte[] bytes, int len){
    Console.WriteLine("Received "+len+" bytes: "+
       System.Text.Encoding.UTF8.GetString(bytes, 0, len));
  }
}

In the current version, a 4-byte length parameter is sent before the data (big-endian); for example, a message containing the word "Bob" would have the byte form 00 00 00 03 42(B) 6F(o) 62(b). Obviously, the other end of the connection needs to be using the same protocol! You can send messages, in the same format, using client.SendMessage(code, bytes).

By now, you're probably wondering what that uint code is all about. As well as MessageType.Length, there is also a MessageType.CodeAndLength which lets you specify a 4-byte code to be sent with each message. (It is sent before the length.) The event handler in this type of an application will typically be something like:

C#
void ReadData(ClientInfo ci, uint code, byte[] bytes, int len){
  switch(code){
    case 0:
      Console.WriteLine("A message: "+
        System.Text.Encoding.UTF8.GetString(bytes, 0, len));
      break;
    case 1:
      Console.WriteLine("An error: "+
        System.Text.Encoding.UTF8.GetString(bytes, 0, len));
      break;
   // ... etc
  }
}

If I were to send the message "Bob" with the tag 0x12345678, its byte stream would be 12 34 56 78 00 00 00 03 42 6F 62.

Being a Server

As well as receiving and sending information (just like a client), a server has to keep track of who is connected to it. It is also useful to be able to broadcast messages, that is send them to every client currently connected (for example, a message indicating the server is about to be taken down for maintenance).

Below is a simple server class which simply 'bounces' messages back where they came from, unless they start with '!' in which case it broadcasts them.

C#
class SimpleServer{
  Server server;
  ClientInfo client;
  void Start(){
    server = new Server(2345, new ClientEvent(ClientConnect));
  }
  
  bool ClientConnect(Server serv, ClientInfo new_client){
    new_client.Delimiter = '\n';
    new_client.OnRead += new ConnectionRead(ReadData);
    return true; // allow this connection
  }

  void ReadData(ClientInfo ci, String text){
    Console.WriteLine("Received from "+ci.ID+": "+text);
    if(text[0] == '!')
     server.Broadcast(Encoding.UTF8.GetBytes(text));
    else ci.Send(text);
  }
}

As you can see, the Connect handler is passed a ClientInfo, which you can treat in the same way as a client (assign the appropriate OnReadXxx handler and MessageType). You can also reject a connection by returning false from Connect. In addition to Broadcast(), there is a BroadcastMessage() if you are using messaged communication.

ClientInfos are assigned an ID when they are created, controlled by the static NextID property. They will be unique within a running application unless you reset NextID at any time after creating one. A server application will often want to keep track of a client's transactions. You can do this in two ways:

  1. There is a Data property of ClientInfo, to which you can assign any data you like. The data is maintained until the ClientInfo is disposed of (guaranteed to be after the connection dies).
  2. You can use the value of client.ID as the key in a Hashtable or similar. If you do that, you should supply a Disconnect handler:
    C#
    void ConnectionClosed(ClientInfo ci){
      myHashtable.Remove(ci.ID);
    }

    ... to make sure the data is cleaned up.

Encryption

This library includes the ability to protect a socket, using the encryption algorithm I wrote about here. This can be turned on by passing a value for encryptionType in the ClientInfo constructor or setting the EncryptionType property before calling BeginReceive, and setting the DefaultEncryptionType property on the server:

C#
public void EncryptionExamples() {
        // Client, Method 1: call the full constructor
        ClientInfo encrypted1 = new ClientInfo(socket1, null, myReadBytesHandler,
                ClientDirection.Both, true, EncryptionType.ServerKey);
        // Client, Method 2: delay receiving and set EncryptionType
        ClientInfo encrypted2 = new ClientInfo(socket2, false);
        encrypted2.EncryptionType = EncryptionType.ServerRSAClientKey;
        encrypted2.OnReadBytes = myReadBytesHandler;
        encrypted2.BeginReceive();
        // Server: set DefaultEncryptionType
        server = new Server(2345, new ClientConnect(ClientConnect));
        server.DefaultEncryptionType = EncryptionType.ServerRSAClientKey;
}

There are three encryption modes supported. The first, None, performs no encryption, and is the default. EncryptionType.ServerKey means that the server sends a symmetric key for the connection when the client first connects; because the key is sent unencrypted, this is not very secure, but it does mean that the communication is not in plain text. The most secure method is ServerRSAClientKey: the server will send an RSA public key upon connection, and the client will generate a symmetric key and encrypt it with the RSA key before sending it to the server. This means the key is never visible to a third party and the connection is quite secure; to access the message you would need to break the encryption algorithm.

If you choose to use encrypted sockets, very little is different in your application code. However, in your server, you should respond to the ClientReady event, not ClientConnect, in most cases. ClientReady is called when key exchange is complete and a client is ready to send and receive data; attempting to send data to a client before this event will result in an exception. Similarly, if you want to send data through an encrypted client socket, you should respond to the OnReady event or check the EncryptionReady property before sending data.

History

  • December 2005: Initial version posted
  • April 2008: Updated with version 1.4, supporting encryption, multi-character delimiters and sending byte arrays
  • November 2011: Updated with version 1.6, including the ability to synchronise events to a UI control

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
United Kingdom United Kingdom
I'm a recent graduate (MSci) from the University of Cambridge, no longer studying Geology. Programming is a hobby so I get to write all the cool things and not all the boring things Smile | :) . However I now have a job in which I have to do a bit of work with a computer too.

Comments and Discussions

 
QuestionIssue with BroadcastMessage when a client disconnects Pin
Member 147406998-Nov-20 6:17
Member 147406998-Nov-20 6:17 
Questioncool; adoptable to WPF Pin
maxoptimus14-May-20 5:12
maxoptimus14-May-20 5:12 
QuestionVery useful intro. Could you help me with a specific need? Pin
Member 429190623-Nov-17 2:02
Member 429190623-Nov-17 2:02 
QuestionClient side Send("text") is not received by server Pin
Istvan D.25-Jun-17 8:23
Istvan D.25-Jun-17 8:23 
AnswerRe: Client side Send("text") is not received by server Pin
Istvan D.28-Jun-17 3:03
Istvan D.28-Jun-17 3:03 
QuestionArithmetic overflow. Pin
bchambers23328-Jul-16 21:07
bchambers23328-Jul-16 21:07 
Hi

i am trying to use this code in a very simple scenario. i want a server to notify multiple clients when an a particular event occurs and at this time send an object to the client.

i have created a server and client but i cannot get the client to connect without throwing an arithmetic overflow error in the bytebuilder Read method.

my server code is as follows

C#
static void Main(string[] args)
        {

 
            _socketServer = new Server(7000);
            _socketServer.DefaultEncryptionType = EncryptionType.ServerRSAClientKey;
            _socketServer.ClientReady += new ClientEvent(connect);
            _socketServer.Connect += new ClientEvent(_socketServer_Connect);





            Console.WriteLine("Started!");
            Console.WriteLine("----------------------");
}

private static bool _socketServer_Connect(Server serv, ClientInfo new_client)
        {
            new_client.MessageType = MessageType.Length;
            return true;
        }

        static bool connect(Server serv, ClientInfo new_client)
        {
            new_client.MessageType = MessageType.Length;
            new_client.SendMessage(0x01020304, new byte[] { 0, 1, 2, 3, 4 }, 0);
            new_client.SendMessage(0x11111111, new byte[] { 0, 0, 4, 23 }, ParameterType.Int);
            new_client.OnReadMessage += new ConnectionReadMessage(ReadMessage);
            return true;
        }

        static void ReadMessage(ClientInfo ci, uint code, byte[] buf, int len)
        {
            Console.WriteLine("Message, code " + code.ToString("X8") + ", content:");
            byte[] ba = new byte[len];
            Array.Copy(buf, ba, len);
            Console.WriteLine("  " + ByteBuilder.FormatParameter(new Parameter(ba, ParameterType.Byte)));
            //ci.SendMessage(code, buf, (byte)0, len);
        }


my client code is

C#
class ClientTest
{
    private static ClientInfo client;

    static void Main(string[] args)
    {
        Socket sock = Sockets.CreateTCPSocket("localhost", 7000);
        client = new ClientInfo(sock, false); // Don't start receiving yet
        client.MessageType = MessageType.Length;
        client.OnRead += new ConnectionRead(ClientOnOnRead);
        client.OnReadMessage += new ConnectionReadMessage(ReadData);
        client.BeginReceive();


        string s;
        while ((s = Console.ReadLine()) != "exit")
        {
            //client.SendMessage(0xABCDEF01, System.Text.Encoding.UTF8.GetBytes(s), 0);
        }
        client.Close();
    }

    private static void ClientOnOnRead(ClientInfo ci, string text)
    {
    }

    static void ReadData(ClientInfo ci, uint code, byte[] bytes, int len)
    {
        Console.WriteLine("Received " + len + " bytes: " +
           System.Text.Encoding.UTF8.GetString(bytes, 0, len));
    }

    static void ReadMessage(ClientInfo ci, uint code, byte[] buf, int len)
    {
        Console.WriteLine("Message, code " + code.ToString("X8") + ", content:");
        byte[] ba = new byte[len];
        Array.Copy(buf, ba, len);
        Console.WriteLine("  " + ByteBuilder.FormatParameter(new Parameter(ba, ParameterType.Byte)));
    }
}



i have not even got to the point of sending the object but i plan to serialize it to a byte array and then send.

Can you help?
AnswerRe: Arithmetic overflow. Pin
s.armin1-Apr-21 15:29
s.armin1-Apr-21 15:29 
Questionwhere do I get redcorona dll ?? Pin
__Master_30-Oct-15 6:18
__Master_30-Oct-15 6:18 
AnswerRe: where do I get redcorona dll ?? Pin
__Master_30-Oct-15 8:05
__Master_30-Oct-15 8:05 
GeneralRe: where do I get redcorona dll ?? Pin
Member 111792899-Dec-15 12:40
Member 111792899-Dec-15 12:40 
GeneralRe: where do I get redcorona dll ?? Pin
BobJanova23-Jan-16 1:36
BobJanova23-Jan-16 1:36 
Question(problem) packet length goes crazy after some intense messaging exchange Pin
ernest_hemingway0772-Jul-14 23:53
professionalernest_hemingway0772-Jul-14 23:53 
Generalimplimentation Pin
Muhammad Zohaib Anjum30-May-14 4:05
Muhammad Zohaib Anjum30-May-14 4:05 
QuestionConsole execution Pin
darkopop776-Apr-14 10:28
darkopop776-Apr-14 10:28 
QuestionSimple example programs? Pin
Jeronymite10-Jan-14 16:50
Jeronymite10-Jan-14 16:50 
AnswerRe: Simple example programs? Pin
BobJanova11-Jan-14 15:16
BobJanova11-Jan-14 15:16 
QuestionVery new to this.... Pin
ewasta18-Sep-13 11:48
ewasta18-Sep-13 11:48 
AnswerRe: Very new to this.... Pin
BobJanova19-Sep-13 7:00
BobJanova19-Sep-13 7:00 
QuestionLove the library so far, but I cannot get the test classes working. Pin
somedudecalledchris23-Jul-13 5:05
somedudecalledchris23-Jul-13 5:05 
AnswerRe: Love the library so far, but I cannot get the test classes working. Pin
BobJanova23-Jul-13 7:12
BobJanova23-Jul-13 7:12 
GeneralRe: Love the library so far, but I cannot get the test classes working. Pin
somedudecalledchris23-Jul-13 21:20
somedudecalledchris23-Jul-13 21:20 
QuestionHow do I know that receiving long msg interrupted? Pin
Tamas2430-Mar-13 4:23
Tamas2430-Mar-13 4:23 
AnswerRe: How do I know that receiving long msg interrupted? Pin
BobJanova30-Mar-13 10:18
BobJanova30-Mar-13 10:18 
QuestionI have problems running this Pin
serpiccio22-Jan-13 1:39
serpiccio22-Jan-13 1:39 
AnswerRe: I have problems running this Pin
Tamas2427-Mar-13 6:48
Tamas2427-Mar-13 6:48 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.