libevent (1) 入门教程:Echo Server based on libevent

http://felix021.com/blog/read.php?2068

花了两天的时间在libevent上,想总结下,就以写简单tutorial的方式吧,貌似没有一篇简单的说明,让人马上就能上手用的。


首先给出官方文档吧:  http://libevent.org  ,首页有个Programming with Libevent,里面是一节一节的介绍libevent,但是感觉信息量太大了,而且还是英文的-。-(当然,如果想好好用libevent,看看还是很有必要的),还有个Reference,大致就是对各个版本的libevent使用doxgen生成的文档,用来查函数原型和基本用法什么的。

下面假定已经学习过基本的socket编程(socket,bind,listen,accept,connect,recv,send,close),并且对异步/callback有基本认识。

基本的socket编程是阻塞/同步的,每个操作除非已经完成或者出错才会返回,这样对于每一个请求,要使用一个线程或者单独的进程去处理,系统资源没法支撑大量的请求(所谓c10k problem?),例如内存:默认情况下每个线程需要占用2~8M的栈空间。posix定义了可以使用异步的select系统调用,但是因为其采用了轮询的方式来判断某个fd是否变成active,效率不高[O(n)],连接数一多,也还是撑不住。于是各系统分别提出了基于异步/callback的系统调用,例如Linux的epoll,BSD的kqueue,Windows的IOCP。由于在内核层面做了支持,所以可以用O(1)的效率查找到active的fd。基本上,libevent就是对这些高效IO的封装,提供统一的API,简化开发。

libevent大概是这样的:

    默认情况下是单线程的(可以配置成多线程,如果有需要的话),每个线程有且只有一个event_base,对应一个struct event_base结构体(以及附于其上的事件管理器),用来schedule托管给它的一系列event,可以和操作系统的进程管理类比,当然,要更简单一点。当一个事件发生后,event_base会在合适的时间(不一定是立即)去调用绑定在这个事件上的函数(传入一些预定义的参数,以及在绑定时指定的一个参数),直到这个函数执行完,再返回schedule其他事件。
//创建一个event_base
struct event_base *base = event_base_new();
assert(base !=  NULL);


    event_base内部有一个循环,循环阻塞在epoll/kqueue等系统调用上,直到有一个/一些事件发生,然后去处理这些事件。当然,这些事件要被绑定在这个event_base上。每个事件对应一个struct event,可以是监听一个fd或者POSIX信号量之类(这里只讲fd了,其他的看manual吧)。struct event使用event_new来创建和绑定,使用event_add来启用:
//创建并绑定一个event
struct event *listen_event;
//参数:event_base, 监听的fd,事件类型及属性,绑定的回调函数,给回调函数的参数
listen_event = event_new(base, listener, EV_READ|EV_PERSIST, callback_func, ( void*)base);
//参数:event,超时时间( struct timeval *类型的, NULL表示无超时设置)
event_add(listen_event,  NULL);

    注:libevent支持的事件及属性包括(使用bitfield实现,所以要用 | 来让它们合体)
    (a) EV_TIMEOUT: 超时
    (b) EV_READ: 只要网络缓冲中还有数据,回调函数就会被触发
    (c) EV_WRITE: 只要塞给网络缓冲的数据被写完,回调函数就会被触发
    (d) EV_SIGNAL: POSIX信号量,参考manual吧
    (e) EV_PERSIST: 不指定这个属性的话,回调函数被触发后事件会被删除
    (f) EV_ET: Edge-Trigger边缘触发,参考EPOLL_ET


    然后需要启动event_base的循环,这样才能开始处理发生的事件。循环的启动使用event_base_dispatch,循环将一直持续,直到不再有需要关注的事件,或者是遇到event_loopbreak()/event_loopexit()函数。
//启动事件循环
event_base_dispatch(base);


    接下来关注下绑定到event的回调函数callback_func:传递给它的是一个socket fd、一个event类型及属性bit_field、以及传递给event_new的最后一个参数(去上面几行回顾一下,把event_base给传进来了,实际上更多地是分配一个结构体,把相关的数据都撂进去,然后丢给event_new,在这里就能取得到了)。其原型是:
