【项目】分布式Json-RPC框架 - 项目介绍与前置知识准备

目录

项目介绍

技术选型

前置知识准备

JsonCpp

Json数据格式

JsonCpp库介绍

Json序列化实践

Json反序列化实践

Muduo

Muduo库介绍

Muduo库实现字典服务端

Muduo库实现字典客户端

C++11异步操作

std::async

std::packaged_task

std::promise


项目介绍

RPC是一种计算机通信协议,允许程序像调用本地服务一样调用另一台计算机(远程主机)上的服务或函数,而无需显式处理底层网络通信细节。它工作在应用层,抽象了网络通信的复杂性,使开发者能够更专注于业务逻辑。

假设我们现在要完成加法运算,此时可能会在本地定义一个加法函数Add,然后调用这个函数完成计算。当计算较为复杂时,对CPU的要求还是比较高的,并不是所有的计算机都能够胜任。此时可以不让计算在本地进行,而是搭建一个服务器,这个服务器的CPU更强,并且这个服务器中也实现了Add这个函数。客户端可以定义自己的Add,但是里面并不实现,而是通过网络将要计算的数据推送给服务器,来请求服务器上的Add算法,让服务器来进行计算,等到计算完成后,再将计算结果返回给客户端。这个过程就称为RPC远程调用。就是在用户看来,只调用了一个函数,而在底层却是一次网络通信。RPC可以使用多种网络协议进行通信,如HTTP、TCP、UDP等,并且在TCP/IP网络四层模型中跨越了传输层和应用层。简言之RPC就是像调用本地方法一样调用远程方法。

这个项目是实现了服务注册/发现的。如果只有一个服务器,所有客户端都请求这个服务器,当服务器挂掉了,此时就没办法进行请求了,这样系统健壮性就太低了。所以,此时可以实现一个注册中心,这样可以有多个服务,每一个服务启动之后,都是先到注册中心进行注册,并告诉注册中心自己能够提供什么服务。每一个客户端需要Add服务时,并不是直接去找服务器,而是先去请求注册中心,进行服务发现,就是询问注册中心,谁可以给我提供Add服务。注册中心就会将提供Add服务的服务器的信息都发给客户端,客户端拿到这些信息后,就可以选择一个服务器去请求服务了。这样即使某个服务器挂掉了,也不会有太大的问题。

这个项目是实现了负载均衡的。我们可以将服务分布到不同的服务器当中,从而得到一个更加均衡的做法。

这个项目是实现了服务的订阅和通知的。假设现在有一个新闻系统,这个新闻系统中有不同的新闻板块,假设有一个音乐新闻、花边新闻、体育新闻,一个歌手发布了音乐新闻,但是这个歌手前段时间因为某些事情上了热搜,所以,客户端不仅仅要将这条新闻交给音乐新闻,还要交给花边新闻,这样是十分麻烦的。此时可以弄一个中间服务器,在这个服务器中创建不同的新闻主题,可以弄一个音乐主题,每发布一个音乐主题的新闻,就会将新闻放到音乐新闻板块中,再弄一个花边歌手主题,每发布一个花边歌手主题的新闻,就会将新闻放到音乐新闻板块、花边新闻板块中。这样,用户只需要发布给某一个主题,然后由服务器来根据订阅决定发布给哪一个板块,音乐新闻肯定会订阅音乐主题、花边歌手主题,花边主题肯定会订阅花边歌手主题。消息通知体系中,多了一个主题的思想:有不同的主题,谁订阅了这个主题,发布消息的时候,并不是直接发布给指定的客户端,而是发布给主题,由主题订阅决定将消息发送给谁。

一个完整RPC通信框架,大概包含以下内容:序列化协议、通信协议、连接复用、服务注册、服务发现、服务订阅和通知、负载均衡、服务监控、同步调用、异步调用

这个项目使用JsonCpp库来实现序列化、反序列化,muduo库来实现网络底层通信服务。

技术选型

技术选型一:根据IDL(接口描述语言)定义公共接口,编写代码生成器根据IDL语言生成相关的C++、Java代码,然后我们的客户端和服务器程序共同向上继承公共接口即可。这种方案因为使用pb生成一部分代码,所以对理解不够友好;如果是json定义IDL语言需要自己编写代码生成器难度较大一点,暂不考虑这种方案。

技术选项二:实现一个远程调用接口call,然后通过传入函数名参数来调用RPC接口,我们采用这种实现方案。

我们选用的方案就是提供给用户一些RPC远程调用的接口,用户想进行远程调用时,直接通过这些接口调用,传入调用的函数名和参数即可。以Add为例,传入两个参数,服务端怎么知道哪一个是num1,哪一个是num2,并且知道它们的类型呢?也就是网络传输的参数和返回值怎么映射到对应的RPC接口上?

  • 使用protobuf的反射机制
  • 使用C++模板、类型萃取、函数萃取等机制
  • 使用更通用的类型,比如JSON类型,设计好参数和返回值协议即可

在我们的项目中,选择第三种方案。

网络传输要怎么做呢?

  • 原生socket-实现难度较大,暂不考虑
  • Boostasio库的异步通信-需要扩展boost库
  • muduo库

要实现一个稳定、高并发的服务器是很困难的所以,需要使用第三方库。我们选择的是muduo库。

序列化与反序列化的怎么完成?

  • Protobuf
  • JSON

因为项目需要使用JSON来定义函数参数和返回值,所以我们采用JSON进行序列与反序列化。

前置知识准备

这里我们使用JsonCpp库来完成数据的序列与反序列化,Muduo库实现网络通信功能。

JsonCpp

Json数据格式

Json是一种数据交换格式,它采用完全独立于编程语言的文本格式来存储和表示数据。Json在组织数据时,是以key: value的形式组织的。Json 的数据类型包括对象,数组,字符串,数字等。

  • 对象:使用花括号 {} 括起来的表示一个对象
  • 数组:使用中括号 [] 括起来的表示一个数组
  • 字符串:使用常规双引|号 "" 括起来的表示一个字符串
  • 数字:包括整形和浮点型,直接使用

假设我们想表示一个学生的信息,使用C++可以这样表示:

cpp 复制代码
char* name="xx";
int age=18;
float score[3] = {88.5, 99, 58};
std::unordered_map<std::string, std::string> hobbies = {
    {"书籍", "西游记"},
    {"运动", "打篮球"}
};

使用Json可以这样表示:

cpp 复制代码
{
    "姓名": "xx",
    "年龄": 18,
    "成绩": [88.5, 99, 58],
    "爱好": {
        "书籍": "西游记",
        "运动": "打篮球"
    }
}

JsonCpp库介绍

JsonCpp库主要是用于实现Json格式数据的序列化和反序列化,它实现了将多个数据对象组织成

为Json格式字符串,以及将Json格式字符串解析得到多个数据对象的功能。说得直白一点,JsonCpp库就是将结构化对象转成Json风格字符串,或者将Json风格字符串转成结构化对象的库

Json数据对象类

cpp 复制代码
class Json::Value{
    // Value重载了[]和=,因此所有的赋值和获取数据都可以通过
    Value &operator=(const Value &other);  

    // 简单的⽅式完成 val["name"] ="xx";
    Value& operator[](const std::string& key);
    Value& operator[](const char* key);

    // 移除元素
    Value removeMember(const char* key);

    // val["score"][0]
    const Value& operator[](ArrayIndex index) const; 

    // 添加数组元素val["score"].append(88);
    Value& append(const Value& value);

    // 获取数组元素个数 val["score"].size();
    ArrayIndex size() const;

    // 转string string name = val["name"].asString();
    std::string asString() const;

    // 转char* char *name = val["name"].asCString();
    const char* asCString() const;

    // 转int int age = val["age"].asInt();
    Int asInt() const;

    // 转float float weight = val["weight"].asFloat();
    float asFloat() const;

    // 转 bool bool ok = val["ok"].asBool();
    bool asBool() const;
};

Json::Value类:中间数据存储类。如果要将数据对象进行序列化,就需要先存储到Json:Value对象中,这样是为了方便组织数据与数据之间的关系,其实就是所有的key: value结构组织好。如果要将数据串进行反序列化,就是解析后,将数据对象放入到Json::Value对象中,会根据解析后的对象维护好key: value结构。Arraylndex是Json::Value类中的一个整型类型。

序列化接口

cpp 复制代码
class JSON_API StreamWriter 
{
    virtual int write(Value const& root, std::ostream* sout) = 0;
}
class JSON_API StreamWriterBuilder : public StreamWriter::Factory 
{
    virtual StreamWriter* newStreamWriter() const;
}
  • Json::StreamWriter类:用于进行数据序列化。
  • Json::StreamWriterBuilder类:Json::StreamWriter工厂类 - 用于生产Jsor:StreamWriter对象。

Json::StreamWriter类中有一个非常重要的接口write,会将Value对象中内容序列化并存储到sout中。但是我们并不能直接定义一个StreamWriter对象然后调用这个接口,这里采用了设计模式中的工厂模式,我们需要通过StreamWriterBuilder中的newStreamWrite接口来定义一个StreamWriter对象,然后再进行调用write接口。

反序列化接口

cpp 复制代码
class JSON_API CharReader 
{
    virtual bool parse(char const* beginDoc, char const* endDoc, Value* root, std::string* errs) = 0;
}
class JSON_API CharReaderBuilder : public CharReader::Factory 
{
    virtual CharReader* newCharReader() const;
}
  • Json::CharReader类:用于进行数据反序列化。
  • Json::CharReaderBuilder:Json::CharReader工厂类-用于生产Json::CharReader对象。

parse函数用于进行反序列化,传入一个字符串的起始地址和结束地址,再传入一个Value对象,就会对起始地址到结束地址进行解析,并将解析结构存放到Value对象当中,当解析失败时,就会返回一个false,并将错误信息写入errs中。

Json序列化实践

JsonCpp库的头文件在这个路径下:

cpp 复制代码
/usr/include/jsoncpp/json
cpp 复制代码
// 实现数据的序列化
void serialize()
{
    const char *name = "小明";
    int age = 18;
    const char *sex = "男";
    float score[3] = {88, 77.5, 66};

    // 将要序列化的数据存储到Json::Value对象中
    Json::Value student;
    student["姓名"] = name;
    student["年龄"] = age;
    student["性别"] = sex;
    // append用于添加数组元素
    student["成绩"].append(score[0]);
    student["成绩"].append(score[1]);
    student["成绩"].append(score[2]);

    // 将要序列化的数据存放到Json::Value对象中之后,就可以进行序列化了
    // 先实例化出一个工厂类对象
    Json::StreamWriterBuilder swb;
    // 通过工厂类对象来生产派生类对象
    Json::StreamWriter *sw = swb.newStreamWriter();
    // 这里直接将序列化后的结果打印出来
    sw->write(student, &std::cout);
    std::cout << std::endl;

    // 注意:这个sw是new出来的,所以最后需要释放
    delete sw;
}

Json::Value是可以进行嵌套的

cpp 复制代码
// 实现数据的序列化
void serialize()
{
    const char *name = "小明";
    int age = 18;
    const char *sex = "男";
    float score[3] = {88, 77.5, 66};

    // 将要序列化的数据存储到Json::Value对象中
    Json::Value student;
    student["姓名"] = name;
    student["年龄"] = age;
    student["性别"] = sex;
    // append用于添加数组元素
    student["成绩"].append(score[0]);
    student["成绩"].append(score[1]);
    student["成绩"].append(score[2]);

    Json::Value fav;
    fav["书籍"] = "西游记";
    fav["运动"] = "打篮球";
    student["爱好"] = fav;

    // 将要序列化的数据存放到Json::Value对象中之后,就可以进行序列化了
    // 先实例化出一个工厂类对象
    Json::StreamWriterBuilder swb;
    // 通过工厂类对象来生产派生类对象
    Json::StreamWriter *sw = swb.newStreamWriter();
    // 这里直接将序列化后的结果打印出来
    sw->write(student, &std::cout);
    std::cout << std::endl;

    // 注意:这个sw是new出来的,所以最后需要释放
    delete sw;
}

像上面这种,Json风格的字符串中有许多的缩进和换行,我们可以将其修改为紧凑格式,也就是没有缩进和换行的格式。

