目录
[1. InetAddr类](#1. InetAddr类)
[3. Epoll类](#3. Epoll类)
[4. 使用模块化设计](#4. 使用模块化设计)
[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 # 启动服务器
客户端可以使用之前的客户端程序作为连接方式,确保与服务器在同一网络下运行。