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

news2024/11/26 22:17:12

高级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服务器的编写

关于多路转接的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必须经历的两部就是等和数据拷贝。

看图:
在这里插入图片描述

如果进程想要访问磁盘上的文件,那就得先打开这个文件,而文件 = 内容 + 属性,所以打开文件后,os要为文件创建相应的struct FILE结构体以维护文件的属性,也就是在内存中维护,而内存是惰性加载的,不会说将文件中的所有数据全部加载完,因为很多数据不一定能用上,os可能会对文件预加载,也就是先加载一部分,当进程想要修改文件中的内容时,就会先将需要的数据加载到内存里:
在这里插入图片描述
此时就是进程先调用的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中的钓鱼一样。

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

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

再来介绍个东西,鱼漂,钓鱼佬应该很熟悉,但是没钓过鱼的同学可能很陌生,看图:
在这里插入图片描述

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

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

张三钓鱼的时候死死盯住鱼漂,啥也不干,非常专注,鱼漂不动他不动。
李四耐不住性子,看一会手机再看鱼漂有反应没,没反应就接着看手机。
王五拿了个铃铛,挂在鱼杆后面,一直在玩手机,铃铛一响就赶紧收杆。
赵六是个方圆五公里内的富二代,一下子拿了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模型:
在这里插入图片描述
左边对应用户空间,右边对应os的内核空间。

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

非阻塞IO

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

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

非阻塞IO往往需要程序员循环的方式反复尝试读写文件描述符, 这个过程称为轮询. 这对CPU来说是较大的浪费, 一般只有特定场景下才使用
在这里插入图片描述

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

信号驱动

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

来看看这个信号:
在这里插入图片描述

流程:
在这里插入图片描述
这里涉及到了信号的相关操作,如果你不懂信号,可以看看我这一篇:信号详解

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

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

多路转接

先来看流程图:

在这里插入图片描述

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

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

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

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

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

异步IO

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

在这里插入图片描述

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

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

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

小结

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

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

代码演示

非阻塞IO

前面我所有的博客都是阻塞式的IO,想要变成非阻塞,就需要在打开文件的时候就设置打开文件的选项O_NONBLOCK。
在这里插入图片描述

还有创建套接字也一样可以设置:
在这里插入图片描述

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

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

但是这样有点麻烦,我们可以用同一的方式来进行非阻塞的设置,即fcntl函数:
在这里插入图片描述
参数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:
在这里插入图片描述
上面0就是标准输入,这就不细讲了,最开始给的那篇博客中有。

此时运行起来就会阻塞在这里:
在这里插入图片描述
因为一直在等待键盘对应文件的资源就绪,输入了之后才等于是资源就绪了:
在这里插入图片描述

然后再来搞一下非阻塞,简单封装一下fcntl:
在这里插入图片描述
这里就是对F_GETFL和F_SETFL的使用,先用F_GETFL获取原先文件描述符对应文件的状态,然后再用F_SETFL来设置文件状态,就是再添加一个非阻塞的标志位,像位图一样,用一个 | 就行,在原始的f1标志位上新增一个非阻塞的标志位,不影响其他标志位。

在前面对0设置非阻塞:
在这里插入图片描述

运行:
在这里插入图片描述
一直在打印err。

不过打印太快了,加一个sleep控制一下:
在这里插入图片描述

这样打印的慢一点:
在这里插入图片描述

我输入后也可以读取:
在这里插入图片描述
但是用起来有点怪,因为打印的时候是往屏幕上打的,输入的时候也是要在屏幕上显示。

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

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

运行:
在这里插入图片描述

所以如果read失败的errno是11,就代表其实read没出错,不过是底层数据没有就绪,所以 s <= 0的时候可以再判断一下errno是否等于11。不过可以不用数字,刚刚再介绍非阻塞的时候说了一个EWOULDBLOCK字段,这个字段的值其实就是11:
在这里插入图片描述
很多地方判断errno是否是11都是这样用的:
在这里插入图片描述
send、recv等IO函数非阻塞的时候也会返回这个EWOULDBLOCK,但是我感觉这两个一个就够了,如果有懂的老铁可以在评论给我解答一下吗,谢谢了。

在这里插入图片描述

运行:
在这里插入图片描述

还有一个很重要的字段,EINTER,就是interrupt,被打断了,用于在等的阶段被其他东西打断了,比如说进程/线程可能收到某个信号,此时os就会将进程/线程唤醒去处理信号,可是处理信号了就不回来了,此时errno就会被设置成EINTER,表示中断了,所以也可以再添加一个:
在这里插入图片描述

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

