多路复用版本:
#include<iostream>
#include<sys/socket.h>
#include<arpa/inet.h>
#include<fcntl.h>
#include<vector>
#include<system_error>
#include<string>
#include<cstring>
#include<netinet/in.h>
#include<sys/epoll.h>
#include<cerrno>
#include<unistd.h>
#include<memory>
constexpr int MAX_EVENTS=1024;
constexpr int PORT=8080;
constexpr int BUFFER_SIZE=4096;
constexpr int EPOLL_TIMEOUT=-1; //表示超时
//定义客户端结构体
struct ClientData {
int client_fd;
char ip[INET_ADDRSTRLEN];//IPv4地址
uint16_t port;
std::string read_buffer;//将读取的信息存入read_buffer
std::string write_buffer;//将要发送的信息存入write_buffer
};
//打印客户端信息
void print_client_info(const ClientData* client,const std::string& title) {
std::cout << "[" << title << "]"
<< "客户端IP:" << client->ip
<< ",客户端端口:" << client->port
<< ",客户端fd:" << client->client_fd
<< std::endl;
}
//将文件描述符设置为非阻塞模式
void set_no_block(int fd) {
int status=fcntl(fd,F_GETFL,0);//获取fd的状态
if(status == -1) {
throw std::system_error(errno,std::generic_category(),"获取fd状态失败");
}
if(fcntl(fd,F_SETFL,status | O_NONBLOCK) == -1) {
throw std::system_error(errno,std::generic_category(),"设置fd状态失败");
}
}
//注册/修改事件
void add_or_modify(int epoll_fd,int fd,uint32_t events,ClientData* client) {
struct epoll_event ev;
memset(&ev,0,sizeof(ev));
ev.events = events;
ev.data.ptr = client;//绑定客户端数据,便于获取
//先尝试修改事件,如果不存在再注册
if(epoll_ctl(epoll_fd,EPOLL_CTL_MOD,fd,&ev) == -1) {
if(errno == ENOENT) {
//ENOENT表示事件不存在,注册
if(epoll_ctl(epoll_fd,EPOLL_CTL_ADD,fd,&ev) == -1) {
throw std::system_error(errno,std::generic_category(),"注册事件失败");
}
} else {
throw std::system_error(errno,std::generic_category(),"修改事件失败");
}
}
}
//删除事件
void remove(int epoll_fd,int fd) {
if(fcntl(epoll_fd,EPOLL_CTL_DEL,fd,nullptr) == -1) {
throw std::system_error(errno,std::generic_category(),"删除事件失败");
}
close(fd);
}
void handle_new_connection(int server_fd,int epoll_fd) {
//server_fd的已完成连接队列有新连接
struct sockaddr_in client_addr;
socklen_t client_len=sizeof(client_addr);
int client_fd=accept4(server_fd,(sockaddr*)(&client_addr),&client_len,SOCK_NONBLOCK);
if(client_fd == -1) {
std::cerr << "accept新连接失败" << std::strerror(errno) << std::endl;
return;
}
auto client = std::make_unique<ClientData>();
client->client_fd = client_fd;
client->port=ntohs(client_addr.sin_port);
if(inet_ntop(AF_INET,&client_addr.sin_addr,client->ip,sizeof(client->ip)) == nullptr) {
std::cerr << "IP转换失败" << std::strerror(errno) << std::endl;
close(client_fd);
return;
}
print_client_info(client.get(),"获得新客户端连接");
add_or_modify(epoll_fd,client_fd,EPOLLIN | EPOLLET,client.release());
}
//处理客户端读事件
void handle_read_event(ClientData* client,int epoll_fd) {
char raw_buf[BUFFER_SIZE];
ssize_t read_bytes;
//ET模式必须一次性读完
while(true) {
memset(raw_buf,0,sizeof(raw_buf));
read_bytes=read(client->client_fd,raw_buf,BUFFER_SIZE-1);//非阻塞read
if(read_bytes > 0) {
//读取成功
client->read_buffer.assign(raw_buf,read_bytes);
std::cout << "收到客户端" << "[IP:" << client->ip << ",端口:" << client->port
<< "]的信息" << client->read_buffer << std::endl;
//回声
client->write_buffer=client->read_buffer;
//注册写事件
add_or_modify(epoll_fd,client->client_fd,EPOLLIN | EPOLLOUT | EPOLLET,client);
} else if(read_bytes == 0) {
//正常关闭
print_client_info(client,"客户端正常关闭连接");
remove(epoll_fd,client->client_fd);
close(client->client_fd);//这一步是否必要?
delete client;
return;
} else {
//读取失败
if(errno == EAGAIN || errno == EWOULDBLOCK) {
//非阻塞read情况下数据已经读完
break;//读完就退出循环
} else {
//客户端出现了异常断开
std::cerr << "客户端连接异常断开" << std::strerror(errno) << std::endl;
remove(epoll_fd,client->client_fd);
close(client->client_fd);
delete client;
return;
}
}
}
}
//处理客户端写事件
void handle_write_event(ClientData* client,int epoll_fd) {
const char* data=client->write_buffer.data();
size_t already_written=0;
size_t data_len=client->write_buffer.size();
ssize_t write_bytes=0;
//循环发送,因为ET必须一次性写完数据
while(already_written < data_len) {
write_bytes=write(client->client_fd,data+already_written,data_len-already_written);
if(write_bytes > 0) {
already_written+=write_bytes;
} else if(write_bytes == 0) {
//写入0字节,无意义,退出循环
break;
} else {
//写入失败
if(errno == EAGAIN || errno == EWOULDBLOCK) {
//数据暂时写不完,等到下次触发事件再写
break;
} else {
std::cerr << "写入数据失败" << std::strerror(errno) << std::endl;
remove(epoll_fd,client->client_fd);
close(client->client_fd);
delete client;
return;
}
}
}
//数据全部写完,清空两个缓冲区,取消写事件
if(already_written == data_len) {
std::cout << "向客户端[IP:" << client->ip << ",端口:" << client->port << "]"
<< "发送回声消息成功" << std::endl;
client->read_buffer.clear();
client->write_buffer.clear();
//取消写事件,保留读事件
add_or_modify(epoll_fd,client->client_fd,EPOLLIN | EPOLLET,client);
}
}
//初始化服务器
int init_server() {
//1.创建socket
int server_fd=socket(AF_INET,SOCK_STREAM,0);
if(server_fd < 0) {
throw std::system_error(errno,std::generic_category(),"创建监听socket失败");
}
//2.绑定地址
struct sockaddr_in server_addr{};
server_addr.sin_family=AF_INET;
server_addr.sin_addr.s_addr=INADDR_ANY;//监听所有网卡
server_addr.sin_port=htons(PORT);
if(bind(server_fd,(sockaddr*)(&server_addr),sizeof(server_addr)) < 0) {
throw std::system_error(errno,std::generic_category(),"绑定地址与端口失败");
}
//3.监听
if(listen(server_fd,128) == -1) {
throw std::system_error(errno,std::generic_category(),"监听失败");
}
//4.设置为非阻塞
set_no_block(server_fd);
std::cout << "服务器初始化成功,监听端口" << PORT << std::endl;
return server_fd;
}
int main() {
try {
//1.初始化服务器
int server_fd=init_server();
//2.创建epoll实例
int epoll_fd=epoll_create1(0);
if(epoll_fd < 0) {
throw std::system_error(errno,std::generic_category(),"创建epoll实例失败");
}
//3.注册server_fd
auto server_data=std::make_unique<ClientData>();
server_data->client_fd=server_fd;
add_or_modify(epoll_fd,server_fd,EPOLLIN | EPOLLET,server_data.release());
//4.循环等待epoll事件
struct epoll_event events[MAX_EVENTS];//存储就绪事件
while(true) {
int ready_events_cnt=epoll_wait(epoll_fd,events,MAX_EVENTS,EPOLL_TIMEOUT);
if(ready_events_cnt == -1) {
if(errno == EINTR) {
//信号被中断,继续循环
continue;
} else {
throw std::system_error(errno,std::generic_category(),"epoll_wait失败");
}
}
//遍历所有就绪事件
for(size_t i=0;i<ready_events_cnt;i++) {
ClientData* data=static_cast<ClientData*>(events[i].data.ptr);
int fd=data->client_fd;
//1.如果是监听socket的事件
if(fd == server_fd) {
//说明监听socket关注的事件(是否有新连接)有了结果
handle_new_connection(server_fd,epoll_fd);
} else {
if(events[i].events && EPOLLIN) {
//如果关注读事件
handle_read_event(data,epoll_fd);
} else if(events[i].events && EPOLLOUT) {
//如果关注写事件
handle_write_event(data,epoll_fd);
}
}
}
}
close(server_fd);
close(epoll_fd);
} catch(const std::exception& e) {
std::cerr << "服务器异常退出" << e.what() <<std::endl;
return 1;
}
return 0;
}
从main函数中我们可以捋清整体思路:
1.初始化服务器(创建socket、绑定地址与端口、开始监听)
2.创建epoll实例并注册server_fd,表示我们关注server_fd(我们关心它是否accept成功)
3.创建events数组,用来存放已就绪的事件
4.调用epoll_wait,等待内核将已就绪的事件拷贝到events数组中
5.遍历events数组:如果已就绪事件的fd恰为server_fd,意味着有新连接;如果不是,就看该事件关心的是读事件还是写事件,分别进行处理即可
一开始我们只注册了server_fd,刚开始events中也只有server_fd(如果有新连接),之后才会调用handle_new_connection,注册新的文件描述符。
几个点:
1.fcntl
fcntl(file control)是Linux系统调用,专门用于修改或查询文件描述符的属性
fcntl(fd,F_GETFL,0);
cmd=F_GETFL,全称Get File Status Flags,作用是读取当前fd的状态标志。第三个参数为0,因为F_GETFL不需要额外参数。返回值是一个整数,每个比特位代表一个状态标志,例如O_NONBLOCK是非阻塞标志
fcntl(fd,F_SETFL,flags | O_NONBLOCK)
显然,这是在设置fd的状态,第三个参数表示新增O_NONBLOCK标志。之所以要用按位或而不是直接传入O_NONBLOCK,是因为这样可能会覆盖原有的标志,导致fd丢失原有属性。这也是为什么我们要先获取fd的状态再设置为非阻塞
2.epoll_event
它是epoll事件的载体,它告诉内核要监听哪个fd的什么事件,以及事件触发时返回什么数据。我们可以通过查看epoll_ctl进而查看epoll_event的字段:
man epoll_ctl
可以看到:
The event argument describes the object linked to the file descriptor
fd. The struct epoll_event is defined as:
typedef union epoll_data {
void *ptr;
int fd;
uint32_t u32;
uint64_t u64;
} epoll_data_t;
struct epoll_event {
uint32_t events; /* Epoll events */
epoll_data_t data; /* User data variable */
};
在add_or_modify函数中,我们先对它进行了初始化,即调用了memset函数,避免结构体中未定义的垃圾值导致监听错误事件
3.add_or_modify函数
先尝试修改,如果发现不存在就再注册。这样比较省事。因为同一个fd可能需要多次更新监听事件,比如读事件触发之后要添加写事件来发送回声数据,如果每次都先判断是否注册就比较麻烦
4.epoll_ctl
它的作用是修改epoll实例中的事件列表,原型:
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
epfd:epoll实例的文件描述符
op:操作类型:EPOLL_CTL_ADD为添加,EPOLL_CTL_MOD为修改,EPOLL_CTL_DEL为删除
fd:要操作的客户端/服务器fd
event:要注册/更新的事件结构体
也就是说,当我们调用epoll_ctl时,相当于告诉内核:我要监听这个fd的EPOLLIN(读)+EPOLLET(边缘触发)事件,并且把这个ClientData数据和它绑定。后续调用epoll_wait时,内核就会把就绪的事件填充到events数组中,相当于告诉我们:fd=xxx触发了EPOLLIN事件,这是你之前绑定的ClientData数据
5.accept4与accept
accept4是accept的加强版,accept4可以在接收新连接时直接设置新客户端fd的属性,避免额外调用fcntl,减少系统调用开销。
两者的函数原型:
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
int accept4(int sockfd, struct sockaddr *addr, socklen_t *addrlen, int flags);
新增的这个flags参数允许我们在接受新连接的同时设置新fd的属性,无需后续调用fcntl
// accept(两步)
int client_fd = accept(listen_fd, &addr, &addrlen);
fcntl(client_fd, F_SETFL, O_NONBLOCK); //额外调用 fcntl
// accept4(一步)
int client_fd = accept4(listen_fd, &addr, &addrlen, SOCK_NONBLOCK);
在高并发场景下,如果用accept,每个连接需要2次系统调用,而accept4只需要1次,相当于把系统调用开销减半。而且accept4是原子操作,可以规避极端条件下的竞态条件。
6.inet_ntoa与inet_ntop
二者都能将IPv4地址从二进制格式转换为字符串格式
#include <arpa/inet.h> //必需头文件
char *inet_ntoa(struct in_addr in);
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);
可以看到inet_ntop的dst参数是用户提供的缓冲区。这也意味着,inet_ntoa是线程不安全的,因为它使用的是静态缓冲区,多线程调用会覆盖结果。而inet_ntop使用的是用户提供的缓冲区,不会相互覆盖,不过相对复杂一点。
7.release()、reset()、get()、move()
add_or_modify(epoll_fd,client_fd,EPOLLIN | EPOLLET,client.release())
std::unique_ptr是独占式智能指针,即同一时间只能有一个unique_ptr管理同一个对象,所有权不能复制,只能移动。它的核心作用是自动管理内存:当unique_ptr超出作用域,如函数返回、被销毁等,会自动调用delete销毁所管理的对象 。
而在我们的代码中,client管理的ClientData对象需要生命周期独立于unique_ptr,因为客户端连接存活期间ClientData必须存在,所以不能让unique_ptr自动销毁对象。这就需要release()
release()做了两件事:返回unique_ptr管理对象的裸指针、让uniuqe_ptr本身失去对对象的所有权。也就是说在调用release()之后,client会变成空指针,不再管理任何对象。这个裸指针被传递给add_or_modify函数,在函数中绑定到epoll_event的data.ptr,直到客户端断开后,我们才手动delete该裸指针。
❓如果不用release(),直接传递client,会如何?
答:一方面,unique_ptr不可复制,add_or_modify的参数是ClientData*,是裸指针,无法直接传递unique_ptr;另一方面,若强行传递&(*client),unique_ptr仍然拥有所有权,超出作用域时会自动delete对象,导致后续data.ptr变成野指针,访问崩溃
❓如果使用std::move()会怎样?
答:std::move()转移所有权,对象被新的智能指针管理。而参数需要的是裸指针,且对象还是自动管理的,局部智能指针超出作用域后还是会自动delete对象。
比喻:你用一个盒子装了一瓶红酒,这个红酒就是对象,盒子就是unique_ptr,用来保护这个对象。release()将智能指针转为裸指针,相当于把这个盒子拆掉,整瓶红酒裸露在外面。刚好我们的add_or_modify就是要求拆掉包装,只留红酒。拆掉盒子后,盒子变为空,后面会自动销毁。而std::move是将这个红酒从一个盒子拿出来,再放到另一个盒子里,再传递给add_or_modify,add_or_modify不收,就算强行让它收也不行。因为还是在用智能指针管理,函数结束之后盒子+红酒都会被销毁。
使用release()避免了智能指针自动销毁导致野指针的问题,后续自己手动delete即可
总结一下:
release():放弃所有权,返回裸指针,不销毁对象
reset():放弃所有权,且销毁对象,unique_ptr变为空
std::move():将unique_ptr的所有权移动给另一个unique_ptr,原unique_ptr变为空,对象仍被管理,不产生裸指针
get():不放弃所有权,返回裸指针。
可见,release()是永久交出对象控制权,让对象脱离智能指针管理。而get()用于临时访问对象,不改变所有权
进行简单的压测:
for i in {1..100}; do (while true; do echo "test"; sleep 0.001; done) | nc 127.0.0.1 8080 > /dev/null 2>&1 & done; sleep 30; pkill -f "nc 127.0.0.1 8080" && echo "压测结束!"
解释一下这个语句:
1.for i in {1..100}; do ... done -> 循环创建100个并发连接
重复执行大括号里的内容100次,相当于你手动打开了100个终端,每个终端都执行一次连接服务器+发数据的操作。
2.(while true; do echo "test"; sleep 0.001; done)
小括号:让里面的命令在后台子进程中运行,即这100个连接互不干扰
echo "test":发送字符串"test"给服务器。即模拟客户端发数据,你的服务器会回声返回
sleep 0.001:每次发完数据,暂停1毫秒,避免发得太快把服务器冲垮
3.| nc 127.0.0.1 8080 -> 把数据通过TCP发给服务器
|(管道符):把前面echo "test"输出的内容传给后面的nc命令
nc 127.0.0.1 8080:建立TCP连接到127.0.0.1:8080,也就是我们的服务器地址。并将前面传来的"test"字符串发给服务器,接收服务器的回声响应
4.> /dev/null 2>&1 -> 屏蔽冗余输出
> /dev/null:让服务器返回的回声数据不显示在终端上
2>&1:把nc命令的错误信息,比如连接断开的提示也一起屏蔽,让终端只显示关键信息
5.&+sleep 30+pkill ...
每个循环末尾的&:让这个连接在后台运行,不阻塞下一个连接的创建,这使得100个连接瞬间同时建立,而不是排队
sleep 30:所有连接创建完后,等待30秒,即压测时长为30秒
pkill -f "nc 127.0.0.1 8080":30秒后,强制关闭所有和服务器连接的nc进程
&& echo "压测结束!" :清理完后,终端提示“压测结束”
在进行压测之前,我们可以在终端输入top并回车查看CPU使用率。根据CPU使用率可以提高并发数量。
1124

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



