【Muduo库】套接字模块,Channel模块,Poller模块

目录

一.套接字模块

1.1.设计并实现

1.2.知识点补充

1.3.编译测试

二.Channel模块

2.1.功能回顾

2.2.代码实现

2.2.1.分模块讲解

2.2.2.代码总览

2.2.3.代码实现小细节

三.Poller模块

3.1.功能回顾

3.2.代码实现

3.3.Poller模块和Channel模块整合

四.Channel模块和Poller模块联合测试


一.套接字模块

1.1.设计并实现

我们这个模块其实是很简单的

  • 创建套接字
  • 绑定地址信息
  • 开始监听
  • 向服务器发起连接
  • 获取新连接
  • 发送数据
  • 关闭套接字
  • 创建一个服务端连接
  • 创建一个客户端连接
  • 设置套接字选项——开启地址端口复用
  • 设置套接字阻塞属性——设置为非阻塞

按照上面的描述,我们很快就能写出下面这个代码

#pragma once
#include "log.hpp"
#include "buffer.hpp"

#define MAX_LISTEN 1024  // 最大监听连接数

class Socket {
private:
    int _sockfd;  // 套接字文件描述符
    
public:
    // 默认构造函数,初始化套接字描述符为-1
    Socket();
    
    // 带参构造函数,用已有的文件描述符初始化
    Socket(int fd);
    
    // 析构函数,自动关闭套接字
    ~Socket();
    
    // 获取套接字文件描述符
    int Fd();
    
    // 创建TCP套接字
    bool Create();
    
    // 绑定地址信息
    bool Bind(const std::string &ip, uint16_t port);
    
    // 开始监听连接请求
    bool Listen(int backlog = MAX_LISTEN);
    
    // 向服务器发起连接请求
    bool Connect(const std::string &ip, uint16_t port);
    
    // 接受新连接请求,返回新连接的套接字描述符
    int Accept();
    
    // 接收数据(默认阻塞模式)
    ssize_t Recv(void *buf, size_t len, int flag = 0);
    
    // 非阻塞接收数据
    ssize_t NonBlockRecv(void *buf, size_t len);
    
    // 发送数据(默认阻塞模式)
    ssize_t Send(const void *buf, size_t len, int flag = 0);
    
    // 非阻塞发送数据
    ssize_t NonBlockSend(void *buf, size_t len);
    
    // 关闭套接字
    void Close();
    
    // 创建服务器端套接字(完整流程)
    bool CreateServer(uint16_t port, const std::string &ip = "0.0.0.0", bool block_flag = false);
    
    // 创建客户端套接字(完整流程)
    bool CreateClient(uint16_t port, const std::string &ip);
    
    // 设置套接字选项:开启地址和端口重用
    void ReuseAddress();
    
    // 设置套接字为非阻塞模式
    void NonBlock();
};

完整代码

#pragma once
#include "log.hpp"
#include "buffer.hpp"

// 定义最大监听连接数,即服务器可以同时处理的最大待处理连接请求数量
#define MAX_LISTEN 1024

class Socket
{
private:
    // 套接字文件描述符,用于唯一标识操作系统中的套接字对象
    int _sockfd; 

public:
    // 默认构造函数,初始化套接字描述符为-1(表示无效套接字)
    // -1 是一个约定俗成的无效描述符值,表示套接字尚未创建或已关闭
    Socket() : _sockfd(-1) {}

    // 带参构造函数,用已有的文件描述符初始化Socket对象
    // 常用于从accept函数接收的新连接创建Socket对象
    Socket(int fd) : _sockfd(fd) {}

    // 析构函数,对象销毁时自动关闭套接字,防止资源泄漏
    // 这是RAII(资源获取即初始化)设计模式的应用
    ~Socket()
    {
        Close();  // 调用Close方法关闭套接字
    }

    // 获取套接字文件描述符,用于需要原始描述符的操作
    // 返回类型为int,因为文件描述符在POSIX系统中是整型
    int Fd()
    {
        return _sockfd;
    }

    // 创建TCP套接字
    // 这是套接字编程的第一步,创建一个通信端点
    bool Create()
    {
        // 调用socket系统函数创建套接字
        // 参数说明:
        // AF_INET - IPv4地址族,表示使用IPv4协议
        // SOCK_STREAM - 流式套接字,提供可靠的、面向连接的、双向字节流通信
        // IPPROTO_TCP - TCP协议,提供流量控制、错误检测和重传机制
        // 返回文件描述符(非负整数)或-1(表示失败)
        _sockfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
        
        // 检查套接字创建是否成功
        if (_sockfd < 0)
        {
            ERR_LOG("CREATE SOCKET FAILED!!");
            return false;  // 创建失败,返回false
        }
        return true;  // 创建成功,返回true
    }

    // 绑定地址信息,将套接字与特定的IP地址和端口号关联
    // 服务器必须绑定地址以便客户端能够连接
    bool Bind(const std::string &ip, uint16_t port)
    {
        // 定义IPv4地址结构体,用于存储绑定信息
        struct sockaddr_in addr;
        
        // 设置地址族为IPv4
        addr.sin_family = AF_INET;
        
        // 设置端口号,htons函数将主机字节序转换为网络字节序(big-endian)
        // 这是网络编程的必要步骤,确保不同架构的计算机能够正确解析端口号
        addr.sin_port = htons(port);
        
        // 设置IP地址,inet_addr函数将点分十进制字符串(如"192.168.1.1")转换为网络字节序的32位整数
        // 如果ip是"0.0.0.0",表示绑定到所有可用的网络接口
        addr.sin_addr.s_addr = inet_addr(ip.c_str());
        
        // 计算地址结构体的长度,bind函数需要这个参数
        socklen_t len = sizeof(struct sockaddr_in);
        
        // 调用bind系统函数,将套接字与地址绑定
        // 参数说明:
        // _sockfd - 要绑定的套接字描述符
        // (struct sockaddr *)&addr - 地址结构体指针,需要进行类型转换
        // len - 地址结构体的长度
        // 返回0表示成功,-1表示失败
        int ret = bind(_sockfd, (struct sockaddr *)&addr, len);
        
        // 检查绑定是否成功
        if (ret < 0)
        {
            // 记录绑定失败的错误信息
            ERR_LOG("BIND ADDRESS FAILED!");
            return false;  // 绑定失败,返回false
        }
        return true;  // 绑定成功,返回true
    }

    // 开始监听连接请求,将套接字设置为被动监听模式
    // 服务器套接字必须调用listen才能接受客户端连接
    bool Listen(int backlog = MAX_LISTEN)
    {
        // 调用listen系统函数,设置套接字为监听状态
        // 参数说明:
        // _sockfd - 要监听的套接字描述符
        // backlog - 等待连接队列的最大长度,即可以同时处理的最大未完成连接请求数
        // 返回0表示成功,-1表示失败
        int ret = listen(_sockfd, backlog);
        
        // 检查监听是否成功
        if (ret < 0)
        {
            // 记录监听失败的错误信息
            ERR_LOG("SOCKET LISTEN FAILED!");
            return false;  // 监听失败,返回false
        }
        return true;  // 监听成功,返回true
    }

    // 向服务器发起连接请求,用于客户端连接服务器
    bool Connect(const std::string &ip, uint16_t port)
    {
        // 定义IPv4地址结构体,用于存储服务器地址信息
        struct sockaddr_in addr;
        
        // 设置地址族为IPv4
        addr.sin_family = AF_INET;
        
        // 设置服务器端口号,转换为网络字节序
        addr.sin_port = htons(port);
        
        // 设置服务器IP地址,转换为网络字节序
        addr.sin_addr.s_addr = inet_addr(ip.c_str());
        
        // 计算地址结构体的长度
        socklen_t len = sizeof(struct sockaddr_in);

        // 调用connect系统函数,建立与服务器的连接
        // 参数说明:
        // _sockfd - 要建立连接的套接字描述符
        // (struct sockaddr *)&addr - 服务器地址结构体指针
        // len - 地址结构体的长度
        // 返回0表示成功,-1表示失败
        int ret = connect(_sockfd, (struct sockaddr *)&addr, len);
        
        // 检查连接是否成功
        if (ret < 0)
        {
            // 记录连接失败的错误信息
            ERR_LOG("CONNECT SERVER FAILED!");
            return false;  // 连接失败,返回false
        }
        return true;  // 连接成功,返回true
    }

