DEV Community

Cover image for Deep Dive into PandApache3: Understanding Connection Management and Response Generation
Mary πŸ‡ͺπŸ‡Ί πŸ‡·πŸ‡΄ πŸ‡«πŸ‡·
Mary πŸ‡ͺπŸ‡Ί πŸ‡·πŸ‡΄ πŸ‡«πŸ‡·

Posted on • Edited on

Deep Dive into PandApache3: Understanding Connection Management and Response Generation

Welcome to the world of PandApache3, your web server of choice. In this article, we will explore the internal workings of this server, detailing how it handles connections, processes HTTP requests, and generates responses. Whether you are a developer or simply curious, you'll discover the key steps that make all this possible.

In the previous article Deep Dive into PandApache3: Launch Code, we saw how the PandApache3 service starts, from initializing the logger to starting the TcpListener. Now, it's time to dive into the heart of the system to understand what happens when a connection is received. Ready to discover the inner workings of PandApache3? Let's go!


How PandApache3 Manages Incoming Connections

Technology Png vectors by Lovepik.com

After the StartServerAsync method, another method is called: RunServerAsync.

private static async Task RunServerAsync(ConnectionManager connectionManager)
{
    TcpListener listener = connectionManager.Listener;
    while (true)
    {
        if (listener.Pending())
        {
            ISocketWrapper client = new SocketWrapper(listener.AcceptSocket());
            await connectionManager.AcceptConnectionsAsync(client);
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

This method is brief and here's what it does: Remember that our ConnectionManager has a TcpListener. We check if there is a connection pending on this listener. If so, we accept the connection for the ConnectionManager to handle. Since we need to constantly check for new connections, the method runs continuously in an infinite loop. Once a connection is made with our server, we have what we call a client or a socket.

Between Us

What is the ISocketWrapper for? Encapsulating the socket in ISocketWrapper abstracts the raw socket implementation details, making it easier to manage connections within the ConnectionManager. This also allows for better testability and maintainability of the code.

Our connection with the client will then be managed in the AcceptConnectionsAsync function.

public async Task AcceptConnectionsAsync(ISocketWrapper client)
{
    if (_clients.Count < ServerConfiguration.Instance.MaxAllowedConnections)
    {
        Guid clientId = Guid.NewGuid();
        _clients.TryAdd(clientId, client);

        Logger.LogInfo("Client connected");

        Task.Run(() => HandleClientAsync(client, clientId));
    }
    else if (_clientsRejected.Count < ServerConfiguration.Instance.MaxRejectedConnections)
    {
        Guid clientId = Guid.NewGuid();
        _clientsRejected.TryAdd(clientId, client);

        Logger.LogWarning("Too many connections - rejecting with HTTP 500");

        Task.Run(() => HandleClientRejectAsync(client, clientId));
    }
    else
    {
        Logger.LogError("Too many connections");
        client.Dispose();

        return;
    }
}
Enter fullscreen mode Exit fullscreen mode

In a normal situation, the client's connection will be handled in a new task (a new thread) to respond to their request, allowing the current thread to accept a new connection, and so on.

In case of server overload, the client's connection, although accepted, will immediately return an error message (also in a new thread). Their request will receive a response, but not the expected one. In an extreme overload situation, it is even possible that the server simply closes the connection without returning anything.

Between Us

How do we decide if the server is overloaded and whether to respond correctly to the client? Imagine each connection to our server, and thus each thread created to perform its task, takes 2 MB of memory, and my server has 512 MB (a number taken for demonstration purposes). I know then that my server cannot support more than 256 connections because the 257th will no longer have resources to function. This can endanger my entire service.


Request Analysis

Faq Icon Png vectors by Lovepik.com

Now that we have accepted the request and decided to process it, it's time to recall what was explained in the previous article. Each connection manager has a pipeline composed of several middlewares that process each request. If you remember, one of the parameters used by each middleware was an HTTP context (HttpContext). Here is the IMiddleware interface to refresh your memory:

public interface IMiddleware
{
    Task InvokeAsync(HttpContext context);
}
Enter fullscreen mode Exit fullscreen mode

And the list of middlewares present in PandApache3:

Middleware description

HttpContext is a fairly simple class composed of a Request object and an HttpResponse object:

public class HttpContext
{
    public Request Request { get; set; }
    public HttpResponse Response { get; set; }

    public HttpContext(Request request, HttpResponse response)
    {
        Request = request;
        Response = response;
    }
}
Enter fullscreen mode Exit fullscreen mode

Our new thread, which handles the request, will start by executing the HandleClientAsync function:

private async Task HandleClientAsync(ISocketWrapper client, Guid clientId)
{
    Request request = await ConnectionUtils.ParseRequestAsync(client);
    if (request == null)
    {
        return;
    }
    HttpContext context = new HttpContext(request, null);

    await _pipeline(context);

    await ConnectionUtils.SendResponseAsync(client, context.Response);
    _clients.TryRemove(clientId, out client);
    client.Dispose();

    Logger.LogInfo("Client closed");
}
Enter fullscreen mode Exit fullscreen mode

First, the HTTP request received by the server is parsed to obtain a Request object. Parsing involves extracting the first line, the path, the HTTP verb, headers, parameters, the body... in short, all the elements that make up an HTTP request.

Since the response is not yet known, it is currently null in the HttpContext. Then, the first middleware of the pipeline is executed. In our case, there is really only one middleware that does anything: the RoutingMiddleware. The first LoggingMiddleware simply logs the incoming request, and the third TerminalMiddleware indicates that we have reached the end of the pipeline.


The Core of Request Processing

Tool Png vectors by Lovepik.com

Here is the RoutingMiddleware function executed for each request:

public async Task InvokeAsync(HttpContext context)
{
    Logger.LogDebug("Router Middleware");

    if (context.Request.Verb.ToUpper().Equals("GET"))
    {
        Request request = context.Request;

        string mainDirectory = ServerConfiguration.Instance.RootDirectory;
        string filePath = Path.Combine(mainDirectory, Utils.GetFilePath(request.Path));

        if (_FileManager.Exists(filePath))
        {
            string fileExtension = Path.GetExtension(filePath).Substring(1).ToLowerInvariant();
            string mimeType = fileExtension switch
            {
                // Images
                "jpg" or "jpeg" => "image/jpeg",
                "png" => "image/png",
                "gif" => "image/gif",
                "svg" => "image/svg+xml",
                "bmp" => "image/bmp",
                "webp" => "image/webp",
                "ico" => "image/x-icon",

                // Text documents
                "txt" => "text/plain",
                "html" or "htm" => "text/html",
                "css" => "text/css",
                "js" => "application/javascript",
                "json" => "application/json",
                "xml" => "text/xml",

                // Default case if the extension is not recognized
                _ => null
            };
        }
        if (mimeType != null)
        {
            byte[] data = await File.ReadAllBytesAsync(filePath);
            HttpResponse httpResponse = new HttpResponse(200)
            {
                Body = new MemoryStream(data)
            };
            httpResponse.AddHeader("Content-Type", mimeType);
            httpResponse.AddHeader("Content-Length", httpResponse.Body.Length.ToString());
            context.Response = httpResponse;

        }
        else
        {
            context.Response = new HttpResponse(404);
        }
    }
    else
    {
        context.Response = new HttpResponse(404);
    }

    await _next(context);
}
Enter fullscreen mode Exit fullscreen mode

Let's start with the first condition. Currently, we want our web server to only return static resources like HTML files. The only HTTP verb required for this is GET. So, any request using another HTTP verb should return a 404 error.
However, if the request is a GET, it means the client wants to obtain a resource on our server. This resource is the path indicated in our request.

For example, if I send a GET request to the URL http://pandapache.com/index.html, my path is index.html, and that is the resource my web server must find and return.

But where exactly should we look for index.html? PandApache3, like most web servers, has what is called a root directory. On your computer your root directory is C:/ on Windows and / on Linux, your web server defines this directory relative to its settings. By default, the root directory of PandApache3 is C:/PandApache3/www/ on Windows and /etc/PandApache3/www on Linux.

We have the root directory and the requested resource. If it exists, we can return it:

string mainDirectory = ServerConfiguration.Instance.RootDirectory;
string filePath = Path.Combine(mainDirectory, Utils.GetFilePath(request.Path));
if (_FileManager.Exists(filePath))
{
  ...
}
Enter fullscreen mode Exit fullscreen mode

Between Us

PandApache is written in .NET Core. Even though it's C#, the code can run on both Windows and Linux without any modification. At startup, the operating system is detected by PandApache3, and the default settings for paths (configuration, logs, root directory) adapt to the platform.

Building a Proper Response

Building Png vectors by Lovepik.com

Let's now see how a proper response can be generated. In addition to knowing if our resource exists, we also need to determine its MIME type. It's simple: the MIME type is determined by the file extension. Here, we will focus only on the MIME types of the most common files for static web resources: HTML, CSS, JavaScript, and images.

Thus, we need to use the file extension to identify the MIME type:

string fileExtension = Path.GetExtension(filePath).Substring(1).ToLowerInvariant();
string mimeType = fileExtension switch
{
    // Images
    "jpg" or "jpeg" => "image/jpeg",
    "png" => "image/png",
    "gif" => "image/gif",
    "svg" => "image/svg+xml",
    "bmp" => "image/bmp",
    "webp" => "image/webp",
    "ico" => "image/x-icon",

    // Text documents
    "txt" => "text/plain",
    "html" or "htm" => "text/html",
    "css" => "text/css",
    "js" => "application/javascript",
    "json" => "application/json",
    "xml" => "text/xml",

    // Default case if the extension is not recognized
    _ => null
};
Enter fullscreen mode Exit fullscreen mode

Once the MIME type is known, we can return the appropriate response:

if (mimeType != null)
{
    byte[] data = await File.ReadAllBytesAsync(filePath);
    HttpResponse httpResponse = new HttpResponse(200)
    {
        Body = new MemoryStream(data)
    };
    httpResponse.AddHeader("Content-Type", mimeType);
    httpResponse.AddHeader("Content-Length", httpResponse.Body.Length.ToString());
    context.Response = httpResponse;

}
else
{
    context.Response = new HttpResponse(404);
}
Enter fullscreen mode Exit fullscreen mode

Here are the necessary attributes for a correct HTTP response:

  • A status code: 200 to indicate that the request and response are correct.
  • The body: which contains the content of the file.
  • The Content-Type: which is the MIME type.
  • The Content-Length: the size of the response, which is simply the size of the body.

There you go, our response is ready!

If the MIME type is not recognized, or if the file does not exist, we return a 404 Not Found response.

Finally, we call the next middleware in the pipeline (although in this case, it doesn't do anything significant).

Between us

Why read files in binary mode? Text mode would work for several types of files, those that you can open with a text editor like HTML, CSS... But have you ever tried to open an image with Notepad? It’s impossible. So, since some resources need to be sent in binary, we might as well do it for all of them!

Still between us

Here, the request path corresponds to a physical file on the disk, but the path could just correspond to what we call an endpoint. In that case, the web service doesn't return a file. Instead, the service can perform an action or construct a dynamic response.
On PandApache3, you can send a GET request to the /echo/ endpoint with a parameter (http://pandapache3/echo/hello). The response will then be "hello", perfect for ensuring your service is running correctly!


Sending the Response

The last step of our journey involves sending the generated response back to the client. This is done in the HandleClientAsync method:

await ConnectionUtils.SendResponseAsync(client, context.Response);
_clients.TryRemove(clientId, out client);
client.Dispose();

Logger.LogInfo("Client closed");
Enter fullscreen mode Exit fullscreen mode

The SendResponseAsync method sends the response to the client via the socket:

byte[] msg = Encoding.UTF8.GetBytes(response.ToString());
await client.SendAsync(msg, SocketFlags.None);

if (response.Body != null)
{
    response.Body.Position = 0; // Ensure the stream is at the beginning
    byte[] buffer = new byte[1024];
    int bytesRead;
    while ((bytesRead = response.Body.Read(buffer, 0, buffer.Length)) > 0)
    {
        await client.SendAsync(new ArraySegment<byte>(buffer, 0, bytesRead), SocketFlags.None);
    }
}
Enter fullscreen mode Exit fullscreen mode

We send the HTTP response to the client in two parts: the first send is for the header, and then the body is sent in packets of 1024 bytes.

Once the response is sent, we remove the client from our connection list and close the connection.

Between us

This method of handling requests and responses is simple, but lacks performance optimization. For example, if your index.html page has 10 images, the server will first receive a request for the HTML page, followed by 10 additional requests for the images. This can be very slow for heavy pages. To solve this problem, we can use multiplexing to send multiple requests and responses over a single TCP connection. This feature is not currently available on PandApache3.


And that's it! We've walked through how PandApache3 manages incoming connections, processes HTTP requests, and generates responses.

In the next article, we will explore more advanced features, including how to handle dynamic content and more complex routing.

Stay tuned!


Thank you so much for exploring the inner workings of PandApache3 with me! Your thoughts and support are crucial in advancing this project. πŸš€
Feel free to share your ideas and impressions in the comments below. I look forward to hearing from you!

Follow my adventures on Twitter @pykpyky to stay updated on all the news.

You can also explore the full project on GitHub and join me for live coding sessions on Twitch for exciting and interactive sessions. See you soon behind the screen!

Top comments (0)