初识 libcurl multi:实现一个 http 请求处理客户端,一个线程玩命发一个线程吐血收

2023-11-17

一、引言

最近在工作中,遇到了这么一个需求:

我们希望拥有一个高性能的 http 请求处理客户端程序,这个客户端要求有这样的架构:

  1. 它拥有两个线程
  2. 一个线程接收业务程序通过消息队列发来的批量的 http 请求信息,进行批量的 http 请求
  3. 另一个线程接收外部的 http 应答,并将应答信息放到本地的消息队列中供业务程序使用
  4. 要求在 http 请求的处理过程中,尽量保持不阻塞高性能处理

这个需求的框架图大概是这个样子:

n 个业务程序 http client n 个 http 服务端程序 消息队列:发送批量的 http 请求报文 libcurl multi: 发起批量的 http 请求 libcurl multi: 处理批量的 http 应答 消息队列:转发 http 服务端的应答报文 n 个业务程序 http client n 个 http 服务端程序

可以看到,我们的需求,其实就是满足平台中大量的业务程序的 http 请求处理需求。其中业务程序自然不止一个,业务程序要请求的 http 服务端也不止一个,我们要做的 http 请求客户端程序实际上就像一个网关中间层。

那么现在问题来了,如果做到 http client 程序的高性能处理。我们当然不想它请求一次就阻塞掉,这样会非常非常的慢;我们也不想让处理应答这种耗时的操作占用太多时间,这样也会非常非常的慢。

此时,我想到了当初学习 libcurl 的时候在官网上读到的那句话:

The multi interface allows a single-threaded application to perform the same kinds of multiple, simultaneous transfers that multi-threaded programs can perform. It allows many of the benefits of multi-threaded transfers without the complexity of managing and synchronizing many threads.

尤其是最关键的执行请求的那个函数 curl_multi_perform 函数是异步的:

curl_multi_perform is asynchronous. It will only perform what can be done now and then return back control to your program. It is designed to never block. You need to keep calling the function until all transfers are completed.

这个接口非常适合用于我们现在的需求场景里面,当 http client 发起批量的 http 请求的时候,使用 libcurl multi 接口就可以实现不阻塞式异步处理。

那么如何设计和实现呢?

这里,我通过参考 libcurl 的官方示例 10-at-a-time.c 的代码框架,设计实现了两个设计方案(思路略有不同)的实现;并且因为工作环境的 libcurl 是老版本的库(没有 curl_multi_wait 函数的版本)而同时实现了新版本(使用 curl_multi_wait)的代码和旧版本(没有 curl_multi_wait 函数的版本)的代码。

接下来让我们开始吧:)

ps:
1. 10-at-a-time.c 官方示例代码网址 https://curl.haxx.se/libcurl/c/10-at-a-time.html
2. 本篇博客中所有的实现代码托管在 GitHub 上,仓库地址 https://github.com/wangying2016/libcurl_multi_http_client

二、分析:10-at-a-time.c 到底实现了什么

在设计实现我们自己的 http client 程序之前,我们先要来看看这个让我们启发很大的官方示例 demo 10-at-a-time.c 到底实现了什么。

根据 libcurl 的版本不同,10-at-a-time.c 的代码也稍有不同,这里我只分析最新的使用 curl_multi_wait 函数的版本(这一版本的代码也相当简洁),思路都是一样的,只是实现方式不一样(如果你跟我一样,也要考虑兼容老版本的 libcurl,那么建议你去官网上下载一个 curl-7.20.0.tar,进去里面的 docs/examples/10-at-a-time.c 中去看看老版本的实现方式)。

这里大家可以通过网址 https://curl.haxx.se/libcurl/c/10-at-a-time.html 点击进去阅读 10-at-a-time.c 的代码。我这里简单分析下这个程序的设计。

1. 10-at-a-time.c 程序功能

通过编译:

$ gcc -o 10-at-a-time 10-at-a-time.c -lcurl

你就可以拿到 10-at-a-time 的执行文件:

$ ./10-at-a-time

你就可以发现,这个程序实现了大量的 http 请求功能。具体并发量是多少呢?10,这也是它的名字的来源。

