分布式通信框架

01 项目简介

技术栈

  • 集群和分布式概念以及原理
  • RPC远程过程调用原理以及实现
  • Protobuf/数据序列化以及反序列化协议
  • ZooKeeper分布式一致性协调服务应用及编程
  • muduo网络库编程
  • conf配置文件读取
  • CMake构建项目集成编译环境
  • github管理项目

02 集群和分布式理论讲解

请添加图片描述
**集群的概念:**每一台服务器独立运行一个工程的所有模块。
**集群的优点:**用户的并发量提升了;
缺点:

  • 项目代码需要需要修改的话,需要重新编译,多次部署;
  • 不能根据模块的需求,配置到相应的硬件资源上。因为各个模块并没有分开去部署到不同的机器上。比如:后台管理要求的并发量很低,却部署到了多台服务器上,根本没有必要。

**分布式:**一个工程拆分了很多模块,每一个模块独立部署在一个服务器主机上,所有服务器协同工作共同提供服务,每一台服务器称作分布式的一个节点,根据节点的并发要求,对一个节点可以再做节点模块集群部署。

上图为单机聊天服务器,下图为集群聊天服务器
上图为单机聊天服务器,下图为集群聊天服务器。

单机版的聊天服务器的性能、或设计上的一些瓶颈有哪些?

  • 首先,受限于硬件资源,聊天服务器所能承受的并发量时受限的;
  • 假设用户管理中某个注销的函数出了bug,那么修改之后整个项目必须重新编译,重新部署。所以说,任意模块的修改都会导致整个项目代码重新编译、部署,费时费力;
  • 系统中,有些模块是属于CPU密集型的,有些模块是IO密集型的,造成各模块对于硬件资源的需求是不一样的;那么将所有模块放到一个服务器中对资源的利用是不合理的。

分布式系统解决了上述所有缺点。

分布式有哪些缺点呢?

  1. 各模块可能会有大量的重复代码;
  2. 各个模块之间的访问会变得复杂;
    请添加图片描述

05 RPC的通信原理以及项目的技术选型

请添加图片描述
请添加图片描述
请添加图片描述

06 项目环境搭建介绍

protobuf安装

GitHub 地址:
https://github.com/protocolbuffers/protobuf

官方文档地址:
https://developers.google.com/protocol-buffers/

Releases 下载地址:
https://github.com/protocolbuffers/protobuf/releases

首先去https://github.com/protocolbuffers/protobuf/releases中找到:
在这里插入图片描述
下载protobuf-cpp-3.21.9.tar.gz
https://github.com/protocolbuffers/protobuf/releases/download/v21.9/protobuf-cpp-3.21.9.tar.gz

# 安装所需配置工具
jixu@ubuntu:~/Downloads/protobuf-3.21.9$ sudo su
jixu@ubuntu:~/Downloads/protobuf-main$ apt-get install autoconf automake libtool curl make g++ unzip
root@ubuntu:/home/jixu/Desktop# sudo apt-get install build-essential
root@ubuntu:/home/jixu/Downloads/protobuf-3.21.9# ./autogen.sh
root@ubuntu:/home/jixu/Downloads/protobuf-3.21.9# ./configure
root@ubuntu:/home/jixu/Desktop# make
root@ubuntu:/home/jixu/Downloads/protobuf-3.21.9# make check
root@ubuntu:/home/jixu/Downloads/protobuf-3.21.9# make install
# 报错 添加环境变量
root@ubuntu:/home/jixu/Desktop# sudo ldconfig
root@ubuntu:/home/jixu/Desktop# protoc --version
libprotoc 3.21.9
# 安装成功

卸载protobuf

sudo rm /usr/local/bin/protoc  //执行文件
sudo rm -rf /usr/local/include/google //头文件
sudo rm -rf /usr/local/lib/libproto* //库文件

07 protobuf实践讲解一√

简单使用示例:

syntax = "proto3"; // 声明了protobuf的版本

package fixbug; // 声明了代码所在的包(对于C++来说就是namespace)

option cc_generic_services = true;

// 登录请求的消息类型
message LoginRequest{
    string name = 1; // 1代表第一个字段
    string pwd = 2; // 2代表第二个字段
}

// 登录响应的消息类型
message LoginResponse{
    int32 errcode = 1;
    string errmsg = 2;
    bool success = 3;
}

生成代码:

jixu@ubuntu:~/Mycode/ShiLei/mprpc$ cd test/
jixu@ubuntu:~/Mycode/ShiLei/mprpc/test$ cd protobuf/
jixu@ubuntu:~/Mycode/ShiLei/mprpc/test/protobuf$ protoc test.proto --cpp_out=./

测试代码:

#include <iostream>
#include "test.pb.h"
using namespace fixbug;

int main(){
    // 封装了login请求对象的数据
    LoginRequest req;
    req.set_name("zhang san");
    req.set_pwd("123456");
    // 对象数据序列化 -> char*;
    std::string send_str;
    if(req.SerializeToString(&send_str)){
        std::cout << send_str.c_str() << std::endl;
    }

    // 反序列化 从send_str反序列化一个login请求
    LoginRequest reqB;
    if(reqB.ParseFromString(send_str)){
        std::cout << reqB.name() << std::endl;
        std::cout << reqB.pwd() << std::endl;
    }

    return 0;
}

编译:

jixu@ubuntu:~/Mycode/ShiLei/mprpc/test/protobuf$ g++ -g -o test_proto main.cpp test.pb.cc -lprotobuf
jixu@ubuntu:~/Mycode/ShiLei/mprpc/test/protobuf$ ./test_proto 

        zhang san123456
zhang san
123456

jixu@jixu-ubuntu:~/Mycode/ShiLei/mprpc/test/protobuf$ gdb test_proto -silent
Reading symbols from test_proto...
(gdb) b 15
Breakpoint 1 at 0x4a10: file main.cpp, line 15.
(gdb) r
Starting program: /home/jixu/Mycode/ShiLei/mprpc/test/protobuf/test_proto 
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".

