Creating a multithreaded server in C #

Foreword

This article is intended for beginners like me. All information in this article is based on my experience of creating one single web server, which was created within the framework of a training project for the 3rd year in the specialty 09.02.07 SPO.

Web server

Before writing our own web server, we need to understand what it is and how it works.

Web server is a server that relies on the TCP / IP and HTTP protocols to interact with the client. The main task of such servers is to accept incoming requests based on the HTTP protocol.

Web server means two things:

  1. Software

  2. Hardware

In this article, we will be looking at a web server like software.

The web server works thanks to a client-server architecture

Figure 1 - Block diagram of the client-server architecture
Figure 1 – Block diagram of the client-server architecture

To make it clearer, let’s break down the work of the architecture point by point:

  1. Formation of a request by the client

  2. Sending a request to the server

  3. Receiving a request on the server

  4. Request processing and response generation

  5. Sending a response to the client

But how does the client communicate with the server? As I said above, the web server uses two protocols:

  1. TCP / IP

  2. HTTP

TCP / IP (Transmission Control Protocol / Internet Protocol) – the two main protocols on which the entire modern Internet is built. TCP is for the transfer of data between network participants, and IP is the Internet protocol that is used to refer to network participants.

HTTP (Hyper Text Transfer Protocol) – protocol of the application layer of data transmission. Its main task is to transfer files with the HTML extension, but it can also transfer other files.

The computer that is the server will listen for incoming connections via the ip: port pair, for example: 127.0.0.1:80, where 127.0.0.1 is the ip-address; 80 – port, used for the HTTP protocol, you can also use port 81.

Implementing a web server in C #

We will write our web server in C # .Net Core. It is advisable that you know the base of the language.

So we went from words to practice, but before that, we need to decide with what we will write our web server? Several classes are presented to our attention that can help us with this:

  1. Socket

  2. TcpListener

Socket – represents the implementation Berkeley sockets in C #, it was empirically found that using this option brings more efficient results.

TcpListener – listens for incoming TCP connections on the ip: port pair. Maybe from my curvature, more than sure of it, or from something else, it turned out so that TcpListener not quite suitable for this task, since when sending packets to the client, some files simply did not arrive and each time the number of files was different.

In this article, we will only consider the class-based option. Socketanyone interested in knowing how to implement a web server on TcpListener, then here is a link to another author’s article.

First, we need to create 2 classes (they should be located in two new files):

  1. Server – this class will designate our server and it will accept incoming connections

  2. Client – this class will designate our client, in this class all request processing will take place

Let’s start filling the class Server… First, we need to add the libraries that we need to our class:

using System;
using System.Collections.Generic;
using System.Threading;
using System.Net;
using System.Net.Sockets;

Then, in the class, we must create variables that we will operate on:

public EndPoint Ip; // представляет ip-адрес
int Listen; // представляет наш port
Socket Listener; // представляет объект, который ведет прослушивание
public bool Active; // представляет состояние сервера, работает он(true) или нет(false)

Now let’s create a constructor for our class. Because Socket works on ip: port, then our constructor will take ip as the first argument, and port as the second:

public Server(string ip, int port)
{
    this.Listen = port;
    this.Ip = new IPEndPoint(IPAddress.Parse(ip), Listen);
    Listener = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
}

We will set the ip as a string and convert it to the class IPEndPoint through the class IPAddress… The port is the simplest, just an ordinary number like int… I think the most incomprehensible for you now is the class constructor Socket:

  • AddressFamily – an enumeration that denotes which version of ip addresses we will work with. InterNetwork indicates that we are using IPv4.

  • SocketType – enumeration, indicates the type of socket and which connection will be established. In our case, we will work with the connection type Stream

  • ProtocolType – enumeration, indicates what type of connections we will accept. Tcp, means that we will work with the TCP protocol.

After the constructor, we should create two functions, the first will initialize the operation of our server, and the second will stop it, respectively:

