3.epoll登场,单线程也可服务多用户

要想用单线程实现并发服务器,也是可行,这时就需要依靠epoll了。epoll是IO多路复用的其中一种方法,其他的还有select,poll。

这里主要讲解epoll。

多路复用的“多路”的是多个网络连接,“复用”指的是复用同一线程。

一般情况,epoll的性能是优于select,poll很多的,epoll是只能使用在linux环境的。

首先回顾下多线程并发的处理流程:

主线程:

调用accept()去检测客户端是否有连接请

        1)若是有新的客户端连接请求,就建立连接

        2)若是无新客户端的连接请求,就阻塞

子线程:

调用read()或write()函数和客户端进行收发数据。

epoll处理流程:

使用epoll_wait()函数去委托内核检测服务器端所有的文件描述符(主要是监听和通信两类),这个检测过程会导致线程阻塞,若检测到有已就绪的文件描述符,那阻塞就解除,并将这些已就绪的文件描述符通过一个结构体传出来。

接着对传出的文件描述符进行判断:

1)若是监听的文件描述符,就调用accept()建立连接,此时不会阻塞,因为这文件描述符是已就绪的。

2)如是通信的文件描述符,就调用通信函数(read(),write())和已建立连接的客户端进行通信。

这样就可以在单线程的场景下实现服务器并发了。

epoll与多线程相比,其系统开销小,不必创建/销毁线程,也不用管理维护线程。

epoll其操作函数

// 创建epoll实例,并且通过一棵红黑树管理待检测集合
int epoll_create(int size);

// 管理红黑树上的文件描述符(添加、修改、删除)
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

// 检测epoll树中是否有就绪的文件描述符
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);

1.epoll_create() 

//创建一个红黑树模型的实例,用于管理待检测的文件描述符的集合。
int epoll_create(int size);
// 在 Linux 内核 2.6.8 版本以后,这个参数是被忽略的,只需要指定一个大于 0 的数值就可以了。
//返回值:失败返回 - 1;成功返回一个有效的文件描述符,通过这个文件描述符就可以访问创建的epoll 实例。

//使用
int epfd=epoll_create(1);

 2.epoll_ctl()

// 联合体, 多个变量共用同一块内存        
typedef union epoll_data {
 	void        *ptr;
	int          fd;	// 一般情况下使用这个成员, 和epoll_ctl的第三个参数相同
	uint32_t     u32;
	uint64_t     u64;
} epoll_data_t;

struct epoll_event {
	uint32_t     events;      /* Epoll events */
	epoll_data_t data;        /* User data variable */
};


//管理红黑树实例上的节点,可以进行添加、修改、删除操作。
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
//参数1是epoll_ctl()函数的返回值
//参数2是个枚举值,控制该函数执行操作
//参数3是我们要进行操作的文件描述符
//参数4,event是一个epoll_event结构体,其中的events表示事件,如EPOLLIN等
//函数返回值:失败返回-1,成功返回 0.

//使用例子
int ret=epoll_ctl(epfd, EPOLL_CTL_ADD, sockfd, &ev);    //添加事件到epoll
int ret=epoll_ctl(epfd, EPOLL_CTL_MOD, sockfd, &ev);    //修改epoll红黑树上的事件
int ret=epoll_ctl(epfd, EPOLL_CTL_DEL, sockfd, NULL);   //删除事件

 3.epoll_wait()

int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
//参数1是epoll_create () 函数的返回值,通过这个参数找到 epoll 实例
//参数2是传出参数,这是一个结构体数组的地址,里边存储了已就绪的文件描述符的信息
//参数3是修饰第二个参数,结构体数组的容量(元素个数)
//参数4是表示最大的等待时间,0:函数不阻;-1:函数一直阻塞
//返回值:成功返回检测到的已就绪的文件描述符的总个数,若是0表示没有检测到满足条件的文件描述符;
//        失败返回-1.

按照上面的讲解,用法如下

//伪代码
int lfd=sokcet();
int ret=bind();
ret=listen();

int epfd=epoll_create(1);    //创建epoll实例

struct epoll_event ev;
ev.data.fd=lfd;    //设置为服务器的监听文件描述符
ev.data.events=EPOLLIN;    //设置要检测的事件
int ret=epoll_ctl(epfd,EPOLL_CTL_ADD,lfd,&ev);//添加到epoll上

struct epoll_event events[1024];
while(1){
    int nums=epoll_wait(epfd,events,1024,-1);//进行检测
    for(int i=0;i

这是epoll的水平工作模式。epoll是默认采用LT触发模式,即水平触发,只要fd上有事件,就会一直通知内核。这样可以保证所有事件都得到处理、不容易丢失,但可能发生的大量重复通知也会影响epoll的性能。

而其还有另一种工作模式:边沿模式,简称为 ET 模式。

ET(edge-triggered)是高速工作方式,只支持no-block socket。在这种模式下,当文件描述符从未就绪变为就绪时,内核会通过epoll通知使用者。然后它会假设使用者知道文件描述符已经就绪,并且不会再为那个文件描述符发送更多的就绪通知(only once)。ET模式在很大程度上减少了epoll事件被重复触发的次数,其效率要比LT模式高,但编程相比水平模式相对复杂。

使用了IO多路复用,把socket设置为非阻塞最好,Linux 手册关于 select 的内容中有如下说明:

Under Linux, select() may report a socket file descriptor as "ready for reading",
 while nevertheless a subsequent read blocks. 
This could  for example  happen  when  data  has arrived but upon examination has wrong checksum and is discarded.  
There may be other circumstances in which a file descriptor is spuriously reported as ready. 
 Thus it may be safer to use O_NONBLOCK on sockets that should not block.

百度翻译结果:

在Linux下,select()可能会将套接字文件描述符报告为“ready for reading”(准备读取),但会报告后续的读取块。这可以用于当数据已经到达,但在检查时校验和错误并被丢弃时,就会发生这种情况。在其他情况下文件描述符被错误地报告为就绪。因此,在不应该阻塞的套接字上使用O_NONBLOCK可能更安全。

通俗点理解,就是epoll返回的事件不一定是可读写,若是使用了阻塞I/O,那在调用read()/write()时候则有可能发生阻塞,所以最好在使用I/O多路复用的时候搭配非阻塞socket。

按照上一节的用法,使用make命令编译后,打开多个终端,一个先输入./Server运行服务端,另几个终端再分别输入./client运行客户端。

完整源代码:https://github.com/liwook/CPPServer/tree/main/code/server_v3

你可能感兴趣的:(Linux服务器开发,服务器,网络,c++)