当前位置: 首页 > news >正文

muduo 学习

1.muduo网络库服务器编写

/*
muduo网络库给用户提供了两个主要的类
TcpServer和TcpClient,分别用于服务器和客户端的编程。epoll + thread pool
好处: 能够将网络I/O代码和业务代码分离,提高并发性。业务:用户的连接和断开        用户的可读写事件
*/
#include <muduo/net/TcpServer.h>
#include <muduo/net/EventLoop.h>
#include <iostream>
#include <functional>
#include <string>using namespace std;
using namespace muduo;
using namespace muduo::net;
using namespace placeholders;
/*
基于muduo网络库开发服务器程序
1. 组合TcpServer 对象
2. 创建EventLoop事件循环指针;
3. 明确TcpServer 构造函数需要什么参数,输出Chatserver的构造函数
4. 在当前服务器类的构造函数中,注册处理连接的回调函数和处理读写事件的回调函数;
5. 设置合适的服务端线程数量,muduo会自己分配IO线程和workr线程;
*/
class ChatServer
{
public:ChatServer(EventLoop* loop, //事件循环 Reactorconst InetAddress& listenAddr, // ip+portconst string& nameArg) // 服务器名字:_server(loop,listenAddr,nameArg),_loop(loop){       // 服务器注册用户连接的创建和断开回调_server.setConnectionCallback(std::bind(&ChatServer::onConnection,this,_1));// 给服务器注册用户读写事件回调_server.setMessageCallback(std::bind(&ChatServer::onMessage,this,_1,_2,_3));// 设置服务器端的线程数量 一个I/O线程 3个worker线程_server.setThreadNum(4);}// 开启事件循环void start(){_server.start();}private:   // 专门处理用户的连接创建和断开 epoll listenfd acceptvoid onConnection(const TcpConnectionPtr& conn){if(conn->connected()){cout << conn->peerAddress().toIpPort() << "->" <<conn->localAddress().toIpPort() << "state:online" << endl;     }else{cout << conn->peerAddress().toIpPort() << "->" <<conn->localAddress().toIpPort() << "state:offline" << endl;  conn->shutdown(); // 调用用于关闭 TCP 连接// _loop->quit(); 退出服务器    这是用于退出 事件循环 的方法  这通常意味着 关闭整个服务器 或者停止继续处理来自网络接口的事件。}}// 专门处理读写事件void onMessage(const TcpConnectionPtr& conn, //连接Buffer* buffer, //缓冲区Timestamp time //接收到数据的事件信息 ){string buf = buffer->retrieveAllAsString();cout << "recv data " << buf << "time: " << time.toString() << endl;conn->send(buf);}TcpServer _server;EventLoop *_loop;  // epoll};int main()
{EventLoop loop; // epollInetAddress addr("192.168.169.132", 8080);ChatServer server(&loop, addr, "Chatserver");server.start(); //listenfd 通过epoll_ctl->epoll 添加到epoll上面loop.loop(); // epoll_wait 以阻塞方式等待新用户连接,或者已连接用户的读写操作;return 0;
}

整体在,muduo库提供的两个接口下进行, 需要注意的两点:

1.更多关注IO网络编程和业务分离,构造函数通过两个回调函数去调用具体的处理函数;我们更多关注的就是这两个 回调函数;

2.bind是一个指的注意的点,因为对于成员函数而言,第一个参数是this,但是作为回调函数,没有示例对象也就没有this,这个时候用bind去绑定解决这个问题,然后后面的_1,_2,_3就是占位符,去代替这个回调函数的参数;

编译运行指令:

g++ -o server muduo_server.cpp  -lmuduo_net -lmuduo_base -lpthread

然后

./server

另外开启一段终端

telnet 192.168....   port 去实现通信

客户端连接上后,服务端会显示connection的提示符也就是之前在onConnection定义的;

项目开始正是编写,先把CMakelist写好,以前用makefile但是现在cmake可以看作makefile升级,有了它可以自动生成makefile;

这段 CMakeLists.txt 文件用于配置一个基于 CMake 的项目,项目名称为 mymuduo,其目标是编译出一个名为 muduo 的共享库(动态库)。以下是对每一部分的详细解释:

cmake_minimum_required(VERSION 2.5)
project(mymuduo)# mymuduo 最终编译成so动态库, 设置动态库路径, 放在根目录的lib目录下
set(LIBRARY_OUTPUT_DIRECTORY ${PROJECT_BINARY_DIR}/lib)
# 设置调试信息 以及启动c++11语言标准
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -g -std=c++11")# 定义参与编译的的源文件
aux_source_directory(. SRC_LIST)
# 编译动态库
add_library((muduo SHARED ${SRC_LIST}))

1. cmake_minimum_required(VERSION 2.5)

  • 该指令指定了 CMake 的最低版本要求。在这里,要求使用 CMake 2.5 或更高版本来生成项目。

2. project(mymuduo)

  • 定义项目名称为 mymuduo

  • CMake 会根据项目名称生成默认的构建目录和工程文件等。

3.set(LIBRARY_OUTPUT_DIRECTORY ${PROJECT_BINARY_DIR}/lib)

  • 设置生成的 动态库输出目录,路径为 ${PROJECT_BINARY_DIR}/lib

  • ${PROJECT_BINARY_DIR}CMake 自动定义的变量,表示生成目录(即构建目录),通常是 build 目录。

  • 最终生成的动态库会存放在 build/lib 目录中。

4. set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -g -std=c++11")

  • 为 C++ 编译器设置编译选项:

    • -g:生成调试信息,用于调试程序。

    • -std=c++11:启用 C++11 标准。

  • ${CMAKE_CXX_FLAGS}CMake 的内置变量,用于存储当前项目的所有编译选项。这行代码在现有选项的基础上追加新的编译标志。

5. aux_source_directory(. SRC_LIST)

  • 将当前目录下(.)的所有源文件(如 .cpp 文件)添加到变量 SRC_LIST 中。

  • SRC_LIST 最终包含了所有需要编译的源文件列表。

6. add_library(muduo SHARED ${SRC_LIST})

  • 编译一个名为 muduo共享库(动态库)

  • SHARED 表示生成动态库(.so 文件,Linux 下动态库后缀)。

  • ${SRC_LIST} 是前面收集的所有源文件列表,作为动态库的编译输入文件。

2. noncopyable

就像注释一样,通过将基类拷贝构造函数和赋值函数delete,派生类就无法进行;

但是对于析构和构造还是可以正常的;并且对于类继承是private缺省,对于struct是public缺省;

这里需要注意的一点是,并不是说派生类对象没有自己的析构和构造,而是说派生类创建实例的过程,构造是由内而外,也就是说从基类构造开始再到派生类构造函数,析构刚好相反;

#pragma once/*noncopyable被继承后,派生类对象可以正常构造和析沟,但是派生类对象无法进行拷贝构造和赋值操作;
*/
class noncopyable
{public:noncopyable(const noncopyable&) = delete;noncopyable& operator=(const noncopyable&) = delete;private:protected:noncopyable() = default;~noncopyable() = default;
};

如果派生类尝试调用 noncopyable 的拷贝构造函数,编译会报错,因此派生类的拷贝和赋值都会失败。

4. 总结

  • noncopyable 禁止拷贝构造和赋值运算符后,所有继承自它的派生类默认也会禁止拷贝构造和赋值。
  • 即使派生类试图定义自己的拷贝构造和赋值运算符,也不能绕过 noncopyable= delete 限制,因为基类的拷贝构造和赋值被删除,无法调用。
  • 这种机制确保派生类也具备不可拷贝和赋值的特性,通常用于需要独占资源(如日志类、单例模式)等场景。

3. Logger

Logger.h

1.用enum枚举定义了不同的日志级别;

2. 日志类使用的是单例模式,也就是说这个类只能有一个实例,通过static实现的,它是类的函数而不属于对象;

然后还定义了设置日志级别的成员函数和写日志的函数;

3. 最后的一个点就是,并利用 宏(#define 来简化日志的记录操作。代码中的每个宏都通过不同的日志级别(INFO、ERROR、FATAL、DEBUG)来记录日志信息,且宏内通过 Logger 类的实例来处理日志写入

#pragma once// 定义日志级别 INFO ---- ERROR ---- FATAL ----DEBUG#include "noncopyable.h"
#include <string>// LOG_INFO("%s, %d", arg1, arg2)
#define LOG_INFO(logmsgFormat, ...) \ do \{ \Logger& logger = Logger::instance(); \logger.setLogLevel(INFO); \char buf[1024] = {0}; \snprintf(buf, 1024, logmsgFormat, ##__VA__ARGS__); \logger.log(buf); \} while(0)#define LOG_ERROR(logmsgFormat, ...) \ do \{ \Logger& logger = Logger::instance(); \logger.setLogLevel(ERROR); \char buf[1024] = {0}; \snprintf(buf, 1024, logmsgFormat, ##__VA__ARGS__); \logger.log(buf); \} while(0)#define LOG_FATAL(logmsgFormat, ...) \ do \{ \Logger& logger = Logger::instance(); \logger.setLogLevel(FATAL); \char buf[1024] = {0}; \snprintf(buf, 1024, logmsgFormat, ##__VA__ARGS__); \logger.log(buf); \} while(0)#ifdef MUDEBUG
#define LOG_DEBUG(logmsgFormat, ...) \ do \{ \Logger& logger = Logger::instance(); \logger.setLogLevel(DEBUG); \char buf[1024] = {0}; \snprintf(buf, 1024, logmsgFormat, ##__VA__ARGS__); \logger.log(buf); \} while(0)
#else#define LOG_DEBUG(logmsgFormat, ...)
#endifenum LogLevel
{INFO, // 普通信息ERROR, // 错误信息FATAL, // core信息DEBUG // 调试信息
};// 一个日志类
class Logger :  noncopyable
{public:// 获取唯一的实例对象;static Logger& instance();// 设置日至级别void setLogLevel(int level);// 写日志void log(std::string msg);private:int loglevel_; // 和系统变量不产生冲突,一般是前_Logger(){}
};
  • LOG_INFOLOG_ERRORLOG_FATALLOG_DEBUG 都是类似的宏定义,使用了 C 语言的变长参数(...)来实现日志信息的动态生成。通过 snprintf 将格式化的日志信息写入到 buf 中,然后调用 logger.log(buf) 输出日志。
  • 宏内的 Logger& logger = Logger::instance() 获取 Logger 类的唯一实例(因为 Logger 是单例模式),并通过 setLogLevel 设置日志级别。
  • snprintf 格式化日志内容并存储在 buf 数组中。##__VA_ARGS__ 语法用于处理可变参数列表,可以传入任意数量的参数并格式化它们。
  • logger.log(buf) 调用 Logger 实例的 log 方法来写日志。
  • 只有在 MUDEBUG 宏被定义时,LOG_DEBUG 才会被启用,这是一种控制调试日志是否启用的机制。
  • 如果 MUDEBUG 被定义(通常是在调试模式下),LOG_DEBUG 宏会记录调试信息;否则,它就什么也不做(通过定义空的 #define LOG_DEBUG)。
  • 是的,使用宏后 LOG_DEBUG 的代码块会自动执行,只要 MUDEBUG 被定义。
  • do-while(0) 保证了语法正确性,在任何上下文中都可以安全调用日志宏。
  • 宏展开在编译前完成,是编译器的一种优化手段,用于减少日志调用的复杂性

源文件cpp

基本 对头文件类里面定义的函数进行实现,记得前缀;

就是设置级别,然后按照级别写日志

#include "Logger.h"
#include <iostream>Logger& Logger::instance()
{static Logger logger;return logger;
}// 设置日至级别
void Logger::setLogLevel(int level)
{loglevel_ = level;
}// 写日志 [级别信息] time : msg
void Logger::log(std::string msg)
{switch (loglevel_){case INFO:std::cout << "[INFO]";break;case ERROR:std::cout << "[ERROR]";break;case FATAL:std::cout << "[FATAL]";break;case DEBUG:std::cout << "[DEBUG]";break;default:break;}// print time and msgstd::cout << "print time: " << msg << std::endl;
}

单例模式 (Singleton Pattern)

单例模式是一种常见的设计模式,目的是确保某个类只有一个实例,并提供全局访问点来获取该实例。通常用于管理共享资源或全局状态,例如日志记录器、数据库连接池、配置管理等。

单例模式的主要特征

  • 私有化构造函数:类的构造函数通常是私有的,防止外部代码通过 new 创建多个实例。
  • 静态成员:通常在类中有一个静态的指针(或引用)来保存唯一的实例。
  • 公共的静态方法:提供一个静态方法来获取该唯一实例。这个方法通常会创建实例(如果尚未创建)并返回该实例的指针。

4. Timestamp

把日志类里面输出时间的类的函数也加上

#pragma once#include <iostream>
#include <string>
class Timestamp
{
public:Timestamp();explicit Timestamp(int64_t microSecondsSinceEpoch);static Timestamp now();std::string toString() const;
private:int64_t microSecondsSinceEpoch_;
};
#include "Timestamp.h"
#include <iostream>
#include <time.h>
Timestamp::Timestamp():microSecondsSinceEpoch_(0) {}Timestamp::Timestamp(int64_t microSecondsSinceEpoch): microSecondsSinceEpoch_(microSecondsSinceEpoch){}Timestamp Timestamp::now()
{time_t ti = time(NULL);return Timestamp(ti);
}std::string Timestamp::toString() const
{char buf[128] = {0};tm* tm_time = localtime(&microSecondsSinceEpoch_);snprintf(buf, 128, "%4d/%02d/%02d %02d:%02d:%02d",tm_time->tm_year+1900,tm_time->tm_mon+1,tm_time->tm_mday,tm_time->tm_hour,tm_time->tm_min,tm_time->tm_sec);return buf;
}#include <iostream>int main()
{std::cout << Timestamp::now().toString() << std::endl;return 0;
}

5.  InetAddress

封装socket地址类型

InetAddress.hpp

#pragma once#include <arpa/inet.h>
#include <netinet/in.h>
#include <string>// 封装socket地址类型
class InetAddress
{public:// 构造函数,使用端口和IP地址初始化InetAddress对象,默认IP为127.0.0.1explicit InetAddress(uint16_t port, std::string ip="127.0.0.1");// 构造函数,使用sockaddr_in结构体初始化InetAddress对象explicit InetAddress(const sockaddr_in& addr): addr_(addr){}// 返回IP地址的字符串表示std::string toIP() const;// 返回IP地址和端口号的字符串表示std::string toIpPort() const;// 返回端口号uint16_t toPort() const;// 返回sockaddr_in结构体的指针const sockaddr_in* getSockAddr() const{return &addr_;}private:// 存储sockaddr_in地址信息sockaddr_in addr_;
};
#include "InetAddress.h"
#include <iostream>
#include <strings.h>
#include <string.h>InetAddress::InetAddress(uint16_t port, std::string ip)
{bzero(&addr_, sizeof addr_); //使用 bzero 函数清空 addr_,确保没有残留数据。addr_.sin_family = AF_INET; // 设置地址族为 IPv4(AF_INET)。addr_.sin_port = htons(port); // 使用 htons 函数将端口号转换为网络字节序,确保跨平台的一致性。addr_.sin_addr.s_addr = inet_addr(ip.c_str()); // 将输入的字符串类型 IP 地址转换为网络字节序,并赋值给 sin_addr.s_addr。
}std::string InetAddress::toIP() const
{// addr_char buf[64] = {0};::inet_ntop(AF_INET, &addr_.sin_addr, buf, sizeof(buf)); // 使用 inet_ntop 函数将网络字节序的 IP 地址转换为可读的字符串格式,并存储在 buf 中,最后返回该字符串。return buf;
}std::string InetAddress::toIpPort() const  // 将 IP 地址和端口号组合成一个字符串,格式为 "ip:port"。
{// ip::portchar buf[64] = {0};::inet_ntop(AF_INET, &addr_.sin_addr, buf, sizeof(buf));size_t end = strlen(buf);uint16_t port = ntohs(addr_.sin_port);sprintf(buf+end, ":%u", port); // 这一行代码的作用是将端口号格式化为字符串并追加到已有的 IP 地址字符串后面。return buf;
}uint16_t InetAddress::toPort() const
{return ntohs(addr_.sin_port); //使用 ntohs 函数将端口号从网络字节序转换为主机字节序并返回。
}

 主机和网络字节序之间的转换函数;一般为了跨平台,都用网络字节序存;

ntohs;ntohl;htons;ntonl ;

s->short;l->long;

ip一般32位,因此用htonl,ntohl;

inet_addr:

将字符串形式的 IP 地址转换为网络字节序的 in_addr 结构体中的 s_addr 成员,这实际上是一个 uint32_t 类型。

inet_ntoa

inet_ntop 

都是用于将网络字节序的 IP 地址转换为字符串格式的函数,但它们有一些重要的区别:

 

6.Channel

到这就需要搞清楚TcpServer\ Channel\ EventLoop之间的关系,和Reactor模型具体如何对应;

 从这个框架结构看出来,reactot和Demultiplex 合在一起就很想epoll机制,注册sockfd和感兴趣事件; 其中reactor则存储是事件和事件处理器绑定的一个集合,更多的操作是由事件分发器Demultiplex完成的,它的功能更像epoll,比如说注册sockfd和感兴趣时间,然后监听到有事件发生,返回发生的事件给Reactor让它去找到对应的事件handler去处理;

 在muduo库中具体实现一个EventLoop就包含了reactor和Demultiplex(poller),然后Channel是event,  也就是说一个poller监控着多个sockfd然后对应channel,相当于一个epoll监控多个sockfd;

所以poller包含Channel;

然后一个线程一个epoll--------one thread one loop;

因为对服务器来说单线程不是很充分利用资源;作为一个高并发服务器

到这才是真正的框架; 

相当于poller可以监控多个sockfd,每个sockfd对应一个channel,用于注册sockfd和感兴趣的事件,因此Channel类就需要设置sockfd相应的事件状态和返回相应的事件状态,并且调用具体发生事件的回调函数

Channel 类的作用

Channel 类本质上是一个事件管理单元,它将一个sockfd与其感兴趣的事件(如可读、可写、异常等)关联,并在事件发生时触发回调函数。

Channel 和 sockfd 的绑定

可以将 sockfdChannel 看作一一对应的关系,你的比喻“将 sockfdChannel 做了 map 绑定在一起”是非常形象的。这种关系的核心作用如下:

  1. 管理状态Channel 保存 sockfd 以及当前感兴趣的事件类型,如 EPOLLIN(可读)、EPOLLOUT(可写)。
  2. 处理回调:当某个 sockfd 发生感兴趣的事件时,Channel 会调用对应的回调函数来处理事件。
  3. 解耦逻辑Channel 将文件描述符和具体的事件处理逻辑解耦,使得 poller(如 epoll)只需要管理事件的分发,而不关心具体的业务逻辑。

工作机制总结

  1. Channel 类负责:

    • 保存 sockfd 和感兴趣的事件(EPOLLINEPOLLOUT等)。
    • 提供注册回调函数的方法,如 setReadCallback
    • 当事件发生时,通过 handleEvent 调用相应的回调函数。
  2. Poller 类负责:

    • 管理 Channelsockfd 的映射。
    • 使用 epoll_ctlsockfd 注册到 epoll 实例。
    • 调用 epoll_wait 等待事件发生并通知相应 Channel 处理。

总结

你描述的 Channel 将 sockfd 与感兴趣的事件绑定并调用回调函数的理解是准确的。这种设计体现了职责分离事件驱动编程思想:

  • Channel 负责具体 sockfd 的事件管理和回调。
  • Poller 负责高效监听和分发事件。
#pragma once#include "noncopyable.h"
#include "Timestamp.h"
#include <memory>#include <functional>class EventLoop;
// class Timestamp;/* 理清楚 EventLoop\ Channel\ poller之间的关系 Channel 理解为通道,封装了sockfd和其他感兴趣event,如EPOLLIN、EPOLLOUT事件还绑定了监听返回的具体事件;
*/
class Channel : noncopyable
{
public:using EventCallback = std::function<void()>;using ReadEventCallback = std::function<void(Timestamp)>;Channel(EventLoop* loop, int fd);~Channel();// fd得到poller通知后处理事件的void handleEvent(Timestamp receiveTime);// 设置回调函数对象void setReadCallback(ReadEventCallback cb) {readCallback_ = std::move(cb);} // move左值转右值void setwriteCallback(EventCallback cb) { writeCallback_ = std::move(cb);}void setCloseCallback(EventCallback cb) { closeCallback_ = std::move(cb);}void setErrorCallback(EventCallback cb) {  errorCallback_ = std::move(cb);}// 防止当Channel被手动remove掉,还在执行回调操作void tie(const std::shared_ptr<void>&);int fd() const{return fd_;}int events() const{return events_;}int set_revents(int revt) { revents_ = revt; } // epoll监听事件,所以channel有一个对外接口让epoll设置到底什么事件发生// 设置fd相应的事件状态void enableReading() {events_ |= kReadEvent; update();}void disableReading() {events_ &= ~kReadEvent; update();}void enableWriting() {events_ |= kWriteEvent; update();}void disableWriting() {events_ &= ~kWriteEvent; update();}void disableAll() {events_ &= kNoneEvent; update();}// 返回fd当前的事件状态bool isNoneEvent() const{ return events_ == kNoneEvent;}bool isWriting() const{ return events_ & kWriteEvent;}bool isReading() const { return events_ & kReadEvent;}int index() { return index_;}void set_index(int idx) { index_ = idx;}// one loop per threadEventLoop* ownerLoop(){return loop_;}void remove();
private:void update();void handleEventWithGuard(Timestamp receiveTime);static const int kNoneEvent;static const int kReadEvent;static const int kWriteEvent;EventLoop* loop_;const int fd_; // poller监听的对象int events_; // 注册fd感兴趣事件 epoll_ctlint revents_; // poller返回具体发生的事件int index_; std::weak_ptr<void> tie_;bool tied_;// 因为Channl通道里面,能够获知fd最终发生的具体的事件revents,所以它负责具体事件的回调操作;ReadEventCallback readCallback_;EventCallback writeCallback_;EventCallback closeCallback_;EventCallback errorCallback_;
};

有了前面的了解之后就可以开始channel类编写了,成员变量就包括:所属loop_, poller监听的fd_,poll而返回具体发生的revents_ 以及index;

这个用的是回调也就和EventHandler对应上了。

利用Channel向EventLoop下的Poller注册或者删除 对应的sockfd和感兴趣事件; 

#include "Channel.h"
#include "EventLoop.h"
#include <sys/epoll.h>
#include <functional>
#include "Logger.h"const int Channel::kNoneEvent = 0;
const int Channel::kReadEvent = EPOLLIN | EPOLLPRI;
const int Channel::kWriteEvent = EPOLLOUT;// EventLoop: ChannelList Poller
Channel::Channel(EventLoop* loop, int fd) : loop_(loop), fd_(fd), events_(0), revents_(0), index_(-1), tied_(false)
{}Channel::~Channel()
{
//     if (loop_->isInLoopThread())
//     {
//         assert(!loop_->hasChannel(this));
//     }  //这个操作就是判断当前这个loop是否是在这个线程下面,以及当前这个channel是否实在这个loop下面,因为目前都是高并发服务器,多线程}void Channel::tie(const std::shared_ptr<void>& obj)
{tie_ = obj;tied_ = true;
}// 该方法的作用是,当改变channel所表示的fd的事件后,update负责在poller里面更改fd相应的事件 epoll_ctl
// EventLoop => ChannelList       Poller     独立
void Channel::update()
{// 通过Channel所属的EventLoop调用poller的相应方法,注册fd的event事件;// add code...// loop_->updateChannl(this);}// 在Channel所属的EventLoop中,把当前的channel删除
void Channel::remove()
{   // add code ..// loop_->removeChannel(this);
}void Channel::handleEvent(Timestamp receiveTime)
{if(tied_){std::shared_ptr<void> guard = tie_.lock();if(guard){handleEventWithGuard(receiveTime);}} else{handleEventWithGuard(receiveTime);}
}// 根据poller通知的channel发生的具体事件,由channel负责调用具体回调操作
void Channel::handleEventWithGuard(Timestamp receiveTime)
{LOG_INFO("channel handleEvent revents:%d\n", revents_);if(revents_ && EPOLLHUP && !(revents_ & EPOLLIN)){if(closeCallback_){closeCallback_();}}if(revents_ & (EPOLLERR)){if(errorCallback_){errorCallback_();}}if(revents_ & (EPOLLIN | EPOLLPRI)){if(readCallback_){readCallback_(receiveTime);}}if(revents_ & EPOLLOUT){if(writeCallback_){writeCallback_();}}
}


http://www.mrgr.cn/news/78916.html

相关文章:

  • Web安全基础实践
  • 使用Tauri创建桌面应用
  • numpy快速入门
  • AnolisOS8基于yum安装mongodb6
  • 第三章:基本语法 3.数据类型 --Go 语言轻松入门
  • 挑战用React封装100个组件【007】
  • hadoop集群搭建
  • leetcode 之 二分查找(java)(2)
  • 机器学习8-决策树CART原理与GBDT原理
  • 南昌大学(NCU)羽毛球场地预约脚本
  • leeCode算法之最接近的三数之和求解
  • 畅游Diffusion数字人(9):Magic-Me: Identity-Specific Video Customized Diffusion
  • 数据结构——排序第三幕(深究快排(非递归实现)、快排的优化、内省排序,排序总结)超详细!!!!
  • 用到动态库的程序运行过程
  • 繁体字异体字整理(未整理完)
  • LeetCode hot100(自用背诵、部分题目、非最优解)
  • PG 库停库超时异常案例
  • 开源项目 - 人脸关键点检测 facial landmark 人脸关键点 (98个关键点)
  • 4399 Android面试题及参考答案
  • Flutter:页面滚动
  • SCAU期末笔记 - 数据库系统概念
  • 洛谷二分题
  • 鸿蒙技术分享:Navigation页面管理-鸿蒙@fw/router框架源码解析(二)
  • OpenCV_Code_LOG
  • 从0学习JavaScript(2)
  • 【大数据技术基础 | 实验十四】Kafka实验:订阅推送示例