使用 libevent 和 libev 提高网络应用性能——I/O模型演进变化史

本文深入探讨了如何使用libevent和libev库来提高网络应用性能,详细介绍了I/O模型的发展,从阻塞网络接口到非阻塞模型,再到select、poll和epoll事件驱动服务器模型。libevent和libev作为高性能事件库,通过事件驱动和I/O多路复用技术,降低了资源占用,提高了服务响应能力。文章对比了libevent和libev的特点与优势,强调了它们在处理大量并发连接时的优势,并提供了使用示例,展示了如何构建基于这些库的事件驱动服务器模型。
摘要由CSDN通过智能技术生成

分享一下我老师大神的人工智能教程!零基础,通俗易懂!http://blog.csdn.net/jiangjunshow

也欢迎大家转载本篇文章。分享知识,造福人民,实现我们中华民族伟大复兴!

               

        构建现代的服务器应用程序需要以某种方法同时接收数百、数千甚至数万个事件,无论它们是内部请求还是网络连接,都要有效地处理它们的操作。

     有许多解决方案,但事件驱动也被广泛应用到网络编程中。并大规模部署在高连接数高吞吐量的服务器程序中,如 http 服务器程序、ftp 服务器程序等。相比于传统的网络编程方式,事件驱动能够极大的降低资源占用,增大服务接待能力,并提高网络传输效率。

       这些事件驱动模型中, libevent 库和 libev库能够大大提高性能和事件处理能力。在本文中,我们要讨论在 UNIX/Linux 应用程序中使用和部署这些解决方案所用的基本结构和方法。libev 和 libevent 都可以在高性能应用程序中使用。

      在讨论libev 和 libevent之前,我们看看I/O模型演进变化历史

1、阻塞网络接口:处理单个客户端

      我们  第一次接触到的网络编程一般都是从  listen() 、 send() 、 recv() 等接口开始的。使用这些接口可以很方便的构建服务器  / 客户机的模型。

       阻塞I/O模型图:在调用recv()函数时,发生在内核中等待数据和复制数据的过程。


     当调用recv()函数时,系统首先查是否有准备好的数据。如果数据没有准备好,那么系统就处于等待状态。当数据准备好后,将数据从系统缓冲区复制到用户空间,然后该函数返回。在套接应用程序中,当调用recv()函数时,未必用户空间就已经存在数据,那么此时recv()函数就会处于等待状态。

       我们注意到,大部分的 socket 接口都是阻塞型的。所谓阻塞型接口是指系统调用(一般是 IO 接口)不返回调用结果并让当前线程一直阻塞,只有当该系统调用获得结果或者超时出错时才返回。

     实际上,除非特别指定,几乎所有的 IO 接口 ( 包括 socket 接口 ) 都是阻塞型的。这给网络编程带来了一个很大的问题,如在调用 send() 的同时,线程将被阻塞,在此期间,线程将无法执行任何运算或响应任何的网络请求。这给多客户机、多业务逻辑的网络编程带来了挑战。这时,很多程序员可能会选择多线程的方式来解决这个问题。

  使用阻塞模式的套接字,开发网络程序比较简单,容易实现。当希望能够立即发送和接收数据,且处理的套接字数量比较少的情况下,即一个一个处理客户端,服务器没什么压力,使用阻塞模式来开发网络程序比较合适。

      阻塞模式给网络编程带来了一个很大的问题,如在调用 send()的同时,线程将被阻塞,在此期间,线程将无法执行任何运算或响应任何的网络请求。如果很多客户端同时访问服务器,服务器就不能同时处理这些请求。这时,我们可能会选择多线程的方式来解决这个问题。

2、多线程/进程处理多个客户端

         应对多客户机的网络应用,最简单的解决方式是在服务器端使用多线程(或多进程)。多线程(或多进程)的目的是让每个连接都拥有独立的线程(或进程),这样任何一个连接的阻塞都不会影响其他的连接。

       具体使用多进程还是多线程,并没有一个特定的模式。传统意义上,进程的开销要远远大于线程,所以,如果需要同时为较多的客户机提供服务,则不推荐使用多进程;如果单个服务执行体需要消耗较多的 CPU 资源,譬如需要进行大规模或长时间的数据运算或文件访问,则进程较为安全。通常,使用 pthread_create () 创建新线程,fork() 创建新进程。即:

     (1)    a new Connection 进来,用 fork() 产生一个 Process 处理。 
     (2)   a new Connection 进来,用 pthread_create() 产生一个 Thread 处理。 

      多线程/进程服务器同时为多个客户机提供应答服务。模型如下:

     

       

    主线程持续等待客户端的连接请求,如果有连接,则创建新线程,并在新线程中提供为前例同样的问答服务。

