IIS处理浏览器请求的流程 | 应用程序的生命周期 |反编译工具用法 |管道事件


我曾在(发布网站 IIS部署网站)一文中说到

我们在IIS上部署一个网站的时候(我们在Internet 信息服务(IIS)管理器,对应网站的右边>编辑网站>基本设置,可以看到,网站名称与应用程序池的名称是一样的:其实我们在IIS里创建一个网站的时候,IIS会自动给我们建立一个与网站名称相同名称的应用程序池,这个应用程序池是干什么用的呢?当你请求这个MyTestWebSite网站以后,网站需要处理我们的请求,这样就需要有一个进程来处理,这个进程就是这个应用程序池里面的进程,帮我们处理对于这个网站的所有请求访问。说白了,这个应用程序池就是支撑这个网站的后台核心进程。 

我们看到应用程序池里面有多个(5个)与各个网站同名的应用程序池,为什么每个网站都要有自己的应用程序池呢? 因为分多个应用程序池,后台就有多个进程,处理不同的网站使用单独的进程,各个网站之间就不会相互影响,即便某个网站流量过大,蹦了,瘫痪了,都不会影响其他的网站的正常运行)



IIS里默认的网站名可能与它的应用程序池名不一样,这个我们不管,但是只要是我们自己在IIS上建立的网站,它的网站名称与应用程序池的名称一般都是是同名的。


当我们访问IIS上的一个网站的时候,比如例如我们在这里访问MyTestWebSite这个网站,我们会发现windows任务管理器里多了一个w3wp.exe的进程,用户名是MyTestWebSite 。这个进程就是这个应用程序池里面的进程,帮我们处理对于这个网站的所有请求访问。说白了,这个应用程序池就是支撑这个网站的后台核心进程。 


当我们在浏览器一下那个默认的网站Default Web Site 又会发现windows任务管理器又多了一个w3wp.exe的进程,用户名是:DefaultAppPool



假如IIS上部署了10个网站,我访问了这个10个网站,windows任务管理器中就会有10个w3wp.exe的进程,如果我很长时间不访问其中的几个了,那么因之前访问这几个网站而启动的w3wp.exe就会自动销毁。如果你再访问的话,它有会自动启动(记住每一个网站都会有对应的一个应用程序池,每一个应用程序池在处理你对这个网站的请求的时候都会启动一个w3wp.exe的进程)

有这么一句话:一个应用程序池对于的就是一个w3wp.exe进程?   这句话是不对的。答案是:不一定,一个应用程序启动以后池至少有一个w3wp.exe进程,但是我们也在这个应用程序池里也可以设置多个进程(即:在应用程序池->右键->高级设置->进程模型>最大工作进程数。默认是1,我们也可以将它设置成多个)



现在我们来看看浏览器请求一个网站,IIS是如何工作的?

我们打开windows任务管理器可以看到一个叫InetMgr.exe的进程,和一个inetinfo.exe进程(注:打开IIS后才会有netMgr.exe这个进程)


InetMgr.exe这个进程是IIS的管理界面进程  (IIS的管理界面就是下图)


而这个inetinfo.exe进程是IIS的后台服务的核心进程,它停止后IIS也就停止了

现在我们就正式来看看浏览器请求一个网站,IIS是如何工作的。

当浏览器向服务器发送一个请求,首先浏览器是根据用户输入的URL地址去访问DNS,然后获取URL对应的IP地址和端口号,然后来封装一个http请求报文,然后通过Socket发送到服务器 (我们知道Socket与服务器连接需要IP地址和端口号的)

,在服务器端 有一个运行在windows内核模式下的http.sys核心组件,这个核心组件是可以监听任何浏览器对服务器80端口的请求

注:windows把cpu运行的时候分成2中模式,一种是内核模式,另外一种是用户模式。

用户模式:我们一般运行的应用程序是在用户模式下运行的,用户模式下,当你启动一个应用程序以后,它会给你启动对应的进程,然后会给这个进程分配对应的地址空间来运行我们的程序,这样即便我们的应用程序崩溃了也不会影响其他的应用程序的正常运行。  