    // 接受新连接请求,服务器从监听队列中取出一个连接请求并创建新的套接字
    // 返回新连接的套接字描述符,用于后续与客户端通信
    int Accept()
    {
        // 调用accept系统函数,接受新的连接
        // 参数说明:
        // _sockfd - 监听套接字描述符
        // NULL, NULL - 不关心客户端地址信息,如果需要可以传入指针获取客户端地址
        // 返回新的套接字描述符(非负整数)或-1(表示失败)
        int newfd = accept(_sockfd, NULL, NULL);
        
        // 检查接受连接是否成功
        if (newfd < 0)
        {
            // 记录接受连接失败的错误信息
            ERR_LOG("SOCKET ACCEPT FAILED!");
            return -1;  // 接受失败,返回-1
        }
        return newfd;  // 接受成功,返回新连接的描述符
    }

    // 接收数据(默认阻塞模式),从已连接的套接字读取数据
    // 这是网络编程中最常用的数据接收方式
    ssize_t Recv(void *buf, size_t len, int flag = 0)
    {
        // 调用recv系统函数,接收数据
        // 参数说明:
        // _sockfd - 要接收数据的套接字描述符
        // buf - 接收数据缓冲区指针
        // len - 缓冲区大小(最大接收字节数)
        // flag - 接收标志,0表示默认阻塞模式
        // 返回实际接收的字节数,0表示连接关闭,-1表示错误
        ssize_t ret = recv(_sockfd, buf, len, flag);
        
        // 检查接收结果
        if (ret <= 0)
        {
            // 如果errno为EAGAIN或EINTR,属于非致命错误,可以重试
            // EAGAIN: 在非阻塞模式下,接收缓冲区没有数据可读
            // EINTR: 当前socket的阻塞等待,被信号中断
            if (errno == EAGAIN || errno == EINTR)
            {
                return 0;  // 返回0表示这次没有接收到数据,但不一定是错误
            }
            
            // 记录真正的接收错误信息
            ERR_LOG("SOCKET RECV FAILED!!");
            return -1;  // 接收失败,返回-1
        }
        return ret;  // 接收成功,返回实际接收的数据长度
    }

    // 非阻塞接收数据,使用MSG_DONTWAIT标志使接收操作立即返回
    ssize_t NonBlockRecv(void *buf, size_t len)
    {
        // 调用Recv函数,但传入MSG_DONTWAIT标志
        // MSG_DONTWAIT标志使recv函数在没有数据可读时立即返回,而不是阻塞等待
        return Recv(buf, len, MSG_DONTWAIT);
    }

    // 发送数据(默认阻塞模式),向已连接的套接字写入数据
    ssize_t Send(const void *buf, size_t len, int flag = 0)
    {
        // 调用send系统函数,发送数据
        // 参数说明:
        // _sockfd - 要发送数据的套接字描述符
        // buf - 发送数据缓冲区指针
        // len - 要发送的数据长度
        // flag - 发送标志,0表示默认阻塞模式
        // 返回实际发送的字节数,-1表示错误
        ssize_t ret = send(_sockfd, buf, len, flag);
        
        // 检查发送结果
        if (ret < 0)
        {
            // 如果errno为EAGAIN或EINTR,属于非致命错误,可以重试
            // EAGAIN: 在非阻塞模式下,发送缓冲区已满
            // EINTR: 系统调用被信号中断
            if (errno == EAGAIN || errno == EINTR)
            {
                return 0;  // 返回0表示这次没有发送数据,但不一定是错误
            }
            
            // 记录真正的发送错误信息
            ERR_LOG("SOCKET SEND FAILED!!");
            return -1;  // 发送失败,返回-1
        }
        return ret;  // 发送成功,返回实际发送的数据长度
    }

    // 非阻塞发送数据,使用MSG_DONTWAIT标志使发送操作立即返回
    ssize_t NonBlockSend(void *buf, size_t len)
    {
        // 检查数据长度是否为0,如果为0则直接返回,避免不必要的系统调用
        if (len == 0)
            return 0;
        
        // 调用Send函数,但传入MSG_DONTWAIT标志
        // MSG_DONTWAIT标志使send函数在发送缓冲区已满时立即返回,而不是阻塞等待
        return Send(buf, len, MSG_DONTWAIT);
    }

    // 关闭套接字,释放系统资源
    void Close()
    {
        // 检查套接字描述符是否为有效值(不等于-1)
        if (_sockfd != -1)
        {
            // 调用close系统函数关闭套接字
            // 这会释放套接字相关的所有系统资源
            close(_sockfd);
            
            // 将套接字描述符设置为-1,表示套接字已关闭,避免重复关闭
            _sockfd = -1;
        }
    }

    // 创建服务器端套接字(完整流程),封装了服务器套接字创建的常见步骤
    // 这是创建服务器的便捷方法,避免了手动调用多个函数
    bool CreateServer(uint16_t port, const std::string &ip = "0.0.0.0", bool block_flag = false)
    {
        // 步骤1: 创建套接字
        if (Create() == false)
            return false;
        
        // 步骤2: 如果需要,设置套接字为非阻塞模式
        // 非阻塞模式通常用于高性能服务器,允许同时处理多个连接
        //注意:默认情况是阻塞模式
        if (block_flag)
        {
            NonBlock();
        }

        // 步骤3: 设置地址和端口重用选项
        // 这允许服务器在关闭后立即重新启动,而不需要等待TIME_WAIT状态结束
        ReuseAddress();

        // 步骤4: 绑定地址信息
        // 默认绑定到"0.0.0.0",表示监听所有网络接口
        if (Bind(ip, port) == false)
            return false;
        
        // 步骤5: 开始监听连接请求
        if (Listen() == false)
        {
            return false;
        }
        
        return true;  // 所有步骤成功,返回true
    }

    // 创建客户端套接字(完整流程),封装了客户端套接字创建的常见步骤
    // 这是连接服务器的便捷方法
    bool CreateClient(uint16_t port, const std::string &ip)
    {
        // 步骤1: 创建套接字
        if (Create() == false)
            return false;
        
        // 步骤2: 连接服务器
        if (Connect(ip, port) == false)
            return false;
        
        return true;  // 所有步骤成功,返回true
    }

    // 设置套接字选项:开启地址和端口重用
    // 这对于服务器开发非常重要,允许快速重启服务器而无需等待端口释放
    void ReuseAddress()
    {
        // 调用setsockopt系统函数设置套接字选项
        // 参数说明:
        // _sockfd - 要设置选项的套接字描述符
        // SOL_SOCKET - 选项级别,表示在套接字级别设置选项
        // SO_REUSEADDR - 选项名称,允许重用本地地址
        // &val - 选项值的指针
        // sizeof(int) - 选项值的大小
        
        // 设置SO_REUSEADDR选项,允许重用本地地址
        // 这在服务器重启时特别有用,可以避免"Address already in use"错误
        int val = 1;
        setsockopt(_sockfd, SOL_SOCKET, SO_REUSEADDR, (void *)&val, sizeof(int));
        
        // 设置SO_REUSEPORT选项,允许重用本地端口
        // 注意:这个选项在一些操作系统上可能不被支持
        val = 1;
        setsockopt(_sockfd, SOL_SOCKET, SO_REUSEPORT, (void *)&val, sizeof(int));
    }

    // 设置套接字为非阻塞模式
    // 非阻塞I/O是高性能网络编程的关键技术之一
    void NonBlock()
    {
        // 调用fcntl系统函数控制文件描述符属性
        // 参数说明:
        // _sockfd - 要设置的文件描述符
        // F_GETFL - 获取文件状态标志
        // 0 - 忽略的参数
        
        // 首先获取当前文件状态标志
        int flag = fcntl(_sockfd, F_GETFL, 0);
        
        // 然后设置新的文件状态标志,添加O_NONBLOCK(非阻塞)标志
        // 使用按位或操作保留原有的其他标志
        fcntl(_sockfd, F_SETFL, flag | O_NONBLOCK);
    }
};

1.2.知识点补充

setsockopt函数

 // 设置套接字选项:开启地址和端口重用
    // 这对于服务器开发非常重要,允许快速重启服务器而无需等待端口释放
    void ReuseAddress()
    {
        // 调用setsockopt系统函数设置套接字选项
        // 参数说明:
        // _sockfd - 要设置选项的套接字描述符
        // SOL_SOCKET - 选项级别,表示在套接字级别设置选项
        // SO_REUSEADDR - 选项名称,允许重用本地地址
        // &val - 选项值的指针
        // sizeof(int) - 选项值的大小
        
        // 设置SO_REUSEADDR选项,允许重用本地地址
        // 这在服务器重启时特别有用,可以避免"Address already in use"错误
        int val = 1;
        setsockopt(_sockfd, SOL_SOCKET, SO_REUSEADDR, (void *)&val, sizeof(int));
        
        // 设置SO_REUSEPORT选项,允许重用本地端口
        // 注意:这个选项在一些操作系统上可能不被支持
        val = 1;
        setsockopt(_sockfd, SOL_SOCKET, SO_REUSEPORT, (void *)&val, sizeof(int));
    }

