高并发HTTP请求实践
liuian 2024-12-04 13:46 21 浏览
当今,正处于互联网高速发展的时代,每个人的生活都离不开互联网,互联网已经影响了每个人生活的方方面面。我们使用淘宝、京东进行购物,使用微信进行沟通,使用美图秀秀进行拍照美化等等。而这些每一步的操作下面,都离不开一个技术概念HTTP(Hypertext Transfer Protocol,超文本传输协议)。
举个,当我们打开京东APP的时候,首先进入的是开屏页面,然后进入首页。在开屏一般是广告,而首页是内容相关,包括秒杀,商品推荐以及各个tag页面,而各个tag也有其对应的内容。当我们在进入开屏之前或者开屏之后(这块依赖于各个app的技术实现),会向后端服务发送一个http请求,这个请求会带上该页面广告位信息,向后端要内容,后端根据广告位的配置,挑选一个合适的广告或者推荐商品返回给APP端进行展示。在这里,为了描述方便,后端当做一个简单的整体,实际上,后端会有非常复杂的业务调度,比如获取用户画像,广告定向,获取素材,计算坐标,返回APP,APP端根据坐标信息,下载素材,然后进行渲染,从而在用户端进行展示,这一切都是秒级甚至毫秒级响应,一个高效的HTTP Client在这里就显得尤为重要,本文主要从业务场景来分析,如何实现一个高效的HTTP Client。
目录
1 概念
curl
libcurl
特点
2 实现
同步
代码实现(easy_curl.cc)
编译
结果
异步
http_request.h
http_request.cc
main.cc
3 性能对比
4 一点心得
1 概念
当我们需要模拟发送一个http请求的时候,往往有两种方式:
1、通过浏览器
2、通过curl命令进行发送请求
如果我们在大规模高并发的业务中,如果使用curl来进行http请求,其效果以及性能是不能满足业务需求的,这就引入了另外一个概念libcurl。
curl
- 利用URL语法在命令行方式下工作的开源文件传输工具。它支持很多协议:DICT, FILE, FTP, FTPS, Gopher, HTTP, HTTPS, IMAP, IMAPS, LDAP, LDAPS, POP3, POP3S, RTMP, RTSP, SCP, SFTP, SMTP, SMTPS, Telnet and TFTP。
- 支持SSL证书,HTTP POST, HTTP PUT,FTP上传,基于表单的HTTP上传,代理(proxies)、cookies、用户名/密码认证(Basic, Digest, NTLM等)、下载文件断点续传,上载文件断点续传(file transfer resume),http代理服务器管道(proxy tunneling)以及其他特性。
libcurl
- 一个免费开源的,客户端url传输库,支持DICT, FILE, FTP, FTPS, Gopher, HTTP, HTTPS, IMAP, IMAPS, LDAP, LDAPS, POP3, POP3S, RTMP, RTSP, SCP, SFTP, SMTP, SMTPS, Telnet and TFTP等协议。
- 支持SSL证书,HTTP POST, HTTP PUT,FTP上传,基于表单的HTTP上传,代理(proxies)、cookies、用户名/密码认证(Basic, Digest, NTLM等)、下载文件断点续传,上载文件断点续传(file transfer resume),http代理服务器管道(proxy tunneling)等。
- 高度可移植,可以工作在不同的平台上,支持Windows,Unix,Linux等。
- 免费的,线程安全的,IPV6兼容的,同事它还有很多其他非常丰富的特性。libcurl已经被很多知名的大企业以及应用程序所采用。
特点
- curl和libcurl都可以利用多种多样的协议来传输文件,包括HTTP, HTTPS, FTP, FTPS, GOPHER, LDAP, DICT, TELNET and FILE等
- 支持SSL证书,HTTP POST, HTTP PUT,FTP上传,基于表单的HTTP上传,代理(proxies)、cookies、用户名/密码认证(Basic, Digest, NTLM等)、下载文件断点续传,上载文件断点续传(file transfer resume),http代理服务器管道(proxy tunneling)等。
- libcurl是一个库,通常与别的程序绑定在一起使用,如命令行工具curl就是封装了libcurl库。所以我们也可以在你自己的程序或项目中使用libcurl以获得类似CURL的强大功能。
2 实现
在开始实现client发送http请求之前,我们先理解两个概念:
同步请求:当客户端向服务器发送同步请求时,服务处理在请求的过程中,客户端会处于等待的状态,一直等待服务器处理完成,客户端将服务端处理后的结果返回给调用方。
sync
异步请求:客户端把请求发送给服务器之后,不会等待服务器返回,而是去做其他事情,待服务器处理完成之后,通知客户端该事件已经完成,客户端在获取到通知后,将服务器处理后的结果返回给调用方。
?async
通过这俩概念,就能看出,异步在实现上,要比同步复杂的多。同步,即我们简单的等待处理结果,待处理结果完成之后,再返回调用方。而对于异步,往往在实现上,需要各种回调机制,各种通知机制,即在处理完成的时候,需要知道是哪个任务完成了,从而通知客户端去处理该任务完成后剩下的逻辑。
下面,我们将从代码实现的角度,来更深一步的理解libcurl在实现同步和异步请求操作上的区别,从而更近异步的了解同步和异步的实现原理图片。
同步
使用libcurl完成同步http请求,原理和代码都比较简单,主要是分位以下几个步骤:
1、初始化easy handle
2、在该easy handle上设置相关参数,在本例中主要有以下几个参数
- CURLOPT_URL,即请求的url
- CURLOPT_WRITEFUNCTION,即回调函数,将http server返回数据写入对应的地方
- CURLOPT_FOLLOWLOCATION,是否获取302跳转后的内容
- CURLOPT_POSTFIELDSIZE,此次发送的数据大小
- CURLOPT_POSTFIELDS,此次发送的数据内容
更多的参数设置,请参考libcurl官网
3、curl_easy_perform,调用该函数发送http请求,并同步等待返回结果
4、curl_easy_cleanup,释放步骤一中申请的easy handle资源
代码实现(easy_curl.cc)
#include <curl/curl.h>
#include <iostream>
#include <string>
std::string resp;
size_t WriteData(
char* buffer, size_t size,
size_t nmemb, void* userp) {
resp.append(buffer, size * nmemb);
return size * nmemb;
}
int main(void) {
CURLcode res;
CURL *curl = curl_easy_init();
std::string post = "abc";
if(curl) {
// 设置url
curl_easy_setopt(curl, CURLOPT_URL, "https://www.baidu.com");
// 设置回调函数,即当有返回的时候,调用回调函数WriteData
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, WriteData);
// 抓取302跳转后d额内容
curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION,1);
// 设置发送的内容大小
curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, post.size());
// 设置发送的内容
curl_easy_setopt(curl, CURLOPT_POSTFIELDS, post.c_str());
// 开始执行http请求,此处是同步的,即等待http服务器响应
res = curl_easy_perform(curl);
/* Check for errors */
if(res != CURLE_OK)
fprintf(stderr, "curl_easy_perform() failed: %s\n",
curl_easy_strerror(res));
/* always cleanup */
curl_easy_cleanup(curl);
}
std::cout << resp << std::endl;
return 0;
}
编译
g++ --std=c++11 easy_curl.cc -I ../artifacts/include/ -L ../artifacts/lib -lcurl -o easy_curl
结果
<!DOCTYPE html>
<!--STATUS OK-->
<html>
<head>
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<meta http-equiv="content-type" content="text/html;charset=utf-8">
<meta content="always" name="referrer">
<script src="https://ss1.bdstatic.com/5eN1bjq8AAUYm2zgoY3K/r/www/nocache/imgdata/seErrorRec.js"></script>
<title>页面不存在_百度搜索</title>
<style data-for="result">
body {color: #333; background: #fff; padding: 0; margin: 0; position: relative; min-width: 700px; font-family: arial; font-size: 12px }
p, form, ol, ul, li, dl, dt, dd, h3 {margin: 0; padding: 0; list-style: none }
input {padding-top: 0; padding-bottom: 0; -moz-box-sizing: border-box; -webkit-box-sizing: border-box; box-sizing: border-box } img {border: none; }
.logo {width: 117px; height: 38px; cursor: pointer }
#wrapper {_zoom: 1 }
#head {padding-left: 35px; margin-bottom: 20px; width: 900px }
...
异步
接触过网络编程的读者,都或多或少的了解多路复用的原理。IO多路复用在Linux下包括了三种,select、poll、epoll,抽象来看,他们功能是类似的,但具体细节各有不同:首先都会对一组文件描述符进行相关事件的注册,然后阻塞等待某些事件的发生或等待超时。
在使用Libcurl进行异步请求,从上层结构来看,简单来说,就是对easy handle 和 multi 接口的结合使用。其中,easy handle底层也是一个socket,multi接口,其底层实现也用的是epoll,那么我们如何使用easy handle和multi接口,来实现一个高性能的异步http 请求client呢?下面我们将使用代码的形式,使得读者能够进一步了解其实现机制。
multi 接口的使用是在easy 接口的基础之上,将easy handle放到一个队列中(multi handle),然后并发发送请求。与easy 接口相比,multi接口是一个异步的,非阻塞的传输方式。
multi接口的使用,主要有以下几个步骤:
- curl_multi _init初始化一个multi handler对象
- 初始化多个easy handler对象,使用curl_easy_setopt进行相关设置
- 调用curl_multi _add_handle把easy handler添加到multi curl对象中
- 添加完毕后执行curl_multi_perform方法进行并发的访问访问结束后curl_multi_remove_handle移除相关easy curl对象,先用curl_easy_cleanup清除easy handler对象,最后curl_multi_cleanup清除multi handler对象。
http_request.h
/*
该类是对easy handle的封装,主要做一些初始化操作,设置url 、发送的内容
header以及回调函数
*/
class HttpRequest {
public:
using FinishCallback = std::function<int()>;
HttpRequest(const std::string& url, const std::string& post_data) :
url_(url), post_data_(post_data) {
}
int Init(const std::vector<std::string> &headers);
CURL* GetHandle();
const std::string& Response();
void SetFinishCallback(const FinishCallback& cb);
int OnFinish(int response_ret);
CURLcode Perform();
void AddMultiHandle(CURLM* multi_handle);
void Clear(CURLM* multi_handle);
private:
void AppendData(char* buffer, size_t size, size_t nmemb);
static size_t WriteData(char *buffer, size_t size, size_t nmemb, void *userp);
std::string url_;
std::string post_data_;
CURL* handle_ = nullptr;
struct curl_slist *chunk_ = nullptr;
FinishCallback cb_;
int response_ret_;
};
http_request.cc
/*http_request.h的实现*/
int HttpRequest::Init(const std::vector<std::string> &headers) {
handle_ = curl_easy_init();
if (handle_ == nullptr) {
return kCurlEasyInitFailed;
}
CURLcode ret = curl_easy_setopt(handle_, CURLOPT_URL, url_.c_str());
if (ret != CURLE_OK) {
return ret;
}
ret = curl_easy_setopt(handle_, CURLOPT_WRITEFUNCTION,
&HttpRequest::WriteData);
if (ret != CURLE_OK) {
return ret;
}
ret = curl_easy_setopt(handle_, CURLOPT_WRITEDATA, this);
if (ret != CURLE_OK) {
return ret;
}
ret = curl_easy_setopt(handle_, CURLOPT_NOSIGNAL, 1);
if (ret != CURLE_OK) {
return ret;
}
ret = curl_easy_setopt(handle_, CURLOPT_PRIVATE, this);
if (ret != CURLE_OK) {
return ret;
}
ret = curl_easy_setopt(handle_, CURLOPT_POSTFIELDSIZE, post_data_.length());
if (ret != CURLE_OK) {
return ret;
}
ret = curl_easy_setopt(handle_, CURLOPT_POSTFIELDS, post_data_.c_str());
if (ret != CURLE_OK) {
return ret;
}
ret = curl_easy_setopt(handle_, CURLOPT_TIMEOUT_MS, 100);
if (ret != CURLE_OK) {
return ret;
}
// ret = curl_easy_setopt(handle_, CURLOPT_CONNECTTIMEOUT_MS, 10);
ret = curl_easy_setopt(handle_, CURLOPT_DNS_CACHE_TIMEOUT,
600);
if (ret != CURLE_OK) {
return ret;
}
chunk_ = curl_slist_append(chunk_, "Expect:");
for (auto item : headers) {
chunk_ = curl_slist_append(chunk_, item.c_str());
}
ret = curl_easy_setopt(handle_, CURLOPT_HTTPHEADER, chunk_);
if (ret != CURLE_OK) {
return ret;
}
// 设置http header
if (boost::algorithm::starts_with(url_, "https://")) {
curl_easy_setopt(handle_, CURLOPT_SSL_VERIFYPEER, false);
curl_easy_setopt(handle_, CURLOPT_SSL_VERIFYHOST, false);
}
return kOk;
}
//获取easy handle
CURL* HttpRequest::GetHandle() {
return handle_;
}
// 获取http server端的响应
const std::string& HttpRequest::Response() {
return response_;
}
//设置回调函数,当server返回完成之后,调用
void HttpRequest::SetFinishCallback(const FinishCallback& cb) {
cb_ = cb;
}
// libcurl 错误码信息
int HttpRequest::OnFinish(int response_ret) {
response_ret_ = response_ret;
if (cb_) {
return cb_();
}
return kRequestFinishCallbackNull;
}
// 执行http请求
CURLcode HttpRequest::Perform() {
CURLcode ret = curl_easy_perform(handle_);
return ret;
}
// 将easy handle 加入到被监控的multi handle
void HttpRequest::AddMultiHandle(CURLM* multi_handle) {
if (multi_handle != nullptr) {
curl_multi_add_handle(multi_handle, handle_);
}
}
// 释放资源
void HttpRequest::Clear(CURLM* multi_handle) {
curl_slist_free_all(chunk_);
if (multi_handle != nullptr) {
curl_multi_remove_handle(multi_handle, handle_);
}
curl_easy_cleanup(handle_);
}
// 获取返回
void HttpRequest::AppendData(char* buffer, size_t size, size_t nmemb) {
response_.append(buffer, size * nmemb);
}
// 回调函数,获取返回内容
size_t HttpRequest::WriteData(
char* buffer, size_t size,
size_t nmemb, void* userp) {
HttpRequest* req = static_cast<HttpRequest*>(userp);
req->AppendData(buffer, size, nmemb);
return size * nmemb;
}
main.cc
curl_global_init(CURL_GLOBAL_DEFAULT);
auto multi_handle_ = curl_multi_init();
int numfds = 0;
int running_handles = 0;
while (true) {
//此处读者来实现,基本功能如下:
// 1、获取上游的请求内容,从里面获取要发送http的相关信息
// 2、通过步骤1获取的相关信息,来创建HttpRequest对象
// 3、将该HttpRequest对象跟multi_handle_对象关联起来
curl_multi_perform(multi_handle_, &running_handles);
CURLMcode mc = curl_multi_wait(multi_handle_, nullptr, 0,
200, &numfds);
if (mc != CURLM_OK) {
std::cerr << "RequestDispatcher::Run"
<< " curl_multi_wait failed, ret: "
<< mc;
continue;
}
curl_multi_perform(multi_handle_, &running_handles);
CURLMsg* msg = nullptr;
int left = 0;
while ((msg = curl_multi_info_read(multi_handle_, &left))) {
// msg->msg will always equals to CURLMSG_DONE.
// CURLMSG_NONE and CURLMSG_LAST were not used.
if (msg->msg != CURLMSG_DONE) {
std::cerr << "RequestDispatcher::Run"
<< " curl_multi_info_read failed, msg: "
<< msg->msg;
continue;
}
CURL* easy_handle = msg->easy_handle;
CURLcode curl_ret = msg->data.result;
int response_ret = kOk;
if (curl_ret != CURLE_OK) {
std::cerr << "RequestDispatcher::Run"
<< " msg->data.result != CURLE_OK, curl_ret: "
<< curl_ret;
response_ret = static_cast<int>(curl_ret);
}
int http_status_code = 0;
curl_ret = curl_easy_getinfo(easy_handle,
CURLINFO_RESPONSE_CODE, &http_status_code);
if (curl_ret != CURLE_OK) {
std::cerr << "RequestDispatcher::Run"
<< " curl_easy_getinfo failed, curl_ret="
<< curl_ret;
if (response_ret == kOk) {
response_ret = static_cast<int>(curl_ret);
}
}
if (http_status_code != 200) {
std::cerr << "RequestDispatcher::Run"
<< " http_status_code=" << http_status_code;
if (response_ret == kOk) {
response_ret = http_status_code;
}
}
HttpRequest* req = nullptr;
// In fact curl_easy_getinfo will
// always return CURLE_OK if CURLINFO_PRIVATE was set.
curl_ret = curl_easy_getinfo(easy_handle, CURLINFO_PRIVATE, &req);
if (curl_ret != CURLE_OK) {
std::cerr << "RequestDispatcher::Run"
<< " curl_easy_getinfo failed, curlf_ret="
<< curl_ret;
if (response_ret == kOk) {
response_ret = static_cast<int>(curl_ret);
}
}
if (req != nullptr) {
ret = req->OnFinish(response_ret);
}
}
}
至此,我们已经可以使用libcurl来实现并发发送http请求,当然这个只是一个简单异步实现功能,更多的功能,还需要读者去使用libcurl中的其他功能去实现,此处留给读者一个问题(这个问题,也是笔者项目中使用的一个功能,该项目已经线上稳定运行4年,日请求量在20E图片),业务需要,某一个请求需要并发发送给指定的几家,即该请求,需要并发发送给几个http server,在一个特定的超时时间内,获取这几个http server的返回内容,并进行处理,那么这种功能应该如何使用libcurl来实现呢?透露下,可以使用libcurl的另外一个参数CURLOPT_PRIVATE。
3 性能对比
至此,我们已经基本完成了高性能http 并发功能的设计,那么到底性能如何呢?笔者从 以下几个角度来做了测试:
1、串行发送同步请求
2、多线程情况下,发送同步请求(此处线程为4个,笔者测试的服务器为4C)
3、使用multi接口
4、使用multi接口,并复用其对应的easy handle
5、使用dns cache(对easy handle设置CURLOPT_DNS_CACHE_TIMEOUT),即不用每次都进行dns解析
方法 | 平均耗时(ms) | 最大耗时(ms) |
串行同步 | 21.381 | 30.617 |
多线程同步 | 4.331 | 16.751 |
multi接口 | 1.376 | 11.974 |
multi接口 连接复用 | 0.352 | 0.748 |
multi 接口使用dns cache | 0.381 | 0.731 |
4 一点心得
libcurl是一个高性能,较易用的HTTP client,在使用其直接,一定要对其接口功能进行详细的了解,否则很容易入坑,犹记得在18年中的时候,上线了某一个功能,会偶现coredump(在上线之前,也进行了大量的性能测试,都没有出现过一次coredump),为了分析这个原因,笔者将服务的代码一直精简精简,然后模拟测试,缩小coredump定位范围,最终发现,只有在超时的时候,才会导致coredump,这就说明了为什么测试环境没有coredump,而线上会产生coredump,这是因为线上的超时时间设置的是5ms,而测试环境超时时间是20ms,这就基本把原因定位到超时导致的coredump。
然后,分析libcurl源码,发送时一个libcurl的参数设置导致coredump,至此,笔者耗费了23个小时,问题才得以解决。
更多文章,请访问
http://www.studyinfo.top
- 上一篇:22.TypeScript异步编程
- 下一篇:eCapture 神奇的工具 实践篇
相关推荐
- Python tkinter学习笔记(七):Notebook和Treeview
-
‘Pythontkinter’是Python自带的GUI工具包,非常适合开发小型的GUI应用。最近使用‘tkinter’开发了一些自己日常使用的小工具,效果不错,于是把开发过程中学习到的一些tkin...
- 如何用 Python实现简单的表格界面
-
Excel有表格编辑功能,为什么我还要弄一个,不是多此一举么。道理是对的,但是很多会员功能才更加强大,不是吗?我们学语言,一来可以练习编码熟练的,巩固知识点,更重要的是你熟悉开发,以后如果你想实现一...
- 土地增值税清算中的施工合同进行判断是否有重复施工的情况
-
对土地增值税清算中的施工合同进行判断是否有重复施工的情况,使用Python中的Pandas库对施工合同的相关数据进行处理,基于文本相似度进行判断。1.读取施工内容数据:将施工内容数据存储在一个...
- 大模型时代必备技能:Embedding与向量数据库开发完全指南
-
本文较长,建议点赞收藏,以免遗失。更多AI大模型应用开发学习视频及资料,尽在官网-聚客AI学院大模型应用开发微调项目实践课程学习平台一.Embeddings与向量数据库1.1Embeddings的...
- 分布式实时搜索和分析引擎——Elasticsearch
-
一、概述Elasticsearch是一个基于Lucene的搜索引擎。它提供了具有HTTPWeb界面和无架构JSON文档的分布式,多租户能力的全文搜索引擎。Elasticsearch是用Java开发的...
- elasticsearch v9.0.0重磅发布!解锁最新核心特性与性能飞跃!
-
时隔3年,Elasticsearch迎来重大版本更新!基于Lucene10.1.0构建,9.0.0版本在AI搜索、安全分析、向量计算、集群管理等多个领域实现突破性升级版本亮点o新...
- Java中间件-Elasticsearch(java中间件技术及其应用开发)
-
Elasticsearch是一个非常强大的搜索引擎。它目前被广泛地使用于各个IT公司。Elasticsearch是由Elastic公司创建。它的代码位于GitHub-elastic/...
- 知名互联网公司和程序员都看好的数据库是什么?
-
2017年数据库领域的最大趋势是什么?什么是最热的数据处理技术?学什么数据库最有前途?程序员们普遍不喜欢的数据库是什么?本文都会一一揭秘。大数据时代,数据库的选择备受关注,此前本号就曾揭秘国内知名互联...
- 快速了解Elasticsearch(快速了解词语浑话的读音、释义等知识点)
-
Elasticsearch是一款基于Lucene的开源分布式全文搜索引擎,它支持实时搜索,具有优秀的可扩展性和可靠性。作为一款搜索引擎,Elasticsearch提供了丰富的API,使得开发人员可以通...
- 面试官:Kafka和ES选主有什么区别?
-
Kafka和ES都是用来处理大数据的中间件,一个是消息中间件的代表(Kafka),另一个是大数据搜索引擎的代表(ES)。它们在Java领域的使用非常广泛,在大数据方面就更不用说了,但它们的选...
- ElasticSearch 23 种映射参数详解
-
ElasticSearch系列教程我们前面已经连着发了四篇了,今天第五篇,我们来聊一聊Es中的23种常见的映射参数。针对这23种常见的映射参数,松哥专门录制了一个视频教程:视频链接:...
- 还不会Elasticsearch?看这些知识入门刚刚好
-
作者:MacroZheng链接:https://juejin.im/post/5e8c7d65518825736512d097记得刚接触Elasticsearch的时候,没找啥资料,直接看了遍Ela...
- Elasticsearch学习,请先看这一篇!
-
题记:Elasticsearch研究有一段时间了,现特将Elasticsearch相关核心知识、原理从初学者认知、学习的角度,从以下9个方面进行详细梳理。欢迎讨论……0.带着问题上路——ES是如何产...
- Elasticsearch企业级应用全景图:原理/场景/优化/避坑四重奏
-
一、核心概念与架构原理1.基本定义Elasticsearch是基于ApacheLucene构建的分布式实时搜索与分析引擎,具有以下核心特性:分布式架构:支持PB级数据水平扩展近实时(NRT):数据...
- ELK Stack系列之基础篇(八) - Elasticsearch原理总结(图示)
-
前言通过前面的知识,我们已经了解到了ELk到底是什么、以及他们的工作原理、ES集群架构、专有名词的一些解释。在进入下一阶段ES实操学习环节前,那么今天我将以图解的方式将ELK重点以及ES的相关逻辑进行...
- 一周热门
-
-
Python实现人事自动打卡,再也不会被批评
-
【验证码逆向专栏】vaptcha 手势验证码逆向分析
-
Psutil + Flask + Pyecharts + Bootstrap 开发动态可视化系统监控
-
一个解决支持HTML/CSS/JS网页转PDF(高质量)的终极解决方案
-
再见Swagger UI 国人开源了一款超好用的 API 文档生成框架,真香
-
网页转成pdf文件的经验分享 网页转成pdf文件的经验分享怎么弄
-
C++ std::vector 简介
-
系统C盘清理:微信PC端文件清理,扩大C盘可用空间步骤
-
10款高性能NAS丨双十一必看,轻松搞定虚拟机、Docker、软路由
-
python使用fitz模块提取pdf中的图片
-
- 最近发表
-
- Python tkinter学习笔记(七):Notebook和Treeview
- 如何用 Python实现简单的表格界面
- 土地增值税清算中的施工合同进行判断是否有重复施工的情况
- 大模型时代必备技能:Embedding与向量数据库开发完全指南
- 分布式实时搜索和分析引擎——Elasticsearch
- elasticsearch v9.0.0重磅发布!解锁最新核心特性与性能飞跃!
- Java中间件-Elasticsearch(java中间件技术及其应用开发)
- 知名互联网公司和程序员都看好的数据库是什么?
- 快速了解Elasticsearch(快速了解词语浑话的读音、释义等知识点)
- 面试官:Kafka和ES选主有什么区别?
- 标签列表
-
- python判断字典是否为空 (50)
- crontab每周一执行 (48)
- aes和des区别 (43)
- bash脚本和shell脚本的区别 (35)
- canvas库 (33)
- dataframe筛选满足条件的行 (35)
- gitlab日志 (33)
- lua xpcall (36)
- blob转json (33)
- python判断是否在列表中 (34)
- python html转pdf (36)
- 安装指定版本npm (37)
- idea搜索jar包内容 (33)
- css鼠标悬停出现隐藏的文字 (34)
- linux nacos启动命令 (33)
- gitlab 日志 (36)
- adb pull (37)
- table.render (33)
- python判断元素在不在列表里 (34)
- python 字典删除元素 (34)
- vscode切换git分支 (35)
- python bytes转16进制 (35)
- grep前后几行 (34)
- hashmap转list (35)
- c++ 字符串查找 (35)