多路转接

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

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

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

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

认识一下select接口:
在这里插入图片描述

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的结构体:
在这里插入图片描述
其中tv_sec单位是以秒,tv_usec单位是微秒。
这个结构体可以配合着gettimeofday来用:

在这里插入图片描述
这个函数可以获取当前系统的时间戳,传一个timeval结构体来获取参数为tz区域的时间,tz给空就是本地的时间。带着C语言中的time函数演示一下:
在这里插入图片描述
在这里插入图片描述
打印出来前面秒级别的和C中的time一样,.后面的是微秒级别的
再说回最后一个参数timeout
在这里插入图片描述
这个参数可以设置等待多个参数的策略,有三种:

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

中间三个参数:
在这里插入图片描述

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

看一下系统中的fd_set最多能容纳多少个文件描述符:
在这里插入图片描述
这里乘以8是因为sizeof求的是字节数,而位图是看有多少比特位的,一个字节8位:
在这里插入图片描述
.
.
再来看这三个参数
在这里插入图片描述
三个参数在用法上都是一样的,我就挑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 <cstdio>
#include <cstring>
#include <ctime>
#include <cstdarg>

#include <unistd.h>

#include <vector>

// 文件名
#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 <iostream>
#include <string>
#include <memory>

#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <sys/types.h>

#include <unistd.h>

// 对套接字相关的接口进行封装
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;
    }

};

然后对服务器简单封装一下:
在这里插入图片描述

这里还剩下一步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:
在这里插入图片描述

这里根据select的返回值来选择该干什么事情:
在这里插入图片描述

这样运行起来的话会先阻塞:
在这里插入图片描述

用telnet连接:
在这里插入图片描述

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

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

timeout 为 {0, 0}

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

在这里插入图片描述

刚运行起来就一直打印time out:
在这里插入图片描述

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

timeout 为 {5, 0}

我再来把timeout改成{5, 0}:
在这里插入图片描述
刚运行没问题:
在这里插入图片描述
但是5s后又开始疯狂打印了:
在这里插入图片描述

因为timeout参数是输入输出型的,第一次作为输出参数会被改成{0, 0},而我刚刚故意将tv的定义放在了while外面,所以就会导致后续的tv都变成{0, 0},这样就会和上面的情况一样,变成了非阻塞IO,所以要把tv定义放在while中或者在while中更新tv中的值:
在这里插入图片描述

这样就不会那么快:
在这里插入图片描述

调用accept

再来说回timeout为nullptr的情况:
在这里插入图片描述

因为接收连接后还会有后续动作,所以再给一个函数把后续动作放到一起更方便观察,这里我们是知道只有一个listenSock的,所以写的简单点,等后面有新场景了再做修改:
在这里插入图片描述

运行起来:
在这里插入图片描述
一切正常。

这里我故意把通信过程留下来了,请问通信的时候能直接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. 遍历数组,找到就绪的事件,根据就绪的事件完成对应的动作。
    }
重写

在这里插入图片描述
在这里插入图片描述
这里直接将数组开完整,select最大能监听的文件描述符的个数为1024个,也就是fd_set位图的位数大小,前面也讲过了。用这个数组来存放合法的sock(合法就是指能用的)。

构造函数里面初始化一下:
在这里插入图片描述

那么代码就要改改了:
在这里插入图片描述

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

每次都要对数组进行操作,变化的就是红框中的:
在这里插入图片描述

EventHandler也要改:
在这里插入图片描述
想要将sock添加到select中,其实只要将sock放到数组中就行,EventHandler调用完毕后会循环回去,遍历后就会放到位图中。

将新的连接加入select中:
在这里插入图片描述
测试一下,刚运行:
在这里插入图片描述

连一个:
在这里插入图片描述

连两个:
在这里插入图片描述

很正常。

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

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

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

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

把这两个函数实现给出:
在这里插入图片描述
其实接收连接就是刚刚写的代码。

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

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

然后EventHandler改成:
在这里插入图片描述

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

在这里插入图片描述

连接一个:
在这里插入图片描述

连接两个:
在这里插入图片描述

连接三个:

在这里插入图片描述

第一个连接通信:
在这里插入图片描述

第二个连接通信:
在这里插入图片描述

第三个连接通信:
在这里插入图片描述

挨个退出:
在这里插入图片描述

成功。

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

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

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

完整代码

服务器头文件:

#include "Sock.hpp"
#include <assert.h>

#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 <memory>

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个数上限的问题。

在这里插入图片描述

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

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

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

poll也是负责两个大问题:

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

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