Breakpoint 1, main () at main.cpp:15
15              std::cout << send_str.c_str() << std::endl;
(gdb) p str
$1 = 0x5555555747b0 "\n\tzhang san\022\006\061\062\063\064\065\066"

08 protobuf实践讲解二√

proto文件中,string替换成bytes,会提高一些效率。代码上使用没有任何改变。
感觉在一种消息中嵌套另一种类型时,我想填充其中的数据时都要使用返回的指针去填充它。

然后主要介绍一下protobuf中列表的使用:

syntax = "proto3"; // 声明了protobuf的版本

package fixbug; // 声明了代码所在的包(对于C++来说就是namespace)

option cc_generic_services = true; // 启用rpc



// 可以发现每个响应类型里面都有errcode、errmsg两个消息类型,很麻烦。因此在这里把它们封装起来:
message ResultCode{
    int32 errcode = 1;
    bytes errmsg = 2;
}

// 登录请求的消息类型
message LoginRequest{
    bytes name = 1; // 1代表第一个字段
    bytes pwd = 2; // 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; // 用repeated定义一个列表类型
}
# 生成
jixu@ubuntu:~/Mycode/ShiLei/mprpc/test/protobuf$ protoc test.proto --cpp_out=./

测试代码:

#include "test.pb.h"
#include <iostream>
#include <string>
using namespace fixbug;

int main()
{

    GetFriendListsResponse rsp;
    ResultCode *rc = rsp.mutable_result();
    rc->set_errcode(0);

    User *user1 = rsp.add_friend_list();
    user1->set_name("zhang san");
    user1->set_age(20);
    user1->set_sex(User::MAN);

    User *user2 = rsp.add_friend_list();
    user2->set_name("li si");
    user2->set_age(22);
    user2->set_sex(User::MAN);

    std::cout << rsp.friend_list_size() << std::endl;
    for(int i  = 0; i <rsp.friend_list_size(); ++i){
        std::cout << rsp.friend_list(i).name() << std::endl;
    }
    return 0;
}

编译:

jixu@ubuntu:~/Mycode/ShiLei/mprpc/test/protobuf$ g++ -o test_proto main.cpp test.pb.cc -lprotobuf
jixu@ubuntu:~/Mycode/ShiLei/mprpc/test/protobuf$ ./test_proto 
2
zhang san
li si

09 protobuf实践讲解三

在protobuf里面怎么定义描述rpc方法的类型 - service
注意定义rpc方法时必须要定义option选项。

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);
}
# 生成
jixu@ubuntu:~/Mycode/ShiLei/mprpc/test/protobuf$ protoc test.proto --cpp_out=./

在这里插入图片描述
在这里插入图片描述

10 protobuf实践讲解四

在这里插入图片描述
在这里插入图片描述

11 本地服务怎么发布成rpc服务一√

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 success = 2;
}

service UserServiceRpc{
    rpc Login(LoginRequest) returns(LoginResponse);
}
# 生成
jixu@ubuntu:~/Mycode/ShiLei/mprpc/test/protobuf$ protoc test.proto --cpp_out=./
#include <iostream>
#include <string>
#include "user.pb.h"
using namespace std;
using namespace fixbug;

#ifdef LOCAL

// 假设UserService 当前是一个本地服务,提供了两个进程内的本地方法 Login和GetFriendLists
class UserService{
public:
    bool Login(std::string name, std:string pwd){
        cout << "doing local service:Login" << endl;
        cout << "name:" << name << " pwd:" << pwd << endl;
    }
};

#else

class UserService: public UserServiceRpc{
public:
    bool Login(std::string name, string pwd){
        cout << "doing local service:Login" << endl;
        cout << "name:" << name << " pwd:" << pwd << endl;
    }

    // 重写基类UserServiceRpc的虚函数 下面这些方法都是框架直接调用的
    virtual void Login(::PROTOBUF_NAMESPACE_ID::RpcController* controller,
                       const ::fixbug::LoginRequest* request,
                       ::fixbug::LoginResponse* response,
                       ::google::protobuf::Closure* done){

    }
};



#endif


int main(){
    
    return 0;
}

12 本地服务怎么发布成rpc服务二√

#include <iostream>
#include <string>
#include "user.pb.h"
using namespace std;
using namespace fixbug;

#ifdef LOCAL // 假设UserService 当前是一个本地服务,提供了两个进程内的本地方法 Login和GetFriendLists

class UserService{
public:
    bool Login(std::string name, std:string pwd){
        cout << "doing local service:Login" << endl;
        cout << "name:" << name << " pwd:" << pwd << endl;
    }
};

#else

class UserService: public UserServiceRpc{
public:
    // 本地业务
    bool Login(std::string name, string pwd){
        cout << "doing local service:Login" << endl;
        cout << "name:" << name << " pwd:" << pwd << endl;
        
    }

    // 重写基类UserServiceRpc的虚函数 下面这些方法都是框架直接调用的
    // caller ===> Login(LoginRequest) ==> muduo ==> callee
    // callee ===> Login(LoginRequest) => 交到下面重写的这个Login方法
    virtual void Login(::google::protobuf::RpcController* controller,
                       const ::fixbug::LoginRequest* request, // 函数参数
                       ::fixbug::LoginResponse* response, // 函数返回值
                       ::google::protobuf::Closure* done){ 
        // 框架给业务上报了请求参数LoginRequest,应用获取相应数据做本地业务
        string name = request->name();
        string pwd = request->pwd();
        // 做本地业务
        bool login_result = Login(name, pwd);
        // 把响应写入
        ResultCode* code = response->mutable_result();
        code->set_errcode(0);
        code->set_errmsg("");
        response->set_success(login_result);
        
        // 执行回调操作, 执行响应对象数据的序列化和网络发送(都是由框架来完成的)
        done->run(); 
    }
};

#endif

int main(){
    
    return 0;
}

13 Mprpc框架基础类设计

