RPC分布式网络通信框架项目

文章目录

      • 对比单机聊天服务器、集群聊天服务器以及分布式聊天服务器
      • RPC通信原理
      • 使用Protobuf做数据的序列化,相比较于json,有哪些优点?
      • 环境配置使用
        • 项目代码工程目录
        • vscode远程开发Linux项目
        • muduo网络库编程示例
        • CMake构建项目集成编译环境
        • Linux环境下搭建muduo网络库
        • 网络I/O模型介绍
      • Protobuf安装配置
        • ubuntu protobuf环境搭建
      • protobuf 实践讲解
      • 如何将本地服务发布成rpc服务?

对比单机聊天服务器、集群聊天服务器以及分布式聊天服务器

(1) 单机聊天服务器
在这里插入图片描述
首先让我们来看单机聊天服务器会遇到的一些性能瓶颈的问题:

  1. 聊天服务器所能承受的用户的并发量受限于硬件资源;
  2. 任何模块的修改,都会导致整个项目代码重新进行编译和部署;
  3. 系统中,有些模块是属于CPU密集型的,有些模块是属于I/O密集型的,造成各模块对于硬件资源的需求是不相同的。所以,将各模块部署在一台服务器上时,没办法按照各模块对资源的需求部署在特定的硬件资源上。

(2)集群聊天服务器
在这里插入图片描述

集群聊天服务器相对于单机聊天服务器,做了哪些优化,还存在哪些性能瓶颈的问题?
优点:
用户的并发量提升、部署集群服务器比较简单。
缺点
1、项目代码还是需要整体重新编译,而且需要进行多次部署;
2、对于某些模块根本不需要高并发(比如上方服务器中的后台管理模块),而使用集群聊天服务器后,在每一台服务器上都部署了相同的模块,可能会造成服务器资源的浪费。

集群:每一台服务器独立运行一个工程的所有模块。
分布式:一个工程拆分了很多模块,每一个模块独立部署运行在一个或多台服务器主机上,所有服务器协同工作共同提供服务,每一台服务器称作分布式的一个节点,根据节点的并发要求,对一个节点可以再做节点模块集群部署。
在这里插入图片描述
当引入了分布式部署聊天服务器的概念后,我们把每个模块按需部署在一台或多台服务器上,使得服务器的资源利用更加充分。同时,如果只是改动了某一个模块的代码,不用将所有的服务器代码都重新编译和部署,只需要重新编译相对应的模块代码。

但是,引入分布式服务器后,会遇到哪些难题需要我们解决呢?

  1. 我们应该如何对大系统的软件模块进行划分呢?怎么划分才能使得各模块的重复代码更少呢?
  2. 当模块被部署在不同的服务器上时,各模块之间如何进行访问呢?比如下图的机器1上的模块如何去调用机器2上的模块的一个业务方法呢?机器1上的模块进程1怎么调用机器1上的模块进程2里面的一个业务方法呢?

在这里插入图片描述
第1个问题通过软件设计师的经验来进行模块的划分,而第2个问题则是通过我们设计的RPC分布式网络框架来实现远程方法的调用,使得客户端基于此框架,能让不同网络结点之间的服务调用像调用本地服务一样简单。


RPC通信原理

RPC(Remote Procedure Call Protocol)远程过程调用协议
在这里插入图片描述
黄色部分:设计rpc方法参数的打包和解析,也就是数据的序列化和反序列化,使用Protobuf
绿色部分:网络部分,包括寻找rpc服务主机,发起rpc调用请求和响应rpc调用结果,使用muduo网络库和zookeeper服务配置中心(专门做服务发现)。
mprpc框架主要包含以上两个部分的内容。


使用Protobuf做数据的序列化,相比较于json,有哪些优点?

  1. Protobuf 是使用二进制存储数据的,而xmljson都是文本存储的;
  2. Protobuf 不需要存储额外的信息,而json是以键值对的方式来存储数据。

环境配置使用

项目代码工程目录

bin:可执行文件
build:项目编译文件
lib:项目库文件
src:源文件
test:测试代码
example:框架代码使用范例
CMakeLists.txt:顶层的cmake文件
README.md:项目自述文件
autobuild.sh:一键编译脚本

vscode远程开发Linux项目
muduo网络库编程示例
CMake构建项目集成编译环境
Linux环境下搭建muduo网络库