内核模式:windows操作系统核心的组件是在内核模式下运行的,所有的内核模式下运行的代码是共享单个虚拟地址空间的(即:共享一个地址空间的)所以内核模式下的代码一旦奔溃,操作系统就崩溃了,而用户模式下的某个应用程序崩溃并不会影响操作系统的正常运行。

windows操作CPU 会自动切换内核模式和用户模式来执行程序

http.sys这个运行在内核模式的核心组件监听到浏览器的请求以后,并不是由它来处理浏览器的请求,它首先会读取系统的注册表,然后从注册表中查询当前哪个程序可以处理对80端口的http请求,如果系统中安装了IIS,它就会监测到注册表里IIS这个程序(进程)可以对这个80端口的http请求进行处理(如果没有安装,这个http.sys核心组件发现没有可以处理对这个80端口的请求的应用程序,,http.sysy就不不理会了,所以就也不会有响应了)  因为inetinfo.exe是IIS后台服务的核心进程,所以http.sys会把请求交给inetinfo.exe这个进程去处理。  

IIS接收到浏览器对这个80端口请求以后,会查找IIS自己的配置信息,根据请求报文的URL后缀名来判断浏览器请求的是什么类型的资源(静态资源或动态资源),

如果浏览器请求的是静态资源,会启动一个叫w3wp.exe的进程(因为w3wp.exe是浏览器请求的网站的应用程序池里面的进程,而IIS开始处理请求的时候又会启动一个w3wp.exe进程,所以我猜测IIS是根据浏览器请求url的端口号来确定到底是启动哪个网站对应的应用程序池里的w3wp.exe进程), w3wp.exe会根据URL路径去读取磁盘上的静态资源,然后把读取到的结果返回给inetinfo.exe(即:IIS),然后IIS又把结果返回给http.sys这个内核模块的核心组件,由它来将结果返回给浏览器。

