文章详情

短信预约-IT技能 免费直播动态提醒

请输入下面的图形验证码

提交验证

短信预约提醒成功

nginx中怎么实现一个事件模块

2023-06-19 10:03

关注

nginx中怎么实现一个事件模块,相信很多没有经验的人对此束手无策,为此本文总结了问题出现的原因和解决方法,通过这篇文章希望你能解决这个问题。

1. ngx_events_block()----events配置块解析

        nginx在解析nginx.conf配置文件时,如果当前解析的配置项名称为events,并且是一个配置块,则会调用ngx_events_block()方法解析该配置块,如下是该方法的源码:

static char * ngx_events_block(ngx_conf_t *cf, ngx_command_t *cmd, void *conf) {  char *rv;  void ***ctx;  ngx_uint_t i;  ngx_conf_t pcf;  ngx_event_module_t *m;  // 如果存储事件模块配置数据的配置项不为空,说明已经解析过配置项了,因而直接返回  if (*(void **) conf) {    return "is duplicate";  }  // 这里主要是计算event模块的个数,并且将各个event模块的相对顺序标记在了该模块的ctx_index属性中  ngx_event_max_module = ngx_count_modules(cf->cycle, NGX_EVENT_MODULE);  // 创建一个存储配置项数组的指针  ctx = ngx_pcalloc(cf->pool, sizeof(void *));  if (ctx == NULL) {    return NGX_CONF_ERROR;  }  // 为配置项指针申请数组内存  *ctx = ngx_pcalloc(cf->pool, ngx_event_max_module * sizeof(void *));  if (*ctx == NULL) {    return NGX_CONF_ERROR;  }  // 将数组值赋值到conf中,也即关联到核心配置对象ngx_cycle_t中  *(void **) conf = ctx;  for (i = 0; cf->cycle->modules[i]; i++) {    if (cf->cycle->modules[i]->type != NGX_EVENT_MODULE) {      continue;    }    m = cf->cycle->modules[i]->ctx;    // 如果当前模块的create_conf()方法不为空,则调用该方法创建存储配置项的结构体    if (m->create_conf) {      (*ctx)[cf->cycle->modules[i]->ctx_index] = m->create_conf(cf->cycle);      if ((*ctx)[cf->cycle->modules[i]->ctx_index] == NULL) {        return NGX_CONF_ERROR;      }    }  }  // 这里将*cf结构体进行了复制,临时存储在pcf中,然后初始化当前的*cf结构体的模块相关的参数,  // 以进行下一步的解析  pcf = *cf;  cf->ctx = ctx;  cf->module_type = NGX_EVENT_MODULE;  cf->cmd_type = NGX_EVENT_CONF;  // 解析events{}配置块中的子配置项  rv = ngx_conf_parse(cf, NULL);  // 重新将pcf复制给*cf,以供后面返回使用  *cf = pcf;  if (rv != NGX_CONF_OK) {    return rv;  }  // 到这里,说明events{}配置块的配置项都解析完成了,因而这里调用各个模块的init_conf()方法,  // 进行配置项的初始化和合并工作  for (i = 0; cf->cycle->modules[i]; i++) {    if (cf->cycle->modules[i]->type != NGX_EVENT_MODULE) {      continue;    }    m = cf->cycle->modules[i]->ctx;    // 如果当前模块的init_conf()不为空,则调用其init_conf()方法初始化配置项    if (m->init_conf) {      rv = m->init_conf(cf->cycle, (*ctx)[cf->cycle->modules[i]->ctx_index]);      if (rv != NGX_CONF_OK) {        return rv;      }    }  }  return NGX_CONF_OK;}

        ngx_events_block()方法主要完成的工作有如下几个:

2. ngx_event_init_conf()----检查事件模块配置结构体是否正常创建

        在nginx解析完nginx.conf配置文件的所有配置项后(包括前一步中讲解的对events配置项的解析),其就会调用所有核心模块的init_conf()方法对核心模块的配置项进行初始化。这里的核心模块就包括ngx_events_module,该模块的init_conf()方法指向的就是这里的ngx_event_init_conf()方法,该方法本质上并没有做什么工作,只是检查了是否创建了存储事件模块配置项的结构体数组。如下是ngx_event_init_conf()方法的源码:

static char *ngx_event_init_conf(ngx_cycle_t *cycle, void *conf) {  if (ngx_get_conf(cycle->conf_ctx, ngx_events_module) == NULL) {    ngx_log_error(NGX_LOG_EMERG, cycle->log, 0,                  "no \"events\" section in configuration");    return NGX_CONF_ERROR;  }  return NGX_CONF_OK;}

        上面两个方法就是ngx_events_module核心模块的两个主要的配置方法,可以看到,这个核心模块的主要作用就是创建了一个数组,用于存储各个事件模块的配置结构体的。下面我们来看一下事件核心模块的主要方法。

3. ngx_event_core_create_conf()----创建事件核心模块配置结构体

        在第1点中我们讲到,解析events配置块的子配置项之前,会调用各个事件模块的create_conf()方法来创建其使用的存储配置数据的结构体,而后调用ngx_conf_parse()方法来解析子配置项,接着调用各个事件模块的init_conf()方法初始化各个模块配置数据的结构体。这里ngx_event_core_module_ctx就是一个事件类型的模块,其create_conf属性指向的就是ngx_event_core_create_conf()方法,而init_conf属性指向的就是ngx_event_core_init_conf()方法。这一节我们首先讲解ngx_event_core_create_conf()方法的实现原理:

static void *ngx_event_core_create_conf(ngx_cycle_t *cycle) {  ngx_event_conf_t *ecf;  ecf = ngx_palloc(cycle->pool, sizeof(ngx_event_conf_t));  if (ecf == NULL) {    return NULL;  }  ecf->connections = NGX_CONF_UNSET_UINT;  ecf->use = NGX_CONF_UNSET_UINT;  ecf->multi_accept = NGX_CONF_UNSET;  ecf->accept_mutex = NGX_CONF_UNSET;  ecf->accept_mutex_delay = NGX_CONF_UNSET_MSEC;  ecf->name = (void *) NGX_CONF_UNSET;  return ecf;}

        可以看到,这里的ngx_event_core_create_conf()方法本质上就是创建了一个ngx_event_conf_t结构体,并且将各个属性都设置为未设置状态。

4. ngx_event_core_init_conf()----初始化配置结构体

        前面我们讲到,在解析完各个子配置项之后,nginx会调用各个事件模块的init_conf()方法,这里的核心事件模块就是这个ngx_event_core_init_conf()方法,如下是该方法的源码:

static char * ngx_event_core_init_conf(ngx_cycle_t *cycle, void *conf) {  ngx_event_conf_t *ecf = conf;#if (NGX_HAVE_EPOLL) && !(NGX_TEST_BUILD_EPOLL)  int                  fd;#endif  ngx_int_t i;  ngx_module_t *module;  ngx_event_module_t *event_module;  module = NULL;#if (NGX_HAVE_EPOLL) && !(NGX_TEST_BUILD_EPOLL)  // 测试是否具有创建epoll句柄的权限  fd = epoll_create(100);  if (fd != -1) {    // 关闭创建的epoll句柄,并且将module指向epoll模块      (void) close(fd);      module = &ngx_epoll_module;  } else if (ngx_errno != NGX_ENOSYS) {      module = &ngx_epoll_module;  }#endif  // 这里,如果没有前面判断的模块类型,则默认使用事件模块中的第一个模块作为事件处理模型  if (module == NULL) {    for (i = 0; cycle->modules[i]; i++) {      if (cycle->modules[i]->type != NGX_EVENT_MODULE) {        continue;      }      event_module = cycle->modules[i]->ctx;      if (ngx_strcmp(event_module->name->data, event_core_name.data) == 0) {        continue;      }      module = cycle->modules[i];      break;    }  }  // 如果此时module还是为NULL,则返回异常  if (module == NULL) {    ngx_log_error(NGX_LOG_EMERG, cycle->log, 0, "no events module found");    return NGX_CONF_ERROR;  }  // 下面的操作主要是判断各个属性是否为初始设置的无效值,如果是,则说明nginx.conf中没有配置  // 关于该属性的配置项,那么这里就会为该属性设置默认值  ngx_conf_init_uint_value(ecf->connections, DEFAULT_CONNECTIONS);  cycle->connection_n = ecf->connections;  ngx_conf_init_uint_value(ecf->use, module->ctx_index);  event_module = module->ctx;  ngx_conf_init_ptr_value(ecf->name, event_module->name->data);  ngx_conf_init_value(ecf->multi_accept, 0);  ngx_conf_init_value(ecf->accept_mutex, 0);  ngx_conf_init_msec_value(ecf->accept_mutex_delay, 500);  return NGX_CONF_OK;}

        ngx_event_core_init_conf()方法的主要做了两件事:

5. ngx_event_module_init()----核心模块的配置项初始化

        对于ngx_event_core_module模块而言,其还指定了两个方法,一个是用于初始化模块的ngx_event_module_init()方法,另一个是用于worker进程执行主循环逻辑之前进行调用的ngx_event_process_init()方法。ngx_event_module_init()方法是在master进程中调用的,其会在解析完nginx.conf文件中的所有配置项之后调用,本质上,该方法的作用就是对当前配置的核心模块(事件模块)进行初始化。如下是ngx_event_module_init()方法的源码:

static ngx_int_t ngx_event_module_init(ngx_cycle_t *cycle) {  void ***cf;  u_char *shared;  size_t size, cl;  ngx_shm_t shm;  ngx_time_t *tp;  ngx_core_conf_t *ccf;  ngx_event_conf_t *ecf;  // 获取core event module的配置结构体  cf = ngx_get_conf(cycle->conf_ctx, ngx_events_module);  ecf = (*cf)[ngx_event_core_module.ctx_index];  if (!ngx_test_config && ngx_process <= NGX_PROCESS_MASTER) {    ngx_log_error(NGX_LOG_NOTICE, cycle->log, 0,                  "using the \"%s\" event method", ecf->name);  }  // 获取core module的配置对象  ccf = (ngx_core_conf_t *) ngx_get_conf(cycle->conf_ctx, ngx_core_module);  ngx_timer_resolution = ccf->timer_resolution;#if !(NGX_WIN32)  {    ngx_int_t limit;    struct rlimit rlmt;    if (getrlimit(RLIMIT_NOFILE, &rlmt) == -1) {      ngx_log_error(NGX_LOG_ALERT, cycle->log, ngx_errno,                    "getrlimit(RLIMIT_NOFILE) failed, ignored");    } else {      // 这里主要是检查当前事件模块配置的connections数目是否超过了操作系统限制的最大文件句柄数,      // 或者超过了配置文件中指定的最大文件句柄数      if (ecf->connections > (ngx_uint_t) rlmt.rlim_cur          && (ccf->rlimit_nofile == NGX_CONF_UNSET              || ecf->connections > (ngx_uint_t) ccf->rlimit_nofile)) {        limit = (ccf->rlimit_nofile == NGX_CONF_UNSET) ?                (ngx_int_t) rlmt.rlim_cur : ccf->rlimit_nofile;        ngx_log_error(NGX_LOG_WARN, cycle->log, 0,                      "%ui worker_connections exceed "                      "open file resource limit: %i",                      ecf->connections, limit);      }    }  }#endif   if (ccf->master == 0) {    return NGX_OK;  }  if (ngx_accept_mutex_ptr) {    return NGX_OK;  }    cl = 128;  size = cl                     + cl                   + cl;         #if (NGX_STAT_STUB)  size += cl                    + cl                   + cl                   + cl                   + cl                   + cl                   + cl;         #endif  // 设置共享内存的大小  shm.size = size;  ngx_str_set(&shm.name, "nginx_shared_zone");  shm.log = cycle->log;  // 为共享内存结构体申请内存块  if (ngx_shm_alloc(&shm) != NGX_OK) {    return NGX_ERROR;  }  // addr就是申请的共享内存块的地址  shared = shm.addr;  ngx_accept_mutex_ptr = (ngx_atomic_t *) shared;  ngx_accept_mutex.spin = (ngx_uint_t) -1;  if (ngx_shmtx_create(&ngx_accept_mutex, (ngx_shmtx_sh_t *) shared, cycle->lock_file.data) != NGX_OK) {    return NGX_ERROR;  }  // 获取ngx_connection_counter的地址  ngx_connection_counter = (ngx_atomic_t *) (shared + 1 * cl);  // 将ngx_connection_counter的值设置为1  (void) ngx_atomic_cmp_set(ngx_connection_counter, 0, 1);  ngx_log_debug2(NGX_LOG_DEBUG_EVENT, cycle->log, 0,                 "counter: %p, %uA",                 ngx_connection_counter, *ngx_connection_counter);  // 获取ngx_temp_number的地址  ngx_temp_number = (ngx_atomic_t *) (shared + 2 * cl);  tp = ngx_timeofday();  // 生成一个随机数  ngx_random_number = (tp->msec << 16) + ngx_pid;#if (NGX_STAT_STUB)  ngx_stat_accepted = (ngx_atomic_t *) (shared + 3 * cl);  ngx_stat_handled = (ngx_atomic_t *) (shared + 4 * cl);  ngx_stat_requests = (ngx_atomic_t *) (shared + 5 * cl);  ngx_stat_active = (ngx_atomic_t *) (shared + 6 * cl);  ngx_stat_reading = (ngx_atomic_t *) (shared + 7 * cl);  ngx_stat_writing = (ngx_atomic_t *) (shared + 8 * cl);  ngx_stat_waiting = (ngx_atomic_t *) (shared + 9 * cl);#endif  return NGX_OK;}

        ngx_event_module_init()方法主要完成的工作有如下几个:

6. ngx_event_process_init()----初始化worker进程

        ngx_event_process_init()方法主要是在worker进程执行主循环之前进行初始化调用的,如下是该方法的源码:

static ngx_int_t ngx_event_process_init(ngx_cycle_t *cycle) {  ngx_uint_t m, i;  ngx_event_t *rev, *wev;  ngx_listening_t *ls;  ngx_connection_t *c, *next, *old;  ngx_core_conf_t *ccf;  ngx_event_conf_t *ecf;  ngx_event_module_t *module;  // 获取核心模块的配置对象  ccf = (ngx_core_conf_t *) ngx_get_conf(cycle->conf_ctx, ngx_core_module);  // 获取事件核心模块的配置对象  ecf = ngx_event_get_conf(cycle->conf_ctx, ngx_event_core_module);  // 判断当前如果满足三个条件,则标记当前为使用共享锁的方式:  // 1. 当前为master-worker模式;  // 2. 当前worker进程的数量大于1;  // 3. 当前打开了使用共享锁的开关;  if (ccf->master && ccf->worker_processes > 1 && ecf->accept_mutex) {    ngx_use_accept_mutex = 1;    ngx_accept_mutex_held = 0;    ngx_accept_mutex_delay = ecf->accept_mutex_delay;  } else {    // 如果不满足上述条件,则指定不使用共享锁    ngx_use_accept_mutex = 0;  }#if (NGX_WIN32)    ngx_use_accept_mutex = 0;#endif  // 这里这两个队列的主要作用在于,每个worker进程在获取到共享锁之后,就会接收客户端accept事件,  // 然后将其放入到ngx_posted_accept_events队列中,接着处理该队列中的事件,并且将客户端连接添加到  // ngx_posted_events队列中,然后再释放锁,也就是说获取锁的worker进程只需要进行accept客户端连接,  // 然后将锁的权限交给其他的进程,并且再自行处理接收到的连接的读写事件  // 创建ngx_posted_accept_events队列,该队列用于接收客户端的连接事件  ngx_queue_init(&ngx_posted_accept_events);  // 创建ngx_posted_events队列,该队列用于处理客户端连接的读写事件  ngx_queue_init(&ngx_posted_events);  // 初始化一个用于存储事件的红黑树  if (ngx_event_timer_init(cycle->log) == NGX_ERROR) {    return NGX_ERROR;  }  for (m = 0; cycle->modules[m]; m++) {    if (cycle->modules[m]->type != NGX_EVENT_MODULE) {      continue;    }    // ecf->use存储了所选用的事件模型的模块序号,这里是找到该模块    if (cycle->modules[m]->ctx_index != ecf->use) {      continue;    }    // module即为所选用的事件模型对应的模块    module = cycle->modules[m]->ctx;    // 调用指定事件模型的初始化方法    if (module->actions.init(cycle, ngx_timer_resolution) != NGX_OK) {            exit(2);    }    break;  }#if !(NGX_WIN32)  // ngx_timer_resolution表示发送更新时间事件的时间间隔  // 这里表示如果设置了ngx_timer_resolution,并且没有设置定时事件。  // ngx_event_flags是在事件模块的初始化中设置的,而且只有eventport和kqueue模型才会将  // NGX_USE_TIMER_EVENT设置到ngx_event_flags中  if (ngx_timer_resolution && !(ngx_event_flags & NGX_USE_TIMER_EVENT)) {    struct sigaction sa;    struct itimerval itv;    ngx_memzero(&sa, sizeof(struct sigaction));    // 这里的sa主要是添加下面的SIGALRM的信号监听事件,该信号的作用是每隔一段时间就会向当前进程发出    // 当前进程收到信号之后就会调用下面的ngx_timer_signal_handler()方法,该方法中会将    // ngx_event_timer_alarm设置为1,而后当前进程在进行事件循环的时候,判断如果    // ngx_event_timer_alarm为1,则会更新当前进程所缓存的时间数据    sa.sa_handler = ngx_timer_signal_handler;    sigemptyset(&sa.sa_mask);    // 添加SIGALRM监听信号    if (sigaction(SIGALRM, &sa, NULL) == -1) {      ngx_log_error(NGX_LOG_ALERT, cycle->log, ngx_errno,                    "sigaction(SIGALRM) failed");      return NGX_ERROR;    }    // 设置时间间隔相关参数    itv.it_interval.tv_sec = ngx_timer_resolution / 1000;    itv.it_interval.tv_usec = (ngx_timer_resolution % 1000) * 1000;    itv.it_value.tv_sec = ngx_timer_resolution / 1000;    itv.it_value.tv_usec = (ngx_timer_resolution % 1000) * 1000;    // 按照指定的时间间隔设置定时器    if (setitimer(ITIMER_REAL, &itv, NULL) == -1) {      ngx_log_error(NGX_LOG_ALERT, cycle->log, ngx_errno,                    "setitimer() failed");    }  }  // NGX_USE_FD_EVENT表示event filter没有透明数据,并需要一个文件描述符表,其主要用于poll、/dev/poll  if (ngx_event_flags & NGX_USE_FD_EVENT) {    struct rlimit rlmt;    if (getrlimit(RLIMIT_NOFILE, &rlmt) == -1) {      ngx_log_error(NGX_LOG_ALERT, cycle->log, ngx_errno,                    "getrlimit(RLIMIT_NOFILE) failed");      return NGX_ERROR;    }    // 这里主要是初始化最大个数的ngx_connection_t结构体,将其保存在files数组中    cycle->files_n = (ngx_uint_t) rlmt.rlim_cur;    cycle->files = ngx_calloc(sizeof(ngx_connection_t *) * cycle->files_n, cycle->log);    if (cycle->files == NULL) {      return NGX_ERROR;    }  }#else  if (ngx_timer_resolution && !(ngx_event_flags & NGX_USE_TIMER_EVENT)) {      ngx_log_error(NGX_LOG_WARN, cycle->log, 0,                    "the \"timer_resolution\" directive is not supported "                    "with the configured event method, ignored");      ngx_timer_resolution = 0;  }#endif  // 申请指定个数的ngx_connection_t数组,这里的connection_n对应的是配置  // 文件中的worker_connections所指定的大小  cycle->connections = ngx_alloc(sizeof(ngx_connection_t) * cycle->connection_n, cycle->log);  if (cycle->connections == NULL) {    return NGX_ERROR;  }  c = cycle->connections;  // 申请指定个数的ngx_event_t数组,其长度与connections数组一致,  // 这样便可以将connections数组与read_events数组进行对应  cycle->read_events = ngx_alloc(sizeof(ngx_event_t) * cycle->connection_n, cycle->log);  if (cycle->read_events == NULL) {    return NGX_ERROR;  }  rev = cycle->read_events;  for (i = 0; i < cycle->connection_n; i++) {    rev[i].closed = 1;  // 初始状态默认读事件都是closed状态    rev[i].instance = 1;  // 初始时初始化instance为1  }  // 申请指定个数的ngx_event_t数组,其长度与connections数组一致,  // 这样便可以将connections数组与write_events数组进行对应  cycle->write_events = ngx_alloc(sizeof(ngx_event_t) * cycle->connection_n, cycle->log);  if (cycle->write_events == NULL) {    return NGX_ERROR;  }  wev = cycle->write_events;  for (i = 0; i < cycle->connection_n; i++) {    wev[i].closed = 1;  // 初始时写事件默认也都是closed状态  }  i = cycle->connection_n;  next = NULL;  do {    i--;    // 将read_events和write_events数组的元素依次赋值到connections数组元素的read和write属性中,    // 并且将connections数组组装成一个单链表    c[i].data = next;    c[i].read = &cycle->read_events[i];    c[i].write = &cycle->write_events[i];    c[i].fd = (ngx_socket_t) -1;    next = &c[i];  } while (i);  // 初始状态时,所有的connections都未被使用,因而需要存储在free_connections链表中  cycle->free_connections = next;  cycle->free_connection_n = cycle->connection_n;    ls = cycle->listening.elts;  for (i = 0; i < cycle->listening.nelts; i++) {#if (NGX_HAVE_REUSEPORT)    if (ls[i].reuseport && ls[i].worker != ngx_worker) {      continue;    }#endif    // 这里是为当前所监听的每一个端口都绑定一个ngx_connection_t结构体    c = ngx_get_connection(ls[i].fd, cycle->log);    if (c == NULL) {      return NGX_ERROR;    }    c->type = ls[i].type;    c->log = &ls[i].log;    c->listening = &ls[i];    ls[i].connection = c;    rev = c->read;    rev->log = c->log;    // 标记accept为1,表示当前可以接收客户端的连接事件    rev->accept = 1;#if (NGX_HAVE_DEFERRED_ACCEPT)    rev->deferred_accept = ls[i].deferred_accept;#endif    if (!(ngx_event_flags & NGX_USE_IOCP_EVENT)) {      if (ls[i].previous) {                // 删除旧的事件        old = ls[i].previous->connection;        if (ngx_del_event(old->read, NGX_READ_EVENT, NGX_CLOSE_EVENT) == NGX_ERROR) {          return NGX_ERROR;        }        old->fd = (ngx_socket_t) -1;      }    }#if (NGX_WIN32)    if (ngx_event_flags & NGX_USE_IOCP_EVENT) {        ngx_iocp_conf_t  *iocpcf;        rev->handler = ngx_event_acceptex;        if (ngx_use_accept_mutex) {            continue;        }        if (ngx_add_event(rev, 0, NGX_IOCP_ACCEPT) == NGX_ERROR) {            return NGX_ERROR;        }        ls[i].log.handler = ngx_acceptex_log_error;        iocpcf = ngx_event_get_conf(cycle->conf_ctx, ngx_iocp_module);        if (ngx_event_post_acceptex(&ls[i], iocpcf->post_acceptex)            == NGX_ERROR)        {            return NGX_ERROR;        }    } else {        rev->handler = ngx_event_accept;        if (ngx_use_accept_mutex) {            continue;        }        if (ngx_add_event(rev, NGX_READ_EVENT, 0) == NGX_ERROR) {            return NGX_ERROR;        }    }#else    // SOCK_STREAM表示TCP,一般都是TCP,也就是说在接收到客户端的accept事件之后,    // 就会调用ngx_event_accept()方法处理该事件    rev->handler = (c->type == SOCK_STREAM) ? ngx_event_accept                                            : ngx_event_recvmsg;#if (NGX_HAVE_REUSEPORT)    // 添加当前事件到事件监听队列中    if (ls[i].reuseport) {      if (ngx_add_event(rev, NGX_READ_EVENT, 0) == NGX_ERROR) {        return NGX_ERROR;      }      continue;    }#endif    if (ngx_use_accept_mutex) {      continue;    }#if (NGX_HAVE_EPOLLEXCLUSIVE)    if ((ngx_event_flags & NGX_USE_EPOLL_EVENT)        && ccf->worker_processes > 1)    {        if (ngx_add_event(rev, NGX_READ_EVENT, NGX_EXCLUSIVE_EVENT)            == NGX_ERROR)        {            return NGX_ERROR;        }        continue;    }#endif    // 添加当前事件到事件监听队列中    if (ngx_add_event(rev, NGX_READ_EVENT, 0) == NGX_ERROR) {      return NGX_ERROR;    }#endif  }  return NGX_OK;}

        这里ngx_event_process_init()方法主要完成了如下几个工作:

看完上述内容,你们掌握nginx中怎么实现一个事件模块的方法了吗?如果还想学到更多技能或想了解更多相关内容,欢迎关注编程网行业资讯频道,感谢各位的阅读!

阅读原文内容投诉

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

软考中级精品资料免费领

  • 历年真题答案解析
  • 备考技巧名师总结
  • 高频考点精准押题
  • 2024年上半年信息系统项目管理师第二批次真题及答案解析(完整版)

    难度     813人已做
    查看
  • 【考后总结】2024年5月26日信息系统项目管理师第2批次考情分析

    难度     354人已做
    查看
  • 【考后总结】2024年5月25日信息系统项目管理师第1批次考情分析

    难度     318人已做
    查看
  • 2024年上半年软考高项第一、二批次真题考点汇总(完整版)

    难度     435人已做
    查看
  • 2024年上半年系统架构设计师考试综合知识真题

    难度     224人已做
    查看

相关文章

发现更多好内容

猜你喜欢

AI推送时光机
位置:首页-资讯-后端开发
咦!没有更多了?去看看其它编程学习网 内容吧
首页课程
资料下载
问答资讯