public void Start()
{
    if (!Active)
    {
       // тело условия
    }
    else
        Console.WriteLine("Server was started");
}

A condition inside the function checks if the server is down. If it is off, then we can start our server. This is necessary so that there is no conflict on the socket. If we try to start a second socket with the same ip and port, then an error will come out.

After we fill in our condition, if the server is turned off then:

Listener.Bind(Ip);
Listener.Listen(16);
Active = true;

Function Bind class Socket means that the listener will work on a specific ip: port, which we pass to this function.

Function Listen starts listening, as an argument we pass a variable of type to it int, which means the possible number of clients in the queue to connect to the socket.

Now let’s get down to implementing multithreading. We will do it based on a class such as ThreadPool… No, of course it could have been easier:

new Task.Run(
	()=>{
		ClientThread(Listener.Accept());
	}
);

But this way not effective, since it will simply create new threads to process the incoming connection, thereby slowing down the server’s work, after all, after all, our processor’s threads are not unlimited. Therefore, we take and insert this piece of code into our condition (after Active = true):

while (Active)
{
    ThreadPool.QueueUserWorkItem(
            new WaitCallback(ClientThread),
            Listener.Accept()
            );
}
  • ThreadPool.QueueUserWorkItem(WaitCallback, object) – adds functions to the queue to be executed

  • WaitCallback(ClientThread) – accepts a function and returns a response about its execution

  • Listener.AcceptTcpClient() – the argument to be passed to the function

The function will loop through incoming connections. Listener.Accept() will temporarily stop the loop until a connection request comes.

Now let’s move on to our server stop function:

public void Stop()
{
    if (Active)
    {
        Listener.Close();
        Active = false;
    }
    else
        Console.WriteLine("Server was stopped");
}

In it we write a condition opposite to that which was in Start, that is, here we must check if the server is turned on.

Function Close class Socket we stop listening. Then we change the value of the variable Active on the false

I think by function Start you noticed that there was a function like ThreadClient, it’s time to create it too. She will be responsible for creating a new client that connects to our server:

public void ClientThread(object client)
{
    new Client((Socket)client);
}

Since the delegate WaitCallback requires the argument to be a simple type object, then the function, respectively, will also take the type object, which we will implicitly transform into a class Socket

It’s time for the class description Client… First, let’s include the libraries we need in the file:

using System;
using System.IO;
using System.Text;
using System.Net.Sockets;
using System.Text.RegularExpressions;

But before describing our class Client, let’s create a structure with which we will parse our HTTP headers:

struct HTTPHeaders
{
    public string Method;
    public string RealPath;
    public string File;
}

This structure will store the values ​​of our HTTP headers:

  • Method – stores the method with which the request is made

  • RealPath – stores the full path to the file on our server (example: C: Users Public Desktop Server www index.html)

  • File – stores an incomplete path to the file (example: www index.html)

Now let’s create the function itself that will parse the headers:

public static HTTPHeaders Parse(string headers) {}

It will return the structure itself, then the structure declaration will look like this:

HTTPHeaders head = HTTPHeaders.Parse(headers);

Now let’s describe the body of the function:

public static HTTPHeaders Parse(string headers)
{
    HTTPHeaders result = new HTTPHeaders();
    result.Method = Regex.Match(headers, @"Aw[a-zA-Z]+", RegexOptions.Multiline).Value;
    result.File = Regex.Match(headers, @"(?<=ws)([Wa-zA-Z0-9]+)(?=sHTTP)", RegexOptions.Multiline).Value;
    result.RealPath = $"{AppDomain.CurrentDomain.BaseDirectory}{result.File}";
    return result;
}

I will not explain how regular expressions work, so there is a link to the documentation at the end of the article.

When assigning a value to a variable RealPath at the structure object result, I wrote: AppDomain.CurrentDomain.BaseDirectory – this means that we take the path to our exe file, for example: C: Users Public Desktop Server, and then we substitute an incomplete path to our file:File, and then our path will look like this: C: Users Public Desktop Server + www index.html = C: Users Public Desktop Server www index.html. That is, the site files will be located relative to our server.

