首页 > 其他 > 详细

I/O复用服务器

时间:2020-04-26 23:39:43      阅读:85      评论:0      收藏:0      [点我收藏+]

1、多进程服务器得缺点和解决方法 

  在多进程服务器中,我们看到了当有新的客户端请求时,服务端进程会创建一个子进程,用于处理和客户端的连接和处理客户端的请求。这是一种并发处理客户端请求的方案,但并不是一个很好的方案,因为创建进程时需要付出很大的代价,需要大量的运算和内存空间,由于每个进程都具有独立的内存空间,所以相互间的数据交换也要求采用相对复杂的方法(IPC属于相对复杂的通信方法)

   那么有没有其他的方案可以在不创建子进程的前提下可以并发处理客户端请求?当然是有的,那就是I/O复用技术了。I/O多路复用是通过一种机制,可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作 。

2、理解select函数并实现服务端 

  select函数可以将多个文件描述符集中到一起统一监视,具体监视的“事件”如下: 

  • 是否存在套接字接收数据?
  • 无需阻塞传输数据的套接字有哪些?
  • 哪些套接字发生异常?

   上述的三种监视项可以称为“事件”,发生监视项对应的情况时,称“事件发生”。接下来,我们介绍一下select函数的调用方法和顺序,如图

 技术分享图片

 

 

   图1-1给出了调用select函数到获取结果所经过程,可以看到,调用select函数前需要一些准备工作,调用后还需查看结果,接下来按照上述顺序逐一讲解

  2.1、设置文件描述符

  利用select函数可以同时监视多个文件描述符,当然,监视文件描述符可以视为监视套接字。此时首先需要将要监视的文件描述符集中到一起。集中时也要按照监视项(接收、传输、异常)进行区分,即按照上述三种监视项分成三类

  使用fd_set数组变量执行此项操作,如图1-2所示,该数组是存有0和1的位数组

技术分享图片

    图1-2   fd_set结构体

  图1-2中最左端的位表示文件描述符0(所在的位置),如果该位设置为1,则表示该文件描述符是监视对象。那么图中哪些文件描述符是监视对象呢?很明显,是文件描述符1和3。

  那么是否应当通过文件描述符的数字直接将值注册到fd_set变量中?当然不是!针对fd_set变量的操作是以单位进行的,这也意味着直接操作该变量会比较繁琐,这些工作如果由自己完成,有可能会出错,于是,在fd_set变量中注册或更改值的操作都是由下列宏完成:

  • FD_ZERO(fd_set *fdset):将fd_set变量的所有位都初始化为0
  • FD_SET(int fd, fd_set *fdset):在参数fdset指向的变量中注册文件描述符fd的信息
  • FD_CLR(int fd, fd_set *fdset):从参数fdset指向的变量中清除文件描述符fd的信息
  • FD_ISSET(int fd, fd_set *fdset):若参数fdset指向的变量中包含文件描述符fd的信息,则返回“真”

  上述函数中,FD_ISSET用于验证select函数的调用结果。通过图1-3解释这些函数的功能

 技术分享图片

      图1-3   fd_set相关函数的功能

  2.2、设置检查(监视)范围及超时

  先来简单介绍select函数

  (1)、 select()作用  

  先看下面的这句代码:

    int temp = recv(socket_fd, data_buffer,1024);

  这是用来接收数据的,在默认的阻塞模式下的套接字里,recv会阻塞在那里,直到套接字连接上有数据可读,把数据读到data_buffer里后recv函数才会返回,不然就会一直阻塞在那里。在单线程的程序里出现这种情况会导致主线程(单线程程序里只有一个默认的主线程)被阻塞,这样整个程序被锁死在这里,如果永远没数据发送过来,那么程序就会被永远锁死。这个问题可以用多线程解决,但是在有多个套接字连接的情况下,这不是一个好的选择,扩展性很差。

  再看下面的代码:

    int temp = ioctlsocket(socket_fd, FIOBIO, (unsigned long *)&ul);
    temp = recv(socket_fd,data_buffer,1024);

  这一次recv的调用不管套接字连接上有没有数据可以接收都会马上返回。原因就在于我们用ioctlsocket()函数把套接字设置为非阻塞模式了。不过你跟踪一下就会发现,在没有数据的情况下,recv确实是马上返回了,但是也返回了一个错误:WSAEWOULDBLOCK,意思就是请求的操作没有成功完成。

  看到这里很多人可能会说,那么就重复调用recv并检查返回值,直到成功为止,但是这样做效率很成问题,开销太大。

  在网络编程的过程中,经常会遇到许多阻塞的函数,read、recv,、recvfrom等函数都是阻塞的函数,当函数不能成功执行的时候,程序就会一直阻塞在这里,无法执行下面的代码。这时就需要用到非阻塞的编程方式,使用select函数就可以实现非阻塞编程。

  select()函数是一个轮循函数,循环询问文件节点,可设置超时时间,超时时间到了就跳过代码继续往下执行。

  (2)、大致原理

  select()需要驱动程序的支持,驱动程序实现fops内的poll函数。select()通过每个设备文件对应的poll函数提供的信息判断当前是否有资源可用(如可读或写),如果有的话则返回可用资源的文件描述符个数,没有的话则睡眠,等待有资源变为可用时再被唤醒继续执行。详细的原理请看这里