setsockopt函数用于设置套接字选项,可以修改套接字的属性,使其行为符合我们的需求。

函数原型:

int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen);

参数说明:

  • sockfd:要设置的套接字的文件描述符。
  • level:选项定义的层次。例如,SOL_SOCKET(通用套接字选项)、IPPROTO_IP(IP选项)、IPPROTO_TCP(TCP选项)等。
  • optname:选项名称,例如SO_REUSEADDR、SO_REUSEPORT等。
  • optval:指向存放选项值的缓冲区。这个值的大小和类型取决于选项。
  • optlen:optval缓冲区的大小。

返回值:

  • 成功返回0,失败返回-1并设置errno。

现在,我们详细讲解一下ReuseAddress函数中设置的两个选项:

  • SO_REUSEADDR:允许重用本地地址。当套接字关闭后,操作系统通常会保持一段时间的TIME_WAIT状态,以确保所有延迟的数据包都能被正确处理。在这段时间内,该端口无法被重新绑定。设置SO_REUSEADDR选项后,可以立即重用该端口,而不必等待TIME_WAIT结束。
  • SO_REUSEPORT:允许重用本地端口。多个套接字可以绑定到相同的端口上,这通常用于多进程或多线程服务器,以提高性能。注意,这个选项在有些系统上可能不可用,或者需要特定的内核版本。

在ReuseAddress函数中,我们设置这两个选项为1(启用)。

fcntl函数

// 设置套接字为非阻塞模式
    // 非阻塞I/O是高性能网络编程的关键技术之一
    void NonBlock()
    {
        // 调用fcntl系统函数控制文件描述符属性
        // 参数说明:
        // _sockfd - 要设置的文件描述符
        // F_GETFL - 获取文件状态标志
        // 0 - 忽略的参数
        
        // 首先获取当前文件状态标志
        int flag = fcntl(_sockfd, F_GETFL, 0);
        
        // 然后设置新的文件状态标志,添加O_NONBLOCK(非阻塞)标志
        // 使用按位或操作保留原有的其他标志
        fcntl(_sockfd, F_SETFL, flag | O_NONBLOCK);
    }

这里使用了一个fcntl函数,那么这个fcntl函数又是何方神圣??

我们首先来了解一下fcntl函数。fcntl是文件控制(file control)的缩写,它用来对已打开的文件描述符进行各种控制操作。在套接字编程中,我们经常用fcntl来设置套接字为非阻塞模式。

fcntl函数原型

#include <unistd.h>
#include <fcntl.h>

int fcntl(int fd, int cmd, ... /* arg */ );

参数说明:

  • fd:文件描述符。
  • cmd:控制命令,指定要执行的操作。
  • ...:可变参数,根据cmd的不同,可能需要传入额外的参数。

常用命令(cmd):

  • F_DUPFD:复制文件描述符。
  • F_GETFD:获取文件描述符标志。
  • F_SETFD:设置文件描述符标志。
  • F_GETFL:获取文件状态标志。
  • F_SETFL:设置文件状态标志。

在设置非阻塞模式时,我们使用F_GETFL和F_SETFL。

文件状态标志(file status flags):

这些标志定义了文件描述符的行为,例如:

  • O_RDONLY:只读打开
  • O_WRONLY:只写打开
  • O_RDWR:读写打开
  • O_APPEND:追加写入
  • O_NONBLOCK:非阻塞模式
  • O_SYNC:同步写入(等待物理写操作完成)
  • 等等。

设置非阻塞模式的步骤:

  1. 使用F_GETFL获取当前文件状态标志。
  2. 使用按位或(|)将O_NONBLOCK标志加上。
  3. 使用F_SETFL设置新的文件状态标志。

例子:将标准输入设置为非阻塞模式

#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>
#include <errno.h>

int main() {
    int flags = fcntl(STDIN_FILENO, F_GETFL, 0);
    if (flags == -1) {
        perror("fcntl");
        return 1;
    }
    
    // 添加非阻塞标志
    if (fcntl(STDIN_FILENO, F_SETFL, flags | O_NONBLOCK) == -1) {
        perror("fcntl");
        return 1;
    }
    
    // 现在标准输入是非阻塞的,尝试读取
    char buf[10];
    ssize_t n = read(STDIN_FILENO, buf, sizeof(buf));
    if (n == -1) {
        if (errno == EAGAIN) {
            printf("No data available (non-blocking)\n");
        } else {
            perror("read");
        }
    } else {
        printf("Read %zd bytes\n", n);
    }
    
    return 0;
}

一运行就结束了

1.3.编译测试

测试1——基本功能测试

tcp_srv.cc

#include"../server/socket.hpp"

int main() {
    // 创建一个监听套接字对象,用于接受客户端连接
    Socket lst_sock;
    
    // 创建服务器,监听8500端口,默认绑定到0.0.0.0(所有网络接口)
    // 如果创建失败,程序会返回错误(假设CreateServer内部会处理)
    lst_sock.CreateServer(8500);
    
    // 无限循环,持续接受客户端连接
    while (1) {
        // 接受一个新的客户端连接
        // 返回新的文件描述符,如果小于0表示接受失败
        int newfd = lst_sock.Accept();
        
        // 如果接受连接失败,跳过本次循环继续等待
        if (newfd < 0) {
            continue;  // 继续等待下一个连接
        }
        
        // 用新连接的文件描述符创建一个客户端套接字对象
        Socket cli_sock(newfd);
        
        // 定义缓冲区,用于接收客户端数据,初始化为0
        char buf[1024] = {0};
        
        // 从客户端接收数据,最多接收1023字节(留一个位置给字符串结束符)
        int ret = cli_sock.Recv(buf, 1023);
        
        // 如果接收失败(ret<0),关闭客户端连接并继续等待下一个连接
        if (ret < 0) {
            cli_sock.Close();
            continue;
        }

        std::cout<<"服务器收到了:"<<buf<<std::endl;
        
        // 将接收到的数据原样发送回客户端(回显)
        cli_sock.Send(buf, ret);
        
        // 关闭客户端连接(一次连接只处理一次收发)
        cli_sock.Close();
    }
    
    // 退出循环后,关闭监听套接字
    lst_sock.Close();
    
    // 程序结束
    return 0;
}

tcp_cli.cc

#include"../server/socket.hpp"

int main() {
    // 创建一个客户端套接字对象,用于连接服务器
    Socket cli_sock;
    
    // 创建客户端连接,连接到本地主机(127.0.0.1)的8500端口
    // 如果连接失败,程序会返回错误(假设CreateClient内部会处理)
    cli_sock.CreateClient(8500, "127.0.0.1");
    
    // 准备要发送给服务器的消息内容
    std::string str = "hello bitejiuyeke!";
    
    // 将消息发送给服务器
    // str.c_str() 获取C风格字符串指针,str.size() 获取字符串长度
    cli_sock.Send(str.c_str(), str.size());
    
    // 定义缓冲区,用于接收服务器返回的数据,初始化为0
    char buf[1024] = {0};
    
    // 从服务器接收响应数据,最多接收1023字节(留一个位置给字符串结束符)
    cli_sock.Recv(buf, 1023);
    
    // 打印接收到的服务器响应
    // %s 表示以字符串格式输出
    DBG_LOG("%s", buf);

    
    // 程序结束,返回0表示成功
    return 0;
    
    // 注意:cli_sock对象在main函数结束时会被自动销毁
    // Socket类的析构函数会自动调用Close()方法关闭套接字
    // 所以这里不需要显式调用cli_sock.Close()
}

完全没有什么问题

测试2——完全测试

#include "socket.hpp"
#include <iostream>
#include <thread>
#include <vector>
#include <string>
#include <cstring>

// 测试Socket类的基本功能
void test_basic_socket() {
    std::cout << "=== 测试1:基本Socket功能 ===" << std::endl;
    
    // 测试默认构造函数
    Socket s1;
    std::cout << "1. 默认构造函数测试: _sockfd = " << s1.Fd() 
              << " (应为-1)" << std::endl;
    
    // 测试创建套接字
    Socket s2;
    if (s2.Create()) {
        std::cout << "2. Create()测试: 成功, _sockfd = " << s2.Fd() << std::endl;
    } else {
        std::cout << "2. Create()测试: 失败" << std::endl;
    }
    
    // 测试关闭套接字
    s2.Close();
    std::cout << "3. Close()测试: 关闭后 _sockfd = " << s2.Fd() 
              << " (应为-1)" << std::endl;
    
    std::cout << std::endl;
}

