目录
一.套接字模块
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:同步写入(等待物理写操作完成)
- 等等。
设置非阻塞模式的步骤:
- 使用F_GETFL获取当前文件状态标志。
- 使用按位或(|)将O_NONBLOCK标志加上。
- 使用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活动。
这是一个至关重要的管理功能,让我们从实际场景来理解它的必要性:
为什么需要知道当前正在监控什么事件?
场景一:避免重复操作,提升性能
假设你的服务器需要向客户端发送一大块数据。流程通常是:
-
先将数据放入发送缓冲区。
-
然后要求系统“在连接可以写入时通知我”(即监听可写事件)。
-
当可写事件触发,系统通知你,你才开始发送数据。
现在,如果在第一块数据还没发完时,上层业务逻辑又产生了一块新数据要发送。此时,一个朴素的做法是:“再次请求监听可写事件”。
但事件状态查询机制允许你在此之前先问一个问题:“这个连接当前已经在监听可写事件了吗?” 如果答案是“是”,那么第二次请求就是完全多余的,可以立即跳过。这就避免了无意义的、消耗资源的重复指令。
场景二:确保状态一致性,防止逻辑错误
考虑一个连接的生命周期管理。当连接关闭时,我们需要确保系统不再监控它的任何事件(既不关心它是否可读,也不关心它是否可写)。
如果没有状态查询,我们可能盲目地发送指令:“停止监听所有事件”。但如果这个连接本来就已经不在监控列表里了呢?这个指令就变得多余,甚至可能引发错误。
有了状态查询,我们可以先检查:“这个连接当前还在被监控吗?具体监控了什么事件?” 然后,基于这个准确的信息,再执行精确的清理操作,比如“仅停止当前正在被监控的可读事件”。
这个“查询”具体查的是什么?
它查的 不是 底层操作系统此刻检测到的物理信号(比如网络卡上是否有电信号),而是 应用层自己之前下达的“监控指令”的记录。
你可以把它理解为: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多路复用的抽象层和事件分发中心。它的核心设计目标包括:
-
简化复杂性:封装epoll等系统调用的底层细节,为上层提供清晰、一致的编程接口。
-
提升效率:通过单次系统调用监控成千上万个文件描述符,实现真正的高并发处理能力。
-
统一管理:对所有需要I/O事件监控的描述符进行集中式、标准化的生命周期管理。
核心功能接口
Poller模块提供三个核心操作接口,完整覆盖了描述符事件监控的生命周期:
-
添加事件监控
-
功能:将一个文件描述符及其关注的事件类型(可读、可写、错误等)添加到监控集合中。
-
内部实现:通常通过
epoll_ctl系统调用的EPOLL_CTL_ADD操作实现。这一操作建立了描述符与Poller之间的监控关系。 -
上层协作:此操作由Channel模块发起。当Channel需要开始监控某个事件时,它会调用Poller的相应接口来完成实际的系统级注册。
-
-
修改事件监控
-
功能:调整已监控描述符所关注的事件类型。
-
应用场景:这是实现动态事件管理的核心。例如,当一个连接从"只监控可读"变为"需要同时监控可写"时,通过此接口更新监控状态,而无需先移除再重新添加。
-
性能优化:直接修改比先移除后添加的效率更高,避免了描述符在内核状态中的短暂缺失。
-
-
移除事件监控
-
功能:将指定描述符从监控集合中完全移除,Poller将不再关注其任何事件。
-
重要性:这是防止内存泄漏和资源浪费的关键操作。当连接关闭或暂时不需要监控时,必须及时移除监控。
-
清理保证:移除操作确保了描述符在销毁前,所有相关的事件监控状态都被彻底清理。
-
与上层模块的协作关系
Poller模块与Channel模块形成紧密的协作关系,共同构建了完整的事件监控体系:
-
注册代理:Channel作为事件监控的"申请者",Poller作为事件监控的"执行者"。Channel封装了单个描述符的事件状态和回调逻辑,而Poller负责将这些离散的监控请求汇总并高效地提交给操作系统。
-
事件反馈循环:当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()
}
我们编译运行一下



没有一点问题!!
如果说这个时候我们关闭客户端

我们会在服务端这边看到

完全没有问题!!
我们重新启动一个客户端



完美!!!


1210

被折叠的 条评论
为什么被折叠?