那么它是怎么实现的呢?

2. 10-at-a-time.c 框架分析

这里我也不浪费口舌,直接上流程图(细节比如说 transfers 是否小于当前全部 url 总数、获取消息是否处理成功等等判断未体现在流程图中,可自行仔细研读代码):

外层循环
使用 curl_multi_perform 批量发起 http 请求,使用 curl_multi_wait 轮询全部请求 handle 可读状态。外层循环主要是在发起请求,并且轮询请求 handle 状态。

Created with Raphaël 2.2.0 开始 添加 10 个初始 url 请求 curl_multi_perform 批量发起 http 请求 内层循环处理应答并且新增请求 curl_multi_wait 轮询全部请求 handle 是否还有活动请求或者还有未处理请求 结束 yes no

内层循环
使用 curl_multi_info_read 读取应答消息结构体,使用 curl_easy_getinfo 获取具体信息,使用 curl_multi_remove_handle 删除已处理请求 handle,在 add_transfer 函数中使用 curl_multi_add_handle 添加新请求。

Created with Raphaël 2.2.0 内层循环开始 curl_multi_info_read 读取应答消息 curl_easy_getinfo 获取具体信息 curl_multi_remove_handle 删除请求 handle curl_multi_add_handle 添加新请求 是否还有可处理应答信息 内层循环结束 yes no

总的来说
10-at-a-time.c 程序,外层循环批量发起 http 请求并且轮询请求 handle 状态,内层循环处理 http 应答,同时删除处理完毕的请求然后添加新的请求。

就这样,10-at-a-time.c 完成了一个高并发 http 请求,并且还能动态新增请求处理的功能。

相当于就是,既可以同时烧 10 把柴火,还能把烧完了的柴火清理掉再自动添加柴火!

这不就是我想要的功能吗:)

三、方案一:线程一批量发和收,线程二处理线程一全局缓存的应答报文

10-at-a-time.c 是一个线程的程序,如何让其变成两个线程的框架呢,我稍稍动了脑子,设计出来了以下的方案:

线程一负责 http 请求的批量发送和接收,然后把接收到的报文存储到全局变量中去;线程二负责将线程一存储到全局变量中的应答报文进行实际的处理

既然用到了全局变量存储应答报文,那么两个线程中的同步一定要做好(mutex)。另外,这里我使用了非常简单的方式来记录是否处理:

定义一个应答报文结构,其中定义一个变量 consumable,它为 1 即为收到应答可以处理,为 2 则意味着处理完毕

1. 存储结构

首先,请求定义在全局变量中:

char *urls[] = {
  "https://www.microsoft.com",
  "https://opensource.org",
  ...
  };

为了记录应答信息,我定义了一个应答报文的结构,这里没有存储应答信息,因为实在是太大了,就简单记录下是否可以供线程二处理的状态(也就是收到了应答,记录 consumable 为 1 状态,线程二看到了 consumable 为 1 就会去处理,处理完毕置为 2):

// response struct
struct res_info {
    int index;
    // 0: initial 1: can consume 2: consume end
    int consumable;
    char url[MAX_URL_LENGTH];
};

其中 index 记录着该 url 在全局 urls 中的序号,consumable 记录着这个应答报文是否可以处理,0 是初始化状态,1 是线程一添加到了全局变量中去可供线程二处理状态,2 则是线程二处理完毕的状态。

// response
struct res_info res_list[NUM_URLS];

res_list 则是全局的应答报文存储变量,NUM_URLS 则是全部的 url 的个数(这只是一个 demo,后续可以简单的将其扩展为链表或者队列进行动态扩展)。在线程二中,我只需要遍历 res_list,读取其中 consumable 为 1 的应答报文进行处理,处理完毕后设置为 2 即标记它处理完毕。

2. 个别函数

添加请求的函数
在线程一中的内层循环中,处理完了一个请求就会新增一个请求(如果还有未处理的请求的话)。这里最重要的是 curl_multi_add_handle 函数。值得注意的是,这里我将 url 记录到了请求信息中去,在获取了应答之后,我可以方便的获取到 url。

