欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

复习篇——网络编程知识点详细总结(待排版,知识点看情况补充

程序员文章站 2022-06-05 22:04:09
...

一, CS模型:(整个流程)
服务器启动后,首先创建一个(或多个)监听socket, 并调用bind函数将其绑定到服务器感兴趣的端口上,然后调用listen函数等待客户连接。服务器稳定运行之后,客户端就可以调用connect函数向服务器发起连接
由于客户连接请求时随机到达的异步事件,服务器需要使用某种I/O模型来监听这一事件, 比如I/O复用技术之一的select系统调用。当监听到连接请求后,服务器就调用accept函数接受它,并分配一个逻辑单元为新的连接服务。逻辑单元可以是新创建的子进程、子进程或者其他。比如服务器可能给给客户端分配的逻辑单元是由fork系统调用创建的子进程。逻辑单元读取客户请求,处理该请求。然后将处理结果返回给客户端。客户端接收到服务器反馈的结果之后,可以继续向服务器发送请求, 也可以主动关闭连接
CS模型中服务器是通信的中心,当访问量过大时,可能所有客户都将得到很慢的响应
二, P2P模型
比CS模型更加符合网络通信的实际情况,P2P摒弃了以服务器为中心的格局,让网络上所有主机重新回归对等的地位, 这样使得每台机器在消耗服务的同时也给别人提供服务,这样资源能够更充分,*地共享。云计算群可以看作P2P模型的一个典范。但缺点是当用户之间传输的请求过多时,网络的负载将加重。P2P模型还通常带有一个专门的发现服务器,这个发现服务器通常还提供查找服务,使每个客户都能尽快找到自己需要的资源。P2P时C/S的扩展:每台主机既是客户端,又是服务器
三, I/O处理单元
是服务器管理客户连接的模块。要完成的工作:等待并接收新的客户连接,接收客户数据,将服务器响应数据返回给客户端。但是数据的收发不一定在I/O处理单元中执行,也可能在逻辑单元中执行(根据事件处理模式)
对一个服务器机群来说,I/O处理单元是一个专门的接入服务器,它实现负载均衡,从所有逻辑服务器中选取负荷最小的一台来为新客户服务
一个逻辑单元通常是一个进程后者线程,它分析并处理客户请求,然后将结果传递给I/O处理单元或者直接发送给客户端,对服务器机群而言,一个逻辑单元本身就是一台逻辑服务器。服务器通常拥有多个逻辑单元, 以实现对多个客户任务的并行处理
四, 请求队列
是各单元之间的通信方式的抽象。I/O处理单元接收到客户端请求时,需要以某种方式通知一个逻辑单元来处理该请求。同样,多个逻辑单元同时访问一个存储单元时,也需要采用某种机制来协调处理竞态条件。请求队列通常呗时限为池的一部分,对于服务器机群而言,请求队列是各台服务器之间预先建立的、静态的、永久的TCP连接。这种TCP连接能提高服务器之间交换数据的效率,因为它避免了动态建立TCP连接导致的额外的系统开销

五, I/O处理单元之IO模型
1, 四个概念:同步 异步 阻塞 非阻塞
一个IO操作其实分为两个步骤:①用户进程向内核发起IO请求,等待内核数据准备②将数据从内核拷贝到进程缓存区中
阻塞和非阻塞主要区别在第一步:向内核发起IO请求是否会被阻塞
同步和异步主要区别在第二步是否阻塞:访问数的方式,同步需要主动读写数据,在读写数的过程中还是会阻塞;异步并不主动读写数据,由操作系统内核完成数据的读写,只需要IO操作完成的通知
2, 同步阻塞I/O
用户进程在发起一个I/O操作以后,必须等待I/O操作的完成,只有当真正完成了I/O操作以后,用户进程才能运行。还是两个阶段:
1,当用户进程调用了recvfrom这个系统调用,对于network io来说,
很多时候数据在一开始还没有到达(比如,还没有收到一个完整的UDP包),
这个时候kernel就要等待足够的数据到来。而在用户进程这边,整个进程会被阻塞
2, 当kernel一直等到数据准备好了,它就会将数据从kernel
中拷贝到用户内存,然后kernel返回结果,用户进程才接触block的状态,重新运行起来
所以,blocking IO的特点就是在IO执行的两个阶段都被block了。
===》 第一阶段:用户进程调用了select,整个进程会被block,而同时kernel会监视所有select负责的socket,当任何一个socket中的数据准备好了,select就会返回
第二阶段:select返回,用户进程再调用read操作,将数据从kernel拷贝到用户进程
3, 同步非阻塞I/O
在此种方式下,用户进程发起一个I/O操作以后边可返回做其它事情,但是用户进程需要时不时的询问I/O操作是否就绪,这就要求用户进程不停的去询问,从而引入不必要的CPU资源浪费。目前Java的NIO就属于同步非阻塞I/O。
=》 第一阶段:当用户进程发出read操作时,如果kernel中的数据还没有准备好,那么它并不会block用户进程,而是立刻返回一个error。从用户进程角度讲 ,它发起一个read操作后,并不需要等待,而是马上就得到了一个结果。用户进程判断结果是一个error时,它就知道数据还没有准备好,于是它可以再次发送read操作。
第二阶段:一旦kernel中的数据准备好了,并且又再次收到了用户进程的system call,那么它马上就将数据拷贝到了用户内存,然后返回。所以,用户进程其实是需要不断的主动询问kernel数据好了没有。EWOULDBLOCK
详细
=》 第一阶段:当需要等待数据的时候,首先用户态会向内核发送一个信号,告诉内核我要什么数据,然后用户态就不管了,做别的事情去了.
第二阶段:当内核态中的数据准备好之后,内核立马发给用户态一个信号,说”数据准备好了,快来查收“,用户态进程收到之后,立马调用recvfrom,等待数据从内核空间复制到用户空间,待完成之后recvfrom返回成功指示,用户态进程才处理别的事情。
信号驱动式I/O模型有种异步操作的赶脚,但是在将数据从内核复制到用户空间这段时间内用户态进程是阻塞的
4, 异步非阻塞I/O
在此种模式下,用户进程只需要发起一个I/O操作然后立即返回,等I/O操作真正的完成以后,应用程序会得到I/O操作完成的通知,此时用户进程只需要对数据进行处理就好了,不需要进行实际的I/O读写操作,因为真正的I/O读取或者写入操作已经由内核完成了。很少有Linux系统支持这种模型 在Windows下的IOCP就是该模型
5, 异步阻塞I/O 略?
六, IO复用(五种IO模型:阻塞IO、非阻塞IO、多路复用IO、信号驱动IO以及异步IO
其中多路复用IO模型是目前使用得比较多的模型
在多路复用IO模型中,会有一个线程不断去轮询多个socket的状态,只有当socket真正有读写事件时,才真正调用实际的IO读写操作。因为在多路复用IO模型中,只需要使用一个线程就可以管理多个socket,系统不需要建立新的进程或者线程,也不必维护这些线程和进程,并且只有在真正有socket读写事件进行时,才会使用IO资源,所以它大大减少了资源占用
多路复用IO为何比非阻塞IO模型的效率高是因为在非阻塞IO中,不断地询问socket状态时通过用户线程去进行的,而在多路复用IO中,轮询每个socket状态是内核在进行的,这个效率要比用户线程要高的多
要注意的是,多路复用IO模型是通过轮询的方式来检测是否有事件到达,并且对到达的事件逐一进行响应。因此对于多路复用IO模型来说,一旦事件响应体很大,那么就会导致后续的事件迟迟得不到处理,并且会影响新的事件轮询
七, 为什么要IO复用(why need select/poll/epoll)
最基础的socket-bind-listen-connect-accept-read 有一个严重的问题,accept函数在没有客户端连接时会阻塞,同样read/write函数在客户端没有数据传过来时也会阻塞,所以在多个客户端请求的过程中,只要排队在上面的客户端没有传数据过来,调用read函数就会阻塞在那里,后面继续到来的客户端请求就会响应不了
同样只要没有新的客户端连接进行,调用accept就会阻塞,前面发数据过来的客户端的请求也响应不了?(为什么会影响前面发数据过来的客户端?

当使用accept函数的时候发现没有客户端连接时会阻塞,同样当使用read/write函数时发现客户端没有数据传过来时也会阻塞,所以在多个客户端请求的过程中,如果在某个循环中调用read桉树阻塞了,这个时候又来了一个客户端连接,由于现在服务器端程序卡在了read这里,就没有办法进入下一个循环到accept那里,那个想建立连接的客户端九一直阻塞在那里,同样假如在某一个循环调用accept阻塞了,服务端程序由于卡在了accept,也就不能往下执行到read函数,所以也响应不了前面已经连接的客户端套接字发过来的数据。这样就不能处理多个客户端的服务请求,导致客户端的请求大大延迟
这样就不能处理多个客户端的服务请求
因此引入了多路IO复用,它有三种实现机制——selsect、poll、epoll
需要指出的是,I/O复用虽然能同时监听多个文件描述符,但它本身是阻塞的,并且当多个文件描述符同时就绪时,如果不采用额外的措施,程序就只能按顺序依次处理每一个文件描述符,这使得服务器程序看起来像是串行工作的,如果要实现并发,只能使用多线程或多进程等手段
八, select函数工作过程
在执行read/accept之前,会先让select检查看哪个文件描述符有连接过来了或者有数据过来了,确定过来了再调用,否则就再进入下一轮的检查

select特点:
缺点:1,每次调用 select(),都需要把 fd 集合从用户态拷贝到内核态,这个开销在 fd 很多时会很大,需要维护一个用来存放大量fd的数据结构,这样会使得用户空间和内核空间在传递该结构时复制开销大。同时每次调用 select() 都需要在内核遍历传递进来的所有 fd,这个开销在 fd 很多时也很大
2,单个进程能够监视的文件描述符的数量存在最大限制,在 Linux 上一般为 1024,可以通过修改宏定义甚至重新编译内核的方式提升这一限制,但是这样也会造成效率的降低
3,select函数在每次调用之前都要对参数进行重新设定,这样做比较麻烦,而且会降低性能
4,对socket进行扫描时是线性扫描,即采用轮询的方法,效率较低。当套接字比较多的时候,每次select()都要通过遍历FD_SETSIZE个Socket来完成调度,不管哪个Socket是活跃的,都遍历一遍。这会浪费很多CPU时间。如果能给套接字注册某个回调函数,当他们活跃时,自动完成相关操作,那就避免了轮询,这正是epoll与kqueue做的。
5,其实select还是同步阻塞的模型 在select设定的时间timeval内,进程会阻塞在select那条语句上 此时内核在遍历select所监管的套接字上 在此期间只要有一个套接字就就绪了(客户端connect过来 或者客户端send数据过来了 ) select就会返回停止阻塞 然后调用read /accept同步读写或者建立连接
或者在内核遍历期间没有套接字就绪 select会超时返回 然后通过while进入下一轮的select遍历
优点:,使用select以后最大的优势是用户可以在一个线程内同时处理多个socket的IO请求。用户可以注册多个socket,然后不断地调用select读取被**的socket,即可达到在同一个线程内同时处理多个IO请求的目的。而在同步阻塞模型中,必须通过多线程的方式才能达到这个目的。;且跨平台支持

select工作流程:
第一步:select工作之前,需要知道要监管哪些套接字
① 创建读写套接字数组
② 创建监听套接字(一开始只有一个连接套接字 交给select监听 后面每个客户端调用connect 到这个连接套接字的时候,在服务器端通过accept会创建针对这个客户端的读写套接字 然后把这个读写套接字加入到select的监听列表),也就是说select一开始只监听我们上面创建的这个连接套接字,到后面有客户端连接的时候会监听一个连接套接字和多个读写套接字)
③ 创建select监听列表(在select的第二到第四个参数是fd_set*类型
fd_set是一种位数组类型,也就是说数组中的数组元素值只能是0或1
readfd表示要进行监管的读操作的套接字的数组,
writefds表示要进行监管的写操作套接字的数组
exceptfds表示要进行监管的异常事件套接字的数组)
④ 进入while循环
⑤ 监听列表清零(readfd,writefds数组一开始需要全部置为0,通过void FD_ZERO(fd_set *set);函数进行此操作.
还有另外一个函数同样需要了解,FD_CLR(int fd, fd_set *set);把文件描述符集合里fd位清0
⑥ 监听套接字和读写套接字放入监听列表(怎么告诉selcet函数要监管哪些套接字描述符呢?就是把要监管的套接字描述符放到readfd,writefds数组中
那怎么把监管的套接字描述符放到readfd,writefds数组中呢?通过FD_SET(int fd, fd_set *set)函数; 把文件描述符fd加入到两个数组
因为fd_set是一个位数组,所以要把某个文件描述符加入监管, 就是把数组中该文件描述符位的元素置1,
这种置1操作就是通过FD_SET函数实现的 nfds表示总共要监管查询的套接字的个数.
再,比较计算出当前生成最大的套接字
第二步:第二步:select开始工作,设定时间内阻塞轮询这些套接字,如果有套接字是有序的,就通过FD_ISSET(int fd, fd_set *set)函数查看set数组中是否存在fd描述符是1,存在则表示这个fd套接字是可读写的,可以调用read/accept同步读写

第三步:select工作完成,根据返回值去获知有无就绪,哪些就绪:

select函数的调用过程:
1) 使用copy_from_user从用户空间拷贝fd_set到内核空间
2) 注册回调函数__pollwait
3) 遍历所有fd,调用其对应的poll方法,如tcp_poll其核心实现就是2的回调函数,__pollwait的主要工作就是把当前进程挂到设备的等待队列中,不同的设备有不同的等待队列,对于tcp_poll来说,其等待队列是sk->sk_sleep,在设备收到一条信息或填写完文件数据后,会唤醒设备等待队列上睡眠的进程,这时current(当前进程)便被唤醒了
4) poll方法返回时会返回一个描述读写操作是否就绪的mask掩码,根据这个mask掩码给fd_set赋值
5) 如果遍历完所有的fd,还没有返回一个可读写的mask掩码,则会调用schedule_timeout使调用select的进程(也就是current)进入睡眠。当设备驱动发生自身资源可读写后,会唤醒其等待队列上睡眠的进程。如果超过一定的超时时间(schedule_timeout指定),还是没人唤醒,则调用select的进程会重新被唤醒获得CPU,进而重新遍历fd,判断有没有就绪的fd
6) 把fd_set从内核空间拷贝到用户空间
7)

