【网络】五中IO模型介绍 + 多路转接中select和poll服务器的简单编写

高级IO

  • 前言
  • 正式开始
    • 前面的IO函数简单过一遍
    • 什么叫做低效的IO
      • 钓鱼的例子
      • 同步IO和异步IO
      • 五种IO模型
        • 阻塞IO
        • 非阻塞IO
        • 信号驱动
        • 多路转接
        • 异步IO
      • 小结
    • 代码演示
      • 非阻塞IO
      • 多路转接
        • select介绍
        • 简易select服务器
          • timeout 为 nullptr
          • timeout 为 {0, 0}
          • timeout 为 {5, 0}
          • 调用accept
        • select编写代码的一般流程
          • 重写
          • 完整代码
          • select优缺点
        • poll
          • poll的优缺点

【网络】五中IO模型介绍 + 多路转接中select和poll服务器的简单编写_第1张图片

前言

本篇主要讲解:

  • 五种IO模型的介绍
  • 重点讲解多路转接
  • select服务器的编写
  • poll服务器的编写

关于多路转接的epoll我会在下一篇详细讲解。

前面我一直在讲网络通信,从创建套接字就可看到网络通信的就是IO,发送方能发也能收,接收方也是能发也能收,站在网络角度来看就是机器把数据扔到了网络里面,站在计算机体系结构角度来看就
是把数据把内存扔到网卡,不管怎么理解,都是IO。

正式开始

前面的IO函数简单过一遍

前面文件部分讲过的IO都是文件IO,单机的,打开文件,将数据从磁盘读到os,再从os将数据拷到用户缓冲区,各设备离的都非常近,在网络中,两台主机相隔千里之外,IO效率一定是要比单机来说低不少的。

IO问什么低效?
read、recv、recvfrom、write、send、sendto这样的IO函数本质上都是一些拷贝函数,都是在用户和内核之间拷贝数据,不过毕竟是从内存中直接拷贝的,效率还算OK。

以read为例,当我们进行read/recv的时候,如果底层没有数据,read/recv会怎么办做?有数据又会怎么做?

没有数据,read/recv进程就会阻塞,也就是让进程等。
如果有数据就直接进行拷贝。

⇒ 所以IO就是 等 + 数据拷贝。

等就是等IO类事件就绪。读就是底层有数据,写就是底层有空间。

write也是一样的,缓冲区满了就不让拷贝(等),没满就拷贝,所以IO必须经历的两部就是等和数据拷贝。

看图:
【网络】五中IO模型介绍 + 多路转接中select和poll服务器的简单编写_第2张图片

如果进程想要访问磁盘上的文件,那就得先打开这个文件,而文件 = 内容 + 属性,所以打开文件后,os要为文件创建相应的struct FILE结构体以维护文件的属性,也就是在内存中维护,而内存是惰性加载的,不会说将文件中的所有数据全部加载完,因为很多数据不一定能用上,os可能会对文件预加载,也就是先加载一部分,当进程想要修改文件中的内容时,就会先将需要的数据加载到内存里:
【网络】五中IO模型介绍 + 多路转接中select和poll服务器的简单编写_第3张图片
此时就是进程先调用的IO类型的函数想要访问文件中的数据,然后os才会做加载的这一步的,也就是os加载之前进程就已经开始调用IO类函数了。

那么os在加载文件的内容时,进程在干嘛?
就是在等。

IO = 等 + 拷贝。上面os在加载的时候,就是等,此刻进程是处于阻塞状态的。

那么拷贝呢?
就是加载完毕之后。进程就会被os唤醒,然后对os加载好的数据进行后续操作。

无论是网络还是单机,只要是访问磁盘、键盘、网卡等等外设,就一定是等 + 数据拷贝。

想一想scanf运行起来之后,为什么会卡在命令行等你输入,其实就是在等待标准输入。cin也是同理,像这样的函数都是在等数据就绪后再将数据从外设搬到内存os的缓冲区中,再从os搬到应用层,这就是数据拷贝。

所以recv、read、send、write等函数看起来是在发送和接收,其实都是在等IO类事件就绪,然后再发起拷贝,拷贝时无非就是从内核到用户或从用户到内核,所以这些函数不是用户直接与硬件进行读写,而是用户和内核之间的“交流”,交流完毕后,os再做后续的事情,比如说将修改后的数据写回磁盘。

在os视角来看,这些函数会让进程阻塞,在IO视角来看就是让进程在等。

什么叫做低效的IO

网络里面谈IO是因为报文从A主机发送到B主机,中间的发送时间会很长,所以网络通信时调用read、recv等函数就要做IO,这样就会花费大量的时间在等上,如何提高IO的效率呢?只要想办法在单位时间内让等的比重变得越低IO的效率就会越高。

单位时间内让等的比重变低,如何做到呢?
前面大佬们已经对于IO进行了深刻研究,总结出来了五中IO模型,这篇重点要讲的就是这五中IO模型。

先说说都是啥:

  1. 阻塞IO
  2. 非阻塞IO
  3. 信号驱动
  4. 多路转接(多路复用)
  5. 异步IO

不过这里先不说这五种IO模型的细节,我先通过一个生活中的例子来帮大家理解理解。

钓鱼的例子

钓鱼应该都见过吧。这里不说打窝这样的细节,简单一点。

就直接说成等 + 鱼上钩的收杆(后面直接说钓,也就是等 + 钓)。就像mc中的钓鱼一样。

什么场景下会说一个人钓鱼的效率非常高呢?
一个人大半天都没有鱼咬钩,一直在等。
另一个人一直是上钩,不带停的。

很明显,第二个人效率高,所以只要单位时间内等的比重非常低,这个人钓鱼的效率就非常高。

再来介绍个东西,鱼漂,钓鱼佬应该很熟悉,但是没钓过鱼的同学可能很陌生,看图:
【网络】五中IO模型介绍 + 多路转接中select和poll服务器的简单编写_第4张图片

钓鱼的时候,鱼漂能够反映出鱼咬钩的讯息。

假如说现在有五个人去钓鱼。

张三钓鱼的时候死死盯住鱼漂,啥也不干,非常专注,鱼漂不动他不动。
李四耐不住性子,看一会手机再看鱼漂有反应没,没反应就接着看手机。
王五拿了个铃铛,挂在鱼杆后面,一直在玩手机,铃铛一响就赶紧收杆。
赵六是个方圆五公里内的富二代,一下子拿了100支鱼竿,安置好后就来回检测哪只哪支鱼竿有鱼咬钩。
田七是个大老板,但是最近想吃鱼了(不是高启强),但是他比较忙,于是给了他手下小王一个桶,让小王去钓,等把桶钓满了再给他打电话,田七再去取。

那么上面这五种情况就对应了五中IO模型。
张三就是阻塞式IO,李四就是非阻塞式IO,王五就是信号驱动,赵六就是多路复用,田七就是异步IO。

那么谁的钓鱼效率更高呢?
赵六。

为啥呢?
站在鱼的角度,鱼脑袋上有104个诱饵(这里认为鱼一定会咬钩,不考虑打窝的情况,诱饵都一样且在某个区域中均匀分布),所以对于每个鱼竿来说,上鱼的概率都是1/104,但是赵六这个人的概率是100/104,而其他人都是1/104,所以单位时间内赵六等的比重是非常低的。

同步IO和异步IO

上面的人就对应的是进程或者线程,进程或线程只要参与了IO就称为同步IO。

