【网络】五中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服务器的编写

关于多路转接的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, // 0NORMAL, // 1WARING, // 2ERROR, // 3FATAL // 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_DEBUGif(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);// 创建失败返回-1if(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和portstatic void Bind(int listenSock, uint16_t port, const std::string& ip = "0.0.0.0"){sockaddr_in local; // 各个字段填充memset(&local, 0, sizeof(local));// 若为空字符串就绑定当前主机所有IPlocal.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 + portstatic 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){// 这里没连接上不能说直接退出,就像张三没有揽到某个客人餐馆就不干了,所以日志等级为ERRORLogMessage(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 // 数组初始化的值,表明没有这个fdclass 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(); // 每次打印一下数组中有效的fdfd_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_NONEif(_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();elseReader(i);                }}}// 接收连接void Accepter(){// 获取连接std::string clientIP;uint16_t clientPort;int sock = Sock::Accept(_listenSock, clientIP, clientPort);// 找空位置放sockint 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.mzph.cn/news/134464.shtml

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

相关文章

第二证券:消费电子概念活跃,博硕科技“20cm”涨停,天龙股份斩获10连板

消费电子概念7日盘中再度拉升&#xff0c;到发稿&#xff0c;博硕科技“20cm”涨停&#xff0c;光大同创、波长光电涨超10%&#xff0c;易德龙、向阳科技、得润电子、天龙股份、同兴达等涨停。 博硕科技强势涨停&#xff0c;公司昨日在接受安排调研时表明&#xff0c;公司从上…

前端框架Vue学习 ——(四)Axios

文章目录 Axios 介绍Axios 入门Vue项目中使用 Axios Axios 介绍 介绍: Axios 对原生的 Ajax 进行了封装&#xff0c;简化书写&#xff0c;快速开发。&#xff08;异步请求&#xff09; 官网: https://www.axios-http.cn/ 官网介绍&#xff1a;Axios 是一个基于 promise 网络请…

数据结构和算法是人工智能的基石

文章目录 1. 引言2. 主要内容3. 联合推荐4. 购买方式5. 总结 1. 引言 数据结构和算法是计算机科学的基石&#xff0c;是计算机的灵魂&#xff0c; 要想成为计算机专业人员&#xff0c;学习和掌握算法是十分必要的。不懂数据结构和算法的人不可能写出效率更高的代码。计算机科学…

php实现钉钉机器人推送消息和图片内容(完整版)