这个数组中元素类型为pollfd:
在这里插入图片描述

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

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

在这里插入图片描述

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

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

来一个示例:

#include <poll.h>
#include <unistd.h>
#include <stdio.h>
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);
        }
    }
}

运行:
在这里插入图片描述

下面来写写poll服务器,其实和select还是有点像的,写起来比select简单一点,这里用一下select的大致框架:
在这里插入图片描述

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

首先poll要有一个数组,元素类型为pollfd:
在这里插入图片描述

构造函数初始化:
在这里插入图片描述

打印有效文件描述符:
在这里插入图片描述

启动:
在这里插入图片描述

EventHandler:
在这里插入图片描述

接收连接:
在这里插入图片描述

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

测试,连一个:
在这里插入图片描述

连两个:
在这里插入图片描述

连三个:
在这里插入图片描述

发消息:
在这里插入图片描述

挨个退:

在这里插入图片描述

正常。

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

#include "Sock.hpp"
#include <assert.h>
#include <poll.h>

#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 <memory>

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。

到此结束。。。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1177571.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

已经不做程序媛4年半了,后悔么?不好说...但再次看到之前写的博客,真的感慨万分。

四年半前&#xff0c;我还在大四实习&#xff0c;做了一年Java开发。四年半后&#xff0c;我是地理信息行业的一名销售经理。 今天&#xff0c;突然点开了CSDN&#xff0c;看到自己的博客有很多人看过&#xff0c;霎时间感慨万千…因为上一次这么认真的对待自己&#xff0c;竟…

网络运维Day06

文章目录 磁盘空间管理一块硬盘的“艺术”之旅识别磁盘分区规划分区模式MBR(主启动记录模式)GPT分区 使用GPT分区方案分区 格式化与挂载使用格式化挂载使用 实现开机自动挂载实验 光盘挂载交换空间 总结 磁盘空间管理 磁盘空间管理 扇区默认512字节 一块硬盘的“艺术”之旅 …

【Spring】spring中存储Bean(对象)的相关注解及相关用法

五大注解&#xff1a;Controller&#xff0c;Service&#xff0c;Repository&#xff0c;Component&#xff0c;Configuration 1、controller 控制器&#xff08;注入服务&#xff09; 控制层,接收请求,对请求进⾏处理,并进⾏响应。用于标注控制层&#xff0c;相当于struts中的…

【mmcv报错】ModuleNotFoundError: No module named ‘mmcv.runner

跑一个代码需要用到mmcv和mmseg 其中有两行代码&#xff1a; from mmcv.runner import load_checkpoint from mmseg.utils import get_root_logger我先是按照官方推荐的安装方法去安装了mmcv和mmseg pip install -U openmim mim install mmcv它会自动帮你安装mmengine 我的cu…

关于Vue使用props传值遇到的一些问题

一、The data property “tableData” is already declared as a prop. Use prop default value instead. 翻译过来&#xff1a;数据属性“tableData”已声明为prop。请改prop默认值。 将父组件的prop传过去变量改一下 二、prop传值&#xff0c;子组件比父组件先渲染&#…

内存取证 worldskills3.vmem与gs02.jpg题目WP

worldskills3.vmem 题目信息 获取admin用户密码是多少&#xff1f; 获取ip和主机名是什么&#xff1f; 获取桌面上的flag.txt文件的内容是什么&#xff1f; 服务器存在一个挖矿病毒&#xff0c;矿池地址是&#xff1f; 恶意代码在系统中注册了服务&#xff0c;服务名是什么&a…

pytest 的使用===谨记

发现用例的规则 a) 文件test_.py开头和_test.py结尾 b) Test开头的类中test开头的方法&#xff08;测试类不能带有__init__方法&#xff09; c) 模块中test开头的函数&#xff08;可以不在class中&#xff09; 注意点&#xff1a; pytest是以方法为单位发现用例的&#xff0c;你…

单通道低压 H 桥电机驱动芯片AT9110H 兼容L9110 马达驱动芯片

H桥直流电机驱动电路是一种用于控制直流电机运转的电路&#xff0c;其主要特点是可以实现正反转控制&#xff0c;控制电机转速和方向&#xff0c;同时也具有过流保护功能。 H桥电路由四个功率晶体管和一些辅助电路组成&#xff0c;其中两个晶体管用于控制电机正转&#xff0c;…

SciCoMap颜色包_共180种--全平台可用