什么叫参与IO呢?
就是要么参与了等,要么参与了拷贝,要么同时都参与。

只要参与了就叫做同步IO。

田七既没有等也没有钓(拷贝),所以田七是异步IO。

再来看看王五是同步IO吗?
前面我讲信号的时候说过信号的产生是异步的,但是王五是参与了IO的,他在等也在等鱼上钩,而且也是亲自钓的,而不是像田七那样直接不在场。也就是说数据没有就绪就先忙着自己的事情,但是一旦就绪了自己就将数据从内核拷贝到用户空间,所以是参与了IO的。这里的信号驱动,和单纯的信号产生有些不同,就在于IO这里有后续的拷贝动作,谈的不是信号的发送是异步的,谈的是信号发送之后要参与IO,还是同步的。

【注】这里信号驱动其实是有争议的,有的人说是同步IO,有的人说是异步IO,但我这里按照同步来说。

张三和李四的阻塞IO和非阻塞IO有什么区别?
都是同步IO,IO = 等 + 拷贝,都要亲手钓,这里没什么区别,主要的区别是在等上,张三是阻塞的等,李四是非阻塞的等。

阻塞式等,就是进程/线程检测某个文件描述符上是否有事件就绪,没有事件就绪就阻塞,也就是将进程的PCB放到等待队列中,后面的工作就由os来做了,并不是进程/线程在检测,而是os在做检测,当检测到对应文件描述符数据就绪了就把对应进程唤醒,并将PCB放到运行队列中,进程/线程阻塞期间什么也做不了,状态为非R。

非阻塞等就是事件没有就绪时os不会将进程/线程的PCB放到等待队列中,而是继续让它执行后续代码,我们经常是写个循环,然后其中调用IO函数,如果数据没有就绪就循环回去执行IO前面的代码,然后再次执行到IO函数,然后再次检测是否就绪,此即轮询。也就是非阻塞IO的非轮询检测。

前面多线程间的同步和这里的IO同步不是一个东西,多线程的同步背景是线程,是多线程执行流在协同工作,而这里的IO同步背景是IO,所以网上看计算机中的同步相关的资料时一定要确定是什么同步。

这里就带各位简单的了解了五中IO模型,下面来细说说。
主要讲一下阻塞、非阻塞和多路转接。信号驱动用的最少,异步IO在网络库或者IO库中是有的,但是很多公司都不太想用,因为可能会导致IO逻辑变的很混乱,但也不是不用,只是用的少。

五种IO模型

张三、李四这些人对应的就是一个进程或线程,鱼竿对应的就是文件描述符,鱼漂对应文件描述符是否有时间就绪,鱼即数据,鱼所在的水域就是缓冲区。

先简单过一遍,然后再写代码。

阻塞IO

阻塞IO: 在内核将数据准备好之前, 系统调用会一直等待. 所有的套接字, 默认都是阻塞方式。

阻塞IO是最常见的IO模型:
【网络】五中IO模型介绍 + 多路转接中select和poll服务器的简单编写_第5张图片
左边对应用户空间,右边对应os的内核空间。

上面用户调用recv这样的系统级别的IO函数,就会进入阻塞状态,后面的工作就是os在做了,用户啥也做不了,数据拷贝好后才能做后续工作。

非阻塞IO

非阻塞IO: 如果内核还未将数据准备好, 系统调用仍然会直接返回, 并且返回EWOULDBLOCK错误码.

这里的EWOULDBLOCK错误码不写代码感受不出来,等会写代码的时候就懂了。

非阻塞IO往往需要程序员循环的方式反复尝试读写文件描述符, 这个过程称为轮询. 这对CPU来说是较大的浪费, 一般只有特定场景下才使用
【网络】五中IO模型介绍 + 多路转接中select和poll服务器的简单编写_第6张图片

这里会对数据是否准备好做轮询检测,如果没有准备好就先干自己的事情,干一会后再检查一下,如果还没好就继续做自己的事情,直到某一次检测数据准备好了,就会对数据进行拷贝。

信号驱动

信号驱动IO: 内核将数据准备好的时候, 使用SIGIO信号通知应用程序进行IO操作。

来看看这个信号:
【网络】五中IO模型介绍 + 多路转接中select和poll服务器的简单编写_第7张图片

流程:
【网络】五中IO模型介绍 + 多路转接中select和poll服务器的简单编写_第8张图片
这里涉及到了信号的相关操作,如果你不懂信号,可以看看我这一篇:信号详解

开始的时候对SIGIO信号自定义处理,定义好信号的捕捉方法sigaction,当接收到SIGIO信号的时候就去执行sigaction函数,sigaction函数中一定是会调用recv这样的IO函数的。

这里就是由争议的地方,信号。但是进程不是在等信号,而是在等数据就绪,但等数据的同时又能自由的做自己的事情,SIGIO到来的时候就去处理SIGIO。不要深究这些东西,没有太大意义。会用就行。

多路转接

先来看流程图:

【网络】五中IO模型介绍 + 多路转接中select和poll服务器的简单编写_第9张图片

IO多路转接: 虽然从流程图上看起来和阻塞IO类似. 实际上最核心在于IO多路转接能够同时等待多个文件描述符的就绪状态。

支持多路转接的OS要提供独有的接口,一个接口专门负责一个等的动作。
而select就是专门负责等多个文件描述符的,不会进行拷贝,这个接口可以向其中添加很多货文件描述符,也就是一次可以等多个文件描述符上的数据准备就绪,多个文件描述符随时有可能准备就绪,如果有文件描述符准备就绪,select就要把准备就绪的文件反应给进程,让进程调用recv等函数进行读取。

所以这里等的时候能并行一块等,读取的时候只能串行一个一个来读,和赵六钓鱼一样的,一下子把100个鱼竿安好(并行等),然后有杆钓上鱼了就去哪个杆(串行)。

select和IO函数各司其职,select这种类似的多路转接的接口只负责等,当数据就绪时就让上层的IO类接口只进行拷贝,此时上层的IO函数就不会出现导致进程阻塞,因为上层的select已经告诉了进程底层有数据了,本次调用recv这样的IO函数绝对不会阻塞,理想情况下只需要拷贝。

当然这里光说的话有点难懂,后面用代码演示就好理解了。

异步IO

异步IO: 由内核在数据拷贝完成时, 通知应用程序(而信号驱动是告诉应用程序何时可以开始拷贝数据).

【网络】五中IO模型介绍 + 多路转接中select和poll服务器的简单编写_第10张图片

aio_read这样的函数一般都是要先给os一端用户级的缓冲区,后续就不需要再等了,不用调用recv之类的函数,os自动帮你把数据拷到你给的缓冲区中,拷贝完后就给你通知拷贝完了。

田七(进程)给小王(os)一个桶(用户级缓冲区),小王去钓(os办事),田七办自己的事,桶钓满(拷贝好了)了通知田七。

注意这里的通知和前面的信号驱动不一样的,前面的信号驱动是要进程自己调用recv拷贝数据的,而这里是os直接帮进程把数据就拷贝好了。

小结

任何IO过程中, 都包含两个步骤. 第一是等待, 第二是拷贝. 而且在实际的应用场景中, 等待消耗的时间往往都远远高于拷贝的时间. 让IO更高效, 最核心的办法就是让等待的时间尽量少.

mmap也是一个高级的IO,想了解的同学请自行查资料看看。