// add request
static void add_transfer(CURLM *cm, int i)
{
  CURL *eh = curl_easy_init();
  curl_easy_setopt(eh, CURLOPT_WRITEFUNCTION, write_cb);
  curl_easy_setopt(eh, CURLOPT_URL, urls[i]);
  curl_easy_setopt(eh, CURLOPT_TIMEOUT, 10L);
  curl_easy_setopt(eh, CURLOPT_PRIVATE, urls[i]);
  curl_multi_add_handle(cm, eh);
}

记录请求序号的函数
这个函数供给线程一来通过 url 获取到它在全局请求列表中的序号,这个序号主要是是用来打日志用的,可以告知你第几个 url 已经被处理完了等等。

// find index
int find_index_of_urls(char *url) {
    int i;
    for (i = 0; i < NUM_URLS; ++i) {
        if (strcmp(urls[i], url) == 0) 
            return i;
    }
    return 0;
}

添加应答报文的函数
线程一在收到了应答,新增一个 res_info 结构,将 consumable 设置 1 标识其可以处理,记录 index 和 url,最后将这个 res_info 更新到全局的 res_list 应答报文列表中去。

// add response
void add_response(struct res_info res) {
    int i = 0;
    pthread_mutex_lock(&res_mutex);
    res_list[res.index] = res;
    res_list[res.index].consumable = 1;
    // sleep(1);
    pthread_mutex_unlock(&res_mutex);
}

处理应答报文的函数
线程二中遍历 res_list 全局应答报文列表,然后进行检查,是否有可以处理的报文,有的话则将其 consumable 置为 2 标识其处理完毕。

// consume response
void consume_response() {
    int i;
    pthread_mutex_lock(&res_mutex);
    for (i = 0; i < NUM_URLS; ++i) {
        if (res_list[i].consumable == 1) {
            printf("Log: t2, consume response, index [%d], url [%s]\n", \
            	res_list[i].index, res_list[i].url);
            res_list[i].consumable = 2;
        }
    }
    pthread_mutex_unlock(&res_mutex);
}

检查是否全部处理完毕的函数
线程二通过这个函数检查全部应答报文处理完毕。

/ check consume finished
int check_consume_finished() {
    int i, finished;
    finished = 1;
    pthread_mutex_lock(&res_mutex);
    for (i = 0; i < NUM_URLS; ++i) {
        if (res_list[i].consumable != 2) {
            finished = 0;
            break;
        }
    }
    pthread_mutex_unlock(&res_mutex);
    return finished;
}

3. 线程函数

线程一
线程一中,批量发送 http 请求,并且处理接收到的应答,并更新应答报文到全局的 res_list 变量中去。

// t1's thread function
void *fun1() {
    printf("Log: t1 begin...\n");

    CURLM *cm;
    CURLMsg *msg;
    int msgs_left = -1;
    int still_alive = 1;
    
    curl_global_init(CURL_GLOBAL_ALL);
    cm = curl_multi_init();
    
    /* Limit the amount of simultaneous connections curl should allow: */ 
    curl_multi_setopt(cm, CURLMOPT_MAXCONNECTS, (long)MAX_PARALLEL);
    
    for(transfers = 0; transfers < MAX_PARALLEL; transfers++)
        add_transfer(cm, transfers);
    
    do {
        curl_multi_perform(cm, &still_alive);
    
        while((msg = curl_multi_info_read(cm, &msgs_left))) {
            if(msg->msg == CURLMSG_DONE) {
                struct res_info res;
                char *url;
                CURL *e = msg->easy_handle;
                curl_easy_getinfo(msg->easy_handle, CURLINFO_PRIVATE, &url);
                strcpy(res.url, url);
                res.index = find_index_of_urls(res.url);
                add_response(res);
                printf("Log: t1, add response, error number [%d], error messsage [%s], url [%s], "
                       "index [%d]\n", msg->data.result, curl_easy_strerror(msg->data.result), res.url,  \
                        res.index);
                curl_multi_remove_handle(cm, e);
                curl_easy_cleanup(e);
            }
            else {
                printf("Log: t1, request error, error number [%d]\n", msg->msg);
            }
            if(transfers < NUM_URLS)
                add_transfer(cm, transfers++);
        }
        if(still_alive)
            curl_multi_wait(cm, NULL, 0, 1000, NULL);
    
    } while(still_alive || (transfers < NUM_URLS));
    
    curl_multi_cleanup(cm);
    curl_global_cleanup();

    printf("Log: t1 end...\n");
}