// 测试服务器功能
void test_server() {
    std::cout << "=== 测试2:服务器功能 ===" << std::endl;
    
    Socket server;
    
    // 创建服务器(阻塞模式)
    if (server.CreateServer(8080, "127.0.0.1", false)) {
        std::cout << "1. 服务器创建成功,监听 127.0.0.1:8080" << std::endl;
        std::cout << "   服务器套接字描述符: " << server.Fd() << std::endl;
        
        // 启动一个线程来测试客户端连接
        std::thread client_thread([]() {
            std::this_thread::sleep_for(std::chrono::seconds(1));
            
            Socket client;
            if (client.CreateClient(8080, "127.0.0.1")) {
                std::cout << "   客户端连接成功" << std::endl;
                
                // 发送测试数据
                std::string message = "Hello Server from Client!";
                ssize_t sent = client.Send(message.c_str(), message.length());
                std::cout << "   客户端发送了 " << sent << " 字节: " << message << std::endl;
                
                // 接收响应
                char buffer[1024];
                ssize_t received = client.Recv(buffer, sizeof(buffer));
                if (received > 0) {
                    buffer[received] = '\0';
                    std::cout << "   客户端收到响应: " << buffer << std::endl;
                }
                
                client.Close();
            } else {
                std::cout << "   客户端连接失败" << std::endl;
            }
        });
        
        // 服务器接受连接
        std::cout << "2. 服务器等待客户端连接..." << std::endl;
        int client_fd = server.Accept();
        if (client_fd > 0) {
            std::cout << "   接受了客户端连接,客户端描述符: " << client_fd << std::endl;
            
            // 创建Socket对象处理客户端
            Socket client_socket(client_fd);
            
            // 接收客户端数据
            char buffer[1024];
            ssize_t received = client_socket.Recv(buffer, sizeof(buffer));
            if (received > 0) {
                buffer[received] = '\0';
                std::cout << "3. 服务器收到消息: " << buffer << std::endl;
                
                // 发送响应
                std::string response = "Hello Client from Server!";
                ssize_t sent = client_socket.Send(response.c_str(), response.length());
                std::cout << "4. 服务器发送了 " << sent << " 字节响应" << std::endl;
            }
            
            // 客户端Socket会在析构时自动关闭
        }
        
        client_thread.join();
        server.Close();
        std::cout << "5. 服务器已关闭" << std::endl;
    } else {
        std::cout << "服务器创建失败" << std::endl;
    }
    
    std::cout << std::endl;
}

// 测试非阻塞模式
void test_nonblocking() {
    std::cout << "=== 测试3:非阻塞模式 ===" << std::endl;
    
    Socket server;
    
    // 创建非阻塞服务器
    if (server.CreateServer(8081, "127.0.0.1", true)) {
        std::cout << "1. 非阻塞服务器创建成功,监听 127.0.0.1:8081" << std::endl;
        
        // 测试非阻塞accept
        std::cout << "2. 测试非阻塞accept..." << std::endl;
        int client_fd = server.Accept();
        if (client_fd < 0) {
            std::cout << "   非阻塞accept返回 -1(正常,因为没有连接)" << std::endl;
        } else if (client_fd == 0) {
            std::cout << "   非阻塞accept返回 0(没有新连接)" << std::endl;
        } else {
            std::cout << "   接受了连接,客户端描述符: " << client_fd << std::endl;
        }
        
        // 测试非阻塞send/recv
        std::cout << "3. 创建客户端连接..." << std::endl;
        
        std::thread client_thread([]() {
            std::this_thread::sleep_for(std::chrono::seconds(1));
            
            Socket client;
            if (client.CreateClient(8081, "127.0.0.1")) {
                std::cout << "   客户端连接成功" << std::endl;
                
                // 非阻塞发送
                std::string message = "Non-blocking test";
                ssize_t sent = client.NonBlockSend((void*)message.c_str(), message.length());
                std::cout << "   客户端非阻塞发送了 " << sent << " 字节" << std::endl;
                
                std::this_thread::sleep_for(std::chrono::seconds(1));
                client.Close();
            }
        });
        
        // 等待客户端连接
        std::this_thread::sleep_for(std::chrono::seconds(2));
        
        int client_fd2 = server.Accept();
        if (client_fd2 > 0) {
            Socket client_socket(client_fd2);
            
            // 非阻塞接收
            char buffer[1024];
            ssize_t received = client_socket.NonBlockRecv(buffer, sizeof(buffer));
            if (received > 0) {
                buffer[received] = '\0';
                std::cout << "4. 服务器非阻塞接收到数据: " << buffer << std::endl;
            } else if (received == 0) {
                std::cout << "4. 服务器非阻塞接收返回0(没有数据或缓冲区满)" << std::endl;
            }
            
            // 测试再次非阻塞接收(应该没有数据)
            received = client_socket.NonBlockRecv(buffer, sizeof(buffer));
            std::cout << "5. 再次非阻塞接收: 返回 " << received << " 字节" << std::endl;
        }
        
        client_thread.join();
        server.Close();
    } else {
        std::cout << "非阻塞服务器创建失败" << std::endl;
    }
    
    std::cout << std::endl;
}

// 测试地址重用功能
void test_address_reuse() {
    std::cout << "=== 测试4:地址重用功能 ===" << std::endl;
    
    // 创建第一个服务器
    Socket server1;
    if (server1.CreateServer(8082, "127.0.0.1", false)) {
        std::cout << "1. 创建第一个服务器成功" << std::endl;
    }
    
    // 立即关闭
    server1.Close();
    std::cout << "2. 关闭第一个服务器" << std::endl;
    
    // 立即创建第二个服务器(使用相同的地址和端口)
    std::this_thread::sleep_for(std::chrono::milliseconds(100));
    
    Socket server2;
    if (server2.CreateServer(8082, "127.0.0.1", false)) {
        std::cout << "3. 成功重新创建第二个服务器(SO_REUSEADDR生效)" << std::endl;
        std::cout << "   如果没有SO_REUSEADDR,这里会失败并显示'Address already in use'" << std::endl;
        
        // 测试连接
        Socket client;
        if (client.CreateClient(8082, "127.0.0.1")) {
            std::cout << "4. 客户端成功连接到第二个服务器" << std::endl;
            
            std::string message = "Reuse test";
            ssize_t sent = client.Send(message.c_str(), message.length());
            std::cout << "   发送了 " << sent << " 字节" << std::endl;
            
            client.Close();
        }
        
        server2.Close();
    } else {
        std::cout << "3. 重新创建服务器失败(可能是没有启用SO_REUSEADDR)" << std::endl;
    }
    
    std::cout << std::endl;
}

// 主函数
int main() {
    std::cout << "开始测试Socket类..." << std::endl;
    std::cout << "============================" << std::endl;
    
    try {
        // 运行所有测试
        test_basic_socket();
        test_server();
        test_nonblocking();
        test_address_reuse();
        
        std::cout << "============================" << std::endl;
        std::cout << "所有测试完成!" << std::endl;
        
    } catch (const std::exception& e) {
        std::cerr << "测试过程中出现异常: " << e.what() << std::endl;
        return 1;
    }
    
    return 0;
}

一点问题都没有!!

二.Channel模块

2.1.功能回顾

Channel模块是Reactor模型中的关键组件,它专注于对单个文件描述符(主要是套接字)的I/O事件进行精细化管理。

其核心职责是将描述符的I/O事件监控、事件状态管理以及事件触发后的回调处理进行统一封装,实现了事件监控与事件处理逻辑的完全解耦。

为什么需要Channel模块?

事件状态查询,指的是能够实时、准确地获知一个网络连接当前正在被系统监控哪些类型的I/O活动

这是一个至关重要的管理功能,让我们从实际场景来理解它的必要性:

为什么需要知道当前正在监控什么事件?

场景一:避免重复操作,提升性能

假设你的服务器需要向客户端发送一大块数据。流程通常是:

  1. 先将数据放入发送缓冲区。

  2. 然后要求系统“在连接可以写入时通知我”(即监听可写事件)。

  3. 当可写事件触发,系统通知你,你才开始发送数据。

现在,如果在第一块数据还没发完时,上层业务逻辑又产生了一块新数据要发送。此时,一个朴素的做法是:“再次请求监听可写事件”。

事件状态查询机制允许你在此之前先问一个问题:“这个连接当前已经在监听可写事件了吗?” 如果答案是“是”,那么第二次请求就是完全多余的,可以立即跳过。这就避免了无意义的、消耗资源的重复指令。

场景二:确保状态一致性,防止逻辑错误

考虑一个连接的生命周期管理。当连接关闭时,我们需要确保系统不再监控它的任何事件(既不关心它是否可读,也不关心它是否可写)。

