【网络】五中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模型。
先说说都是啥:
- 阻塞IO
- 非阻塞IO
- 信号驱动
- 多路转接(多路复用)
- 异步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功能就有两个:
- 帮助用户进行一次等待多个文件fd
- 当哪些文件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
这个参数可以设置等待多个参数的策略,有三种:
- 阻塞式IO,timeout设置为空。
- 非阻塞式IO,timeout设置为{0, 0}。
- 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编写代码的一般流程
再看看这个接口:
第一个参数nfds,随着我们获取的sock越来越多,需要添加到select中的sock也就会越来越多,那么就注定了每一次调用select时nfds都可能要改变,所以要对nfds动态计算。
readfds/writefds/exceptfds都是输入输出参数,输入和输出不一定会一样,比如说传入1111,输出0010,那再次输入的时候还要改成1111,所以我们每一次都要对rfds重新添加。
timeout,也是输入输出,如果设置了时间,每次都要重置。
对于1、2两点而言,主要原因是文件描述符可能每次都在变,想要完全掌握其变化就要自己将合法的文件描述符全部保存起来,用来支持更新最大fd和更新位图结构。
所以select服务器编写的时候:
需要一个第三方数组用来保存所有合法的fd,数组就是select能同时监听的fd个数(元素个数)。我这里等会就直接用原生数组来实现了,也可以用vector,会更方便一点,但至于为什么用原生数组等会写完了再说。
上面的流程大致如下:
while(1)
{
- 遍历数组,更新最大的fd,用于select中第一个参数
- 遍历数组,添加所有需要关心的fd到fd_set位图中,用于select第二个参数
- 调用select进行实践检测
- 遍历数组,找到就绪的事件,根据就绪的事件完成对应的动作。
}
重写
这里直接将数组开完整,select最大能监听的文件描述符的个数为1024个,也就是fd_set位图的位数大小,前面也讲过了。用这个数组来存放合法的sock(合法就是指能用的)。
构造函数里面初始化一下:
那么代码就要改改了:
每次都打印一下其中有效的文件描述符:
每次都要对数组进行操作,变化的就是红框中的:
EventHandler也要改:
想要将sock添加到select中,其实只要将sock放到数组中就行,EventHandler调用完毕后会循环回去,遍历后就会放到位图中。
将新的连接加入select中:
测试一下,刚运行:
连一个:
连两个:
很正常。
每次进行select的时候,若有文件描述符就绪,会有两种情况:
- 就绪的是listenSock
- 就绪的是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优缺点
优点:
- 效率高,相比于前面多线程多进程的服务器,select服务器比多进/线程服务器效率会更高。select()函数可以同时等待多个文件描述符,而不需要建立多个线程、进程就可以实现一对多的通信。但是select放在整个多路转接中的效率还是一般的,好的都在后面讲。
- 应用场景:有大量的连接,但是只有少量是活跃的。前面的多进程/多线程服务器,有一个连接就要维护一个进程/线程的空间,对于资源的消耗会很大。但这里select不需要维护这些空间,只有一个线程。
其实任何一个多路转接都具备上述两个优点。
缺点:
- 为了维护第三方数组,select服务器会充满大量的遍历,os底层帮我们关心fd的时候也要遍历。
- 每一次都要对select参数进行重新设定
- 能够同时管理的fd的个数是有上限的,一千多个,有点少,中小型应用还好,用户量一大就扛不住。
- 因为几乎每一个参数都是输入输出型,select一定会频繁的进行用户到内核,内核到用户的参数数据拷贝。
- 编写代码比较复杂,主要还是前面4个缺点导致的。
poll可以解决这里的部分缺点。下面就来说说poll。
poll
poll也是多路转接的方案,也是只负责IO中的等。
poll将输入输出参数做了分离,不用再对参数重新设定了。而且解决了同时管理fd个数上限的问题。
三个参数。fds是看成数组,nfds就是数组中元素的个数。等会细说pollfd结构体。
timeout是一个毫秒级别的时间单位,比如说你传一个1000,就是未就绪1s后超时,如果传0就是非阻塞,如果传-1就是阻塞。
poll返回值大于零,是几就是几个文件描述符就绪了。
等于零,超时。
小于零,poll失败,代码写错了,比如根本不存在5号文件描述符但是你把文件描述符添加到了第一个参数数组中。
poll也是负责两个大问题:
- 用户告诉内核,你要帮我关心哪些fd的哪些事件
- 内核告诉用户,哪些事件已经就绪了。
第一个参数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的优缺点
优点:
-
效率高(更select一样)
-
适用场景:有大量的连接但是只有少量连接是活跃的,节省资源
-
输入输出参数是分离的,不需要进行大量的重置。
-
poll参数nfds可以自行设定,没有上限(除非内存不够)。
缺点:
-
poll依旧需要不少的遍历,在用户层监测事件就绪与内核监测fd就绪,都是一样的,当只有几个就绪时就要将整个数组遍历一遍,效率比较低(连接越多越低)
-
poll需要用户和内核进行拷贝,更多的是需要内核到用户的拷贝,少不了的。
-
poll代码比select容易,但还是有点复杂
最需要关心的缺点就是第一点,用户还是要维护数组。
为了解决上述问题,epoll出现了,强化版本的poll,要比poll强得多,关于epoll下一篇再详细说。
本篇就先讲到这里。下一篇详细讲解多路转接中最重要的epoll。
到此结束。。。
相关文章:

【网络】五中IO模型介绍 + 多路转接中select和poll服务器的简单编写
高级IO 前言正式开始前面的IO函数简单过一遍什么叫做低效的IO钓鱼的例子同步IO和异步IO五种IO模型阻塞IO非阻塞IO信号驱动多路转接异步IO 小结 代码演示非阻塞IO多路转接select介绍简易select服务器timeout 为 nullptrtimeout 为 {0, 0}timeout 为 {5, 0}调用accept select编写…...

Camtasia2024破解版电脑屏幕录制剪辑软件
屏幕录制剪辑 TechSmith Camtasia for Mac v2021是 TechSmith 公司所开发出一款专业屏幕录像和编辑, Camtasia Studio2024版是由TechSmith公司官方进行汉化推出的最新版本,除2023版以下版本均没有官方汉化。 同时TechSmith公司打击第三方贩卖Camtasia Studio汉化的…...

c语言进阶部分详解(《高质量C-C++编程》经典例题讲解及柔性数组)
上篇文章我介绍了介绍动态内存管理 的相关内容:c语言进阶部分详解(详细解析动态内存管理)-CSDN博客 各种源码大家可以去我的github主页进行查找:唔姆/比特学习过程2 (gitee.com) 今天便接“上回书所言”,来介绍《高质…...
Unreal PythonScriptPlugin
Unreal PythonScriptPlugin 文章目录 Unreal PythonScriptPluginPython vs UnLua官方文档PyStubDoString 示例代码,引擎里有很多插件已经用 py 写编辑器脚本了 unreal.get_editor_subsystem(unreal.LevelEditorSubsystem).load_level("/Game/maps/UVlayoutTes…...

什么是数据可视化,为什么数据可视化很重要?
数据可视化是数据的图形表示,可以帮助人们更轻松地理解和解释复杂的信息。它涉及创建数据的视觉表示,例如图表、图形、地图和其他视觉元素,以传达数据中的见解、模式和趋势。数据可视化是将原始数据转化为可操作知识的关键工具。 以下是数据…...
chatgpt相关问题解答
1. openAI的chatgpt的收费方式有哪几种? 根据OpenAI官方的信息,ChatGPT的收费方式包括两种: 1.订阅计划(Subscription Plan):OpenAI提供了ChatGPT Plus订阅计划,每月收费20美元。订阅计划的用…...

nssm将exe应用封装成windows服务
一、简介 NSSM(Non-Sucking Service Manager)是一个用于在Windows操作系统上管理和运行应用程序作为服务的工具。它提供了一种简单的方法来将任意可执行文件转换为Windows服务,并提供了一些额外的功能和配置选项。 优点: 简单易…...

golang实现极简todolist
ToDoList 最近跟着qimi老师做了一个ToDoList,我做的GitHub地址贴在这里,但由于前端出了点问题,所以都是用postman进行测试 原项目地址 部分功能展示 删除代办 查找代办 下面给出思路 思路 其实这是一个很简单的增删改查的实现ÿ…...

C# Onnx Dense Face 3D人脸重建,人脸Mesh
效果 项目 代码 using OpenCvSharp; using System; using System.Collections.Generic; using System.Drawing; using System.Windows.Forms;namespace Onnx_Demo {public partial class frmMain : Form{public frmMain(){InitializeComponent();}string fileFilter "*.…...

Java 8 新特性 Stream 的使用场景(不定期更新)
方便在写代码的过程中直接使用,好记性不如好文章,直接 CV 改了直接用。提高 办(摸)公(鱼)效(时)率(间), 不然就直接问 GPT 也不是说不行。 只符合…...

公开IP属地信息如何保护用户的隐私?
公开IP属地信息通常涉及与用户或组织的隐私有关,因此在公开此类信息时需要非常小心,以避免侵犯他人的隐私权。以下是触碰底线的几种情况以及如何保护网络安全和用户隐私: 个人隐私保护: 公开IP属地信息可能泄露用户的物理位置&…...

大桌子初步使用
大桌子安装成功后进入应用市场首推安装首页和网盘 一键安装的时候如果出现这样的错误,只要你能保证服务器是正常联网的就再试一次,十有八九就是网络不稳定 安装成功后,要到已安装里去启用一下 然后回到这个页面 http://服务器ip/dzzoffice/h…...
初步了解ORM框架之一Mybatis
ORM(对象关系映射)框架是现代软件开发中不可或缺的一部分。它们简化了将对象模型映射到关系数据库的过程,提供了更加便捷和高效的数据库操作方式,常见有:Hibernate、JPA(Java Persistence API)、…...

2023 electron最新最简版windows、mac打包、自动升级详解
这里我将讲解一下从0搭建一个electron最简版架子,以及如何实现打包自动化更新 之前我有写过两篇文章关于electron框架概述以及 常用api的使用,感兴趣的同学可以看看 Electron桌面应用开发 Electron桌面应用开发2 搭建electron 官方文档:ht…...

Ubuntu18.04安装pcl-1.12.1,make时报错:/usr/bin/ld: cannot find -lvtkIOMPIImage
解决方案: 在vtk安装包中,重新打开cmake-gui,然后勾选上VTK_Group_MPI和VTK_Group_Imaging。 cd VTK-8.2.0 cd build cmake-gui然后重新编译生成。 make -j8 # 或者j4,量力而行。 sudo make install 就可以解决了。 然后重新回到pcl安装…...
表单验证不通过的一个点form中未定义这个字段
这个坑就是犯了好几次了,一直记不住,尤其是加了字段后的时候,总是忘记,然后导致验证不通过。 以前我认为,只要表单绑定的内容中的属性有这个值就在ruler里面就可以验证他,,以至于我总是不在dat…...

最新、最全、最详细的 K8S 学习笔记总结
Kubernetes就是一个编排容器的工具,一个可以管理应用全生命周期的工具,从创建应用,应用的部署,应用提供服务,扩容缩容应用,应用更新,都非常的方便,而且可以做到故障自愈。 K8S的前景…...

Emacs之高亮显示超过80个字符部分(一百三十)
简介: CSDN博客专家,专注Android/Linux系统,分享多mic语音方案、音视频、编解码等技术,与大家一起成长! 优质专栏:Audio工程师进阶系列【原创干货持续更新中……】🚀 人生格言: 人生…...

C语言 每日一题 PTA 11.7 day13
1.求e的近似值 自然常数 e 可以用级数 1 1 / 1! 1 / 2! ⋯ 1 / n! ⋯ 来近似计算。 本题要求对给定的非负整数 n,求该级数的前 n 1 项和。 代码实现 #include<stdio.h> void main() {int a, i, j; double b 1; double c 1;printf("请输入一个数\n…...

产品经理进阶:产品的起点是发现并理解问题
目录 简介 发现实际问题 接下来选择一个问题 之后就是验证问题 最后总结一下 CSDN学院(硬件产品经理进阶课) 简介 你花费了大量的时间来思考如何构建一款每个人都会喜欢的产品。 但最终却发现没有人愿意为之买单。 这其实就是没有真正理解客户问…...

Lombok 的 @Data 注解失效,未生成 getter/setter 方法引发的HTTP 406 错误
HTTP 状态码 406 (Not Acceptable) 和 500 (Internal Server Error) 是两类完全不同的错误,它们的含义、原因和解决方法都有显著区别。以下是详细对比: 1. HTTP 406 (Not Acceptable) 含义: 客户端请求的内容类型与服务器支持的内容类型不匹…...

【Oracle APEX开发小技巧12】
有如下需求: 有一个问题反馈页面,要实现在apex页面展示能直观看到反馈时间超过7天未处理的数据,方便管理员及时处理反馈。 我的方法:直接将逻辑写在SQL中,这样可以直接在页面展示 完整代码: SELECTSF.FE…...
pam_env.so模块配置解析
在PAM(Pluggable Authentication Modules)配置中, /etc/pam.d/su 文件相关配置含义如下: 配置解析 auth required pam_env.so1. 字段分解 字段值说明模块类型auth认证类模块,负责验证用户身份&am…...

NFT模式:数字资产确权与链游经济系统构建
NFT模式:数字资产确权与链游经济系统构建 ——从技术架构到可持续生态的范式革命 一、确权技术革新:构建可信数字资产基石 1. 区块链底层架构的进化 跨链互操作协议:基于LayerZero协议实现以太坊、Solana等公链资产互通,通过零知…...
AI编程--插件对比分析:CodeRider、GitHub Copilot及其他
AI编程插件对比分析:CodeRider、GitHub Copilot及其他 随着人工智能技术的快速发展,AI编程插件已成为提升开发者生产力的重要工具。CodeRider和GitHub Copilot作为市场上的领先者,分别以其独特的特性和生态系统吸引了大量开发者。本文将从功…...

Maven 概述、安装、配置、仓库、私服详解
目录 1、Maven 概述 1.1 Maven 的定义 1.2 Maven 解决的问题 1.3 Maven 的核心特性与优势 2、Maven 安装 2.1 下载 Maven 2.2 安装配置 Maven 2.3 测试安装 2.4 修改 Maven 本地仓库的默认路径 3、Maven 配置 3.1 配置本地仓库 3.2 配置 JDK 3.3 IDEA 配置本地 Ma…...
Python 包管理器 uv 介绍
Python 包管理器 uv 全面介绍 uv 是由 Astral(热门工具 Ruff 的开发者)推出的下一代高性能 Python 包管理器和构建工具,用 Rust 编写。它旨在解决传统工具(如 pip、virtualenv、pip-tools)的性能瓶颈,同时…...
React---day11
14.4 react-redux第三方库 提供connect、thunk之类的函数 以获取一个banner数据为例子 store: 我们在使用异步的时候理应是要使用中间件的,但是configureStore 已经自动集成了 redux-thunk,注意action里面要返回函数 import { configureS…...

用机器学习破解新能源领域的“弃风”难题
音乐发烧友深有体会,玩音乐的本质就是玩电网。火电声音偏暖,水电偏冷,风电偏空旷。至于太阳能发的电,则略显朦胧和单薄。 不知你是否有感觉,近两年家里的音响声音越来越冷,听起来越来越单薄? —…...

深入浅出深度学习基础:从感知机到全连接神经网络的核心原理与应用
文章目录 前言一、感知机 (Perceptron)1.1 基础介绍1.1.1 感知机是什么?1.1.2 感知机的工作原理 1.2 感知机的简单应用:基本逻辑门1.2.1 逻辑与 (Logic AND)1.2.2 逻辑或 (Logic OR)1.2.3 逻辑与非 (Logic NAND) 1.3 感知机的实现1.3.1 简单实现 (基于阈…...