技术分享图片

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

select()系统调用代码走读,调用顺序如下:sys_select() à core_sys_select() à do_select() à fop->poll()

技术分享图片

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

   (3)、函数定义

int select(int maxfd, fd_set *read_fds, fd_set *write_fds, fd_set *except_fds, struct timeval *timeout);
函数作用:调用select函数时,拥塞等待文件描述符事件的到来,如果超过了设定的时间,则不再等待,继续往下执行,不再阻塞。

参数:
  maxfd:这是一个整数值,是指集合中所有文件描述符的范围,即所有文件描述符中的最大值加1,不能错!在Windows中这个参数值无所谓,可以设置不正确。
  read_fds:指向fd_set结构的指针,这个集合中包括文件描述符,我们是要监视这些文件描述符的读变化的,即我们关心是否可以从这些文件中读取数据了,如果这个集合中有一个文件可读,select就会返回一个大于0的值,表示有文件可读。如果没有可读的文件,则根据timeout参数再判断是否超时,若超出timeout的时间,select返回0,若发生错误返回负值。可以传入NULL值,表示不关心任何文件的读变化。
  write_fds:指向fd_set结构的指针,这个集合中应该包括文件描述符,我们是要监视这些文件描述符的写变化的,即我们关心是否可以向这些文件中写入数据了,如果这个集合中有一个文件可写,select就会返回         一个大于0的值,表示有文件可写。如果没有可写的文件,则根据timeout再判断是否超时,若超出timeout的时间,select返回0,若发生错误返回负值。可以传入NULL值,表示不关心任何文件的写变化。
  except_fds:指向fd_set结构的指针,这个集合中应该包括文件描述符,我们是要监视这些文件描述符是否有异常。
  timeout:用于设置select函数的超时时间,即告诉内核select等待多长时间之后就放弃等待。
    NULL:一直阻塞,当监视的文件描述符集合中的某一个描述符发生变化才会返回结果并向下执行。
    时间值为0:将select()函数置为非阻塞状态,执行select()后立即返回,无论文件描述符是否发生变化,只要检测完文件描述符集的状态,就立即返回。
    时间值不为0:将select()函数的超时时间设为这个值,在超时时间内阻塞,超时后返回结果。在指定时间内,如果没有事件发生,则超时返回。