如果没有状态查询,我们可能盲目地发送指令:“停止监听所有事件”。但如果这个连接本来就已经不在监控列表里了呢?这个指令就变得多余,甚至可能引发错误。

有了状态查询,我们可以先检查:“这个连接当前还在被监控吗?具体监控了什么事件?” 然后,基于这个准确的信息,再执行精确的清理操作,比如“仅停止当前正在被监控的可读事件”。

这个“查询”具体查的是什么?

它查的 不是 底层操作系统此刻检测到的物理信号(比如网络卡上是否有电信号),而是 应用层自己之前下达的“监控指令”的记录

你可以把它理解为:Channel模块内部维护的一张“监控任务清单”。每次你通过Channel模块要求“请帮我注意这个连接什么时候可读”,这个要求就会被记录在这张清单上。每次你要求“不用再注意它是否可写了”,这个条目就会被划掉。

“事件状态查询”就是随时查看这份清单的能力。它能立刻告诉你:

  • “清单上目前有一条任务:监控可读事件。”

  • “清单上目前有两条任务:同时监控可读和可写事件。”

  • “清单是空的:当前不要求监控任何事件。”

核心功能设计

1. 事件监控状态管理

Channel模块维护描述符的完整事件监控状态,提供了丰富的状态查询与设置接口:

  • 事件状态查询:这个套接字当前正在被系统监控哪些类型的I/O活动。(当前套接字是被监听了读事件还是写事件)

  • 事件状态设置:精确控制对特定事件的监控状态,包括:

    • 启用可读事件监控:当期望从描述符读取数据时启用

    • 启用可写事件监控:当需要向描述符写入数据且当前不能立即写入时启用

    • 禁用可读事件监控:当暂时不需要读取数据时禁用,减少不必要的唤醒

    • 禁用可写事件监控:当数据已全部发送完成时禁用,避免CPU忙循环

    • 禁用所有事件监控:在连接关闭或暂时不活跃时使用

2. 事件触发处理机制

Channel模块的核心优势在于其事件处理机制的设计,当Poller检测到描述符上的事件就绪时,Channel将根据事件类型分派到对应的处理函数:

  • 回调函数注册:为不同事件类型预置独立的回调处理函数,包括:

    • 读事件回调:处理数据到达、连接建立等场景

    • 写事件回调:处理数据发送、缓冲区清空等场景

    • 错误事件回调:处理连接错误、协议错误等异常情况

    • 关闭事件回调:处理连接关闭、资源清理等任务

  • 事件分发与处理:当事件就绪时,Channel根据事件类型精确调用相应的回调函数,实现了"事件类型-处理函数"的一对一映射

2.2.代码实现

2.2.1.分模块讲解

回调函数模块

首先这里最重要的就是回调函数的管理

  • 读事件回调:处理数据到达、连接建立等场景

  • 写事件回调:处理数据发送、缓冲区清空等场景

  • 错误事件回调:处理连接错误、协议错误等异常情况

  • 关闭事件回调:处理连接关闭、资源清理等任务

  • 任意事件回调:触发了任意事件都会调用

首先这个类里面就专门搞了5个回调函数成员变量,来存储上层模块传递进来的回调函数

 // 定义事件回调函数类型
    using EventCallback = std::function<void()>;

    // 各种事件的回调函数
    EventCallback _read_callback;  // 可读事件被触发的回调函数
    EventCallback _write_callback; // 可写事件被触发的回调函数
    EventCallback _error_callback; // 错误事件被触发的回调函数
    EventCallback _close_callback; // 连接断开事件被触发的回调函数
    EventCallback _event_callback; // 任意事件被触发的通用回调函数

那么很奇怪,这个构造函数里面没有初始化这些回调函数,那是在哪里来实现注册这些回调函数的呢?其实这个也很简单,就是通过下面这4个成员函数来对回调函数进行设置

// 设置各种事件的回调函数
    void SetReadCallback(const EventCallback &cb) { _read_callback = cb; }
    void SetWriteCallback(const EventCallback &cb) { _write_callback = cb; }
    void SetErrorCallback(const EventCallback &cb) { _error_callback = cb; }
    void SetCloseCallback(const EventCallback &cb) { _close_callback = cb; }
    void SetEventCallback(const EventCallback &cb) { _event_callback = cb; }

那么很好,上层模块就通过这5个函数进行回调的设置。

那么很好,回调函数设置了,那么哪里会调用这些回调函数呢?

其实也很明显,就是下面这个函数,这个函数会根据事件的不同类型,来调用不同的消息处理回调函数

// 事件处理函数:当文件描述符上发生事件时被调用
    // 根据实际触发的事件类型调用相应的回调函数
    void HandleEvent()
    {
        // 处理可读事件(包括普通数据可读、对端关闭连接、带外数据等情况)
        if ((_revents & EPOLLIN) || (_revents & EPOLLRDHUP) || (_revents & EPOLLPRI))
        {
            // 调用通用事件回调(无论发生什么事件都会调用)
            if (_event_callback)
            {
                _event_callback();
            }
            // 调用读事件回调函数
            if (_read_callback)
            {
                _read_callback();
            }
        }

        /*有可能会释放连接的操作事件,一次只处理一个*/

        // 处理可写事件
        if (_revents & EPOLLOUT)
        {
            // 调用通用事件回调(无论发生什么事件都会调用)
            if (_event_callback)
            {
                _event_callback();
            }
            //调用写回调
            if (_write_callback)
            {
                _write_callback();
            }
        }
        // 处理错误事件(优先级较高,因为错误后通常会关闭连接)
        else if (_revents & EPOLLERR)
        {
            // 调用通用事件回调(无论发生什么事件都会调用)
            if (_event_callback)
            {
                _event_callback();
            }
            // 一旦出错,就会释放连接,因此要放到前边调用任意回调
            if (_error_callback)
            {
                _error_callback();
            }
        }
        // 处理挂起事件(对端关闭连接)
        else if (_revents & EPOLLHUP)
        {
            // 调用通用事件回调(无论发生什么事件都会调用)
            if (_event_callback)
            {
                _event_callback();
            }
            //调用关闭回调
            if (_close_callback)
            {
                _close_callback();
            }
        }
    }

注意我们这里其实是使用了epoll机制来进行事件的通知的

但是epoll不知道会返回什么样的事件,为了做一个大一统的接口,我们只能在这个函数里面智能的根据epoll返回的就绪事件的类型来调用所对应的不同的回调函数。

epoll事件相关

其实这个模块是最没有什么东西去讲的,就是纯粹的epoll那几个事件相关的。

大家只需要知道epoll相关事件的类型即可,至于具体的细节,大家去看代码

2.2.2.代码总览

#pragma once
#include "log.hpp"

#include <iostream>
#include <string>
#include <cassert>
#include <cstring>
#include <functional>
#include <memory>
#include <typeinfo>
#include <fcntl.h>
#include <signal.h>
#include <unistd.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/epoll.h>
#include <sys/eventfd.h>

// 前向声明,避免循环依赖
class Poller;
class EventLoop;

// Channel类:封装文件描述符的事件监控和回调处理
// 每个Channel对象负责一个文件描述符的事件监控和事件分发
class Channel
{
private:
    int _fd;           // 监控的文件描述符
    EventLoop *_loop;  // 所属的EventLoop,用于事件循环处理
    uint32_t _events;  // 当前需要监控的事件集合(EPOLLIN/EPOLLOUT等)
    uint32_t _revents; // 当前实际触发的事件集合(由epoll_wait设置)

    // 定义事件回调函数类型
    using EventCallback = std::function<void()>;

    // 各种事件的回调函数
    EventCallback _read_callback;  // 可读事件被触发的回调函数
    EventCallback _write_callback; // 可写事件被触发的回调函数
    EventCallback _error_callback; // 错误事件被触发的回调函数
    EventCallback _close_callback; // 连接断开事件被触发的回调函数
    EventCallback _event_callback; // 任意事件被触发的通用回调函数

public:
    // 构造函数:初始化Channel对象
    // 参数:loop-所属的事件循环,fd-要监控的文件描述符
    Channel(EventLoop *loop, int fd) : _fd(fd), _events(0), _revents(0), _loop(loop) {}

    // 获取文件描述符
    int Fd() { return _fd; }

    // 获取当前监控的事件集合
    uint32_t Events() { return _events; }

    // 设置实际触发的事件集合(由Poller调用)
    void SetREvents(uint32_t events) { _revents = events; }

