65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.74/5 (10投票s)

2018 年 1 月 11 日

CPOL

5分钟阅读

viewsIcon

35926

基于 System.Net.HttpListener 的适用于 .NET Core 的简单 HTTP 服务器。

摘要

引言

本文介绍了一个基于 System.Net.HttpListener 的简单独立 HTTP 服务器,它

  • 轻量级
    没有依赖项。

  • 简单
    只有一个相关的方法 Route.Add,它将路由与操作关联起来。其他方法是 HttpListenerRequestHttpListenerRequest 类的扩展。零配置。

它支持部分文件流、文件缓存 (ETag)、简单模板化、单次正文解析(无临时文件)。

创建路由

每个路由都使用 static 方法定义
Route.Add(<选择器>, (rq, rp, args) => {/* 处理函数 */}).
rprp 分别对应 HttpListenerRequestHttpListenerResponse 对象,而 args 对应 Dictionary<string, string> 对象。处理函数也可以是 async 的。

根据选择器类型,可以通过模式或选择器函数来形成路由。

1. 按模式选择

定义路由最常见的方式是使用字符串模式。变量定义在括号内。它们会被自动解析并分配给处理函数中的 args 参数。

在下面的示例中,处理函数使用当前目录作为根目录来提供指定的文件。指定 HTTP 方法是可选的,默认为 "GET"。

Route.Add("/myPath/{file}", (rq, rp, args) => rp.AsFile(rq, args["file"]), "GET");

2. 按函数选择

如果路由选择器不能用字符串模式表示,或者需要额外的验证,则可以通过选择器函数定义路由,该函数接收 requestresponse 和空的 args 字典,该字典可以更新供处理函数使用。如果路由选择是通过函数指定的,则必须手动进行基于 HTTP 方法(GETPOSTDELETEHEAD)的过滤。

在下面的代码片段中,通过检查路径是否包含扩展名来验证文件路由。如果选择器函数返回 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 函数。该函数接收 requestresponse。如果请求已处理,返回值应为 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"])); 

根据哪个路由先定义,执行的操作将不同。

扩展

库的大部分代码都是使用操作 HttpListenerRequestHttpListenerResponse 类的扩展函数来简化使用的。请求扩展是 ParseBody。响应扩展可以分为

  • With*

    返回修改后的响应的扩展,适用于方法链式调用:WithCORSWithContentTypeWithHeaderWithCodeWithCookie

  • As*

    执行响应后无法进行响应修改的扩展:AsTextAsRedirectAsFileAsBytesAsStream

部分数据服务

扩展 AsFileAsBytesAsStream 支持 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 处理程序。参数是:requestresponse 和抛出的 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 日 - 发布第一个版本
© . All rights reserved.