nginx listen监听套接字

Nginx频道

ngx_channel_t频道是Nginx master进程与worker进程之间通信的常用工具,它是使用本机套接字实现的。socketpair方法,用于创建父子进程间使用的套接字。
int socketpair ( int d, int type, int protocol, int sv[2] );
通常在父子进程之间通信前,会先调用socketpair创建一组套接字,在调用fork方法创建出子进程后,将会在父进程中关闭sv[1]套接字,子进程关闭sv[0]套接字。
ngx_channel_t频道结构体是Nginx定义的master父进程和worker子进程间通信的消息格式。

为了追求高并发和快速响应,并发连接是任何服务端程序都逃不掉的重要性能指标,如何处理大量并发连接无疑是服务器端程序设计时所要考虑的第一问题。nginx采用的是大部分HTTP服务器的做法,即master-worker模型,一个master进程管理一个或者多个worker进程,基本的事件处理都是放在worker进程,master负责一些全局初始化,以及对worker进程的管理。

nginx中,master进程和worker进程的通信主要是通过socketpair来实现的,每当fork完一个子进程之后,就将这个子进程的socketpair句柄传递给前面已经存在的子进程,这样子进程之间也就可以通信了。Nginxfork子进程的函数是ngx_spawn_process()。主要实现代码在ngx_process.hngx_process.c文件中。

 
Nginx模块开发(12)—进程模型 - cjhust - 我一直在努力

基本上,父进程(即主进程)一开始会初始化及读取配置,并加载各模块的功能,然后fork()出N个子进程(即工作进程),具有相同的工作逻辑和功能。父进程负责监听信号(如HUP,QUIT等),通过socket pair把信号传递给子进程(子进程间一般不通信)。子进程通过事件来处理父进程传递的信号。因为每个子进程都共享服务监听端口(如http 80),当用户发送请求时,会触发子进程的事件调用函数。因此在accept()请求的时候,需要用到mutex,保证只有一个工作进程接受并处理请求。

先看看nginx process 的定义

ngx_process.h