线程二
线程二中,一直检查全局应答报文列表,如果存在可以处理的应答报文,则进行处理,直到全部都处理完毕为止。

// t2's thread function
void *fun2() {
    int i = 0;
    
    printf("Log: t2 begin...\n");
    do {
        consume_response();
        sleep(1);
    } while (!check_consume_finished());
    printf("Log: t2 end...\n");
}

至此,我们就实现了一个简单的高性能 http 请求处理客户端 demo 程序。详细代码可以参考我的 GitHub 上的 http_client_v1.c,其中 http_client_v1_old.c 是兼容没有 curl_multi_wait 函数的 libcurl 库版本代码。

四、方案二:线程一只负责批量发,线程二只负责批量处理

第一个方案,说实话有些不优雅,通过一个全局缓存的应答报文列表(并且通过 mutex 加锁同步)实现两个线程之间的分工。实际上,还是线程一做完了全部的工作,既负责发,又要负责收。

那么,libcurl multi 到底是否支持将 http 的请求跟处理分离到两个线程里面去呢:

ibcurl is thread safe but has no internal thread synchronization. You may have to provide your own locking should you meet any of the thread safety exceptions below.

Handles. You must never share the same handle in multiple threads. You can pass the handles around among threads, but you must never use a single handle from more than one thread at any given time.

请看加粗的地方,libcurl 官方文档中明确说明了,libcurl 不建议在两个线程中共享 handle。这就麻烦了,libcurl multi 的 handle 究竟可不可以在两个线程中共享使用呢?

我的设想是:一个线程使用 libcurl multi handle 就负责发请求,然后增加未处理请求;另一个线程使用 libcurl multi handle 就负责处理请求,然后删已处理请求。按道理说,线程一就一直在添加请求,也就是往 libcurl multi 中的 easy handle 栈中压栈,却并没有对已经存在的 easy handle 进行什么操作,料想应该不会影响线程二中对应答报文的处理吧。

所以,说不定可以?!

所以,不如我们尝试下?

T_T,说干就干!

1. 存储结构

这一方案中去掉了方案一的用来存储应答报文的全局变量,因为用不到了(线程二直接进行处理)。

应答报文结构体
这个有点变化,consumable 为 0 为初始化,consumable 为 1 则为处理完毕。

// response struct
struct res_info {
    int index;
    // 0: initial 1: consume end
    int consumable;
    char url[MAX_URL_LENGTH];
};

libcurl multi handle
定义在了全局变量中,在 main 函数中进行初始化和清理:

// multi handle
CURLM *cm;
...
int main() {
	// Initialize multi handle
    curl_global_init(CURL_GLOBAL_ALL);
    cm = curl_multi_init();
    ...
    // Clean mutli handle
    curl_multi_cleanup(cm);
    curl_global_cleanup();
    ...
}

当前正在处理请求请求计数
增加了一个记录当前正在处理的请求的变量,方便线程一在添加新请求的时候,不超过设置的并发上限的值(并对其进行加锁同步处理):

// on-going request
unsigned int doing_cnt = 0;

// cnt mutex
pthread_mutex_t cnt_mutex = PTHREAD_MUTEX_INITIALIZER;

2. 线程函数

线程一
现在,线程一完全就在做 10-at-a-time.c 中的外层循环的工作。不停地发起 http 请求,然后轮询请求状态,然后添加新的请求。