    // 设置各种事件的回调函数
    void SetReadCallback(const EventCallback &cb) { _read_callback = cb; }
    void SetWriteCallback(const EventCallback &cb) { _write_callback = cb; }
    void SetErrorCallback(const EventCallback &cb) { _error_callback = cb; }
    void SetCloseCallback(const EventCallback &cb) { _close_callback = cb; }
    void SetEventCallback(const EventCallback &cb) { _event_callback = cb; }

    // 判断当前是否监控了可读事件
    bool ReadAble() { return (_events & EPOLLIN); }

    // 判断当前是否监控了可写事件
    bool WriteAble() { return (_events & EPOLLOUT); }

    // 启动读事件监控:将EPOLLIN事件添加到监控事件集合中
    void EnableRead()
    {
        _events |= EPOLLIN;
        Update(); // 这个不在这个类里面实现,核心功能就是将当前事件监控状态添加到EventLoop的事件监控中
    }

    // 启动写事件监控:将EPOLLOUT事件添加到监控事件集合中
    void EnableWrite()
    {
        _events |= EPOLLOUT;
        Update(); // 这个不在这个类里面实现,核心功能就是将当前事件监控状态添加到EventLoop的事件监控中
    }

    // 关闭读事件监控:从监控事件集合中移除EPOLLIN事件
    void DisableRead()
    {
        _events &= ~EPOLLIN;
        Update(); // 这个不在这个类里面实现,核心功能就是将当前事件监控状态添加到EventLoop的事件监控中
    }

    // 关闭写事件监控:从监控事件集合中移除EPOLLOUT事件
    void DisableWrite()
    {
        _events &= ~EPOLLOUT;
        Update(); // 这个不在这个类里面实现,核心功能就是将当前事件监控状态添加到EventLoop的事件监控中
    }

    // 关闭所有事件监控:清空监控事件集合
    void DisableAll()
    {
        _events = 0;
        Update(); // 这个不在这个类里面实现,核心功能就是将当前事件监控状态添加到EventLoop的事件监控中
    }

    // 移除监控:从EventLoop中移除该Channel的监控(不在这里进行实现)
    void Remove();

    // 更新监控:将当前的事件监控设置同步到EventLoop中(不在这里进行实现)
    void Update();

    // 事件处理函数:当文件描述符上发生事件时被调用
    // 根据实际触发的事件类型调用相应的回调函数
    void HandleEvent()
    {
        // 处理可读事件(包括普通数据可读、对端关闭连接、带外数据等情况)
        if ((_revents & EPOLLIN) || (_revents & EPOLLRDHUP) || (_revents & EPOLLPRI))
        {
            // 调用读事件回调函数
            if (_read_callback)
            {
                _read_callback();
            }
        }

        /*有可能会释放连接的操作事件,一次只处理一个*/

        // 处理可写事件
        if (_revents & EPOLLOUT)
        {
            if (_write_callback)
            {
                _write_callback();
            }
        }
        // 处理错误事件(优先级较高,因为错误后通常会关闭连接)
        else if (_revents & EPOLLERR)
        {
            // 一旦出错,就会释放连接,因此要放到前边调用任意回调
            if (_error_callback)
            {
                _error_callback();
            }
        }
        // 处理挂起事件(对端关闭连接)
        else if (_revents & EPOLLHUP)
        {
            if (_close_callback)
            {
                _close_callback();
            }
        }

        // 最后调用通用事件回调(无论发生什么事件都会调用)
        if (_event_callback)
        {
            _event_callback();
        }
    }
};

2.2.3.代码实现小细节

这里面其实还是存留了两个未实现的函数

// 移除监控:从EventLoop中移除该Channel的监控(不在这里进行实现)
void Remove();

// 更新监控:将当前的事件监控设置同步到EventLoop中(不在这里进行实现)
void Update();

这两个函数我们目前这个模块是实现不了的,仔细观察一下就会发现,这两个函数都依赖于一个EventLoop对象,也就是我们这里的成员变量EventLoop *_loop;但是我们这个EventLoop类的代码还没有写,所以说还是比较难去完成这两个成员函数的

注意点一

首先我们需要知道,我们后边是使用epoll进行事件监控的

常见epoll事件类型:

  • EPOLLIN:可读事件。表示对应的文件描述符可以读取数据。对于监听套接字,表示有新的连接到达;对于已连接的套接字,表示接收缓冲区中有数据可读,或者对端关闭连接(此时读取数据将返回0)。

  • EPOLLOUT:可写事件。表示对应的文件描述符可以写入数据。当发送缓冲区有可用空间时,该事件会被触发。此外,在非阻塞连接中,连接成功建立后也会触发该事件。

  • EPOLLPRI:紧急数据可读事件。表示有带外数据(out-of-band data)到达,需要优先读取。例如,TCP套接字上的紧急数据(使用MSG_OOB标志发送的数据)。

  • EPOLLERR:错误事件。表示对应的文件描述符发生错误。该事件会被自动监控,即使没有明确设置,当错误发生时也会触发。

  • EPOLLHUP:挂起事件。表示对应的文件描述符被挂起,通常意味着对端已经关闭连接。注意,在epoll中,读和写事件都会监控到挂起事件,触发后可能还需要读取缓冲区中剩余的数据。

  • EPOLLRDHUP:对端关闭连接事件(或连接半关闭)。表示对端已经关闭了连接,或者关闭了写端(例如调用了shutdown(SHUT_WR))。该事件是边缘触发的,通常在处理连接关闭时使用,可以更精确地知道对端已经关闭。

其实这几种类型本质就是一个整数,我们就采用uint_32类型的成员来进行保存当前需要监控的事件

enum EPOLL_EVENTS
  {
    EPOLLIN = 0x001,
#define EPOLLIN EPOLLIN
    EPOLLPRI = 0x002,
#define EPOLLPRI EPOLLPRI
    EPOLLOUT = 0x004,
#define EPOLLOUT EPOLLOUT
    EPOLLRDNORM = 0x040,
#define EPOLLRDNORM EPOLLRDNORM
    EPOLLRDBAND = 0x080,
#define EPOLLRDBAND EPOLLRDBAND
    EPOLLWRNORM = 0x100,
#define EPOLLWRNORM EPOLLWRNORM
    EPOLLWRBAND = 0x200,
#define EPOLLWRBAND EPOLLWRBAND
    EPOLLMSG = 0x400,
#define EPOLLMSG EPOLLMSG
    EPOLLERR = 0x008,
#define EPOLLERR EPOLLERR
    EPOLLHUP = 0x010,
#define EPOLLHUP EPOLLHUP
    EPOLLRDHUP = 0x2000,
#define EPOLLRDHUP EPOLLRDHUP
    EPOLLEXCLUSIVE = 1u << 28,
#define EPOLLEXCLUSIVE EPOLLEXCLUSIVE
    EPOLLWAKEUP = 1u << 29,
#define EPOLLWAKEUP EPOLLWAKEUP
    EPOLLONESHOT = 1u << 30,
#define EPOLLONESHOT EPOLLONESHOT
    EPOLLET = 1u << 31
#define EPOLLET EPOLLET
  };

这些事件标志在设计时就被定义为位标志(bit flags),每个标志的值都是2的幂(即只有一个位被设置为1),因此可以通过按位或 | 运算来组合多个事件。

注意点二

 // 判断当前是否监控了可读事件
    bool ReadAble() { return (_events & EPOLLIN); } 

为什么我们这里就能直接判断是否监控的可读事件?

因为 _events 变量是一个位掩码(bitmask),它使用二进制位来记录当前监控的事件类型。每个事件类型都被定义为一个只有一个二进制位为1的整数值,它们之间互不重叠。

工作原理:

事件标志的二进制表示:

EPOLLIN   = 0x001 = 二进制 0000 0000 0000 0001
EPOLLOUT  = 0x004 = 二进制 0000 0000 0000 0100
EPOLLPRI  = 0x002 = 二进制 0000 0000 0000 0010

按位与运算(&)的作用:

按位与运算会逐位比较两个数字的二进制表示:

  • 只有当两个数字在同一位置都是1时,结果在该位置才是1
  • 否则结果为0

监控状态的判断:

假设 _events 当前监控了可读和可写事件:

_events = EPOLLIN | EPOLLOUT 
        = 0x001 | 0x004 
        = 0x005

二进制:0000 0000 0000 0101

进行按位与运算:
_events & EPOLLIN = 0x005 & 0x001

二进制:0000 0000 0000 0101
       0000 0000 0000 0001 &
       -------------------
       0000 0000 0000 0001 = 0x001 (非零,返回true)

