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

在 IIS 下运行非 ASP.NET 网站开发

starIconstarIconstarIconstarIconstarIcon

5.00/5 (6投票s)

2017年7月9日

CPOL

19分钟阅读

viewsIcon

17464

downloadIcon

106

探索 IIS 的一些细微之处,并深入了解 Katana/Owin 的初始化方式。

目录

引言

我自己的网站不使用 ASP.NET,而是更倾向于使用更中立的 Web 开发工具,这意味着没有 Razor 引擎或其他微软技术。 虽然我用 C# 实现了一个自己的 Web 服务器,但“服务”和后端路由处理也可以用 Python、Node.js 或其他框架实现,而网站内容几乎保持不变。 即便如此,我需要能够让我的 Web 服务器在 IIS 下运行,这样,正如我在 上一篇文章中写到的那样,我就可以托管多个 HTTPS 域。

虽然我从未写过关于 Web 服务器代码的文章,但它利用了 The Clifton Method 中的代码库,并且可以在 GitHub 上找到。 我的 Web 服务器代码执行基本操作:

  • 路由(语义路由,意味着路由处理程序由数据包触发,而不是由路由 URL 触发)
  • 身份验证
  • 授权
  • 会话管理
  • 内容服务
  • 错误处理/报告

在实现中,该实现旨在为每个路由处理程序工作流的阶段利用多个处理器/线程。 它也非常模块化,例如,这是我网站之一的配置文件,它定义了用于日志记录、错误处理、请求路由等的模块。

<Modules>
  <Module AssemblyName='Clifton.AppConfigService.dll'/>
  <Module AssemblyName='Clifton.ConsoleCriticalExceptionService.dll'/>
  <Module AssemblyName='Clifton.PaperTrailAppLoggerService.dll'/>
  <Module AssemblyName='Clifton.EmailService.dll'/>
  <Module AssemblyName='Clifton.SemanticProcessorService.dll'/>
  <Module AssemblyName='Clifton.WebRouterService.dll'/>
  <Module AssemblyName='Clifton.WebResponseService.dll'/>
  <Module AssemblyName='Clifton.WebFileResponseService.dll'/>
  <Module AssemblyName='Clifton.WebWorkflowService.dll'/>
  <Module AssemblyName='Clifton.WebDefaultWorkflowService.dll'/>
  <Module AssemblyName='Clifton.WebSessionService.dll'/>
  <Module AssemblyName='Clifton.WebServerService.dll'/>
</Modules>

所有组件都实现为服务,并使用发布/订阅模式(这就是 SemanticProcessorService)在服务之间进行通信。

然而,本文的目的不是讨论我的 Web 服务器技术,而是关于将最后一个模块切换到

  <Module AssemblyName='Clifton.IISService.dll'/>

以便网站作为自定义 HTTP 模块在 IIS 中运行的试验、磨难和发现。 我在此过程中学到的东西,嗯,“很有趣”。 坦白说,这篇文章对我来说主要是我必须做什么来让我的 Web 服务器技术正常工作的文档。 但是,我对 IIS 和 Katana/Owin 的了解是,任何试图做我正在做的事情的人,或者仅仅是对 IHttpModuleIHttpHandlerIHttpAsyncHandler 和 Katana/Owin 初始化过程的细微之处感到好奇的人,都应该知道这些。 所以希望这里也有对你有用的内容!

发现

让我们从创建一个基本的 HTTPModule 实现开始,这是从 MSDN 示例 这里借用的(如果链接发生更改,请谷歌搜索“Custom HttpModule Example”)。 

创建 ASP.NET Web 应用程序项目

你应该从“添加新项目”对话框中执行此操作,选择 ASP.NET Web 应用程序(.NET Framework)

然后选择一个空项目

创建 HttpModule 处理程序

向生成的项目中添加一个 C# 文件,我称之为“Hook”

using System;
using System.IO;
using System.Web;

public class Module : IHttpModule
{
  public void Init(HttpApplication application)
  {
    File.Delete(@"c:\temp\out.txt");
    application.BeginRequest += new EventHandler(BeginRequest);
    application.EndRequest += new EventHandler(EndRequest);
  }

  public void Dispose()
  {
  }

 private void BeginRequest(object sender, EventArgs e)
  { 
    HttpApplication application = (HttpApplication)sender;
    HttpContext context = application.Context;
    File.AppendAllText(@"c:\temp\out.txt", "BeginRequest: " + context.Request.Url + "\r\n");
    context.Response.ContentType = "text/html";
    context.Response.Write("<h1><font color=red>HelloWorldModule: Beginning of Request</font></h1><hr>");
  }

  private void EndRequest(object sender, EventArgs e)
  {
    HttpApplication application = (HttpApplication)sender;
    HttpContext context = application.Context;
    File.AppendAllText(@"c:\temp\out.txt", "EndRequest: " + context.Request.Url + "\r\n");
    context.Response.Write("<hr><h2><font color=blue>HelloWorldModule: End of Request</font></h2>");
  }
}