代码演示

非阻塞IO

前面我所有的博客都是阻塞式的IO,想要变成非阻塞,就需要在打开文件的时候就设置打开文件的选项O_NONBLOCK。
【网络】五中IO模型介绍 + 多路转接中select和poll服务器的简单编写_第11张图片

还有创建套接字也一样可以设置:
【网络】五中IO模型介绍 + 多路转接中select和poll服务器的简单编写_第12张图片

设置了之后就文件就具有了非阻塞的属性。

所以想要让文件描述符在读写的时候能进行非阻塞读写,就要进行属性设置,打开文件时就设定。(无论是创建套接字还是普通的文件)。

但是这样有点麻烦,我们可以用同一的方式来进行非阻塞的设置,即fcntl函数:
【网络】五中IO模型介绍 + 多路转接中select和poll服务器的简单编写_第13张图片
参数fd就是文件描述符,cmd就是你要选择哪种功能,后面的…表示这是可变参数。

传入的cmd的值不同, 后面追加的参数也不相同。

fcntl函数有5种功能:

  • 复制一个现有的描述符(cmd=F_DUPFD).
  • 获得/设置文件描述符标记(cmd=F_GETFD或F_SETFD).
  • 获得/设置文件状态标记(cmd=F_GETFL或F_SETFL).
  • 获得/设置异步I/O所有权(cmd=F_GETOWN或F_SETOWN).
  • 获得/设置记录锁(cmd=F_GETLK,F_SETLK或F_SETLKW).

这里我们要改的是文件的状态,也就是阻塞还是非阻塞,所以等会用的就是第三行的F_GETFL和F_SETFL。F_GETFL是获取状态,F_SETFL是设置状态。

这里无论是普通文件、管道文件还是套接字文件,只要是文件描述符就行,fcntl都可以将对应文件状态设置成非阻塞模式。

函数返回值:
【网络】五中IO模型介绍 + 多路转接中select和poll服务器的简单编写_第14张图片

下面来写写代码。

先来看一个基本的阻塞IO:
【网络】五中IO模型介绍 + 多路转接中select和poll服务器的简单编写_第15张图片
上面0就是标准输入,这就不细讲了,最开始给的那篇博客中有。

此时运行起来就会阻塞在这里:
【网络】五中IO模型介绍 + 多路转接中select和poll服务器的简单编写_第16张图片
因为一直在等待键盘对应文件的资源就绪,输入了之后才等于是资源就绪了:
【网络】五中IO模型介绍 + 多路转接中select和poll服务器的简单编写_第17张图片

然后再来搞一下非阻塞,简单封装一下fcntl:
【网络】五中IO模型介绍 + 多路转接中select和poll服务器的简单编写_第18张图片
这里就是对F_GETFL和F_SETFL的使用,先用F_GETFL获取原先文件描述符对应文件的状态,然后再用F_SETFL来设置文件状态,就是再添加一个非阻塞的标志位,像位图一样,用一个 | 就行,在原始的f1标志位上新增一个非阻塞的标志位,不影响其他标志位。

在前面对0设置非阻塞:
【网络】五中IO模型介绍 + 多路转接中select和poll服务器的简单编写_第19张图片

运行:
【网络】五中IO模型介绍 + 多路转接中select和poll服务器的简单编写_第20张图片
一直在打印err。

不过打印太快了,加一个sleep控制一下:
【网络】五中IO模型介绍 + 多路转接中select和poll服务器的简单编写_第21张图片

这样打印的慢一点:
【网络】五中IO模型介绍 + 多路转接中select和poll服务器的简单编写_第22张图片

我输入后也可以读取:
【网络】五中IO模型介绍 + 多路转接中select和poll服务器的简单编写_第23张图片
但是用起来有点怪,因为打印的时候是往屏幕上打的,输入的时候也是要在屏幕上显示。

所以非阻塞的时候是IO函数是以出错的形式返回,告知上层数据没有就绪,如果数据就绪了话,正常读取就行。那么我们如何甄别是真的出错了还因为数据没有准备就绪呢?

出错不仅仅通过read的返回值判断的,出错了系统还会设置errno,所以还可以通过errno来判断是什么问题。
在这里插入图片描述

运行:
【网络】五中IO模型介绍 + 多路转接中select和poll服务器的简单编写_第24张图片

所以如果read失败的errno是11,就代表其实read没出错,不过是底层数据没有就绪,所以 s <= 0的时候可以再判断一下errno是否等于11。不过可以不用数字,刚刚再介绍非阻塞的时候说了一个EWOULDBLOCK字段,这个字段的值其实就是11:
【网络】五中IO模型介绍 + 多路转接中select和poll服务器的简单编写_第25张图片
很多地方判断errno是否是11都是这样用的:
【网络】五中IO模型介绍 + 多路转接中select和poll服务器的简单编写_第26张图片
send、recv等IO函数非阻塞的时候也会返回这个EWOULDBLOCK,但是我感觉这两个一个就够了,如果有懂的老铁可以在评论给我解答一下吗,谢谢了。

【网络】五中IO模型介绍 + 多路转接中select和poll服务器的简单编写_第27张图片

运行:
【网络】五中IO模型介绍 + 多路转接中select和poll服务器的简单编写_第28张图片

还有一个很重要的字段,EINTER,就是interrupt,被打断了,用于在等的阶段被其他东西打断了,比如说进程/线程可能收到某个信号,此时os就会将进程/线程唤醒去处理信号,可是处理信号了就不回来了,此时errno就会被设置成EINTER,表示中断了,所以也可以再添加一个:
【网络】五中IO模型介绍 + 多路转接中select和poll服务器的简单编写_第29张图片

相当于是IO没读完就被中断了,需要重新读取。所以二者都是正常情况,直接continue就行。但我这里整不出来相关的场景,就不演示了。

多路转接

select用的稍微多一点,但是工作中也不会直接从0开始写,不过这里还是要写写这个了解一下过程,方便理解。

select是Linux提供的多路转接方案中的一种,根据前面所讲的赵六,一次可以等多个文件描述符,那么select功能就有两个:

  1. 帮助用户进行一次等待多个文件fd
  2. 当哪些文件fd就绪了,select就要通知用户对应就绪的fd有哪些

然后用户再调用recv/read这样的函数进行数据读取,记住多路转接是为我们提供一个更高效的等待方案,一次可以等多个文件描述符。

认识一下select接口:
【网络】五中IO模型介绍 + 多路转接中select和poll服务器的简单编写_第30张图片

select介绍

展开来看:
在这里插入图片描述
select作用就是让os注意多个文件描述符,如果有文件描述符就绪了就告诉用户哪个就绪了。

挑着讲:

第一个参数nfds是你让os注意的最大文件描述符 + 1。

  • 比如说最大文件描述符的值为5,那么nfds就是6(0、1、2、3、4、5正好六个)

返回值就是就绪的fd的个数,有3个就绪了就是3,有5个就绪了就是5,1个就绪了就是1,至少有一个fd数据就绪/空间就绪了就可以返回了。

后四个参数都是输入输出型参数,先来说最后一个timeout,其类型为timeval的结构体:
【网络】五中IO模型介绍 + 多路转接中select和poll服务器的简单编写_第31张图片
其中tv_sec单位是以秒,tv_usec单位是微秒。
这个结构体可以配合着gettimeofday来用:

【网络】五中IO模型介绍 + 多路转接中select和poll服务器的简单编写_第32张图片
这个函数可以获取当前系统的时间戳,传一个timeval结构体来获取参数为tz区域的时间,tz给空就是本地的时间。带着C语言中的time函数演示一下:
【网络】五中IO模型介绍 + 多路转接中select和poll服务器的简单编写_第33张图片
在这里插入图片描述
打印出来前面秒级别的和C中的time一样,.后面的是微秒级别的
再说回最后一个参数timeout
【网络】五中IO模型介绍 + 多路转接中select和poll服务器的简单编写_第34张图片
这个参数可以设置等待多个参数的策略,有三种:

  1. 阻塞式IO,timeout设置为空。
  2. 非阻塞式IO,timeout设置为{0, 0}。
  3. timeout规定时间内阻塞,时间一到立马返回,比如说设置为{5, 0},就是5s。5s是输入性参数的含义,还有输出型参数的含义:若等待时间内有fd就绪,timeout就表示剩余多少时间,比如说设置5s,2s时有文件就绪,那么time此时就是{3, 0},也就是剩余三秒。

中间三个参数:
【网络】五中IO模型介绍 + 多路转接中select和poll服务器的简单编写_第35张图片

  • 三个参数,分别对应有文件的读事件,写事件和异常事件,类型都是fd_set,是一个系统提供的类型,底层是位图,每一个比特位表示一个文件描述符的状态,等会细讲。
  • 作为输入的时候是用户告诉内核,你要帮我关心哪个/哪些fd上的那种事件。作为输出时,就是内核告诉用户,我所关心的fd中,哪些fd上的哪类时间已经就绪了。
  • 先来说说fd_set:
    【网络】五中IO模型介绍 + 多路转接中select和poll服务器的简单编写_第36张图片
    系统是用一个定长的数组来表示的位图。结构体是由系统提供的,用户不能直接对其进行按位与、按位或等操作,而是用系统提供的方法:
    【网络】五中IO模型介绍 + 多路转接中select和poll服务器的简单编写_第37张图片
    这四个函数作用分别是:CLR清除一个文件描述符,ISSET判断某个文件描述符在不在位图中,SET设置一个文件描述符,ZERO将文件描述符清空。

看一下系统中的fd_set最多能容纳多少个文件描述符:
在这里插入图片描述
这里乘以8是因为sizeof求的是字节数,而位图是看有多少比特位的,一个字节8位:
在这里插入图片描述
.
.
再来看这三个参数
【网络】五中IO模型介绍 + 多路转接中select和poll服务器的简单编写_第38张图片
三个参数在用法上都是一样的,我就挑readfds来说,就是读文件描述符集。
a. 作为输入型参数时,是用户通知内核,我的比特位中,比特位的位置就表示文件描述符的值,比特位的内容表示是否关心,比如说 0000 1010,左边是高位,右边是低位,低位从0开始,这里就是指0 ~ 7的文件描述符,这里就表示0、2、4、5、6、7号文件描述符不关心读,1、3关心读。
b. 输出的时候内核告诉用户,用户你让我关心的多个fd有结果了,比特位的位置依旧表示文件描述符的值,比特位的内容表示是否就绪,比如说刚刚让os关心1号和3号,如果只有三号就绪,返回的就是0000 1000,表示用户可以直接读取3号而不会发送阻塞。

故用户和内核都修改同一个位图结构,所以这个参数用一次之后一定需要进行重新设定,剩下的三个一样,如果既关心读又关心写,就可以同时把文件描述符加到其中,虽然这样的情况很少,下面就来写写代码,等会肯定是写一会就写不下去了,因为还没说select的一般的编写代码的模式(直接将模式的话不能理解,得先见见select怎么用)。

简易select服务器

关于怎么写服务器不再详谈,我前面的博客中有,不懂的同学请自行查看。

我这里就直接用我前面封装好的套接字接口来写了,两个现成的文件:

打印日志:

#pragma once
#include 
#include 
#include 
#include 

#include 

#include 

// 文件名
#define _F __FILE__
// 所在行
#define _L __LINE__

enum level
{
    DEBUG, // 0
    NORMAL, // 1
    WARING, // 2
    ERROR, // 3
    FATAL // 4
};

std::vector<const char*> gLevelMap = {
    "DEBUG",
    "NORMAL",
    "WARING",
    "ERROR",
    "FATAL"
};

#define FILE_NAME "./log.txt"

void LogMessage(int level, const char* file, int line, const char* format, ...)
{
#ifdef NO_DEBUG
    if(level == DEBUG)  return;
#endif

    // 固定格式
    char FixBuffer[512];
    time_t tm = time(nullptr);
    // 日志级别 时间 哪一个文件 哪一行
    snprintf(FixBuffer, sizeof(FixBuffer), \
    "<%s>==[file->%s] [line->%d] ----------------------------------- time:: %s", gLevelMap[level], file, line, ctime(&tm));

    // 用户自定义格式
    char DefBuffer[512];
    va_list args; // 定义一个可变参数
    va_start(args, format); // 用format初始化可变参数
    vsnprintf(DefBuffer, sizeof DefBuffer, format, args); // 将可变参数格式化打印到DefBuffer中
    va_end(args); // 销毁可变参数

    // 往显示器打
    printf("%s\t=\n\t=> %s\n\n\n", FixBuffer, DefBuffer);
    
    // 往文件中打
    // FILE* pf = fopen(FILE_NAME, "a");
    // fprintf(pf, "%s\t==> %s\n\n\n", FixBuffer, DefBuffer);
    // fclose(pf);
}

套接字相关:

#pragma once
#include "LogMessage.hpp"

#include 
#include 
#include 

#include 
#include 
#include 
#include 

#include 

// 对套接字相关的接口进行封装
class Sock
{
private:
    static const int gBackLog = 20;

public:
        // 1. 创建套接字
    static int Socket()
    {
             /*先AF_INET确定网络通信*/  /*这里用的是TCP,所以用SOCK_STREAM*/
        int listenSock = socket(AF_INET, SOCK_STREAM, 0);
            // 创建失败返回-1
        if(listenSock == -1)
        {
            LogMessage(FATAL, _F, _L, "server create socket fail");
            exit(2);
        }
        LogMessage(DEBUG, _F, _L, "server create socket success, listen sock::%d", listenSock);


        // 创建成功
        return listenSock;
    }

        // 2. bind 绑定IP和port
    static void Bind(int listenSock, uint16_t port, const std::string& ip = "0.0.0.0")
    {
        sockaddr_in local; // 各个字段填充
        memset(&local, 0, sizeof(local));
                                        // 若为空字符串就绑定当前主机所有IP
        local.sin_addr.s_addr = inet_addr(ip.c_str());
        local.sin_port = htons(port);
        local.sin_family = AF_INET;
                                            /*填充好了绑定*/
        if(bind(listenSock, reinterpret_cast<sockaddr*>(&local), sizeof(local)) < 0)
        {
            LogMessage(FATAL, _F, _L, "server bind IP+port fail :: %d:%s", errno, strerror(errno));
            exit(3);
        }
        LogMessage(DEBUG, _F, _L, "server bind IP+port success");
    }

        // 3. listen为套接字设置监听状态
    static void Listen(int listenSock)
    {
        if(listen(listenSock, gBackLog/*后面再详谈listen第二个参数*/) < 0)
        {
            LogMessage(FATAL, _F, _L, "srever listen fail");
            exit(4);
        }
        LogMessage(NORMAL, _F, _L, "server init success");
    }