// t1's thread function
void *fun1() {
    printf("Log: t1 begin...\n");

    int still_alive = 1;
    
    /* Limit the amount of simultaneous connections curl should allow: */ 
    curl_multi_setopt(cm, CURLMOPT_MAXCONNECTS, (long)MAX_PARALLEL);

    for(transfers = 0; transfers < MAX_PARALLEL; transfers++) {
        add_transfer(transfers);
        printf("Log: t1, add request, index = [%d], url = [%s]\n", \
            find_index_of_urls(urls[transfers]), urls[transfers]);
    }
    
    do {
        curl_multi_perform(cm, &still_alive);
        if(still_alive)
            curl_multi_wait(cm, NULL, 0, 1000, NULL);
        if(transfers < NUM_URLS && doing_cnt < MAX_PARALLEL) {
            add_transfer(transfers);
            printf("Log: t1, add request, index = [%d], url = [%s]\n", \
                find_index_of_urls(urls[transfers]), urls[transfers]);
            transfers++;
        }
        // sleep(1);
    } while(still_alive || (transfers < NUM_URLS));

    printf("Log: t1 end...\n");
}

线程二
线程二中,执行的是 10-at-a-time.c 中的内层循环的工作,一直在读取可以处理的应答报文,然后删除已处理的请求。

// t2's thread function
void *fun2() {
    int i = 0;
    int msgs_left = -1;
    CURLMsg *msg;
    
    printf("Log: t2 begin...\n");
    do {
        while((msg = curl_multi_info_read(cm, &msgs_left))) {
            if(msg->msg == CURLMSG_DONE) {
                char *url;
                CURL *e = msg->easy_handle;
                curl_easy_getinfo(msg->easy_handle, CURLINFO_PRIVATE, &url);

                struct res_info res;
                strcpy(res.url, url);
                res.index = find_index_of_urls(res.url);
                res.consumable = 1;
                res_list[res.index] = res;
                printf("Log: t2, accept response, index [%d], url [%s], error number [%d], error messsage [%s]\n", \
                    res.index, res.url, ms  g->data.result, curl_easy_strerror(msg->data.result));

                curl_multi_remove_handle(cm, e);
                curl_easy_cleanup(e);
            }
            else {
                printf("Log: t2, request error, error number [%d]\n", msg->msg);
            }
            pthread_mutex_lock(&cnt_mutex);
            doing_cnt--;
            pthread_mutex_unlock(&cnt_mutex);
        }
        // sleep(1);
    } while (!check_consume_finished());
    printf("Log: t2 end...\n");
}

3. 存疑

虽然我这边修改了下代码,实现了方案二的代码。但是我还是很疑惑:

1. 我并没有对 libcurl multi handle 进行同步加锁机制,但是看上去不管怎么测试,代码都运行正常。难道官网上说的,不能在多个线程中共享 libcurl 的 handle 是有一定的例外情况的?

2. 我冒 libcurl 官网之大不韪,在两个线程中共享了 libcurl multi 接口的 handle,实际上并未出现问题。难道是因为我线程一只有添加请求的操作,从而没有影响到线程二的 handle 使用的原因吗?

3. 我方案二的代码是否是有问题的代码呢?至少目前看来,在执行上没有问题,并且再多 url 请求也没有出现问题。

这些问题,目前的我解答不了,或许需要自己日后深入 libcurl 的源码中去探索答案。这里也希望有相关经验的网友能够提供给我答案,这里悉心请教了:)

方案二的代码在 GitHub 上的 http_client_v2.c 中,同样的, http_client_v2_old.c 是兼容没有 curl_multi_wait 函数的 libcurl 库版本的代码。

2020-02-19 更新,本方案代码一定有问题

根据官网说明:

libcurl is thread safe but has no internal thread synchronization.

libcurl 是没有内部的同步返回措施的,因此这个方案的代码高并发一定会有问题。一个线程对 multi handle 操作增加,一个线程对 multi handle 操作删除,高并发下会有线程安全问题。

因此,方案二的代码仅供探索试错,有问题的地方也在此强调,还请注意:)

五、总结

通过学习 libcurl multi 接口的使用示例 10-at-a-time.c 的代码,我实现了一个使用了两个线程的高性能 http 客户端请求程序:

1. 一个线程玩命发
2. 一个线程吐血收

在这个过程中对 libcurl multi 接口的运行机制有了更加深入的了解。也在探索过程中实现了两个版本的代码,并且兼容了没有 curl_multi_wait 的函数的 libcurl 老版本的库代码。