返回值:成功时返回fd的总数,超时时返回0,错失败时返回SOCKET_ERROR(
-1

fd_set是一个数组的宏定义,是一个unsigned long类型的数组,是一个socket集合。每一个数组元素都能与一打开的文件句柄(socket、文件、管道、设备等)建立联系,建立联系的工作由程序员完成,当调用select()时,由内核根据IO状态修改fd_set的内容,由此来通知执行了select()的进程哪个句柄可读。常用如下宏来对fd_set进行操作:

#define __NFDBITS (8 * sizeof(unsigned long))                       //每个ulong型可以表示多少个bit,
#define __FD_SETSIZE 1024                                                   //socket最大取值为1024
#define __FDSET_LONGS (__FD_SETSIZE/__NFDBITS)      //bitmap一共有1024个bit,共需要多少个unsigned long


typedef struct 
{
  unsigned long fds_bits [__FDSET_LONGS];       //用unsigned long数组来表示bitmap
} __kernel_fd_set;
typedef __kernel_fd_set fd_set;

struct timeval

  long tv_sec;             /*秒 */
  long tv_usec;            /*微秒 */
}

//每个unsigned long为32位,可以表示32个bit。
//fd >> 5 即 fd / 32,找到对应的ulong下标i;fd & 31 即fd % 32,找到在ulong[i]内部的位置

#define __FD_ZERO(fdsetp) (memset (fdsetp, 0, sizeof (*(fd_set *)(fdsetp))))                                          //memset bitmap
#define __FD_SET(fd, fdsetp) (((fd_set *)(fdsetp))->fds_bits[(fd) >> 5] |= (1<<((fd) & 31)))                       //设置对应的bit
#define __FD_CLR(fd, fdsetp) (((fd_set *)(fdsetp))->fds_bits[(fd) >> 5] &= ~(1<<((fd) & 31)))                   //清除对应的bit
#define __FD_ISSET(fd, fdsetp) ((((fd_set *)(fdsetp))->fds_bits[(fd) >> 5] & (1<<((fd) & 31))) != 0)          //判断对应的bit是否为1

FD_ZERO(fd_set *fdset);                 //把fd_set队列初始为空,即把fd_set类型变量的所有位都设为 0;
FD_SET(int fd, fd_set *fdset);          //把套接字fd添加到fdset中,即使用FD_SET将变量的某一位置位;
FD_CLR(int fd, fd_set *fdset);         //从sfdet中删除套接字fd,即手动清除某个位时可以使用 FD_CLR;
FD_ISSET(int fd, fd_set *fdset);      //检查套接字fd是否存在与fdset中,即使用 FD_ISSET来测试某个位是否被置位;

需要说明一点,在内核中,socket对应struct socket结构,但在返回给用户空间之前,内核做了一个关联:调用get_unused_fd_flags从当前进程中获取一个可用的文件描述符fd ,将struct socket结构关联到该fd,并返回fd给用户空间。所以在用户空间中,socket为文件描述符。另外,进程可以打开的文件数是有限制的,为1024,故socket的取值小于1024。

 

  (4)、fd_set是如何实现的呢

  假如实现这样一个集合,可以往里添加任意0~1024之间的数(FD_SET操作),也可以将加入到集合中的数移除,移除一个(FD_CLR操作)或全部(FD_ZERO),你会如何实现?

  一种比较好的思路是使用位图bitmap,往集合了添加数据n时只需将第n个bit位置1(比如添加套接字fd=5,就将第5bit置1),移除n时只需将第n个bit置0,移除所有数据时,只需将所有bit置为0,可以通过memset操作来实现。fd_set的实现就是采用位图bitmap(关于位图可以参考《编程珠玑》第一章)。

  从流程上来看,使用select函数进行IO请求和同步阻塞模型没有太大的区别,甚至还多了添加监视socket,以及调用select函数的额外操作,效率更差。但是,使用select以后最大的优势是用户可以在一个线程内同时处理多个socket的IO请求。用户可以注册多个socket,然后不断地调用select读取被激活的socket,即可达到在同一个线程内同时处理多个IO请求的目的。而在同步阻塞模型中,必须通过多线程的方式才能达到这个目的。

  select模型的出现就是为了解决上述问题。select模型的关键是使用一种有序的方式,对多个套接字进行统一管理与调度 。