Now let’s write a function that will return the extensions of our file to us, let’s call it FileExtention:

public static string FileExtention(string file)
{
    return Regex.Match(file, @"(?<=[W])w+(?=[W]{0,}$)").Value;
}

Again, we do this with regular expressions.

This structure was made for convenience, because when we parse a large number of headers, it is better if they are stored in one place.

Let’s create in the class Client variables:

Socket client; // подключенный клиент
HTTPHeaders Headers; // распарсенные заголовки

The constructor of our class will have only 1 argument, which will take Socket:

public Client(Socket c)

After in the constructor, we must assign to our variable client our argument and start accepting data from the client:

client = c;
byte[] data = new byte[1024]; 
string request = ""; 
client.Receive(data); // считываем входящий запрос и записываем его в наш буфер data
request = Encoding.UTF8.GetString(data); // преобразуем принятые нами байты с помощью кодировки UTF8 в читабельный вид

The code above describes how the server accepts requests from the client:

  • data – an array that takes bytes

  • request – query as a string

  • client.Receive(data) – reads the incoming bytes and writes them to the array.

After we write the received data from the client into a byte array data, we must make it understandable, for this we will use the class Encoding, with which we convert bytes to characters:

Encoding.UTF8.GetString(data); 

Now it’s time to check and parse our headers.

The first condition checks if any request came at all? If not, then we disconnect the client and exit the function:

if (request == "")
{
    client.Close();
    return;
}

If we still have something, then it’s time to use the structure and parse the received message and display a message about the connection to the console:

Headers = HTTPHeaders.Parse(request);
Console.WriteLine($@"[{client.RemoteEndPoint}]
File: {Headers.File}
Date: {DateTime.Now}");

Next, we check our link for the presence of “..”, if this value exists, that is, greater than -1, then we display an error message:

if (Headers.RealPath.IndexOf("..") != -1)
{
    SendError(404);
    client.Close();
    return;
}

And finally, the last check in this function, if the file in the specified path Headers.RealPath exists, then we start working with this file, otherwise display an error:

if (File.Exists(Headers.RealPath))
		GetSheet(Headers);
else
		SendError(404);
client.Close();

Before describing the main function GetSheetthat will return a response to the user, we will create a couple of functions.

First function SendError, it will return an error code to the user:

public void SendError(int code)
{
    string html = $"<html><head><title></title></head><body><h1>Error {code}</h1></body></html>";
    string headers = $"HTTP/1.1 {code} OKnContent-type: text/htmlnContent-Length: {html.Length}nn{html}";
    byte[] data = Encoding.UTF8.GetBytes(headers);
    client.Send(data, data.Length, SocketFlags.None);
    client.Close();
}
  • html – represents the markup of our page

  • headers – presents headers

  • data – byte array

  • client.Send(data, data.Length, SocketFlags.None);– sends data to the client

  • client.Close(); – closes the current connection

Now let’s create a function that will return the content type, since this article presents a simple version of the server, we will restrict ourselves to the types: text and image. We output the content type so that the file we sent can be recognized, we write this value in a special header Content-Type(example: Content-Type: text/html):

string GetContentType(HTTPHeaders head)
{
    string result = "";
    string format = HTTPHeaders.FileExtention(Headers.File);
    switch (format)
    {
        //image
        case "gif":
        case "jpeg":
        case "pjpeg":
        case "png":
        case "tiff":
        case "webp":
            result = $"image/{format}";
            break;
        case "svg":
            result = $"image/svg+xml";
            break;
        case "ico":
            result = $"image/vnd.microsoft.icon";
            break;
        case "wbmp":
            result = $"image/vnd.map.wbmp";
            break;
        case "jpg":
            result = $"image/jpeg";
            break;
        // text
        case "css":
            result = $"text/css";
            break;
        case "html":
            result = $"text/{format}";
            break;
        case "javascript":
        case "js":
            result = $"text/javascript";
            break;
        case "php":
            result = $"text/html";
            break;
        case "htm":
            result = $"text/html";
            break;
        default:
            result = "application/unknown";
            break;
    }
    return result;
}

This function takes our structure HTTPHeaders… First, we will use a function that will return the file extension to us, and then we will begin to check it in a conditional construction switch… If not one of the options listed among case will not show up. then we will return: applacation/unknown – this means that the file was not recognized.

Now let’s describe our last function GetSheet and it will be possible to test our server:

public void GetSheet(HTTPHeaders head){}

This function takes our structure as an argument. HTTPHeaders… First, you should wrap the function in an error handling block. try catchas there might be any errors:

try
{
    // тело оператора try
}
catch (Exception ex)
{
    Console.WriteLine($"Func: GetSheet()    link: {head.RealPath}nException: {ex}/nMessage: {ex.Message}");
}

Now let’s describe the body of the operator try:

string content_type = GetContentType(head);    
FileStream fs = new FileStream(head.RealPath, FileMode.Open, FileAccess.Read, FileShare.Read);  
string headers = $"HTTP/1.1 200 OKnContent-type: {content_type}nContent-Length: {fs.Length}nn";  
// OUTPUT HEADERS    
byte[] data_headers = Encoding.UTF8.GetBytes(headers);   
client.Send(data_headers, data_headers.Length, SocketFlags.None); 

After we have translated our headers into a byte array, we will send them to the client using the method Send() class Socketwhich takes the following parameters:

  1. byte[] – byte array

  2. byte[].Length – the length of the transmitted array

  3. SocketFlags is an enumeration that represents the behavior of the socket when sending and receiving packets. Meaning None indicates that flags are not used

And at the very end of our operator, we transmit the content that the client requested. Since we did it with FileStream, then first we need to: read the data, write it to an array of bytes and send it over the network.

// OUTPUT CONTENT
while (fs.Position < fs.Length)
{
    byte[] data = new byte[1024];
    int length = fs.Read(data, 0, data.Length);
    client.Send(data, data.Length, SocketFlags.None);
}

This time we put SocketFlags.Partial… This means that in this case, part of the message is sent, since not all bytes of the file can fit into an array of size 1024. But it can also work with SocketFlags.None

Since we have a multithreaded server that runs on ThreadPool, first, in the file that contains the function Main we will include the library: System.Threading, and then we indicate the minimum number of threads that it can use:

ThreadPool.SetMinThreads(2, 2);

The first parameter indicates the minimum number of working threads, and the second – the minimum number of asynchronously working threads. The minimum value should always be specified as 2, because if you specify 1, the main thread will be blocked to process the request.

Now let’s set the maximum values ​​for our pool:

ThreadPool.SetMaxThreads(4, 4);

Then we just initialize our class. Server in a function and run it:

static void Main(string[] args)
{
		ThreadPool.SetMinThreads(2, 2);
    ThreadPool.SetMinThreads(4, 4);
    Server server = new Server("127.0.0.1", 80);
    server.Start();
}

Let’s create a simple html file in the folder where our exe is located (example path: ../ project / bin / Debug / netx.x / – where project is the name of your project) file:

<!DOCTYPE html>
<html>
<head>
</head>
<body>
<h1>Hello Server!</h1>
</body>
</html>

After that, we write in the address bar: http://127.0.0.1/index.html and check the result. We should display the inscription Hello Server !, and it should also display information about the current connection in the console.

Now you have an idea of ​​how to implement a simple, multithreaded server using network sockets in C #. For a better understanding of how different classes work, I recommend reading the documentation:

Thank you for paying attention to my article, I hope that if I was wrong somewhere, you will point it out to me in the comments and help me become better.

Link to the server on Github, this version of the server implements php support.

Link to the source of this article.

Similar Posts

Leave a Reply