如果浏览器请求的是动态资源,它会在启动w3wp.exe这个进程之前 去处理程序映射找aspx,ashx等.net动态网页应该使用哪个组件来处理?  于是找到了aspnet_isapi.dll这个组件,然后启动w3wp.exe进程,w3wp.exe会加载并运行aspnet_isapi.dll这个组件来处理(注:aspnet_isapi.dll是一个C++写的文件,不是.net程序集,所以这个aspnet_isapi.dll会在寄宿在w3wp.exe这个进程中开始执行。


那这个aspnet_isapi.dll是如何执行的呢?

第一步:aspnet_isapi.dll开始启动.net运行时(运行环境),创建应用程序域(即:启动.net runtime, 启动.net运行时后会执行这么几个方法:1>获取一个实现了IISAPIRuntime接口类型的对象,这个类型就是ISAPIRuntime,然后调用该对象的ProcessRequest(ecb)方法,这个参数ecb其实就是HTTP的请求报文  ,在ProcessRquest(ecb)这个方法中对浏览器的请求报文做了简单的封装,把请求报文数据封装成了一个ISAPIWorkerRequest类型的对象(这个对象名字叫wr),注意:这个封装是及其简单的,不像HttpContext那么详细。 2>开始调用HttpRuntime.ProcessRequestNoDemand(wr)方法,在方法中又调用了ProcessRequestNow(wr)方法,而在ProcessRequestNow(wr)方法中又调用了_theRuntime.ProcessRequestInternal(wr)方法,然后在这个方法中对wr这个对象又进行了详细的封装,封装成了HttpContext对象(注:HttpContext对象里面有HttpRequest类型的属性Request,和HttpResponse类型的属性Response)。





下面提供一下ProcessRequestInternal(wr)这个方法的源代码 ;在这个方法中奖wr对象封装成了HttpContext对象

private void ProcessRequestInternal(HttpWorkerRequest wr)
{
    Interlocked.Increment(ref this._activeRequestCount);
    if (this._disposingHttpRuntime)
    {
        try
        {
            wr.SendStatus(0x1f7, "Server Too Busy");
            wr.SendKnownResponseHeader(12, "text/html; charset=utf-8");
            byte[] bytes = Encoding.ASCII.GetBytes("<html><body>Server Too Busy</body></html>");
            wr.SendResponseFromMemory(bytes, bytes.Length);
            wr.FlushResponse(true);
            wr.EndOfRequest();
        }
        finally
        {
            Interlocked.Decrement(ref this._activeRequestCount);
        }
    }
    else
    {
        HttpContext context;
        try
        {
            context = new HttpContext(wr, false);
        }
        catch
        {
            try
            {
                wr.SendStatus(400, "Bad Request");
                wr.SendKnownResponseHeader(12, "text/html; charset=utf-8");
                byte[] data = Encoding.ASCII.GetBytes("<html><body>Bad Request</body></html>");
                wr.SendResponseFromMemory(data, data.Length);
                wr.FlushResponse(true);
                wr.EndOfRequest();
                return;
            }
            finally
            {
                Interlocked.Decrement(ref this._activeRequestCount);
            }
        }
        wr.SetEndOfSendNotification(this._asyncEndOfSendCallback, context);
        HostingEnvironment.IncrementBusyCount();
        try
        {
            try
            {
                this.EnsureFirstRequestInit(context);
            }
            catch
            {
                if (!context.Request.IsDebuggingRequest)
                {
                    throw;
                }
            }
            context.Response.InitResponseWriter();
            IHttpHandler applicationInstance = HttpApplicationFactory.GetApplicationInstance(context);
            if (applicationInstance == null)
            {
                throw new HttpException(SR.GetString("Unable_create_app_object"));
            }
            if (EtwTrace.IsTraceEnabled(5, 1))
            {
                EtwTrace.Trace(EtwTraceType.ETW_TYPE_START_HANDLER, context.WorkerRequest, applicationInstance.GetType().FullName, "Start");
            }
            if (applicationInstance is IHttpAsyncHandler)
            {
                IHttpAsyncHandler handler2 = (IHttpAsyncHandler) applicationInstance;
                context.AsyncAppHandler = handler2;
                handler2.BeginProcessRequest(context, this._handlerCompletionCallback, context);
            }
            else
            {
                applicationInstance.ProcessRequest(context);
                this.FinishRequest(context.WorkerRequest, context, null);
            }
        }
        catch (Exception exception)
        {
            context.Response.InitResponseWriter();
            this.FinishRequest(wr, context, exception);
        }
    }
}

 

 
  private void ProcessRequestInternal(HttpWorkerRequest wr)
{
    Interlocked.Increment(ref this._activeRequestCount);
    if (this._disposingHttpRuntime)
    {
        try
        {
            wr.SendStatus(0x1f7, "Server Too Busy");
            wr.SendKnownResponseHeader(12, "text/html; charset=utf-8");
            byte[] bytes = Encoding.ASCII.GetBytes("<html><body>Server Too Busy</body></html>");
            wr.SendResponseFromMemory(bytes, bytes.Length);
            wr.FlushResponse(true);
            wr.EndOfRequest();
        }
        finally
        {
            Interlocked.Decrement(ref this._activeRequestCount);
        }
    }
    else
    {
        HttpContext context;
        try
        {
            context = new HttpContext(wr, false);
        }
        catch
        {
            try
            {
                wr.SendStatus(400, "Bad Request");
                wr.SendKnownResponseHeader(12, "text/html; charset=utf-8");
                byte[] data = Encoding.ASCII.GetBytes("<html><body>Bad Request</body></html>");
                wr.SendResponseFromMemory(data, data.Length);
                wr.FlushResponse(true);
                wr.EndOfRequest();
                return;
            }
            finally
            {
                Interlocked.Decrement(ref this._activeRequestCount);
            }
        }
        wr.SetEndOfSendNotification(this._asyncEndOfSendCallback, context);
        HostingEnvironment.IncrementBusyCount();
        try
        {
            try
            {
                this.EnsureFirstRequestInit(context);
            }
            catch
            {
                if (!context.Request.IsDebuggingRequest)
                {
                    throw;
                }
            }
            context.Response.InitResponseWriter();
            IHttpHandler applicationInstance = HttpApplicationFactory.GetApplicationInstance(context);
            if (applicationInstance == null)
            {
                throw new HttpException(SR.GetString("Unable_create_app_object"));
            }
            if (EtwTrace.IsTraceEnabled(5, 1))
            {
                EtwTrace.Trace(EtwTraceType.ETW_TYPE_START_HANDLER, context.WorkerRequest, applicationInstance.GetType().FullName, "Start");
            }
            if (applicationInstance is IHttpAsyncHandler)
            {
                IHttpAsyncHandler handler2 = (IHttpAsyncHandler) applicationInstance;
                context.AsyncAppHandler = handler2;
                handler2.BeginProcessRequest(context, this._handlerCompletionCallback, context);
            }
            else
            {
                applicationInstance.ProcessRequest(context);
                this.FinishRequest(context.WorkerRequest, context, null);
            }
        }
        catch (Exception exception)
        {
            context.Response.InitResponseWriter();
            this.FinishRequest(wr, context, exception);
        }
    }
} 

HttpApplication对象是通过工厂(池)的方式来创建的。(注意:在创建HttpApplication对象之前,如果网页当中

没有添加一个名字叫Global.asax的文件(其实这个Global.asax文件它是一个类),它会先去HttpApplication对象池中查找是否有现成的HttpApplication对象可以使用,如果有,则从池中直接返回该对象,如果没有,它会通过反射直接给你创建一个HttpApplication对象。
如果你在网页在添加了一个名字叫Global.asax的文件,它会先去HttpApplication对象池中查找是否有现成的HttpApplication对象可以使用,如果有,则从池中直接返回该对象,如果没有,它会将这个Global.asax文件编译成一个类,并且这个类是继承自HttpApplication这个类的,然后要通过反射创建HttpApplication对象,因为Global.asax这个类型也是继承自HttpApplication类的,所以它在这里创建了Global.asax这个类型的对象,也就等于创建了HttpApplication类对象;注意这里并没有创建HttpApplication对象,只是创建了Global.asax类型对象,只是因为Global.asax类型继承了HttpApplication类,所以是相当于创建了HttpApplication对象)


HttpApplication对象一次只能处理一个请求,所以说你每次请求都会调用HttpApplication对象的生命周期


那这个Global.asax文件到底有什么用处呢?
你要是想给HttpApplication注册它里面19个事件中的某一个事件的响应程序,都可以在这个Global.asax文件中直接定义,
在这里定义后,HttpApplication触发事件时候,就会触发你在Global.asax中定义的事件(特别提醒:Global.asax这个类型是继承自HttpApplication类的)


让我们来看看HttpApplication对象的创建过程明细。

在这个ProcessRequestInternal(wr)方法中,我们可以看到一段代码:IHttpHandler applicationInstance = HttpApplicationFactory.GetApplicationInstance(context);

这段代码就是创建HttpApplication对象的过程


现在我们打开GetApplicationInstance(context)方法


我们再打开这个EnsureInited()方法





我们再打开这个CompileApplication()方法



然后我们在回到GetApplicationInstance(HttpContext context)这个方法


我们再打开GetNormalApplicationInstance(HttpContext context)方法


以上步骤就是创建了HttpApplication对象

3> HttpRuntime.ProcessRequestNoDemand(wr)方法中通过工厂模式创建一个HttpApplication对象,创建完该对象后调用该对象的一系列方法对浏览器的请求做处理(其中肯定会调用一个方法,这个方法就是ProcessRequest(context) 方法),处理用户的请求就会触发19个事件,23步,这样就开始了我们应用程序的生命周期。等都处理完后,就将处理的结果返回给inetinfo.exe ,然后由inetinfo.exe将结果返回给http.sys,然后http.sys将结果通过socket返回给浏览器)

)

