适用于 .NET 的简单 HTTP 服务器介绍






4.74/5 (10投票s)
基于 System.Net.HttpListener 的适用于 .NET Core 的简单 HTTP 服务器。
![]() |
摘要
引言
本文介绍了一个基于 System.Net.HttpListener
的简单独立 HTTP 服务器,它
-
轻量级
没有依赖项。 -
简单
只有一个相关的方法Route.Add
,它将路由与操作关联起来。其他方法是HttpListenerRequest
和HttpListenerRequest
类的扩展。零配置。
它支持部分文件流、文件缓存 (ETag)、简单模板化、单次正文解析(无临时文件)。
创建路由
每个路由都使用 static
方法定义
Route.Add(<选择器>, (rq, rp, args) => {/* 处理函数 */})
.rp
和 rp
分别对应 HttpListenerRequest
和 HttpListenerResponse
对象,而 args
对应 Dictionary<string, string>
对象。处理函数也可以是 async
的。
根据选择器类型,可以通过模式或选择器函数来形成路由。
1. 按模式选择
定义路由最常见的方式是使用字符串模式。变量定义在括号内。它们会被自动解析并分配给处理函数中的 args
参数。
在下面的示例中,处理函数使用当前目录作为根目录来提供指定的文件。指定 HTTP 方法是可选的,默认为 "GET
"。
Route.Add("/myPath/{file}", (rq, rp, args) => rp.AsFile(rq, args["file"]), "GET");
2. 按函数选择
如果路由选择器不能用字符串模式表示,或者需要额外的验证,则可以通过选择器函数定义路由,该函数接收 request
、response
和空的 args
字典,该字典可以更新供处理函数使用。如果路由选择是通过函数指定的,则必须手动进行基于 HTTP 方法(GET
、POST
;DELETE
;HEAD
)的过滤。
在下面的代码片段中,通过检查路径是否包含扩展名来验证文件路由。如果选择器函数返回 true
,则选择该操作,否则 args
变量会被清除,并选择另一个路由进行验证。
Route.Add((rq, rp, args) =>
{
//true is the action needs to be processed
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; //resume processing
};
路由选择
路由按照定义的顺序进行验证,这意味着先定义的路由将首先被验证。选择第一个有效的路由。因此,用户在定义路由时应小心。下面的示例显示了调用顺序很重要的模糊路由。
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*
返回修改后的响应的扩展,适用于方法链式调用:
WithCORS
、WithContentType
、WithHeader
、WithCode
、WithCookie
。As*
执行响应后无法进行响应修改的扩展:
AsText
、AsRedirect
、AsFile
、AsBytes
、AsStream
。
部分数据服务
扩展 AsFile
、AsBytes
、AsStream
支持 byte-range
请求,即只服务一部分内容。通过服务视频文件(只发送一部分视频)可以轻松观察到这一点。
文件缓存
当提供文件时,还会发送由文件修改日期获得的 ETag
。下次浏览器发送带有相同 ETag
的请求时,将返回 NoContent
响应,表示可以从本地缓存使用该文件。这样,就可以大大减少流量。服务器会自动执行此操作。
读取请求正文
请求的正文使用 ParseBody
扩展函数读取和解析。该函数同时解析表单键值对和提供的文件。表单键值对存储在提供的字典中。下面的示例显示了正文表单值和文件的提取。
Route.Add("/myForm/", (rq, rp, args) =>
{
var files = rq.ParseBody(args);
//save files
foreach (var f in files.Values)
f.Save(f.FileName);
//write form-fields
foreach (var a in args)
Console.WriteLine(a.Key + " " + a.Value);
},
"POST");
默认情况下,每个文件都存储在 MemoryStream
中。如果文件很大,建议直接将文件写入磁盘。ParseBody
函数通过提供一个回调来实现此行为,当文件即将被读取时,该回调就会激活。该函数需要返回一个流。当返回的 HttpFile
被释放时,流将被关闭。
Route.Add("/myForm/", (rq, rp, args) =>
{
//files are directly saved to disc (useful for large files)
var files = rq.ParseBody(args,
(name, fileName, mime) => File.OpenRead(fileName));
//close file streams if not needed
foreach(var f in files)
f.Dispose();
},
"POST");
错误处理
当抛出异常时,会调用 Route.OnError
处理程序。参数是:request
、response
和抛出的 exception
。
默认处理程序会生成一个文本响应,消息是异常消息。状态码是先前设置的状态码,除非其值在 [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 问题跟踪器 - .NET Core。
基于 Windows 的解决方案包括将证书导入本地证书存储,并使用 netsh
实用程序进行适当的 HTTPS 预留。该库包含两个脚本,位于存储库的 **脚本映射** 中。第一个脚本生成一个测试证书,第二个脚本将证书导入存储并进行 HTTPS 预留。在 Richard Astbury 的博客文章中提供了手动执行(非脚本方式)的步骤。
模板化
该库实现了一个简单的模板引擎,它将大括号中定义的所有字符串作为键,并用指定的值替换它们。
替换值由一个包含键值对的字典定义,如下所示。
var str = "My name is {name} and surname {surname}";
str = Templating.RenderString(new Dictionary<string, string>
{
{"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"
除了 RenderString
函数外,还可以通过使用 RenderFile
函数指定文件而不是模板字符串来进行替换。
结论
本文介绍了为 .NET 编写的 HTTP 独立服务器库。如文章所示,其简单的用法使其对小型项目具有吸引力。所显示的片段应该足以基本了解工作原理。完整的示例都在存储库中,等待被测试并得到良好利用 :)
历史
- 2018 年 1 月 11 日 - 发布第一个版本