实际判断过程:

  • 如果监控了可读事件:当 _events 包含了 EPOLLIN 标志时,在 EPOLLIN 对应的那个二进制位上一定是1。执行 _events & EPOLLIN 后,会得到一个非零值,在C++中非零值被转换为 true。
  • 如果没有监控可读事件:当 _events 不包含 EPOLLIN 标志时,在 EPOLLIN 对应的二进制位上是0。执行 _events & EPOLLIN 后,结果为0,被转换为 false。

三.Poller模块

3.1.功能回顾

Poller模块是对epoll进⾏封装的⼀个模块,主要实现epoll的IO事件添加,修改,移除,获取活跃连接功能。

Poller模块的定位是跨平台I/O多路复用的抽象层事件分发中心。它的核心设计目标包括:

  1. 简化复杂性:封装epoll等系统调用的底层细节,为上层提供清晰、一致的编程接口。

  2. 提升效率:通过单次系统调用监控成千上万个文件描述符,实现真正的高并发处理能力。

  3. 统一管理:对所有需要I/O事件监控的描述符进行集中式、标准化的生命周期管理。

核心功能接口

Poller模块提供三个核心操作接口,完整覆盖了描述符事件监控的生命周期:

  1. 添加事件监控

    • 功能:将一个文件描述符及其关注的事件类型(可读、可写、错误等)添加到监控集合中。

    • 内部实现:通常通过epoll_ctl系统调用的EPOLL_CTL_ADD操作实现。这一操作建立了描述符与Poller之间的监控关系。

    • 上层协作:此操作由Channel模块发起。当Channel需要开始监控某个事件时,它会调用Poller的相应接口来完成实际的系统级注册。

  2. 修改事件监控

    • 功能:调整已监控描述符所关注的事件类型。

    • 应用场景:这是实现动态事件管理的核心。例如,当一个连接从"只监控可读"变为"需要同时监控可写"时,通过此接口更新监控状态,而无需先移除再重新添加。

    • 性能优化:直接修改比先移除后添加的效率更高,避免了描述符在内核状态中的短暂缺失。

  3. 移除事件监控

    • 功能:将指定描述符从监控集合中完全移除,Poller将不再关注其任何事件。

    • 重要性:这是防止内存泄漏和资源浪费的关键操作。当连接关闭或暂时不需要监控时,必须及时移除监控。

    • 清理保证:移除操作确保了描述符在销毁前,所有相关的事件监控状态都被彻底清理。

与上层模块的协作关系

Poller模块与Channel模块形成紧密的协作关系,共同构建了完整的事件监控体系:

  1. 注册代理:Channel作为事件监控的"申请者",Poller作为事件监控的"执行者"。Channel封装了单个描述符的事件状态和回调逻辑,而Poller负责将这些离散的监控请求汇总并高效地提交给操作系统。

  2. 事件反馈循环:当Poller检测到事件就绪,它会将事件信息"回填"到对应的Channel中,然后由EventLoop统一调度这些就绪Channel的处理回调。这一机制确保了事件从检测到处理的完整闭环。

3.2.代码实现

#define MAX_EPOLLEVENTS 1024  // 定义最大epoll事件数量

class Poller {
private:
    int _epfd;  // epoll文件描述符
    struct epoll_event _evs[MAX_EPOLLEVENTS];  // epoll事件数组,用于存储就绪事件
    std::unordered_map<int, Channel *> _channels;  // 文件描述符到Channel的映射表
    
private:
    // 对epoll的直接操作
    // 参数:channel - 要操作的通道,op - epoll操作类型(ADD/MOD/DEL)
    void Update(Channel *channel, int op);
    
    // 判断一个Channel是否已经添加了事件监控
    // 参数:channel - 要检查的通道
    // 返回值:true-已存在,false-不存在
    bool HasChannel(Channel *channel);
    
public:
    // 构造函数,创建epoll实例
    Poller();
    
    // 添加或修改监控事件
    // 参数:channel - 要添加或修改的通道
    void UpdateEvent(Channel *channel);
    
    // 移除监控
    // 参数:channel - 要移除的通道
    void RemoveEvent(Channel *channel);
    
    // 开始监控,返回活跃连接
    // 参数:active - 存储活跃通道的向量
    void Poll(std::vector<Channel*> *active);
};

注意:epoll的用法可以去:【网络】高级IO——epoll版本TCP服务器初阶_tcp epoll-CSDN博客

#pragma once
#include"channel.hpp"
#include <unordered_map>

#define MAX_EPOLLEVENTS 1024  // 定义最大epoll事件数量

class Poller {
private:
    int _epfd;  // epoll文件描述符
    struct epoll_event _evs[MAX_EPOLLEVENTS];  // 存储epoll就绪事件的数组,最大容量为MAX_EPOLLEVENTS
    std::unordered_map<int, Channel *> _channels;  // 文件描述符到Channel的映射表,管理着添加了事件监控的文件描述符
    
private:
    // 对epoll的直接操作,包括添加、修改、移除
    // 参数:channel - 要操作的通道,op - epoll操作类型(ADD/MOD/DEL)
    void Update(Channel *channel, int op) {
        // int epoll_ctl(int epfd, int op,  int fd,  struct epoll_event *ev);
        int fd = channel->Fd();  // 获取文件描述符
        struct epoll_event ev;   // 创建epoll事件结构
        ev.data.fd = fd;         // 设置事件关联的文件描述符
        ev.events = channel->Events();  // 设置要监听的事件类型
        int ret = epoll_ctl(_epfd, op, fd, &ev);  // 执行epoll控制操作
        if (ret < 0) {
            ERR_LOG("EPOLLCTL FAILED!");  // 操作失败记录错误日志
        }
        return;
    }
    
    // 判断一个Channel是否已经添加了事件监控
    //判断要更新事件的描述符是否已经存在于监控表中
    // 参数:channel - 要检查的通道
    // 返回值:true-已存在,false-不存在
    bool HasChannel(Channel *channel) {
        auto it = _channels.find(channel->Fd());  // 在映射表中查找
        if (it == _channels.end()) {
            return false;  // 未找到
        }
        return true;  // 找到
    }
    
public:
    // 构造函数,创建epoll实例
    Poller() {
        _epfd = epoll_create(MAX_EPOLLEVENTS);  // 创建epoll实例
        if (_epfd < 0) {
            ERR_LOG("EPOLL CREATE FAILED!!");  // 创建失败记录错误日志
            abort();  // 退出程序
        }
    }
    
    // 添加或修改监控事件
    // 参数:channel - 要添加或修改的通道
    void UpdateEvent(Channel *channel) {
        bool ret = HasChannel(channel);  // 判断一个Channel是否已经添加了事件监控
        if (ret == false) {
            // 不存在事件监控则添加
            _channels.insert(std::make_pair(channel->Fd(), channel));  // 插入到映射表
            return Update(channel, EPOLL_CTL_ADD);  // 执行添加操作
        }
        return Update(channel, EPOLL_CTL_MOD);  // 已存在则执行修改操作
    }
    
    // 移除监控
    // 参数:channel - 要移除的通道
    void RemoveEvent(Channel *channel) {
        auto it = _channels.find(channel->Fd());  // 查找通道
        if (it != _channels.end()) {
            _channels.erase(it);  // 从映射表中删除
        }
        Update(channel, EPOLL_CTL_DEL);  // 执行删除操作
    }
    
    // 开始监控,返回活跃连接
    // 参数:active - 存储活跃通道的向量
    void Poll(std::vector<Channel*> *active) {
        // int epoll_wait(int epfd, struct epoll_event *evs, int maxevents, int timeout)
        // 等待事件发生,-1表示无限等待
        int nfds = epoll_wait(_epfd, _evs, MAX_EPOLLEVENTS, -1);//就绪好的事件会被内核自动填充到_evs数组
        if (nfds < 0) {
            if (errno == EINTR) {
                return ;  // 被信号中断,直接返回
            }
            ERR_LOG("EPOLL WAIT ERROR:%s\n", strerror(errno));  // 错误记录日志
            abort();  // 退出程序
        }
        
        // 遍历所有就绪的事件
        for (int i = 0; i < nfds; i++) {
            auto it = _channels.find(_evs[i].data.fd);  // 根据文件描述符来查找对应的Channel
            assert(it != _channels.end());  // 断言:必须找到对应的Channel
            it->second->SetREvents(_evs[i].events);  // 设置Channel的实际就绪事件
            active->push_back(it->second);  // 将活跃Channel添加到结果向量中
        }
        return;
    }
};

3.3.Poller模块和Channel模块整合

还记得我们这个Channel模块里面我们没有实现的那两个函数了吗

// 移除监控:从EventLoop中移除该Channel的监控(不在这里进行实现)
void Remove();

// 更新监控:将当前的事件监控设置同步到EventLoop中(不在这里进行实现)
void Update();