        // 4.accept接收连接           输出型参数,返回客户端的IP + port
    static int Accept(int listenSock, std::string &clientIp, uint16_t &clientPort)
    {
            /*客户端相关字段*/
        sockaddr_in clientMessage;
        socklen_t clientLen = sizeof(clientMessage);
        memset(&clientMessage, 0, clientLen);
        // 接收连接
        int serverSock = accept(listenSock, reinterpret_cast<sockaddr*>(&clientMessage), &clientLen);

        // 对端的IP和port信息
        clientIp = inet_ntoa(clientMessage.sin_addr);
        clientPort = ntohs(clientMessage.sin_port);

        if(serverSock < 0)
        {
            // 这里没连接上不能说直接退出,就像张三没有揽到某个客人餐馆就不干了,所以日志等级为ERROR
            LogMessage(ERROR, _F, _L, "server accept connection fail");
            return -1;
        }
        else
        {
            LogMessage(NORMAL, _F, _L, "server accept connection success ::[%s:%d] server sock::%d", \
                                                                clientIp.c_str(), clientPort,serverSock);
        }

        return serverSock;
    }

};

然后对服务器简单封装一下:
【网络】五中IO模型介绍 + 多路转接中select和poll服务器的简单编写_第39张图片

这里还剩下一步accept就可以进行通信了,但是有个问题,这一篇要讲高级IO,如果直接accept就会导致服务器阻塞在accept处等待连接。想要高级一点,那就不要阻塞,用select来进行多路转接,此处我们是知道除了0、1、2这三个文件描述符就只有一个_listenSock了,后面文件描述符会随着不断地accept而越来越多,是一个动态增加的过程,而且这里的动态增长完全是通过listenSock来实现的。

前面讲TCP的时候,通信前要进行三次握手,而三次握手本质上也是在通信(握手报文的通信),获取新的连接,在IO角度来看,就是input事件,对于连接的input,所以listenSock读事件就绪,对应的就是能获取新连接了,对应到普通文件的读事件就绪就是能进行读取。

如果没有连接到来,accept就会阻塞,和前面讲的read阻塞是一样的,都是等这个listenSock文件描述符,所以这里就不能直接调用accept了,因为调了进程就会自己去等。

所以这里也要把listenSock当成一个普通的文件描述符加入到select中去,让select帮进程等,select只要告诉用户listenSock就绪了,就直接调用accept,这样accept就不会再阻塞了,所以这里要先调用select。

本篇所讲的select相对于epoll来说没有那么重要,所以只演示一下读文件描述符集,等后面讲epoll了再将三个文件描述符集都演示一下。

timeout 为 nullptr

调用select:
【网络】五中IO模型介绍 + 多路转接中select和poll服务器的简单编写_第40张图片

这里根据select的返回值来选择该干什么事情:
【网络】五中IO模型介绍 + 多路转接中select和poll服务器的简单编写_第41张图片

这样运行起来的话会先阻塞:
【网络】五中IO模型介绍 + 多路转接中select和poll服务器的简单编写_第42张图片

用telnet连接:
【网络】五中IO模型介绍 + 多路转接中select和poll服务器的简单编写_第43张图片

会死循环打印listenSock的读已经准备好了。

因为连接上了以后一直没有取走连接,底层中listenSock对应的资源一直是就绪的,就是连接已经建立完成了,accept一直没有取走底层对应连接的文件描述符,所以select要一直通知你赶紧调用select。

timeout 为 {0, 0}

先不调用accept,把timeout改成{0,0}看看:

【网络】五中IO模型介绍 + 多路转接中select和poll服务器的简单编写_第44张图片

刚运行起来就一直打印time out:
【网络】五中IO模型介绍 + 多路转接中select和poll服务器的简单编写_第45张图片

因为这里timeout设置成{0, 0}就是非阻塞等待,和前面的非阻塞的read一样,所以一般不这么用。

timeout 为 {5, 0}

我再来把timeout改成{5, 0}:
【网络】五中IO模型介绍 + 多路转接中select和poll服务器的简单编写_第46张图片
刚运行没问题:
【网络】五中IO模型介绍 + 多路转接中select和poll服务器的简单编写_第47张图片
但是5s后又开始疯狂打印了:
【网络】五中IO模型介绍 + 多路转接中select和poll服务器的简单编写_第48张图片

因为timeout参数是输入输出型的,第一次作为输出参数会被改成{0, 0},而我刚刚故意将tv的定义放在了while外面,所以就会导致后续的tv都变成{0, 0},这样就会和上面的情况一样,变成了非阻塞IO,所以要把tv定义放在while中或者在while中更新tv中的值:
【网络】五中IO模型介绍 + 多路转接中select和poll服务器的简单编写_第49张图片

这样就不会那么快:
【网络】五中IO模型介绍 + 多路转接中select和poll服务器的简单编写_第50张图片

调用accept

再来说回timeout为nullptr的情况:
【网络】五中IO模型介绍 + 多路转接中select和poll服务器的简单编写_第51张图片

因为接收连接后还会有后续动作,所以再给一个函数把后续动作放到一起更方便观察,这里我们是知道只有一个listenSock的,所以写的简单点,等后面有新场景了再做修改:
【网络】五中IO模型介绍 + 多路转接中select和poll服务器的简单编写_第52张图片

运行起来:
【网络】五中IO模型介绍 + 多路转接中select和poll服务器的简单编写_第53张图片
一切正常。

这里我故意把通信过程留下来了,请问通信的时候能直接recv/read吗?
很显然是不能的,我前面写的TCP服务器至少都是创建进程/线程去专门负责读取,更不用谈现在单进程的情况下想直接读了,我们这里想实现一个单线程既能实现监听又能实现接受连接的,但当前状态下单线程直接读,如果用户不发消息进程直接就阻塞了,没办法向后执行,也就无法处理新的连接,本质原因还是我们不清楚sock上面数据什么时候到来,但是如果把sock也能放到select中select就清楚什么时候到来。

所以得到新的连接后,此时我应该考虑的是将新的sock托管给select,让select帮我们进行监测sock上是否有新数据,有了新数据select就会通知我,此时再进行读取就不会再阻塞,但是如何把新的sock交给select呢?以现在的写法无法实现。

前面说了,写一半就写不下去了,下面就得讲讲select编写代码的一般流程了。

select编写代码的一般流程

再看看这个接口:
在这里插入图片描述

  1. 第一个参数nfds,随着我们获取的sock越来越多,需要添加到select中的sock也就会越来越多,那么就注定了每一次调用select时nfds都可能要改变,所以要对nfds动态计算。

  2. readfds/writefds/exceptfds都是输入输出参数,输入和输出不一定会一样,比如说传入1111,输出0010,那再次输入的时候还要改成1111,所以我们每一次都要对rfds重新添加。

  3. timeout,也是输入输出,如果设置了时间,每次都要重置。

对于1、2两点而言,主要原因是文件描述符可能每次都在变,想要完全掌握其变化就要自己将合法的文件描述符全部保存起来,用来支持更新最大fd和更新位图结构。

所以select服务器编写的时候:
需要一个第三方数组用来保存所有合法的fd,数组就是select能同时监听的fd个数(元素个数)。我这里等会就直接用原生数组来实现了,也可以用vector,会更方便一点,但至于为什么用原生数组等会写完了再说。