//处理外带数据 网络程序中,select能处理的异常情况只有一种,socket上接收到外带数据
外带数据比普通数据有更高的优先级,外带数据映射到现有的连接中,而不是在客户等级和服务器间再用一个连接
我们写的select程序经常都用于接收普通数据的,当我们的服务器需要同时接收普通数据和外带数据怎么处理二者呢?

对于普通的用recv函数读取数据,对于异常事件,采用带MSG_OOB标志的recv函数读取外带数据

select处理多客户问题:
用select以后最大的优势是用户可以在一个县城内同时处理多个socket的IO请求。在网络编程中,当涉及到多客户访问服务器的情况,我们首先想到的办法就是fork出多个进程来处理每个客户链接。现在我们可以使用select来处理多客户问题,而不用fork

九, poll函数
poll函数工作原理与select函数类似,也是监管一系列的文件描述符,看这些文件描述符是否可读/可写/异常,再去调用io函数读写
不过poll函数没有监管文件描述符个数的限制 ,与 select() 在本质上没有多大差别,管理多个描述符也是进行轮询,根据描述符的状态进行处理。

第一步:poll工作之前需要知道要监管哪些套接字,
① poll监管套接字结构体数组创建:
poll函数中第一个参数引入了pollfd这个结构体的数组,数组中每一个元素就表示了要监管的套接字

