基础知识

网络IO的两个阶段

第一阶段:数据准备阶段 (Waiting for the data to be ready)
这个阶段是指数据从网络到达网卡,再经过内核处理,直到**内核缓冲区(TCP 接收缓冲区)**中有足够的数据可读的过程。
阻塞 (Blocking):用户线程发起调用后被挂起,什么都不干,直到数据准备好。
非阻塞 (Non-blocking):如果数据没准备好,内核立即返回一个错误码(如 EAGAIN 或 EWOULDBLOCK)。用户线程不会被挂起,可以轮询或去做别的事。
关键点:这个阶段取决于文件描述符(fd)的属性设置。

第二阶段:数据读写阶段 (Copying data from the kernel to user space)
这个阶段是指数据已经到达内核缓冲区后,将其从内核态拷贝到用户态(应用程序内存)的过程。
同步 (Synchronous):用户线程亲自参与拷贝。在数据从内核缓冲区拷贝到用户态内存的过程中,用户线程是阻塞的(即便第一阶段是非阻塞的,拷贝这一步也得等)。
常见的同步 I/O:阻塞 I/O、非阻塞 I/O、I/O 复用(select/poll/epoll)、信号驱动 I/O。
异步 (Asynchronous):用户线程完全不参与拷贝。用户只需告诉内核:“把数据读完后放到这个数组里并通知我”。内核在后台完成“等待数据”和“拷贝数据”两个阶段,完成后再通知用户。
关键点:区分同步与异步的唯一标准是
“谁负责把数据搬进用户房间”
。如果内核搬,就是异步;如果用户自己动手搬,就是同步。

Node.js是高能的异步非阻塞高性能服务器

Unix的五种IO模型

  1. 阻塞 I/O (Blocking I/O)
    这是最简单的模型。当进程调用一个 I/O 函数(如 recvfrom)时,该调用会一直等待,直到数据到达并被拷贝到用户态缓冲区为止。
    第一阶段(等数据):进程阻塞,放弃 CPU 控制权。
    第二阶段(拷贝):进程继续阻塞,直到拷贝完成。
    特点:实现简单,但线程利用率极低。如果数据没来,线程就死等。
    在这里插入图片描述

  2. 非阻塞 I/O (Non-blocking I/O)
    进程通过设置文件描述符为非阻塞,如果内核缓冲区没有数据,函数会立即返回一个错误(如 EWOULDBLOCK),而不是挂起线程。
    第一阶段(等数据):进程不断轮询(Polling)内核:数据好了吗?没好就立即返回,好就进入第二阶段。
    第二阶段(拷贝):进程阻塞,手动完成数据拷贝。
    特点:线程不会被挂起,可以做别的事,但轮询操作会大量消耗 CPU 资源。
    在这里插入图片描述

  3. I/O 复用 (I/O Multiplexing)
    这是目前高性能服务器(如 Redis, Nginx)最常用的模型。进程调用 select、poll 或 epoll,这些函数可以同时监控多个文件描述符。
    第一阶段(等数据):进程阻塞在 select 或 epoll_wait 调用上。一旦有一个或多个描述符就绪(有数据到了),函数返回。
    第二阶段(拷贝):进程紧接着调用 recvfrom,阻塞并完成数据拷贝。
    特点:单个线程可以同时处理成千上万个连接,虽然第一阶段仍是阻塞的,但它极大地提高了系统并发能力。
    在这里插入图片描述
    在这里插入图片描述在这里插入图片描述在这里插入图片描述

  4. 信号驱动 I/O (Signal Driven I/O)
    进程通知内核:当数据准备好时,请给我发一个 SIGIO 信号。
    第一阶段(等数据):进程不阻塞,继续执行其他任务。当数据准备好,内核发信号。
    第二阶段(拷贝):进程在信号处理函数中调用 recvfrom,阻塞并完成数据拷贝。
    特点:等待数据阶段是非阻塞的,但数据拷贝阶段依然是同步阻塞的。
    在这里插入图片描述

  5. 异步 I/O (Asynchronous I/O)
    这是真正的异步模型。进程发起 I/O 请求后立即返回,内核负责等待数据并将其直接拷贝到用户指定的缓冲区,全部完成后再通知进程。
    第一阶段(等数据):内核自动处理,进程完全不感知,不阻塞。
    第二阶段(拷贝):内核自动处理,进程完全不感知,不阻塞。
    特点:两个阶段都不阻塞进程。这是性能最高的模型,但实现复杂度最高,在 Unix/Linux 下的成熟度(AIO)不如 I/O 复用。
    在这里插入图片描述