(注意:在HttpApplication对象创建开始,调用RrocessRequest(context)方法,直到调用结束(这里面会触发19个事件,23步)整个过程叫做asp.net应用程序的生命周期,而不是asp.net页面的生命周期,页面的生命周期包含在应用程序生命周期里面的,在应用程序生命周期的19个事件中的第8个和第9个事件之间创建了ashx或者aspx等页面类对象,此时还没有开始页面的生命周期。然后再第11个事件和第12个事件之间开始了一系列的页面生命周期)


(当执行到 1>获取一个实现了IISAPIRuntime接口类型的对象,这个类型就是ISAPIRuntime,然后调用该对象的ProcessRequest(ecb)方法,。而调用这个ProcessRequest(ecb)方法,就实现了在非托管资源中封装的请求报文传递到.net framework的托管资源中了,因为这个参数ecb就是HTTP请求报文)

(通过配置IIS中的处理程序映射,来决定不同的后缀名使用什么样的方式来处理来处理,比如请求的URL后缀名是.aspx 就调用aspnet_isapi.dll这个组件来处理)


Global.asax文件

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Security;
using System.Web.SessionState;

namespace HttpApplication
{
    public class Global : System.Web.HttpApplication
    {
        //注意:Application_Start(object sender, EventArgs e)与Application_End(object sender, EventArgs e)这两个事件并不是(HttpApplication)应用程序生命周期19个事件中的事件,只是额外增加的这两个事件,这两个事件,任何一个,只触发一次