muduo库的安装需要依赖boost库,具体安装配置步骤参考博客:
Linux环境下搭建muduo网络库

网络I/O模型介绍
  • accept + read/write
    不是并发服务器
  • accept + fork - process-pre-connection
    适合并发连接数不大,计算任务工作量大于fork的开销
  • accept + thread thread-pre-connection
    比方案2的开销小了一点,但是并发造成线程堆积过多
  • muduo的设计:reactors in threads - one loop per thread
    方案的特点是one loop per thread,有一个main reactor(I/O)负载accept连接,然后把连接分发到某个sub reactor(Worker),该连接的所用操作都在那个sub reactor所处的线程中完成,多个连接可能被分派到多个线程中,以充分利用CPU。
    如果有过多的耗费CPU I/O的计算任务,可以创建新的线程专门处理耗时的计算任务。
  • reactors in process - one loop pre process
    nginx服务器的网络模块设计,基于进程设计,采用多个Reactors充当I/O进程和工作进程,通过一把accept锁,完美解决多个Reactors的“惊群现象”。

Protobuf安装配置

protobuf(protocol buffer)是google 的一种数据交换的格式,它独立于平台语言。
google 提供了protobuf多种语言的实现:java、c#、c++、go 和 python,每一种实现都包含了相应语言的编译器以及库文件。
由于它是一种二进制的格式,比使用 xml(20倍) 、json(10倍)进行数据交换快许多。可以把它用于分布式应用之间的数据通信或者异构环境下的数据交换。作为一种效率和兼容性都很优秀的二进制数据传输格式,可以用于诸如网络传输、配置文件、数据存储等诸多领域。

ubuntu protobuf环境搭建

见项目资料下载地址或者在github源代码下载地址:https://github.com/google/protobuf
源码包中的src/README.md,有详细的安装说明,安装过程如下:
1、解压压缩包:unzip protobuf-master.zip
2、进入解压后的文件夹:cd protobuf-master
3、安装所需工具:sudo apt-get install autoconf automake libtool curl make g++ unzip
4、自动生成configure配置文件:./autogen.sh
5、配置环境:./configure
6、编译源代码(时间比较长):make
7、安装:sudo make install
8、刷新动态库:sudo ldconfig


protobuf 实践讲解

首先在服务端编写proto配置文件,比如测试代码中的test.proto,配置文件中包含需要需要进行序列化的请求消息和响应消息,以及在protobuf中定义了描述rpc方法的服务类型,传入相应的rpc请求方法名以及相应的参数,返回参数。

syntax = "proto3"; // 声明了protobuf的版本package fixbug; // 声明了代码所在的包(对于C++来说是namespace)// 定义下面的选项,表示生成service服务类和rpc方法描述,默认不生成
option cc_generic_services = true;message ResultCode
{int32 errcode = 1;bytes errmsg = 2;
}// 数据   列表   映射表
// 定义登录请求消息类型  name   pwd
message LoginRequest
{bytes name = 1;bytes pwd = 2;
}// 定义登录响应消息类型
message LoginResponse
{ResultCode result = 1;bool success = 2;
}message GetFriendListsRequest
{uint32 userid = 1;
}message User
{bytes name = 1;uint32 age = 2;enum Sex{MAN = 0;WOMAN = 1;}Sex sex = 3;
}message GetFriendListsResponse
{ResultCode result = 1;repeated User friend_list = 2;  // 定义了一个列表类型
}// 在protobuf里面怎么定义描述rpc方法的类型 - service
service UserServiceRpc
{rpc Login(LoginRequest) returns(LoginResponse);rpc GetFriendLists(GetFriendListsRequest) returns(GetFriendListsResponse);
}

