做网站一般用什么语言,wordpress 互动性,wordpress生成ios app,免费商标图片我们在使用Redis的时候#xff0c;通常是多个客户端连接Redis服务器#xff0c;然后各自发送命令请求(例如Get、Set)到Redis服务器#xff0c;最后Redis处理这些请求返回结果。 从上一篇博文《Redis#xff08;08#xff09;| 线程模型》中知道Redis是单线程。Redis除了处…我们在使用Redis的时候通常是多个客户端连接Redis服务器然后各自发送命令请求(例如Get、Set)到Redis服务器最后Redis处理这些请求返回结果。 从上一篇博文《Redis08| 线程模型》中知道Redis是单线程。Redis除了处理客户端的命令请求还有诸如RDB持久化、AOF重写这样的事情要做而在做这些事情的时候Redis会fork子进程去完成。但对于accept客户端连接、处理客户端请求、返回命令结果等等这些Redis是使用主进程及主线程来完成的。我们可能会惊讶Redis在使用单进程及单线程来处理请求为什么会如此高效上个博文已经大致描述这里不在赘述。本次我们先来讨论一个I/O多路复用的模式–Reactor。
Reactor模式
思考场景问题
考虑这样一个问题有10000个客户端需要连上一个服务器并保持TCP连接客户端会不定时的发送请求给服务器服务器收到请求后需及时处理并返回结果。我们应该怎么解决?
方案一我们使用一个线程来监听当一个新的客户端发起连接时建立连接并new一个线程来处理这个新连接。 缺点当客户端数量很多时服务端线程数过多即便不压垮服务器由于CPU有限其性能也极其不理想。因此此方案不可用。
方案二我们使用一个线程监听当一个新的客户端发起连接时建立连接并使用线程池处理该连接。 优点客户端连接数量不会压垮服务端。 缺点服务端处理能力受限于线程池的线程数而且如果客户端连接中大部分处于空闲状态的话服务端的线程资源被浪费。
因此一个线程仅仅处理一个客户端连接无论如何都是不可接受的。那能不能一个线程处理多个连接呢该线程轮询每个连接如果某个连接有请求则处理请求没有请求则处理下一个连接这样可以实现吗 答案是肯定的而且不必轮询。我们可以通过I/O多路复用技术来解决这个问题。
I/O多路复用技术
现代的UNIX操作系统提供了select/poll/kqueue/epoll这样的系统调用这些系统调用的功能是你告知我一批套接字当这些套接字的可读或可写事件发生时我通知你这些事件信息。
根据圣经《UNIX网络编程卷1》。
当如下任一情况发生时会产生套接字的可读事件
该套接字的接收缓冲区中的数据字节数大于等于套接字接收缓冲区低水位标记的大小该套接字的读半部关闭也就是收到了FIN对这样的套接字的读操作将返回0也就是返回EOF该套接字是一个监听套接字且已完成的连接数不为0该套接字有错误待处理对这样的套接字的读操作将返回-1。
当如下任一情况发生时会产生套接字的可写事件
该套接字的发送缓冲区中的可用空间字节数大于等于套接字发送缓冲区低水位标记的大小该套接字的写半部关闭继续写会产生SIGPIPE信号非阻塞模式下connect返回之后该套接字连接成功或失败该套接字有错误待处理对这样的套接字的写操作将返回-1。
此外在UNIX系统上一切皆文件。套接字也不例外每一个套接字都有对应的fd即文件描述符。我们简单看看这几个系统调用的原型。 select(int nfds, fd_set *r, fd_set *w, fd_set *e, struct timeval *timeout)
对于select()我们需要传3个集合rw和e。其中 r表示我们对哪些fd的可读事件感兴趣 w表示我们对哪些fd的可写事件感兴趣。
每个集合其实是一个bitmap通过0/1表示我们感兴趣的fd。例如我们对于fd为6的可读事件感兴趣那么r集合的第6个bit需要被设置为1。这个系统调用会阻塞直到我们感兴趣的事件至少一个发生。调用返回时内核同样使用这3个集合来存放fd实际发生的事件信息。也就是说调用前这3个集合表示我们感兴趣的事件调用后这3个集合表示实际发生的事件。
select为最早期的UNIX系统调用它存在4个问题 1这3个bitmap有大小限制FD_SETSIZE通常为1024 2由于这3个集合在返回时会被内核修改因此我们每次调用时都需要重新设置 3我们在调用完成后需要扫描这3个集合才能知道哪些fd的读/写事件发生了一般情况下全量集合比较大而实际发生读/写事件的fd比较少效率比较低下 4内核在每次调用都需要扫描这3个fd集合然后查看哪些fd的事件实际发生在读/写比较稀疏的情况下同样存在效率问题。
由于存在这些问题于是人们对select进行了改进从而有了poll。
poll(struct pollfd *fds, int nfds, int timeout)
struct pollfd {int fd;short events;short revents;
}poll调用需要传递的是一个pollfd结构的数组调用返回时结果信息也存放在这个数组里面。 pollfd的结构中存放着fd、我们对该fd感兴趣的事件(events)以及该fd实际发生的事件(revents)。poll传递的不是固定大小的bitmap因此select的问题1解决了poll将感兴趣事件和实际发生事件分开了因此select的问题2也解决了。但select的问题3和问题4仍然没有解决。 select问题3比较容易解决只要系统调用返回的是实际发生相应事件的fd集合我们便不需要扫描全量的fd集合。 对于select的问题4我们为什么需要每次调用都传递全量的fd呢内核可不可以在第一次调用的时候记录这些fd然后我们在以后的调用中不需要再传这些fd呢 问题的关键在于无状态。对于每一次系统调用内核不会记录下任何信息所以每次调用都需要重复传递相同信息。 上帝说要有状态所以我们有了epoll和kqueue。
int epoll_create(int size);
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);epoll_create的作用是创建一个context这个context相当于状态保存者的概念。 epoll_ctl的作用是当你对一个新的fd的读/写事件感兴趣时通过该调用将fd与相应的感兴趣事件更新到context中。 epoll_wait的作用是等待context中fd的事件发生。
就是这么简单。 epoll是Linux中的实现kqueue则是在FreeBSD的实现。
int kqueue(void);
int kevent(int kq, const struct kevent *changelist, int nchanges, struct kevent *eventlist, int nevents, const struct timespec *timeout);与epoll相同的是kqueue创建一个context与epoll不同的是kqueue用kevent代替了epoll_ctl和epoll_wait。
epoll和kqueue解决了select存在的问题。通过它们我们可以高效的通过系统调用来获取多个套接字的读/写事件从而解决一个线程处理多个连接的问题。
Reactor的定义
通过select/poll/epoll/kqueue这些I/O多路复用函数库我们解决了一个线程处理多个连接的问题但整个Reactor模式的完整框架是怎样的呢参考这篇paper我们可以对Reactor模式有个完整的描述。
Handles 表示操作系统管理的资源我们可以理解为fd。 Synchronous Event Demultiplexer 同步事件分离器阻塞等待Handles中的事件发生。 Initiation Dispatcher 初始分派器作用为添加Event handler事件处理器、删除Event handler以及分派事件给Event handler。也就是说Synchronous Event Demultiplexer负责等待新事件发生事件发生时通知Initiation Dispatcher然后Initiation Dispatcher调用event handler处理事件。 Event Handler 事件处理器的接口 Concrete Event Handler 事件处理器的实际实现而且绑定了一个Handle。因为在实际情况中我们往往不止一种事件处理器因此这里将事件处理器接口和实现分开与C、Java这些高级语言中的多态类似。
以上各子模块间协作的步骤描述如下
我们注册Concrete Event Handler到Initiation Dispatcher中。Initiation Dispatcher调用每个Event Handler的get_handle接口获取其绑定的Handle。Initiation Dispatcher调用handle_events开始事件处理循环。在这里Initiation Dispatcher会将步骤2获取的所有Handle都收集起来使用Synchronous Event Demultiplexer来等待这些Handle的事件发生。当某个或某几个Handle的事件发生时Synchronous Event Demultiplexer通知Initiation Dispatcher。Initiation Dispatcher根据发生事件的Handle找出所对应的Handler。Initiation Dispatcher调用Handler的handle_event方法处理事件。 时序图如下
另外该文章举了一个分布式日志处理的例子感兴趣的同学可以看下。 通过以上的叙述我们清楚了Reactor的大概框架以及涉及到的底层I/O多路复用技术。 Java中的NIO与Netty 谈到Reactor模式在这里奉上Java大神Doug Lea的Scalable IO in Java里面提到了Java网络编程中的经典模式、NIO以及Reactor并且有相关代码帮助理解看完后获益良多。
另外Java的NIO是比较底层的我们实际在网络编程中还需要自己处理很多问题譬如socket的读半包稍不注意就会掉进坑里。幸好我们有了Netty这么一个网络处理框架免去了很多麻烦。
Redis与Reactor
在上面的讨论中我们了解了Reactor模式那么Redis中又是怎么使用Reactor模式的呢 首先Redis服务器中有两类事件文件事件和时间事件。 ● 文件事件file eventRedis客户端通过socket与Redis服务器连接而文件事件就是服务器对套接字操作的抽象。例如客户端发了一个GET命令请求对于Redis服务器来说就是一个文件事件。 ● 时间事件time event服务器定时或周期性执行的事件。例如定期执行RDB持久化。 在这里我们主要关注Redis处理文件事件的模型。参考《Redis的设计与实现》Redis的文件事件处理模型是这样的
在这个模型中Redis服务器用主线程执行I/O多路复用程序、文件事件分派器以及事件处理器。而且尽管多个文件事件可能会并发出现Redis服务器是顺序处理各个文件事件的。 Redis服务器主线程的执行流程在Redis.c的main函数中体现而关于处理文件事件的主要的有这几行
int main(int argc, char **argv) {...initServer();...aeMain();...aeDeleteEventLoop(server.el);return 0;
}在initServer()中建立各个事件处理器在aeMain()中执行事件处理循环在aeDeleteEventLoop(server.el)中关闭停止事件处理循环最后退出。