【Linux C++】实现Reactor服务器:基础epoll服务端|封装InetAddress|封装Socket|封装Epoll类
日期:2025.2.15(凌晨)2025.2.16(凌晨)
学习内容:
- 简单的epoll服务端
- 封装InetAddress类
- 封装Socket类
- 封装Epoll类
个人总结:
简单的epoll服务端代码:
首先先声明,从本次笔记开始,我们将从简单的epoll服务端逐渐进行优化,最终形成Reactor服务器。另外,在最后会贴上客户端的代码方便各位测试检测,我们的重点将会放在服务端上。
在此之前,我们首先需要了解的前置知识是IO多路复用模型中的epoll模型。
那首先我们先将最简单的epoll服务端的代码贴出来。
#include <iostream>
#include <sys/socket.h>
#include <stdio.h>
#include <errno.h>
#include <unistd.h>
#include <netinet/in.h>
#include <string.h>
#include <sys/fcntl.h>
#include <netinet/tcp.h>
#include <sys/epoll.h>
using std::cin;
using std::cout;
using std::endl;
#define PORT 5005
void SetNonblocking(int fd) {
fcntl(fd, F_SETFL, fcntl(fd, F_GETFL) | O_NONBLOCK);
}
int main(int argv, char* argc[]) {
int listenfd = socket(AF_INET, SOCK_STREAM | SOCK_NONBLOCK, IPPROTO_TCP);
if (listenfd == -1) {
perror("socket");
return -1;
}
int opt = 1;
setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, static_cast<socklen_t>(sizeof opt));
setsockopt(listenfd, SOL_SOCKET, TCP_NODELAY, &opt, static_cast<socklen_t>(sizeof opt));
setsockopt(listenfd, SOL_SOCKET, SO_REUSEPORT, &opt, static_cast<socklen_t>(sizeof opt));
setsockopt(listenfd, SOL_SOCKET, SO_KEEPALIVE, &opt, static_cast<socklen_t>(sizeof opt));
// SetNonblocking(listenfd);
struct sockaddr_in serv_addr;
memset(&serv_addr, 0, sizeof serv_addr);
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
serv_addr.sin_port = htons(PORT);
if (bind(listenfd, (struct sockaddr*)&serv_addr, sizeof serv_addr) != 0) {
perror("bind");
return -1;
}
if (listen(listenfd, 128) == -1) {
perror("listen");
return -1;
}
int epollfds = epoll_create(1);
struct epoll_event epoll_one;
epoll_one.data.fd = listenfd;
epoll_one.events = EPOLLIN;
epoll_ctl(epollfds, EPOLL_CTL_ADD, listenfd, &epoll_one);
struct epoll_event events[10];
while (true) {
int new_nums = epoll_wait(epollfds, events, 10, -1);
if (new_nums < 0) {
break;
}
else if (new_nums == 0) {
break;
}
for (int i = 0; i < new_nums; i++) {
if (events[i].events & EPOLLIN) {
if (events[i].data.fd == listenfd) {
struct sockaddr_in client_addr;
socklen_t client_addr_len = sizeof client_addr;
int clientfd = accept4(listenfd, (struct sockaddr*)&client_addr, &client_addr_len, SOCK_NONBLOCK);
// SetNonblocking(clientfd);
printf("New Client fd:%d\n", clientfd);
epoll_one.data.fd = clientfd;
epoll_one.events = EPOLLIN | EPOLLET;
epoll_ctl(epollfds, EPOLL_CTL_ADD, clientfd, &epoll_one);
}
else {
char buf[1024];
while (true) {
memset(buf, 0, sizeof buf);
ssize_t read_siz = read(events[i].data.fd, buf, sizeof buf);
if (read_siz > 0) {
printf("recv: (fd: %d) %s\n", events[i].data.fd, buf);
send(events[i].data.fd, buf, sizeof buf, 0);
}
else if (read_siz == -1 and errno == EINTR) {
continue;
}
else if (read_siz == -1 and (errno == EAGAIN || errno == EWOULDBLOCK)) {
break;
}
else if (read_siz == 0) {
printf("disconnect: (fd: %d) \n", events[i].data.fd);
close(events[i].data.fd);
break;
}
}
}
}
else if (events[i].events & EPOLLOUT) {
}
else {
printf("error: (fd: %d) \n", events[i].data.fd);
close(events[i].data.fd);
}
}
}
close(listenfd);
return 0;
}
优化1——关于SetNonBlocking函数
我们之前将套接字描述符设置成非阻塞是通过这种方式:
void SetNonblocking(int fd) {
fcntl(fd, F_SETFL, fcntl(fd, F_GETFL) | O_NONBLOCK);
}
但是有一些更好的表达方式:
关于listen描述符我们可以使用
int listenfd = socket(AF_INET, SOCK_STREAM | SOCK_NONBLOCK, IPPROTO_TCP);
在第二个参数中或上SOCK_NONBLOCK,同样有这样的作用。
关于已连接套接字描述符(简单的说是客户端描述符),我们可以将原本的accept函数换成accept4函数,这个函数相较于之前的函数多了一个参数flags,用来填一些选项,我们在第四个参数同样填上SOCK_NONBLOCK就可以了。
int clientfd = accept4(listenfd, (struct sockaddr*)&client_addr, &client_addr_len, SOCK_NONBLOCK);
改动2——监听描述符的设置
可以看到我们多了这些神秘代码
int opt = 1;
setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, static_cast<socklen_t>(sizeof opt));
setsockopt(listenfd, SOL_SOCKET, TCP_NODELAY, &opt, static_cast<socklen_t>(sizeof opt));
setsockopt(listenfd, SOL_SOCKET, SO_REUSEPORT, &opt, static_cast<socklen_t>(sizeof opt));
setsockopt(listenfd, SOL_SOCKET, SO_KEEPALIVE, &opt, static_cast<socklen_t>(sizeof opt));
这里我们按照第三个参数分别解释一下:
SO_REUSEADDR:这个宏的作用是当描述符关闭的时候允许地址重用,解决TIME_WAIT
问题(服务端崩溃后快速重启)
TCP_NODELAY:这个宏的作用是禁用NAGLE 算法可以降低延迟提高数据传输效率。
SO_REUSEPORT:这个宏的多个套接字可以同时监听同一个端口,常用于多线程多进程。
SO_KEEPALIVE:这个宏的作用是启用保活机制机制当长时间客户端和服务端没有传输数据时将会自动检测是否还连接如果没有连接将释放资源。
改动3——封装InetAddr类
这个类主要是用来封装struct sockaddr_in类的,这个类型本身是可以获取到ip地址或者端口的,但是很麻烦,所以不妨直接封装成类。
先贴上代码:
class InetAddress {
private:
sockaddr_in m_addr;
public:
InetAddress() = default;
InetAddress(const std::string& ip, uint16_t port);
InetAddress(const sockaddr_in& addr);
~InetAddress();
void SetAddress(const sockaddr_in& one);
const sockaddr* Addr() const;
const char* IP() const;
const short Port() const;
};
可以看到成员就是一个m_addr,初始化的时候我们一般要么是用ip地址和端口来初始化,要么使用一个现成的直接赋值进去。
这里关于初始化里注意的一点是:
之前我们会用以下内容:
struct sockaddr_in serv_addr;
memset(&serv_addr, 0, sizeof serv_addr);
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
serv_addr.sin_port = htons(PORT);
其中关于ip地址的赋值是取当前服务端中的任意网卡都可以,而现在我们是固定的一个ip地址用来监听,所以这个地方我们进行小修改:
m_addr.sin_addr.s_addr = inet_addr(ip.c_str());
这个inet_addr函数是将一个const char*的格式转化成一个十进制点浮制的ip地址。相对应的,如果我们需要ip地址的时候,我们需要使用inet_ntoa函数,注意这两者的变量。
const char* InetAddress::IP() const {
return inet_ntoa(m_addr.sin_addr);
}
另外的就不提了。以下内容是实现InetAddress类的具体代码:
#include "InetAddress.h"
InetAddress::InetAddress(const std::string& ip, uint16_t port) {
m_addr.sin_family = AF_INET;
m_addr.sin_port = htons(port);
m_addr.sin_addr.s_addr = inet_addr(ip.c_str());
}
InetAddress::InetAddress(const sockaddr_in& addr) {
m_addr = addr;
}
InetAddress:: ~InetAddress() {
}
const sockaddr* InetAddress::Addr() const {
return (sockaddr*)&m_addr;
}
const char* InetAddress::IP() const {
return inet_ntoa(m_addr.sin_addr);
}
const short InetAddress::Port() const {
return htons(m_addr.sin_port);
}
void InetAddress::SetAddress(const sockaddr_in& one) {
m_addr = one;
}
改动4——封装Socket类
我们可以注意到,关于bind,listen,accept,还有开头的一些初始化之类的事情也是比较繁琐的,我们同样也可以封装起来。
int CreatNonBlocking() {
return socket(AF_INET, SOCK_STREAM | SOCK_NONBLOCK, IPPROTO_TCP);
}
class Socket {
private:
int m_fd;
public:
Socket(int fd);
~Socket();
void ReuseAddr(bool opt);
void ReusePort(bool opt);
void KeepAlive(bool opt);
void NoDelay(bool opt);
void Bind(const InetAddress& addr);
void Listen(int siz = 128);
int Accept(InetAddress& addr);
const int GetFD() const;
};
这里开头的第一个函数是我们用来返回一个非阻塞的描述符。
具体的使用是这样:
Socket listen(CreatNonBlocking());
这样就可以创建出来listen描述符。
然后这个Socket类主要使用一个fd来创建出来,对应的会有Bind,Listen,Accept操作。
只是这里我们需要将Socket类和InetAddress类联系起来,思考一下细节。
我们在用bind函数的时候,主要的思想就是将描述符和ip地址捆绑起来,所以我们的参数就是一个InetAddress。
在使用listen函数的时候,我们主要是在乎队列的大小,所以参数用一个siz来决定待链接队列里的容量就好了。
accept函数,我们的目的有两个:
- 返回出来已连接描述符
- 由于传入的是指针,会修改出来ip地址。
所以我们要完成这两个目的,用int返回类型,然后参数用引用来在中间修改出来ip地址。
而我们每一个Socket都需要用fd来创建出来,所以关于已连接套接字描述符我们可以用这种方式
Socket* client = new Socket(listen.Accept(client_addr1));
由于我们本身用accept函数就是从监听描述符的代链接队列中取出来,所以这个函数也只有是listen描述符才会使用。
将创建出来的fd也用Socket类包装起来就好了,这样就能直观的得到client。(关于这里为什么是指针的原因是因为我们不希望析构函数会close掉fd,所以用指针来逃避析构,最后如何解决这种问题我们先按下不表)
这里是具体的实现细节:
Socket::Socket(int fd) :m_fd(fd) {
// m_fd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
// m_fd = socket(AF_INET, SOCK_STREAM | SOCK_NONBLOCK, IPPROTO_TCP);
}
void Socket::ReuseAddr(bool opt) {
setsockopt(m_fd, SOL_SOCKET, SO_REUSEADDR, &opt, static_cast<socklen_t>(sizeof opt));
}
void Socket::ReusePort(bool opt) {
setsockopt(m_fd, SOL_SOCKET, SO_REUSEPORT, &opt, static_cast<socklen_t>(sizeof opt));
}
void Socket::KeepAlive(bool opt) {
//套接字开启保活机制,如果长时间没有传输数据会检测是否链接,没有链接就会释放资源
setsockopt(m_fd, SOL_SOCKET, SO_KEEPALIVE, &opt, static_cast<socklen_t>(sizeof opt));
}
void Socket::NoDelay(bool opt) {
//禁用nagle算法,减少数据延迟
setsockopt(m_fd, SOL_SOCKET, TCP_NODELAY, &opt, static_cast<socklen_t>(sizeof opt));
}
void Socket::Bind(const InetAddress& addr) {
if (bind(m_fd, addr.Addr(), sizeof(sockaddr_in)) < 0) {
perror("Bind");
exit(-1);
}
}
void Socket::Listen(int siz) {
if (listen(m_fd, siz) < 0) {
perror("Listen");
exit(-1);
}
}
int Socket::Accept(InetAddress& addr) {
struct sockaddr_in one;
socklen_t len = sizeof(sockaddr_in);
int clientfd = accept4(m_fd, (sockaddr*)&one, &len, SOCK_NONBLOCK);
addr.SetAddress(one);
return clientfd;
}
const int Socket::GetFD()const {
return m_fd;
}
改动5——封装Epoll类
这个封装就比较简单了,我们的主要目的就是一个add函数和一个返回任务(vector)的函数。
这是声明的部分:
class Epoll {
private:
static const int MAXSIZE = 10;
int m_epollfd;
std::vector<epoll_event> m_evs;
public:
Epoll();
~Epoll();
void AddFD(int fd, uint32_t opt);
std::vector<epoll_event> NewEvents(int timeout = -1);
};
其中Newevent函数我们防止大量的数据拷贝造成的性能损耗,所以我们将原本的数组改成了vector,然后用了move,在原本的wait函数中使用了.data函数可以将vector变成数组,才知道有这个东西。
以下代码是NewEvent函数的实现细节:
std::vector<epoll_event> Epoll::NewEvents(int timeout) {
m_evs.resize(MAXSIZE);
int newthings = epoll_wait(m_epollfd, m_evs.data(), MAXSIZE, timeout);
if (newthings < 0) {
perror("epoll_wait");
exit(-1);
}
else if (newthings == 0) {
return std::vector<epoll_event>();
}
m_evs.resize(newthings);
return std::move(m_evs);
}
但是目前的问题是频繁的resize也会导致性能损耗,这里我们先不管,日后再改。
优化后的服务端代码:
#include <arpa/inet.h>
#include <errno.h>
#include <netinet/in.h>
#include <netinet/tcp.h>
#include <stdio.h>
#include <string.h>
#include <sys/epoll.h>
#include <sys/fcntl.h>
#include <sys/socket.h>
#include <unistd.h>
#include <iostream>
#include "InetAddress.h"
#include "Socket.h"
#include "Epoll.h"
using std::cin;
using std::cout;
using std::endl;
int main(int argc, char* argv[]) {
if (argc != 3) {
cout << "./server 192.168.11.132 5005" << endl;
return 0;
}
Socket listen(CreatNonBlocking());
int listenfd = listen.GetFD();
listen.KeepAlive(1);
listen.NoDelay(1);
listen.ReuseAddr(1);
listen.ReusePort(1);
InetAddress serv_addr(argv[1], atoi(argv[2]));
listen.Bind(serv_addr);
listen.Listen(128);
Epoll ep;
ep.AddFD(listenfd, EPOLLIN);
while (true) {
auto new_evs = ep.NewEvents();
for (auto& ev : new_evs) {
if (ev.events & EPOLLIN) {
if (ev.data.fd == listenfd) {
InetAddress client_addr1;
Socket* client = new Socket(listen.Accept(client_addr1));
int clientfd = client->GetFD();
printf("New Client fd:%d ip:%s port:%hu \n", clientfd, client_addr1.IP(), client_addr1.Port());
ep.AddFD(clientfd, EPOLLIN | EPOLLET);
}
else {
char buf[1024];
while (true) {
memset(buf, 0, sizeof buf);
ssize_t read_siz =
read(ev.data.fd, buf, sizeof buf);
if (read_siz > 0) {
printf("recv: (fd: %d) %s\n", ev.data.fd,
buf);
send(ev.data.fd, buf, sizeof buf, 0);
}
else if (read_siz == -1 and errno == EINTR) {
continue;
}
else if (read_siz == -1 and (errno == EAGAIN || errno == EWOULDBLOCK)) {
break;
}
else if (read_siz == 0) {
printf("disconnect: (fd: %d) \n", ev.data.fd);
close(ev.data.fd);
break;
}
}
}
}
else if (ev.events & EPOLLOUT) {
}
else {
printf("error: (fd: %d) \n", ev.data.fd);
close(ev.data.fd);
}
}
}
close(listenfd);
return 0;
}
Epoll.cpp:
#include "Epoll.h"
Epoll::Epoll() { m_epollfd = epoll_create(1); }
Epoll::~Epoll() { close(m_epollfd); }
void Epoll::AddFD(int fd, uint32_t opt) {
epoll_event one;
one.data.fd = fd;
one.events = opt;
epoll_ctl(m_epollfd, EPOLL_CTL_ADD, fd, &one);
}
std::vector<epoll_event> Epoll::NewEvents(int timeout) {
m_evs.resize(MAXSIZE);
int newthings = epoll_wait(m_epollfd, m_evs.data(), MAXSIZE, timeout);
if (newthings < 0) {
perror("epoll_wait");
exit(-1);
}
else if (newthings == 0) {
return std::vector<epoll_event>();
}
m_evs.resize(newthings);
return std::move(m_evs);
}
客户端代码:
#include <iostream>
#include <sys/socket.h>
#include <string.h>
#include <unistd.h>
#include <netdb.h>
using std::cin;
using std::cout;
using std::endl;
int main(int argv, char* argc[]) {
if (argv != 3) {
cout << "./client 192.168.11.132 5005" << endl;
return 0;
}
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd == -1) {
perror("socket");
return -1;
}
struct hostent* k_host;
k_host = gethostbyname(argc[1]);
if (k_host == 0) {
perror("gethostbyname");
return -1;
}
struct sockaddr_in serv_addr;
serv_addr.sin_port = htons(atoi(argc[2]));
serv_addr.sin_family = AF_INET;
memcpy(&serv_addr.sin_addr, k_host->h_addr, k_host->h_length);
if (connect(sockfd, (struct sockaddr*)&serv_addr, sizeof serv_addr) < 0) {
perror("connect");
return -1;
}
printf("connect OK!\n");
char buf[1024];
for (int i = 1; i < 100; i++) {
memset(buf, 0, sizeof buf);
printf("please input:");
scanf("%s", buf);
if (send(sockfd, buf, sizeof buf, 0) <= 0) {
perror("send");
break;
}
memset(buf, 0, sizeof buf);
if (recv(sockfd, buf, sizeof buf, 0) <= 0) {
perror("receive");
break;
}
printf("recv: %s\n", buf);
}
printf("%s\n", buf);
close(sockfd);
return 0;
}
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· 没有源码,如何修改代码逻辑?
· PowerShell开发游戏 · 打蜜蜂
· 在鹅厂做java开发是什么体验
· WPF到Web的无缝过渡:英雄联盟客户端的OpenSilver迁移实战