请注意文件输出(将输出发送到调试输出窗口是不可靠的),以及假设你有一个 c:\temp 文件夹。

更新 Web.config 文件

修改 Web.config 文件,添加模块(你可以删除所有其他内容)

<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <system.web>
    <compilation debug="true" targetFramework="4.5.2"/>
    <httpRuntime targetFramework="4.5.2"/>
  </system.web>
  <system.webServer>
    <modules>
      <add name="Demo" type="Module, iisdemo"/>
    </modules>
  </system.webServer>
</configuration>

创建 index.html 文件

添加 index.html 文件,其中包含你喜欢的任何内容(一些简单的内容),例如:

<p>Hello World!</p>

回顾我们目前的位置

你的项目应该看起来像这样(我将 index.html 添加到了项目中)

你的项目文件夹应该看起来类似这样

如果你愿意,可以删除“roslyn”文件夹并删除对 Microsoft.CodeDom.Providers.DotNetCompiler.CSharpCodeProvider 的引用,因为我们不需要它们。

探索结果

运行模块时,你应该会看到

检查跟踪输出

请注意,这将在 Edge 中运行。 现在看看输出文件

BeginRequest: https://:63302/index.html
EndRequest: https://:63302/index.html
EndRequest: https://:63302/
EndRequest: https://:63302/

这本身就应该令人大开眼界,因为我们得到了几个不匹配 BeginRequest 调用和 EndRequest 调用的情况。 

现在在 Chrome 中运行完全相同的 URL,然后查看输出文件

BeginRequest: https://:63302/index.html
EndRequest: https://:63302/index.html
EndRequest: https://:63302/
EndRequest: https://:63302/
BeginRequest: https://:63302/favicon.ico
EndRequest: https://:63302/favicon.ico

好的,唯一的区别是 Chrome 也请求了 favicon.ico。

当我们删除 index.html 时会发生什么?

我将我的网页放在了 IIS 期望的另一个位置,所以我删除了 index.html 文件。 令我惊讶的是,我们现在看到了这个

看起来 BeginRequest 调用不再被调用了! 实际上,它是被调用了——看看 out.txt 文件

BeginRequest: https://:63302/
BeginRequest: https://:63302/
EndRequest: https://:63302/
EndRequest: https://:63302/

它就在那里,但我们输出到响应流的内容却神秘地消失了! 这真是奇怪的行为,表明我对 IIS 管道的工作原理(尤其是关于“错误”条件)的理解不够深入。

为 Web 应用设置 IIS

现在让我们再玩得开心一些。 在 IIS 中设置相同的应用

检查跟踪输出

现在我们看到了什么?

Edge

BeginRequest: https:///index.html
EndRequest: https:///index.html
EndRequest: https:///
EndRequest: https:///

没有变化!

但在 Chrome 中

BeginRequest: https:///
BeginRequest: https:///
BeginRequest: https:///index.html
EndRequest: https:///index.html
EndRequest: https:///
EndRequest: https:///
BeginRequest: https:///favicon.ico
EndRequest: https:///favicon.ico

但有时我们会看到这个(以及可能的其他变体)

BeginRequest: https:///index.html
EndRequest: https:///index.html
EndRequest: https:///
EndRequest: https:///
BeginRequest: https:///favicon.ico
EndRequest: https:///favicon.ico

当我们添加 HttpHandler 时会发生什么?

现在让我们添加一个什么都不做的 HttpHandler 并将其连接到 Web.config

public class Handler : IHttpHandler
{
  public bool IsReusable { get { return true; } }

  public void ProcessRequest(HttpContext context)
  {
    File.AppendAllText(@"c:\temp\out.txt", "ProcessRequest: " + context.Request.Url + "\r\n");
    context.Response.Write("<p>Hello from the handler!</p>");
  }
}

此处理程序在写入响应流方面没有任何作用,但它会在被调用时记录。

Web.config 部分

<system.webServer>
  <modules>
    <add name="Demo" type="Module, iisdemo"/>
  </modules>
  <handlers>
    <add name="Test" verb="*" path="*" type="Handler, iisdemo"/>
  </handlers>
</system.webServer>

再次运行 Web 应用(IIS Express)

运行 Web 应用,然后注意 index.html 中的任何内容都不再被渲染

检查跟踪输出

现在查看跟踪输出文件

Edge

BeginRequest: https://:63302/
ProcessRequest: https://:63302/
EndRequest: https://:63302/

Chrome

BeginRequest: https://:63302/
ProcessRequest: https://:63302/
EndRequest: https://:63302/
BeginRequest: https://:63302/favicon.ico
ProcessRequest: https://:63302/favicon.ico
EndRequest: https://:63302/favicon.ico

好的,哇,begin/end 请求现在匹配了! 所以添加一个 HttpHandler 会将 IIS 管道更改为更一致的 begin/end 请求过程! 真是奇怪,不是吗? 另外请注意,ProcessRequest 出现在 begin/end 请求之间!

为什么这很重要?

这很重要,因为我想以一种合理的方式自己处理 HttpResponse 内容。 这意味着我期望生成 begin/end 请求的 IIS 管道表现合理,从我在这里的发现来看,合理意味着包括一个 HttpHandler,即使它什么都不做。