pollfd结构体的原型为:
struct pollfd {
int fd; /* 文件描述符 /
short events; /
注册的事件 /
short revents; /
实际发生的事件,由内核填充 */
};
通过给pollfd结构的fd成员赋值,同时设定好events参数,表示就把这个文件描述符加到poll函数的监管列表去了.

与select函数不同的一点就是对不同文件描述符监管事件的确定方式不同,select采用了三个类型的位数组分别表示哪些是可读时**,哪些是可写时**,哪些是异常时**
poll函数用events这个参数在指定单个文件描述符时就制定了要监管的事件,可读/可写还是异常 一般给events参数赋值的宏,在linux下用POLLIN、POLLOUT、POLLERR,/可同时设置一个文件描述符的多个监管事件,用按位或运算符连接就好了

第二步:poll开始工作,阻塞轮询监管的套接字
#include <poll.h>
int poll(struct pollfd *fds, nfds_t nfds, int timeout);

如果有套接字是有序的,怎么知道这些就绪的套接字里有没有想要读写的套接字(?)
select函数使用FD_ISSET函数判断某个套接字是否是**状态,

poll函数使用pollfd结构体中revents参数,revents变量在每一次poll函数调用完成后,内核设置会设置revents的值,这个值其实也就是events的宏,以说明对该描述符发生了什么事件,
比如,调用完poll函数后要查看某一个文件描述符是否处于**状态(比如可读),是通过调用pollfd参数的revents参数与POLLIN做比较,如果相等,则说明该文件描述符现在是可读的
使用语句 if(poll_fd.revents == POLLIN)