短暂的探索有了一些成果,不过长期的探索仍然还需要进行。在探索过程中,总会发现自己的知识储存太薄弱,或许在我学习了更多网络相关的知识后,再回来看这些内容,会有不一样的理解吧 ^_^

To be Stronger:)

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

初识 libcurl multi:实现一个 http 请求处理客户端,一个线程玩命发一个线程吐血收 的相关文章

  • 拆分字符串以仅获取前 5 个字符

    我想去那个地点 var log src ap kernelmodule 10 001 100 但看起来我的代码必须处理 ap kernelmodule 10 002 100 ap kernelmodule 10 003 101 等 我想使用
  • 使用 find - 删除除任何一个之外的所有文件/目录(在 Linux 中)

    如果我们想删除我们使用的所有文件和目录 rm rf 但是 如果我希望一次性删除除一个特定文件之外的所有文件和目录怎么办 有什么命令可以做到这一点吗 rm rf 可以轻松地一次性删除 甚至可以删除我最喜欢的文件 目录 提前致谢 find ht
  • 为什么我收到“无法进行二进制日志记录”的信息。在我的 MySQL 服务器上?

    当我今天启动 MySQL 服务器并尝试使用以下命令进行一些更改时用于 MySQL 的 Toad http www quest com toad for mysql 我收到此消息 MySQL 数据库错误 无法进行二进制日志记录 消息 交易级别
  • Elasticsearch 无法写入日志文件

    我想激活 elasticsearch 的日志 当我运行 elasticsearch 二进制文件时 我意识到我在日志记录方面遇到问题 无法加载配置 这是输出 sudo usr share elasticsearch bin elasticse
  • 在 Linux 上更快地分叉大型进程?

    在现代 Linux 上达到与 Linux 相同效果的最快 最好的方法是什么 fork execve combo 从一个大的过程 我的问题是进程分叉大约 500MByte 大 并且一个简单的基准测试只能从进程中实现约 50 个分叉 秒 比较最
  • 强制卸载 NFS 安装目录 [关闭]

    Closed 这个问题不符合堆栈溢出指南 help closed questions 目前不接受答案 Locked 这个问题及其答案是locked help locked posts因为这个问题是题外话 但却具有历史意义 目前不接受新的答案
  • 如何在 Linux 中编写文本模式 GUI? [关闭]

    Closed 这个问题正在寻求书籍 工具 软件库等的推荐 不满足堆栈溢出指南 help closed questions 目前不接受答案 当我编写脚本 程序时 我经常想弹出一个简单的文本 gui 来提示输入 我该怎么做 例如 来自 Shel
  • 如何禁用 GNOME 桌面屏幕锁定? [关闭]

    Closed 这个问题不符合堆栈溢出指南 help closed questions 目前不接受答案 如何阻止 GNOME 桌面在几分钟空闲时间后锁定屏幕 我已经尝试过官方手册了在红帽 https access redhat com doc
  • Jenkins中找不到环境变量

    我想在詹金斯中设置很多变量 我试过把它们放进去 bashrc bash profile and profile of the jenkins用户 但 Jenkins 在构建发生时找不到它们 唯一有效的方法是将所有环境变量放入Jenkinsf
  • 如何根据 HTTP 请求使用 Python 和 Flask 执行 shell 命令并流输出?

    下列的这个帖子 https stackoverflow com questions 15092961 how to continuously display python output in a webpage 我能够tail f网页的日志
  • 如何在bash中使用jq从变量中包含的json中提取值

    我正在编写一个 bash 脚本 其中存储了一个 json 值 现在我想使用 Jq 提取该 json 中的值 使用的代码是 json val code lyz1To6ZTWClDHSiaeXyxg redirect to http examp
  • 为什么内核需要虚拟寻址?

    在Linux中 每个进程都有其虚拟地址空间 例如 32位系统为4GB 其中3GB为进程保留 1GB为内核保留 这种虚拟寻址机制有助于隔离每个进程的地址空间 对于流程来说这是可以理解的 因为有很多流程 但既然我们只有 1 个内核 那么为什么我
  • 我可以从命令行打印 html 文件(带有图像、css)吗?

    我想从脚本中打印带有图像的样式化 html 页面 谁能建议一个开源解决方案 我使用的是 Linux Ubuntu 8 04 但也对其他操作系统的解决方案感兴趣 你可以给html2ps http user it uu se jan html2
  • 所有平台上的java

    如果您想用 java 为 Windows Mac 和 Linux 编写桌面应用程序 那么所有这些代码都相同吗 您只需更改 GUI 即可使 Windows 应用程序更像 Windows 等等 如果不深入细节 它是如何工作的 Java 的卖点之
  • 如何有效截断文件头?

    大家都知道truncate file size 函数 通过截断文件尾部将文件大小更改为给定大小 但是如何做同样的事情 只截断文件的尾部和头部呢 通常 您必须重写整个文件 最简单的方法是跳过前几个字节 将其他所有内容复制到临时文件中 并在完成
  • 尝试安装 LESS 时出现“请尝试以 root/管理员身份再次运行此命令”错误

    我正在尝试在我的计算机上安装 LESS 并且已经安装了节点 但是 当我输入 node install g less 时 出现以下错误 并且不知道该怎么办 FPaulMAC bin paul npm install g less npm ER
  • 如何在 Linux shell 中将十六进制转换为 ASCII 字符?

    假设我有一个字符串5a 这是 ASCII 字母的十六进制表示Z 我需要找到一个 Linux shell 命令 它将接受一个十六进制字符串并输出该十六进制字符串代表的 ASCII 字符 所以如果我这样做 echo 5a command im
  • 如何将目录及其子目录中的所有 PDF 文件复制到一个位置?

    如何全部复制PDF文件从目录及其子目录到单个目录 实际上还有更多的文件 并且深度有些任意 假设四个目录的最大深度是公平的 我想这些文件需要重命名 如果a pdf例如 位于多个目录中 因为我会adding https ebooks stack
  • CentOS:无法安装 Chromium 浏览器

    我正在尝试在 centOS 6 i 中安装 chromium 以 root 用户身份运行以下命令 cd etc yum repos d wget http repos fedorapeople org repos spot chromium
  • os.Mkdir 和 os.MkdirAll 权限

    我正在尝试在程序开始时创建一个日志文件 我需要检查是否 log如果不创建目录 则目录存在 然后继续创建日志文件 好吧 我尝试使用os Mkdir 也os MkdirAll 但无论我在第二个参数中输入什么值 我都会得到一个没有权限的锁定文件夹

