本文介绍了一个基于.NET的简单HTTP独立服务器库,它轻量级、无依赖、简单易用,支持部分文件流、文件缓存、简单模板和单次体解析。这个库的设计理念是简化HTTP服务器的创建和维护,使得开发者可以快速搭建起一个基本的Web服务。
这个HTTP服务器库是基于.NET的System.Net.HttpListener构建的,它具有以下特点:
每个路由都是使用静态方法Route.Add(<selector>, (rq, rp, args) => {/* 处理函数 */})定义的。rp和rp分别对应HttpListenerRequest和HttpListenerResponse对象,args对应Dictionary<string, string>对象。处理函数也可以是异步的。
根据选择器类型,可以通过使用模式或选择器函数来形成路由。
1. 通过模式选择
定义路由的最常见方式是使用字符串模式。变量在括号内定义。它们会自动解析并分配给处理函数中的args参数。
Route.Add(
"/myPath/{file}",
(rq, rp, args) => rp.AsFile(rq, args["file"]),
"GET");
2. 通过函数选择
如果路由选择器不能表示为字符串模式,或者需要额外的验证,可以通过选择器函数定义路由,该函数接收request、response和空的args字典,可以在处理函数中更新。如果路由选择是通过函数指定的,那么需要手动根据http方法(GET、POST、DELETE、HEAD)进行过滤。
Route.Add((rq, rp, args) => {
return rq.HttpMethod == "GET" &&
rq.Url.PathAndQuery.TryMatch("/myPath/{file}", args) &&
Path.HasExtension(args["file"]);
},
(rq, rp, args) => rp.AsFile(rq, args["file"]));
为了在执行相应动作之前拦截请求,必须定义Route.OnBefore函数。该函数接收request和response。如果请求已处理,则返回true,否则返回false以继续执行。下面是一个用于记录日志的示例。
Route.OnBefore = (rq, rp) => {
Console.WriteLine($"Requested: {rq.Url.PathAndQuery}");
return false; // 继续处理
};
路由按照定义的顺序进行验证,这意味着先定义的路由会先被验证。因此,用户在定义路由时应该小心。下面的例子展示了具有歧义的路由,调用顺序很重要。
Route.Add(
"/hello-{word}",
(rq, rp, args) => rp.AsText("1) " + args["world"]));
Route.Add((rq, rp, args) => {
var p = rq.Url.PathAndQuery;
if (!p.StartsWith("/hello-"))
return false;
args["world"] = p.Replace("/hello-", String.Empty);
return true;
},
(rq, rp, args) => rp.AsText(rq, "2) " + args["world"]));
库的大部分内容都是使用扩展函数编写的,这些函数操作HttpListenerRequest和HttpListenerResponse类,以简化使用。请求扩展是ParseBody。响应扩展可以分为:
AsFile、AsBytes、AsStream扩展支持字节范围请求,这意味着只有部分内容会被服务。这可以通过服务视频文件来轻松观察,其中只有视频的一部分会被发送。
当服务文件时,通过文件修改日期获得的ETag也会被发送。下次,当浏览器发送带有相同ETag的请求时,会给出NoContent响应,意味着文件可以从本地缓存中使用。这样可以实现显著的流量减少。服务器会自动执行这种行为。
请求体是通过ParseBody扩展函数读取和解析的。该函数解析表单键值对和提供的文件。表单键值对存储在提供的字典中。下面的例子展示了提取body-form值和文件。
Route.Add(
"/myForm/",
(rq, rp, args) => {
var files = rq.ParseBody(args);
// 保存文件
foreach (var f in files.Values)
f.Save(f.FileName);
// 写入表单字段
foreach (var a in args)
Console.WriteLine(a.Key + " " + a.Value);
},
"POST");
当抛出异常时,会调用Route.OnError处理器。参数包括:请求、响应和抛出的异常。默认处理器会做出文本响应,消息是异常消息。状态代码是之前设置的状态,除非其值在[200 .. 299]范围内,在这种情况下代码会被替换为400(坏请求)。
Route.OnError = (rq, rp, ex) => {
if (ex is RouteNotFoundException) {
rp.WithCode(HttpStatusCode.NotFound).AsText("Sorry, nothing here.");
} else if (ex is FileNotFoundException) {
rp.WithCode(HttpStatusCode.NotFound).AsText("The requested file not found");
} else {
rp.WithCode(HttpStatusCode.InternalServerError).AsText(ex.Message);
}
};
要启用安全的(HTTPS)连接,需要为HttpListener设置证书。这里将解释基于Windows的方法,因为截至2018年1月的通用操作系统支持尚未准备好。当前状态可以在Github Issue Tracker - .NET Core中查看。Windows解决方案包括将证书导入本地证书存储,并使用netsh实用程序进行适当的HTTPS预留。库包括两个脚本,位于存储库的Script map中。第一个脚本生成测试证书,另一个将证书导入存储并进行HTTPS预留。如何在不使用脚本的情况下手动执行此操作的步骤在Richard Astbury的博客文章中给出。
库实现了一个简单的模板引擎,它接受所有在大括号中定义的字符串作为键,并用指定的值替换它们。
var str = "My name is {name} and surname {surname}";
str = Templating.RenderString(
new Dictionary {
{ "name", "John" },
{ "surname", "Smith" }
});
// str is "My name is John and surname Smith"
替换值也可以通过一个类来指定,其中变量名被解释为键。
var str = "My name is {Name} and surname {Surname}";
str = Templating.RenderString(
new {
Name = "John",
Surname = "Smith"
});
// str is "My name is John and surname Smith"