第三步:poll工作完成后,根据返回值去获知有无就绪,那些就绪(poll返回的也是套接字个数)

poll函数的优缺点:
优点:
1,poll() 不要求开发者计算最大文件描述符加一的大小。
2,poll() 在应付大数目的文件描述符的时候速度更快,相比于select
3,它没有最大连接数的限制,原因是poll是基于链表来存储的
4,在调用参数的时候,只需要对参数进行一次设置就好了,而每次调用select前都要重新在监听的位数组中重新设置文件描述符,因为事件发生之后,文件描述符集合将被内核修改
缺点:
1, 大量的fd的数组被整体复制于用户态和内核地址空间之间,而不管这样的复制是不是有意义
2, 与select一样,poll返回后,需要轮询pollfd来获取就绪的描述符,这样会使性能下降
3, 同时连接的大量客户端在一时刻可能只有很少的就绪状态,因此随着监视的描述符数量的增长,其效率也会线性下降

十, epoll函数
1, 为什么要epoll函数/epoll的引入:
先总结select。poll的工作流程:
-----> 创建监听套接字
-----> 创建的套接字加入select/poll的监管列表
----->进入while循环
----->select/poll在有限时间内阻塞地轮询每个监听地套接字,最后返回就绪套接字的个数
----->看这些就绪的套接字里面是否有自己想要读写的套接字,如果有就read/write/accept, 否则跳过
----->进入下一个while循环,重复步骤