SciCoMap颜色包_共180种–全平台可用 往期推荐&#xff1a; 海洋专用cmocean颜色包_共22种–全平台可用 Python语言_matplotlib包_共80种–全平台可用 Python语言_single_color_共140种–全平台可用 R语言_RColorBrewer包–全平台可用 R语言gplots包的颜色索引表–全平台可用 …

Linux-Shell命令行解释器的模拟实现

引言&#xff1a;本篇文章主要是简单实现一个shell命令行解释器&#xff0c;可以支持基础常见的linux的命令&#xff0c;支持内建命名echo、cd&#xff0c;同时支持重定向的操作&#xff01; 一、代码剖析 1. 头文件引入&#xff1a; 因代码是在linux下实现&#xff0c;引入的…

性能测试知多少---了解前端性能

我的上一篇博文中讲到了响应时间&#xff0c;我们在做性能测试时&#xff0c;能过工具可以屏蔽客户端呈现时间&#xff0c;通过局域网的高宽带可以忽略数据传输速度的障碍。这并不是说他们不会对系统造成性能影响。相反&#xff0c;从用户的感受来看&#xff0c;虽然传输速度受…

阿里巴巴1688商品详情 API 接口示例

1688.item_get 公共参数 请求地址: https://o0b.cn/anzexi 名称类型必须描述keyString是调用key&#xff08;必须以GET方式拼接在URL中&#xff09;secretString是调用密钥api_nameString是API接口名称&#xff08;包括在请求地址中&#xff09;[item_search,item_get,item_…

软件外包开发质量控制方法

在软件外包开发项目中&#xff0c;质量控制是确保交付的软件符合预期质量标准的关键步骤。以下是一些常用的软件外包开发质量控制方法&#xff0c;希望对大家有所帮助。北京木奇移动技术有限公司&#xff0c;专业的软件外包开发公司&#xff0c;欢迎交流合作。 需求明确&#x…

wpf添加Halcon的窗口控件报错:下列控件已成功添加到工具箱中,但未在活动设计器中启用

报错截图如下&#xff1a; 注意一下新建工程的时候选择wpf应用而不是wpf应用程序。 添加成功的控件&#xff1a;

博弈论——霍特林博弈(Hotelling Game)

0 引言 前一篇文章在特殊的伯特兰德博弈模型的基础上&#xff0c;解释了伯特兰德悖论&#xff0c;我们先简单回顾一下&#xff1a; 三个假设&#xff1a; &#xff08;1&#xff09;各寡头厂商通过选择价格进行竞争&#xff1b; &#xff08;2&#xff09;各寡头厂商生产的产品…

Devchat-AI 编程助手:Devchat-AI 尝鲜测评+场景实践

本心、输入输出、结果 文章目录 Devchat-AI 编程助手&#xff1a;Devchat-AI 尝鲜测评场景实践前言DevChat 简介DevChat 是什么DevChat AI 编程助手有哪些优势 DevChat 的申请和使用运行环境要求DevChat 的申请DevChat 激活DevChat 定价DevChat 的安装DevChat 的简单使用 相关图…

【GEE】使用GEE批量查询下载Landsat8数据

刚发了一篇Landsat8地表温度反演的博文&#xff0c;顺便分享一下如何使用GEE批量查询、下载Landsat8数据集。代码比较简单就是查询函数和导出函数&#xff0c;然后还有一个显示函数。网上的教程一大堆&#xff0c;都差不多的代码&#xff0c;在这里要感谢一些前辈们的无私奉献。…

JUC-1-并发编程基础

一 并发编程简介 1 什么是并发编程&#xff1f; 所谓并发编程是指在一台处理器上 “同时” 处理多个任务。并发是在同一实体上的多个事件。多个事件在同一时间间隔发生。 并发编程&#xff0c;从程序设计的角度来说&#xff0c;是希望通过某些机制让计算机可以在一个时间段内…

技术分享 | web自动化测试-PageObject 设计模式

为 UI 页面写测试用例时&#xff08;比如 web 页面&#xff0c;移动端页面&#xff09;&#xff0c;测试用例会存在大量元素和操作细节。当 UI 变化时&#xff0c;测试用例也要跟着变化&#xff0c; PageObject 很好的解决了这个问题。 使用 UI 自动化测试工具时&#xff08;包…

LangChain+LLM实战---Embedding、从入门到生产使用

搜索功能已经深入到我们的日常生活中&#xff0c;我们常说“Google一下就知道了”&#xff0c;用户已经开始期望几乎每个应用程序和网站都提供某种类型的搜索功能。随着有效搜索变得越来越相关(双关语)&#xff0c;寻找新的方法和体系结构来改进搜索结果对于架构师和开发人员来…