zoukankan      html  css  js  c++  java
  • 管道处理模型一(摘抄cainong2005博客)

    一.HTTP请求

    我们自己写的程序,是怎样进行处理的?一个完整的HTTP请求流程:

    1.用户浏览器输入地址,例如 http://www.csdn.net

    2.DNS解析(域名供应商):将输入的网址解析成IP+端口

    3.请求到达服务器Server:IP可以在互联网上唯一定位一台服务器,而端口是用来确定进程的,端口还可以带有协议信息,用于穿过防火墙。

    4.HTTP.SYS服务接收HTTP请求:

    我们可以自己用IIS部署一个网站,模拟HTTP请求。顺序是部署网站----指定一个端口监听----请求到服务器----带了端口信息和协议----被HTTP.SYS监听到。HTTP.SYS是安装IIS时自动装上去的。

    5.IIS将请求转发给ISAPI

    IIS不能处理我们写的代码,它只能将我们的代码转发到对应的程序进行处理,它里面有一个“处理映射程序”,这里配置的是IIS的处理方式,即请求是什么后缀名,就用哪种dll处理程序进行处理,其中*.cshtml、*.aspx、*.ashx都是由asp.net_isapi.dll来进行处理,如图

    因为IIS只是将请求根据后缀转发到不同的处理程序,所以我们甚至可以给java和php指定处理程序。比如将php转发给php_ISAPI,java转发给java_ISAPI,只要配置好就可以实现。

    如果是js,css,html等静态文件,IIS是直接返回的。

    这里有个小问题,聪明的你可能会说,像mvc这样 Home/Index,没有后缀怎么办?

    IIS6和它之前都不支持mvc的,后来出现了mvc,没有后缀怎么写匹配?IIS会给没有后缀的加一个axd的后缀再处理,如图:

    到了IIS7.0,就不需要这么做了。

    IIS的应用程序池分为集成和经典,如图:

    经典模式表明是旧的,所以一般都用集成模式,

    6.HttpWorkerRequest:在上一步,会将请求包装成一个对象,通过pipeline传到这里来,这里才是asp.net开发的入口,前面是系统帮我们做好的,我们程序员是从这里开始搞事情的!

    好了,终于进到我们自己写的程序中来了,看看HttpWorkerRequest的定义,如图:

    7.HttpRuntime.processrequest  

    HttpRuntime:Http运行时,processrequest 是它下面的一个方法,看定义,如图:

    ProcessRequest 方法需要一个HttpWorkerRequest参数,也就是上一步得到的。

    1.  
      public ActionResult Index()
    2.  
      {
    3.  
      HttpRuntime.ProcessRequest(null);//web请求的入口
    4.  
      return View();
    5.  
      }

    至于ProcessRequest是怎么处理的,就要进入“管道处理模型”了。

    什么是“管道处理模型”呢?就是一个请求进入到HttpRunTime之后,也就是从第7步开始,要做的事情,就叫做“管道处理模型”,因为前6步都是系统做好的,我们管不着,从第7步才开始运行我们写的代码。

    我们用反编译工具打开System.Web.HttpRunTime,找到ProcessRequest

    传入参数是HttpWorkerRequest类型的,如果传入null,抛出异常;如果没有使用管道模型,也抛出异常,如果都OK,就访问下一个方法 ProcessRequestNoDemand(wr)。

    这里有个RequestQueue,因为Http请求也可以队列,如果队列不为空,就执行GetRequestToExecute(wr)方法进行处理,再看下面的ProcessRequestNow(wr)方法:

    它调用了ProcessRequestInternal(wr)方法:

    ProcessRequestInternal(wr)方法是怎么做的呢?如果被释放了(disposingHttpRuntime),那么就发送一个503的错误,"Server Too Busy",并用一个html页面来显示。如果没有被释放,就往下走,初始化一个HttpContext,它是Http请求上下文,如果初始化HttpContext失败了,就用html页面给用户返回一个400错误,下面的代码比较长,我再抓一个下面的图给大家看:

    如果HttpContext初始化成功了,就把它拿到HttpApplicationFactory.GetApplicationInstance方法里面创建了一个HttpApplication对象。

    每个请求都经过了上面的步骤,创建了一个HttpApplication对象,用这个HttpApplication对象来处理请求,HttpApplication是我们管道模型的核心。

    通过上述步骤,终于执行到了我们写的代码,前面我们几乎没做什么,都是框架做的,我们也扩展不了。

    看看HttpApplication的代码:

    为什么有这么多的事件呢?因为HttpApplication要处理各种不同的请求,每个请求也许要做相同的事情,也可能不同的事情,也可能要做的事情的顺序不同,我们要把共性的部分封装起来,所以就封装成了这么多的事件(event),这样做的好处就是,遇到不同的请求,我们可以把这些事件排列和组合起来,就能完成请求的处理。

    下图是对各个事件功能的介绍:

    其中BeginRequest和EndRequest是方便我们做扩展的,可以在这两个方法里面加上我们要的触发动作。

    PostMapRequestHandler就是把我们的请求创建一个处理器对象,我们写的MVC,Webform,都在它里面,要让它来实际执行的。

    二.HttpModule

    先写一段代码,用反射的方式获取所有系统自带的HttpApplication 的 Event事件:

    1.  
      public ViewResult Events()
    2.  
      {
    3.  
      //获取当前上下文的HttpApplication实例
    4.  
      HttpApplication app = base.HttpContext.ApplicationInstance;
    5.  
       
    6.  
      List<SysEvent> sysEventsList = new List<SysEvent>();
    7.  
      int i = 1;
    8.  
      foreach (EventInfo e in app.GetType().GetEvents())
    9.  
      {
    10.  
      sysEventsList.Add(new SysEvent()
    11.  
      {
    12.  
      Id = i++,
    13.  
      Name = e.Name,
    14.  
      TypeName = e.GetType().Name
    15.  
      });
    16.  
      }
    17.  
      return View(sysEventsList);
    18.  
      }

    运行一下,25个Event事件:

    先说一下,HttpModule是什么:对HttpApplication做的扩展事件(上面图上,都是HttpApplication原本有的事件)。先看一下IHttpModule接口里面是什么:

    很简单对不对?Init方法,参数是HttpApplication。接下来,我们就做对IHttpModule做一个实现吧:

    新建一个MyCustomModule,继承自IHttpModule,具体做的事情就是给原有事件输出一段文字。

    1.  
      public class MyCustomModule : IHttpModule
    2.  
      {
    3.  
      public void Dispose()
    4.  
      {
    5.  
      //此处放置清除代码。
    6.  
      }
    7.  
       
    8.  
      public void Init(HttpApplication application)
    9.  
      {
    10.  
      application.AcquireRequestState += (s, e) => application.Response.Write(string.Format("<h1 style='color:#00f'>来自MyCustomModule 的处理,{0}请求到达 {1}</h1><hr>", DateTime.Now.ToString(), "AcquireRequestState "));
    11.  
      application.AuthenticateRequest += (s, e) => application.Response.Write(string.Format("<h1 style='color:#00f'>来自MyCustomModule 的处理,{0}请求到达 {1}</h1><hr>", DateTime.Now.ToString(), "AuthenticateRequest "));
    12.  
      application.AuthorizeRequest += (s, e) => application.Response.Write(string.Format("<h1 style='color:#00f'>来自MyCustomModule 的处理,{0}请求到达 {1}</h1><hr>", DateTime.Now.ToString(), "AuthorizeRequest "));
    13.  
      application.BeginRequest += (s, e) => application.Response.Write(string.Format("<h1 style='color:#00f'>来自MyCustomModule 的处理,{0}请求到达 {1}</h1><hr>", DateTime.Now.ToString(), "BeginRequest "));
    14.  
      application.Disposed += (s, e) => application.Response.Write(string.Format("<h1 style='color:#00f'>来自MyCustomModule 的处理,{0}请求到达 {1}</h1><hr>", DateTime.Now.ToString(), "Disposed "));
    15.  
      application.EndRequest += (s, e) => application.Response.Write(string.Format("<h1 style='color:#00f'>来自MyCustomModule 的处理,{0}请求到达 {1}</h1><hr>", DateTime.Now.ToString(), "EndRequest "));
    16.  
      application.Error += (s, e) => application.Response.Write(string.Format("<h1 style='color:#00f'>来自MyCustomModule 的处理,{0}请求到达 {1}</h1><hr>", DateTime.Now.ToString(), "Error "));
    17.  
      application.LogRequest += (s, e) => application.Response.Write(string.Format("<h1 style='color:#00f'>来自MyCustomModule 的处理,{0}请求到达 {1}</h1><hr>", DateTime.Now.ToString(), "LogRequest "));
    18.  
      application.MapRequestHandler += (s, e) => application.Response.Write(string.Format("<h1 style='color:#00f'>来自MyCustomModule 的处理,{0}请求到达 {1}</h1><hr>", DateTime.Now.ToString(), "MapRequestHandler "));
    19.  
      application.PostAcquireRequestState += (s, e) => application.Response.Write(string.Format("<h1 style='color:#00f'>来自MyCustomModule 的处理,{0}请求到达 {1}</h1><hr>", DateTime.Now.ToString(), "PostAcquireRequestState "));
    20.  
      application.PostAuthenticateRequest += (s, e) => application.Response.Write(string.Format("<h1 style='color:#00f'>来自MyCustomModule 的处理,{0}请求到达 {1}</h1><hr>", DateTime.Now.ToString(), "PostAuthenticateRequest "));
    21.  
      application.PostAuthorizeRequest += (s, e) => application.Response.Write(string.Format("<h1 style='color:#00f'>来自MyCustomModule 的处理,{0}请求到达 {1}</h1><hr>", DateTime.Now.ToString(), "PostAuthorizeRequest "));
    22.  
      application.PostLogRequest += (s, e) => application.Response.Write(string.Format("<h1 style='color:#00f'>来自MyCustomModule 的处理,{0}请求到达 {1}</h1><hr>", DateTime.Now.ToString(), "PostLogRequest "));
    23.  
      application.PostMapRequestHandler += (s, e) => application.Response.Write(string.Format("<h1 style='color:#00f'>来自MyCustomModule 的处理,{0}请求到达 {1}</h1><hr>", DateTime.Now.ToString(), "PostMapRequestHandler "));
    24.  
      application.PostReleaseRequestState += (s, e) => application.Response.Write(string.Format("<h1 style='color:#00f'>来自MyCustomModule 的处理,{0}请求到达 {1}</h1><hr>", DateTime.Now.ToString(), "PostReleaseRequestState "));
    25.  
      application.PostRequestHandlerExecute += (s, e) => application.Response.Write(string.Format("<h1 style='color:#00f'>来自MyCustomModule 的处理,{0}请求到达 {1}</h1><hr>", DateTime.Now.ToString(), "PostRequestHandlerExecute "));
    26.  
      application.PostResolveRequestCache += (s, e) => application.Response.Write(string.Format("<h1 style='color:#00f'>来自MyCustomModule 的处理,{0}请求到达 {1}</h1><hr>", DateTime.Now.ToString(), "PostResolveRequestCache "));
    27.  
      application.PostUpdateRequestCache += (s, e) => application.Response.Write(string.Format("<h1 style='color:#00f'>来自MyCustomModule 的处理,{0}请求到达 {1}</h1><hr>", DateTime.Now.ToString(), "PostUpdateRequestCache "));
    28.  
      application.PreRequestHandlerExecute += (s, e) => application.Response.Write(string.Format("<h1 style='color:#00f'>来自MyCustomModule 的处理,{0}请求到达 {1}</h1><hr>", DateTime.Now.ToString(), "PreRequestHandlerExecute "));
    29.  
      application.PreSendRequestContent += (s, e) => application.Response.Write(string.Format("<h1 style='color:#00f'>来自MyCustomModule 的处理,{0}请求到达 {1}</h1><hr>", DateTime.Now.ToString(), "PreSendRequestContent "));
    30.  
      application.PreSendRequestHeaders += (s, e) => application.Response.Write(string.Format("<h1 style='color:#00f'>来自MyCustomModule 的处理,{0}请求到达 {1}</h1><hr>", DateTime.Now.ToString(), "PreSendRequestHeaders "));
    31.  
      application.ReleaseRequestState += (s, e) => application.Response.Write(string.Format("<h1 style='color:#00f'>来自MyCustomModule 的处理,{0}请求到达 {1}</h1><hr>", DateTime.Now.ToString(), "ReleaseRequestState "));
    32.  
      application.RequestCompleted += (s, e) => application.Response.Write(string.Format("<h1 style='color:#00f'>来自MyCustomModule 的处理,{0}请求到达 {1}</h1><hr>", DateTime.Now.ToString(), "RequestCompleted "));
    33.  
      application.ResolveRequestCache += (s, e) => application.Response.Write(string.Format("<h1 style='color:#00f'>来自MyCustomModule 的处理,{0}请求到达 {1}</h1><hr>", DateTime.Now.ToString(), "ResolveRequestCache "));
    34.  
      application.UpdateRequestCache += (s, e) => application.Response.Write(string.Format("<h1 style='color:#00f'>来自MyCustomModule 的处理,{0}请求到达 {1}</h1><hr>", DateTime.Now.ToString(), "UpdateRequestCache "));
    35.  
       
    36.  
      }
    37.  
      }
    好了,自定义的MyCustomModule写好了,但是还不能使用,要去webconfig注册,注册后,每个页面都会执行这个Module。

    运行一下,发现注册后比以前多了一些东西:

    MyCustomModule被从头到尾执行了一遍,这些蓝色的文字,说明在这个module中执行了哪些事件以及执行的顺序,而以前通过反射找到HttpApplication中事件(ShowEvents方法),是在PreRequstHandlerExecute方法中执行的,执行完后,还进行了收尾操作,比如PostRequestHandlerExecute(已经执行了处理程序),ReleaseRequestState(释放请求的状态),PostReleaseRequestState(已经释放了请求的状态)等等。

    好了,上面就是我们自定义的Module,现在看看框架自带的Module,打开

    web.config中有一段:

    看到没?有OutputCache,Session,WindowsAuthentication等,这些都是.net4.0框架自己注册好了的Module,是全局的,运行在这台服务器上的Web程序都要用到这个config,每个页面用到了这些Module。但是这里列的一些Module我们是用不上的,比如WindowsAuthentication,FormsAuthentication,PassportAuthentication,可以干掉的,不过不要在这个全局的webconfig中干掉,可以在每个项目的webconfig中干掉,比如刚才项目的webcongif中可以加这么一些代码干掉不要的Module:

    有没有干掉呢?看效果:

    写一个方法,获取全部的Module,包括我们扩展的MyCustomModule,还有框架自带的

    1.  
      public ViewResult Modules()
    2.  
      {
    3.  
      HttpApplication app = base.HttpContext.ApplicationInstance; //获取当前上下文的HttpApplication实例
    4.  
      List<SysModules> sysModulesList = new List<SysModules>();
    5.  
      int i = 1;
    6.  
      foreach (string name in app.Modules.AllKeys)
    7.  
      {
    8.  
      sysModulesList.Add(new SysModules()
    9.  
      {
    10.  
      Id = i++,
    11.  
      Name = name,
    12.  
      TypeName = app.Modules[name].ToString()
    13.  
      });
    14.  
      }//1 我们自定义配置的config 2 来自于.Net框架的配置
    15.  
       
    16.  
      return View(sysModulesList);
    17.  
      }

    运行一下,发现在webconfig中remove的Module,它们都不出现了:

    总结一下,HttpModule要实现IHttpModule接口,在webconfig注册,里面给HttpApplication事件去添加动作,并且HttpModule是每个页面(每次请求)都要执行的。

    三.HttpModule 能干什么

    1.权限认证:每个请求都经过Module,所以做权限认证很好。

    2.URL转发:

    新注册一个BaseModule

    Controller添加对应方法

    1.  
      public class BaseModule : IHttpModule
    2.  
      {
    3.  
      /// <summary>
    4.  
      /// Init方法仅用于给期望的事件注册方法
    5.  
      /// </summary>
    6.  
      /// <param name="httpApplication"></param>
    7.  
      public void Init(HttpApplication httpApplication)
    8.  
      {
    9.  
      httpApplication.BeginRequest += new EventHandler(context_BeginRequest);//Asp.net处理的第一个事件,表示处理的开始
    10.  
      httpApplication.EndRequest += new EventHandler(context_EndRequest);//本次请求处理完成
    11.  
      }
    12.  
      }
    1.  
      // 处理BeginRequest 事件的实际代码
    2.  
      private void context_BeginRequest(object sender, EventArgs e)
    3.  
      {
    4.  
      HttpApplication application = (HttpApplication)sender;
    5.  
      HttpContext context = application.Context;
    6.  
      string extension = Path.GetExtension(context.Request.Url.AbsoluteUri);
    7.  
      if (string.IsNullOrWhiteSpace(extension) && !context.Request.Url.AbsolutePath.Contains("Verify"))
    8.  
      {
    9.  
      context.Response.Write(string.Format("<h4 style='color:#00f'>来自BaseModule 的处理,{0}请求到达</h4><hr>", DateTime.Now.ToString()));
    10.  
      }
    11.  
       
    12.  
      //处理地址重写
    13.  
      if (context.Request.Url.AbsolutePath.Equals("/Pipe/Some", StringComparison.OrdinalIgnoreCase))
    14.  
      context.RewritePath("/Pipe/Handler");
    15.  
      }

    注意这两句,先获取到了HttpApplication,后来又获取到了HttpContext,有了HttpContext就有了全世界。

    1.  
      HttpApplication application = (HttpApplication)sender;
    2.  
      HttpContext context = application.Context;

    接下来如果访问 "/Pipe/Some",Module会偷偷跳到"/Pipe/Handler",而且浏览器的地址栏不改变,还是"/Pipe/Some"。

    MVC其实就是Module的扩展,即上面说过的UrlRoutingModule, MVC框架的诞生对webform没有影响,只是在原来框架流程上增加了一个UrlRoutingModule,它会把我们的请求做一次映射处理,指向MvcHandler,MVC路由为什么能生效啊?就是利用了UrlRoutingModule,处理方式和上面说的Url转发是一样的。

    伪静态也可以这样做,先在IIS里面给后缀指定处理程序asp.net_isapi(上面有介绍),请求进到HttpApplication后,我们扩展一个Module,在里面建立一些跳转页面的规则。

    3.反爬虫:

    每个请求都要经过Module,所以可以记录每个IP,如果某个IP请求太频繁,那么就不让它访问原始页面,让它访问一个需要输入验证码的页面,验证通过了才能继续访问。

    但也有Module不适合的场景,比如对一些特殊请求的处理。Module是每个请求都会执行的,单独给某些请求服务的不合适。

    又总结一下,管道处理模型就是请求进入System.Web(HttpRuntime.processrequest)之后,那些处理的类、方法、过程,这些就叫管道处理模型,mvc只是其中很小的一部分。

    四.Module的扩展

    新注册一个GlobalModule,添加事件

    1.  
      public class GlobalModule : IHttpModule
    2.  
      {
    3.  
      public event EventHandler GlobalModuleEvent;
    4.  
       
    5.  
      /// <summary>
    6.  
      /// Init方法仅用于给期望的事件注册方法
    7.  
      /// </summary>
    8.  
      /// <param name="httpApplication"></param>
    9.  
      public void Init(HttpApplication httpApplication)
    10.  
      {
    11.  
      httpApplication.BeginRequest += new EventHandler(context_BeginRequest);//Asp.net处理的第一个事件,表示处理的开始
    12.  
       
    13.  
      httpApplication.EndRequest += new EventHandler(context_EndRequest);//本次请求处理完成
    14.  
      }
    15.  
       
    16.  
      // 处理BeginRequest 事件的实际代码
    17.  
      void context_BeginRequest(object sender, EventArgs e)
    18.  
      {
    19.  
      HttpApplication application = (HttpApplication)sender;
    20.  
      HttpContext context = application.Context;
    21.  
      string extension = Path.GetExtension(context.Request.Url.AbsoluteUri);
    22.  
      if (string.IsNullOrWhiteSpace(extension) && !context.Request.Url.AbsolutePath.Contains("Verify"))
    23.  
      context.Response.Write(string.Format("<h1 style='color:#00f'>来自GlobalModule 的处理,{0}请求到达</h1><hr>", DateTime.Now.ToString()));
    24.  
       
    25.  
      //处理地址重写
    26.  
      if (context.Request.Url.AbsolutePath.Equals("/Pipe/Some", StringComparison.OrdinalIgnoreCase))
    27.  
      context.RewritePath("/Pipe/Handler");
    28.  
       
    29.  
      if (GlobalModuleEvent != null)
    30.  
      GlobalModuleEvent.Invoke(this, e);
    31.  
      }
    32.  
       
    33.  
      // 处理EndRequest 事件的实际代码
    34.  
      void context_EndRequest(object sender, EventArgs e)
    35.  
      {
    36.  
      HttpApplication application = (HttpApplication)sender;
    37.  
      HttpContext context = application.Context;
    38.  
      string extension = Path.GetExtension(context.Request.Url.AbsoluteUri);
    39.  
      if (string.IsNullOrWhiteSpace(extension) && !context.Request.Url.AbsolutePath.Contains("Verify"))
    40.  
      context.Response.Write(string.Format("<hr><h1 style='color:#f00'>来自GlobalModule的处理,{0}请求结束</h1>", DateTime.Now.ToString()));
    41.  
      }
    42.  
       
    43.  
      public void Dispose()
    44.  
      {
    45.  
      }
    46.  
      }

    注意看这里了:

    public event EventHandler GlobalModuleEvent;

    本来Module是用来添加事件的,比如:

    httpApplication.BeginRequest += new EventHandler(context_BeginRequest);//Asp.net处理的第一个事件,表示处理的开始

    这里结果又给Module配置了一个事件GlobalModuleEvent。

    怎样让这个GlobalModuleEvent生效呢?在global里面添加代码:

    1.  
      /// <summary>
    2.  
      /// HttpModule注册名称_事件名称
    3.  
      /// 约定的
    4.  
      /// </summary>
    5.  
      /// <param name="sender"></param>
    6.  
      /// <param name="e"></param>
    7.  
      protected void GlobalModule_GlobalModuleEvent(object sender, EventArgs e)
    8.  
      {
    9.  
      Response.Write("<h3 style='color:#800800'>来自 Global.asax 的文字 GlobalModule_GlobalModuleEvent</h2>");
    10.  
      }

    其中方法名 = webconfig中注册的Module名称 + ‘_’+ Module中的事件名称

    这样,这个方法就会自动触发,如图:

    有没有类似的?有的,大家应该见过这个,也是在global里面

    1.  
      protected void Session_Start(object sender, EventArgs e)
    2.  
      {
    3.  
      // 在新会话启动时运行的代码
    4.  
      Console.WriteLine("Session_Start 啥也不干");
    5.  
      logger.Info("Session_Start");
    6.  
      }
    7.  
      protected void Session_End(object sender, EventArgs e)
    8.  
      {
    9.  
      // 在会话结束时运行的代码。
    10.  
      // 注意: 只有在 Web.config 文件中的 sessionstate 模式设置为
    11.  
      // InProc(默认内存里) 时,才会引发 Session_End 事件。如果会话模式设置为 StateServer
    12.  
      // 或 SQLServer,则不会引发该事件。
    13.  
       
    14.  
      Console.WriteLine("Session_End 啥也不干");
    15.  
      logger.Info("Session_End");
    16.  
      }

    Start和End就是Session这个Module里面定义的Event。有证据吗?有的!先看看全局的webconfig,session注册过。

    然后通过反编译工具,找到SessionState这个类,从它内部找到了Start和End。

    这样做的好处是:我们封装了Module之后,Module里面还有要扩展的,就可以做成Event,在global里面去实现,并且也只能在global里面实现,什么时候执行这个Event呢?根据自己的需求。使用套路如图:

    五.其他

    1.  
      /// <summary>
    2.  
      /// 请求出现异常,都可以处理
    3.  
      /// 也可以完成全局异常处理
    4.  
      /// filter只能处理控制器里面的异常
    5.  
      /// </summary>
    6.  
      /// <param name="sender"></param>
    7.  
      /// <param name="e"></param>
    8.  
      protected void Application_Error(object sender, EventArgs e)
    9.  
      {
    10.  
      logger.Info("Application_Error");
    11.  
      Response.Write("出错");
    12.  
      Server.ClearError();
    13.  
      }
    Application_Error是处理异常的,filter也是处理异常的,但前提是要进入了控制器,如果是cshtml出错了,或者页面不存在,filter就管不到了,但可以被Application_Error处理,它可以捕获整个网站的异常,不管是控件级事件、页面级事件还是请求级事件,都可以get到。
  • 相关阅读:
    效果1时间展示隐藏
    css书写轮播图样式
    jquery案例1导航栏事件
    jquery案例三导航展示
    go并发
    效果2滑动滑入效果
    php解决导出大数据execl问题
    jquery案例3模仿京东轮播图
    jquery案例2手风琴案例
    latex自适应resize超长表格
  • 原文地址:https://www.cnblogs.com/zcbk/p/12190400.html
Copyright © 2011-2022 走看看