Pull to refresh

Enabling Cross-Process Dialogue with C# Sockets

Level of difficultyMedium
Reading time4 min
Views266

Introduction

In the ever-evolving landscape of software development, communication between processes has been a fundamental requirement since the inception of computer networking. As technology advanced, the need for efficient and reliable inter-process communication (IPC) mechanisms became increasingly crucial. One such mechanism that has stood the test of time is the socket.
Sockets trace their origins back to the early days of computer networking in the 1970s. The concept was first introduced in the Unix operating system by researchers at the University of California, Berkeley, as part of their work on the ARPANET project, which eventually evolved into the modern internet. Sockets were designed to provide a standardized interface for network communication, allowing processes to exchange data across different machines and networks.
Initially, sockets were primarily used for network programming tasks, such as building client-server applications and facilitating communication between distributed systems. However, their versatility soon became apparent, and sockets found their way into various domains, including inter-process communication (IPC) within a single machine.
Over the years, sockets have undergone numerous enhancements and standardizations, with the Berkeley Software Distribution (BSD) sockets becoming the de facto standard for network programming. This standard was later adopted by other operating systems, including Microsoft Windows, ensuring cross-platform compatibility and enabling widespread adoption.
In the world of software development, communication between processes is a common requirement, whether it’s a client-server architecture, distributed systems, or inter-process communication (IPC). Sockets provide a powerful mechanism for processes to exchange data efficiently and reliably. This article explores a simple implementation of a TCP client-server communication using sockets in C#, showcasing the versatility and robustness of this time-tested technology.

The Codebase

1. Config.cs:

This class serves as a base for both the server and the client. It defines the IP address and port number to be used for communication and creates a shared Socket object.

using System.Net.Sockets;
using System.Net;

namespace amSocket;

public class Config
{
    static string _ip = "127.0.0.1";
    static int _port = 8088;

    protected static IPEndPoint _endPoint = new(IPAddress.Parse(_ip), _port);
    protected static Socket _socket = new(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
}

2. Message.cs:

This class represents the data structure that is sent between the client and the server. It contains an ID, a source string, and a message string. The Pack method converts the Message object into a string representation, separating the fields with the | character.

namespace amSocket;

public class Message
{
    public int id = 0;
    public string src = "";
    public string msg = "";
  
    public Message(string packed)
    {
        var ar = packed.Split('|');
        id = int.Parse(ar[0]);
        src = ar[1];
        msg = ar[2];
    }
  
    public string Pack() { return $"{id}|{src}|{msg}"; }
}

3. Client.cs:

This class handles the client-side logic for sending data to the server. The Send method connects to the specified IP and port, converts the provided Message object to a string, and sends it to the server.

using System.Text;

namespace amSocket;

public class Client : Config
{
    public static void Send(Message b)
    {
        _socket.Connect(_endPoint);

        string message = b.Pack();
        byte[] messageBytes = Encoding.UTF8.GetBytes(message);
        _socket.Send(messageBytes);
        _socket.Close();
    }
}

4. Server.cs:

This class handles the server-side logic for listening for incoming connections. The StartListen method binds the server socket to the specified IP and port, starts listening for connections, and accepts incoming connections asynchronously. When a connection is accepted, it receives data from the client and passes it to the provided callback as a Message object.

using System.Net.Sockets;
using System.Text;

namespace amSocket;

public class Server : Config
{
    static bool _needToClose = false;
    public static void StartListen(Action<Message> OnSocketMessage)
    {
        Task.Run(async () =>
        {
            try
            {
                _socket.Bind(_endPoint);
                _socket.Listen(5);

                while (true)
                {
                    var listener = _socket.Accept();
                    var buffer = new byte[1_024];
                    var received = await listener.ReceiveAsync(buffer, SocketFlags.None);
                    var response = Encoding.UTF8.GetString(buffer, 0, received);

                    Message b = new(response);
                    OnSocketMessage(b);

                    if (_needToClose) break;
                }
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex.ToString());
            }
        });
    }
    public static void StopListen()
    {
        _needToClose = true;
        _socket.Close();
    }
}

Usage

To use this code, follow these steps:

1. Start the Server:

Create an instance of the Server class and call the StartListen method, passing a callback function to handle incoming messages:

Server.StartListen(OnSocketMessage);

void OnSocketMessage(Message b)
{
    Console.WriteLine($"Received message: {b.msg} from {b.src} with ID {b.id}");
}

2. Send Data from the Client:

Create an instance of the Client class and call the Send method with a Message object containing the data you want to send:

Message b = new Message { id = 1, src = "ClientApp", msg = "Hello, Server!" };
Client.Send(b);

The server will receive the message and print it to the console:

Received message: Hello, Server! from ClientApp with ID 1

Considerations

This implementation is a simple example and does not include error handling, concurrency management, or other advanced features that would be necessary for a production-ready application. Here are some considerations:

  • Error Handling: The code does not handle exceptions or errors that may occur during communication, such as network issues or invalid data.

  • Concurrency: The server implementation uses a single thread to handle incoming connections. In a real-world scenario, you would need to handle multiple concurrent connections using techniques like asynchronous programming or threading.

  • Security: The implementation does not include any security measures, such as encryption or authentication. In a production environment, you would need to ensure secure communication channels.

  • Scalability: The current implementation is limited to a single server instance. For large-scale applications, you may need to consider load balancing, clustering, or other scalability techniques.

Despite its simplicity, this codebase serves as a good starting point for understanding the fundamentals of socket-based communication in C#. With proper enhancements and modifications, it can be adapted to meet the requirements of more complex and robust applications.

Tags:
Hubs:
If this publication inspired you and you want to support the author, do not hesitate to click on the button
+3
Comments0

Articles