服务器应用程序可以通过读取和写入 Socket 对象
#define MAX_FD 65536 // 最大的文件描述符个数
#define MAX_EVENT_NUMBER 10000 // 监听的最大的事件数量
// 添加信号捕捉
void addsig(int sig, void( handler )(int)){//信号处理函数
struct sigaction sa;//创建信号量
memset( &sa, '\0', sizeof( sa ) );
sa.sa_handler = handler;
sigfillset( &sa.sa_mask );//设置信号临时阻塞等级
assert( sigaction( sig, &sa, NULL ) != -1 );//注册信号
}
int main( int argc, char* argv[] ) {
// 判断参数个数,至少要传递一个端口号
if( argc <= 1 ) {
printf( "usage: %s port_number\n", basename(argv[0]));
return 1;
}
// 获取端口号,转换成整数
int port = atoi( argv[1] );
addsig( SIGPIPE, SIG_IGN );
// 使用socketAPI编写Reactor组件,通过监听socket文件描述符获取连接请求
int listenfd = socket( PF_INET, SOCK_STREAM, 0 ); // 创建用于监听的socket文件描述符
int ret = 0;
// 存放服务器的地址信息
struct sockaddr_in address;
address.sin_family = AF_INET;//使用IPv4协议
address.sin_addr.s_addr = INADDR_ANY; //监听所有网卡的连接请求
address.sin_port = htons( port );//将端口号(大端小端)转换为网络字节序,并保存到address结构体中
// 端口复用
int reuse = 1;
// 让多个进程绑定同一个端口,从而实现负载均衡或者高可用等功能
setsockopt( listenfd, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof( reuse ) );
// 绑定服务器的地址信息
ret = bind( listenfd, ( struct sockaddr* )&address, sizeof( address ) );
ret = listen( listenfd, 5 );
...
return 0;
}
// 创建线程池,并初始化
// 来一个任务之后,要封装成一个任务对象,交给线程池去处理
threadpool< http_conn >* pool = NULL;
try {
pool = new threadpool;
} catch( ... ) {
return 1;
}
// 创建一个数组用于保存所有的客户端信息
// 每当有新连接进来时,都会在 users 数组中找到一个未使用的 http_conn 对象,
// 进行初始化并保存该连接对应的信息
http_conn* users = new http_conn[ MAX_FD ];
// 创建epoll对象,和事件数组,添加
epoll_event events[ MAX_EVENT_NUMBER ];
int epollfd = epoll_create( 5 );//创建epoll对象,通过该文件描述符对 epoll 进行控制和管理(监听)
// 将监听的文件描述符添加到 epoll 对象中
addfd( epollfd, listenfd, false );
http_conn::m_epollfd = epollfd;//赋值
// 添加文件描述符到 epoll 中
extern void addfd( int epollfd, int fd, bool one_shot );
// 向epoll中添加需要监听的文件描述符
void addfd( int epollfd, int fd, bool one_shot ) {
epoll_event event;
event.data.fd = fd;
event.events = EPOLLIN | EPOLLRDHUP;
if(one_shot)
{
// 防止同一个通信被不同的线程处理
event.events |= EPOLLONESHOT;
}
epoll_ctl(epollfd, EPOLL_CTL_ADD, fd, &event);
// 设置文件描述符非阻塞
setnonblocking(fd);
}
// 创建epoll实例,通过一棵红黑树管理待检测集合
// 参数 size 从 Linux 2.6.8 以后就不再使用,但是必须设置一个大于 0 的值。epoll_create 函数调用成功返回一个非负值的 epollfd,调用失败返回 -1。
int epoll_create(int size);
// 对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 : 要检测的文件描述符
- event : 检测文件描述符什么事情
// 检测函数----检测epoll树中是否有就绪的文件描述符
// 创建了epfd,设置好某个fd上需要检测事件并将该fd绑定到epfd上去后,就可以调用epoll_wait
// 检测事件了
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
- 参数:
- epfd : epoll实例对应的文件描述符
- events : 传出参数,保存了发送了变化的文件描述符的信息
- maxevents : 第二个参数结构体数组的大小
- timeout : 阻塞时间
- 0 : 不阻塞
- -1 : 阻塞,直到检测到fd数据发生变化,解除阻塞
- > 0 : 阻塞的时长(毫秒)
- 返回值:
- 成功,返回发送变化的文件描述符的个数 > 0
- 失败 -1
struct epoll_event {
uint32_t events; /* Epoll events */
epoll_data_t data; /* User data variable */
};
events描述事件类型,其中epoll事件类型有以下几种
- EPOLLIN:表示对应的文件描述符可以读(包括对端SOCKET正常关闭)
- EPOLLOUT:表示对应的文件描述符可以写
- EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来)
- EPOLLERR:表示对应的文件描述符发生错误
- EPOLLHUP:表示对应的文件描述符被挂断;
- EPOLLET:将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)而言的
- EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里
epoll() 多路复用 和 两种工作模式_呵呵哒( ̄▽ ̄)"的博客-CSDN博客https://blog.csdn.net/weixin_41987016/article/details/132523789?spm=1001.2014.3001.5501
epoll 基于多线程的边沿非阻塞处理_呵呵哒( ̄▽ ̄)"的博客-CSDN博客https://blog.csdn.net/weixin_41987016/article/details/132539393?spm=1001.2014.3001.5501
// 同步 I/O 模拟 Proactor 模式的工作流程
while(true){
/*
1、阻塞等待文件描述符监听到的事件
2、遍历事件数组,判断事件类型,进行对应处理
*/
}
// 模拟 Proactor 模式
while(true) {
// 具体来说就是使用epoll_wait获取监听socket的文件描述符所返回得到事件数量
int number = epoll_wait( epollfd, events, MAX_EVENT_NUMBER, -1 );
if ( ( number < 0 ) && ( errno != EINTR ) ) {
printf( "epoll failure\n" );
break;
}
// 循环遍历事件数组
for ( int i = 0; i < number; i++ ) {
int sockfd = events[i].data.fd;
if( sockfd == listenfd ) {
// 有客户端连接进来
...
}else if( events[i].events & ( EPOLLRDHUP | EPOLLHUP | EPOLLERR ) ) {
// 对方异常断开或错误异常
...
}else if(events[i].events & EPOLLIN) {
// 判断是否有读事件发生
...
} else if( events[i].events & EPOLLOUT ) {
// 判断是否有写事件发生
...
}
}
}
**************************************************************************************************************
以下总结来自这篇文章:【从0开始编写webserver·基础篇#02】服务器的核心---I/O处理单元和任务类 - dayceng - 博客园 (cnblogs.com)
epollfd 代表了一个 epoll实例,负责管理需要监视的文件描述符集合,而 listenfd 则是需要被监视的文件描述符之一,它被添加到 epollfd 所管理的文件描述符集合中,以便在有新的客户端连接请求时能够及时通知服务器程序。当 epoll_wait 函数返回时,它会将事件列表填入events数组中,告诉服务器哪些文件描述符发生了I/O事件,然后服务器应用程序根据这些事件来执行相应的操作。
**************************************************************************************************************
if( sockfd == listenfd ) {
// 有客户端连接进来
struct sockaddr_in client_address;
socklen_t client_addrlength = sizeof( client_address );
int connfd = accept( listenfd, ( struct sockaddr* )&client_address, &client_addrlength );
if ( connfd < 0 ) {
printf( "errno is: %d\n", errno );
continue;
}
if( http_conn::m_user_count >= MAX_FD ) {
// 目前连接满了
printf("服务器正忙...\n");
close(connfd);
continue;
}
// 将新的客户的数据初始化,放到数组中
users[connfd].init( connfd, client_address);
}
// 对方异常断开或错误异常
else if( events[i].events & ( EPOLLRDHUP | EPOLLHUP | EPOLLERR ) ) {
// http_conn类型的users数组中该客户端的状态设置为关闭
users[sockfd].close_conn();
}
else if(events[i].events & EPOLLIN) {
// 判断是否有读事件发生
if(users[sockfd].read()) {// 一次性读取完所有请求数据,read()
// 成功读完后要交给工作线程处理
// 调用线程池,追加任务
// 线程池执行 run 函数,不断从队列去取
// 取到就做业务处理,解析、生成响应数据
pool->append(users + sockfd);
} else {//读失败,关闭
users[sockfd].close_conn();
}
}
else if( events[i].events & EPOLLOUT ) {
// 将响应数据发送给客户端,若发送成功则继续等待下一个写事件发生,
// 否则关闭该链接
if( !users[sockfd].write() ) {
users[sockfd].close_conn();
}
}
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include "locker.h"
#include "threadpool.h"
#include "http_conn.h"
#define MAX_FD 65536 // 最大的文件描述符个数
#define MAX_EVENT_NUMBER 10000 // 监听的最大的事件数量
/*
自定义函数 addfd,需要把监听的文件描述符 listenfd 添加到 epoll 对象中,
即将它加入到内核事件表中
*/
// 添加文件描述符到 epoll 中
extern void addfd( int epollfd, int fd, bool one_shot );
extern void removefd( int epollfd, int fd );
// 添加信号捕捉
void addsig(int sig, void( handler )(int)){//信号处理函数
struct sigaction sa;//创建信号量
memset( &sa, '\0', sizeof( sa ) );
sa.sa_handler = handler;
sigfillset( &sa.sa_mask );//设置信号临时阻塞等级
assert( sigaction( sig, &sa, NULL ) != -1 );//注册信号
}
/*
模拟 proactor 模式,主线程监听事件
当有读事件产生,在主线程中一次性读出来,封装成一个任务对象(用任务类)
然后交给子线程(线程池队列中的工作线程),线程池再去取任务做任务
*/
int main( int argc, char* argv[] ) {
// 判断参数个数,至少要传递一个端口号
if( argc <= 1 ) {
printf( "usage: %s port_number\n", basename(argv[0]));
return 1;
}
// 获取端口号,转换成整数
int port = atoi( argv[1] );
addsig( SIGPIPE, SIG_IGN );
// 创建线程池,并初始化
// 来一个任务之后,要封装成一个任务对象,交给线程池去处理
threadpool< http_conn >* pool = NULL;
try {
pool = new threadpool;
} catch( ... ) {
return 1;
}
// 创建一个数组用于保存所有的客户端信息
// 每当有新连接进来时,都会在 users 数组中找到一个未使用的 http_conn 对象,进行初始化并保存该连接对应的信息
http_conn* users = new http_conn[ MAX_FD ];
// 使用socketAPI编写Reactor组件,通过监听socket文件描述符获取连接请求
int listenfd = socket( PF_INET, SOCK_STREAM, 0 ); // 创建用于监听的socket文件描述符
int ret = 0;
// 存放服务器的地址信息
struct sockaddr_in address;
address.sin_family = AF_INET;//使用IPv4协议
address.sin_addr.s_addr = INADDR_ANY; //监听所有网卡的连接请求
address.sin_port = htons( port );//将端口号(大端小端)转换为网络字节序,并保存到address结构体中
// 端口复用
int reuse = 1;
setsockopt( listenfd, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof( reuse ) );//让多个进程绑定同一个端口,从而实现负载均衡或者高可用等功能
// 绑定服务器的地址信息
ret = bind( listenfd, ( struct sockaddr* )&address, sizeof( address ) );
ret = listen( listenfd, 5 );
// 创建epoll对象,和事件数组,添加
epoll_event events[ MAX_EVENT_NUMBER ];
int epollfd = epoll_create( 5 );//创建epoll对象,通过该文件描述符对 epoll 进行控制和管理(监听)
// 将监听的文件描述符添加到 epoll 对象中
addfd( epollfd, listenfd, false );
http_conn::m_epollfd = epollfd;//赋值
// 编写Reactor组件
while(true) {
// 具体来说就是使用epoll_wait获取监听socket的文件描述符所返回得到事件数量
int number = epoll_wait( epollfd, events, MAX_EVENT_NUMBER, -1 );
if ( ( number < 0 ) && ( errno != EINTR ) ) {
printf( "epoll failure\n" );
break;
}
// 循环遍历事件数组
for ( int i = 0; i < number; i++ ) {
int sockfd = events[i].data.fd;
if( sockfd == listenfd ) {
// 有客户端连接进来
struct sockaddr_in client_address;
socklen_t client_addrlength = sizeof( client_address );
int connfd = accept( listenfd, ( struct sockaddr* )&client_address, &client_addrlength );
if ( connfd < 0 ) {
printf( "errno is: %d\n", errno );
continue;
}
if( http_conn::m_user_count >= MAX_FD ) {
// 目前连接满了
printf("服务器正忙...\n");
close(connfd);
continue;
}
// 将新的客户的数据初始化,放到数组中
users[connfd].init( connfd, client_address);
} else if( events[i].events & ( EPOLLRDHUP | EPOLLHUP | EPOLLERR ) ) {
// 对方异常断开或错误异常
users[sockfd].close_conn();
} else if(events[i].events & EPOLLIN) {
// 判断是否有读事件发生
if(users[sockfd].read()) {// 一次性读出数据,read()
// 成功读完后要交给工作线程处理
// 调用线程池,追加任务
// 线程池执行 run 函数,不断从队列去取
// 取到就做业务处理,解析、生成响应数据
pool->append(users + sockfd);
} else {//读失败,关闭
users[sockfd].close_conn();
}
} else if( events[i].events & EPOLLOUT ) {
if( !users[sockfd].write() ) {
users[sockfd].close_conn();
}
}
}
}
close( epollfd );
close( listenfd );
delete [] users;
delete pool;
return 0;
}
推荐和参考此文章:
【从0开始编写webserver·基础篇#02】服务器的核心---I/O处理单元和任务类 - dayceng - 博客园 (cnblogs.com)