技术分享图片

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

  如上所示,用户首先将需要进行IO操作的socket添加到select中,然后阻塞等待select系统调用返回。当数据到达时,socket被激活,select函数返回。用户线程正式发起read请求,读取数据并继续执行。

  从流程上来看,使用select函数进行IO请求和同步阻塞模型没有太大的区别,甚至还多了添加监视socket,以及调用select函数的额外操作,效率更差。但是,使用select以后最大的优势是用户可以在一个线程内同时处理多个socket的IO请求。用户可以注册多个socket,然后不断地调用select读取被激活的socket,即可达到在同一个线程内同时处理多个IO请求的目的。而在同步阻塞模型中,必须通过多线程的方式才能达到这个目的。

  (5)、深入理解select模型

  理解select模型的关键在于理解fd_set,为说明方便,取fd_set长度为1字节,fd_set中的每一bit可以对应一个文件描述符fd。则1字节长的fd_set最大可以对应8个fd。

  select()根据看对应的文件描述符是否有读写操作,如果有就将对应的bit为置1,如果没有将其置0踢出集合。

  (1) FD_ZERO(&set);-------则set用位表示是00000000。

  (2)若sock_fd=5,执行FD_SET(sock_fd,&set),后set变为0001,0000(第5位置为1)

  (3)若再加入sock_fd=2,sock_fd=1,则set变为0001,0011

  (4)执行select(100,&set,0,0,0)阻塞等待

  (5)若sock_fd=1,sock_fd=2上都发生可读事件,则select返回,此时set变为0000,0011。注意:没有事件发生的fd=5被清空。

  基于上面的讨论,可以轻松得出select模型的特点:

  (1)可监控的文件描述符个数取决与sizeof(fd_set)的值。我这边服务器上sizeof(fd_set)=512,每bit表示一个文件描述符,则我服务器上支持的最大文件描述符是512*8=4096。据说可调,另有说虽然可调,但调整上      限受于编译内核时的变量值。

  (2)将fd加入select监控集的同时,还要再使用一个数据结构array保存放到select监控集中的fd,一是用于再select返回后,array作为源数据和fd_set进行FD_ISSET判断。二是select返回后会把以前加入的但并无事件     发生的fd清空,则每次开始select前都要重新从array取得fd逐一加入(FD_ZERO最先),扫描array的同时取得fd最大值maxfd,用于select的第一个参数。

  (3)可见select模型必须在select前循环加fd,取maxfd,select返回后利用FD_ISSET判断是否有事件发生。

  (6)、用select()处理带外数据

  网络程序中,select能处理的异常情况只有一种:socket上接收到带外数据。什么是带外数据?

  带外数据(out—of—band data),有时也称为加速数据(expedited data),是指连接双方中的一方发生重要事情,想要迅速地通知对方。这种通知在已经排队等待发送的任何“普通”(有时称为“带内”)数据之前发送。带外数据设计为比普通数据有更高的优先级。带外数据是映射到现有的连接中的,而不是在客户机和服务器间再用一个连接。

  我们写的select程序经常都是用于接收普通数据的,当我们的服务器需要同时接收普通数据和带外数据,我们如何使用select进行处理二者呢? 

  2.3、调用select函数后查看结果

  虽未给出示例,但图1-1中的步骤一“select函数调用前的所有准备工作”已讲解完毕,同时也介绍了select函数。而函数调用后查看结果也同样重要,我们已讨论过select函数的返回值,如果返回大于0的整数,说明相应数量的文件描述符发生变化。

  select函数返回正整数时,怎样获知哪些文件描述符发生了变化?向select函数的第二到第四个参数传递的fd_set变量中将产生如图1-4所示变化,获知过程并不难。