存在的问题:

拿select模型为例,假设我们的服务器需要支持100万的并发连接,
则在__FD_SETSIZE 为1024的情况下,则我们至少需要开辟1k个进程才能实现100万的并发连接。
除了进程间上下文切换的时间消耗外,从内核/用户空间大量的无脑内存拷贝、数组轮询等,是系统难以承受的。因此,基于select模型的服务器程序,要达到10万级别的并发访问,是一个很难完成的任务。

epoll的实现机制与select/poll机制完全不同,上面所说的select的缺点在epoll上不复存在

2, epoll工作过程:
第一步 epoll工作前需要知道要监管哪些套接字
(1创建监听套接字;2创建监听套接字对应的触发事件结构体; 3创建epoll事件表; 4将监听套接字和其对应的触发事件结构体加入epoll事件表)
第二步epoll开始工作,检查是否有事件发生
第三步epoll工作完成 根据返回值去获知有无就绪,哪些就绪
detail:
第一步 epoll工作前 需要知道要监管哪些套件字:
① 创建监听套接字
② 创建监听套接字对应的触发事件结构体

epoll有两种工作方式:LT和ET
	LT(level triggered)是缺省的工作方式:并且同时支持block和no-block socket.在这种做法中,内核告诉你一个文件描述符是否就绪了,然后你可以对这个就绪的fd进行IO操作。如果你不作任何操作,内核还是会继续通知你的,所以,这种模式编程出错误可能性要小一点。传统的select/poll都是这种模型的代表.
	ET (edge-triggered)是高速工作方式:只支持no-block socket。在这种模式下,当描述符从未就绪变为就绪时,内核通过epoll告诉你。然后它会假设你知道文件描述符已经就绪,并且不会再为那个文件描述符发送更多的就绪通知,直到你做了某些操作导致那个文件描述符不再为就绪状态了

③ 创建epoll事件表

1, ☆☆☆epoll_create():建立一个epoll对象(在spoll文件系统中为这个句柄分配资源),当某一进程调用epoll_create时,Linux内核会创建一个eventpoll结构体,这个结构体中有两个成员与epoll的使用方式密切相关:
eventpoll结构体如下所示:
struct eventpoll{
struct rb_root rbr; /红黑树的根节点,这棵树中存储着所有添加到epoll中的需要监控的事件/

struct list_head rdlist; /*双链表中则存放着将要通过epoll_wait返回给用户的满足条件的事件*/
....

};
每一个epoll对象都有一个独立的eventpoll结构体
在epoll中,对于每一个事件,都会建立一个epitem结构体

struct epitem{
struct rb_node rbn;//红黑树节点
struct list_head rdllink;//双向链表节点
struct epoll_filefd ffd; //事件句柄信息
struct eventpoll *ep; //指向其所属的eventpoll对象
struct epoll_event event; //期待发生的事件类型
}

∴ 1) int epoll_create(int size);创建一个epoll的句柄,size用来告诉内核这个监听的数目一共多大,当创建好epoll句柄后,它就是会占用一个fd值,所以在使用完epoll之后必须调用close(),否则可能导致fd被耗尽

④ 将监听套接字和其对应的触发事件结构体加入epoll事件表
1, epoll_ctl(),epoll的事件注册函数, 调用epoll_ctl向epoll对象中添加(100万个)连接的套接字用于存放通过epoll_ctl方法向epoll对象中添加进来的事件。这些事件都会挂载在红黑树中, 如此重复添加的事件就可以通过红黑树而高效地识别出来
所有添加到epoll中的事件都会与设备(网卡)驱动程序建立回调关系,也就是说,当相应的事件发生时会调用这个回调方法。这个回调方法在内核中叫ep_poll_callback, 它会将发生的事件添加到rdlist双链表中
∴ 2) int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
epoll的事件注册函数,它不同与select()是在监听事件时(epoll使用epoll_wait监听)告诉内核要监听什么类型的事件,而是在这里先注册要监听的事件类型。第一个参数是epoll_create()的返回值,第二个参数表示动作,用三个宏来表示:
EPOLL_CTL_ADD:注册新的fd到epfd中;
EPOLL_CTL_MOD:修改已经注册的fd的监听事件;
EPOLL_CTL_DEL:从epfd中删除一个fd;
第三个参数是需要监听的fd,第四个参数是告诉内核需要监听什么事,EPOLLIN、EPOLLOUT、EPOLLPRI、EPOLLONESHOT、EPOLLET、EPOLLERR、EPOLLHUP
其中注意EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里

 第二步:epoll开始工作,检查是否有事件发生  epoll_wait

