基于.NET的轻量级HTTP服务器库

本文介绍了一个基于.NET的简单HTTP独立服务器库,它轻量级、无依赖、简单易用,支持部分文件流、文件缓存、简单模板和单次体解析。这个库的设计理念是简化HTTP服务器的创建和维护,使得开发者可以快速搭建起一个基本的Web服务。

简介

这个HTTP服务器库是基于.NET的System.Net.HttpListener构建的,它具有以下特点:

  • 轻量级:不需要额外的依赖。
  • 简单:只有一个核心方法Route.Add,用于将路由与动作关联起来。
  • 零配置:支持部分文件流、文件缓存(ETag)、简单模板和单次体解析(无需临时文件)。

创建路由

每个路由都是使用静态方法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。响应扩展可以分为:

  • With*扩展:返回修改后的响应,适用于方法链。
  • As*扩展:执行响应,之后无法修改响应。

部分数据服务

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

要启用安全的(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"
沪ICP备2024098111号-1
上海秋旦网络科技中心:上海市奉贤区金大公路8218号1幢 联系电话:17898875485