【项目】C++从零实现JsonRpc框架——项目引入

文章目录

  • 一、项目介绍
  • 二、技术选择
  • 三、第三方库使用介绍
    • [1. JsonCpp库](#1. JsonCpp库)
      • [1.1 Json是什么](#1.1 Json是什么)
      • [1.2 JsonCpp库的三个核心类](#1.2 JsonCpp库的三个核心类)
      • [1.3 使用示例](#1.3 使用示例)
    • [2. Muduo库](#2. Muduo库)
      • [2.1 Muduo库介绍](#2.1 Muduo库介绍)
      • [2.2 Muduo库核心类接口](#2.2 Muduo库核心类接口)
      • [2.3 简单使用示例](#2.3 简单使用示例)
    • [3. C++11的异步操作:std::future](#3. C++11的异步操作:std::future)
      • [3.1 使用std::async关联异步任务​](#3.1 使用std::async关联异步任务)
      • [3.2 使用std::packaged_task和std::future配合​](#3.2 使用std::packaged_task和std::future配合)
      • [3.3 使用std::promise和std::future配合​](#3.3 使用std::promise和std::future配合)

一、项目介绍

RPC(Remote Procedure Call):远程过程调用,是一种通过网络从远程计算机上请求服务,而不需要了解底层网络通信细节。

RPC可以使用多种网络协议进行通信, 如HTTP、TCP、UDP等, 并且在TCP/IP网络四层模型中跨越了传输层和应用层。简言之RPC就是像调用本地方法一样调用远程方法。​

这个过程可以理解为业务处理、计算任务。更直白的说,就是程序/方法/函数等,就是像调用本地方法一样调用远程方法。

一个完整RPC通信框架,大概包含以下内容:​

  • 序列化协议
  • 通信协议
  • 连接复用
  • 服务注册
  • 服务发现
  • 服务订阅和通知
  • 负载均衡
  • 服务监控
  • 同步调用
  • 异步调用

我们的项目是基于C++、JsonCpp库、muduo网络库实现一个简单、易用的RPC通信框架,即使是不懂网络的开发者也可以很快速的上手,它实现了同步调用、异步callback调用、异步futrue调用、服务注册/发现,服务上线/下线以及发布订阅等功能设计。​

二、技术选择

我们选择的RPC实现方案是:实现一个远程调用接口call, 然后通过传入函数名参数来调用RPC接口。

网络传输的参数和返回值怎么映射到对应的RPC接口上?​使用通用的类型,比如JSON类型,设计好参数和返回值协议即可。

网络传输怎么做?使用第三方库muduo库,学习开发成本较低​。

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

三、第三方库使用介绍

1. JsonCpp库

1.1 Json是什么

Json是一种数据交换格式,它采用完全独立于编程语言的文本格式来存储和表示数据。

Json 的数据类型包括对象,数组,字符串,数字等。

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

1.2 JsonCpp库的三个核心类

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

为json格式字符串,以及将Json格式字符串解析得到多个数据对象的功能。

Json::Value类,中间数据存储类,如果要将数据对象序列化/反序列化,需要先存起来。

cpp 复制代码
class Json::Value
{
    Value&
    operator=(const Value& other); // Value重载了[]和=,因此所有的赋值和获取数据都可以通过​
    Value& operator[](const std::string& key); // 简单的方式完成 val["name"] = "xx";
    Value& operator[](const char* key);
    Value removeMember(const char* key);             // 移除元素​
    const Value& operator[](ArrayIndex index) const; // val["score"][0]
    Value& append(const Value& value);               // 添加数组元素val["score"].append(88); ​
    ArrayIndex size() const;                         // 获取数组元素个数 val["score"].size();​
    std::string asString() const;                    // 转string string name = val["name"].asString();
    const char* asCString() const;                   // 转char* char *name = val["name"].asCString();
    Int asInt() const;                               // 转int int age = val["age"].asInt();​
    float asFloat() const;                           // 转float float weight = val["weight"].asFloat();​
    bool asBool() const;                             // 转bool bool ok = val["ok"].asBool();​
};

Json::StreamWrite类,进行序列化的类。

cpp 复制代码
class JSON_API StreamWriter 
{
	// 序列化函数
	virtual int write(Value const& root, std::ostream* sout) = 0;
}

class JSON_API StreamWriterBuilder : public StreamWriter::Factory 
{
	// 工厂类,用于生产Json::StreamWriter对象
	virtual StreamWriter* newStreamWriter() const;
}

Json::CharReader类,进行反序列化的类。

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 
{
	// 工厂类,用于生产Json::CharReader对象
	virtual CharReader* newCharReader() const;
}

1.3 使用示例

cpp 复制代码
#include <iostream>
#include <jsoncpp/json/json.h>
#include <memory>
#include <sstream>
#include <string>

// 实现数据的序列化
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 serialize failed!\n";
        return false;
    }
    *body = ss.str();
    return true;
}

// 实现json字符串的反序列化
bool unserialize(const std::string& body, Json::Value* val)
{
    // 实例化工厂类对象
    Json::CharReaderBuilder crb;
    // 生产CharReader对象
    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 student;
    student["姓名"] = name;
    student["年龄"] = age;
    student["性别"] = sex;
    student["成绩"].append(score[0]);
    student["成绩"].append(score[1]);
    student["成绩"].append(score[2]);

    std::string body;
    serialize(student, &body);
    std::cout << "序列化结果:" << std::endl;
    std::cout << body << std::endl;

    Json::Value root;
    bool ret = unserialize(body, &root);
    if (ret == false)
        return -1;
    std::cout << "反序列化结果:" << std::endl;
    std::cout << "姓名: " << root["姓名"].asString() << std::endl;
    std::cout << "年龄: " << root["年龄"].asInt() << std::endl;
    std::cout << "性别: " << root["性别"].asCString() << std::endl;
    int sz = root["成绩"].size();
    for (int i = 0; i < sz; i++)
    {
        std::cout << "成绩: " << root["成绩"][i].asFloat() << std::endl;
    }
    return 0;
}

效果演示,序列化后中文可能有一些问题。但是反序列化后是正常的,没问题。

2. Muduo库

2.1 Muduo库介绍

muduo是陈硕开发的Linux平台下C++高性能网络库,基于epoll实现Reactor事件驱动模型,采用一线程一事件循环的多线程架构,封装好了TCP网络底层细节,依靠非阻塞IO与回调机制高效处理高并发网络连接,适合快速开发稳定高效的后端网络服务程序。

其使用的线程模型是one loop per thread, 所谓one loop per thread指的是:

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

2.2 Muduo库核心类接口

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();
    // cb函数,当一个新连接建立成功的时候会被调用​
    void setConnectionCallback(const ConnectionCallback& cb)
    {
        connectionCallback_ = cb;
    }
    /// cb函数,是消息的业务处理回调函数--收到新连接消息的时候被调用的函数​
    void setMessageCallback(const MessageCallback& cb)
    {
        messageCallback_ = cb;
    }
};

EventLoop类:

cpp 复制代码
class EventLoop : noncopyable
{
public:
    /// Loops forever.
    /// Must be called in the same thread as creation of the object.
    
    void loop();
    /// Quits loop.
    /// This is not 100% thread safe, if you call through a raw pointer,
    /// better to call through shared_ptr<EventLoop> for 100% safety.
    void quit();
    TimerId runAt(Timestamp time, TimerCallback cb);
    /// Runs callback after @c delay seconds.
    /// Safe to call from other threads.
    TimerId runAfter(double delay, TimerCallback cb);
    /// Runs callback every @c interval seconds.
    /// Safe to call from other threads.
    TimerId runEvery(double interval, TimerCallback cb);
    /// Cancels the timer.
    /// Safe to call from other threads.
    void cancel(TimerId timerId);

private:
    std::atomic<bool> quit_;
    std::unique_ptr<Poller> poller_;
    mutable MutexLock mutex_;
    std::vector<Functor> pendingFunctors_ GUARDED_BY(mutex_);
};

TcpConnection类:

cpp 复制代码
class TcpConnection : noncopyable, public std::enable_shared_from_this<TcpConnection>
{
public:
    /// Constructs a TcpConnection with a connected sockfd
    ///
    /// User should not create this object.
    TcpConnection(EventLoop* loop, const string& name, int sockfd, const InetAddress& localAddr,
                  const InetAddress& peerAddr);
    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_;
};

TcpCilent类:

cpp 复制代码
class TcpClient : noncopyable
{
public:
    // TcpClient(EventLoop* loop);
    // TcpClient(EventLoop* loop, const string& host, uint16_t port);
    TcpClient(EventLoop* loop, const InetAddress& serverAddr, const string& nameArg);
    ~TcpClient();      // force out-line dtor, for std::unique_ptr members.
    void connect();    // 连接服务器​
    void disconnect(); // 关闭连接​
    void stop();
    // 获取客户端对应的通信连接Connection对象的接口,发起connect后,有可能还没有连接建立成功​
    TcpConnectionPtr connection() const
    {
        MutexLockGuard lock(mutex_);
        return connection_;
    }
    /// cb函数,会在连接服务器成功时被回调​
    void setConnectionCallback(ConnectionCallback cb)
    {
        connectionCallback_ = std::move(cb);
    }
    /// 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_);
};

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() // 数据读取位置向后偏移4字节(删除起始四字节)
    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 // 尝试从缓冲区获取4字节数据,进行网络字节序转为整型,数据并不从缓冲区删除
    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[];
};

2.3 简单使用示例

server.cc

cpp 复制代码
// 实现一个翻译服务器,客户端发送过来一个英语单词,返回一个汉语词语
#include <iostream>
#include <muduo/net/Buffer.h>
#include <muduo/net/EventLoop.h>
#include <muduo/net/TcpConnection.h>
#include <muduo/net/TcpServer.h>
#include <string>
#include <unordered_map>

class DictServer
{
public:
    DictServer(int port)
        : _server(&_baseloop, muduo::net::InetAddress("0.0.0.0", port), "DictServer", muduo::net::TcpServer::kReusePort)
    {
        // 设置连接事件(连接建立/管理)的回调
        _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::net::EventLoop _baseloop;
    muduo::net::TcpServer _server;
};

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

client.cc

cpp 复制代码
#include <iostream>
#include <muduo/base/CountDownLatch.h>
#include <muduo/net/Buffer.h>
#include <muduo/net/EventLoop.h>
#include <muduo/net/EventLoopThread.h>
#include <muduo/net/TcpClient.h>
#include <muduo/net/TcpConnection.h>
#include <string>

class DictClient
{
public:
    DictClient(const std::string& sip, int sport)
        : _baseloop(_loopthread.startLoop()), _downlatch(1), // 初始化计数器为1,因为为0时才会唤醒
          _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->connected() == false)
        {
            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(); // 计数--,为0时唤醒阻塞
            _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;
};

int main()
{
    DictClient client("127.0.0.1", 8888);
    while (1)
    {
        std::string msg;
        std::cin >> msg;
        client.send(msg);
    }
    return 0;
}

效果演示:

3. C++11的异步操作:std::future

std::future是C++11标准库中的一个模板类,它表示一个异步操作的结果,是辅助我们获取异步任务结果的东西。当我们在多线程编程中使用异步任务时,std::future可以帮助我们在需要的时候获取任务的执行结果。std::future的一个重要特性是能够阻塞当前线程,直到异步操作完成,从而确保我们在获取结果时不会遇到未完成的操作。

应用场景:

  • 异步任务: 当我们需要在后台执行一些耗时操作时,如网络请求或计算密集型任务等,std::future
    可以用来表示这些异步任务的结果。通过将任务与主线程分离,我们可以实现任务的并行处理,从而提高程序的执行效率。
  • 并发控制: 在多线程编程中,我们可能需要等待某些任务完成后才能继续执行其他操作。通过使用std::future,我们可以实现线程之间的同步,确保任务完成后再获取结果并继续执行后续操作。
  • 结果获取:std::future提供了一种安全的方式来获取异步任务的结果。我们可以使用std::future::get()函数来获取任务的结果,此函数会阻塞当前线程,直到异步操作完成。这样,在
    调用get()函数时,我们可以确保已经获取到了所需的结果。

std::future需要搭配一些能够执行异步任务的模板类或函数一起使用,常用的有:

3.1 使用std::async关联异步任务​

std::async是一种将任务与std::future关联的简单方法。它创建并运行一个异步任务,并返回一个与该任务结果关联的std::future对象。默认情况下,std::async是否启动一个新线程,或者在等待future时,任务是否同步运行都取决于你给的 参数。这个参数为std::launch类型:​

  • std::launch::deferred 表明该函数会被延迟调用,直到在future上调用get()或者wait()才会开始执行任务
  • std::launch::async 表明函数会在自己创建的线程上运行
  • std::launch::deferred | std::launch::async 内部通过系统等条件自动选择策略
cpp 复制代码
#include <future>
#include <iostream>

int Add(int num1, int num2)
{
    std::cout << "into add!\n";
    return num1 + num2;
}

int main()
{
    // std::launch::async策略:内部创建一个线程执行函数,函数运行结果通过future获取
    // std::launch::deferred策略:同步策略,获取结果的时候再去执行任务
    std::future<int> res = std::async(std::launch::deferred, Add, 11, 22); // 进行了一个异步非阻塞调用
    std::cout << "--------------------------\n";
    // std::future<int>::get()  用于获取异步任务的结果,如果还没有结果就会阻塞
    std::cout << res.get() << std::endl;
    return 0;
}

3.2 使用std::packaged_task和std::future配合​

std::packaged_task就是将任务和 std::future 绑定在一起的模板,是一种对任务的封装。我们可以通过std::packaged_task对象获取任务相关联的std::future对象,通过调用get_future()方法获得。

std::packaged_task的模板参数是函数类型。​可以把std::future和std::async看成是分开的, 而std::packaged_task则是一个整体。

cpp 复制代码
#include <future>
#include <iostream>
#include <memory>
#include <thread>

int Add(int num1, int num2)
{
    std::cout << "into add!\n";
    return num1 + num2;
}

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

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

    // 3. 创建一个线程执行任务
    std::thread thr([task]() { (*task)(11, 22); });

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

3.3 使用std::promise和std::future配合​

std::promise提供了一种设置值的方式,它可以在设置之后通过相关联的std::future对象进行读取。换句话说,之前说过std::future可以读取一个异步函数的返回值, 但是要等待就绪, 而std::promise提供一种方式手动让 std::future就绪​。

cpp 复制代码
#include <future>
#include <iostream>
#include <thread>

int Add(int num1, int num2)
{
    std::cout << "into add!\n";
    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 thr(
        [&pro]()
        {
            int sum = Add(11, 22);
            pro.set_value(sum);
        });

    std::cout << res.get() << std::endl;
    thr.join();
    return 0;
}
相关推荐
墨月白1 小时前
【Python】程序设计基本方法
开发语言·python
凌波粒1 小时前
LeetCode--101. 对称二叉树(二叉树)
算法·leetcode·职场和发展
不知名的忻1 小时前
堆排序(Java)
java·数据结构·算法·排序算法
TAN-90°-1 小时前
Java 5——final 抽象 接口
java·开发语言
_深海凉_1 小时前
LeetCode热题100-二叉树的最大深度
算法·leetcode·职场和发展
Andy1 小时前
C++ 容器适配器_栈_队列_双端队列
开发语言·网络·c++
吴声子夜歌1 小时前
Java——显示锁
java·开发语言
菜_小_白1 小时前
tcpdump
linux·网络·测试工具·http·tcpdump
思麟呀1 小时前
在C++基础上理解Csharp-2
开发语言·jvm·c++·c#