☆❗调用epoll_wait收集发生的事件的连接,当调用epoll_wait检查是否有事件发生时,只需要检查eventpoll对象中的rdlist双链表中是否有epitem元素即可
如果rdlist不为空,则把发生的事件复制到用户态,同时将事件数量返回给用户
∴ 3) int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
等待事件的产生,类似于select()调用。参数events用来从内核得到所有就绪事件的集合(哪一个套接字就绪哪一个就放在里面)
maxevents告之内核这个events有多大,这个 maxevents的值不能大于创建epoll_create()时的size,参数timeout是超时时间(毫秒,0会立即返回,-1是永久阻塞)。该函数返回需要处理的事件数目,如返回0表示已超时

通过epoll_create、epoll_ctl、epoll_wait 三个函数实现epoll,只需要在进程启动时建立一个epoll对象,然后在需要的时候向这个epoll对象中添加或者删除连接,同时epoll_wait的效率也非常高,因为调用epoll_wait时并没有一股脑的向操作系统复制这100玩个连接的句柄数据,内核也不用去遍历全部的连接

第三步:epoll工作完成后,根据返回值去获知有无就绪,哪些就绪
返回 -1 出错 ///返回0 超时/// 返回大于0 存在就绪
???

如果是读写套接字就绪:

创建读取缓冲区,并清空

epoll的优点:
1) 支持一个进程打开大数目的socket描述符(FD)
select 最不能忍受的是一个进程所打开的FD是有一定限制的,由FD_SETSIZE设置,默认值是2048。对于那些需要支持的上万连接数目的IM服务器来说显 然太少了。这时候你一是可以选择修改这个宏然后重新编译内核,不过资料也同时指出这样会带来网络效率的下降,二是可以选择多进程的解决方案(传统的 Apache方案),不过虽然linux上面创建进程的代价比较小,但仍旧是不可忽视的,加上进程间数据同步远比不上线程间同步的高效,所以也不是一种完 美的方案。不过 epoll则没有这个限制,它所支持的FD上限是最大可以打开文件的数目,这个数字一般远大于2048,举个例子,在1GB内存的机器上大约是10万左 右,具体数目可以cat /proc/sys/fs/file-max察看,一般来说这个数目和系统内存关系很大。
2) IO 效率不随FD数目增加而线性下降
传统的select/poll另一个致命弱点就是当你拥有一个很大的socket集合,不过由于网络延时,任一时间只有部分的socket是"活跃"的, 但是select/poll每次调用都会线性扫描全部的集合,导致效率呈现线性下降。
但是epoll不存在这个问题,它只会对"活跃"的socket进行 操作
这是因为在内核实现中epoll是根据每个fd上面的callback函数实现的。那么,只有"活跃"的socket才会主动的去调用 callback函数,其他idle状态socket则不会,在这点上,epoll实现了一个"伪"AIO??,因为这时候推动力在os内核。在一些 benchmark中,如果所有的socket基本上都是活跃的—比如一个高速LAN环境,epoll并不比select/poll有什么效率,相反,如果过多使用epoll_ctl,效率相比还有稍微的下降。但是一旦使用idle connections模拟WAN环境,epoll的效率就远在select/poll之上了

3) 使用mmap加速内核 与用户空间的消息传递。
这点实际上涉及到epoll的具体实现了。无论是select,poll还是epoll都需要内核把FD消息通知给用户空间,如何避免不必要的内存拷贝就 很重要,在这点上,epoll是通过内核与用户空间mmap同一块内存实现的。???

  1. 内核微调

三组I/O复用函数的比较:
这三组函数否使通过某种结构体变量来告诉内核监听哪些文件描述符上的哪些事件,并使用该结构体类型的参数来获取内核处理的结果。select的参数类型fd_set没有将文件描述符和事件绑定,它仅仅是一个文件描述符集合,因此需要提供3个这种类型的参数来分别传入和输出可读、可写及异常等事件,这使得select不能处理更多类型的事件,另一方面由于内核对fd_set的在线修改,应用程序下次调用select前不得不重置这3个fd_set集合。poll的参数类型好一点,将文件描述符和事件都定义其中,任何事件都能被统一处理,而events成员保持不变,因此下次调用poll不用重置pollfd类型的事件集参数。由于select和poll调用都返回整个用户注册的事件集合,所以应用程序索引就绪文件描述符的时间复杂度是O(N);
epoll采用完全不同的方式管理用户注册的事件,它在内核中维护一个事件表,并提供一个独立的系统调用epoll_ctl来控制往其中添加、删除、修改事件,这样每次epoll_wait调用都直接从该内核事件表中取得用户注册的事件,而无需反复从用户空间读入这些事件。epoll_wait系统调用的events参数仅用来返回就绪的事件,这使得应用程序索引就绪文件描述符的时间复杂度达到O(1),还有监听个数也不一样