        //在用户第一次请求动态网页的时候触发一次(注意是动态网页,如果是静态网页的话在真正的windows IIS中是不会被触发的。而对于HttpApplication里的19个事件,每次请求动态网页都会被触发一次)
        protected void Application_Start(object sender, EventArgs e)
        {
		</span>//网站开启的时候执行一次 (或者说只有应用程序启动时执行一次)
        }
        //在新会话启动时运行的代码
        protected void Session_Start(object sender, EventArgs e)
        {
		</span>//会话开始的时候执行一次
        }

        //BeginRequest 事件发出信号表示创建任何给定的新请求。 此事件始终被引发,并且始终是请求处理期间发生的第一个事件。
        //在 ASP.NET 响应请求时作为 HTTP 执行管线链中的第一个事件发生。
        protected void Application_BeginRequest(object sender, EventArgs e)
        {
<span style="white-space:pre">		</span>//这其实就是第一个管道事件。
        }

        protected void Application_AuthenticateRequest(object sender, EventArgs e)
        {
		</span>//这是第二个管道事件
        }
        //在出现未处理的错误时运行的代码
        protected void Application_Error(object sender, EventArgs e)
        {
		</span>//处理统一错误信息(有的地方我们没有使用try,cache来捕捉异常,如果出现错误后可以在这里捕捉到)
        }
        //在会话结束时运行的代码
        //注意:只有在web.config文件中sessionstate模式设置为InProc时,才会引发Session_End事件,如果会话模式设置为StateServer或SQLServer则不会引发该事件
        protected void Session_End(object sender, EventArgs e)
        {
		</span>//会话关闭的时候会执行一次(比如关闭浏览器)
        }
        //在应用程序关闭时运行的代码,比如:重启IIS,重启网站,重启对应网站的“应用程序池”,结束w3wp.exe进程等情况下运行。
        protected void Application_End(object sender, EventArgs e)
        {
		</span>//网站关闭的时候执行一次  (或者说 只有当应用程序停止时执行一次)
        }
    }
}




下面我们来看一下项目


Global文件

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Security;
using System.Web.SessionState;

namespace WebApp
{
    //应用程序生命周期里的19个事件在Global文件中可以定义,平常用到的一般就是以下几个
    public class Global : System.Web.HttpApplication
    {
        protected void Application_Start(object sender, EventArgs e)
        {
            System.IO.File.AppendAllText(@"d:/log.txt", "第一步:应用程序启动了===" + DateTime.Now.ToString());
        }

        protected void Session_Start(object sender, EventArgs e)
        {
            System.IO.File.AppendAllText(@"d:/log.txt", "第三步:会话开始了===" + DateTime.Now.ToString());
        }

        protected void Application_BeginRequest(object sender, EventArgs e)
        {
            System.IO.File.AppendAllText(@"d:/log.txt", "第二步:请求开始了===" + DateTime.Now.ToString());
        }

        protected void Application_AuthenticateRequest(object sender, EventArgs e)
        {
            System.IO.File.AppendAllText(@"d:/log.txt", "当安全模块已建立用户标识时发生。AuthenticateRequest 事件发出信号表示配置的身份验证机制已对当前请求进行了身份验证。===" + DateTime.Now.ToString());
        }