我们还需要 index.html 吗?

不! 一旦实现了 IHttpHandler,我们又会看到 BeginRequest,即使没有 index.html 文件。

事实上,即使处理程序不写入任何内容,index.html 文件现在也被忽略了。 注释掉 Response.Write

public void ProcessRequest(HttpContext context)
{
  File.AppendAllText(@"c:\temp\out.txt", "ProcessRequest: " + context.Request.Url + "\r\n");
  // context.Response.Write("<p>Hello from the handler!</p>");
}

结果是

这里的教训是,要非常仔细地理解 IIS 基于模块、处理程序和文件在做什么。

其他人是如何做的 - Katana/Owin?

这让我开始思考,其他实现自己 Web 服务器的实现是如何挂钩到 IIS 的? 我决定看看开源项目 AspNetKatana,微软的 OWIN 实现。 这是我在 Microsoft.Owin.Host.SystemWeb 文件夹中找到的内容。

首先,有一个 IHttpModule 的实现

internal sealed class OwinHttpModule : IHttpModule

顺便说一句,阅读这段代码时要非常小心

public void Init(HttpApplication context)

变量 context 实际上是 HttpApplication,而不是 HttpContext!!! 糟糕的微软! 如果你不仔细阅读代码,这会非常具有误导性!

此类初始化 IntegratedPipelineContext,它会挂钩到 IIS 管道的许多进程中

public void Initialize(HttpApplication application)
{
  for (IntegratedPipelineBlueprintStage stage = _blueprint.FirstStage; stage != null; stage = stage.NextStage)
  {
    var segment = new IntegratedPipelineContextStage(this, stage);
    switch (stage.Name)
    {
      case Constants.StageAuthenticate:
      application.AddOnAuthenticateRequestAsync(segment.BeginEvent, segment.EndEvent);
      break;
    case Constants.StagePostAuthenticate:
      application.AddOnPostAuthenticateRequestAsync(segment.BeginEvent, segment.EndEvent);
      break;
    case Constants.StageAuthorize:
      application.AddOnAuthorizeRequestAsync(segment.BeginEvent, segment.EndEvent);
      break;
    case Constants.StagePostAuthorize:
      application.AddOnPostAuthorizeRequestAsync(segment.BeginEvent, segment.EndEvent);
      break;
    case Constants.StageResolveCache:
      application.AddOnResolveRequestCacheAsync(segment.BeginEvent, segment.EndEvent);
      break;
    case Constants.StagePostResolveCache:
      application.AddOnPostResolveRequestCacheAsync(segment.BeginEvent, segment.EndEvent);
      break;
    case Constants.StageMapHandler:
      application.AddOnMapRequestHandlerAsync(segment.BeginEvent, segment.EndEvent);
      break;
    case Constants.StagePostMapHandler:
      application.AddOnPostMapRequestHandlerAsync(segment.BeginEvent, segment.EndEvent);
      break;
    case Constants.StageAcquireState:
      application.AddOnAcquireRequestStateAsync(segment.BeginEvent, segment.EndEvent);
      break;
    case Constants.StagePostAcquireState:
      application.AddOnPostAcquireRequestStateAsync(segment.BeginEvent, segment.EndEvent);
      break;
    case Constants.StagePreHandlerExecute:
      application.AddOnPreRequestHandlerExecuteAsync(segment.BeginEvent, segment.EndEvent);
      break;
    default:
      throw new NotSupportedException(
      string.Format(CultureInfo.InvariantCulture, Resources.Exception_UnsupportedPipelineStage, stage.Name));
    }
  }
  // application.PreSendRequestHeaders += PreSendRequestHeaders; // Null refs for async un-buffered requests with bodies.
  application.AddOnEndRequestAsync(BeginFinalWork, EndFinalWork);
}

请注意,此代码没有挂钩 BeginRequest! 事实上,搜索整个代码库,没有任何内容涉及 BeginRequest

还有一个用于 IHttpHandler 的异步处理程序

public sealed class OwinHttpHandler : IHttpAsyncHandler

我们注意到同步处理程序会抛出异常,而异步处理程序会调用 BeginProcessRequest

void IHttpHandler.ProcessRequest(HttpContext context)
{
  // the synchronous version of this handler must never be called
  throw new NotImplementedException();
}

IAsyncResult IHttpAsyncHandler.BeginProcessRequest(HttpContext context, AsyncCallback cb, object extraData)
{
  return BeginProcessRequest(new HttpContextWrapper(context), cb, extraData);
}

Owin 创建自己的“上下文”,然后“执行”它

OwinCallContext callContext = appContext.CreateCallContext(
  requestContext,
  requestPathBase,
  requestPath,
  callback,
  extraData);

try
{
  callContext.Execute();
}
catch (Exception ex)
{
  if (!callContext.TryRelayExceptionToIntegratedPipeline(true, ex))
  {
    callContext.Complete(true, ErrorState.Capture(ex));
  }
}