十一, epoll实例
① 基本实例:
创建监听套接字 int lisfd = socket(…) bind;listen…
创建监听套接字对应的事件结构体,
struct epoll_event lisfd_event;,
lisfd_event.data.fd = lisfd;
lisfd_event.events = EPOLLIN
创建epoll事件表:int epoll_event_table = epoll_create(EPOLL_EVENT_SIZE)
创建epoll就绪事件结构体数组 将epoll查询结果放在里面
epoll_event events_result[20];??
监听套接字加入epoll事件表
epoll_ctl(epoll_event_table,EPOLL_CTL_ADD,lisfd,&lisfd_event);
while{ 用epoll_wait查看有无就绪事件,有的话判断就绪的是监听套接字还是读写套接字–>读完EPOLL_CTL_DEL掉}

② EPOLLONESHOT事件
我们期望一个socket连接在任意时刻都只被一个线程处理,这就可以使用epoll的EPOLLONESHOT事件实现
即使使用了ET模式,一个socket上的某个事件还是可能被触发多次,这在并发程序上会出现以下问题,比如一个进程在读取完某个socket上的数据后处理这些数据,在处理过程中socket上又有新的数据可以读,即EPOLLIN再次被触发,此时另外一个线程就会被唤醒来读取这些新的数据,这就出现了两个线程操作同一个socket的局面

一个socket在不同时间可能被不同的线程处理, 但同一时刻肯定只有一个线程为它服务

③ 客户端select实现非阻塞connect
connect()函数是针对客户端的,该函数的功能就是对服务器建立三次握手连接,,连接后放入上面所说的“连接队列”
注意:连接的过程是由内核完成,不是这个函数完成的,这个函数的作用仅仅是通知 Linux 内核,让 Linux 内核自动完成 TCP 三次握手连接,最后把连接的结果返回给这个函数的返回值(成功连接为0, 失败为-1)
要实现真正的非阻塞得有的效果是,connect函数可以立即返回,同时在内核上正在进行阻塞连接,应该把这个正在进行阻塞连接的 套接字纳入select的监管,因为select可以设置超时时间,如果在超时时间内没有检测到就绪套接字了就关闭这个套接字,如果在超时时间内有检测到就绪套接字,这个套接字写就绪了但获得的套接字连接error属性为0表示连接成功 返回这个套接字

步骤:
1, 创建目标连接套接字,设置目标套接字为非阻塞
2, connect到目标套接字,返回值为0,表示连接成功,可以进行后面的io操作,返回值为-1且errno不为EINPROGRESS表示连接已经失败,返回-1,errno为EINPROGRESS表示连接还在进行,此时将Socket套接字加入select列表
2.1,

