在 WinForm 中完整支持在多级目录中保存的 ASP.NET (转)

AI权益加码!Claude Code、Cursor等20+工具免费用! 购周边限时加赠Coding Plan Lite,畅享主流AI工具!学习进阶更高效! 阅读详情
大概半年前曾写过一个在 WinForm 程序中嵌入 ASP.NET 的简单例子, 《在WinForm程序中嵌入ASP.NET》。因为是试验性质的工作,所以当时偷懒直接使用系统自带的 SimpleWorkerRequest 完成 ASP.NET 页面请求的处理工作。使用自带工具类在实现上虽然简单,但受到系统的诸多功能限制,如后面有朋友提到无法直接处理多级子目录的问题等等。(如虚拟目录为 "/" 时无法处理 "/help/about.aspx" 类型的页面请求)
    对于此类需求,一个最好的实现实例就是 www.asp.net 提供的  Cassini。这个例子完整地演示了如何实现一个支持 ASP.NET 的简单 Web 服务器功能,并被 Borland 的 Delphi.NET 等许多开源项目,当作调试用 Web 服务器。虽然只有几十 K 的源代码,但麻雀虽小五脏俱全,还是非常值得一看的。但因为 Cassini 是为处理 Web 服务而设计,因此需要在了解其结构的基础上,做一些定制来满足我们的需求。

    首先来看看 Cassini 的程序结构。

    与我前文例子中采用的结构类似,Cassini 包括界面(CassiniForm)、服务器(Server)、宿主(Host)和请求处理器(Request)等几个主要部分,并通过 Connection 等几个工具类,完成 Web 请求的解析与应答功能。

    总体工作流程图如下:
以下内容为程序代码:

    +-------+ [1] +-------------+ [2] +--------+
    | Admin |---->| CassiniForm |---->| Server |
    +-------+     +-------------+     +--------+
                                          | [3]
                                          V
                       +--------+ [4] +------+
                       | Client |---->| Host |
                       +--------+     +------+
                           ^              | [5]
                           |              V
                           |        +------------+ [6] +---------+
                        [7]|        | Connection |---->| Request |--+
                           |        +------------+     +---------+  | [7]
                           +----------------------------------------+

    [1] Cassini 的管理者(Admin)首先通过 CassiniForm 的界面,设定 Web 服务器端口、页面物理目录和虚拟目录等配置信息;
    [2] 然后以配置信息构造 Server 对象,并调用 Server.Start 方法启动 Web 服务器;
以下内容为程序代码:

public class CassiniForm : Form
{
  private void Start()
  {
    // ...
    try {
        _server = new Cassini.Server(portNumber, _virtRoot, _appPath);
        _server.Start();
    }
    catch {
      // 显示错误信息
    }
    // ...
  }
}

    [3] Server 对象在建立时,将获取或自动初始化 ASP.NET 的注册表配置。这个工作是通过 Server.GetInstallPathAndConfigureAspNetIfNeeded 方法完成的。工作原理是通过 HttpRuntime 所在 Assembly (System.Web.dll) 的版本获得合适的 ASP.NET 版本;然后从注册表中查询 HKEY_LOCAL_MACHINESOFTWAREMicrosoftASP.NET 下是否有正确的 ASP.NET 的安装路径;如果有则返回之;否则会根据 System.Web.dll 的版本,以及 HKEY_LOCAL_MACHINESOFTWAREMicrosoft.NETFramework 下 .NET Framework 按照目录等信息,动态构造一个合适的 ASP.NET 注册表配置。进行这个工作的原因是 ASP.NET 可以在按照 .NET Framework 后,使用 aspnet_regiis.exe 手工注销掉,而运行支持 ASP.NET 的 Web 服务器,又必须有合适的设置。
    在完成配置和 ASP.NET 安装路径后,Server 将建立并配置 Host 对象作为 ASP.NET 的宿主。
以下内容为程序代码:

public class Server : MarshalByRefObject
{
  private void CreateHost() {
    _host = (Host)ApplicationHost.CreateApplicationHost(typeof(Host), _virtualPath, _physicalPath);
    _host.Configure(this, _port, _virtualPath, _physicalPath, _installPath);
  }

  public void Start() {
    if (_host != null)
        _host.Start();
  }
}

    [4] Host 类作为 ASP.NET 的宿主类,主要完成三部分工作:配置 ASP.NET 的运行时环境、响应客户端(Client)发起的 Web 页面请求、以及判断客户端请求的有效性。
    配置 ASP.NET 的运行时环境主要工作是,为 ASP.NET 的执行和后面请求有效性的判断获取足够的配置信息。例如 Server 能够提供的 Web 服务端口、页面虚拟路径、页面物理路径以及 ASP.NET 程序安装路径等等,以及 Host 根据这些信息计算出的 ASP.NET 客户端脚本的虚拟和物理路径等等。此外还会接管线程所在 AppDomain 的卸载事件 AppDomain.DomainUnload,在 Web 服务器停止的时候自动终止 Web 服务。
    响应客户端(Client)发起的 Web 页面请求功能,是通过建立 Socket 监听 Server 对象指定的 Web 服务 TCP 端口来完成的。Host.Start 方法建立 Socket,并通过线程池异步调用 Host.OnStart 方法在后台监听请求;Host.OnStart 方法则在 接收到 Web 请求后,通过线程池异步调用 Host.OnSocketAccept 方法完成请求的响应工作;Host.OnSocketAccept 则负责在处理 Web 请求的时候,建立 Connection 对象,并进一步调用 Connection.ProcessOneRequest 方法处理 Web 请求。虽然 Host 没有使用复杂的请求分配算法,但因为线程池的灵活使用,使得其性能完全不受处理瓶颈的限制,也是线程池使用的良好范例。
以下内容为程序代码:

internal class Host : MarshalByRefObject
{
  public void Start() {
    if (_started)
      throw new InvalidOperationException();

    // 建立 Socket 监听 Web 服务端口
    _socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
    _socket.Bind(new IPEndPoint(IPAddress.Any, _port));
    _socket.Listen((int)SocketOptionName.MaxConnections);

    _started = true;
    ThreadPool.QueueUserWorkItem(_onStart); // 通过线程池异步调用
  }

  private void OnStart(Object unused) {
    while (_started) {
      try {
        Socket socket = _socket.Accept(); // 响应客户端请求
        ThreadPool.QueueUserWorkItem(_onSocketAccept, socket); // 通过线程池异步调用
      }
      catch {
        Thread.Sleep(100);
      }
    }
    _stopped = true;
  }

  private void OnSocketAccept(Object acceptedSocket) {
    Connection conn =  new Connection(this, (Socket)acceptedSocket);
    conn.ProcessOneRequest(); // 处理客户端请求
  }
}

    最后,判断客户端请求的有效性的功能,是通过三个重载的 Host.IsVirtualPathInApp 方法,提供给 Connection 在具体处理客户端请求时调用,来判断请求的有效性,下面讨论 Connection 时再详细解释。

    [5] Host 在建立 Connection 对象并调用其 ProcessOneRequest 方法处理用户请求时,Connection 对象会首先等待客户端请求数据(WaitForRequestBytes),然后创建 Request 对象,并调用 Request.Process 方法处理请求。而其自身,则通过一堆 WaitXXX 函数,为 Request 类提供支持。
以下内容为程序代码:

internal class Connection {
  public void ProcessOneRequest() {
    // wait for at least some input
    if (WaitForRequestBytes() == 0) { // 等待客户端请求数据
      WriteErrorAndClose(400); // 发送 HTTP 400 错误给客户端
      return;
    }

    Request request = new Request(_host, this);
    request.Process();
  }