上面的流程大致如下:
while(1)
{

  1. 遍历数组,更新最大的fd,用于select中第一个参数
  2. 遍历数组,添加所有需要关心的fd到fd_set位图中,用于select第二个参数
  3. 调用select进行实践检测
  4. 遍历数组,找到就绪的事件,根据就绪的事件完成对应的动作。
    }
重写

在这里插入图片描述
【网络】五中IO模型介绍 + 多路转接中select和poll服务器的简单编写_第54张图片
这里直接将数组开完整,select最大能监听的文件描述符的个数为1024个,也就是fd_set位图的位数大小,前面也讲过了。用这个数组来存放合法的sock(合法就是指能用的)。

构造函数里面初始化一下:
【网络】五中IO模型介绍 + 多路转接中select和poll服务器的简单编写_第55张图片

那么代码就要改改了:
【网络】五中IO模型介绍 + 多路转接中select和poll服务器的简单编写_第56张图片

每次都打印一下其中有效的文件描述符:
在这里插入图片描述

每次都要对数组进行操作,变化的就是红框中的:
【网络】五中IO模型介绍 + 多路转接中select和poll服务器的简单编写_第57张图片

EventHandler也要改:
【网络】五中IO模型介绍 + 多路转接中select和poll服务器的简单编写_第58张图片
想要将sock添加到select中,其实只要将sock放到数组中就行,EventHandler调用完毕后会循环回去,遍历后就会放到位图中。

将新的连接加入select中:
【网络】五中IO模型介绍 + 多路转接中select和poll服务器的简单编写_第59张图片
测试一下,刚运行:
【网络】五中IO模型介绍 + 多路转接中select和poll服务器的简单编写_第60张图片

连一个:
【网络】五中IO模型介绍 + 多路转接中select和poll服务器的简单编写_第61张图片

连两个:
【网络】五中IO模型介绍 + 多路转接中select和poll服务器的简单编写_第62张图片

很正常。

每次进行select的时候,若有文件描述符就绪,会有两种情况:

  1. 就绪的是listenSock
  2. 就绪的是sock

这两种文件描述符是不同的情况,处理方式也是不同的。listenSock是用来获取连接的,sock是用来通信时读取用户数据的。

那么EventHandler处理就绪的文件描述符时要先遍历一下_fdArray,找到合法的文件描述符并判断文件描述符是否在os输出的rfds中(用来判断有效的文件描述符是否就绪),若在,还要判断是listenSock还是普通通信的sock,如果是listenSock就要接收连接,如果是sock,就要进行读取。分两种方式,那么刚刚实现的EventHandler只是实现了接收连接,读取还没有实现,这两个方法完全可以再实现成两个函数,一个reader用来实现读取,一个accepter用来实现接收连接。

把这两个函数实现给出:
【网络】五中IO模型介绍 + 多路转接中select和poll服务器的简单编写_第63张图片
其实接收连接就是刚刚写的代码。

获取数据:
在这里插入图片描述

这样本次读取的时候就不会再阻塞。

然后EventHandler改成:
【网络】五中IO模型介绍 + 多路转接中select和poll服务器的简单编写_第64张图片

测试一下,刚运行(这里接收到连接后的listenSock is ready忘改了,你懂我就行):

【网络】五中IO模型介绍 + 多路转接中select和poll服务器的简单编写_第65张图片

连接一个:
【网络】五中IO模型介绍 + 多路转接中select和poll服务器的简单编写_第66张图片

连接两个:
【网络】五中IO模型介绍 + 多路转接中select和poll服务器的简单编写_第67张图片

连接三个:

【网络】五中IO模型介绍 + 多路转接中select和poll服务器的简单编写_第68张图片

第一个连接通信:
【网络】五中IO模型介绍 + 多路转接中select和poll服务器的简单编写_第69张图片

第二个连接通信:
【网络】五中IO模型介绍 + 多路转接中select和poll服务器的简单编写_第70张图片

第三个连接通信:
【网络】五中IO模型介绍 + 多路转接中select和poll服务器的简单编写_第71张图片

挨个退出:
【网络】五中IO模型介绍 + 多路转接中select和poll服务器的简单编写_第72张图片

成功。

其实上面的read是有bug的,因为传输层TCP是面向字节流的,不能保证每次读取到的是一个完整的报文,就像我前面的网络版本计算器一样,应用层需要自己手动定制协议,不软会出现粘包问题,这里就不改了,等后面讲epoll的博客再解决这个问题。

上面的select服务器是一个单进程单线程的服务器,但是依旧能并发的执行任务。

如果想要引入写呢?也就是writefds参数。
简单说一下思路,就是再定义一个_wrArray数组,用来保存写的文件描述符,后续的流程和_rdArray差不多。这里就不细说了,等后面讲epoll了再说。

完整代码

服务器头文件:

#include "Sock.hpp"
#include 

#define NUM (sizeof(fd_set) * 8) // 数组元素个数
#define FD_NONE -1 // 数组初始化的值,表明没有这个fd

class SelectServer
{
public:
    SelectServer(uint16_t port = 8080)
        :_port(port)
    {
        // 创建套接字
        _listenSock = Sock::Socket();
        
        // bind绑定
        Sock::Bind(_listenSock, _port);

        // 设置监听状态
        Sock::Listen(_listenSock);

        // 对_rdArray数组初始化
        for(int i = 0; i < NUM; ++i)
        {
            _rdArray[i] = FD_NONE; // 每一个都设置成FD_NONE,表明某一位没有文件描述符
        }
        // 规定第一个位为_listenSock,因为_listenSock一直存在
        _rdArray[0] = _listenSock;
    }

    void Start()
    {
        while(1)
        {
            showFds(); // 每次打印一下数组中有效的fd

            fd_set rfds; // 读文件描述符集
            FD_ZERO(&rfds); // 初始化

            // 找出最大的文件描述符
            int maxfd = _listenSock;

            for(int i = 0; i < NUM; ++i)
            {
                if(_rdArray[i] == FD_NONE) continue;
               
                // 找出最大的文件描述符
                if(maxfd < _rdArray[i]) maxfd = _rdArray[i];
                // 有效的文件描述符设置到select中
                FD_SET(_rdArray[i], &rfds);
            }

            int n = select(maxfd + 1, &rfds, nullptr, nullptr, nullptr);
            // select第一个参数为最大文件描述符 + 1,这里最大的文件描述符就是maxfd
            // 中间只关心读文件描述符集,所以只搞了一个,后面两个都是空
            // 最后一个是timeout,先演示一下nullptr为空,阻塞等待
            
            // timeval tv;
            // tv.tv_sec = 5;
            // tv.tv_usec = 0;
            // int n = select(_listenSock + 1, &rfds, nullptr, nullptr, &tv);
            switch(n)
            {
            case 0:
                LogMessage(DEBUG, _F, _L, "time out");
                break;
            case -1:
                LogMessage(ERROR, _F, _L, "select err, errno::%d, strerror::", errno, strerror(errno));
                break;
            default:
                LogMessage(NORMAL, _F, _L, "fd is ready");
                EventHandler(rfds);
                break;
            }
        }
    }