通过在终端中执行protoc test.proto --cpp_out=./即可在当前文件夹中生成test.pb.cctest.pb.h,其中massage以及service生成了相应的类,且都是通过继承得到的,class LoginRequest: public::google::protobuf::Message,而UserServiceRpc服务生成了两个类:class UserServiceRpc: public google::protobuf::Service提供给服务提供者进行调用、class UserService_Stub: public UserServiceRpc提供给服务消费者进行调用。如下所示:
在这里插入图片描述在这里插入图片描述
在服务提供者方,由于生成的UerServiceRpc类从public google::protobuf::Service继承而来,而类中的Login()方法和GetFriendLists()方法都是虚函数,因此服务提供者需要对相应的方法进行重写。
在服务消费者方,我们可以发现生成的UserServiceRpc_Stub类没有提供默认的构造函数,而只提供了一个含有google::protobuf::RpcChannel* channe参数的构造函数,而且类中的Login()方法和GetFriendLists方法也都是虚函数,在两个方法的底层都调用了RpcChannel类的CallMethod方法。由于RpcChannel 类中的CallMethod方法是一个纯虚函数,因此我们需要重新定义一个类MyRpcChannel继承RpcChannel,在派生类MyRpcChannel中对继承而来的CallMethod方法进行重写。在构造UserServiceRpc_Stub类时,直接向其传递一个MyRpcChannel对象,即可调用派生类中重写函数。


如何将本地服务发布成rpc服务?

用户要将本地服务发布成一个可分布式部署的rpc服务,首先需要通过在user.proto配置文件中写出描述这个rpc方法的方法名字、参数类型以及返回值的响应类型。然后通过在终端执行protoc user.proto --cpp_out=./生成user.pb.ccuser.pb.h,这样就相当于rpc调用方和rpc提供方就生成了一个协议。在rpc方法的提供方,我们可以将本地服务UserServiceUerServiceRpc继承而来,然后重写Login()方法,重写好之后由框架对Login()方法进行直接调用。最后在服务发布方初始化调用的框架MprpcApplication::Init(argc, argv);,定义一个rpc网络服务对象RpcProvider provider;,把UserService对象发布到rpc节点上provider.NotifyService(new UserService());,启动一个rpc服务发布节点provider.Run();Run以后,进程进入阻塞状态,等待远程的rpc调用请求,这样就完成了将本地服务发布成rpc远程服务。

example/user.proto

syntax = "proto3";package fixbug;option cc_generic_services = true;message ResultCode
{int32 errcode = 1; bytes errmsg = 2;
}message LoginRequest
{bytes name = 1;bytes pwd = 2;
}message LoginResponse
{ResultCode result = 1;bool sucess = 2;
}message RegisterRequest
{uint32 id = 1;bytes name = 2;bytes pwd = 3;
}message RegisterResponse
{ResultCode result = 1;bool sucess = 2;
}service UserServiceRpc
{rpc Login(LoginRequest) returns(LoginResponse);rpc Register(RegisterRequest) returns(RegisterResponse);
}

example/callee/usrservice.cc