技术分享图片

              图1-4   fd_set变量的变化

  由图1-4可知,select函数调用完成后,向其传递的fd_set变量中将发生变化。原来为1的所有位均变为0,但发生变化的文件描述符对应位除外。因此,可以认为值仍然为1的位置上的文件描述符发生了变化

select.c

#include <stdio.h>
#include <unistd.h>
#include <sys/time.h>
#include <sys/select.h>
#define BUF_SIZE 30
 
int main(int argc, char *argv[])
{
    fd_set reads, temps;
    int result, str_len;
    char buf[BUF_SIZE];
    struct timeval timeout;
 
    FD_ZERO(&reads);
    FD_SET(0, &reads); // 0 is standard input(console)
 
    /*
    timeout.tv_sec=5;
    timeout.tv_usec=5000;
    */
 
    while (1)
    {
        temps = reads;
        timeout.tv_sec = 5;
        timeout.tv_usec = 0;
        result = select(1, &temps, 0, 0, &timeout);
        if (result == -1)
        {
            puts("select() error!");
            break;
        }
        else if (result == 0)
        {
            puts("Time-out!");
        }
        else
        {
            if (FD_ISSET(0, &temps))
            {
                str_len = read(0, buf, BUF_SIZE);
                buf[str_len] = 0;
                printf("message from console: %s", buf);
            }
        }
    }
    return 0;
}
  • 第14、15行:第14行初始化fd_set变量,第15行将文件描述符0对应的位设置为1。换言之,需要监视标准输入的变化
  • 第24行:将准备好的fd_set变量reads的内容复制到temps变量,因为之前讲过,调用select函数后,除发生变化的文件描述符对应位外,剩下的所有位将初始化为0。因此,为了记住初始值,必须经过这种复制过程。这是使用select函数的通用方法
  • 第18、19行:请观察被注释的代码,这是为了设置select函数的超时而添加的。但不能在此时设置超时,因为调用select函数后,结构体timeval的成员tv_sec和tv_usec的值将被替换为超时前剩余时间。因此,调用select函数前,每次都要初始化timeval结构体变量
  • 第25、26行:将初始化timeval结构体的代码插入循环后,每次调用select函数前都会初始化新值
  • 第27行:调用select函数,如果有控制台输入数据,则返回大于0的整数。如果没有输入数据而引发超时,则返回0
  • 第39~44行:select函数返回大于0的值时的运行区域,验证发生变化的文件描述符是否为标准输入。若是,则从标准输入读取数据并向控制台输出

编译select.c并运行

# gcc select.c -o select
# ./select
Hi
message from console: Hi
Hello
message from console: Hello
Time-out!
Time-out!
Good Bye
message from console: Good Bye
Time-out!

实现I/O复用服务端

下面通过select函数实现I/O复用服务端,之前已给出关于select函数的所有说明,下面示例是基于I/O复用的回声服务端

echo_selectserv.c

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <sys/time.h>
#include <sys/select.h>
 
#define BUF_SIZE 100
void error_handling(char *buf);
 