    void EventHandler(fd_set& rfds)
    {
        for(int i = 0; i < NUM; ++i)
        {
            // 是否有效
            if(_rdArray[i] == FD_NONE) continue;

            // 是否就绪
            if(FD_ISSET(_rdArray[i], &rfds))
            {
                if(i == 0)// 是listenSock
                {
                    Accepter();
                }
                else // 是通信的sock
                {
                    Reader(i);
                }
            }
        }

        // if(FD_ISSET(_listenSock, &rfds))
        // {
        //     // 客户端IP + 端口
        //     std::string clientIP;
        //     uint16_t clientPort;

        //     int sock = Sock::Accept(_listenSock, clientIP, clientPort);
        //     assert(sock >= 0);
        //     LogMessage(NORMAL, _F, _L, "get link -->client[%s:%d]", clientIP.c_str(), clientPort);
            
        //     // 通信过程...
        //     int pos = 1;
        //     for(; pos < NUM; ++pos)
        //     {// 找FD_NONE
        //         if(_rdArray[pos] == FD_NONE) break;
        //     }
        //     if(pos == NUM)
        //     {// 没找到
        //         std::cout << "文件描述符集已满, 无法继续接收连接" << std::endl;
        //         close(sock);
        //         return;
        //     }
        //     else
        //     {// 找到了
        //         std::cout << "new fd::" << sock << std::endl;
        //         _rdArray[pos] = sock;
        //     }
        // }
    }

    void Accepter()
    {
        // 客户端IP + 端口
        std::string clientIP;
        uint16_t clientPort;

        int sock = Sock::Accept(_listenSock, clientIP, clientPort);
        assert(sock >= 0);
        LogMessage(NORMAL, _F, _L, "get link -->client[%s:%d]", clientIP.c_str(), clientPort);
        
        // 通信过程...
        int pos = 1;
        for(; pos < NUM; ++pos)
        {// 找FD_NONE
            if(_rdArray[pos] == FD_NONE) break;
        }
        if(pos == NUM)
        {// 没找到
            std::cout << "文件描述符集已满, 无法继续接收连接" << std::endl;
            close(sock);
            return;
        }
        else
        {// 找到了
            std::cout << "new fd::" << sock << std::endl;
            _rdArray[pos] = sock;
        }
    }

    void Reader(int pos)
    {
        char buff[128] = {0};
        ssize_t res = read(_rdArray[pos], buff, sizeof(buff) - 1);
        if(res > 0)
        {// 读取到数据
            buff[res - 1] = 0;
            printf("get client[%d] message # %s\n", _rdArray[pos], buff);
        }
        else if(res == 0)
        {// 对端关闭连接
            printf("client[%d] closed, me too\n", _rdArray[pos]);
            close(_rdArray[pos]);
            // 记得要把数组中对应位置置为FD_NONE
            _rdArray[pos] = FD_NONE;
        }
        else
        {// read出错
            printf("read err, close client[%d]\n", _rdArray[pos]);
            std::cout << "read err ::" << errno << strerror(errno) << std::endl; 
            close(_rdArray[pos]);
            // 记得要把数组中对应位置置为FD_NONE
            _rdArray[pos] = FD_NONE;
        }
    }

    void showFds()
    {
        std::cout << "fds ::";
        for(auto e : _rdArray)
        {
            if(e == FD_NONE) continue;
            std::cout << e << ' ';
        }

        std::cout << std::endl;
    }

    ~SelectServer()
    {
        if(_listenSock >= 0)
        {
            close(_listenSock);
        }
    }


private:
    uint16_t _port;
    int _listenSock;
    int _rdArray[NUM];
};

主函数:

#include "SelectServer.hpp"
#include 

int main()
{
    std::unique_ptr<SelectServer> pss(new SelectServer);
    pss->Start();
    
    return 0;
}
select优缺点

优点:

  1. 效率高,相比于前面多线程多进程的服务器,select服务器比多进/线程服务器效率会更高。select()函数可以同时等待多个文件描述符,而不需要建立多个线程、进程就可以实现一对多的通信。但是select放在整个多路转接中的效率还是一般的,好的都在后面讲。
  2. 应用场景:有大量的连接,但是只有少量是活跃的。前面的多进程/多线程服务器,有一个连接就要维护一个进程/线程的空间,对于资源的消耗会很大。但这里select不需要维护这些空间,只有一个线程。

其实任何一个多路转接都具备上述两个优点。

缺点:

  1. 为了维护第三方数组,select服务器会充满大量的遍历,os底层帮我们关心fd的时候也要遍历。
  2. 每一次都要对select参数进行重新设定
  3. 能够同时管理的fd的个数是有上限的,一千多个,有点少,中小型应用还好,用户量一大就扛不住。
  4. 因为几乎每一个参数都是输入输出型,select一定会频繁的进行用户到内核,内核到用户的参数数据拷贝。
  5. 编写代码比较复杂,主要还是前面4个缺点导致的。

poll可以解决这里的部分缺点。下面就来说说poll。

poll

poll也是多路转接的方案,也是只负责IO中的等。

poll将输入输出参数做了分离,不用再对参数重新设定了。而且解决了同时管理fd个数上限的问题。

【网络】五中IO模型介绍 + 多路转接中select和poll服务器的简单编写_第73张图片

三个参数。fds是看成数组,nfds就是数组中元素的个数。等会细说pollfd结构体。

timeout是一个毫秒级别的时间单位,比如说你传一个1000,就是未就绪1s后超时,如果传0就是非阻塞,如果传-1就是阻塞。

poll返回值大于零,是几就是几个文件描述符就绪了。
等于零,超时。
小于零,poll失败,代码写错了,比如根本不存在5号文件描述符但是你把文件描述符添加到了第一个参数数组中。

poll也是负责两个大问题:

  1. 用户告诉内核,你要帮我关心哪些fd的哪些事件
  2. 内核告诉用户,哪些事件已经就绪了。

第一个参数fds就能解决这两个问题。

这个数组中元素类型为pollfd:
【网络】五中IO模型介绍 + 多路转接中select和poll服务器的简单编写_第74张图片

三个成员:
fd就是文件描述符,不管是用户到内核还是内核到用户,都不会修改fd。
events就是你要让os关心的fd的什么事件,是一个输入型参数。
revents算是一个输出型参数,表明你要让os关心的fd中的事件是否就绪。
这样每次调用poll的时候就不会像select那样重新初始化了。

select中有读、写、异常这样的事件,events如何表示这类事件呢?
想一想文件操作open,当我们想要打开文件的标记位,就是用或运算,比如O_CREAT,O_WRONLY,O_RDONLY这样的标记位。同理,poll用的也是这样的宏来表示某种特定事件:

【网络】五中IO模型介绍 + 多路转接中select和poll服务器的简单编写_第75张图片

我已经把常用的标出来了。in、out就是读写,err就是错误。剩下的都是一些属于异常范畴的,因为event类型为short,只有16个位,所以最多只能有16中标记。上面这些每一个都是宏,用或即可添加选项。

看看POLLPRI,高优先级数据可读,前面我讲TCP报头的时候其中有一个urg标志位,还有一个紧急指针,在这里就可配合POLLPRI来实现。

来一个示例:

#include 
#include 
#include 
int main()
{
	// 这里就监测一下标准输入,就不搞那么多文件描述符了
    struct pollfd poll_fd;
    poll_fd.fd = 0;
    poll_fd.events = POLLIN; // 标准输入的读事件
	
    for (;;)
    {
    	// 每隔一秒poll一次
        int ret = poll(&poll_fd, 1, 1000);
        if (ret < 0)
        { // poll错误
            perror("poll");
            continue;
        }
        if (ret == 0)
        { // 超时
            printf("poll timeout\n");
            continue;
        }
        
        // 事件准备就绪
        if (poll_fd.revents == POLLIN)
        {// 判断一下是不是读事件就绪了
            char buf[1024] = {0};
            read(0, buf, sizeof(buf) - 1);
            printf("stdin:%s", buf);
        }
    }
}