cpp 复制代码
// 实现数据的序列化
void serialize()
{
    const char *name = "小明";
    int age = 18;
    const char *sex = "男";
    float score[3] = {88, 77.5, 66};

    // 将要序列化的数据存储到Json::Value对象中
    Json::Value student;
    student["姓名"] = name;
    student["年龄"] = age;
    student["性别"] = sex;
    // append用于添加数组元素
    student["成绩"].append(score[0]);
    student["成绩"].append(score[1]);
    student["成绩"].append(score[2]);

    Json::Value fav;
    fav["书籍"] = "西游记";
    fav["运动"] = "打篮球";
    student["爱好"] = fav;

    // 将要序列化的数据存放到Json::Value对象中之后,就可以进行序列化了
    // 先实例化出一个工厂类对象
    Json::StreamWriterBuilder swb;
    // 取消缩进和换行
    swb.settings_["indentation"] = "";  
    // 通过工厂类对象来生产派生类对象
    Json::StreamWriter *sw = swb.newStreamWriter();
    // 这里直接将序列化后的结果打印出来
    sw->write(student, &std::cout);
    std::cout << std::endl;

    // 注意:这个sw是new出来的,所以最后需要释放
    delete sw;
}

我们对上面的函数进行封装。

cpp 复制代码
// 实现数据的序列化
bool serialize(const Json::Value &val, std::string &body)
{
    std::stringstream ss;
    // 先实例化出一个工厂类对象
    Json::StreamWriterBuilder swb; 
    // 通过工厂类对象来生产派生类对象
    std::unique_ptr<Json::StreamWriter> sw(swb.newStreamWriter());
    // 这里直接将序列化后的结果打印出来
    int ret = sw->write(val, &ss);
    if(ret != 0)
    {
        std::cout << "json serualize failed\n";
        return false;
    }
    body = ss.str();
    return true;
}

int main()
{
    const char *name = "小明";
    int age = 18;
    const char *sex = "男";
    float score[3] = {88, 77.5, 66};

    // 将要序列化的数据存储到Json::Value对象中
    Json::Value student;
    student["姓名"] = name;
    student["年龄"] = age;
    student["性别"] = sex;
    // append用于添加数组元素
    student["成绩"].append(score[0]);
    student["成绩"].append(score[1]);
    student["成绩"].append(score[2]);

    Json::Value fav;
    fav["书籍"] = "西游记";
    fav["运动"] = "打篮球";
    student["爱好"] = fav;
    // 序列化后的结果保存在body中
    std::string body;
    serialize(student, body);
    std::cout << body << std::endl;
    return 0;
}

Json反序列化实践

cpp 复制代码
// 实现数据的反序列化
bool unserialize(const std::string &body, Json::Value &val)
{
    // 实例化工厂对象
    Json::CharReaderBuilder crb;
    // 通过工厂类对象来生产派生类对象
    std::string errs;
    std::unique_ptr<Json::CharReader> cr(crb.newCharReader());
    bool ret = cr->parse(body.c_str(), body.c_str() + body.size(), &val, &errs);
    if(ret == false)
    {
        std::cout << "json unserialize failed: " << errs << std::endl;
        return false;
    }
    return true;
}

int main()
{
    const char *name = "小明";
    int age = 18;
    const char *sex = "男";
    float score[3] = {88, 77.5, 66};

    // 将要序列化的数据存储到Json::Value对象中
    Json::Value student;
    student["姓名"] = name;
    student["年龄"] = age;
    student["性别"] = sex;
    // append用于添加数组元素
    student["成绩"].append(score[0]);
    student["成绩"].append(score[1]);
    student["成绩"].append(score[2]);

    // 序列化后的结果保存在body中
    std::string body;
    serialize(student, body);
    std::cout << body << std::endl;

    Json::Value stu;
    // 将body的内容反序列化到stu中
    bool ret = unserialize(body, stu);
    if(ret == false)
        return -1;
    std::cout << "姓名: " << stu["姓名"].asString() << std::endl; 
    std::cout << "年龄: " << stu["年龄"].asString() << std::endl;
    std::cout << "性别: " << stu["性别"].asString() << std::endl; 
    int sz = stu["成绩"].size();
    for(int i = 0;i < sz;i ++)
    {
        std::cout << "成绩: " << stu["成绩"][i].asFloat() << std::endl;
    }
    return 0;
}

Muduo

Muduo库介绍

Muduo库是一个基于非阻塞IO和事件驱动的C++高并发TCP网络编程库。它是一款基于主从Reactor模型的网络库,其使用的线程模型是oneloop per thread,所谓oneloop per thread是指:

  • 一个线程只能有一个事件循环(EventLoop),用于响应计时器和IO事件
  • 一个文件描述符只能由一个线程进行读写,换句话说就是一个TCP连接必须归属于某个EventLoop管理

我们来看看Reactor模型是什么。Reactor也叫做事件器,是一个基于事件触发的高并发网络通信模型。我们之前在使用TCP进行通信时,会在服务端创建一个监听套接字,每当客户端发送过来新连接时,就创建一个线程来处理请求。此时是会有效率问题的,每当有新连接到来时,都需要创建一个线程来处理请求,可能有些客户端仅仅只是与服务器建立了连接,但是迟迟不发送数据,此时这个线程会一直阻塞着,所以这是一个非常消耗资源的做法。后来我们介绍了多路转接模型,在服务端的主线程中除了有一个监听套接字进行网络监听之外,还有一个epoll/select/poll,来进行套接字的事件监控,将监听套接字加入到多路转接模型的事件监控当中,所谓加入到事件监控当中其实就是谁触发了事件,就对谁进行处理。当有一个客户端的连接请求到来时,就创建一个套接字,并将这个套接字加入到事件监控当中。一个执行流中,有一个多路转接模型进行socket事件监控,触发IO事件后进行IO处理的这种通信处理模型就叫做Reactor模型。但是这种Reactor模型是有性能问题的,假设有非常多的客户端连接了服务器,就会创建出非常多的套接字,并且这些套接字都位于一个epoll/select/pol中进行事件监控,等到事件触发,然后进行IO处理。但是IO处理是比较慢的,并且IO读取完数据后,还需要进行业务处理,两者都是需要时间的,在这段时间之内,触发了时间的连接就得不到处理,需要等这个处理完成之后才能进行处理,这显然是不好的。多线程多Reactor模型。在主线程中,只对监听套接字进行事件监控,所以,即使IO再多,也不影响新连接的建立。当有新连接时,就会创建出新的Reactor,并将新连接的套接字交给新的Reactor,由新的Reactor来进行lO和业务处理。

