Session
在 上一篇文章 中,我们实现了 Web 服务器的路由功能,并实现了控制器的基本支持。本来,我们应该高高兴兴的继续向其中添加功能,不过马上就发现一个尴尬的问题————我们还没有 Session。更具体的说,我们一直在使用的 HttpListenerContext 只提供了 Request/Response,却没有 Session 属性。这意味着我们的服务器毫无记性,只能把每次请求都当作新的用户。
出现这种情况也是情理之中的。基础类库之中的 HttpListenerXXX 系列类为我们创建 Web 应用提供了一个很好的起点,但实现 Sesssion 则是 Web 框架的事情,并不是 Web 服务器的职责。有的同学可能会问,Web 服务器和 Web 框架的区别在哪?嗯,其实这个问题也没有严格的定义,不过一般来讲,Web 服务器通常是独立于编程语言和框架的,比如 Apache/Nginx 都有支持多种语言/框架的能力;IIS 通过插件也可以运行 PHP,并且 Web 服务器通常更关心基础设施方面的问题,包括站点管理、HTTP
压缩、证书、性能和吞吐量等。而 Web 框架一般是和具体的语言或平台绑定的,希望充分利用语言本身的特性来更好的支持业务逻辑,例如 Express(Nodejs)、Django(Python)、ASP.NET MVC(.Net)等。Session 这个东西,对于后端业务是非常必要的(区分用户是绝大多数后台系统的基本要求),但对于 Web 服务器却不是绝对必需的,而且会在一定程度上影响服务器的吞吐量,所以一般会把它放到 Web Framework 的层面去实现它。
Session 要求服务器有一定的机制去记住当前请求的用户。目前绝大多数的 Session 实现都是基于 Cookie 的。在具体实现层面,又需要考虑把多少内容放在 cookie 里的问题。主流的实现会把绝大多数内容放在服务器端,客户端只记录一个用于鉴别的 key,这种实现在网络流量以及安全性方面都是极好的,缺点是会占据较多的服务器空间。也有一些实现为了减轻服务器压力以及方便客户端处理,会把部分数据放到客户端,但这样又需要考虑安全性和数据丢失的问题。我们这里不讨论方案的优劣问题,为了示例的目的,采用第一种方案——即将所有内容保存在服务端。
此外,请允许我再多说一句:Session 是一种机制,没有什么规定要求它一定是位于内存中的。许多同学似乎误解了这一点,他们似乎认为只要 Session 就一定是使用内存的。事实当然不是这样,用其他的存储机制来保存 Session 是完全合法的。之所以有这样的误会,可能是因为大多数 Session 实现默认使用内存——因为这是最简单的方式。但许多 Web 框架都提供了诸如 Session Storage 或 Session Provider 这样的扩展点,以便将 Session 保存在其他地方,比如数据库或远程
Redis/Memcached。如果要实现跨多个服务器的分布式 Session,那么内存肯定不是一个好的选择。我们在这里的实现为了简化问题也使用了内存,但请务必清楚这一点:即 Session 并非一定要保存在内存中。
代码
本文的示例代码已经全部放到 Github,每篇文章关联的代码放在独立的分支,方便读者参考。因此,要获取本文示例代码,请使用如下命令:
1 | 复制代码git clone https://github.com/shuhari/web-server-succinctly-example.git |
实现
在开头部分我们说过,HttpListenerContext 并没有提供给我们一个 Session 接口,所以我们必须在它之上再封装一层,提供 Web 框架所需的功能。
首先声明 Session 接口。对于大多数典型使用场景,Session 可以当作一个字典:
1 | 复制代码public interface ISession |
接下来,对 HttpListenerContext 进行再次封装(为了避免和 ASP.NET MVC 混淆,这里我们称为 HttpServerContext):
1 | 复制代码public class HttpServerContext |
对于已有的属性,我们可以直接委托过去。Session 则是需要我们声明的。另外,我们也重新声明了 User,这是因为默认的实现是只读的,并没有设置用户的方法(后续的用户验证部分我们还会用到它)。
接下来,我们需要把所有对 HttpListenerContext 的引用替换为 HttpServerContext。这涉及了大多数代码文件,但只是简单的替换动作,相信你可以自己完成。
在 MiddlewarePipeline 中的代码也需要稍作改动:
1 | 复制代码 internal class MiddlewarePipeline |
控制器增加几个辅助方法,方便访问 Session:
1 | 复制代码public abstract class Controller |
一切就绪,我们实现一个处理 Session 的中间件:
1 | 复制代码public class SessionManager : IMiddleware |
Session 的实现原理非常简单:用 Cookie 记录一个 key,对应服务器端中的数据,如果没有的话就新建一个。如果用于生产服务器的话,Cookie 是必须加密的,并且还要其他一些保护手段。由于实现加密需要引入很多代码,这里就不去实现了。郑重声明:虽然自己实现一个 Session 从原理上来讲并不复杂,要实现真正安全、正确且健壮的 Session 并非易事,并且 Session 也是很多黑客的攻击点。但除非你自认是安全方面的高手,请勿试图手造轮子,否则很容易引入未知的缺陷。
Session 中间件已经实现,我们可以把它加入处理管线中去:
1 | 复制代码class Program |
最后,对控制器代码稍作修改,看看是否真的生效了:
1 | 复制代码public class HomeController : Controller |
打开浏览器,多刷新几次,你会看到计数器确实在增长,说明 Session 生效了。
我们已经实现了 Session,让服务器不再患有记忆丧失症。不过你或许没有意识到的是,这里为 HttpListenerContext 的封装也为后续的其他功能提供了一个很好的起点。在下一篇文章中,我们将引入视图引擎(View Engine)的支持,从而让框架能够输出真正的 HTML 页面,而不是硬编码的字符串。
系列文章
- 用 C# 自己动手编写一个 Web 服务器 (索引)
- 用 C# 自己动手编写一个 Web 服务器,第一部分——基础
- 用 C# 自己动手编写一个 Web 服务器,第二部分——中间件
- 用 C# 自己动手编写一个 Web 服务器,第三部分——路由
- 用 C# 自己动手编写一个 Web 服务器,第四部分——Session
- 用 C# 自己动手编写一个 Web 服务器,第五部分——视图引擎
- 用 C# 自己动手编写一个 Web 服务器,第六部分——用户验证
本文转载自: 掘金