callContext.Execute 调用 AppFunc,传递一些上下文信息并返回一个 Task,该任务定义为

using AppFunc = Func<IDictionary<string, object>, Task>;

AppFunc 也是一个通过生成器设置的属性(这是一个奇怪的调用,为什么不使用泛型,如 builder.Build<AppFunc>()???)

AppFunc = (AppFunc)builder.Build(typeof(AppFunc));

这最终调用 BuildInternal,它会调用一个在元组中定义的委托。 调用是

app = middlewareDelegate.DynamicInvoke(invokeParameters);

从这个元组获得

private readonly IList<Tuple<Type, Delegate, object[]>> _middleware;

它按相反顺序处理,奇怪的是,它将返回一个单独的 app,即使它可能创建任何 app 实例

foreach (var middleware in _middleware.Reverse())
{
  Type neededSignature = middleware.Item1;
  Delegate middlewareDelegate = middleware.Item2;
  object[] middlewareArgs = middleware.Item3;

  app = Convert(neededSignature, app);
  object[] invokeParameters = new[] { app }.Concat(middlewareArgs).ToArray();
  app = middlewareDelegate.DynamicInvoke(invokeParameters);
  app = Convert(neededSignature, app);
}

return Convert(signature, app);

这是怎么回事? 嗯,它很狡猾。 请注意,前面的 app 作为第一个参数传递给下一个 app

object[] invokeParameters = new[] { app }.Concat(middlewareArgs).ToArray();

因此,它正在构建一个管道,这就是为什么列表按相反的顺序处理——for 循环处理的最后一个项是管道中的第一个项。

所有这些中间件都是在这里创建的

 public IAppBuilder Use(object middleware, params object[] args)
{
  _middleware.Add(ToMiddlewareFactory(middleware, args));
  return this;
}

该方法是终点——您可以在此挂钩到 Owin 引擎以定义自己的管道以及管道中每个节点的参数。 非常酷。 代码注释也非常有帮助!

这篇文章(关于构建 Owin 管道的许多文章之一,如果您喜欢 CP 文章,这里有一篇极好的文章)提供了几个示例。

这一切让我想起了我在 Web 服务器引擎中实现的默认工作流,使用了我的 WebWorkflowService

ServiceManager.Get<IWebWorkflowService>().RegisterPreRouterWorkflow(new WorkflowItem<PreRouteWorkflowData>(PreRouter));
ServiceManager.Get<IWebWorkflowService>().RegisterPostRouterWorkflow(new WorkflowItem<PostRouteWorkflowData>(PostRouterInjectLayout));
ServiceManager.Get<IWebWorkflowService>().RegisterPostRouterWorkflow(new WorkflowItem<PostRouteWorkflowData>(PostRouterRendering));

还有其他相似之处——我的工作流异步执行工作流项,但使用自定义线程池而不是 Task

我们学到了什么?

  1. 我们必须实现一个 HttpHandler 才能从 IIS 获得合理的 begin/process/end 结果。
  2. Katana/Owin 在 HttpHandler 中处理其所有管道处理。
  3. 调查并理解 IIS 的行为!
  4. 查看其他代码以获取示例和更好的理解!

所以,这定义了我必须将 Web 服务器挂入 IIS 管道的位置/方式。

引导

现在请求的 begin/end 是一致的,我们有两个选择。 我们可以挂入 EndRequest 以通过我的 Web 服务器提供响应,或者我们可以添加一个机制,通过 HttpHandlerProcessRequest 调用挂入。 后者更有意义,但存在一个问题:在 HttpHandler 对象实例化之前,就会调用模块的 Init。 对演示代码进行一些重构

public class Handler : IHttpHandler
{
  public bool IsReusable { get { return true; } }

  public Handler()
  {
    File.AppendAllText(@"c:\temp\out.txt", "Handler Instantiated.\r\n");
  }

  public void ProcessRequest(HttpContext context)
  {
    File.AppendAllText(@"c:\temp\out.txt", "ProcessRequest: " + context.Request.Url + "\r\n");
  }
}