如何使用Muduo库搭建TCP服务器和TCP客户端呢?此时就需要了解几个类。

TcpServer类介绍

cpp 复制代码
typedef std::shared_ptr<TcpConnection> TcpConnectionPtr;
typedef std::function<void (const TcpConnectionPtr&)> ConnectionCallback;
typedef std::function<void (const TcpConnectionPtr&, Buffer*, Timestamp)> MessageCallback;
class InetAddress : public muduo::copyable
{
public:
    InetAddress(StringArg ip, uint16_t port, bool ipv6 = false);
};
class TcpServer : noncopyable
{
public:
    enum Option
    {
        kNoReusePort,
        kReusePort,
    };
    TcpServer(EventLoop* loop,                // 主事件循环(必须由调用线程运行)
              const InetAddress& listenAddr,  // 服务器监听的地址和端口
              const string& nameArg,          // 服务器名称(用于标识)    
              Option option = kNoReusePort    // 端口复用选项(可选)
    );
    void setThreadNum(int numThreads);
    void start();
    // 当⼀个新连接建⽴成功的时候被调⽤
    void setConnectionCallback(const ConnectionCallback& cb)
    { connectionCallback_ = cb; }
    // 消息的业务处理回调函数---这是收到新连接消息的时候被调⽤的函数
    void setMessageCallback(const MessageCallback& cb)
    { messageCallback_ = cb; }
}

在TcpServer类中,有一个非常重要的成员函数setThreadNum,是用来设置线程数量的,其实就是设置Readtor的数量,因为有几个线程就有几个Reactor。 start是开始进行事件监控、获取新连接,是启动服务器的函数。 setConnectionCallack是设置连接建立/关闭时的回调函数,如在一个聊天程序中,谁上线/下线了就通知一下所有人。 setMessageCallback,设置消息处理回调函数,就是设置连接的业务处理函数。在Muduo库中,事件监控与套接字是分开的,事件监控有一个单独的类EventLoop。

EventLoop类介绍

cpp 复制代码
class EventLoop : noncopyable
{
public:
    void loop();
    void quit();
    TimerId runAt(Timestamp time, TimerCallback cb);
    TimerId runAfter(double delay, TimerCallback cb);
    TimerId runEvery(double interval, TimerCallback cb);
    void cancel(TimerId timerId);
private:
    std::atomic<bool> quit_;
    std::unique_ptr<Poller> poller_;
    mutable MutexLock mutex_;
    std::vector<Functor> pendingFunctors_ GUARDED_BY(mutex_);
}

EventLoop,这个类才是真正使用epoll来进行事件监控的类。loop是开始时间监控循环。quit是停止循环。并且EventLoop类中还有定时任务的处理,runAt是在指定的时间运行一个任务,runAfter是多少秒后运行一个任务,runEvery是每隔几秒就运行一次任务。cancel是取消定时任务的。构建TcpServer对象时,是需要传入一个EventLoop对象的指针的。

刚刚说了TcpServer类有两个回调函数,我们来看看回调函数的格式:

cpp 复制代码
ConnectionCallback:
    connectionCb(TcpConnectionPtr &conn);

MessageCallback:
    onMessageCb(TcpConnectionPtr &conn, Buffer *buf)
    {
        // 针对conn连接所接收放入到buf中的数据进行处理
    }

所以,通过TcpServer和EventLoop确实可以完成一个服务器的搭建,但是没办法对数据进行处理要对数据进行处理,还需要学习两个类。

TcpConnection类介绍

cpp 复制代码
class TcpConnection : noncopyable,
public std::enable_shared_from_this<TcpConnection>
{
public:
    TcpConnection(EventLoop* loop,              // 所属的事件循环
                  const string& name,           // 连接的唯一标识名称
                  int sockfd,                   // 已建立的 TCP socket 文件描述符
                  const InetAddress& localAddr, // 本地地址(IP + 端口)
                  const InetAddress& peerAddr   // 对端地址(IP + 端口)
    );
    bool connected() const { return state_ == kConnected; }
    bool disconnected() const { return state_ == kDisconnected; }
    void send(string&& message); // C++11
    void send(const void* message, int len);
    void send(const StringPiece& message);
    // void send(Buffer&& message); // C++11
    void send(Buffer* message); // this one will swap data
    void shutdown(); // NOT thread safe, no simultaneous calling
    void setContext(const boost::any& context)
    { context_ = context; }
    const boost::any& getContext() const
    { return context_; }
    boost::any* getMutableContext()
    { return &context_; }
    void setConnectionCallback(const ConnectionCallback& cb)
    { connectionCallback_ = cb; }
    void setMessageCallback(const MessageCallback& cb)
    { messageCallback_ = cb; }
private:
    enum StateE { kDisconnected, kConnecting, kConnected, kDisconnecting };
    EventLoop* loop_;
    ConnectionCallback connectionCallback_;
    MessageCallback messageCallback_;
    WriteCompleteCallback writeCompleteCallback_;
    boost::any context_;
};

TcpConnection类是针对连接进行封装管理的一个类。send用于发送数据。connect用于判断当前连接是否正常。shutdown用于关闭连接。

Buffer类介绍