  private int WaitForRequestBytes() {
    int availBytes = 0;

    try {
      if (_socket.Available == 0) {
        // poll until there is data
        _socket.Poll(100000 /* 100ms */, SelectMode.SelectRead); // 等待客户端数据 100ms 时间
        if (_socket.Available == 0 && _socket.Connected)
          _socket.Poll(10000000 /* 10sec */, SelectMode.SelectRead);
      }

      availBytes = _socket.Available;
    }
    catch {
    }

    return availBytes;
  }


    [6] Request 在接收到 Connection 的请求后,将从客户端读取请求内容,并按照 HTTP 协议进行分析。因为本文不是做 HTTP 协议的分析工作,所以这部分代码就不详细讨论了。
    在 Request.ParseRequestLine 函数分析 HTTP 请求获得请求页面路径后,会调用前面提到过的 Host.IsVirtualPathInApp 函数判断此路径是否在 Web 服务器提供的虚拟路径下级,并且返回此虚拟路径是否指向 ASP.NET 的客户端脚本。如果 Web 请求的虚拟路径以 "/" 结尾,则调用 Request.ProcessDirectoryListingRequest 方法返回列目录的响应;否则调用 HttpRuntime.ProcessRequest 方法完成实际的 ASP.NET 请求处理工作。
    HttpRuntime 通过 Request 的基类 HttpWorkerRequest 提供的统一接口,采用 IoC 的策略获取最终页面的所在。与我前面文章中使用的 SimpleWorkerRequest 实现最大不同在于 Request.MapPath 完成了一个较为完整的虚拟目录到物理目录映射机制。
    SimpleWorkerRequest.MapPath 实现相对简陋:
以下内容为程序代码:

public override string SimpleWorkerRequest.MapPath(string path)
{
  if (!this._hasRuntimeInfo)
  {
    return null;
  }

  string physPath = null;
  string appPhysPath = this._appPhysPath.Substring(0, (this._appPhysPath.Length - 1)); // 去掉末尾斜杠

  if (((path == null) || (path.Length == 0)) || path.Equals("/"))
  {
    physPath = appPhysPath;
  }

  if (path.StartsWith(this._appVirtPath))
  {
    physPath = appPhysPath + path.Substring(this._appVirtPath.Length).Replace('/', '/');
  }

  InternalSecurityPermissions.PathDiscovery(physPath).Demand();

  return physPath;
}

    Request.MapPath 的实现则相对要完善许多,考虑了很多 SimpleWorkerRequest 无法处理的情况,使得 Request 的适应性更强。
以下内容为程序代码:

public override String Request.MapPath(String path) {
  String mappedPath = String.Empty;

  if (path == null || path.Length == 0 || path.Equals("/")) {
    // asking for the site root
    if (_host.VirtualPath == "/") {
      // app at the site root
      mappedPath = _host.PhysicalPath;
    }
    else {
      // unknown site root - don't point to app root to avoid double config inclusion
      mappedPath = Environment.SystemDirectory;
    }
  }
  else if (_host.IsVirtualPathAppPath(path)) {
    // application path
    mappedPath = _host.PhysicalPath;
  }
  else if (_host.IsVirtualPathInApp(path)) {
    // inside app but not the app path itself
    mappedPath = _host.PhysicalPath + path.Substring(_host.NormalizedVirtualPath.Length);
  }
  else {
    // outside of app -- make relative to app path
    if (path.StartsWith("/"))
      mappedPath = _host.PhysicalPath + path.Substring(1);
    else
      mappedPath = _host.PhysicalPath + path;
  }

  mappedPath = mappedPath.Replace('/', '/');

  if (mappedPath.EndsWith("/") && !mappedPath.EndsWith(":/"))
    mappedPath = mappedPath.Substring(0, mappedPath.Length-1);

  return mappedPath;
}


    关于 Cassini 的进一步讨论,可以参考 www.asp.net 上的 讨论专版

    [7] 在 HttRuntime 完成具体的 ASP.NET 页面处理工作后,会通过 Request.SendResponseFromXXX 系列函数,将页面结果返回给客户端。

