4.Socket类、InetAddr类、Epoll类实现模块化

目录

[1. InetAddr类](#1. InetAddr类)

类定义

代码说明

类实现

2.Socket类

类定义

类实现

[3. Epoll类](#3. Epoll类)

类定义

构造与析构函数

方法实现

类实现

[4. 使用模块化设计](#4. 使用模块化设计)

示例使用(main.cpp))

[5. 运行程序](#5. 运行程序)


随着程序复杂度的增加,单一的面向过程的代码会变得难以理解和维护。为了提高代码的可读性和可维护性,我们可以通过模块化的方式,将程序分解为多个类,每个类负责特定的功能。这种设计不仅提高了代码的复用率,还能帮助开发者集中精力在每个单独的功能模块上,便于维护和扩展。

在本文中,我们将介绍一种简单的模块化设计方法,使用C++面向对象的特性,来设计一个网络编程框架。我们将创建三个关键类:InetAddr、Socket和Epoll。这些类的设计目的在于简化网络编程的过程,提高代码的可读性和可维护性。

1. InetAddr类

InetAddr类主要负责处理与IP地址相关的操作。它封装了sockaddr_in结构体,并提供了一系列方法来获取IP地址和端口。

类定义

cpp 复制代码
#pragma once // 确保该头文件只被包含一次  

#include <string>     // 引入字符串类  
#include <arpa/inet.h> // 提供网络地址结构定义及函数  
#include <stdio.h>    // 标准输入输出库  

class InetAddr {
public:
    // 默认构造函数  
    InetAddr();

    // 带参数的构造函数,接受端口和可选的IP地址  
    InetAddr(unsigned short port, const char* ip = nullptr);

    // 获取sockaddr_in结构的指针,供外部使用  
    const sockaddr_in* getAddr() const { return &addr_; }

    // 设置sockaddr_in结构的地址,供外部使用  
    void setAddr(const struct sockaddr_in& addr) { addr_ = addr; }

    // 将地址转换为IP字符串格式  
    std::string toIp() const;

    // 将地址转换为IP:端口格式的字符串  
    std::string toIpPort() const;

    // 获取端口号  
    unsigned short toPort() const;

private:
    struct sockaddr_in addr_; // 存储网络地址信息的结构体  
};

代码说明

  • InetAddr 类用于处理网络地址信息,主要封装了 sockaddr_in 结构体,提供了获取和设置网络地址、IP转换等操作。
  • 构造函数 :
    • 默认构造函数:初始化为0的地址,设置地址族为IPv4。
    • 带参数的构造函数 :允许用户指定端口和IP地址,并适当地初始化 sockaddr_in 结构体。
  • 公共方法 :
    • getAddr():返回存储的地址信息的常量引用,便于外部访问。
    • setAddr() :允许设置或修改当前存储的 sockaddr_in 地址。
    • toIp():转换当前地址为点分十进制格式的字符串。
    • toIpPort():将IP和端口组合为字符串,便于显示。
    • toPort():获取当前对象的端口号,返回值为无符号短整型。

Socket类负责封装与套接字相关的操作,包括创建、绑定、监听和接受连接等。

类实现

cpp 复制代码
#include "InetAddr.h"         // 包含InetAddr类的定义  
#include <string.h>           // 包含字符串操作函数  
#include <arpa/inet.h>       // 包含网络地址转换函数  

// 默认构造函数  
InetAddr::InetAddr() {
    // 将地址结构清零,确保没有未定义的值  
    memset(&addr_, 0, sizeof(addr_));
}

// 带参数的构造函数,用于初始化port和ip  
InetAddr::InetAddr(unsigned short port, const char* ip) {
    // 将地址结构清零  
    memset(&addr_, 0, sizeof(addr_));
    addr_.sin_family = AF_INET; // 设置地址族为IPv4  
    addr_.sin_port = htons(port); // 将主机字节序的端口转换为网络字节序  
    // 如果未提供IP,则使用INADDR_ANY,允许接受任何IP地址的连接  
    if (ip == nullptr) {
        addr_.sin_addr.s_addr = htonl(INADDR_ANY);
    }
    else {
        // 将字符串形式的IP地址转换为网络字节序的二进制格式  
        inet_pton(AF_INET, ip, &addr_.sin_addr.s_addr);
    }
}

// 将存储的IP地址转换为字符串格式  
std::string InetAddr::toIp() const {
    char ip[64] = { 0 }; // 存储转换后的IP地址  
    // 将网络字节序的IP地址转换为字符串形式  
    inet_ntop(AF_INET, &addr_.sin_addr.s_addr, ip, sizeof(ip));
    return ip; // 返回IP字符串  
}

// 将IP地址和端口组合为 "IP:port" 的格式  
std::string InetAddr::toIpPort() const {
    char buf[128] = { 0 }; // 存储组合后的字符串  
    // 使用sprintf将IP和端口格式化为字符串  
    sprintf(buf, "%s:%d", toIp().c_str(), toPort());
    return buf; // 返回组合后的字符串  
}

// 获取存储的端口号  
unsigned short InetAddr::toPort() const {
    // 将网络字节序的端口转换为主机字节序并返回  
    return ntohs(addr_.sin_port);
}

2.Socket类

类定义

cpp 复制代码
#pragma once // 确保该头文件只被包含一次  

class InetAddr; // 前向声明InetAddr类  

class Socket
{
public:
    // 默认构造函数,初始化Socket对象  
    Socket();

    // 带参数的构造函数,使用给定的文件描述符初始化Socket  
    Socket(int fd);

    // 析构函数,关闭Socket以释放资源  
    ~Socket();

    // 将Socket绑定到指定的InetAddr地址  
    void bind(InetAddr* serv_addr);

    // 接受来自客户端的连接,并返回新的套接字文件描述符  
    int accept(InetAddr* addr);

    // 将Socket设置为监听状态,准备接受连接  
    void listen();

    // 设置Socket为非阻塞模式  
    void setNonblock();

    // 获取套接字文件描述符  
    int fd() const { return sockfd_; }

private:
    int sockfd_; // 存储套接字的文件描述符  
};
  • 构造函数:

    • Socket(): 默认构造函数,用于创建一个新的TCP套接字。
    • Socket(int fd): 通过传入的文件描述符初始化套接字对象,允许外部使用现有套接字。
  • 成员函数:

    • void bind(InetAddr* serv_addr): 绑定给定的地址信息到套接字。
    • void listen(): 设置套接字为监听状态,准备接收连接请求。
    • int accept(InetAddr* peerAddr): 接受客户端连接,并返回新连接的套接字文件描述符,此外可以获取客户端地址。
    • int fd() const: 返回当前套接字的文件描述符,供外部访问。
  • 私有成员变量:

    • const int sockfd_: 存储套接字的文件描述符,使用 const 限制其在对象生命周期内不可更改。

类实现

cpp 复制代码
#include "Socket.h"                // 引入Socket类的定义  
#include "util.h"          
#include "InetAddr.h"              // 引入InetAddr类的定义  
#include <fcntl.h>                 // 提供fcntl函数的定义  
#include <unistd.h>                // 提供close函数的定义  

// 默认构造函数,创建一个TCP套接字  
Socket::Socket()
    : sockfd_(socket(AF_INET, SOCK_STREAM, 0)) // 创建一个IPv4 TCP套接字  
{
    // 检查套接字创建是否成功,如果失败、打印错误信息  
    perror_if(sockfd_ == -1, "socket");
}

// 带参数的构造函数,通过给定的文件描述符初始化套接字  
Socket::Socket(int fd)
    : sockfd_(fd) // 使用提供的文件描述符进行初始化  
{
    // 检查文件描述符是否有效  
    perror_if(sockfd_ == -1, "socket(int fd)");
}

// 析构函数,关闭套接字以释放资源  
Socket::~Socket()
{
    if (sockfd_ != -1) { // 确保套接字有效  
        close(sockfd_); // 关闭套接字文件描述符  
        sockfd_ = -1;   // 将文件描述符标记为无效,防止重复关闭  
    }
}

// 将套接字绑定到指定的InetAddr地址  
void Socket::bind(InetAddr* serv_addr)
{
    // 调用系统级绑定函数  
    int ret = ::bind(sockfd_, (sockaddr*)serv_addr->getAddr(), sizeof(sockaddr_in));

    // 检查绑定是否成功,如果失败,打印错误信息  
    perror_if(ret == -1, "bind");
}

// 接受客户端连接,并返回新的套接字文件描述符  
int Socket::accept(InetAddr* addr)
{
    struct sockaddr_in cliaddr; // 存储客户端地址信息  
    socklen_t len = sizeof(cliaddr); // 存储地址长度  
    // 调用系统级接受函数  
    int cfd = ::accept(sockfd_, (struct sockaddr*)&cliaddr, &len);

    // 检查接受客户端连接是否成功,如果失败,打印错误信息  
    perror_if(cfd == -1, "accept");

    // 设置已连接客户端的地址信息  
    addr->setAddr(cliaddr);

    // 输出新连接的客户端信息  
    printf("new client fd %d ip: %s, port: %d connected..\n", cfd, addr->toIp().c_str(), addr->toPort());

    return cfd; // 返回新连接的套接字文件描述符  
}

// 将套接字设置为监听状态,准备接受连接  
void Socket::listen()
{
    // 调用系统级监听函数,最多同时处理128个连接请求  
    int ret = ::listen(sockfd_, 128);

    // 检查监听是否成功,如果失败,打印错误信息  
    perror_if(ret == -1, "listen");
}

// 将套接字设置为非阻塞模式  
void Socket::setNonblock()
{
    // 获取当前文件描述符的标志  
    int flag = fcntl(sockfd_, F_GETFL);
    flag |= O_NONBLOCK; // 将非阻塞标志添加到当前标志中  
    // 将新的标志设置回文件描述符  
    fcntl(sockfd_, F_SETFL, flag);
}

3. Epoll类

Epoll类用于处理epoll事件,包括创建epoll实例、管理文件描述符添加/删除以及等待事件的发生。

类定义

cpp 复制代码
#pragma once // 确保该头文件只被包含一次  

#include <sys/epoll.h> // 包含epoll相关的系统调用  
#include <vector>      // 引入vector标准库  
using std::vector;    // 使用std命名空间中的vector类  

class Epoll
{
public:
    // 构造函数,初始化epoll实例  
    Epoll();

    // 析构函数,清理epoll资源  
    ~Epoll();

    // 更新给定文件描述符的事件  
    void update(int sockfd, int events, int op);

    // 从epoll中删除指定的文件描述符  
    void epoll_delete(int fd);

    // 等待事件发生,返回活跃的文件描述符事件  
    void Epoll_wait(vector<epoll_event>& active, int timeout = 10);

private:
    int epfd_;                       // epoll实例的文件描述符  
    struct epoll_event* events_;      // 存储返回的事件  
};

构造与析构函数

  • 创建epoll实例并初始化事件数组。
  • 在析构函数中释放资源。

方法实现

  • update(): 添加、修改或删除文件描述符的事件。
  • wait(): 使用epoll_wait()等待活动事件并填充事件数组。

类实现

cpp 复制代码
#include "Epoll.h"               // 引入Epoll类的定义  
#include "util.h"                // 引入自定义工具函数头文件  
#include <string.h>              // 引入cstring库以使用memset和相关函数  
#include <unistd.h>             // 引入unistd.h以使用close函数

const int SIZE = 1024;         // 定义epoll事件数组的大小  

// 构造函数,创建一个新的epoll实例并初始化事件数组  
Epoll::Epoll()
    : epfd_(epoll_create(1)), // 创建epoll实例,参数为1,表示初始的事件数  
    events_(new epoll_event[SIZE]) // 动态分配事件数组  
{
    // 检查epoll_create是否成功,失败则调用perror_if输出错误信息  
    perror_if(epfd_ == -1, "epoll_create");
    // 初始化事件数组,清空内存  
    memset(events_, 0, sizeof(epoll_event) * SIZE);
}

// 析构函数,清理epoll资源  
Epoll::~Epoll()
{
    // 删除事件数组  
    delete[] events_; // 释放动态分配的事件数组  
    // 将epfd_设为-1以避免重复关闭  
    if (epfd_ != -1) {
        close(epfd_); // 关闭epoll实例的文件描述符  
        epfd_ = -1; // 将文件描述符设置为-1表示无效  
    }
}

// 更新给定的文件描述符,设置其事件类型  
void Epoll::update(int sockfd, int events, int op)
{
    struct epoll_event ev; // 创建epoll_event结构体以存储事件信息  
    memset(&ev, 0, sizeof(ev)); // 清空结构体  
    ev.data.fd = sockfd; // 将文件描述符存储在event结构体中  
    ev.events = events; // 设置感兴趣的事件  

    // 调用epoll_ctl更新epoll实例  
    int ret = epoll_ctl(epfd_, op, sockfd, &ev);
    // 检查epoll_ctl是否成功  
    perror_if(ret == -1, "epoll_ctl");
}

// 从epoll中删除指定的文件描述符  
void Epoll::epoll_delete(int fd)
{
    // 调用epoll_ctl删除指定的文件描述符  
    int ret = epoll_ctl(epfd_, EPOLL_CTL_DEL, fd, nullptr);
    // 检查epoll_ctl是否成功  
    perror_if(ret == -1, "epoll_ctl del");
}

// 等待事件发生,返回活跃的事件列表  
void Epoll::Epoll_wait(vector<epoll_event>& active, int timeout)
{
    // 调用epoll_wait等待事件的发生  
    int nums = epoll_wait(epfd_, events_, SIZE, timeout);
    // 检查epoll_wait是否成功  
    perror_if(nums == -1, "epoll_wait");

    // 将活跃的事件添加到active vector中  
    for (int i = 0; i < nums; ++i) {
        active.emplace_back(events_[i]); // 将每个活跃事件存入active数组  
    }
}

4. 使用模块化设计

通过将网络相关的功能分割成几个类,可以简化主要的服务器逻辑。使用这些类,我们可以方便地进行网络编程而不必关注底层的细节。

示例使用(main.cpp)

下面是如何使用这些类来构建一个简单的服务器:

cpp 复制代码
#include "Epoll.h"        // 引入Epoll类的定义  
#include "Socket.h"      // 引入Socket类的定义  
#include "util.h"          // 引入工具函数的头文件  
#include "InetAddr.h"       // 引入InetAddr类的定义  
#include <stdio.h>              // 引入标准输入输出库  
#include <string.h>             // 引入cstring库以使用memset和相关函数  
#include <unistd.h>             // 引入unistd库以使用read和close函数  

const int READ_BUFFER = 1024;  // 定义读取缓冲区的大小  
const int MAXSIZE = 1024;      // 定义最大连接数(未使用)  

// 前向声明函数,用于处理事件  
void handleEvent(int sockfd, Epoll& poll);

int main()
{
    Socket serv_socket;             // 创建一个服务器套接字  
    InetAddr saddr(10000);         // 创建一个InetAddr对象,用于绑定到端口10000  
    serv_socket.bind(&saddr);       // 绑定服务器套接字到指定地址  
    serv_socket.listen();           // 开始监听客户端连接  
    serv_socket.setNonblock();      // 设置服务器套接字为非阻塞模式  

    Epoll poll;                     // 创建epoll实例  
    poll.update(serv_socket.fd(), EPOLLIN, EPOLL_CTL_ADD); // 将服务器套接字添加到epoll实例中,监控可读事件  

    // 主循环,持续处理事件  
    while (1)
    {
        vector<epoll_event> active; // 存储活动事件的向量  
        poll.Epoll_wait(active);     // 等待事件的发生  

        int nums = active.size();    // 当前活动事件的数量  
        for (int i = 0; i < nums; ++i) {
            int curfd = active[i].data.fd; // 获取当前事件对应的文件描述符  

            // 检查是否是可读事件  
            if (active[i].events & EPOLLIN) {
                if (curfd == serv_socket.fd()) { // 如果当前文件描述符是服务器套接字  
                    InetAddr caddr;              // 创建一个InetAddr对象,存储客户端地址  
                    Socket* cli_socket = new Socket(serv_socket.accept(&caddr)); // 接受客户端连接并创建新的套接字  

                    // 注意:需要处理内存泄漏(后续版本将修复内存管理)  
                    cli_socket->setNonblock();   // 设置客户端套接字为非阻塞模式  
                    poll.update(cli_socket->fd(), EPOLLIN, EPOLL_CTL_ADD); // 将客户端套接字添加到epoll实例  
                }
                else {
                    handleEvent(curfd, poll);   // 处理其他可读事件  
                }
            }
            else if (active[i].events & EPOLLOUT) {
                // 其他事件以后的版本会实现  
            }
        }
    }

    return 0; // 主程序结束  
}

// 处理可读事件的函数  
void handleEvent(int sockfd, Epoll& poll) {
    char buf[READ_BUFFER];         // 声明读取缓冲区  
    memset(buf, 0, sizeof(buf));   // 清空缓冲区  

    // 从套接字读取数据  
    ssize_t bytes_read = read(sockfd, buf, sizeof(buf));

    // 判断读取结果  
    if (bytes_read > 0) {
        // 成功读取数据,输出客户端发来的消息  
        printf("client fd %d says: %s\n", sockfd, buf);
        // 将接收到的数据写回给客户端(回显)  
        write(sockfd, buf, bytes_read);
    }
    else if (bytes_read == -1) { // 读取出错  
        perror_if(1, "read");    // 调用错误处理函数  
    }
    else if (bytes_read == 0) {  // 客户端断开连接  
        printf("client fd %d disconnected\n", sockfd);
        poll.epoll_delete(sockfd); // 从epoll实例中删除该套接字  
        close(sockfd);             // 关闭套接字  
    }
}

5. 运行程序

复制代码
g++ -o Server main.cpp Epoll.cpp util.cpp Socket.cpp InetAddr.cpp

编译完成后,可以通过以下命令运行服务器:

复制代码
./Server        # 启动服务器

客户端可以使用之前的客户端程序作为连接方式,确保与服务器在同一网络下运行。

相关推荐
律宏阔2 小时前
WSL Docker 端口明明空闲,但就是绑不上端口
linux·windows
律宏阔2 小时前
WSL 突然断网,无法 ping 内网或外网
linux·windows
穷人小水滴2 小时前
用容器编译 VirtualBox 虚拟机软件 (ArchLinux, podman)
linux·容器·virtualbox
乱码三千2 小时前
如何优雅地直连无公网 IP 的远程 GPU 服务器
linux·人工智能·程序员
yunwei372 小时前
使用 eBPF 跟踪 Nginx 请求
linux·后端·性能优化
yunwei372 小时前
使用 eBPF 跟踪 MySQL 查询
linux·后端·性能优化
yunwei372 小时前
eBPF 示例教程:使用 XDP 捕获 TCP 信息
linux·后端·性能优化
GeW2 小时前
制造业高端化,为什么必须重估Linux和数据库?
linux
GeW2 小时前
工业场景中的Linux与数据库选型:高端化转型的技术底座
linux
yunwei372 小时前
eBPF 开发者教程: 简单的 XDP 负载均衡器
linux·后端·性能优化