cpp 复制代码
class Buffer : public muduo::copyable
{
public:
    static const size_t kCheapPrepend = 8;
    static const size_t kInitialSize = 1024;
    explicit Buffer(size_t initialSize = kInitialSize)
        : buffer_(kCheapPrepend + initialSize),
          readerIndex_(kCheapPrepend),
          writerIndex_(kCheapPrepend);
    void swap(Buffer& rhs)
    size_t readableBytes() const
    size_t writableBytes() const
    const char* peek() const
    const char* findEOL() const
    const char* findEOL(const char* start) const
    void retrieve(size_t len)
    void retrieveInt64()
    void retrieveInt32()
    void retrieveInt16()
    void retrieveInt8()
    string retrieveAllAsString()
    string retrieveAsString(size_t len)
    void append(const StringPiece& str)
    void append(const char* /*restrict*/ data, size_t len)
    void append(const void* /*restrict*/ data, size_t len)
    char* beginWrite()
    const char* beginWrite() const
    void hasWritten(size_t len)
    void appendInt64(int64_t x)
    void appendInt32(int32_t x)
    void appendInt16(int16_t x)
    void appendInt8(int8_t x)
    int64_t readInt64()
    int32_t readInt32()
    int16_t readInt16()
    int8_t readInt8()
    int64_t peekInt64() const
    int32_t peekInt32() const
    int16_t peekInt16() const
    int8_t peekInt8() const
    void prependInt64(int64_t x)
    void prependInt32(int32_t x)
    void prependInt16(int16_t x)
    void prependInt8(int8_t x)
    void prepend(const void* /*restrict*/ data, size_t len)
private:
    std::vector<char> buffer_;
    size_t readerIndex_;
    size_t writerIndex_;
    static const char kCRLF[];
}

Buffer类是一个缓冲区类,这里的缓冲区实际上就是一个vector<char>。readableBytes用于获取缓冲区可读数据大小。peek用于获取缓冲区中数据的起始地址。peeklnt32是尝试从缓冲区获取4字节数据,并将数据从网络字节序转换为一个整数,但是数据并不从缓冲区删除。retrievelnt32数据读取位置向后偏移4字节,向后偏移就相对于将数据从缓冲区中删除。readlnt32是上面两个函数的合并。retrieveAlIAsString表示从缓冲区取出所有数据,当作string返回,并删除缓冲区中的数据。retrieveAsString(size_tlen),从缓冲区取出len长度的数据,当作string返回,并删除缓冲区中的数据。到此,就可以构建出服务端了。

如果要构建服务端的话,还需要再了解一个类。

TcpClient类介绍

cpp 复制代码
class TcpClient : noncopyable
{
public:
    TcpClient(EventLoop* loop,               // 事件循环
              const InetAddress& serverAddr, // 服务端地址
              const string& nameArg);        // 客户端名称        
    ~TcpClient(); 
    void connect();//连接服务器
    void disconnect();//关闭连接
    void stop();
    //获取客⼾端对应的通信连接Connection对象的接⼝,发起connect后,有可能还没有连接建⽴成功
    TcpConnectionPtr connection() const
    {
        MutexLockGuard lock(mutex_);
        return connection_;
    }
    // 连接服务器成功时的回调函数
    void setConnectionCallback(ConnectionCallback cb)
    { connectionCallback_ = std::move(cb); }
    // 收到服务器发送的消息时的回调函数
    void setMessageCallback(MessageCallback cb)
    { messageCallback_ = std::move(cb); }
private:
    EventLoop* loop_;
    ConnectionCallback connectionCallback_;
    MessageCallback messageCallback_;
    WriteCompleteCallback writeCompleteCallback_;
    TcpConnectionPtr connection_ GUARDED_BY(mutex_);
};
/*
需要注意的是,因为muduo库不管是服务端还是客⼾端都是异步操作,
对于客⼾端来说如果我们在连接还没有完全建⽴成功的时候发送数据,这是不被允许的。
因此我们可以使⽤内置的CountDownLatch类进⾏同步控制
*/
class CountDownLatch : noncopyable
{
public:
    explicit CountDownLatch(int count);
    void wait(){
        MutexLockGuard lock(mutex_);
        while (count_ > 0)
        {
            condition_.wait();
        }
    }
    void countDown(){
        MutexLockGuard lock(mutex_);
        --count_;
        if (count_ == 0)
        {
            condition_.notifyAll();
        }
    }
    int getCount() const;
private:
    mutable MutexLock mutex_;
    Condition condition_ GUARDED_BY(mutex_);
    int count_ GUARDED_BY(mutex_);
}

TcpClient类。需要传入客户端的地址信息。connect用于连接服务器,disconnect用于关闭连接,stop是停止通信。connection用于获取客户端对应的TcpConnection连接,作为客户端应该提供send接口,用于向客户端发送请求,但是TcpClient中并没有send接口,send接口在TcpConnection中,所以先获取连接,然后通过连接调用send去发送数据。客户端也是通过EventLoop进行lO事件监控处理的,所以创建时需要传入loop,所以也有setConnectionCallback和setMessageCallback。所以,Muduo库中并没有提供recv接口,不能发送一个数据之后调用recv来接收应答,业务它的I0都是通过Eventloop进行事件监控的,一旦监控触发了,就会将数据读取到连接的一个缓冲区中,然后调用回调函数来进行处理。所以,就可以通过TcpClient和EventLoop来搭建一个客户端。

CountDownLatch类是一个做计数同步的类。因为TcpClient的connect是一个非阻塞接口,所以可能出现还没有建立完连接就调用coction接口获取新连接,send发送数据。CountDownLatch中的wait是计数大于0则阻塞。CountDownLatch中的countDown是计数--,为o时唤醒wait。所以,可以在发起连接时通过wait进行等待,直到setConnectionCallback的连接建立成功的回调函数中调用countDown。这样就能够保证是在连接建立成功之后才进行lO操作。

我们使用Muduo库实现一个翻译服务器,客户端发送过来一个英语单词,服务端返回汉语翻译。

Muduo库实现字典服务端