int main(int argc, char *argv[])
{
    int serv_sock, clnt_sock;
    struct sockaddr_in serv_adr, clnt_adr;
    struct timeval timeout;
    fd_set reads, cpy_reads;
 
    socklen_t adr_sz;
    int fd_max, str_len, fd_num, i;
    char buf[BUF_SIZE];
    if (argc != 2) {
        printf("Usage : %s <port>\n", argv[0]);
        exit(1);
    }
 
    serv_sock = socket(PF_INET, SOCK_STREAM, 0);
    memset(&serv_adr, 0, sizeof(serv_adr));
    serv_adr.sin_family = AF_INET;
    serv_adr.sin_addr.s_addr = htonl(INADDR_ANY);
    serv_adr.sin_port = htons(atoi(argv[1]));
 
    if (bind(serv_sock, (struct sockaddr *)&serv_adr, sizeof(serv_adr)) == -1)
        error_handling("bind() error");
    if (listen(serv_sock, 5) == -1)
        error_handling("listen() error");
 
    FD_ZERO(&reads);
    FD_SET(serv_sock, &reads);
    fd_max = serv_sock;
 
    while (1)
    {
        cpy_reads = reads;
        timeout.tv_sec = 5;
        timeout.tv_usec = 5000;
 
        if ((fd_num = select(fd_max + 1, &cpy_reads, 0, 0, &timeout)) == -1)
            break;
        if (fd_num == 0)
            continue;
 
        for (i = 0; i < fd_max + 1; i++)
        {
            if (FD_ISSET(i, &cpy_reads))
            {
                if (i == serv_sock) // connection request!
                {
                    adr_sz = sizeof(clnt_adr);
                    clnt_sock =
                        accept(serv_sock, (struct sockaddr *)&clnt_adr, &adr_sz);
                    FD_SET(clnt_sock, &reads);
                    if (fd_max < clnt_sock)
                        fd_max = clnt_sock;
                    printf("connected client: %d \n", clnt_sock);
                }
                else // read message!
                {
                    str_len = read(i, buf, BUF_SIZE);
                    if (str_len == 0) // close request!
                    {
                        FD_CLR(i, &reads);
                        close(i);
                        printf("closed client: %d \n", i);
                    }
                    else
                    {
                        write(i, buf, str_len); // echo!
                    }
                }
            }
        }
    }
    close(serv_sock);
    return 0;
}
 
void error_handling(char *buf)
{
    fputs(buf, stderr);
    fputc(\n, stderr);
    exit(1);
}
  • 第40行:向要传到select函数第二个参数的fd_set变量reads注册服务端套接字,这样,接收数据情况的监视对象就包含了服务端套接字。客户端的连接请求同样通过传输数据完成,因此,服务端套接字中有接收的数据,就意味着有新的请求
  • 第49行:在while无限循环中调用select函数,select函数的第三和第四个参数为空,只需根据监视目的传递必要的参数
  • 第54、56行:select函数返回大于等于1的值时执行的循环,第56行调用FD_ISSET函数,查找发生状态变化的(有接收数据的套接字)文件描述符
  • 第58、63行:发生状态变化时,首先验证服务端套接字中是否有变化?如果是服务端套接字的变化,将受理连接请求。特别需要注意的是,第63行在fd_set变量reads中注册了与客户端连接的套接字文件描述符
  • 第68行:发生变化的套接字并非服务端套接字时,即有要接收的数据,但此时需要确认接收的数据是字符串还是代表断开连接的EOF
  • 第73、74行:接收的数据为EOF时需关闭套接字,并从reads中删除相应信息
  • 第79行:接收的数据为字符串时,执行回声服务

编译echo_selectserv.c并运行

# gcc echo_selectserv.c -o echo_selectserv
# ./echo_selectserv 8500
connected client: 4
connected client: 5
closed client: 4
closed client: 5

echo_client ONE:

# ./echo_client 127.0.0.1 8500
Connected...........
Input message(Q to quit): Hello world!
Message from server: Hello world!
Input message(Q to quit): Apple
Message from server: Apple
Input message(Q to quit): Banana
Message from server: Banana
Input message(Q to quit): q

echo_client TWO:

# ./echo_client 127.0.0.1 8500
Connected...........
Input message(Q to quit): Java
Message from server: Java
Input message(Q to quit): Python
Message from server: Python
Input message(Q to quit): Golang
Message from server: Golang
Input message(Q to quit): q

 

I/O复用服务器

原文:https://www.cnblogs.com/The-explosion/p/12189996.html

(0)
(0)
   
举报
评论 一句话评论(0
关于我们 - 联系我们 - 留言反馈 - 联系我们:wmxa8@hotmail.com
© 2014 bubuko.com 版权所有
打开技术之扣,分享程序人生!