#include <iostream>
#include <string>
#include "user.pb.h"
#include "mprpcapplication.h"
#include "rpcprovider.h"/*
UserService原来是一个本地服务,提供了两个进程内的本地方法,Login和GetFriendLists
*/
class UserService : public fixbug::UserServiceRpc // 使用在rpc服务发布端(rpc服务提供者)
{
public:bool Login(std::string name, std::string pwd){std::cout << "doing local service: Login" << std::endl;std::cout << "name:" << name << " pwd:" << pwd << std::endl;  return false;}bool Register(uint32_t id, std::string name, std::string pwd){std::cout << "doing local service: Register" << std::endl;std::cout << "id:" << id << "name:" << name << " pwd:" << pwd << std::endl;return true;}/*重写基类UserServiceRpc的虚函数 下面这些方法都是框架直接调用的1. caller   ===>   Login(LoginRequest)  => muduo =>   callee 2. callee   ===>    Login(LoginRequest)  => 交到下面重写的这个Login方法上了*/void Login(::google::protobuf::RpcController* controller,const ::fixbug::LoginRequest* request,::fixbug::LoginResponse* response,::google::protobuf::Closure* done){// 框架给业务上报了请求参数LoginRequest,应用获取相应数据做本地业务std::string name = request->name();std::string pwd = request->pwd();// 做本地业务bool login_result = Login(name, pwd); // 把响应写入  包括错误码、错误消息、返回值fixbug::ResultCode *code = response->mutable_result();code->set_errcode(0);code->set_errmsg("");response->set_sucess(login_result);// 执行回调操作   执行响应对象数据的序列化和网络发送(都是由框架来完成的)done->Run();}void Register(::google::protobuf::RpcController* controller,const ::fixbug::RegisterRequest* request,::fixbug::RegisterResponse* response,::google::protobuf::Closure* done){uint32_t id = request->id();std::string name = request->name();std::string pwd = request->pwd();bool ret = Register(id, name, pwd);response->mutable_result()->set_errcode(0);response->mutable_result()->set_errmsg("");response->set_sucess(ret);done->Run();}
};int main(int argc, char **argv)
{// 调用框架的初始化操作MprpcApplication::Init(argc, argv);// provider是一个rpc网络服务对象。把UserService对象发布到rpc节点上RpcProvider provider;provider.NotifyService(new UserService());// 启动一个rpc服务发布节点   Run以后,进程进入阻塞状态,等待远程的rpc调用请求provider.Run();return 0;
}

当完成了rpc服务提供方的代码后,接下来我们就来看看如何设计和编写的底层框架的代码的,首先关注框架的初始化操作MprpcApplication::Init(argc, argv);,我们在src目录下创建mprpcapplication.hmprpcapplication.cc
mprpcapplication.h

#pragma once#include "mprpcconfig.h"
#include "mprpcchannel.h"
#include "mprpccontroller.h"// mprpc框架的基础类,负责框架的一些初始化操作
class MprpcApplication
{
public:static void Init(int argc, char **argv);static MprpcApplication& GetInstance();static MprpcConfig& GetConfig();
private:static MprpcConfig m_config;MprpcApplication(){}MprpcApplication(const MprpcApplication&) = delete;MprpcApplication(MprpcApplication&&) = delete;
};

mprpcapplication.cc

#include "mprpcapplication.h"
#include <iostream>
#include <unistd.h>
#include <string>MprpcConfig MprpcApplication::m_config;void ShowArgsHelp()
{std::cout<<"format: command -i <configfile>" << std::endl;
}void MprpcApplication::Init(int argc, char **argv)
{if (argc < 2){ShowArgsHelp();exit(EXIT_FAILURE);}int c = 0;std::string config_file;while((c = getopt(argc, argv, "i:")) != -1){switch (c){case 'i':config_file = optarg;break;case '?':ShowArgsHelp();exit(EXIT_FAILURE);case ':':ShowArgsHelp();exit(EXIT_FAILURE);default:break;}}// 开始加载配置文件了 rpcserver_ip=  rpcserver_port   zookeeper_ip=  zookepper_port=m_config.LoadConfigFile(config_file.c_str());// std::cout << "rpcserverip:" << m_config.Load("rpcserverip") << std::endl;// std::cout << "rpcserverport:" << m_config.Load("rpcserverport") << std::endl;// std::cout << "zookeeperip:" << m_config.Load("zookeeperip") << std::endl;// std::cout << "zookeeperport:" << m_config.Load("zookeeperport") << std::endl;
}MprpcApplication& MprpcApplication::GetInstance()
{static MprpcApplication app;return app;
}MprpcConfig& MprpcApplication::GetConfig()
{return m_config;
}

当获取到配置文件以后,加载配置文件的函数LoadConfigFile()以及查询配置项信息的函数Load()mprpcconfig.hmprpcconfig.cc中定义和实现。
mprpcconfig.h

#pragma once#include <unordered_map>
#include <string>// rpcserverip   rpcserverport    zookeeperip   zookeeperport
// 框架读取配置文件类
class MprpcConfig
{
public:// 负责解析加载配置文件void LoadConfigFile(const char *config_file);// 查询配置项信息std::string Load(const std::string &key);
private:std::unordered_map<std::string, std::string> m_configMap;// 去掉字符串前后的空格void Trim(std::string &src_buf);
};

mprpcconfig.cc

#include "mprpcconfig.h"#include <iostream>
#include <string>// 负责解析加载配置文件
void MprpcConfig::LoadConfigFile(const char *config_file)
{FILE *pf = fopen(config_file, "r");if (nullptr == pf){std::cout << config_file << " is note exist!" << std::endl;exit(EXIT_FAILURE);}// 1.注释   2.正确的配置项 =    3.去掉开头的多余的空格 while(!feof(pf)){char buf[512] = {0};fgets(buf, 512, pf);// 去掉字符串前面多余的空格std::string read_buf(buf);Trim(read_buf);// 判断#的注释if (read_buf[0] == '#' || read_buf.empty()){continue;}// 解析配置项int idx = read_buf.find('=');if (idx == -1){// 配置项不合法continue;}std::string key;std::string value;key = read_buf.substr(0, idx);Trim(key);// rpcserverip=127.0.0.1\nint endidx = read_buf.find('\n', idx);value = read_buf.substr(idx+1, endidx-idx-1);Trim(value);m_configMap.insert({key, value});}fclose(pf);
}// 查询配置项信息
std::string MprpcConfig::Load(const std::string &key)
{auto it = m_configMap.find(key);if (it == m_configMap.end()){return "";}return it->second;
}// 去掉字符串前后的空格
void MprpcConfig::Trim(std::string &src_buf)
{int idx = src_buf.find_first_not_of(' ');if (idx != -1){// 说明字符串前面有空格src_buf = src_buf.substr(idx, src_buf.size()-idx);}// 去掉字符串后面多余的空格idx = src_buf.find_last_not_of(' ');if (idx != -1){// 说明字符串后面有空格src_buf = src_buf.substr(0, idx+1);}
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/150598.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

在Android中实现动态应用图标

在Android中实现动态应用图标 你可能已经遇到过那些能够完成一个神奇的技巧的应用程序——在你的生日时改变他们的应用图标&#xff0c;然后无缝切换回常规图标。这是一种引发你好奇心的功能&#xff0c;让你想知道&#xff0c;“他们到底是如何做到的&#xff1f;”。嗯&…

HTML 笔记 表格

1 表格基本语法 tr&#xff1a;table row th&#xff1a;table head 2 表格属性 2.1 基本属性 表格的基本属性是指表格的行、列和单元格但并不是每个表格的单元格大小都是统一的&#xff0c;所以需要设计者通过一些属性参数来修改表格的样子&#xff0c;让它们可以更更多样…

VR全景展示带来旅游新体验,助力旅游业发展!

引言&#xff1a; VR&#xff08;虚拟现实&#xff09;技术正以惊人的速度改变着各行各业&#xff0c;在旅游业中&#xff0c;VR全景展示也展现了其惊人的影响力&#xff0c;为景区带来了全新的宣传机会和游客体验。 一&#xff0e;什么是VR全景展示&#xff1f; VR全景展示是…

华硕平板k013me176cx线刷方法

1.下载adb刷机工具, 或者刷机精灵 2.下载刷机rom包 华硕asus k013 me176cx rom固件刷机包-CSDN博客 3.平板进入刷机界面 进入方法参考&#xff1a; ASUS (k013) ME176CX不进入系统恢复出厂设置的方法-CSDN博客 4.解压ME176C-CN-3_2_23_182.zip&#xff0c;把UL-K013-CN-3.2.…

软件测试面试之问——角色扮演

作为软件测试工程师&#xff0c;在求职面试中经常会被问到这样一个问题&#xff1a;你认为测试工程师在企业中扮演着什么样的角色呢&#xff1f; 某度百科是这样概括的&#xff1a;“软件测试工程师在一家软件企业中担当的是‘质量管理’角色&#xff0c;及时发现软件问题并及…

2.5 数字传输系统

笔记&#xff1a; 针对这一节的内容&#xff0c;我为您提供一个笔记的整理方法。将内容按重要性、逻辑关系进行组织&#xff0c;再进行简化。 ## 2.5 数字传输系统 ### 背景介绍&#xff1a; 1. **早期电话网**&#xff1a;市话局到用户采用双绞线电缆&#xff0c;长途干线采…

css的gap设置元素之间的间隔

在felx布局中可以使用gap来设置元素之间的间隔&#xff1b; .box{width: 800px;height: auto;border: 1px solid green;display: flex;flex-wrap: wrap;gap: 100px; } .inner{width: 200px;height: 200px;background-color: skyblue; } <div class"box"><…

【Unity】RenderFeature笔记

【Unity】RenderFeature笔记 RenderFeature是在urp中添加的额外渲染pass&#xff0c;并可以将这个pass插入到渲染列队中的任意位置。内置渲染管线中Graphics 的功能需要在RenderFeature里实现,常见的如DrawMesh和Blit ​ 可以实现的效果包括但不限于 后处理&#xff0c;可以编写…

访问控制、RBAC和ABAC模型

访问控制、RBAC和ABAC模型 访问控制 访问控制的目的是保护对象&#xff08;数据、服务、可执行应用该程序、网络设备或其他类型的信息技术&#xff09;不受未经授权的操作的影响。操作包括&#xff1a;发现、读取、创建、编辑、删除和执行等。 为实现访问控制&#xff0c; 计…

JavaScript系列从入门到精通系列第十六篇:JavaScript使用函数作为属性以及枚举对象中的属性

文章目录 前言 1&#xff1a;对象属性可以是函数 2&#xff1a;对象属性函数被称为方法 一&#xff1a;枚举对象中的属性 1&#xff1a;for...in 枚举对象中的属性 前言 1&#xff1a;对象属性可以是函数 对象的属性值可以是任何的数据类型&#xff0c;也可以是函数。 v…

linux系统中常见注册函数的使用方法

大家好&#xff0c;今天给大家分享一下&#xff0c;linux系统中常见的注册函数register_chrdev_region()、register_chrdev()、 alloc_chrdev_region()的使用方法​。 一、函数包含的头文件&#xff1a; 分配设备编号&#xff0c;注册设备与注销设备的函数均在fs.h中申明&…

mac文件为什么不能拖进U盘?

对于Mac用户来说&#xff0c;可能会遭遇一些烦恼&#xff0c;比如在试图将文件从Mac电脑拖入U盘时&#xff0c;却发现文件无法成功传输。这无疑给用户带来了很大的不便。那么&#xff0c;mac文件为什么不能拖进U盘&#xff0c;看完这篇你就知道了。 一、U盘的读写权限问题 如果…

[Python入门教程]01 Python开发环境搭建

Python开发环境搭建 本文介绍python开发环境的安装&#xff0c;使用anaconda做环境管理&#xff0c;VS code写代码。搭建开发环境是学习的第一步&#xff0c;本文将详细介绍anaconda和vs code的安装过程&#xff0c;并测试安装结果。 视频教程链接&#xff1a;https://www.bil…

localhost和127.0.0.1都可以访问项目,但是本地的外网IP不能访问

使用localhost和127.0.0.1都可以访问接口&#xff0c;比如&#xff1a; http://localhost:8080/zhgl/login/login-fy-list或者 http://127.0.0.1:8080/zhgl/login/login-fy-list返回json {"_code":10000,"_msg":"Success","_data":…

毛玻璃用户卡交互

效果展示 页面结构组成 从效果展示可以看到&#xff0c;此效果都是比较常规的。主要的核心就是卡片的悬停效果。 CSS 知识点 backdrop-filter 回顾transitiontransform 页面基础布局实现 <section><div class"container"><div class"card&q…

基于虚拟阻抗的下垂控制——孤岛双机并联Simulink仿真

&#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️&#x1f4a5;&#x1f4a5; &#x1f3c6;博主优势&#xff1a;&#x1f31e;&#x1f31e;&#x1f31e;博客内容尽量做到思维缜密&#xff0c;逻辑清晰&#xff0c;为了方便读者。 ⛳️座右铭&a…

回顾C++

大一的时候学过C&#xff0c;当时学得也不深&#xff0c;考试也是糊弄过去的&#xff0c;最近刷力扣的时候&#xff0c;决定一边刷题&#xff0c;一边复习和学习C&#xff0c;在此记录一些C的知识点。反正遇到一点就记录一点&#xff0c;会一直更新。

jwt的基本介绍

说出我的悲惨故事给大家乐呵乐呵&#xff1a;公司刚来了一个实习生&#xff0c;老板让他写几个接口给我&#xff0c;我页面还没画完呢。他就把接口给我了&#xff0c;我敲开心&#xff0c;第一次见这么高效率的后端。但我很快就笑不出来了。他似乎不知道HTTP通信是无状态的。他…

深入解读redis的zset和跳表【源码分析】

1.基本指令 部分指令&#xff0c;涉及到第4章的api&#xff0c;没有具体看实现&#xff0c;但是逻辑应该差不多。 zadd <key><score1><value1><score2><value2>... 将一个或多个member元素及其score值加入到有序集key当中。根据zslInsert zran…

手把手教你开发律师法律咨询小程序

随着科技的快速发展&#xff0c;移动互联网已经成为人们获取信息和服务的主要途径之一。对于律师和法律机构来说&#xff0c;开发一个律师法律咨询小程序&#xff0c;可以更好地满足用户的需求&#xff0c;提供便捷的法律咨询服务。本文将引导您如何使用乔拓云网这个第三方制作…