#include <stdio.h>#include <stdlib.h>#include <string.h>      #include <unistd.h>#include <sys/socket.h>#include <netinet/in.h>#include <arpa/inet.h> void do_service(int conn);void err_log(string err, int sockfd) { perror("binding"); close(sockfd);  exit(-1);}   int main(int argc, char *argv[])unsigned short port = 8000;  int sockfd; sockfd = socket(AF_INET, SOCK_STREAM, 0);// 创建通信端点:套接字 if(sockfd < 0) {  perror("socket");  exit(-1); }  struct sockaddr_in my_addr; bzero(&my_addr, sizeof(my_addr));       my_addr.sin_family = AF_INET; my_addr.sin_port   = htons(port); my_addr.sin_addr.s_addr = htonl(INADDR_ANY);  int err_log = bind(sockfd, (struct sockaddr*)&my_addr, sizeof(my_addr)); if( err_log != 0)   err_log("binding"); err_log = listen(sockfd, 10); if(err_log != 0) err_log("listen");  struct sockaddr_in peeraddr; //传出参数    socklen_t peerlen = sizeof(peeraddr); //传入传出参数,必须有初始值    int conn; // 已连接套接字(变为主动套接字,即可以主动connect) pid_t pid;    while (1) {        if ((conn = accept(sockfd, (struct sockaddr *)&peeraddr, &peerlen)) < 0) //3次握手完成的序列            err_log("accept error");        printf("recv connect ip=%s port=%d/n", inet_ntoa(peeraddr.sin_addr),ntohs(peeraddr.sin_port));        pid = fork();        if (pid == -1)            err_log("fork error");        if (pid == 0) {
   // 子进程            close(listenfd);            do_service(conn);            exit(EXIT_SUCCESS);        }        else            close(conn); //父进程    } return 0;} void do_service(int conn) {    char recvbuf[1024];    while (1)  {        memset(recvbuf, 0, sizeof(recvbuf));        int ret = read(conn, recvbuf, sizeof(recvbuf));        if (ret == 0)    { //客户端关闭了            printf("client close/n");            break;        }        else if (ret == -1)            ERR_EXIT("read error");        fputs(recvbuf, stdout);        write(conn, recvbuf, ret);    }}


   很多初学者可能不明白为何一个 socket 可以 accept 多次。实际上,socket 的设计者可能特意为多客户机的情况留下了伏笔,让 accept() 能够返回一个新的 socket。下面是 accept 接口的原型:

int accept(int s, struct sockaddr *addr, socklen_t *addrlen);

     输入参数 s 是从 socket(),bind() 和 listen() 中沿用下来的 socket 句柄值。执行完 bind() 和 listen() 后,操作系统已经开始在指定的端口处监听所有的连接请求,如果有请求,则将该连接请求加入请求队列。调用 accept() 接口正是从 socket s 的请求队列抽取第一个连接信息,创建一个与 s 同类的新的 socket 返回句柄。新的 socket 句柄即是后续 read() 和 recv() 的输入参数。如果请求队列当前没有请求,则 accept() 将进入阻塞状态直到有请求进入队列。

     上述多线程的服务器模型似乎完美的解决了为多个客户机提供问答服务的要求,但其实并不尽然。如果要同时响应成百上千路的连接请求,则无论多线程还是多进程都会严重占据系统资源,降低系统对外界响应效率,而线程与进程本身也更容易进入假死状态。

      因此其缺点:

     1)用 fork() 的问题在于每一个 Connection 进来时的成本太高,如果同时接入的并发连接数太多容易进程数量很多,进程之间的切换开销会很大,同时对于老的内核(Linux)会产生雪崩效应。 

      2)用 Multi-thread 的问题在于 Thread-safe 与 Deadlock 问题难以解决,另外有 Memory-leak 的问题要处理,这个问题对于很多程序员来说无异于恶梦,尤其是对于连续服务器的服务器程序更是不可以接受。 如果才用 Event-based 的方式在于实做上不好写,尤其是要注意到事件产生时必须 Nonblocking,于是会需要实做 Buffering 的问题,而 Multi-thread 所会遇到的 Memory-leak 问题在这边会更严重。而在多 CPU 的系统上没有办法使用到所有的 CPU resource。 

       由此可能会考虑使用“线程池”或“连接池”。“线程池”旨在减少创建和销毁线程的频率,其维持一定合理数量的线程,并让空闲的线程重新承担新的执行任务。“连接池”维持连接的缓存池,尽量重用已有的连接、减少创建和关闭连接的频率。这两种技术都可以很好的降低系统开销,都被广泛应用很多大型系统,如apache,mysql数据库等。

      但是,“线程池”和“连接池”技术也只是在一定程度上缓解了频繁调用 IO 接口带来的资源占用。而且,所谓“池”始终有其上限,当请求大大超过上限时,“池”构成的系统对外界的响应并不比没有池的时候效果好多少。所以使用“池”必须考虑其面临的响应规模,并根据响应规模调整“池”

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值