typedef  void(* event_callback_fn)(evutil_socket_t sockfd,  short event_type,  void *arg)


    对于一个服务器而言,上面的流程大概是这样组合的:
    1. listener = socket(),bind(),listen(),设置nonblocking(POSIX系统中可使用fcntl设置,windows不需要设置,实际上libevent提供了统一的包装evutil_make_socket_nonblocking)
    2. 创建一个event_base
    3. 创建一个event,将该socket托管给event_base,指定要监听的事件类型,并绑定上相应的回调函数(及需要给它的参数)。对于listener socket来说,只需要监听EV_READ|EV_PERSIST
    4. 启用该事件
    5. 进入事件循环
    ---------------
    6. (异步) 当有client发起请求的时候,调用该回调函数,进行处理。

    问题:为什么不在listen完马上调用accept,获得客户端连接以后再丢给event_base呢?这个问题先想想噢。

    回调函数要做什么事情呢?当然是处理client的请求了。首先要accept,获得一个可以与client通信的sockfd,然后……调用recv/send吗?错!大错特错!如果直接调用recv/send的话,这个线程就阻塞在这个地方了,如果这个客户端非常的阴险(比如一直不发消息,或者网络不好,老是丢包),libevent就只能等它,没法处理其他的请求了——所以应该创建一个新的event来托管这个sockfd。

    在老版本libevent上的实现,比较罗嗦[如果不想详细了解的话,看下一部分]。
    对于服务器希望先从client获取数据的情况,大致流程是这样的:
    1. 将这个sockfd设置为nonblocking
    2. 创建2个event:
        event_read,绑上sockfd的EV_READ|EV_PERSIST,设置回调函数和参数(后面提到的struct)
        event_write,绑上sockfd的EV_WRITE|EV_PERSIST,设置回调函数和参数(后面提到的struct)
    3. 启用event_read事件
    ------
    4. (异步) 等待event_read事件的发生, 调用相应的回调函数。这里麻烦来了:回调函数用recv读入的数据,不能直接用send丢给sockfd了事——因为sockfd是nonblocking的,丢给它的话,不能保证正确(为什么呢?)。所以需要一个自己管理的缓存用来保存读入的数据中(在accept以后就创建一个struct,作为第2步回调函数的arg传进来),在合适的时间(比如遇到换行符)启用event_write事件【event_add(event_write, NULL)】,等待EV_WRITE事件的触发
    ------
    5. (异步) 当event_write事件的回调函数被调用的时候,往sockfd写入数据,然后删除event_write事件【event_del(event_write)】,等待event_read事件的下一次执行。
    以上步骤比较晦涩,具体代码可参考 官方文档 里面的【Example: A low-level ROT13 server with Libevent】


    由于需要自己管理缓冲区,且过程晦涩难懂,并且不兼容于Windows的IOCP,所以libevent2开始,提供了bufferevent这个神器,用来提供更加优雅、易用的API。struct bufferevent内建了两个event(read/write)和对应的缓冲区【struct evbuffer *input, *output】,并提供相应的函数用来操作缓冲区(或者直接操作bufferevent)。每当有数据被读入input的时候,read_cb函数被调用;每当output被输出完的时候,write_cb被调用;在网络IO操作出现错误的情况(连接中断、超时、其他错误),error_cb被调用。于是上一部分的步骤被简化为:
    1. 设置sockfd为nonblocking
    2. 使用bufferevent_socket_new创建一个struct bufferevent *bev,关联该sockfd,托管给event_base
    3. 使用bufferevent_setcb(bev, read_cb, write_cb, error_cb, (void *)arg)将EV_READ/EV_WRITE对应的函数
    4. 使用bufferevent_enable(bev, EV_READ|EV_WRITE|EV_PERSIST)来启用read/write事件
    ------
    5. (异步)
        在read_cb里面从input读取数据,处理完毕后塞到output里(会被自动写入到sockfd)
        在write_cb里面(需要做什么吗?对于一个echo server来说,read_cb就足够了)
        在error_cb里面处理遇到的错误
    *. 可以使用bufferevent_set_timeouts(bev, struct timeval *READ, struct timeval *WRITE)来设置读写超时, 在error_cb里面处理超时。
    *. read_cb和write_cb的原型是
        void read_or_write_callback(struct bufferevent *bev, void *arg)
      error_cb的原型是
        void error_cb(struct bufferevent *bev, short error, void *arg) //这个是event的标准回调函数原型
      可以从bev中用libevent的API提取出event_base、sockfd、input/output等相关数据,详情RTFM~
    

    于是代码简化到只需要几行的read_cb和error_cb函数即可:
void read_cb( struct bufferevent *bev,  void *arg) {
     char line[256];
     int n;
    evutil_socket_t fd = bufferevent_getfd(bev);
     while (n = bufferevent_read(bev, line, 256), n > 0)
        bufferevent_write(bev, line, n);
}

void error_cb( struct bufferevent *bev,  short event,  void *arg) {
    bufferevent_free(bev);
}


    于是一个支持大并发量的echo server就成型了!下面附上无注释的echo server源码,110行,多抄几遍,就能完全弄懂啦!更复杂的例子参见 官方文档 里面的【Example: A simpler ROT13 server with Libevent】
# include <stdio.h>
# include <stdlib.h>
# include <errno.h>
# include <assert.h>

# include <event2/event.h>
# include <event2/bufferevent.h>

# define LISTEN_PORT 9999
# define LISTEN_BACKLOG 32

void do_accept(evutil_socket_t listener,  short event,  void *arg);
void read_cb( struct bufferevent *bev,  void *arg);
void error_cb( struct bufferevent *bev,  short event,  void *arg);
void write_cb( struct bufferevent *bev,  void *arg);

int main( int argc,  char *argv[])
{
     int ret;
    evutil_socket_t listener;
    listener = socket(AF_INET, SOCK_STREAM, 0);
    assert(listener > 0);
    evutil_make_listen_socket_reuseable(listener);

     struct sockaddr_in sin;
    sin.sin_family = AF_INET;
    sin.sin_addr.s_addr = 0;
    sin.sin_port = htons(LISTEN_PORT);

     if (bind(listener, ( struct sockaddr *)&sin,  sizeof(sin)) < 0) {
        perror("bind");
         return 1;
    }

     if (listen(listener, LISTEN_BACKLOG) < 0) {
        perror("listen");
         return 1;
    }

     printf ("Listening...\n");

    evutil_make_socket_nonblocking(listener);

     struct event_base *base = event_base_new();
    assert(base !=  NULL);
     struct event *listen_event;
    listen_event = event_new(base, listener, EV_READ|EV_PERSIST, do_accept, ( void*)base);
    event_add(listen_event,  NULL);
    event_base_dispatch(base);

     printf("The End.");
     return 0;
}

void do_accept(evutil_socket_t listener,  short event,  void *arg)
{
     struct event_base *base = ( struct event_base *)arg;
    evutil_socket_t fd;
     struct sockaddr_in sin;
    socklen_t slen;
    fd = accept(listener, ( struct sockaddr *)&sin, &slen);
     if (fd < 0) {
        perror("accept");
         return;
    }
     if (fd > FD_SETSIZE) {
        perror("fd > FD_SETSIZE\n");
         return;
    }

     printf("ACCEPT: fd = %u\n", fd);

     struct bufferevent *bev = bufferevent_socket_new(base, fd, BEV_OPT_CLOSE_ON_FREE);
    bufferevent_setcb(bev, read_cb,  NULL, error_cb, arg);
    bufferevent_enable(bev, EV_READ|EV_WRITE|EV_PERSIST);
}

void read_cb( struct bufferevent *bev,  void *arg)
{
# define MAX_LINE    256
     char line[MAX_LINE+1];
     int n;
    evutil_socket_t fd = bufferevent_getfd(bev);

     while (n = bufferevent_read(bev, line, MAX_LINE), n > 0) {
        line[n] = '\0';
         printf("fd=%u, read line: %s\n", fd, line);

        bufferevent_write(bev, line, n);
    }
}

void write_cb( struct bufferevent *bev,  void *arg) {}