运行:
【网络】五中IO模型介绍 + 多路转接中select和poll服务器的简单编写_第76张图片

下面来写写poll服务器,其实和select还是有点像的,写起来比select简单一点,这里用一下select的大致框架:
【网络】五中IO模型介绍 + 多路转接中select和poll服务器的简单编写_第77张图片

其中一些函数参数如果用到了再添加。

首先poll要有一个数组,元素类型为pollfd:
【网络】五中IO模型介绍 + 多路转接中select和poll服务器的简单编写_第78张图片

构造函数初始化:
【网络】五中IO模型介绍 + 多路转接中select和poll服务器的简单编写_第79张图片

打印有效文件描述符:
【网络】五中IO模型介绍 + 多路转接中select和poll服务器的简单编写_第80张图片

启动:
【网络】五中IO模型介绍 + 多路转接中select和poll服务器的简单编写_第81张图片

EventHandler:
【网络】五中IO模型介绍 + 多路转接中select和poll服务器的简单编写_第82张图片

接收连接:
【网络】五中IO模型介绍 + 多路转接中select和poll服务器的简单编写_第83张图片

读取数据:
【网络】五中IO模型介绍 + 多路转接中select和poll服务器的简单编写_第84张图片

测试,连一个:
【网络】五中IO模型介绍 + 多路转接中select和poll服务器的简单编写_第85张图片

连两个:
【网络】五中IO模型介绍 + 多路转接中select和poll服务器的简单编写_第86张图片

连三个:
【网络】五中IO模型介绍 + 多路转接中select和poll服务器的简单编写_第87张图片

发消息:
【网络】五中IO模型介绍 + 多路转接中select和poll服务器的简单编写_第88张图片

挨个退:

【网络】五中IO模型介绍 + 多路转接中select和poll服务器的简单编写_第89张图片

正常。

完整代码:
服务器封装的头文件:

#include "Sock.hpp"
#include 
#include 

#define FD_NONE -1 // 每个fd的初始化的值
#define NFDS 100 // 数组元素个数

class PollServer
{
public:
    PollServer(uint16_t port = 8080)
        : _port(port)
        , _nfds(NFDS)
    {
        // 创建套接字
        _listenSock = Sock::Socket();
        
        // bind绑定
        Sock::Bind(_listenSock, _port);

        // 设置监听状态
        Sock::Listen(_listenSock);

        // 开辟空间
        _fds = new pollfd[_nfds];
        for(int i = 0; i < _nfds; ++i)
        { // 初始化
            _fds[i].fd = FD_NONE;
            _fds[i].events = _fds[i].revents = 0;
        }

        // 第零个位置给成listenSock
        _fds[0].fd = _listenSock;
        _fds[0].events = POLLIN; // 关系listenSock的读
    }

    void showFds()
    {
        std::cout << "fds:: ";
        for(int i = 0; i < _nfds; ++i)
        {
            if(_fds[i].fd == FD_NONE) continue;
            
            std::cout << _fds[i].fd << ' ';
        }
        std::cout << std::endl;
    }

    void Start()
    {
        while(1)
        {
            showFds();

            // 1s间隔
            int res = poll(_fds, _nfds, -1);
            if(res > 0)
            { // 有文件描述符就绪
                std::cout << "some fds' ready" << std::endl;
                EventHandler();
            }
            else if(res == 0)
            { // 超时
                std::cout << "time out" << std::endl;
            }
            else
            { // poll出错
                printf("poll err, errno[%d], strerror::%s", errno, strerror(errno));
            }
        }
    }

    void EventHandler()
    {
        for(int i = 0; i < _nfds; ++i)
        {
            // 第i位不是有效文件描述符
            if(_fds[i].fd == FD_NONE) continue;
            
            // 读事件时候就绪
            if(_fds[i].revents & POLLIN)
            {
                if(i == 0)
                    Accepter();
                else
                    Reader(i);                
            }
        }
    }

    // 接收连接
    void Accepter()
    {
        // 获取连接
        std::string clientIP;
        uint16_t clientPort;
        int sock = Sock::Accept(_listenSock, clientIP, clientPort);

        // 找空位置放sock
        int pos = 1;
        for(; pos < _nfds; ++pos)
        {
            if(_fds[pos].fd == FD_NONE) break;
        }

        if(pos == _nfds)
        { // 没找到,不过这里也可以选择对_fds进行扩容,但是我懒得搞了,你要是有兴趣可以自己搞一下
            std::cout << "_nfds is full" << std::endl;
            close(sock);
        }
        else
        { // 找到了
            std::cout << "get a new link ::" << sock << std::endl;
            _fds[pos].fd = sock;
            _fds[pos].events = POLLIN;
        }
    }

    // 读取数据
    void Reader(int pos)
    {
        char buff[128];
        int res = read(_fds[pos].fd, buff, sizeof(buff) - 1);
        if(res > 0)
        { // 读取到数据
            buff[res] = 0;
            std::cout << "client #" << buff << std::endl;
        }
        else if(res == 0)
        { // 对端关闭连接
            std::cout << "clinet closed" << std::endl;
            // 记得后续工作
            close(_fds[pos].fd);
            _fds[pos].fd = FD_NONE;
            _fds[pos].events = _fds[pos].events = 0;
        }
        else
        { // 读取出错
            printf("read err, errno[%d], strerror::%s", errno, strerror(errno));
            // 记得后续工作
            close(_fds[pos].fd);
            _fds[pos].fd = FD_NONE;
            _fds[pos].events = _fds[pos].events = 0;
        }
    }


    ~PollServer()
    {
        if(_listenSock >= 0) close(_listenSock);

        if(_fds != nullptr) delete[] _fds;
    }


private:
    uint16_t _port;
    int _listenSock;
    pollfd *_fds;
    int _nfds;
};

主函数:

#include "PollServer.hpp"
#include 

int main()
{
    std::unique_ptr<PollServer> pps(new PollServer);
    pps->Start();
    
    return 0;
}
poll的优缺点

优点:

  1. 效率高(更select一样)

  2. 适用场景:有大量的连接但是只有少量连接是活跃的,节省资源

  3. 输入输出参数是分离的,不需要进行大量的重置。

  4. poll参数nfds可以自行设定,没有上限(除非内存不够)。

缺点:

  1. poll依旧需要不少的遍历,在用户层监测事件就绪与内核监测fd就绪,都是一样的,当只有几个就绪时就要将整个数组遍历一遍,效率比较低(连接越多越低)

  2. poll需要用户和内核进行拷贝,更多的是需要内核到用户的拷贝,少不了的。

  3. poll代码比select容易,但还是有点复杂

最需要关心的缺点就是第一点,用户还是要维护数组。

为了解决上述问题,epoll出现了,强化版本的poll,要比poll强得多,关于epoll下一篇再详细说。

本篇就先讲到这里。下一篇详细讲解多路转接中最重要的epoll。

到此结束。。。

你可能感兴趣的:(网络,网络,服务器,运维,多路转接,linux,centos)