实例见 图谱(代码清单 9-5 非阻塞connect

④ 统一事件源 处理信号
在网络服务程序中 以epoll为例 主程序为一个while循环 这个时候如果收到一个信号的话存在一些问题:1,由于信号是异步事件 相当于在主循环之外加了一个不受控制的处理信号的支线; 2,而且在信号函数处理期间 主循环再次收到信号是不能再次进入信号处理函数的
总结一下就是
频繁地直接处理信号不利于程序的性能和可靠性,我们把信号也包装成一个事件,添加进多路复用函数的事件集里进行统一处理

一种解决方案是, 信号处理函数知识简单的通知主循环(用于处理I/O事件)并告诉信号值,真正的信号处理逻辑被主循环调用,根据信号值做出相应的处理。
信号处理函数和主循环之间通常用管道做通信。信号处理函数从管道的写端写入信号值,主循环从管道的读端读取信号值,
因为主循环本身就要利用I/O复用函数监听链接进来的socket,所以将这个管道一并注册进I/O复用函数就能在主循环中及时得到信号到来的通知。

⑤ 利用超时参数实现定时触发

十二, 聊天室——实例演练
以poll为例实现一个简单的聊天室程序,以删除如何使用I/O复用技术来同时处理网络连接和用户输入。聊天室程序能让所有用户同时在线群聊,分为客户端和服务端,其中客户端有两个功能,一是从标准输入终端读入用户数据,并将用户数据发送至服务器;二是往标准输出终端打印打印服务器发送给它的数据。
服务器的功能是接收客户数据,并把数据发送给每一个登录到该服务器上的客户端

客户端设计:

服务端设计:

十三, 要处理的事件:1,IO事件 (IO复用) 2,信号事件(进程间通信)
3, 定时事件
定时事件的几个要素:1,服务器程序通过什么机制去实现隔固定时间的触发定时器 即定时器的触发方式; 2,服务器程序通过什么机制去管理多个要执行的定时器 即定时器的组织方式
1, 定时器的组织方式
服务器程序中往往设定的不止一个定时任务 即会有不止一个定时器
比如维护客户端心跳时间、检查多个数据包的超时重传等
通过某种数据结构比如链表、时间轮、时间堆 等将所有定时器串联起来实现对定时事件的统一管理

定时器通常至少包含两个成员:一个超时时间,和一个任务回调函数,有时候还可能包含回调函数被执行时需要传入的参数,以及是否重启定时器等
分类:
① 升序链表:如果使用链表,则每个定时器还要包含指向下一个定时器的指针成员, 双向链表的话还要能指向前一个定时器
② 时间轮:基于排序链表的定时器在添加定时器时的效率时偏低的
基于排序链表的定时器使用唯一的一条链表来管理所有定时器,所以插入操作的效率随着定时器数目的增加而降低。而时间轮使用哈希表的思想,将定时器散列到不同的链表上,这样每条链表上的定时器数目都将明显少于原来排序链表上的定时器数目,插入操作的效率基本不受定时器数目的影响
③ 时间堆:前两个都是以固定的频率调用心搏函数,并在其中依次检测到期的定时器,然后执行到期定时器上的回调函数。另一种思路是:将所有定时器中超时时间最小的一个定时器的超时值作为心搏函数,如此反复,就实现了较为精准的定时。
最小堆就很适合这种定时方案,最小堆每个节点的值都小于或等于其子节点的值的完全二叉树,
2, 定时器的触发方式
定时是指在一段时间之后触发某段代码的机制,定时机制是定时器得以被处理的原动力,Linux提供了三种定时方法:
再看

十四, 逻辑单元
1, 多线程处理网络io
多个线程同时处理同一个操作文件描述符是安全的,但是很麻烦
2, 多线程处理磁盘io---->多线程可以加速磁盘IO吗
多个线程read/write同一个磁盘上的多个文件不见得。因此每块磁盘上都有一个操作队列,多个线程的读写请求到了内核时排队执行的。只有在内核缓存了大部分数据的情况下,多线程读这些热数据才可能比单线程快。多线程磁盘IO的一个思路时每个磁盘配一个线程,把所有针对此磁盘的IO都挪到同一个线程

3, 多线程处理IO的原则
每个文件描述符只由一个线程操作,从而轻松解决消息收发的顺序问题,也避免了关闭文件描述符的各种race condition。 一个线程可以操作多个文件描述符,但是一个线程不能操作别的线程拥有的文件描述符
4, 多线程与文件描述符⭐⭐⭐
用RAII包装文件描述符
在单线程程序中或许可以通过某种全局表来避免串话,在多线程程序中,每次读写都加锁的话是没有效率的
C++用RAII进行解决

多线程安全销毁对象

十五, 逻辑处理方式
有限状态机:
有的应用层协议头部包含数据包类型字段,每种类型可以映射为逻辑单元的一种执行状态,服务器可以根据它来编写相应的处理逻辑

十六, Reactor模式:
以小饭馆的服务模式为例

每一个顾客就对应这服务器所接受到的外部事件(IO事件,定时器,信号),
服务员对应着事件分离器 负责监听外部事件
服务员会把菜单交给厨师 然后厨师针对这个事件进行处理 也就是做菜

我们以读操作为例来看看Reactor中的具体步骤:
1,应用程序注册读就绪事件和相关联的事件处理器
在饭馆里面每一个菜对应着有一个对应的厨师做那道菜
要先让服务员知道每个菜(注册就绪事件) 和那道菜对应的厨师 (注册对应的事件处理器)
2, 事件分离器等待事件的发生相当于服务员等待客人点菜
3, 当发生读就需事件的时候,事件分离器调用第一步注册的事件处理器,相当于客人点菜了 服务员找到对应这道菜的厨师
4, 事件处理器首先执行实际的读取操作,然后根据读取到的内容进行进一步的处理相当于大厨先知道这道菜的菜名 然后开始做
但是怎么做 用那些配料等等这些后续的操作是他自己来决定的,

由于这里真正执行IO操作的,也就是从内核去读取IO数据到缓存区这一步的,是事件处理器自身,也就是说在IO操作的第二阶段应用程序时阻塞的,所以我们说Reactor模式是同步的

  1. 应用程序注册读就绪事件和相关联的事件处理器
  2. 事件分离器等待事件的发生
  3. 当发生读就需事件的时候,事件分离器调用第一步注册的事件处理器
  4. 事件处理器首先执行实际的读取操作,然后根据读取到的内容进行进一步的处理

十七, Proactor模式

まだ

十八, 优化方向:池化技术/数据复制/上下文切换和锁
十九, 模块实现基础:多进程/多线程 在操作系统中再看吧

二十, 日志系统

二十一, :libevent

二十二, socket函数

二十三, bind函数
二十四, listen函数
二十五, accept函数
二十六, connect函数
二十七, send函数
二十八, Recv函数
二十九, Close函数
三十, Shutdown函数