cpp 复制代码
class DictServer
{
public:
    DictServer(int port): _server(&_baseloop,
        muduo::net::InetAddress("0.0.0.0", port),
        "DictServer", muduo::net::TcpServer::kReusePort)
    {
        // 0.0.0.0表示绑定本机的任意一个IP地址
        // 构造完成_server后,就需要设置回调函数了
        // 此时是不能直接设置的,因为下面的两个函数都是类的成员函数,会有一个隐藏的this指针
        // 我们使用bind来进行参数的适配
        _server.setConnectionCallback(std::bind(&DictServer::onConnection, this, std::placeholders::_1));
        _server.setMessageCallback(std::bind(&DictServer::onMessage, this, 
            std::placeholders::_1, std::placeholders::_2, std::placeholders::_3));
    }
    void start()
    {
        // 这两者的顺序不能变化
        _server.start();  // 先开始监听
        _baseloop.loop(); // 开始死循环进行事件监控
    }
private:
    // 链接建立或关闭时的回调函数
    void onConnection(const muduo::net::TcpConnectionPtr &conn)
    {
        if(conn->connected())
        {
            std::cout << "连接建立!\n";
        }
        else
        {
            std::cout << "连接断开!\n";
        }
    }
    // 业务处理函数
    // 第三个参数是记录消息到达的时间的,我们直接不管
    void onMessage(const muduo::net::TcpConnectionPtr &conn, muduo::net::Buffer *buf, muduo::Timestamp)
    {
        static std::unordered_map<std::string, std::string> dict_map = {
            {"hello", "你好"},
            {"world", "世界"}
        };
        // 从缓冲区中拿出数据,进行翻译,然后将结果发送给客户端
        // 这里就直接获取缓冲区中全部的数据当成一个英语单词了,因为这里只是测试
        std::string msg = buf->retrieveAllAsString();
        std::string res;
        auto it = dict_map.find(msg);
        if(it != dict_map.end())
        {
            res = it->second;
        }
        else
        {
            res = "未知单词";
        }
        conn->send(res);
    }
private:
    // 这个服务器是基于Muduo库搭建的,所以成员变量中一定要有TcpServer类型的变量
    // TcpServer类型的变量在构造函数中需要传入EventLoop类型的对象
    // 所以成员变量还需要有一个EventLoop类型的对象,且要定义在TcpServer类型对象之前
    // 因为要使用EcentLoop类型的对象去构造TcpServer类型的对象
    muduo::net::EventLoop _baseloop;
    muduo::net::TcpServer _server;
};

int main()
{
    DictServer server(8080);
    server.start();
    return 0;
}

实际上很简单,就是初始化完成TcpServer类的对象后,设置好回调函数,然后先开始事件监听,然后进行事件监控即可。当有新连接到来或链接接收到数据后,就会调用设置好的回调函数进行处理了。

Muduo库实现字典客户端

cpp 复制代码
class DictClient
{
public:
    DictClient(const std::string &sip, int sport):
        _client(&_baseloop, muduo::net::InetAddress(sip, sport), "DictClient")
    {
        // 设置回调函数
        _client.setConnectionCallback(std::bind(&DictClient::onConnection, this, std::placeholders::_1));
        _client.setMessageCallback(std::bind(&DictClient::onMessage, this, 
            std::placeholders::_1, std::placeholders::_2, std::placeholders::_3));

        // 连接服务器
        _client.connect();
    }
private:
    void onConnection(const muduo::net::TcpConnectionPtr &conn)
    {
        if(conn->connected())
        {
            std::cout << "连接建立!\n";
        }
        else
        {
            std::cout << "连接断开!\n";
        }
    }
    void onMessage(const muduo::net::TcpConnectionPtr &conn, muduo::net::Buffer *buf, muduo::Timestamp)
    {
        // 接收到应答之后,直接将结果打印一下
        std::string res = buf->retrieveAllAsString();
        std::cout << res << std::endl;
    }
private:
    muduo::net::EventLoop _baseloop;
    muduo::net::TcpClient _client;
};

这样写是有问题的。因为connect是一个非阻塞的接口,所以connect调用完成后,链接不一定建立完成了。此时直接发送数据就会出现问题。所以需要进行同步,让链接建立完成之后,再向下走。

cpp 复制代码
class DictClient
{
public:
    DictClient(const std::string &sip, int sport):
        _downlatch(1), // 计数器初始化为1
        _client(&_baseloop, muduo::net::InetAddress(sip, sport), "DictClient")
    {
        // 设置回调函数
        _client.setConnectionCallback(std::bind(&DictClient::onConnection, this, std::placeholders::_1));
        _client.setMessageCallback(std::bind(&DictClient::onMessage, this, 
            std::placeholders::_1, std::placeholders::_2, std::placeholders::_3));

        // 连接服务器
        _client.connect();
        // 等待计数器被唤醒再向下运行
        _downlatch.wait();
    }
private:
    void onConnection(const muduo::net::TcpConnectionPtr &conn)
    {
        if(conn->connected())
        {
            std::cout << "连接建立!\n";
            // 链接建立成功,让计数器--,从而唤醒计数器
            _downlatch.countDown();
        }
        else
        {
            std::cout << "连接断开!\n";
        }
    }
    void onMessage(const muduo::net::TcpConnectionPtr &conn, muduo::net::Buffer *buf, muduo::Timestamp)
    {
        // 接收到应答之后,直接将结果打印一下
        std::string res = buf->retrieveAllAsString();
        std::cout << res << std::endl;
    }
private:
    muduo::CountDownLatch _downlatch; // 计数器
    muduo::net::EventLoop _baseloop;
    muduo::net::TcpClient _client;
};

现在客户端已经能够连接服务器了,接下来完成向服务器发送数据的接口。这里要注意:发送数据时,不能使用_client发送,需要使用链接对象发送。

cpp 复制代码
class DictClient
{
public:
    DictClient(const std::string &sip, int sport):
        _downlatch(1), // 计数器初始化为1
        _client(&_baseloop, muduo::net::InetAddress(sip, sport), "DictClient")
    {
        // 设置回调函数
        _client.setConnectionCallback(std::bind(&DictClient::onConnection, this, std::placeholders::_1));
        _client.setMessageCallback(std::bind(&DictClient::onMessage, this, 
            std::placeholders::_1, std::placeholders::_2, std::placeholders::_3));

        // 连接服务器
        _client.connect();
        // 等待计数器被唤醒再向下运行
        _downlatch.wait();
    }
    bool send(const std::string &msg)
    {
        if (!_conn || !_conn->connected())  // 检查 _conn 是否有效
        {
            std::cout << "连接已经断开,发送数据失败!\n";
            return false;
        }
        _conn->send(msg);
        return true;
    }
private:
    void onConnection(const muduo::net::TcpConnectionPtr &conn)
    {
        if(conn->connected())
        {
            std::cout << "连接建立!\n";
            // 链接建立成功,让计数器--,从而唤醒计数器
            _downlatch.countDown();
            _conn = conn;
        }
        else
        {
            std::cout << "连接断开!\n";
            _conn.reset();
        }
    }
    void onMessage(const muduo::net::TcpConnectionPtr &conn, muduo::net::Buffer *buf, muduo::Timestamp)
    {
        // 接收到应答之后,直接将结果打印一下
        std::string res = buf->retrieveAllAsString();
        std::cout << res << std::endl;
    }
private:
    muduo::net::TcpConnectionPtr _conn;
    muduo::CountDownLatch _downlatch; // 计数器
    muduo::net::EventLoop _baseloop;
    muduo::net::TcpClient _client;
};