public class Module : IHttpModule
{
  public void Init(HttpApplication application)
  {
    File.AppendAllText(@"c:\temp\out.txt", "HttpModule.Init called.\r\n");
    application.BeginRequest += new EventHandler(BeginRequest);
    application.EndRequest += new EventHandler(EndRequest);
  }
...

我们看到

HttpModule.Init called.
HttpModule.Init called.
BeginRequest: https://:63302/
Handler Instantiated.
ProcessRequest: https://:63302/
EndRequest: https://:63302/

Web.config 文件中模块和处理程序的定义顺序无关紧要。 我们注意到,由于处理程序是可重用的,因此后续调用不会再次实例化处理程序。

HttpModule.Init called.
HttpModule.Init called.
BeginRequest: https://:63302/
Handler Instantiated.
ProcessRequest: https://:63302/
EndRequest: <a href="https://:63302/">https://:63302/</a>
--- page refresh ---
BeginRequest: https://:63302/
ProcessRequest: https://:63302/
EndRequest: https://:63302/

如果指示处理程序*不可*重用,我们会看到

HttpModule.Init called.
HttpModule.Init called.
BeginRequest: https://:63302/
Handler Instantiated.
ProcessRequest: https://:63302/
EndRequest: <a href="https://:63302/">https://:63302/</a>
--- page refresh ---
BeginRequest: https://:63302/
Handler Instantiated.
ProcessRequest: https://:63302/
EndRequest: https://:63302/

这里,处理程序会在每次页面刷新/导航时实例化。

Katana 做什么?

那么 Katana 是如何初始化 HttpHandlerBeginProcessRequest 方法中使用的 _appAccessor 之类的东西的呢?

OwinAppContext appContext = _appAccessor.Invoke();

嗯,它是通过向 IIS 调用的公共构造函数提供一个单独的内部构造函数来完成的。

public OwinHttpHandler()
  : this(Utils.NormalizePath(HttpRuntime.AppDomainAppVirtualPath), OwinApplication.Accessor)
{
}

... 

internal OwinHttpHandler(string pathBase, Func<OwinAppContext> appAccessor)
{
  _pathBase = pathBase;
  _appAccessor = appAccessor;
}

OwinApplication.Acessor 返回 OwinBuilder.Build 实例的 Lazy 实例化(注意 static)

private static Lazy<OwinAppContext> _instance = new Lazy<OwinAppContext>(OwinBuilder.Build);

...

internal static Func<OwinAppContext> Accessor
{
  get { return () => _instance.Value; }
  ...
}

……这是一个静态方法

 internal static OwinAppContext Build()
{
  Action<IAppBuilder> startup = GetAppStartup();
  return Build(startup);
}

……尽管没有注释,但它似乎初始化了程序集加载器,加载了应用程序设置中 owin:AppStartup 指定的程序集,并返回启动 Action

哇! 

我会怎么做?

内部构造函数的使用是一种我可以用在启动过程中的好技术,例如:

public class Handler : IHttpHandler
{
  public Bootstrapper bootstrapper;
  public ServiceManager serviceManager;

  public bool IsReusable { get { return true; } }

  public Handler() : 
    this(MyWebServerInitialization)
  {
  }

  internal Handler(Action<Handler> initializer)
  {
    initializer(this);
    FinishInitialization();
  }

  public void ProcessRequest(HttpContext context)
  {
    IWebServerService server = serviceManager.Get<IWebServerService>();
    server.ProcessRequest(context);
  }

  private static void MyWebServerInitialization(Handler handler)
  {
    handler.bootstrapper = new Bootstrapper();
    handler.serviceManager = handler.bootstrapper.Bootstrap(@"c:\websites\projourn\data", @"c:\websites\projourn\bin");
    ISemanticProcessor semProc = handler.serviceManager.Get<ISemanticProcessor>();

    // Required unless we wait for the SemanticProcessor's tasks to complete.
    semProc.ForceSingleThreaded = true; 
  }

