Session

In the last article, we implemented the routing capabilities of the Web server and implemented basic support for the controller. We should have happily continued to add functionality to it, but soon discovered an embarrassing problem ———— we don’t have a Session yet. More specifically, HttpListenerContext we’ve been using only provides Request/Response, but has no Session attribute. This means that our server has no memory and can only treat each request as a new user.

It makes sense. The HttpListenerXXX family of classes in the base library provides a good starting point for creating Web applications, but implementing Sesssion is the responsibility of the Web framework, not the Web server. Some of you might ask, what’s the difference between a Web server and a Web framework? Well, there’s no exact definition of this, but generally speaking, Web servers are independent of programming languages and frameworks. Apache/Nginx has the ability to support multiple languages/frameworks. IIS can also run PHP with plug-ins, and Web servers are often more concerned with infrastructure issues, including site management, HTTP compression, certificates, performance, and throughput. The Web framework is generally bound to a specific language or platform, hoping to make full use of the features of the language itself to better support business logic, such as Express (Nodejs), Django (Python), ASP.NET MVC (.net), etc. Session, which is essential for back-end business (differentiating users is a basic requirement for most backend systems), is not absolutely necessary for Web servers and affects server throughput to a certain extent, so it is generally implemented at the level of the Web Framework.

Sessions require the server to have some mechanism to remember who is currently requesting them. Currently most Session implementations are based on cookies. At the implementation level, you need to consider how much content to put in cookies. Mainstream implementations put most of the content on the server side, and the client only records a key for authentication. This implementation is excellent for network traffic and security, but it takes up a lot of server space. Some implementations put part of the data on the client side to ease server stress and facilitate client processing, but this requires security and data loss concerns. We won’t discuss the pros and cons here, but for example purposes, we’ll go with the first option, which is to keep everything on the server side.

In addition, let me say one more thing: Sessions are a mechanism, and there is no requirement that they be in memory. Many students seem to misunderstand this, and they seem to think that as long as a Session is in use of memory. This is certainly not the case, and it is perfectly legal to store sessions using other storage mechanisms. This misunderstanding is probably due to the fact that most Session implementations use memory by default — because it’s the easiest way. However, many Web frameworks provide extension points such as Session Storage or Session Provider to store sessions somewhere else, such as a database or remote Redis/Memcached. If you want to implement distributed sessions across multiple servers, memory is definitely not a good choice. Our implementation here also uses memory to simplify matters, but it is important to remember that sessions do not have to be stored in memory.

code

The sample code of this article has been put on Github, and the code associated with each article is placed in a separate branch for readers’ reference. Therefore, to get the sample code for this article, use the following command:

git clone https://github.com/shuhari/web-server-succinctly-example.git
git checkout -b  04-session origin/04-session 
Copy the code

implementation

As we said at the beginning, HttpListenerContext does not give us a Session interface, so we must encapsulate it to provide the functionality required by the Web framework.

First declare the Session interface. Session can be used as a dictionary for most typical usage scenarios:

public interface ISession
{
    object this[string name] { get; set; }

    void Remove(string name);
}
Copy the code

Next, wrap HttpListenerContext again (we’ll call it HttpServerContext to avoid confusion with ASP.NET MVC) :

public class HttpServerContext { public HttpServerContext(HttpListenerContext context) { _innerContext = context; } private readonly HttpListenerContext _innerContext; public HttpListenerRequest Request => _innerContext.Request; public HttpListenerResponse Response => _innerContext.Response; public IPrincipal User { get; internal set; } public ISession Session { get; internal set; }}Copy the code

For existing attributes, we can delegate directly. Session is something that we have to declare. In addition, we have redeclared User because the default implementation is read-only and there is no way to set the User (we will use this later in the User authentication section).

Next, we need to replace all references to HttpListenerContext with HttpServerContext. This involves most of the code files, but is a simple substitution action that you can do yourself.

The code in the MiddlewarePipeline also needs to be changed slightly:

internal class MiddlewarePipeline { internal void Execute(HttpListenerContext context) { var serverContext = new HttpServerContext(context); try { foreach (var middleware in _middlewares) { var result = middleware.Execute(serverContext); . }} catch (Exception ex) {... }}}Copy the code

The controller adds several helper methods to facilitate Session access:

public abstract class Controller
{
    public HttpServerContext HttpContext { get; internal set; }

    protected ISession Session => HttpContext.Session;

    protected IPrincipal User => HttpContext.User;
}
Copy the code

With everything in place, we implement a middleware that handles sessions:

public class SessionManager : IMiddleware
{
    public SessionManager()
    {
        _sessions = new ConcurrentDictionary<string, Session>();            
    }

    private const string _cookieName = "__sessionid__";

    private ConcurrentDictionary<string, Session> _sessions;

    public MiddlewareResult Execute(HttpServerContext context)
    {
        var cookie = context.Request.Cookies[_cookieName];
        Session session = null;
        if (cookie != null)
        {
            _sessions.TryGetValue(cookie.Value, out session);
        }
        if (session == null)
        {
            session = new Session();
            var sessionId = GenerateSessionId();
            _sessions[sessionId] = session;
            cookie = new Cookie(_cookieName, sessionId);
            context.Response.SetCookie(cookie);
        }
        context.Session = session;
        return MiddlewareResult.Continue;
    }

    private string GenerateSessionId()
    {
        return Guid.NewGuid().ToString();
    }
}
Copy the code

Session implementation principle is very simple: use Cookie to record a key, corresponding to the data on the server side, if there is no new one. If used on production servers, cookies must be encrypted and protected by other means. Since implementing encryption requires a lot of code, I won’t do it here. Solemnly declare: although it is not complicated to implement a Session in principle, it is not easy to achieve a truly secure, correct and robust Session, and Session is also the attack point of many hackers. But unless you consider yourself a safety expert, don’t try to build a wheel by hand, or you can easily introduce unknown defects.

The Session middleware is already implemented and we can add it to the processing pipeline:

class Program { static void RegisterMiddlewares(IWebServerBuilder builder) { builder.Use(new HttpLog()); / / builder. Use (new BlockIp (" : "1", "127.0.0.1")); builder.Use(new SessionManager()); / /... Similarly hereinafter}}Copy the code

Finally, make a few changes to the controller code to see if it really works:

public class HomeController : Controller
{ 
    public ActionResult Index()
    {
        int counter = (Session["counter"] != null) ? (int)Session["counter"] : 0;
        counter++;
        Session["counter"] = counter;
        return "counter=" + counter;
    }
}
Copy the code

Open your browser and refresh a few times, and you’ll see that the counter actually grows, indicating that the Session is working.

We’ve implemented sessions so that the server doesn’t suffer from memory loss. But what you may not realize is that the encapsulation of HttpListenerContext provides a good starting point for subsequent functionality. In the next article, we’ll introduce support for the View Engine, which will allow the framework to output real HTML pages instead of hard-coded strings.

series

  • Write your own Web server (index) in C#
  • Write a Web server in C#, part 1 – basics
  • Write a Web server in C#, part 2 – middleware
  • Write a Web server in C#, part 3 – routing
  • Write your own Web server in C#, part 4 – Session
  • Write your own Web server in C#, part 5 – view engine
  • Writing your own Web server in C#, part 6 – user authentication