客户端也是需要事件循环监控的,所以我们要让其进行事件循环监控

cpp 复制代码
class DictClient
{
public:
    DictClient(const std::string &sip, int sport):
        _downlatch(1), // 计数器初始化为1
        _client(&_baseloop, muduo::net::InetAddress(sip, sport), "DictClient")
    {
        // 设置回调函数
        _client.setConnectionCallback(std::bind(&DictClient::onConnection, this, std::placeholders::_1));
        _client.setMessageCallback(std::bind(&DictClient::onMessage, this, 
            std::placeholders::_1, std::placeholders::_2, std::placeholders::_3));

        // 连接服务器
        _client.connect();
        // 等待计数器被唤醒再向下运行
        _downlatch.wait();

        // 进行事件循环监控
        _baseloop.loop();
    }
    bool send(const std::string &msg)
    {
        if (!_conn || !_conn->connected())  // 检查 _conn 是否有效
        {
            std::cout << "连接已经断开,发送数据失败!\n";
            return false;
        }
        _conn->send(msg);
        return true;
    }
private:
    void onConnection(const muduo::net::TcpConnectionPtr &conn)
    {
        if(conn->connected())
        {
            std::cout << "连接建立!\n";
            // 链接建立成功,让计数器--,从而唤醒计数器
            _downlatch.countDown();
            _conn = conn;
        }
        else
        {
            std::cout << "连接断开!\n";
            _conn.reset();
        }
    }
    void onMessage(const muduo::net::TcpConnectionPtr &conn, muduo::net::Buffer *buf, muduo::Timestamp)
    {
        // 接收到应答之后,直接将结果打印一下
        std::string res = buf->retrieveAllAsString();
        std::cout << res << std::endl;
    }
private:
    muduo::net::TcpConnectionPtr _conn;
    muduo::CountDownLatch _downlatch; // 计数器
    muduo::net::EventLoop _baseloop;
    muduo::net::TcpClient _client;
};

但是这样进行事件循环监控会有一个问题。loop是一个死循环,开始事件监控后就会一直死循环,这样怎么调用send去发送数据呢?此时可以创建一个线程来进行事件监控,这里的线程在EventLoopThread类中有提供。

cpp 复制代码
class DictClient
{
public:
    DictClient(const std::string &sip, int sport):
        _baseloop(_loopthread.startLoop()),
        _downlatch(1), // 计数器初始化为1
        _client(_baseloop, muduo::net::InetAddress(sip, sport), "DictClient")
    {
        // 设置回调函数
        _client.setConnectionCallback(std::bind(&DictClient::onConnection, this, std::placeholders::_1));
        _client.setMessageCallback(std::bind(&DictClient::onMessage, this, 
            std::placeholders::_1, std::placeholders::_2, std::placeholders::_3));

        // 连接服务器
        _client.connect();
        // 等待计数器被唤醒再向下运行
        _downlatch.wait();
    }
    bool send(const std::string &msg)
    {
        if (!_conn || !_conn->connected())  // 检查 _conn 是否有效
        {
            std::cout << "连接已经断开,发送数据失败!\n";
            return false;
        }
        _conn->send(msg);
        return true;
    }
private:
    void onConnection(const muduo::net::TcpConnectionPtr &conn)
    {
        if(conn->connected())
        {
            std::cout << "连接建立!\n";
            // 链接建立成功,让计数器--,从而唤醒计数器
            _downlatch.countDown();
            _conn = conn;
        }
        else
        {
            std::cout << "连接断开!\n";
            _conn.reset();
        }
    }
    void onMessage(const muduo::net::TcpConnectionPtr &conn, muduo::net::Buffer *buf, muduo::Timestamp)
    {
        // 接收到应答之后,直接将结果打印一下
        std::string res = buf->retrieveAllAsString();
        std::cout << res << std::endl;
    }
private:
    muduo::net::TcpConnectionPtr _conn;
    muduo::CountDownLatch _downlatch; // 计数器
    muduo::net::EventLoopThread _loopthread;
    muduo::net::EventLoop *_baseloop;
    muduo::net::TcpClient _client;
};

通过EventLoopThread对象的startLoop函数可以获取到内部EventLoop类型的指针对象,所以将_baseloop修改为指针。并且此时不需要显示的事件循环监控了,因为只要初始化好了_loopthread,就会自动进行事件循环监控。

现在来测试一下能否与服务端进行通信。

cpp 复制代码
int main()
{
    DictClient client("127.0.0.1", 8080);
    while(1)
    {
        std::string msg;
        std::cin >> msg;
        client.send(msg);
    }
    return 0;
}


此时就成功地进行了通信。

C++11异步操作

异步和同步的区别是一个任务是否是当前进程自身完成的,若是自身完成的,是同步操作,不是自身完成的,是异步操作。 C++11中提供了一个辅助完成异步操作的类,即std::future。 std::future是C++11标准库中的一个模板类,它表示一个异步操作的结果。"表示一个异步操作的结果"要怎么理解呢?我们要执行一个任务,在C/C++中,执行一个任务其实就是执行一个函数,异步执行任务就是让其他的执行流帮助我们执行函数。可是这是其他执行流帮助我们执行的函数,我们要怎么获取函数的执行结果呢?这就是std::future的功能了。也就是说,std::future可以帮助我们获取到这个函数的执行结果,其实就是进行了线程间通信。

