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

目录

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

类定义

代码说明

类实现

2.Socket类

类定义

类实现

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

类定义

构造与析构函数

方法实现

类实现

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

示例使用(main.cpp))

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


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

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

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        # 启动服务器

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

相关推荐
车载小杜几秒前
基于指针的线程池
开发语言·c++
李迟31 分钟前
跨系统平台实践:在内网自建kylin服务版系统yum源
linux
odoo-卜永1 小时前
ubuntu22.04连接爱普生打印机型号L385
linux·经验分享·ubuntu
网络抓包与爬虫1 小时前
Wireshark——抓包分析
websocket·网络协议·tcp/ip·http·网络安全·https·udp
小麦嵌入式2 小时前
Linux驱动开发实战(十一):GPIO子系统深度解析与RGB LED驱动实践
linux·c语言·驱动开发·stm32·嵌入式硬件·物联网·ubuntu
刘若水2 小时前
Linux: 进程信号初识
linux·运维·服务器
云 无 心 以 出 岫2 小时前
贪心算法QwQ
数据结构·c++·算法·贪心算法
仙女很美哦2 小时前
Flutter视频播放、Flutter VideoPlayer 视频播放组件精要
websocket·网络协议·tcp/ip·http·网络安全·https·udp
换一颗红豆3 小时前
【C++ 多态】—— 礼器九鼎,釉下乾坤,多态中的 “风水寻龙诀“
c++
随便昵称3 小时前
蓝桥杯专项复习——前缀和和差分
c++·算法·前缀和·蓝桥杯