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<nums;i++){
        if(events[i].data.fd==lfd){    //是监听描述符
            int cfd=accept();        //建立连接        
            struct epoll_event ev;
            ev.data.fd = cfd;   
            ev.events = EPOLLIN ; 
            epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &ev);   
        }
        else if(events[i].events &&EPOLLIN){    //通信的
            handleEvent();        //处理函数
        }
    }
}

这是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

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
基于epoll和多线程的高并发服务器源码的主要思想是通过利用epoll事件驱动模型和多线程来处理并发连接。 以下是一个简单的示例代码: ```python import socket import select import threading # 处理客户端请求的线程 def handle_client(client): while True: data = client.recv(1024) if not data: break client.send(data) client.close() # 主线程监听连接,并将连接交给处理线程 def main(): server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) server.bind(('127.0.0.1', 8080)) server.listen(10) epoll = select.epoll() epoll.register(server.fileno(), select.EPOLLIN) connections = {} threads = {} while True: events = epoll.poll() for fileno, event in events: if fileno == server.fileno(): # 新连接 client, address = server.accept() connections[client.fileno()] = client epoll.register(client.fileno(), select.EPOLLIN) threads[client.fileno()] = threading.Thread(target=handle_client, args=(client,)) threads[client.fileno()].start() elif event & select.EPOLLIN: # 有数据可读 client = connections[fileno] threads[fileno].join() # 等待线程结束 del threads[fileno] epoll.unregister(fileno) del connections[fileno] client.close() if __name__ == '__main__': main() ``` 这段代码使用了select模块和epoll来进行事件管理和调度,在主线程中创建了一个套接字并监听端口。随后,将服务器套接字注册到epoll对象中,然后使用epoll的`poll()`方法监听事件。当有新连接到来时,主线程接受连接,并将连接套接字注册到epoll中,并创建一个新的线程来处理该连接的请求。每个线程读取客户端数据并发送回客户端,直到客户端断开连接。最后,线程结束,清理资源。 该源码可以实现简单的高并发服务器,能够同时处理多个客户端的连接请求,并且能够较好地利用系统资源。但值得注意的是,该示例代码仅提供了基本的框架,还需要根据实际需求进行完善和优化。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值