在我们创建线程去执行任务之前,可以先创建一个std::future对象,std:future<int>res,未来可以通过res.get()获取执行结果,当调用res.get()时,若还没有执行完成,则会阻塞。所以,std::future是辅助我们获取异步任务结果的模板类。std::future不是单独使用的,因为它只是获取异步任务结果的,还需要搭配一些能够执行异步任务的模板类或模板函数使用。与std::future搭配使用:

  • std::async模板函数:异步执行一个函数,返回一个future对象用于获取函数结果
  • std::package_task类模板:为一个函数生成一个异步任务对象(可调用对象),用于在其他线程中执行
  • std::promise类模板:实例化的对象可以返回一个future,在其他线程中向promise对象设置数据,其他线程的关联future就可以获取数据

std::async

我们看下面那个。第一个参数是异步任务的执行策略,第二个参数是一个函数名,后面的参数就是这个函数需要的参数。返回值是一个future,结果就在这个future中。

  • launch::async是异步的,会创建一个新线程去执行函数,并且是立即执行
  • launch::deferred是同步的,不会创建一个新线程去执行函数,并且不是立即执行,等到主线程调用了get或wait才进行执行
  • launch::async | launch::deferred是系统默认的执行策略,具体执行方法由系统决定
cpp 复制代码
int Add(int num1, int num2)
{
    return num1 + num2;
}

int main()
{
    // 异步执行Add函数
    // 进行一个异步非阻塞调用
    std::future<int> res = std::async(std::launch::async, Add, 33, 44);
    // future的模板参数类型是int是因为Add的返回值是int,表示里面可以保存一个int的数据
    std::cout << res.get() << std::endl;
    return 0;
}

std::packaged_task

std::async是一个模板函数,内部会创建线程执行异步任务。std::packaged_task是一个模板类,是一个任务包,是对一个函数进行二次封装,封装成为一个可调用对象作为任务放到其他线程执行的。任务包封装好了以后,可以在任意位置进行调用,通过关联的future来获取执行结果。通过get_future成员函数即可获取任务包关联的future对象。

模板类的参数就是函数的说明。

使用packaged_task执行任务样例的步骤为:

  • 封装任务
  • 获取任务包关联的future对象
  • 执行任务
  • 通过future获取任务执行结果
cpp 复制代码
int Add(int num1, int num2)
{
    return num1 + num2;
}

int main()
{
    // 1. 封装任务
    std::packaged_task<int(int, int)> task(Add);

    // 2. 获取任务包关联的future对象
    std::future<int> res = task.get_future();

    // 3. 执行任务
    task(33, 44);

    // 4. 获取结果
    std::cout << res.get() << std::endl;

    return 0;
}

上面这样,执行任务是在主线程执行的,这样显然是不好的,如果要这样就不需要使用packaged_task了,直接调用即可。所以,我们可以手动创建一个线程,让这个线程去执行任务。

cpp 复制代码
int Add(int num1, int num2)
{
    return num1 + num2;
}

int main()
{
    // 1. 封装任务
    std::packaged_task<int(int, int)> task(Add);

    // 2. 获取任务包关联的future对象
    std::future<int> res = task.get_future();

    // 3. 执行任务
    std::thread th(std::move(task), 33, 44);

    // 4. 获取结果
    std::cout << res.get() << std::endl;
    th.join();
    return 0;
}

这样仍然是不够好的,因为每执行一个任务就需要创建一个线程,会造成线程频繁地创建和销毁,所以最好的做法是使用一个线程池,将参数进行绑定,并封装成一个任务后,将任务交给线程池中的一个线程来处理,但是这里只是测试,就不弄线程池了。另外,packaged_task是不允许拷贝的,引用也不行,因为存在作用域的问题,所以我们在封装任务时,最好是将其封装成一个指针。在往后的使用当中,因为类packaged_task不能赋值、拷贝,所以最好使用指针。

cpp 复制代码
int Add(int num1, int num2)
{
    return num1 + num2;
}

int main()
{
    // 1. 封装任务
    auto task = std::make_shared<std::packaged_task<int(int, int)>>(Add);

    // 2. 获取任务包关联的future对象
    std::future<int> res = task->get_future();

    // 3. 执行任务
    std::thread th([task](){
        (*task)(33, 44);
    });

    // 4. 获取结果
    std::cout << res.get() << std::endl;
    th.join();
    return 0;
}

std::promise

std::promise是一个模板类,是对于结果的封装。

  • 在使用的时候,需要先实例化一个指定结果的promise对象
  • 通过promise对象,获取关联的future对象
  • 在任意位置给promise设置数据,就可以通过关联的future获取到这个设置的数据了
cpp 复制代码
int Add(int num1, int num2)
{
    return num1 + num2;
}

int main()
{
    // 1. 实例化一个指定结果的promise对象
    std::promise<int> pro;

    // 2. 通过promise对象,获取关联的future对象
    std::future<int> res = pro.get_future();

    // 3. 在任意未知给promise对象设置数据,就可以通过关联的future获取到设置进去的数据了
    std::thread th([&pro](){
        int sum = Add(33, 44);
        pro.set_value(sum);
    });

    std::cout << res.get() << std::endl;
    th.join();
    return 0;
}

这里也是会涉及到作用域的问题的,所以promise类型的对象也可以定义成指针。当pro被释放了,res也没有任何作用了。在项目中,我们使用的是promise。

相关推荐
知白守黑26715 分钟前
Linux磁盘阵列
linux·运维·服务器
三年呀1 小时前
标题:移动端安全加固:发散创新,筑牢安全防线引言:随着移动互联网
网络·python·安全
维尔切1 小时前
Linux中基于Centos7使用lamp架构搭建个人论坛(wordpress)
linux·运维·架构
x.Jessica2 小时前
网络的构成元素
网络·学习·计算机网络
BYSJMG3 小时前
计算机大数据毕业设计推荐:基于Hadoop+Spark的食物口味差异分析可视化系统【源码+文档+调试】
大数据·hadoop·分布式·python·spark·django·课程设计
正在努力的小河5 小时前
Linux设备树简介
linux·运维·服务器
荣光波比5 小时前
Linux(十一)——LVM磁盘配额整理
linux·运维·云计算
前端世界5 小时前
在鸿蒙里优雅地处理网络错误:从 Demo 到实战案例
网络·华为·harmonyos
Viking_bird5 小时前
Apache Spark 3.2.0 开发测试环境部署指南
大数据·分布式·ajax·spark·apache