    虽然 SimpleWorkerRequest.MapPath 方法实现简单,但理论上完全可以处理多级目录的情况。之所以在使用 SimpleWorkerRequest 时,无法处理嵌套目录,是因为 SimpleWorkerRequest 在构造函数中错误地分解了请求的页面所在虚拟目录等信息。
    SimpleWorkerRequest 的两个构造函数,在将请求页面虚拟路径(如"/help/about.aspx")保存后,都调用了 ExtractPagePathInfo 方法对页面路径做进一步的分解工作。
以下内容为程序代码:

private void SimpleWorkerRequest.ExtractPagePathInfo()
{
  int idx = this._page.IndexOf('/');
  if (idx >= 0)
  {
    this._pathInfo = this._page.Substring(idx);
    this._page = this._page.Substring(0, idx);
  }
}

    this._pathInfo 是为实现 HttpWorkerRequest.GetPathInfo 提供的存储字段。而 GetPathInfo 将返回 URL 中在页面后的路径信息,例如对 "path/virdir/page.html/tail" 将返回 "/tail"。早期的许多 HTTP 客户端程序,如 Delphi 中 WebAction 的分发,都利用了这个路径信息的特性,在 Web 页面或 ISAPI 一级之后,再次进行请求分发。但因为 SimpleWorkerRequest 实现上或者设计上的限制,导致在处理 PathInfo 时会将 "/help/about.aspx" 类似的多级 url 错误切断。最终返回给 HttpRuntime 的 this._path 将变成空字符串,而 this._pathInfo 被设置为 "/help/about.aspx",而单级路径如 "about.aspx" 不受影响。
    知道了这个原理后,就可以对 SimpleWorkerRequest 稍作修改,重载受到 ExtractPagePathInfo 影响的几个方法,即可完成对多级目录结构下页面的支持。如果需要进一步的映射支持,如同时支持多个虚拟子目录,可以参照 Cassini 的 Request 实现 MapPath 等方法。
以下内容为程序代码:

public class Request : SimpleWorkerRequest
{
  private string _appPhysPath;
  private string _appVirtPath;

  private string _page;
  private string _pathInfo;

  public Request(string page, string query, TextWriter output) : base(page, query, output)
  {
    this._appPhysPath = Thread.GetDomain().GetData(".appPath").ToString();
    this._appVirtPath = Thread.GetDomain().GetData(".hostingVirtualPath").ToString();

    this._page = page;

    // TODO: 从 page 中进一步解析 Path Info
  }

  public override string GetPathInfo()
  {
    if (this._pathInfo == null)
    {
      return string.Empty;
    }
    return this._pathInfo;
  }

  private string GetPathInternal(bool includePathInfo)
  {
    string path = (_appVirtPath.Equals("/") ? _page : _appVirtPath + _page);

    if (includePathInfo && (_pathInfo != null))
    {
      return path + this._pathInfo;
    }
    else
    {
      return path;
    }
  }

  public override string GetUriPath()
  {
    return GetPathInternal(true);
  }

  public override string GetFilePath()
  {
    return GetPathInternal(false);
  }

  public override string GetRawUrl()
  {
    string query = this.GetQueryString();

    if ((query != null) && (query.Length > 0))
    {
      return GetPathInternal(true) + "?" + query;
    }
    else
    {
      return GetPathInternal(true);
    }
  }

  public override string GetFilePathTranslated()
  {
    return _appPhysPath + _page.Replace('/', '/');
  }