在这里插入图片描述

Reactor

在这里插入图片描述

Muduo

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

简单的使用

#include <muduo/net/TcpServer.h>
#include <muduo/net/EventLoop.h>
#include <iostream>
#include <string>

using std::cout;
using std::endl;
using namespace muduo;
using namespace muduo::net;

/*
 * Muduo Server 开发一般步骤:
 * 1. 组合 TcpServer 对象
 * 2. 创建 EventLoop 事件循环对象的指针
 * 3. 明确 TcpServer 构造函数参数
 * 4. 注册回调函数 (连接建立/断开回调, 消息读写回调)
 * 5. 设置线程数量
 */
class ChatServer
{
public:
    ChatServer(EventLoop *loop,
               const InetAddress &listenAddr,
               const string &nameArg)
        : server_(loop, listenAddr, nameArg), loop_(loop)
    {
        // 1. 给服务器注册用户连接的创建和断开回调
        server_.setConnectionCallback(
            std::bind(&ChatServer::onConnection, this, _1));

        // 2. 给服务器注册用户读写事件回调
        server_.setMessageCallback(
            std::bind(&ChatServer::onMessage, this, _1, _2, _3));

        // 3. 设置后端线程数量 (1个 I/O 线程,N个 工作线程)
        server_.setThreadNum(4);
    }

    // 开启事件循环
    void start()
    {
        server_.start();
    }

private:
    // 处理用户的连接创建和断开 (epoll listenfd accept)
    void 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(); // close(fd)
        }
    }

    // 处理用户的读写事件 (epoll connfd 读写事件)
    // buf: 缓冲区 (Muduo封装好的,自动扩容)
    // time: 接收到数据的时间信息
    void onMessage(const TcpConnectionPtr &conn,
                   Buffer *buf,
                   Timestamp time)
    {
        string msg = buf->retrieveAllAsString(); // 把接收到的数据全部转成 string
        cout << "收到消息: " << msg << " 长度: " << msg.size() << endl;

        // 回显:把收到的消息原封不动发回去
        conn->send(msg);
    }

    TcpServer server_; // #1
    EventLoop *loop_;  // #2
};

int main()
{
    // 创建 EventLoop,相当于 epoll_create
    EventLoop loop;

    // 创建地址对象,监听 6000 端口
    InetAddress addr(6000);

    // 创建 Server 对象
    ChatServer server(&loop, addr, "ChatServer");

    // 启动 Server,相当于 bind + listen
    server.start();

    // 进入事件循环,相当于 epoll_wait (阻塞)
    cout << "Server is running on port 6000..." << endl;
    loop.loop();

    return 0;
}

源码

noncopyable

class noncopyable
{
 public:
  noncopyable(const noncopyable&) = delete;
  void operator=(const noncopyable&) = delete;

 protected:
  noncopyable() = default;
  ~noncopyable() = default;
};

  • 当一个类不能被拷贝和赋值时可以继承这个noncopyable类,保证子类可以
    在这里插入图片描述
    在这里插入图片描述

日志代码的书写

Do-While-Zero

导致语句没有执行完全
在这里插入图片描述
由于习惯的分号结尾导致的语法错误
在这里插入图片描述

#pragma once
#include "./nocopyable.h"
#include <string>
#include <memory>