void error_cb( struct bufferevent *bev,  short event,  void *arg)
{
    evutil_socket_t fd = bufferevent_getfd(bev);
     printf("fd = %u, ", fd);
     if (event & BEV_EVENT_TIMEOUT) {
         printf("Timed out\n"); // if bufferevent_set_timeouts() called
    }
     else  if (event & BEV_EVENT_EOF) {
         printf("connection closed\n");
    }
     else  if (event & BEV_EVENT_ERROR) {
         printf("some other error\n");
    }
    bufferevent_free(bev);
}
还是出现这个问题 zhihonghe@ubuntu:~/Desktop/docker-ub-18$ sudo docker build -t ubuntu_openwrt_18 -f Dockerfile . Sending build context to Docker daemon 8.704kB Step 1/23 : FROM ubuntu:18.04 ---> f9a80a55f492 Step 2/23 : LABEL Authors="Kun Liu <liukun@tp-link.com.hk> " ---> Using cache ---> 0c24f88a2436 Step 3/23 : LABEL Description="Broadcom SDK Build based on 18.04 LTS" Version="1.0" ---> Using cache ---> 6e934d1e19e4 Step 4/23 : ENV STAGINGDIR="" ---> Using cache ---> b30883665d79 Step 5/23 : ENV CONFIGDIR="" ---> Using cache ---> 60ee218d114e Step 6/23 : ENV PACKAGEDIR="" ---> Using cache ---> 13d94fd9880e Step 7/23 : RUN echo 'debconf debconf/frontend select Noninteractive' | debconf-set-selections ---> Using cache ---> 597ec0c7bcd1 Step 8/23 : RUN apt-get clean && apt-get update && apt-get install --yes --no-install-recommends --no-install-suggests build-essential net-tools inetutils-ping libncurses5-dev libdigest-crc-perl libpopt-dev xxd liblzo2-dev zlib1g-dev lib32z1 libssl-dev libglib2.0-dev libreadline-dev libjson-c-dev libxml2-dev libsqlite3-dev sqlite3 libuv1-dev libxslt1-dev libfcgi-dev libevent-dev libyajl-dev liburiparser-dev libwebsockets-dev locales lua5.1 liblua5.1-dev python python3 python3-dev python3-pip git git-lfs subversion autoconf automake pkg-config ccache cmake ninja-build clang-tools-6.0 bison flex gettext ca-certificates libtool patch vim unzip uuid-dev wget curl cpio bc gawk gdb valgrind diffstat chrpath texinfo ssh openssh-client openssh-server netbase quilt protobuf-c-compiler dropbear-bin openvswitch-switch tmux dirmngr lighttpd alien mlocate iproute2 dos2unix lsb-release rsync gdisk man pax moreutils npm sudo ---> Running in 055b66ac3c61 Get:1 http://archive.ubuntu.com/ubuntu bionic InRelease [242 kB] Get:2 http://security.ubuntu.com/ubuntu bionic-security InRelease [102 kB] Get:3 http://security.ubuntu.com/ubuntu bionic-security/universe amd64 Packages [1637 kB] Get:4 http://archive.ubuntu.com/ubuntu bionic-updates InRelease [102 kB] Get:5 http://archive.ubuntu.com/ubuntu bionic-backports InRelease [102 kB] Get:6 http://archive.ubuntu.com/ubuntu bionic/multiverse amd64 Packages [186 kB] Get:7 http://archive.ubuntu.com/ubuntu bionic/main amd64 Packages [1344 kB] Get:8 http://security.ubuntu.com/ubuntu bionic-security/restricted amd64 Packages [1688 kB] Get:9 http://security.ubuntu.com/ubuntu bionic-security/main amd64 Packages [3373 kB] Get:10 http://archive.ubuntu.com/ubuntu bionic/universe amd64 Packages [11.3 MB] Get:11 http://security.ubuntu.com/ubuntu bionic-security/multiverse amd64 Packages [23.8 kB] Get:12 http://archive.ubuntu.com/ubuntu bionic/restricted amd64 Packages [13.5 kB] Get:13 http://archive.ubuntu.com/ubuntu bionic-updates/universe amd64 Packages [2411 kB] Get:14 http://archive.ubuntu.com/ubuntu bionic-updates/multiverse amd64 Packages [30.8 kB] Get:15 http://archive.ubuntu.com/ubuntu bionic-updates/main amd64 Packages [3786 kB] Get:16 http://archive.ubuntu.com/ubuntu bionic-updates/restricted amd64 Packages [1728 kB] Get:17 http://archive.ubuntu.com/ubuntu bionic-backports/universe amd64 Packages [20.6 kB] Get:18 http://archive.ubuntu.com/ubuntu bionic-backports/main amd64 Packages [64.0 kB] Fetched 28.2 MB in 19s (1467 kB/s) Reading package lists... Reading package lists... Building dependency tree... Reading state information... Some packages could not be installed. This may mean that you have requested an impossible situation or if you are using the unstable distribution that some required packages have not yet been created or been moved out of Incoming. The following information may help to resolve the situation: The following packages have unmet dependencies: npm : Depends: node-gyp (>= 0.10.9) but it is not going to be installed E: Unable to correct problems, you have held broken packages. The command '/bin/sh -c apt-get clean && apt-get update && apt-get install --yes --no-install-recommends --no-install-suggests build-essential net-tools inetutils-ping libncurses5-dev libdigest-crc-perl libpopt-dev xxd liblzo2-dev zlib1g-dev lib32z1 libssl-dev libglib2.0-dev libreadline-dev libjson-c-dev libxml2-dev libsqlite3-dev sqlite3 libuv1-dev libxslt1-dev libfcgi-dev libevent-dev libyajl-dev liburiparser-dev libwebsockets-dev locales lua5.1 liblua5.1-dev python python3 python3-dev python3-pip git git-lfs subversion autoconf automake pkg-config ccache cmake ninja-build clang-tools-6.0 bison flex gettext ca-certificates libtool patch vim unzip uuid-dev wget curl cpio bc gawk gdb valgrind diffstat chrpath texinfo ssh openssh-client openssh-server netbase quilt protobuf-c-compiler dropbear-bin openvswitch-switch tmux dirmngr lighttpd alien mlocate iproute2 dos2unix lsb-release rsync gdisk man pax moreutils npm sudo' returned a non-zero code: 100
最新发布
11-08
<think>我们注意到问题仍然存在:npm依赖node-gyp但无法安装,提示有未满足的依赖关系和被保留的损坏包(held broken packages)。由于之前推荐的NodeSource方案未被采用(用户仍然在尝试通过系统源安装npm),我们需要更彻底的解决方案。 ### 根本原因分析 1. **Ubuntu 18.04官方源的npm版本过旧** bionic仓库中的npm版本为3.5.2,而node-gyp需要更高版本(>=0.10.9),但仓库中可能没有满足依赖的node-gyp版本。 2. **依赖链断裂** 系统尝试安装npm时,找不到符合要求的node-gyp包,可能是仓库中根本没有node-gyp,或者版本不满足。 3. **缓存或锁定问题** 尽管清理了缓存,但可能存在残留的锁定状态(held packages)。 ### 终极解决方案 #### 方案1:完全移除系统npm,使用NodeSource安装Node.js(自带npm) 修改Dockerfile,将原来的`apt-get install`命令拆分为两步: ```dockerfile # 第一步:安装除npm之外的所有包 RUN apt-get clean && \ apt-get update && \ apt-get install --yes \ --no-install-recommends \ --no-install-suggests \ build-essential net-tools inetutils-ping \ libncurses5-dev libdigest-crc-perl libpopt-dev xxd \ liblzo2-dev zlib1g-dev lib32z1 \ libssl-dev libglib2.0-dev libreadline-dev \ libjson-c-dev libxml2-dev \ libsqlite3-dev sqlite3 \ libuv1-dev libxslt1-dev libfcgi-dev \ libevent-dev libyajl-dev liburiparser-dev libwebsockets-dev \ locales lua5.1 liblua5.1-dev \ python python3 python3-dev python3-pip \ git git-lfs \ subversion \ autoconf automake \ pkg-config ccache \ cmake ninja-build \ clang-tools-6.0 \ bison flex gettext \ ca-certificates \ libtool patch vim \ unzip uuid-dev \ wget curl cpio \ bc gawk \ gdb valgrind diffstat \ chrpath texinfo \ ssh openssh-client openssh-server netbase \ quilt protobuf-c-compiler dropbear-bin openvswitch-switch \ tmux dirmngr lighttpd alien \ mlocate iproute2 dos2unix \ lsb-release \ rsync \ gdisk \ man \ pax \ moreutils \ sudo # 注意:这里移除了npm # 第二步:通过NodeSource安装Node.js(包含npm) RUN curl -fsSL https://deb.nodesource.com/setup_18.x | bash - && \ apt-get install -y --no-install-recommends nodejs ``` #### 方案2:手动安装node-gyp(不推荐,复杂且易出错) 如果坚持使用系统npm,则需要手动解决依赖: ```dockerfile # 尝试手动安装node-gyp所需的依赖 RUN apt-get install -y python2.7 make g++ && \ ln -s /usr/bin/python2.7 /usr/bin/python2 # node-gyp需要python2 # 然后安装node-gyp(通过npm) RUN npm install -g node-gyp@latest ``` 但这种方法可能仍然失败,因为系统npm版本太低可能不支持新版node-gyp。 ### 注意事项 1. **移除重复包**:检查原始命令,确保没有重复的包名(如`build-essential`出现了两次)。 2. **使用缓存加速**:将安装NodeSource的步骤放在后面,这样前面安装系统包的步骤可以充分利用Docker缓存。 3. **清理无用文件**:在安装完成后,可以添加清理命令减少镜像大小: ```dockerfile RUN apt-get clean && rm -rf /var/lib/apt/lists/* ``` §§相关问题§§ 1. 如何使用Docker的多阶段构建来优化包含Node.js环境的镜像大小? 2. 在Ubuntu 18.04中,如何为Python 2创建符号链接以兼容旧版构建工具? 3. 如何验证从第三方源(如NodeSource)下载的安装脚本的安全性? 4. 当Docker构建因网络问题失败时,有哪些重试机制或镜像加速方案? 5. 如何编写Dockerfile以同时支持AMD64和ARM64架构的构建? </think>根据您提供的 Docker 构建日志,问题依然存在且未解决。**npm 依赖 node-gyp 无法安装**的核心问题需要更彻底的解决方案。以下是针对该问题的深度分析和完整解决方法: ### 根本原因诊断 1. **Ubuntu 18.04 官方源缺陷** `bionic` 仓库中的 npm 包(v3.5.2)依赖 node-gyp,但仓库中: - 无独立 `node-gyp` 包(需通过 npm 安装) - Node.js 版本过低(v8.10.0)导致 npm 无法自解析依赖 2. **依赖链断裂** ```mermaid graph LR A[npm] --> B[node-gyp] B --> C[Node.js] C --> D[python2.7] ``` Ubuntu 18.04 官方源未提供完整依赖链 3. **缓存污染** 尽管使用 `apt-get clean`,但 Docker 层缓存可能保留无效状态 --- ### 根治解决方案(修改 Dockerfile) #### 方案 1:完全跳过系统 npm,使用 NodeSource 安装(推荐) ```dockerfile # 步骤1:安装除npm外的所有依赖 RUN apt-get clean && \ apt-get update && \ apt-get install --yes \ --no-install-recommends \ --no-install-suggests \ build-essential net-tools inetutils-ping \ # ... 其他所有包(完整列表) ... sudo \ # 保留sudo python2.7 \ # 必须添加Python2支持 && rm -rf /var/lib/apt/lists/* # 步骤2:安装现代Node.js环境 RUN curl -fsSL https://deb.nodesource.com/setup_18.x | bash - \ && apt-get install -y --no-install-recommends nodejs \ && corepack enable # 激活现代包管理器 ``` #### 方案 2:强制重建依赖树(适用于受限环境) ```dockerfile RUN apt-get clean && \ rm -rf /var/lib/apt/lists/* && \ dpkg --configure -a && \ apt-get update -y && \ # 优先安装关键依赖 apt-get install -y python2.7 nodejs npm --no-install-recommends && \ # 修复npm环境 npm install -g npm@latest node-gyp && \ # 再安装其他包 apt-get install --yes \ --no-install-recommends \ --no-install-suggests \ build-essential ...其他包... ``` --- ### 关键操作说明 1. **必须添加 python2.7** node-gyp 依赖 Python 2.x,Ubuntu 18.04 默认不安装 ```dockerfile apt-get install -y python2.7 ``` 2. **彻底清理缓存** 在 `apt-get update` 前执行: ```dockerfile rm -rf /var/lib/apt/lists/* ``` 3. **验证修复** 在 Dockerfile 中添加诊断命令: ```dockerfile RUN node -v && npm -v && which node-gyp ``` 4. **构建命令优化** 重建镜像时强制清除缓存: ```bash docker build --no-cache -t ubuntu_openwrt_18 . ``` --- ### 预防措施 | 措施 | 实现方式 | 作用 | |------|----------|------| | 分离安装阶段 | 拆分为多个 RUN 指令 | 避免单点失败影响全局 | | 固定版本 | `npm@8.19.4` | 避免版本漂移 | | 镜像源检查 | `apt-cache policy npm` | 确认源有效性 | | 依赖树分析 | `apt-rdepends npm` | 预见依赖冲突 |
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值