  public override string MapPath(string path)
  {
    string physPath = null;

    if (((path == null) || (path.Length == 0)) || path.Equals("/"))
    {
      physPath = this._appPhysPath;
    }

    if (path.StartsWith(this._appVirtPath))
    {
      physPath = this._appPhysPath + path.Substring(this._appVirtPath.Length).Replace('/', '/');
    }

    return physPath;
  }
}
Winform菜单两种实现方式 MainStrip, ToolStripMenuItem,,, 阅读详情

相关推荐

WinForm程序脱离.NET框架方法

WinForm程序脱离.NET Framework的方法有以下几种: ­ ( 1 ) .Net Linker :飞信就利用了它的核心。飞信是使用C#在.NET上开发的。.Net Linker比较贵,最便宜的版本要1000多美金,不过倒版的就例外了。 ­ ( 2 ) 用飞信的虚拟机来引导自己开发的.NET程序。 ­ ( 3 ) 使用MONO实现.NET框架脱离,还可实现跨平台。(开源免费,速度

u010561359的专栏 2673

C#客户端和服务Socket 之间的通信(asp.net

这是C#中的Socket用套接字实现一个服务器与多个客户端的通信代码,希望能对初学者有个帮助!

C#实现创建级联菜单(附完整源码)

C#实现创建级联菜单(附完整源码)

希望我的博客,能帮上你解决学习中工作中所遇到的问题 248

WinForm应用实战开发指南 - 如何实现多层级的数据展示

本文将为大家介绍如何用DevExpress控件实现多层级的数据展示,欢迎下载相关组件体验!

需要界面开发、IDE工具研发中文教程资料的小伙伴,记得私信我~ 1413

html 多级菜单_(十三)C# Winform自定义控件导航菜单

准备工作 有时候我们需要左侧的导航菜单,那么来整一个吧先来分析分析,导航菜单一般分为2级或多级,如果是多级的话 用前面的treeview更合适,这里只做2级,为了父子节点样式更方便控制,我们分别实现父子节点。为了更加的Open,我们使用接口来定义一下开始 中间代码太多,复制太累,大家直接看后面的效果,需要的话访问本站链接或者原文链接查看代码吧。用处及效果 效果:最后的话 如果你喜欢的话,...

weixin_30233335的博客 746

Winfrom menuStrip添加多级菜单 及递归

Winfrom menuStrip添加多级菜单 及递归

weixin_46256404的博客 633

WinForm完整支持多级目录保存ASP.NET

进行这个工作的原因是 ASP.NET 可以在按照 .NET Framework 后,使用 aspnet_regiis.exe 手工注销掉,而运行支持 ASP.NETWeb 服务器,又必须有合适的设置。虽然只有几十 K 的源代码,但麻雀虽小五脏俱全,还是非常值得一看的。最后,判断客户端请求的有效性的功能,是通过三个重载的 Host.IsVirtualPathInApp 方法,提供给 Connection 在具体处理客户端请求时调用,来判断请求的有效性,下面讨论 Connection 时再详细解释。

biyusr的专栏 943

WinForm程序中嵌入ASP.NET

现在的流行趋势是桌面程序Web化,Web程序桌面化,呵呵。简单的方法是直接使用ApplicationHost.CreateApplicationHost函数为指定的虚拟目录和物理路径建立ASP.NET引擎宿主的实例,如。但这样建立的程序有个BT的要求,他会在指定目录的bin子目录中去尝试载入宿主类型(WebHost)的assembly,也就是说你必须把程序在bin子目录下复制一份,非常不爽。这个缺省的请求包装使用是简单,但对中文的兼容性不太好,过两天有空再自己写个强一点的吧,呵呵。

biyusr的专栏 898

ASP.NET程序也能像WinForm程序一样运行

由于现在会使用WinForm的人是越来越少了,可能有时候做点小东西就只好用ASP.NET去完成了(喜欢控制台的朋友请不要顶针), 如果是这样,悲剧就发生了:一个小工具(或者小的演示项目),发给朋友去用,总不至于让人家也装个IIS或者VS20XX吧? 如果没有这二样,这种小工具还真不方便运行。怎么办? 做过ASP.NET开发的人都知道:网站通常要布署到IIS上才能直接运行, 当然...

weixin_34248118的博客 445

asp.net像php一样开发,.NET_ASP.NET也像WinForm程序一样运行的实现方法,由于现在会使用WinForm的人是越 - phpStudy...

ASP.NET也像WinForm程序一样运行的实现方法由于现在会使用WinForm的人是越来越少了,可能有时候做点小东西就只好用ASP.NET去完成了(喜欢控制台的朋友请不要顶针),如果是这样,悲剧就发生了:一个小工具(或者小的演示项目),发给朋友去用,总不至于让人家也装个IIS或者VS20XX吧?如果没有这二样,这种小工具还真不方便运行。怎么办?做过ASP.NET开发的人都知道:网站通常要布署到...

weixin_31124163的博客 208

ASP.NET程序也像WinForm程序一样运行

我们今天要谈到的是让ASP.NET的程序也像WinForm一样的运行,这样就不需要安装IIS或者Visual Studio这样的特定环境了。 AD: 由于现在会使用WinForm的人是越来越少了,可能有时候做点小东西就只好用ASP.NET去完成了(喜欢控制台的朋友请不要顶针),如果是这样,悲剧就发生了:一个小工具(或者小的演示项目),发给朋友去用,总不

1429

B/S C/S在WinForm程序中嵌入ASP.NET WebForm应用程序

B/S C/S在WinForm程序中嵌入ASP.NET WebForm应用程序   现在的流行趋势是桌面程序Web化,Web程序桌面化,呵呵。最终目标就是你中有我,我中有你。例如MSN Explorer就是一个很好的展示,让用户在使用的时候分不清什么时候是在本地什么时候是在网络。而这类程序往往需要有一个后台服务器如IIS的支持,这对大多数桌面应用来说too heavy了。本着简单就是美

Zfrong繁荣的IT技术、IT项目、解决方案专栏Blog-上海 5085

ASP.NET程序也能像WinForm程序一样运行[载]

阅读目录 开始 操作方式 支持ASP.NET程序类别 它也是个HTTP服务支持远程机器访问 不受限于Windows防火墙 尊重每个人的操作习惯 内置多标签浏览器支持 启动参数及配置文件 支持 .NET 4.0 部署问题 由于现在会使用WinForm的人是越来越少了,可能有时候做点小东西就只好用ASP.NET去完成了(喜欢控制台的朋友请...

diaofuzuo3286的博客 162

ASP.NET也像WinForm程序一样运行

由于现在会使用WinForm的人是越来越少了,可能有时候做点小东西就只好用ASP.NET去完成了(喜欢控制台的朋友请不要顶针),如果是这样,悲剧就发生了:一个小工具(或者小的演示项目),发给朋友去用,总不至于让人家也装个IIS或者VS20XX吧?如果没有这二样,这种小工具还真不方便运行。怎么办?做过ASP.NET开发的人都知道:网站通常要布署到IIS上才能直接运行,当然也不排除你用VS打...

diaokanyou3991的博客 166

ASP.NET全部教程

Razor 是一种将基于服务器的代码添加到网页中的标记语法Razor 具有传统 ASP.NET 标记的功能,但更容易使用并且更容易学习Razor 是一种服务器端标记语法,与 ASP 和 PHP 很像Razor 支持 C# 和 Visual Basic 编程语言。

东华攻城狮的博客 2125

ASP.NET MVC

ViewStart.cshtml是一个全局的布局文件,程序中每个.cshtml页面都会默认使用_ViewStart提供的布局。同时.NET Framework 提供了多种技术框架,ASP.NET MVC是.NET Framework提供的众多技术框架中的一种,用于开发Web应用。ASP.NET MVC框架使用了约定大于配置的思想,按照框架约定的结构组织文件,浏览器请求某个Controller方法时,该方法默认返回与方法同名的.cshtml文件,浏览器接收.cshtml并渲染页面。.cshtml文件同名。

weixin_47249500的博客 1126
上一篇: ASP.Net 文章集锦
下一篇: 从C++向COM演进[转]
foxmail
博客等级 码龄25年 11粉丝 92原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值