14 Mprpc框架项目动态库编译

首先先定义一个.proto文件作文协议。

15 Mprpc框架的配置文件加载一×

这个用法需要查一下

int c = 0;
    std::string config_file;
    while(c = getopt(argc, argv, "i:") != -1){ // !!! 这里写的是啥  后面再看
        switch(c){
            case 'i':
                config_file = optarg;
                break;
            case '?':
                std::cout << "invalid args!" << std::endl;
                showArgsHelp();
                exit(EXIT_FAILURE);
            case ':':
                std::cout << "need <configfile>!" << std::endl;
                showArgsHelp();
                exit(EXIT_FAILURE);
            default:
                break;
        }
    }

16 Mprpc框架的配置文件加载二

    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;

output:

jixu@ubuntu:~/Mycode/ShiLei/mprpc/bin$ ./provider -i ./test.conf 
rpcserverip:127.0.0.1

rpcserverport:8000

zookeeperip:127.0.0.1

zookeeperport:5000
jixu@ubuntu:~/Mycode/ShiLei/mprpc/bin$ 

调试了一下为什么多出一个换行符,因为配置文件中,每行末尾会有一个换行符。

17 本地服务怎么发布成rpc服务一√

18 RpcProvider发布服务方法一×

理论太多,代码写完了再看。

19 RpcProvider发布服务方法二×

主要是实现RpcProvider发布服务的代码。

// 这里是框架给外部使用的,可以发布rpc方法的函数接口
void RpcProvider::NotifyService(google::protobuf::Service *service){

    ServiceInfo service_info;
    // 获取了服务对象的描述信息
    const google::protobuf::ServiceDescriptor* pserviceDesc = service->GetDescriptor();
    // 获取服务的名字
    std::string service_name = pserviceDesc->name();
    // 获取服务对象service的方法的数量
    int methodCnt = pserviceDesc->method_count();
    for(int i = 0; i < methodCnt; ++i){
        const google::protobuf::MethodDescriptor* pmethodDesc = pserviceDesc->method(i);
        std::string method_name = pmethodDesc->name();
        service_info.m_methodMap.insert({method_name, pmethodDesc});
    }
    service_info.m_service = service;
    m_serviceMap.insert({service_name, service_info});
    std::cout << "service_name: " << service_name <<std::endl;
}

20 RpcProvider分发rpc服务一√

主要是用protobuf实现一下rpc发布者与消费者之间通讯的协议:

syntax = "proto3";

package mprpc;

message RpcHeader{
    bytes service_name = 1;
    bytes method_name = 2;
    int32 args_size = 3;
}

header_size是为了记录消息的长度。然后把消息交给protobuf去反序列化,解析出服务名和方法名,还有参数的大小。把序列化的参数字符串再交给响应的proto类去反序列化。

void RpcProvider::OnMessage(const muduo::net::TcpConnectionPtr& conn, muduo::net::Buffer* buffer , muduo::Timestamp){
    std::string recv_buf = buffer->retrieveAllAsString();
    
    // 从字符流中读取前四个字节的内容
    uint32_t header_size = 0;
    recv_buf.copy((char*)&header_size, 4, 0);
    // 根据head_size读取数据头的原始字符流
    std::string rpc_header_str = recv_buf.substr(4, header_size);
    mprpc::RpcHeader rpcHeader;
    std::string service_name;
    std::string method_name;
    uint32_t args_size;
    if(rpcHeader.ParseFromString(rpc_header_str)){
        // 数据反序列化成功
        service_name = rpcHeader.service_name();
        method_name = rpcHeader.method_name();
        args_size = rpcHeader.args_size();
    }else{
        // 数据反序列化失败
        std::cout << "rpc_header_str: " << rpc_header_str << " parse error" << std::endl;
        return;
    }
    // 获取rpc方法参数的字符流数据
    std::string args_str = recv_buf.substr(4 + header_size, args_size);

    // 打印调试信息
    std::cout << "===========================" << std::endl;
    std::cout << "header_size: " << header_size << std::endl;
    std::cout << "rpc_header_str: " << rpc_header_str << std::endl;
    std::cout << "service_name: " << service_name << std::endl;
    std::cout << "args_size: " << args_size << std::endl;


}

22 RpcProvider分发rpc服务二

22 RpcProvider的rpc响应回调实现

23 RpcChannel的调用过程

谁提供的rpc服务,谁就要来定义proto文件。

24 实现RPC方法的调用过程一 √

实现的是请求远程调用的数据序列化以及网络发送。

25 实现RPC方法的调用过程二

为什么连接失败就调用exit,发送失败就return呢?

后面关闭套接字,尝试改成用智能指针使用自定义删除器。

26 点对点通信功能测试

服务端:

jixu@ubuntu:~/Mycode/ShiLei/mprpc/bin$ ./provider -i test.conf
rpcserverip:127.0.0.1
rpcserverport:8000
zookeeperip:127.0.0.1
zookeeperport:5000
service_name: UserServiceRpc
method_name: Login
RpcProvider start service at ip:127.0.0.1 port:8000
20230321 14:10:23.240633Z 17999 INFO  TcpServer::newConnection [RpcProvider] - new connection [RpcProvider-127.0.0.1:8000#1] from 127.0.0.1:59888 - TcpServer.cc:80
===========================
header_size: 25
rpc_header_str: 
UserServiceRpcLogin
service_name: UserServiceRpc
args_size: 19
===========================
doing local service:Login
name:zhang san pwd:123456
20230321 14:10:23.240916Z 17999 INFO  TcpServer::removeConnectionInLoop [RpcProvider] - connection RpcProvider-127.0.0.1:8000#1 - TcpServer.cc:109

客户端:

jixu@ubuntu:~/Mycode/ShiLei/mprpc/bin$ ./consumer -i test.conf
rpcserverip:127.0.0.1
rpcserverport:8000
zookeeperip:127.0.0.1
zookeeperport:5000
===========================
header_size: 25
rpc_header_str: 
UserServiceRpcLogin
service_name: UserServiceRpc
method_name: Login
args_str: 
        zhang san123456
===========================
rpc login response success :0

调试rpc login response success :0返回值不正确的问题:

jixu@ubuntu:~/Mycode/ShiLei/mprpc/bin$ gdb ./consumer -q
Reading symbols from ./consumer...
(gdb) b mprpcchannel.cpp:84
Breakpoint 1 at 0x1ba6d: file ../src/mprpcchannel.cpp, line 87.
(gdb) run -i test.conf
Starting program: /home/jixu/Mycode/ShiLei/mprpc/bin/consumer -i test.conf
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
rpcserverip:127.0.0.1
rpcserverport:8000
zookeeperip:127.0.0.1
zookeeperport:5000
===========================
header_size: 25
rpc_header_str: 
UserServiceRpcLogin
service_name: UserServiceRpc
method_name: Login
args_str: 
        zhang san123456
===========================

Breakpoint 1, MprpcChannel::CallMethod (this=0x55555558e6c0, method=0x55555559cca0, controller=0x0, 
    request=0x7fffffffdee0, response=0x7fffffffdec0, done=0x0) at ../src/mprpcchannel.cpp:87
87          char recv_buf[1024] = {0};
(gdb) n
88          int recv_size = 0;
(gdb) n
89          if(-1 == (recv_size = recv(clientfd, recv_buf, 1024, 0))){
(gdb) n
94          std::string response_str(recv_buf, 0, recv_size);
(gdb) n
95          if(response->ParseFromString(response_str)){
(gdb) n
100         close(clientfd);
(gdb) p recv_buf
$1 = "\n\000\020\001", '\000' <repeats 1019 times>
(gdb) n
101     }
(gdb) p response_str 
$2 = "\n" # !!!!!!!!!!!!!!!!!!!!
(gdb) 

在这里插入图片描述

27 Mprpc框架的应用示例

假设rpc服务方想要新发布一个方法Register。

  • 一、 那么首先要在proto 文件中添加:
    	message RegisterRequest{
        uint32 id = 1;
        bytes name = 1;
        bytes pwd = 2;
    }
    message RegisterResponse{
        ResultCode result = 1;
        bool success = 2;
    }
    
    
    service UserServiceRpc{
        rpc Login(LoginRequest) returns(LoginResponse);
        rpc Register(RegisterRequest) returns(RegisterResponse); // 新增
    }
    
  • 二 、生成proto代码:
    jixu@ubuntu:~/Mycode/ShiLei/mprpc/test/protobuf$ protoc test.proto --cpp_out=./
    
  • 编写业务代码:

28 RpcController控制模块实现 √

对于rpc服务的调用方来说,先定义一个代理对象Stub,传入一个channel,channel是框架实现的。
在远程调用的过程中,会有很多异常产生并直接return的情况,所以后续读函数的返回值response是没意义的。这时就要用到rpc方法的第一个参数Controller。
所以我们可以自定义一个MprpcController。没什么复杂的,就是记录rpc调用过程中可能发生的各种错误,然后在rpc调用返回后判断一下是否产生了错误,如果有错误就不继续解析response了。

29 日志系统设计实现一√

因为写日志是个磁盘I/O操作,所以我们不能占用业务的时间来写日志。
故创建一个消息队列,每个线程都可以往队尾写日志,同时新建一个线程,专门用于从队头取出日志写到日志文件中。

30 日志系统设计实现二√

31 异步日志缓冲队列实现

记得复习一下多线程。

32 Zookeeper分布式协调服务

33 zk服务配置中心和znode节点

zookeeper简介:什么是zookeeper

先按照这个教程装jdk:ubuntu安装jdk
再按照这个教程安装zookeeper:ubuntu安装zookeeper

# 启动服务
jixu@ubuntu:~/Downloads/zookeeper-3.4.10/bin$ ./zkServer.sh start
jixu@ubuntu:~/Documents/apache-zookeeper-3.7.1-bin/bin$ sudo netstat -tanp
[sudo] password for jixu: 
Active Internet connections (servers and established)
Proto Recv-Q Send-Q Local Address           Foreign Address         State       PID/Program name    
tcp6       0      0 :::2181                 :::*                    LISTEN      4946/java   
# 启动一个zkClient试试

zk也有一个服务器和一个客户端,与mysql是一样的,也就是要启动一个sql server,然后我们自己作为客户端
在这里插入图片描述

34 zk的watcher机制和原生api安装

ZK的原生开发API(C/C++接口)

 # 进入src/c下
 sudo ./configure
 sudo make
 sudo make install
 # 过程中会报错,需要把MakeFile中的-Weror(把警告当错误)删掉,再执行第二步

ZookKeeper原生提供了C和Java的客户端编程接口,但是使用起来相对复杂,几个弱点:
2 设置监听watcher只能是一次性的,每次触发后需要重复设置
3 znode节点只存储简单的byte字节数组,如果存储对象,需要自己转换对象生成字节数组。

35 封装zookeeper的客户端类

测试:

jixu@jixu-ubuntu:~/Documents/zookeeper-3.4.10/bin$ ./zkCli.sh
Connecting to localhost:2181
2023-04-14 04:12:01,818 [myid:] - INFO  [main:Environment@100] - Client environment:zookeeper.version=3.4.10-39d3a4f269333c922ed3db283be479f9deacaa0f, built on 03/23/2017 10:13 GMT
2023-04-14 04:12:01,821 [myid:] - INFO  [main:Environment@100] - Client environment:host.name=jixu-ubuntu
2023-04-14 04:12:01,821 [myid:] - INFO  [main:Environment@100] - Client environment:java.version=1.8.0_361
2023-04-14 04:12:01,822 [myid:] - INFO  [main:Environment@100] - Client environment:java.vendor=Oracle Corporation
2023-04-14 04:12:01,822 [myid:] - INFO  [main:Environment@100] - Client environment:java.home=/home/jixu/Documents/jdk-8u361-linux-x64/jdk1.8.0_361/jre
2023-04-14 04:12:01,822 [myid:] - INFO  [main:Environment@100] - Client environment:java.class.path=/home/jixu/Documents/zookeeper-3.4.10/bin/../build/classes:/home/jixu/Documents/zookeeper-3.4.10/bin/../build/lib/*.jar:/home/jixu/Documents/zookeeper-3.4.10/bin/../lib/slf4j-log4j12-1.6.1.jar:/home/jixu/Documents/zookeeper-3.4.10/bin/../lib/slf4j-api-1.6.1.jar:/home/jixu/Documents/zookeeper-3.4.10/bin/../lib/netty-3.10.5.Final.jar:/home/jixu/Documents/zookeeper-3.4.10/bin/../lib/log4j-1.2.16.jar:/home/jixu/Documents/zookeeper-3.4.10/bin/../lib/jline-0.9.94.jar:/home/jixu/Documents/zookeeper-3.4.10/bin/../zookeeper-3.4.10.jar:/home/jixu/Documents/zookeeper-3.4.10/bin/../src/java/lib/*.jar:/home/jixu/Documents/zookeeper-3.4.10/bin/../conf:.:/home/jixu/Documents/jdk-8u361-linux-x64/jdk1.8.0_361/lib:/home/jixu/Documents/jdk-8u361-linux-x64/jdk1.8.0_361/jre/lib:
2023-04-14 04:12:01,823 [myid:] - INFO  [main:Environment@100] - Client environment:java.library.path=/usr/java/packages/lib/amd64:/usr/lib64:/lib64:/lib:/usr/lib
2023-04-14 04:12:01,823 [myid:] - INFO  [main:Environment@100] - Client environment:java.io.tmpdir=/tmp
2023-04-14 04:12:01,823 [myid:] - INFO  [main:Environment@100] - Client environment:java.compiler=<NA>
2023-04-14 04:12:01,823 [myid:] - INFO  [main:Environment@100] - Client environment:os.name=Linux
2023-04-14 04:12:01,823 [myid:] - INFO  [main:Environment@100] - Client environment:os.arch=amd64
2023-04-14 04:12:01,823 [myid:] - INFO  [main:Environment@100] - Client environment:os.version=5.19.0-38-generic
2023-04-14 04:12:01,823 [myid:] - INFO  [main:Environment@100] - Client environment:user.name=jixu
2023-04-14 04:12:01,823 [myid:] - INFO  [main:Environment@100] - Client environment:user.home=/home/jixu
2023-04-14 04:12:01,823 [myid:] - INFO  [main:Environment@100] - Client environment:user.dir=/home/jixu/Documents/zookeeper-3.4.10/bin
2023-04-14 04:12:01,824 [myid:] - INFO  [main:ZooKeeper@438] - Initiating client connection, connectString=localhost:2181 sessionTimeout=30000 watcher=org.apache.zookeeper.ZooKeeperMain$MyWatcher@306a30c7
Welcome to ZooKeeper!
2023-04-14 04:12:01,834 [myid:] - INFO  [main-SendThread(localhost:2181):ClientCnxn$SendThread@1032] - Opening socket connection to server localhost/127.0.0.1:2181. Will not attempt to authenticate using SASL (unknown error)
JLine support is enabled
2023-04-14 04:12:01,840 [myid:] - INFO  [main-SendThread(localhost:2181):ClientCnxn$SendThread@876] - Socket connection established to localhost/127.0.0.1:2181, initiating session
2023-04-14 04:12:01,845 [myid:] - INFO  [main-SendThread(localhost:2181):ClientCnxn$SendThread@1299] - Session establishment complete on server localhost/127.0.0.1:2181, sessionid = 0x1876ead296f0003, negotiated timeout = 30000

WATCHER::

WatchedEvent state:SyncConnected type:None path:null
[zk: localhost:2181(CONNECTED) 0] ls /
[UserServiceRpc, zookeeper]
[zk: localhost:2181(CONNECTED) 1] ls /UserServiceRpc
[Login, Register]
[zk: localhost:2181(CONNECTED) 2] get /UserServiceRpc

cZxid = 0x2
ctime = Fri Apr 14 04:03:38 EDT 2023
mZxid = 0x2
mtime = Fri Apr 14 04:03:38 EDT 2023
pZxid = 0x4
cversion = 2
dataVersion = 0
aclVersion = 0
ephemeralOwner = 0x0
dataLength = 0
numChildren = 2
[zk: localhost:2181(CONNECTED) 3] get /UserServiceRpc/Login
127.0.0.1:8000
cZxid = 0x4
ctime = Fri Apr 14 04:03:38 EDT 2023
mZxid = 0x4
mtime = Fri Apr 14 04:03:38 EDT 2023
pZxid = 0x4
cversion = 0
dataVersion = 0
aclVersion = 0
ephemeralOwner = 0x1876ead296f0000
dataLength = 14
numChildren = 0
[zk: localhost:2181(CONNECTED) 4] 

测试心跳消息:

jixu@jixu-ubuntu:~/Mycode/ShiLei/mprpc$ sudo tcpdump -i lo port 2181
[sudo] password for jixu: 
tcpdump: verbose output suppressed, use -v[v]... for full protocol decode
listening on lo, link-type EN10MB (Ethernet), snapshot length 262144 bytes
04:15:39.282951 IP localhost.54054 > localhost.2181: Flags [P.], seq 2103290286:2103290298, ack 1666618150, win 512, options [nop,nop,TS val 3426300562 ecr 3426290551], length 12
04:15:39.283466 IP localhost.2181 > localhost.54054: Flags [P.], seq 1:21, ack 12, win 512, options [nop,nop,TS val 3426300563 ecr 3426300562], length 20
04:15:39.283495 IP localhost.54054 > localhost.2181: Flags [.], ack 21, win 512, options [nop,nop,TS val 3426300563 ecr 3426300563], length 0
04:15:48.872189 IP localhost.40294 > localhost.2181: Flags [P.], seq 2478688742:2478688746, ack 2537665670, win 512, options [nop,nop,TS val 3426310151 ecr 3426300142], length 4
04:15:48.872263 IP localhost.40294 > localhost.2181: Flags [P.], seq 4:12, ack 1, win 512, options [nop,nop,TS val 3426310152 ecr 3426300142], length 8
04:15:48.872403 IP localhost.2181 > localhost.40294: Flags [.], ack 12, win 512, options [nop,nop,TS val 3426310152 ecr 3426310151], length 0
04:15:48.872806 IP localhost.2181 > localhost.40294: Flags [P.], seq 1:21, ack 12, win 512, options [nop,nop,TS val 3426310152 ecr 3426310151], length 20
04:15:48.872826 IP localhost.40294 > localhost.2181: Flags [.], ack 21, win 512, options [nop,nop,TS val 3426310152 ecr 3426310152], length 0
04:15:49.291792 IP localhost.54054 > localhost.2181: Flags [P.], seq 12:24, ack 21, win 512, options [nop,nop,TS val 3426310571 ecr 3426300563], length 12
04:15:49.292329 IP localhost.2181 > localhost.54054: Flags [P.], seq 21:41, ack 24, win 512, options [nop,nop,TS val 3426310572 ecr 3426310571], length 20
04:15:49.292354 IP localhost.54054 > localhost.2181: Flags [.], ack 41, win 512, options [nop,nop,TS val 3426310572 ecr 3426310572], length 0
04:15:58.883220 IP localhost.40294 > localhost.2181: Flags [P.], seq 12:16, ack 21, win 512, options [nop,nop,TS val 3426320162 ecr 3426310152], length 4
04:15:58.883270 IP localhost.40294 > localhost.2181: Flags [P.], seq 16:24, ack 21, win 512, options [nop,nop,TS val 3426320163 ecr 3426310152], length 8
04:15:58.883452 IP localhost.2181 > localhost.40294: Flags [.], ack 24, win 512, options [nop,nop,TS val 3426320163 ecr 3426320162], length 0
04:15:58.884005 IP localhost.2181 > localhost.40294: Flags [P.], seq 21:41, ack 24, win 512, options [nop,nop,TS val 3426320163 ecr 3426320162], length 20
04:15:58.884057 IP localhost.40294 > localhost.2181: Flags [.], ack 41, win 512, options [nop,nop,TS val 3426320163 ecr 3426320163], length 0
04:15:59.294002 IP localhost.54054 > localhost.2181: Flags [P.], seq 24:36, ack 41, win 512, options [nop,nop,TS val 3426320573 ecr 3426310572], length 12
04:15:59.294719 IP localhost.2181 > localhost.54054: Flags [P.], seq 41:61, ack 36, win 512, options [nop,nop,TS val 3426320574 ecr 3426320573], length 20
04:15:59.294770 IP localhost.54054 > localhost.2181: Flags [.], ack 61, win 512, options [nop,nop,TS val 3426320574 ecr 3426320574], length 0
04:16:08.894239 IP localhost.40294 > localhost.2181: Flags [P.], seq 24:28, ack 41, win 512, options [nop,nop,TS val 3426330173 ecr 3426320163], length 4
04:16:08.894292 IP localhost.40294 > localhost.2181: Flags [P.], seq 28:36, ack 41, win 512, options [nop,nop,TS val 3426330174 ecr 3426320163], length 8
04:16:08.894488 IP localhost.2181 > localhost.40294: Flags [.], ack 36, win 512, options [nop,nop,TS val 3426330174 ecr 3426330173], length 0
04:16:08.895027 IP localhost.2181 > localhost.40294: Flags [P.], seq 41:61, ack 36, win 512, options [nop,nop,TS val 3426330174 ecr 3426330173], length 20
04:16:08.895053 IP localhost.40294 > localhost.2181: Flags [.], ack 61, win 512, options [nop,nop,TS val 3426330174 ecr 3426330174], length 0
04:16:09.305122 IP localhost.54054 > localhost.2181: Flags [P.], seq 36:48, ack 61, win 512, options [nop,nop,TS val 3426330584 ecr 3426320574], length 12
04:16:09.305727 IP localhost.2181 > localhost.54054: Flags [P.], seq 61:81, ack 48, win 512, options [nop,nop,TS val 3426330585 ecr 3426330584], length 20
04:16:09.305764 IP localhost.54054 > localhost.2181: Flags [.], ack 81, win 512, options [nop,nop,TS val 3426330585 ecr 3426330585], length 0
04:16:18.905794 IP localhost.40294 > localhost.2181: Flags [P.], seq 36:40, ack 61, win 512, options [nop,nop,TS val 3426340185 ecr 3426330174], length 4
04:16:18.905827 IP localhost.40294 > localhost.2181: Flags [P.], seq 40:48, ack 61, win 512, options [nop,nop,TS val 3426340185 ecr 3426330174], length 8
04:16:18.905988 IP localhost.2181 > localhost.40294: Flags [.], ack 48, win 512, options [nop,nop,TS val 3426340185 ecr 3426340185], length 0
04:16:18.906366 IP localhost.2181 > localhost.40294: Flags [P.], seq 61:81, ack 48, win 512, options [nop,nop,TS val 3426340186 ecr 3426340185], length 20
04:16:18.906385 IP localhost.40294 > localhost.2181: Flags [.], ack 81, win 512, options [nop,nop,TS val 3426340186 ecr 3426340186], length 0
04:16:19.316382 IP localhost.54054 > localhost.2181: Flags [P.], seq 48:60, ack 81, win 512, options [nop,nop,TS val 3426340596 ecr 3426330585], length 12
04:16:19.316834 IP localhost.2181 > localhost.54054: Flags [P.], seq 81:101, ack 60, win 512, options [nop,nop,TS val 3426340596 ecr 3426340596], length 20
04:16:19.316853 IP localhost.54054 > localhost.2181: Flags [.], ack 101, win 512, options [nop,nop,TS val 3426340596 ecr 3426340596], length 0
04:16:28.916699 IP localhost.40294 > localhost.2181: Flags [P.], seq 48:52, ack 81, win 512, options [nop,nop,TS val 3426350196 ecr 3426340186], length 4
04:16:28.916728 IP localhost.40294 > localhost.2181: Flags [P.], seq 52:60, ack 81, win 512, options [nop,nop,TS val 3426350196 ecr 3426340186], length 8
04:16:28.917008 IP localhost.2181 > localhost.40294: Flags [.], ack 60, win 512, options [nop,nop,TS val 3426350196 ecr 3426350196], length 0
04:16:28.917430 IP localhost.2181 > localhost.40294: Flags [P.], seq 81:101, ack 60, win 512, options [nop,nop,TS val 3426350197 ecr 3426350196], length 20
04:16:28.917444 IP localhost.40294 > localhost.2181: Flags [.], ack 101, win 512, options [nop,nop,TS val 3426350197 ecr 3426350197], length 0
04:16:29.318247 IP localhost.54054 > localhost.2181: Flags [P.], seq 60:72, ack 101, win 512, options [nop,nop,TS val 3426350598 ecr 3426340596], length 12
04:16:29.319942 IP localhost.2181 > localhost.54054: Flags [P.], seq 101:121, ack 72, win 512, options [nop,nop,TS val 3426350599 ecr 3426350598], length 20
04:16:29.320051 IP localhost.54054 > localhost.2181: Flags [.], ack 121, win 512, options [nop,nop,TS val 3426350599 ecr 3426350599], length 0

36 zk在项目上的应用实践√

37项目总结以及编译脚本√

框架总结

框架给rpc服务方提供:

MprpcApplication:mprpc应用类,主要是执行一些初始化操作,读取配置文件等等;
RpcProvider:框架提供的专门发布rpc服务的网络对象类,在该类中有以下几个成员函数:
NotifyService

  • **成员函数,用于发布rpc方法的函数接口,它的形参类型是google::protobuf::Service *service,rpc服务方编写的rpc服务类都继承于这个service基类。举例:UserService继承于UserServiceRpc,目的是重写其中的rpc方法;UserServiceRpc又继承于service。
  • 函数主要的作用是将服务以及服务对应的rpc方法存到map中,以供收到rpc请求时快速检索rpc调用方想调用的方法是什么。
// 这里是框架给外部使用的,可以发布rpc方法的函数接口
void RpcProvider::NotifyService(google::protobuf::Service *service){

    ServiceInfo service_info;
    // 获取了服务对象的描述符的指针 GetDescriptor是个纯虚函数,在proto生成的UserServiceRpc类中会重写这个方法,在该描述符中包括服务的名字,rpc方法的数量,同时还可以返回每个rpc方法的描述符,每个rpc方法的描述符中又包括方法的名字,从下面的代码可以看出
    const google::protobuf::ServiceDescriptor* pserviceDesc = service->GetDescriptor();
    // 获取服务的名字
    std::string service_name = pserviceDesc->name();
    std::cout << "service_name: " << service_name <<std::endl;
    // 获取服务对象service的方法的数量 
    int methodCnt = pserviceDesc->method_count();
    for(int i = 0; i < methodCnt; ++i){
        const google::protobuf::MethodDescriptor* pmethodDesc = pserviceDesc->method(i);
        std::string method_name = pmethodDesc->name();
        // 最关键的是在这一行,我们要把方法名和方法对应的描述符存储到一个map中!!!!!!
        service_info.m_methodMap.insert({method_name, pmethodDesc});
        std::cout << "method_name: " << method_name <<std::endl;
    }
    service_info.m_service = service; // 一个rpc服务节点可能有多个服务,每一个服务对应一个map
    m_serviceMap.insert({service_name, service_info});
    
}

Run():主要是启动网络服务,后面的OnMessage回调的过程比较重要。

// 启动rpc服务节点,开始提供rpc远程远程网络调用服务
void RpcProvider::Run(){
    // 组合了TcpServer
    std::unique_ptr<muduo::net::TcpServer> m_tcpserverPtr;
    std::string ip = MprpcApplication::GetInstance().GetConfig().Load("rpcserverip");
    uint16_t port = atoi(MprpcApplication::GetInstance().GetConfig().Load("rpcserverport").c_str());
    muduo::net::InetAddress address(ip, port);
    // 创建TcpServer对象
    muduo::net::TcpServer server(&m_eventLoop, address, "RpcProvider");
    // 绑定连接回调和消息读写回调方法 分离了网络代码和业务代码
    server.setConnectionCallback(std::bind(&RpcProvider::OnConnection, this, std::placeholders::_1));
    server.setMessageCallback(std::bind(&RpcProvider::OnMessage, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3));
    // 绑定muduo库的线程数量
    server.setThreadNum(4);


    // rpc服务启动,打印信息
    std::cout << "RpcProvider start service at ip:" << ip << " port:" << port << std::endl;
    // 启动网络服务
    server.start();
    m_eventLoop.loop();
}

OnMessage():在这个回调函数中:

  • 当收到从网上收到字节流时,首先要从数据的前四个字节提取出rpcHeader的长度,rpcHeader包括服务名,方法名,参数字符串的长度。
  • 通过proto反序列化出rpcHeader,就得到了包括服务名,方法名,参数字符串的长度。
  • 解析出参数字符串;
  • 这个时候就可以通过rpcHeader中的服务名,方法名在m_serviceMap中找到对应的rpc方法的描述符。
  • 再然后就可以通过rpc方法的描述符得到一个request对象和response对象。
	// 生成rpc方法调用的请求request和响应response参数
    google::protobuf::Message* request = service->GetRequestPrototype(method).New();
    if(!request->ParsePartialFromString(args_str)){
        std::cout << "request parse error, content: " << args_str << std::endl;
    }
    google::protobuf::Message* response = service->GetResponsePrototype(method).New();
  • 这时就可以把参数字符串反序列化到request对象中了。
  • 现在知道了服务名,方法名,方法参数,返回值类型,那么框架就可以通过这些来调用rpc方法了:
    // 给下面的method方法的调用,绑定一个Closure的回调函数
    google::protobuf::Closure* done = google::protobuf::NewCallback<RpcProvider, const muduo::net::TcpConnectionPtr&, google::protobuf::Message*>(this, &RpcProvider::SendRpcResponse, conn, response);

    // 在框架上根据远端rpc请求,调用当前rpc节点上发布的方法
    service->CallMethod(method, nullptr, request, response, done);
    

void RpcProvider::SendRpcResponse(const muduo::net::TcpConnectionPtr& conn, google::protobuf::Message* response){
    std::string response_str;
    if(response->SerializeToString(&response_str)){ // response进行序列化
        // 序列化成功后,通过网络把rpc方法执行的结果发送回给rpc的调用方
        conn->send(response_str);
    }
    else{
        std::cout << "serialize response_str error!" << std::endl;
    }
    conn->shutdown(); // 模拟http的短连接服务,由rpcProvider主动断开连接
}
  • CallMethod第二个参数controller的作用:记录rpc调用过程中发生的各种错误。

  • done !!!这个要好好看看:首先就是,这个callMethod是Service定义的纯虚函数,UserServiceRpc中重写了纯虚函数,然后用户定义的UserService继承了UserServiceRpc也就继承了这个类。同时用户在UserService也重写了rpc方法Login(),所以使用在UserService对象中调用CallMethod进而调用rpc方法Login会发生动态绑定,调用用户重写的Login(),并且在其中调用done->Run(),执行rpc方法的返回值数据的序列化和网络发送,也就是说,这个done,起一个回调函数的作用。done是由框架来定义的并且传递给用户的Rpc方法。那就有必要分析google::protobuf::NewCallback这个函数了:
    1 NewCallback是个函数模板,三个模板参数分别代表要调用的回调函数所属的类类型,回调函数的两个参数;
    2 NewCallback的形参分别是回调函数所属的类的指针,回调函数的函数指针,回调函数的两个参数。
    3 用这些参数来构造一个Closure类型的MethodClosure2对象,其中有个Run()方法,帮我们调用自己的回调函数。第三个参数代表这个临时构造的对象用完就要自己及时销毁。
    4 再看我们自己往NewCallback中传入了什么:this, &RpcProvider::SendRpcResponse, conn, response(RpcProvider的指针,回调函数的地址,Tcp连接对象,rpc方法的未序列化的返回值)。

template <typename Class, typename Arg1, typename Arg2>
inline Closure* NewCallback(Class* object, void (Class::*method)(Arg1, Arg2),
                            Arg1 arg1, Arg2 arg2) {
  return new internal::MethodClosure2<Class, Arg1, Arg2>(
    object, method, true, arg1, arg2);
}
template <typename Class, typename Arg1, typename Arg2>
class MethodClosure2 : public Closure {
 public:
  typedef void (Class::*MethodType)(Arg1 arg1, Arg2 arg2);

  MethodClosure2(Class* object, MethodType method, bool self_deleting,
                 Arg1 arg1, Arg2 arg2)
    : object_(object), method_(method), self_deleting_(self_deleting),
      arg1_(arg1), arg2_(arg2) {}
  ~MethodClosure2() {}

  void Run() override {
    bool needs_delete = self_deleting_;  // read in case callback deletes
    (object_->*method_)(arg1_, arg2_);
    if (needs_delete) delete this;
  }

所以说框架到底为rpc服务方提供了什么呢?它提供了rpcProvider类。
在NotifyService中接受rpc服务方传入的服务对象,帮忙发布了服务,意思是将服务和服务中的方法存到Map中。
当收到rpc调用方发来的请求数据时,rpcProvider类帮忙解析了请求头,进而调用服务方的本地方法,再把返回值发回给调用方。

给rpc调用方提供

rpc服务方要自己继承ServiceRpc实现子类;
对于rpc调用方来说,先看一下整体的rpc流程吧:

1 fixbug::UserServiceRpc_Stub stub(new MprpcChannel());
2 stub.Login(nullptr, &request, &response, nullptr);
3 在Login中:这个channel_就是框架定义的channel
channel_->CallMethod(descriptor()->method(0),
                       controller, request, response, done);
到此,函数的返回值也就存到response中了。

因此,我们要实现的也就是MprpcChannel类,它的作用就是序列化参数-发送请求-接受返回值。但是这里只有一个channel。那有多个channel怎么办?

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Golang分布式任务框架是使用Golang编程语言开发的一种工具,用于简化分布式环境下的任务调度和处理。 首先,Golang分布式任务框架提供了一个简单而强大的任务调度器,可以将任务分配给不同的节点进行并行处理。这个调度器可以根据任务的类型和优先级来动态地分配任务,并且可以实时监控任务的执行情况和进度。 其次,该框架提供了一套灵活的任务管理机制,可以方便地定义和管理任务。我们可以通过编写简单的代码来定义任务的逻辑,并且可以为任务设置各种参数,例如执行时间间隔、重试次数等。此外,该框架还支持任务的持久化存储,确保任务在节点故障或系统重启后能够正确地恢复和继续执行。 此外,Golang分布式任务框架还提供了一套高效的通信机制,用于节点之间的消息传递和数据交换。通过这种通信机制,不同节点之间可以共享任务和数据,实现更加高效和协同的任务处理。此外,该框架还支持集群的动态扩展和节点的负载均衡,以应对不同规模和负载的分布式环境。 总结来说,Golang分布式任务框架通过提供强大的任务调度、任务管理和通信机制,简化了分布式环境下的任务处理。它具有灵活、高效和可靠的特点,适用于各种规模和负载的分布式系统应用。通过使用该框架,我们可以更加轻松地开发和管理分布式任务,提高系统的性能和可扩展性。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值