先来看下实现效果: 代码如下: function send_dingtalk_markdown($webhook , $title , $message "", $atMobiles [], $atUserIds []) {$data ["msgtype" > "markdown","markdown" > ["title" > $title,&quo…

Luckysheet 实现excel多人在线协同编辑

前言 前些天看到Luckysheet支持协同编辑Excel&#xff0c;正符合我们协同项目的一部分&#xff0c;故而想进一步完善协同文章&#xff0c;但是遇到了一下困难&#xff0c;特此做声明哈&#xff0c;若侵权&#xff0c;请联系我删除文章&#xff01; 若侵犯版权、个人隐私&#x…

五、计算机网络

&#xff08;一&#xff09;OSI/RM 七层模型 七层模型是计算机网络的基石&#xff0c;整个计算机网络是构建与七层模型之上的。 在数据链路层&#xff0c;数据开始以帧为单位&#xff0c;网卡的 MAC 地址就是数据帧的地址&#xff0c;数据的传输开始有地址了。 局域网是工作…

目标检测中的评价指标

目标检测中的评价指标 将检测目标分为正样本和负样本。 真阳性&#xff08;true positives , TP&#xff09; : 正样本被正确识别为正样本。 假阳性&#xff08;false positives, FP&#xff09;: 负样本被错误识别为正样本。 假阴性&#xff08;false negatives, FN&#…

Floor报错注入理论及实战

rand()函数&#xff1a;随机返回0-1之间的小数 floor()函数&#xff1a;小数向下取证书。向上取整数ceiling() concat_ws函数&#xff1a;将括号内数据用第一个字段连接起来 group by子句&#xff1a;分组语句&#xff0c;常用语结合统计函数&#xff0c;根据一个或多个列&a…

word统计全部字符数。

问题描述&#xff1a;在投稿SCI论文时&#xff0c;有时会要求提交一个highlight文档&#xff0c;要求不超过85个字符。 具体如下&#xff1a;maximum 85 characters per bullet point including spaces 这里的字符不单单包括字母和汉字&#xff0c;还包括标点和空格键。那么如…

5G及其后的5G非地面网络:趋势和研究挑战-HARQ部分

NTN组件纳入5G架构第一步 在NTN SI中定义了一组架构选项。就NT部分而言&#xff0c;已确定了两大类&#xff1a;星载&#xff08;即基于卫星的通信平台&#xff09;和机载&#xff08;即HAPS&#xff09;设备 并行管理HARQ最大进程数 NHARQRTT(NTX−1)2μ NTX&#xff1a;传输…

【Vue】vant2使用van-tree-select实现【全选、反选、搜索】,自定义组件,拿去即用。2.0版本保姆级教程

系列文章目录 这是原篇教程&#xff0c;本篇为升级版&#xff0c;旧版已废弃。对你们不友好。 【Vue】vue2移动端 &#xff0c;vant2使用van-tree-select分类选择实现【全选】和【取消全选】、【搜索过滤当前children】&#xff0c;只影响当前显示children&#xff0c;并且去重…

破解密码 LLM(代码LLM如何从 RNN 发展到 Transformer)

舒巴姆阿加瓦尔 一、说明 近年来&#xff0c;随着 Transformer 的引入&#xff0c;语言模型发生了显着的演变&#xff0c;它彻底改变了我们执行日常任务的方式&#xff0c;例如编写电子邮件、创建文档、搜索网络甚至编码方式。随着研究人员在代码智能任务中应用大型语言模型&am…

Docker概述

Docker概述 Docker概述Docker安装Docker命令 镜像命令容器命令操作命令 … Docker镜像容器数据卷DockerFileDocker网络管理IDEA整合DockerDocker ComposeDocker Swarm 简化版的K8s Docker为什么出现&#xff1f; 传统的项目部署&#xff0c;环境配置是十分麻烦&#xff0c;第…

如何利用JSON Schema校验JSON数据格式

最近笔者在工作中需要监控一批http接口&#xff0c;并对返回的JSON数据进行校验。正好之前在某前端大神的分享中得知这个神器的存在&#xff0c;调研一番之后应用在该项目中&#xff0c;并取得了不错的效果&#xff0c;特地在此分享给各位读者。<br style"box-sizing: …

STM32 IIC 实验

1. 可以选择I2C1&#xff0c;也可以选择I2C2&#xff0c;或者同时选择&#xff0c;同时运行 配置时钟信号 为节约空间&#xff0c;选择这两个&#xff0c;然后选择GENERATE CODE 二、HAL_I2C_Mem_Write I2C_HandleTypeDef *hi2c&#xff1a;I2C设备句柄 uint16_t DevAddress&am…

IDEA JAVA项目 导入JAR包,打JAR包 和 JAVA运行JAR命令提示没有主清单属性

一、导入JAR包 1、java项目在没有导入该jar包之前&#xff0c;如图&#xff1a;2、点击 File -> Project Structure&#xff08;快捷键 Ctrl Alt Shift s&#xff09;&#xff0c;点击Project Structure界面左侧的“Modules”如图&#xff1a;3.在 “Dependencies” 标签…

《006.Springboot+vue之旅游信息推荐系统》【有文档】

《006.Springbootvue之旅游信息推荐系统》【有文档】 项目简介 [1]本系统涉及到的技术主要如下&#xff1a; 推荐环境配置&#xff1a;DEA jdk1.8 Maven MySQL 前后端分离; 后台&#xff1a;SpringBootMybatis; 前台&#xff1a;vueElementUI; [2]功能模块展示&#xff1a; …

nodejs express uniapp 图书借阅管理系统源码

开发环境及工具&#xff1a; nodejs&#xff0c;mysql5.7&#xff0c;HBuilder X&#xff0c;vscode&#xff08;webstorm&#xff09; 技术说明&#xff1a; nodejs express vue elementui uniapp 功能介绍&#xff1a; 用户端&#xff1a; 登录注册 首页显示轮播图&am…

为全志T507-H开发板配置Samba服务,高效实现跨系统的文件共享

作为一款经典的国产芯&#xff0c;全志T507-H芯片被广泛应用于车载电子、电力、医疗、工业控制、物联网、智能终端等诸多领域当中&#xff0c;而在各种复杂的嵌入式Linux应用场景当中&#xff0c;“打通ARM板卡与Windows设备间的壁垒以实现跨平台的文件共享”是一项不能被忽视的…

电脑风扇控制软件 Macs Fan Control Pro mac中文版功能介绍

Macs Fan Control mac是一款专门为 Mac 用户设计的软件&#xff0c;它可以帮助用户控制和监控 Mac 设备的风扇速度和温度。这款软件允许用户手动调整风扇速度&#xff0c;以提高设备的散热效果&#xff0c;减少过热造成的风险。 Macs Fan Control 可以在菜单栏上显示当前系统温…