        protected void Application_Error(object sender, EventArgs e)
        {
            System.IO.File.AppendAllText(@"d:/log.txt", "发生错误===" + DateTime.Now.ToString());
        }

        protected void Session_End(object sender, EventArgs e)
        {
            System.IO.File.AppendAllText(@"d:/log.txt", "第五步:会话结束了(关闭IIS后发生)===" + DateTime.Now.ToString());
        }

        protected void Application_End(object sender, EventArgs e)
        {
            System.IO.File.AppendAllText(@"d:/log.txt", "最后一步:应用程序结束了(关闭IIS后发生)===" + DateTime.Now.ToString());
        }

        protected void Application_EndRequest(object sender, EventArgs e)
        {
            System.IO.File.AppendAllText(@"d:/log.txt", "第四步:请求结束了===" + DateTime.Now.ToString());
        }
    }
}

WebForm.aspx.cs文件

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;

namespace WebApp
{
    //页面生命周期里的事件可以在这个WebForm1类中进行定义
    public partial class WebForm1 : System.Web.UI.Page
    {
        protected void Page_Load(object sender, EventArgs e)
        {
            System.IO.File.AppendAllText(@"d:/log.txt", "页面生命周期Page_Load事件启动了===" + DateTime.Now.ToString()+"</n/r>");
        }

        protected void Page_Init(object sender, EventArgs e)
        {
            System.IO.File.AppendAllText(@"d:/log.txt", "页面生命周期Page_Init事件启动了===" + DateTime.Now.ToString());
        }

        protected void Page_PreInit(object sender, EventArgs e)
        {
            System.IO.File.AppendAllText(@"d:/log.txt", "页面生命周期Page_PreInit事件启动了===" + DateTime.Now.ToString());
        }
    }
}
我们浏览这个WebForm1.aspx页面。然后再关闭IIS 。在D盘下找到 log.txt文件打开



以下提供应用程序生命周期里面的19个事件的名称 。如果要在Global文件中使用的话,只要按照以下规则写对应的事件就可以了。

即定义一个方法,方法名称就是以Application开头,后面加_下划线,下划线后面就是你要定义的这19个事件中的某一个事件名称,比如AuthenticateRequest

  protected void Application_事件名称(object sender, EventArgs e)
        {
            System.IO.File.AppendAllText(@"d:/log.txt", "。。。。===" + DateTime.Now.ToString());
        }

例如:

        protected void Application_PostAuthorizeRequest(object sender, EventArgs e)
        {
            System.IO.File.AppendAllText(@"d:/log.txt", "用户请求已经获取了权限===" + DateTime.Now.ToString());
        }



下面是请求管道中的19个事件.


(1)BeginRequest: 开始处理请求


(2)AuthenticateRequest授权验证请求,获取用户授权信息


(3):PostAuthenticateRequest获取成功


(4): AunthorizeRequest 授权,一般来检查用户是否获得权限


(5):PostAuthorizeRequest:获得授权


(6):ResolveRequestCache:获取页面缓存结果






(7):PostResolveRequestCache 已获取缓存   当前请求映射到MvcHandler(pr):  创建控制器工厂 ,创建控制器,调用action执行,view→response


//action   Handler : PR()


(8):PostMapRequestHandler 创建页面对象 :创建 最终处理当前http请求的 Handler  实例:  第一从HttpContext中获取当前的PR Handler   ,Create (在这里创建了页面类对象)


(9):AcquireRequestState 获取Session  (先判断当前页面对象是否实现了IRequiresSessionState接口,如果实现了,则从浏览器发来的请求报文中获得SessionId,并到服务器的Session池中获得对于的Session对象。最后赋值给HttpContext的Session属性)


(10)PostAcquireRequestState 获得Session


(11)PreRequestHandlerExecute:准备执行页面对象
执行页面对象的ProcessRequest方法

        
(12)PostRequestHandlerExecute 执行完页面对象了


(13)ReleaseRequestState 释放请求状态


(14)PostReleaseRequestState 已释放请求状态


(15)UpdateRequestCache 更新缓存


(16)PostUpdateRequestCache 已更新缓存


(17)LogRequest 日志记录


(18)PostLogRequest 已完成日志


(19)EndRequest 完成、



评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值