typedef struct {
  ngx_pid_t		   pid;
  int				 status;
  ngx_socket_t		channel[2];//这里就是用来存放 socketpair 的两个描述符

  ngx_spawn_proc_pt   proc;
  void			   *data;

在nginx_process.c中

ngx_pid_t
ngx_spawn_process(ngx_cycle_t *cycle, ngx_spawn_proc_pt proc, void *data,
  char *name, ngx_int_t respawn)
{
  u_long	 on;
  ngx_pid_t  pid;
  ngx_int_t  s;


...
   if (socketpair(AF_UNIX, SOCK_STREAM, 0, ngx_processes[s].channel) == -1)  //注意是在fork之前哦
    {
      ngx_log_error(NGX_LOG_ALERT, cycle->log, ngx_errno,
              "socketpair() failed while spawning \"%s\"", name);
      return NGX_INVALID_PID;
    }
...
  pid=fork();
...
在执行fork之前,先调用 socketpair()创建一对socket 描述符存放变量ngx_process[s].channel内,在fork()之后,子进程继承了父进程的这一对socket描述符,Nginx此时会把channel[0]给父进程使用,把channel[1]给子进程使用。从这以后就可以实现父子进程的通信。
if (ngx_add_channel_event(cycle, ngx_channel, NGX_READ_EVENT,
                              ngx_channel_handler)   //子进程把channel加入到监听事件,ngx_channel_handler为事件响应的回调函数
        == NGX_ERROR)
    {

那么子进程和子进程直接是怎么通信的呢?

实际上子进程work_process直接也是通过这写socket进行通信的  : master父进程每次fork一个新进程的时候都会把这个新进程的信息告知前面已经生成的子进程

在ngx_process_cycle.c中

ngx_pass_open_channel(ngx_cycle_t *cycle, ngx_channel_t *ch)
{
  ngx_int_t  i;

  for (i = 0; i < ngx_last_process; i++) {

    if (i == ngx_process_slot
      || ngx_processes[i].pid == -1
      || ngx_processes[i].channel[0] == -1)
    {
      continue;
    }
参数ch包含了刚刚创建的新的子进程pid,进程信息,channel描述符在全局数组中。

这样子进程直接都有了各自的信息包括socket描述符。这样可以彼此可以通过此完成通信。



4、具体实现

nginx的进程启动过程是在ngx_master_process_cycle()函数中实现的,单进程是通过ngx_single_process_cycle()函数中完成,在多进程模型中,会根据配置文件的worker_processes值创建多个子进程,即一个master和多个worker子进程。进程之间、进程与外部之间保持通信,进程之间是通过socketpair进行通信的,进程与外部之间是通过信号通信的。

master进程主要进行一些全局性的初始化工作和管理worker子进程的工作,事件处理是在worker子进程中进行的。进程启动过程中,有些全局数据会被设置,最重要的是进程表ngx_processesmaster进程没创建一个worker子进程,都会把一个设置好的ngx_process_t结构变量放入ngx_processes中,进程表长度是1024

ngx_open_listening_sockets(cycle)

主要功能:读取配置文件,绑定、监听服务端口。

ngx_int_t

ngx_open_listening_sockets(ngx_cycle_t *cycle)

{

   。。。

    for (tries = 5; tries; tries--) {

        failed = 0;

 

        /* for each listening socket */

        ls = cycle->listening.elts;

        for (i = 0; i < cycle->listening.nelts; i++) {

           s = ngx_socket(ls[i].sockaddr->sa_family, ls[i].type, 0);     //socket

 

            if (setsockopt(s, SOL_SOCKET, SO_REUSEADDR,

                           (const void *) &reuseaddr, sizeof(int))   //setsockopt

                == -1)

            {

            }

 

#if (NGX_HAVE_INET6 && defined IPV6_V6ONLY)

            if (ls[i].sockaddr->sa_family == AF_INET6 && ls[i].ipv6only) {

            }

#endif

 

 

           if (bind(s, ls[i].sockaddr, ls[i].socklen) == -1) {            //bind

            }

 

#if (NGX_HAVE_UNIX_DOMAIN)

            if (ls[i].sockaddr->sa_family == AF_UNIX) {

            }

#endif

 

           if (listen(s, ls[i].backlog) == -1) {                       //listen

            }

 

            ls[i].listen = 1;

 

            ls[i].fd = s;

        }//for cycle

 

        if (!failed) {

            break;

        }

 

        ngx_msleep(500);

   } //for tries=5

 

    if (failed) {

        ngx_log_error(NGX_LOG_EMERG, log, 0, "still could not bind()");

        return NGX_ERROR;

    }

 

    return NGX_OK;

}

备注:可以看到ngx_init_cycle()里的ngx_open_listening_sockets()主要功能是socketbindlisten函数的调用,最终创建完的监听套接字就在cycle结构体的listening域里

ngx_master_process_cycle(ngx_cycle_t *cycle)

void

ngx_master_process_cycle(ngx_cycle_t *cycle)

{

   //添加信号集

    sigemptyset(&set);

    sigaddset(&set, SIGCHLD);

    sigaddset(&set, SIGALRM);

    sigaddset(&set, SIGIO);

    sigaddset(&set, SIGINT);

    sigaddset(&set, ngx_signal_value(NGX_RECONFIGURE_SIGNAL));

    sigaddset(&set, ngx_signal_value(NGX_REOPEN_SIGNAL));

    sigaddset(&set, ngx_signal_value(NGX_NOACCEPT_SIGNAL));

    sigaddset(&set, ngx_signal_value(NGX_TERMINATE_SIGNAL));

    sigaddset(&set, ngx_signal_value(NGX_SHUTDOWN_SIGNAL));

    sigaddset(&set, ngx_signal_value(NGX_CHANGEBIN_SIGNAL));

 

    if (sigprocmask(SIG_BLOCK, &set, NULL) == -1) {

        ngx_log_error(NGX_LOG_ALERT, cycle->log, ngx_errno,

                      "sigprocmask() failed");

    }

    sigemptyset(&set);

 

。。。

//根据配置文件,启动子进程,子进程进入自己的事件循环

    ngx_start_worker_processes(cycle, ccf->worker_processes,

                               NGX_PROCESS_RESPAWN);

 

   //master进程进入自己的事件循环,即接收信号、管理worker进程

for ( ;; ) {

       。。。

    }

}

ngx_start_worker_processes(ngx_cycle_t *cycle, ngx_int_t n, ngx_int_t type)

主要功能:创建worker子进程。

static void

ngx_start_worker_processes(ngx_cycle_t *cycle, ngx_int_t n, ngx_int_t type)

{

 

    for (i = 0; i < n; i++) {

        cpu_affinity = ngx_get_cpu_affinity(i);

 

       ngx_spawn_process(cycle, ngx_worker_process_cycle, NULL,

                          "worker process", type);

 

        ch.pid = ngx_processes[ngx_process_slot].pid;

        ch.slot = ngx_process_slot;

        ch.fd = ngx_processes[ngx_process_slot].channel[0];

 

        ngx_pass_open_channel(cycle, &ch);

    }

}

ngx_spawn_process(ngx_cycle_t *cycle, ngx_spawn_proc_pt proc, void *data,char *name, ngx_int_t respawn)

ngx_pid_t

ngx_spawn_process(ngx_cycle_t *cycle, ngx_spawn_proc_pt proc, void *data,

    char *name, ngx_int_t respawn)

{

。。。

   pid = fork();

 

    switch (pid) {

 

    case -1:

        ngx_log_error(NGX_LOG_ALERT, cycle->log, ngx_errno,

                      "fork() failed while spawning \"%s\"", name);

        ngx_close_channel(ngx_processes[s].channel, cycle->log);

        return NGX_INVALID_PID;

 

    case 0:   //子进程

        ngx_pid = ngx_getpid();

        proc(cycle, data);    //子进程进入自己的事件循环

        break;

 

    default:  //父进程

        break;

    }

 

   。。。

}

 

ngx_worker_process_cycle(ngx_cycle_t *cycle, void *data)

Nginx模块开发(12)—进程模型 - cjhust - 我一直在努力

static void

ngx_worker_process_cycle(ngx_cycle_t *cycle, void *data)

{

    ngx_uint_t         i;

    ngx_connection_t  *c;

 

    ngx_process = NGX_PROCESS_WORKER;

    //初始化,并设置子进程title

    ngx_worker_process_init(cycle, 1);

  ngx_setproctitle("worker process");

 

。。。

   //子进程自己的事件循环

    for ( ;; ) {

       //退出状态已设置,关闭所有连接

        if (ngx_exiting) {

            c = cycle->connections;

            for (i = 0; i < cycle->connection_n; i++) {

                if (c[i].fd != -1 && c[i].idle) {

                    c[i].close = 1;

                    c[i].read->handler(c[i].read);

                }

            }

 

            if (ngx_event_timer_rbtree.root == ngx_event_timer_rbtree.sentinel)

            {

                ngx_log_error(NGX_LOG_NOTICE, cycle->log, 0, "exiting");

 

                ngx_worker_process_exit(cycle);

            }

        }

 

        ngx_log_debug0(NGX_LOG_DEBUG_EVENT, cycle->log, 0, "worker cycle");

 

        //处理事件和计时

        ngx_process_events_and_timers(cycle);

 

       。。。

    }

}

备注:worker进程的事件循环就是监听网络事件并处理(如新建连接、断开连接、处理请求、发送响应等),所以真正的连接最终是连到了worker进程,但是worker进程之间是怎么调用accept()函数呢?

所有的worker进程都有监听套接字,都能够accept一个连接,但是nginx准备了一个accept锁,因此所有的子进程在走到处理新连接这一步的时候都要争下accept锁,争到锁的worker进程可以调用accept()并接受新连接。

这样做的目的就是为了防止多个进程同时accept,当一个连接来的时候多个进程同时被唤起,即惊群。

ngx_process_events_and_timers(ngx_cycle_t *cycle)

函数功能:事件循环的核心。

 

Nginx模块开发(12)—进程模型 - cjhust - 我一直在努力

 

void ngx_process_events_and_timers(ngx_cycle_t *cycle)
{
    ngx_uint_t  flags;
    ngx_msec_t  timer, delta;

 //
如果配置文件中设置了时间精度
    if (ngx_timer_resolution) {
        timer = NGX_TIMER_INFINITE;
        flags = 0; 
    } else {
        timer = ngx_event_find_timer();
        flags = NGX_UPDATE_TIME;
        ...
    }    

 // ngx_use_accept_mutex
变量代表是否使用accept互斥体,默认使用,accept_mutex off,指令关闭。accept mutex的作用就是避免惊群,同时实现负载均衡。
    if (ngx_use_accept_mutex) {

 

// ngx_accept_disabled变量在ngx_event_accept函数中计算。如果ngx_accept_disabled大于0,就表示该进程接受的连接过多,因此就放弃一次争抢accept mutex的机会,同时将

自己减1。然后,继续处理已有连接上的事件。nginx就借用此变量实现了进程关于连接的基本负载均衡。
        if (ngx_accept_disabled > 0) { 
            ngx_accept_disabled--;
        } else {


 // 尝试加锁accept mutex,只有成功获取锁的进程,才会将listen套接字放入epool中,因此保证了只有一个进程拥有监听套接口,故所有进程阻塞在epool_wait时,不会出现惊群现象。
            if (ngx_trylock_accept_mutex(cycle) == NGX_ERROR) {
                return;
            }    
           
            if (ngx_accept_mutex_held) {

//获取锁的进程,将添加一个NGX_POST_EVENTS标志,此标志的作用是将所有产生的事件放入一个队列中,等释放锁后,再慢慢来处理事件。因为,处理事件可能会很耗时,如果不先释放锁再处理的话,该进程就长时间霸占了锁,导致其他进程无法获取锁,这样accept

的效率就低了。

        flags |= NGX_POST_EVENTS;
            } else {
                if (timer == NGX_TIMER_INFINITE
                    || timer > ngx_accept_mutex_delay)
                {

//设置最长延迟多久,再去争抢锁

timer = ngx_accept_mutex_delay;
                }
            }
        }
    }

    delta = ngx_current_msec;

 //
调用process_events钩子轮询事件,有些事件即时调用事件处理函数处理,有些事件放入延迟队列等待后面处理,ngx_process_events的具体实现是对应到epoll模块中的ngx_epoll_process_events函数
    (void) ngx_process_events(cycle, timer, flags);

    delta = ngx_current_msec - delta;

    ngx_log_debug1(NGX_LOG_DEBUG_EVENT, cycle->log, 0,
                   "timer delta: %M", delta);

    //
有需要延迟处理的监听套接口事件
    if (ngx_posted_accept_events) {
        ngx_event_process_posted(cycle, &ngx_posted_accept_events);
    }

    //
释放锁
    if (ngx_accept_mutex_held) {
        ngx_shmtx_unlock(&ngx_accept_mutex);
    }
 

//delta是上文对epool wait事件的耗时统计,存在毫秒级的耗时就对所有事件的timer进行检查,如果time out就从timer rbtree中删除到期的timer,同时调用相应事件的handler函数完成处理
    if (delta) {
        ngx_event_expire_timers();
    }

    ngx_log_debug1(NGX_LOG_DEBUG_EVENT, cycle->log, 0,
                   "posted events %p", ngx_posted_events);

    //
有需要延迟处理的数据套接口事件
    if (ngx_posted_events) {
        //
处理
        if (ngx_threaded) {
            ngx_wakeup_worker_thread(cycle);
        } else {
            ngx_event_process_posted(cycle, &ngx_posted_events);
        }
    }
}







体结构如下图所示:







对于服务端来讲,处理并发连接无疑要达到的效果是:高并发,快响应。Nginx架构采用的是Master-Worker的多进程协作模式。所以如何让每个worker进程都平均的处理连接也是一个要考虑的问题。


就上图,listen套接字是Master进程初始化的时候创建的,然后fork子进程的时候自然的继承给子进程的。上代码。


在src/core/nginx.c的main函数里依次有如下两行调用:


1
2
3 cycle = ngx_init_cycle(&init_cycle); 
  
ngx_master_process_cycle(cycle); 


在nginx代码中,一个cycle代表一个进程,所有进程相关变量(包括连接)都在这个结构体里。main函数里先调用ngx_init_cycle来初始化了一个主进程实例,80端口的监听套接字也是在这个函数里创建的:


1
2
3 if (ngx_open_listening_sockets(cycle) != NGX_OK) { 
        goto failed; 



在ngx_open_listening_sockets函数的代码中可以看到bind、listen等套接字函数的调用。最终创建完的监听套接字就在cycle结构体的listening域里。相关代码如下:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16 ls = cycle->listening.elts; 
  
s = ngx_socket(ls[i].sockaddr->sa_family, ls[i].type, 0); 
  
if (setsockopt(s, SOL_SOCKET, SO_REUSEADDR, 
                           (const void *) &reuseaddr, sizeof(int)) 
                == -1) 
            {... 
  
if (bind(s, ls[i].sockaddr, ls[i].socklen) == -1) {... 
  
if (listen(s, ls[i].backlog) == -1) {... 
  
ls[i].listen = 1; 
  
ls[i].fd = s; 


你懂的。


main函数里面调用的ngx_master_process_cycle就是创建worker进程的地方了。


在ngx_master_process_cycle函数里调用了ngx_start_worker_processes(cycle, ccf->worker_processes, NGX_PROCESS_RESPAWN);函数,在ngx_start_worker_processes函数里我们有能看到


1
2
3
4
5
6
7
8
9
10
11
12
13 for (i = 0; i < n; i++) { 
  
cpu_affinity = ngx_get_cpu_affinity(i); 
  
ngx_spawn_process(cycle, ngx_worker_process_cycle, NULL, 
"worker process", type); 
  
ch.pid = ngx_processes[ngx_process_slot].pid; 
ch.slot = ngx_process_slot; 
ch.fd = ngx_processes[ngx_process_slot].channel[0]; 
  
ngx_pass_open_channel(cycle, &ch); 



好了,自此Master的活就干得差不多了,之后Master进程和worker进程纷纷进入自己的事件循环。Master的事件循环就是收收信号,管理管理worker进程,而worker进程的事件循环就是监听网络事件并处理(如新建连接,断开连接,处理请求发送响应等等),所以真正的连接最终是连到了worker进程上的,各个worker进程之间又是怎么接受(调用accept()函数)的呢。所有的worker进程都有监听套接字,都能够accept一个连接,所以,nginx准备了一个accept锁,如图,所有的子进程在走到处理新连接这一步的时候都要争一下这个锁,争到锁的worker进程可以调用accept接受新连接。这样做的目的就是为了防止多个进程同时accept,当一个连接来的时候多个进程同时被唤起——所谓惊群(BTW:据说新版本内核已经没有惊群了,待考证)。

第七章 请求处理
创建监听套接口
前面章节曾陆陆续续的提到过nginx对客户端请求的处理,但不甚连贯,所以本章就尝试把这个请求处理响应过程完整的描述一遍。下面先来看后http请求处理的前置准备工作,也就是监听套接口的创建以及组织等。
创建哪些监听套接口当然是由用户来指定的,nginx提供的配置指令为listen(仅关注http模块:http://wiki.nginx.org/HttpCoreModule#listen),该指令功能非常的丰富,不过在大部分情况下,我们都用得比较简单,一般是指定监听ip和端口号(因为http协议是基于tcp,所以这里自然也就是tcp端口),比如:listen 192.168.1.1:80;,这表示nginx仅监听目的ip是192.168.1.1且端口是80的http请求;如果主机上还有一个192.168.1.2的ip地址,那么客户端对该地址的80端口访问将被拒绝,要让该地址也正常访问需同样把该ip加入:listen 192.168.1.2:80;,如果有更多ip,这样逐个加入比较麻烦,因而另一种更偷懒的配置方法是只指定端口号:listen 80;,那么此时任意目的ip都可以访问到。不过,这两种不同的配置方式会影响到nginx创建监听套接口的数目,前一种方式nginx会对应的创建多个监听套接口,而后一种方式,由于listen 80;包含了所有的目标ip,所以创建一个监听套接口就足以,即便是配置文件里还有listen 192.168.1.1:80;这样的配置。看看实例,感性的认识一下:
30:        Filename : nginx.conf
15:            server {
16:                listen       80;
17:        ...
34:            server {
35:                listen       192.168.1.1:80;
36:        
上面配置中有两个server,第一个配置listen的目标ip为任意(只要是本主机有的),第二个配置listen的目标ip为192.168.1.1,但是nginx在创建监听套接口时却只创建了一个:
[root@localhost html]# netstat -ntap | grep nginx
tcp    0    0    0.0.0.0:80    0.0.0.0:*    LISTEN    13040/nginx
        如果将第16行配置改为listen 192.168.1.2:80;,那么此时的nginx将创建两个监听套接口:
[root@localhost nginx]# netstat -ntpa | grep nginx
tcp    0    0    192.168.1.1:80    0.0.0.0:*    LISTEN    13145/nginx
tcp    0    0    192.168.1.2:80    0.0.0.0:*    LISTEN    13145/nginx
再来看nginx代码的具体实现,配置指令listen的使用上下文为server,其对应的处理函数为ngx_http_core_listen(),该函数本身的功能比较单一,主要是解析listen指令并将对应的结果存到变量lsopt(可能有人注意到这是一个局部变量,不过没关系,在后面的函数调用里通过结构体赋值的方式,将它的值全部复制给另外一个变量addr->opt)内,最后调用函数ngx_http_add_listen(),这才是此处关注的核心函数,它将所有的listen配置以[port,addr]的形式组织在http核心配置ngx_http_core_main_conf_t下的ports数组字段内。另外,如果有一个server内没有配置监听端口,那么nginx会自动创建一个变量lsopt并给出一些默认值,然后调用函数ngx_http_add_listen()将其组织到ports数组字段内,这在server块配置的回调函数ngx_http_core_server()最后可以看到:
2700:        Filename : ngx_http_core_module.c
2701:        static char *
2702:        ngx_http_core_server(ngx_conf_t *cf, ngx_command_t *cmd, void *dummy)
2703:        {
2704:        …
2790:            if (rv == NGX_CONF_OK && !cscf->listen) {
2791:                ngx_memzero(&lsopt, sizeof(ngx_http_listen_opt_t));
2792:        …
2795:                sin->sin_family = AF_INET;
2796:        …
2799:                sin->sin_port = htons((getuid() == 0) ? 80 : 8000);
2800:        #endif
2801:                sin->sin_addr.s_addr = INADDR_ANY;
2802:        …
2816:                if (ngx_http_add_listen(cf, cscf, &lsopt) != NGX_OK) {
2817:        …
默认的设置是ipv4协议族、80或8000端口、任意目的ip,所以不管怎么样,一个server配置至少有一个监听套接口。回过头来看ngx_http_add_listen()函数以及相关逻辑,具体代码并没有什么难以理解的地方,我们直接看一个实例以及对应的图示,这样更直观且能把握全局。仍接着前面的实例,再加一个server配置:
00:        Filename : nginx.conf
15:            server {
16:                listen       80;
17:                server_name  www.other_all.com;
18:        ...
34:            server {
35:                listen       192.168.1.1:80;
36:                server_name  www.web_test1.com;
37:        …
53:            server {
54:                listen       192.168.1.2:8000;
55:                server_name  www.web_test2.com;
56:        …

当nginx的http配置块全部解析完后,所有的监听套接口信息(包括用户主动listen配置或nginx默认添加)都已被收集起来,先按port端口分类形成数组存储在cmcf->ports内,然后再在每一个port内按ip地址分类形成数组存储在port->addrs内,也就是一个[port, addr]的二维划分,如上图所示。附带说一下,其实一个[port, addr]可以对应有多个server配置块,但这里的实例中server配置块只有一个,所以也就是默认配置块default_server;对应server配置块的多少并不会影响到监听套接口的创建逻辑,因为创建监听套接口依赖的是[port, addr]本身,而非它对应的server配置块。回到刚才的思路上,在http配置指令的回调函数ngx_http_block()最后,也就是http配置块全部解析完后,将调用ngx_http_optimize_servers()函数‘创建’对应的监听套接口,之所以打上引号是因为这里还只是名义上的创建,也就只是创建了每个监听套接口所对应的结构体变量ngx_listening_s,并以数组的形式组织在全局变量cycle->listening内,具体的函数调用关系如下:



本篇文章来源于 Linux公社网站(www.linuxidc.com)  原文链接:http://www.linuxidc.com/Linux/2011-09/42248.htm
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值