这两个函数我们目前这个模块是实现不了的,仔细观察一下就会发现,这两个函数都依赖于一个EventLoop对象,也就是我们这里的成员变量EventLoop *_loop;但是我们这个EventLoop类的代码还没有写,所以说还是比较难去完成这两个成员函数的

虽然我们这里还是没有实现EventLoop类的任何定义,但是我这里可以先告诉你,将

  • 从EventLoop中移除对该Channel的监控
  • 将当前的事件监控设置同步到EventLoop中

这两个的本质都是通过Channel模块来进行的

我这里可以先告诉大家,这两个函数的最终样子其实就是下面这样子的,成员变量EventLoop *_loop;分别去调用它们的成员函数

我们仔细看看这两个成员函数,它们居然又去调用成员变量EventLoop *_loop;的成员变量_poller的成员函数去设置

也就是说是,本质上这两个函数还是通过Channel来实现最终功能的。

在这里我为了调试方便,先暂且先将Channel模块小改一下,将Channel里面的EventLoop先暂且换成Poller,方便我们进行测试。

我们在channel.hpp里面修改下面这两部分

class Channel
{

//EventLoop *_loop;  // 所属的EventLoop,用于事件循环处理
    Poller* _poller;  //新增这个,暂且作测试用

public:
    // 构造函数:初始化Channel对象
    // 参数:loop-所属的事件循环,fd-要监控的文件描述符
    Channel(Poller* poller, int fd) : _fd(fd), _events(0), _revents(0), _poller(poller) {}

……
}

然后我们再在poller.hpp这里添加下面这两句

void Channel::Remove()
{
    _poller->RemoveEvent(this);
}
void Channel::Update()
{
    _poller->UpdateEvent(this);
}

经过上面的小改,我们现在总算是可以对这个Poller模块和Channel模块进行单元测试了

四.Channel模块和Poller模块联合测试

我们现在就很快就能写出一个服务端和客户端来进行测试

tcp_srv.cpp

#include"../server/socket.hpp"
#include"../server/poller.hpp"
#include"../server/channel.hpp"

// 处理关闭事件的回调函数
// 参数:channel - 指向要处理的Channel对象的指针
void HandleClose(Channel *channel) {
    // 输出关闭连接的日志,显示文件描述符
    std::cout << "close:" << channel->Fd() << std::endl;
    // 从事件监控器(epoll)中移除该Channel的事件监控
    channel->Remove();
    // 释放Channel对象占用的内存
    delete channel;
}

// 处理读事件的回调函数
// 当文件描述符有数据可读时被调用
void HandleRead(Channel *channel) {
    // 从Channel对象中获取文件描述符
    int fd = channel->Fd();
    // 定义接收缓冲区,初始化为0,大小1024字节
    // 注意:实际上只能接收1023个数据字节,最后一个字节用于字符串结束符'\0'
    char buf[1024] = {0};
    
    // 从套接字接收数据
    // 参数:
    //   fd - 套接字文件描述符
    //   buf - 接收缓冲区
    //   1023 - 最大接收字节数(留一个字节给结束符)
    //   0 - 接收标志,0表示默认阻塞模式(实际上这里是非阻塞模式)
    int ret = recv(fd, buf, 1023, 0);
    
    // 检查接收结果
    if (ret <= 0) {
        return HandleClose(channel);//关闭并释放连接
    }
    // 启用可写事件监控,准备将接收到的数据发送回去
    // 注意:首先别人发了数据过来,就会触发可读事件回调函数(注意我们在前面开启了读事件监控),人家都发数据来了,你必须回应人家吧,
    //当有数据要发送时,才需要启用写事件监控
    channel->EnableWrite();
    // 打印接收到的数据到控制台
    std::cout << buf << std::endl;
}

// 处理写事件的回调函数
void HandleWrite(Channel *channel) {
    int fd=channel->Fd();
    const char* data="天气还不错";
    int ret=send(fd,data,strlen(data),0);
    if(ret<0)
    {
        return HandleClose(channel);//关闭释放
    }
    channel->DisableWrite();//关闭写事件监控
}

// 处理错误事件的回调函数
void HandleError(Channel *channel) {
    return HandleClose(channel);//发生错误,直接关闭释放好吧
}
// 处理任意事件的回调函数
void HandleEvent(Channel *channel) {
    std::cout<<"有了一个事件!!"<<std::endl;
}


//要添加事件监控,肯定需要一个Poller
// 接受新连接的函数
// 参数:poller - 事件轮询器,lst_channel - 监听套接字的Channel
void Acceptor(Poller *poller, Channel *lst_channel) {
    // 从监听Channel获取监听套接字文件描述符
    int fd = lst_channel->Fd();
    
    // 接受一个新的客户端连接
    // accept 函数从监听套接字的连接队列中取出一个连接
    // 参数1:监听套接字描述符
    // 参数2、3:设为NULL表示不关心客户端的地址信息
    int newfd = accept(fd, NULL, NULL);
    
    // 如果接受连接失败,直接返回
    if (newfd < 0) { 
        return; 
    }
    
    // 为新的客户端连接创建一个Channel对象
    // Channel封装了文件描述符的事件监控和回调处理
    Channel *channel = new Channel(poller, newfd);
    
    
    channel->SetReadCallback(std::bind(HandleRead,channel));// 为通信套接字设置可读事件的回调函数,注意会对之前设置的读事件回调进行覆盖处理
    channel->SetWriteCallback(std::bind(HandleWrite,channel));// 为通信套接字设置可写事件的回调函数
    channel->SetCloseCallback(std::bind(HandleClose,channel));// 为通信套接字设置关闭事件的回调函数
    channel->SetErrorCallback(std::bind(HandleError,channel));// 为通信套接字设置错误事件的回调函数
    channel->SetEventCallback(std::bind(HandleEvent,channel));// 为通信套接字设置任意事件的回调函数

    // 启用读事件监控,开始监听客户端发送数据
    channel->EnableRead();
    
    // 注意:这里创建了动态分配的Channel对象,需要记得在适当的时候释放内存
    // 通常在连接关闭时,在关闭回调函数中删除Channel对象
}

int main() {
    Poller poller;

    // 创建一个监听套接字对象,用于接受客户端连接
    Socket lst_sock;
    
    // 创建服务器,监听8500端口,默认绑定到0.0.0.0(所有网络接口)
    // 如果创建失败,程序会返回错误
    lst_sock.CreateServer(8500);

    //为监听套接字,创建一个Channel进行事件的管理,以及事件的处理
    Channel channel(&poller,lst_sock.Fd());
    channel.SetReadCallback(std::bind(Acceptor,&poller,&channel));//设置读回调函数——获取新连接,为新连接创建Channel并添加监控
    channel.EnableRead();//启动读事件监控,这样子才能接受别人发来的数据
    
    // 无限循环,持续接受客户端连接
    while (1) {
        std::vector<Channel*>actives;//活跃连接
        poller.Poll(&actives);
        for(auto &a : actives)
        {
            a->HandleEvent();//根据触发了什么样的事件,来调用什么样的回调函数
        }
    }
    
    // 退出循环后,关闭监听套接字
    lst_sock.Close();
    
    // 程序结束
    return 0;
}

tcp_cli.cpp

#include "../server/socket.hpp"

int main()
{
    // 创建一个客户端套接字对象,用于连接服务器
    Socket cli_sock;

    // 创建客户端连接,连接到本地主机(127.0.0.1)的8500端口
    // 如果连接失败,程序会返回错误(假设CreateClient内部会处理)
    cli_sock.CreateClient(8500, "127.0.0.1");
    while (1)
    {
        // 准备要发送给服务器的消息内容
        std::string str = "hello bitejiuyeke!";

        // 将消息发送给服务器
        // str.c_str() 获取C风格字符串指针,str.size() 获取字符串长度
        cli_sock.Send(str.c_str(), str.size());

        // 定义缓冲区,用于接收服务器返回的数据,初始化为0
        char buf[1024] = {0};

        // 从服务器接收响应数据,最多接收1023字节(留一个位置给字符串结束符)
        cli_sock.Recv(buf, 1023);

        // 打印接收到的服务器响应
        // %s 表示以字符串格式输出
        DBG_LOG("%s", buf);

        sleep(2);
    }

    // 程序结束,返回0表示成功
    return 0;

    // 注意:cli_sock对象在main函数结束时会被自动销毁
    // Socket类的析构函数会自动调用Close()方法关闭套接字
    // 所以这里不需要显式调用cli_sock.Close()
}

我们编译运行一下

没有一点问题!!

如果说这个时候我们关闭客户端

我们会在服务端这边看到

完全没有问题!!

我们重新启动一个客户端

完美!!!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值