// 定义日志宏,方便使用,为什么方便?
/*
    // 惨不忍睹的直接调用写法
    char buffer[1024];
    snprintf(buffer, sizeof(buffer), "User %s login failed, id: %d", userName.c_str(), userId); // 1. 手动格式化
    Logger::instance().setLogLevel(INFO); // 2. 手动设置级别
    Logger::instance().log(buffer);       // 3. 发送日志

    1. 使用宏定义,简化日志调用
    LOG_INFO("User %s login failed, id: %d", userName.c_str(), userId);
*/
// LOG_INFO("%s %d",arg1,arg2)
#define LOG_INFO(logmsgFormat, ...)                              \
    do                                                           \
    {                                                            \
        Logger::instance().setLogLevel(INFO);                    \
        char buf[1024] = {0};                                    \
        snprintf(buf, sizeof(buf), logmsgFormat, ##__VA_ARGS__); \
        Logger::instance().log(buf);                             \
    } while (0)
/* 书写宏的注意事项
1. 宏定义尽量使用大写字母,避免和变量名冲突
2. 宏定义如果有多条语句,使用 do{...}while(0) 包裹,避免使用时出现语法错误
3. 有多行的宏定义,每行末尾使用反斜杠 \ 连接,而且其后面不能有任何字符,包括空格和注释
4. 宏定义中如果有参数,参数名前后不要加括号
     为什么: #define SQUARE(x) ((x)*(x))  比 #define SQUARE(x) (x)*(x) 好
5. ##__VA_ARGS__ 用于处理可变参数宏,表示宏参数列表中的所有参数
 */
#define LOG_ERROR(logmsgFormat, ...)                             \
    do                                                           \
    {                                                            \
        Logger::instance().setLogLevel(ERROR);                   \
        char buf[1024] = {0};                                    \
        snprintf(buf, sizeof(buf), logmsgFormat, ##__VA_ARGS__); \
        Logger::instance().log(buf);                             \
    } while (0)
#define LOG_FATAL(logmsgFormat, ...)                             \
    do                                                           \
    {                                                            \
        Logger::instance().setLogLevel(FATAL);                   \
        char buf[1024] = {0};                                    \
        snprintf(buf, sizeof(buf), logmsgFormat, ##__VA_ARGS__); \
        Logger::instance().log(buf);                             \
    } while (0)

// 由于debug的信息比较多,一般发布版本不需要打印debug信息
#ifdef NDEBUG
#define LOG_DEBUG(logmsgFormat, ...)                             \
    do                                                           \
    {                                                            \
        Logger::instance().setLogLevel(DEBUG);                   \
        char buf[1024] = {0};                                    \
        snprintf(buf, sizeof(buf), logmsgFormat, ##__VA_ARGS__); \
        Logger::instance().log(buf);                             \
    } while (0)
#else
#define LOG_DEBUG(logmsgFormat, ...) \
    do                               \
    {                                \
    } while (0)
#endif

enum LogLevel
{
    INFO,  // 普通信息
    ERROR, // 错误信息
    FATAL, // 致命错误信息
    DEBUG  // 调试信息
};

class Logger : noncopyable
{
public:
    // 获取日志唯一的实例对象
    Logger &instance();

    // 设置日志级别
    void setLogLevel(int level);
    // 写日志
    void log(std::string msg);

private:
    int logLevel_;
    Logger() {}
};


#include "./logger.h"
#include <iostream>
using std::cout;
using std::endl;

Logger &Logger::instance()
{
    static Logger logger;
    return logger;
}

void Logger::setLogLevel(int level)
{
    logLevel_ = level;
}

// 写日志 [级别信息] 时间 信息
void Logger::log(std::string msg)
{
    // 此处省略具体的日志写入实现
    switch (logLevel_)
    {
    case INFO:
        cout << "[INFO]: " << msg << endl;
        break;
    case ERROR:
        cout << "[ERROR]: " << msg << endl;
        break;
    case FATAL:
        cout << "[FATAL]: " << msg << endl;
        break;
    case DEBUG:
        cout << "[DEBUG]: " << msg << endl;
        break;
    default:
        cout << "[UNKNOWN]: " << msg << endl;
        break;
    }

    // 打印时间和msg
    cout << "print time: " << time(nullptr) << " msg: " << msg << endl;
}

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