  private void FinishInitialization()
  {
    InitializeDatabaseContext();
    InitializeRoutes();
    RegisterRouteReceptors();
  }
  ...

(忽略硬编码的字面字符串,这是用于测试 IIS 的站点。)

稍后我们将讨论为什么我将语义处理器(pubsub 系统)强制设为单线程模式。

这很有趣的是,我们不需要在 Web.config 中初始化模块,只需要初始化处理程序。

<handlers>
  <add name="All" verb="*" path="*" type="Handler, ProjournHttpModule"/>
</handlers>

(在上面的代码中,这是我的 ProJourn 网站初始化的程序集名称。)

酷! 这奏效了,尽管有一些我们需要重新审视的地方。

HttpContext 与 HttpListenerContext

我的下一个问题是,我的 Web 服务器使用 HttpListener,它返回一个 HttpListenerContext

[fragment]
  listener = new HttpListener();
  listener.Start();
  Task.Run(() => WaitForConnection(listener));
[/fragment]

protected virtual void WaitForConnection(object objListener)
{
  HttpListener listener = (HttpListener)objListener;

  while (true)
  {
    // Wait for a connection. Return to caller while we wait.
    HttpListenerContext context = listener.GetContext();
    ...

IIS 使用 HttpContext(及其各种附属类,而不是 HttpListener... 类),而微软没有提供两者之间的任何通用接口。 这意味着我必须为 HttpContext/HttpListenerContextHttpRequest/HttpListenerRequestHttpResponse/HttpListenerResponse 类实现自己的通用接口,因为前者在 System.Web 命名空间中,而后者在 System.Net 命名空间中。 叹气。

public interface IContext
{
  IPAddress EndpointAddress();
  HttpVerb Verb();
  UriPath Path();
  UriExtension Extension();
  IRequest Request { get; }
  IResponse Response { get; }
  HttpSessionState Session { get; } 

  void Redirect(string url);
}

public interface IRequest
{
  NameValueCollection QueryString { get; }
}

public interface IResponse
{
  int StatusCode { get; set; }
  string ContentType { get; set; }
  Encoding ContentEncoding { get; set; }
  long ContentLength64 { get; set; }
  Stream OutputStream { get; }

  void Close();
  void Write(string data, string contentType = "text/text", int statusCode = 200);
  void Write(byte[] data, string contentType = "text/text", int statusCode = 200);
}

经过大约一个小时的重构并将我需要的通用方法/属性放在两个实现之间,我完成了这部分工作。

会话状态

会话对象很重要,因为我的 Web 服务器除了 Web 应用程序本身可能需要的其他内容之外,还管理会话的生存期以及身份验证/授权信息。 因为我只初始化 Web 服务器一次,所以这不是问题。 与 IIS 交互会带来两个问题:

  1. 在 ProcessRequest 调用中,上下文的 IIS Session 属性为 null。 为什么?
  2. IIS 可能会启动 Web 应用的多个副本,从而导致我的 Web 服务器的多个、独立的实例化,导致我的会话管理器有单独且不同的实例。

第一点的证明

第一个问题的解决方案很简单——在处理程序类中添加一个空的接口 IRequiresSessionState

不过有两个问题:

  1. 如何获取我的 Web 服务器实例之间的通用会话状态,或强制单个实例?
  2. 我是否甚至想使用 IIS 的会话对象?

如果对 #2 的答案是“是”,那么 #1 带来的问题就会消失,因为 IIS 管理会话的会话对象,并为处理程序实例提供正确的会话对象,无论哪个处理程序实例正在运行。 所以这是最简单的方法,并且只需要对我的会话管理器服务进行很少的修改。

protected SessionInfo CreateSessionInfoIfMissing(IContext context)
{
  SessionInfo sessionInfo;

  if (context is HttpListenerContextWrapper)
  {
    IPAddress addr = context.EndpointAddress();

    if (!sessionInfoMap.TryGetValue(addr, out sessionInfo))
    {
      sessionInfo = new SessionInfo(states);
      sessionInfoMap[addr] = sessionInfo;
    }
  }
  else
  {
    if (!context.Session.TryGetValue(SESSION_INFO, out sessionInfo))
    {
      sessionInfo = new SessionInfo(states);
      context.Session[SESSION_INFO] = sessionInfo;
    }
  }

  return sessionInfo;
}

是的,这可以通过派生对象来处理,但按照这种方式实现,我可以在我的模块列表中使用相同的 WebSessionService 模块。 请注意,IIS 的会话对象仅用于存储会话中其他对象的键值对,原因很简单,我实现了我的会话管理器中的一些特定 getter。

public virtual T GetSessionObject<T>(IContext context, string objectName)
public virtual string GetSessionObject(IContext context, string objectName)
public virtual dynamic GetSessionObjectAsDynamic(IContext context, string objectName)

突然,性能问题!

然而,在使用 IIS 会话状态方面,有一个非常令人不安的因素——在实际访问会话对象时,性能非常糟糕。 我突然从一个响应迅速的网站变成了一个明显滞后的网站(至少一到两秒),访问会话时使用 HttpSessionState

所以,让我们回顾一下我们的两个问题:

不过有两个问题:

  1. 如何获取我的 Web 服务器实例之间的通用会话状态,或强制单个实例?
  2. 我是否甚至想使用 IIS 的会话对象?

#2 的答案是“绝对不!” 这就剩下解决问题 #1,最简单的方法是使用几个静态全局变量。

public class Handler : IHttpHandler// , IRequiresSessionState <--- good riddance!
{
  public static Bootstrapper bootstrapper;
  public static ServiceManager serviceManager;

  public bool IsReusable { get { return true; } }

  public Handler() : 
    this(MyWebServerInitialization)
  {
  }

  internal Handler(Action<Handler> initializer)
  {
    initializer(this);
  }

  public void ProcessRequest(HttpContext context)
  {
    IWebServerService server = serviceManager.Get<IWebServerService>();
    server.ProcessRequest(context);
  }

  private static void MyWebServerInitialization(Handler handler)
  {
    if (bootstrapper == null)
    {
      bootstrapper = new Bootstrapper();
      serviceManager = bootstrapper.Bootstrap(@"c:\websites\projourn\data", @"c:\websites\projourn\bin");
      ISemanticProcessor semProc = serviceManager.Get<ISemanticProcessor>();

      // Required unless we wait for the SemanticProcessor's tasks to complete.
      semProc.ForceSingleThreaded = true;
      handler.FinishInitialization();
    }
  }
...

太棒了! 现在我的会话状态正常工作了,我闪电般的性能也回来了! 不过,仅供记录,它仍然不如我的非 IIS Web 服务器快,但这可能是 IIS-Express 的问题。 我注意到使用实际域名访问 IIS 比在本地使用 IIS Express 似乎更快! 或者也许是因为我使用 Edge 配合 IIS Express,而使用 Chrome 访问实际网站。 无论如何,差异并不显著到值得担心。

我们学到了什么?

  1. 不要轻易相信“微软之道”,特别是关于 IIS 的,而没有仔细考虑你所相信的东西。
  2. 只实现你需要的东西——走 HttpModule 的路线是个无底洞,正确的方法是实现 HttpHandler
  3. 应用程序初始化是一个有趣的问题,当其他东西正在进行类构造而你的应用程序尚未初始化时——在我多年的 C# 编码生涯中,我从未不得不使用 class : this() 这样的语法。

线程和多个处理程序

还记得这个吗?

// Required unless we wait for the SemanticProcessor's tasks to complete.
semProc.ForceSingleThreaded = true;

存在此的原因是,在我的 Web 服务器的工作流中,上下文会传递给工作流中的每个项目。 任何工作流项都可以关闭响应流并终止工作流,并且在任何步骤中发生的异常都会导致异常处理程序优雅地处理上下文响应流。 此外,由于请求处理程序(再次,在我的服务器中)从不关心请求如何或何时终止,因此它除了将请求发布给发布者之外,不做任何其他事情。 发布/订阅服务,除非另有指示,否则将使用我自己的线程池异步启动任何感兴趣的接收者的进程处理程序,而不是使用与处理器数量和糟糕的 .NET 线程池体系结构紧密相关的 Task

不幸的是,我这个故意的设计无法使用,因为当 IIS 调用 ProcessRequest 时,我的服务器中的某些东西需要在 ProcessRequest 返回到 IIS 之前被放入响应流。 否则,IIS 的线程很可能会在我的服务器上的某个线程完成请求之前关闭流。 幸运的是,由于我的发布/订阅架构同时处理异步和“强制同步”请求,因此很容易修改以用于 IIS,但我对解决方案并不满意。

不过有一件事确实让我困扰——那两个将所有内容过滤到单个 Web 服务器实例的静态变量。

public static Bootstrapper bootstrapper;
public static ServiceManager serviceManager;

这唯一的理由是为了处理实际在所有可能实例之间共享会话管理器这一需求。 因此,对初始化过程进行一些重构。

private static void MyWebServerInitialization(Handler handler)
{
  if (bootstrapper == null)
  {
    bootstrapper = new Bootstrapper();
    serviceManager = bootstrapper.Bootstrap(@"c:\websites\projourn\data", @"c:\websites\projourn\bin");
    ISemanticProcessor semProc = serviceManager.Get<ISemanticProcessor>();

    // Required unless we wait for the SemanticProcessor's tasks to complete.
    semProc.ForceSingleThreaded = true;
    handler.FinishInitialization(serviceManager);
  }
  else
  {
    Bootstrapper newBootstrapper = new Bootstrapper();
    ServiceManager newServiceManager = newBootstrapper.Bootstrap(@"c:\websites\projourn\data", @"c:\websites\projourn\bin");
    var sessionService = serviceManager.Get<IWebSessionService>();
    newServiceManager.RegisterSingleton<IWebSessionService>(sessionService);
    ISemanticProcessor semProc = newServiceManager.Get<ISemanticProcessor>();

    // Required unless we wait for the SemanticProcessor's tasks to complete.
    semProc.ForceSingleThreaded = true;
    handler.FinishInitialization(newServiceManager);
  }
}

注意这部分:

var sessionService = serviceManager.Get<IWebSessionService>();
newServiceManager.RegisterSingleton<IWebSessionService>(sessionService); 

在这里,我们从第一次初始化中获取会话服务,并将其设置在新服务器实例中,替换了第二次(及后续)服务器实例中创建的会话服务实例。 我更喜欢这个实现!

这里的重大假设

我在这里(以及之前的实现)做了一个假设,即 Handler 构造函数是*同步*调用的! 我找不到关于我的这个假设是否正确的指导,但如果我们想真正安全,我们可以使用 lock

private static Bootstrapper bootstrapper;
private static ServiceManager serviceManager;
private static object locker = new Object();

public Handler() : 
  this(MyWebServerInitialization)
{
}

internal Handler(Action<Handler> initializer)
{
  lock (locker)
  {
    initializer(this);
  }
}

微软的某个人看到这个可能会尖叫。

IHttpAsyncHander 怎么样?

Stack Overflow 上的 Samuel Neff 有一个很好的解释,我在此完整引用:

ASP.NET 使用线程池来处理传入的 HTTP 请求。

当调用 IHttpHandler 时,会使用一个线程池线程来运行该请求,并且同一个线程用于处理整个请求。如果该请求调用数据库、另一个 Web 服务或任何可能耗时的事情,线程池线程就会等待。这意味着线程池线程花费时间等待事情,而它们本可以用于处理其他请求。

相反,当使用 IHttpAsyncHandler 时,存在一种机制允许请求注册一个回调,并在请求完全处理之前将线程池线程返回给池。线程池线程开始为请求执行一些处理。可能调用数据库调用或 Web 服务等的异步方法,然后注册一个回调供 ASP.NET 在该调用返回时调用。此时,处理 HTTP 请求的线程池线程将返回给池以处理另一个 HTTP 请求。当数据库调用或任何其他调用返回时,ASP.NET 会在新的线程池线程上触发注册的回调。最终结果是线程池线程不会等待 I/O 绑定的操作,您可以更有效地使用线程池。

对于非常高并发的应用程序(数百或数千个真正的同时用户),IHttpAsyncHandler 可以提供巨大的并发提升。对于较少的用户,如果您有非常耗时的请求(例如长轮询请求),仍然可以获得好处。但是,IHttpAsyncHandler 下的编程更加复杂,因此在不需要时应避免使用。

通过一点重构和从 MSDN 复制粘贴,我也可以利用 IHttpAsyncHandler

public class Handler : /*IHttpHandler,*/ IHttpAsyncHandler
{
  ...
  public void ProcessRequest(HttpContext context)
  {
    throw new InvalidOperationException();
    // Gone:
    //IWebServerService server = serviceManager.Get<IWebServerService>();
    //server.ProcessRequest(context);
  }

  public IAsyncResult BeginProcessRequest(HttpContext context, AsyncCallback cb, Object extraData)
  {
    // The new way!
    return new AsyncServer(cb, context, extraData).ServeRequest(serviceManager);
  }
  ...

现在工作将在我的新 AsyncServer 类中完成。

public class AsyncServer : IAsyncResult
{
  private AsyncCallback callback;
  private HttpContext context;

  public bool IsCompleted { get; protected set; }
  public WaitHandle AsyncWaitHandle => null;
  public object AsyncState { get; protected set; }
  public bool CompletedSynchronously => false;

  public AsyncServer(AsyncCallback callback, HttpContext context, Object state)
  {
    this.callback = callback;
    this.context = context;
    AsyncState = state;
    IsCompleted = false;
  }

  public AsyncServer ServeRequest(ServiceManager serviceManager)
  {
    ThreadPool.QueueUserWorkItem(new WaitCallback(StartAsyncTask), serviceManager);

    return this;
  }

  private void StartAsyncTask(object sm)
  {
    ServiceManager serviceManager = (ServiceManager)sm;
    IWebServerService server = serviceManager.Get<IWebServerService>();
    server.ProcessRequest(context);
    IsCompleted = true;
    callback(this);
  }
}

为什么 StartAsyncTask 中没有 try-catch? 因为我相信我的服务器工作流会处理异常。

同样,我并不是特别喜欢 ThreadPool,但为了简单起见(好吧,我承认,我还没有完全抽象出我的线程池类!),我将使用标准的示例,直到我重构我的代码,使我的线程池不嵌入在语义处理器服务中。

结论

我发现写这样一篇文章的两个主要好处是:

  1. 我应该看看别人是如何做的,并向他们学习。
  2. 作为 #1 的结果,我能做得更好。

这篇文章*真是*如此。 当我开始写这篇文章时,我的 Web 服务器在 IIS 下工作正常,我处理了会话管理问题,并且我的代码中有一个非常丑陋的瑕疵,有点像:

if (context.Response.ContentType != null) return;

为什么? 因为我在 EndRequest 调用中注入我的响应,并且我还没有完全理解 IIS 在做什么。 正如第一个示例所示,我收到了对同一 URL 的多个请求。 我的响应服务正在抛出异常,因为一旦设置了内容类型,再次设置它就会抛出 .NET 异常。 这不仅仅是代码异味,简直是代码恶臭。 我无法想象其他人也必须做同样愚蠢的事情,所以我开始尝试 IIS 的各种东西,并很快发现了正确的方法——啊,空气再次清新!

我一直处理的另一个问题是 IIS 的会话管理器。 当 BeginRequest 触发时,上下文的会话不存在。 当 EndRequest 触发时,上下文的会话已经消失——请看 这里的图表,它显示会话状态是在 BeginRequest 之后获取,并在 EndRequest 之前释放,分别在 HttpApplicationAcquireRequestStateHttpApplicationReleaseRequestState 中。 我真的想使用 IIS 的会话管理器,认为它会比我实现的更好,至少可以整合我的一些会话管理器提供的功能。 这当然导致发现了使用 IIS 会话管理器带来的性能损失(我怀疑这与序列化/持久化有关),这使我放弃使用 IIS 的会话管理器,尽管我没有进一步研究性能问题——我有一个已经运行良好的解决方案,只需要稍作调整即可用于 IIS。

我还发现了一个有趣的探索路径——虽然我在 IIS Express 中处理 POST 动词到其相应处理程序的路由没有问题,但我的登录从一开始在 IIS 中就失败了,因为我的登录是 POST jQuery 调用。 为什么失败? 事实证明,默认情况下 IIS 只接受 GET 动词。 这导致了一个添加自定义 POST 处理程序的无底洞,而当添加我的自定义处理程序(请注意,此处理程序接受所有路径的所有动词)后,这个处理程序完全变得不必要了。

<add name="Test" verb="*" path="*" type="Handler, iisdemo"/>

在写这篇文章时,我脑子里一直有个挥之不去的想法,即我在这里写的东西会受到审视,代码异味是一个危险信号,表明我还没有足够理解问题,因此,我进行了一些进一步的挖掘、研究和返工,得出了我认为是正确的解决方案,并且能够经受考验。 啊,文档的好处!

© . All rights reserved.