随机推荐

  • 注意力模型CBAM

    论文 CBAM Convolutional Block Attention Module Convolutional Block Attention Module CBAM 表示卷积模块的注意力机制模块 是一种结合了空间 spatial 和
  • 【Vue2】之简单自定义插件开发,含demo

    一个vue2 x的简单插件开发实例 首先创建文件夹和文件 个人习惯把插件都放在src plugins文件夹里 创建本次demo插件目录src plugins demo 以及目录文件 demo index js demo src main j
  • python,求解字符串的所有子串

    网上的一种解法 def cut s str results num 0 x 1 表示子字符串长度 for x in range len s i 表示偏移量 for i in range len s x results append s i
  • 部署 - 前后端发布策略

    前端发布策略 前端发布的本质是静态资源的发布 主要关心缓存和资源同步问题 HTTP缓存 合理的使用缓存让未修改的文件复用可以有效的减轻服务器负担和提高前端页面渲染效率 1 协商缓存 2 本地缓存 本地缓存无需跟服务器再次确认 直接根据文件名
  • CORS policy: header is present on the requested

    CORS policy header is present on the requested 欢迎使用Markdown编辑器 解决方法 请求结果 欢迎使用Markdown编辑器 Access to XMLHttpRequest at htt
  • spring boot读取pom.xml变量

    1 application yml配置 version project version 项目版本号 为什么不使用 project version 呢 避免与避免与Spring语法冲突 项目pom继承了spring boot starter
  • DCDC相关

    1 同步和异步 同步 MOSFET管 效率高 价格贵 零件数多 电路复杂 异步 二极管 效率低 价格低 电路简单 同步整流上管S1和下管S2需要一相同频率信号以互补方式进行驱动 保证S1导通时S2截止 S1截止S2导通 异步二极管损耗 在电
  • plsql 修改sql窗口字体

    工具 首选项 用户界面 字体 编辑器 选择
  • Android自定义一个广播接收器BroadcastReceiver监听系统wifi连接

    概述 注册一个广播用来接收系统发送的广播 比如 发送或接收到一个短信 用Toast或Notification通知提醒 或者是打开或者断开网络连接 用Toast做出提示 注册文件
  • uniapp-计算属性、watch 侦听器、props验证

    一 计算属性 计算属性本质上就是一个 function 函数 它可以实时监听 data 中数据的变化 并 return 一个计算后的新值 1 声明与使用计算属性 计算属性需要以 function 函数的形式声明到组件的 computed 选
  • win11不兼容很多游戏?win11不兼容哪些游戏

    很多用户升级win11系统之后 最担心的就是win11兼容性不强 很多游戏都玩不了 那到底win11不兼容哪些游戏 下面小编就来给大家讲讲 win11很多游戏不兼容 1 其实win11系统并没有那么多无法兼容的游戏 基本上win10可以兼容
  • java插入gif_Java swing(纯代码和含部分个人解析)插入png或gif图片的方法,切换界面功能的实现...

    package swing public class mains public static void main String args new swing package swing import java awt Color impor
  • 听说,你想做大模型时代的应用层创业!

    亲爱的科技探险家们和代码魔法师们 未来的钟声已经敲响 预示着一场极度炫酷的虚拟现实游戏即将展开 从初期简单的智能识别 到设计师级别的图纸设计 生成式AI技术 Generative AI 以其独特理念和创新模式重塑了传统内容生产效率和交互模式
  • Unity手机端3档震动

    using System Collections using System Collections Generic using UnityEngine public class VibrateHelper MonoBehaviour sta
  • elementui dialog组件固定高度

    弹窗高度过大 想设置个自适应的高度 固定头尾 deep el dialog margin 5vh auto important deep el dialog body height 70vh overflow auto margin hei
  • ChatGPT救命!4岁男孩3年求医17位专家无果,大模型精准揪出病因

    克雷西 萧箫 发自 凹非寺量子位 公众号 QbitAI 怪病 缠身3年求医无果 最终竟然被ChatGPT成功诊断 这是发生在一名4岁男孩身上的真实经历 某次运动后 他身体开始剧痛 母亲前后带她看了17名医生 从儿科 骨科到各种专家 先后进行
  • DeferredResult的使用场景及用法

    场景 假设我们现在要实现这样一个功能 浏览器要实时展示服务端计算出来的数据 一种可能的实现是 浏览器频繁 例如定时1秒 向服务端发起请求以获得服务端数据 但定时请求并不能 实时 反应服务端的数据变化情况 若定时周期为S 则数据延迟周期最大即
  • 小程序支付完整过程。足够详细!

    1 注册小程序 拿到App id 和 AppSecret 小程序密钥 取得商户的微信支付商户号 MCHID 和 微信支付密钥 APIKEY 2 流程 微信用户在微信端选商品下单 后台响应下单生成单号 产生第一次签名数据 提交微信统一支付接口
  • IDA工具介绍

    往期推荐 IDA工具安装 分享 ARM处理器寻址方式 ARM指令集 ARM汇编语言程序结构 昨天给大家概括性的了解了IDA工具 今天分享IDA工具的实际应用 一 IDA打开so文件 1 首先打开IDA工具 进入选项界面 直接选中Go选项 如
  • 初识 libcurl multi:实现一个 http 请求处理客户端,一个线程玩命发一个线程吐血收

    一 引言 最近在工作中 遇到了这么一个需求 我们希望拥有一个高性能的 http 请求处理客户端程序 这个客户端要求有这样的架构 它拥有两个线程 一个线程接收业务程序通过消息队列发来的批量的 http 请求信息 进行批量的 http 请求 另