为什么要
思路
如果要让服务器服务多个客户端,那么最直接的方式就是为每一条连接创建线程
- 其实创建进程也是可以的,原理是一样的,进程和线程的区别在于线程比较轻量级些,线程的创建和线程将切换的成本要小一些,为了简便描述,后面都以线程为例
- 处理完业务逻辑后,随着连接关闭后线程也同样要销毁了,但是这样不停地创建和销毁线程,不仅会带来性能开销,也会造成浪费资源,而且如果要连接几万条连接,创建几万个线程去应对也是不现实的。
要怎么解决这个问题呢?我们可以使用资源复用的关系
- 也就是不用再为每个连接创建线程,而是创建一个“线程池”,将连接分配给线程,然后一个线程可以处理多个连接的业务。
不过,这样又引来了一个新的问题,线程怎样才能高效的处理多个连接的业务?
- 当一个连接对应一个线程时,线程一般采用【read-业务处理-send】的处理流程,如果当前连接没有数据可读,那么线程就会阻塞在
read
操作上(socket默认情况是阻塞IO),不过这样阻塞方式并不影响其他线程。 - 但是引入了线程池,那么一个线程要处理多个连接的业务,线程在处理某个连接的read操作时,如果没有数据可读,就会发生阻塞,那么线程就没有办法继续处理其他连接的业务。
要解决这一个问题,最简单的方法就是
- 将socket改成非阻塞,然后线程不断的轮询调用read操作来判断是否有数据
- 这种方式虽然能够解决阻塞的问题,但是解决的方式比较粗暴,因为轮询是要消耗 CPU 的,而且随着一个 线程处理的连接越多,轮询的效率就会越低。
上面的问题在于,线程并不知道当前连接是否有数据可读,从而需要每次通过 read 去试探。
那有没有办法在只有当连接上有数据的时候,线程才去发起读请求呢?答案是有的,实现这一技术的就是 I/O 多路复用。
- IO多路复用技术会用一个系统调用函数来监听我们所有关心的连接,也就是说可以在一个监控线程里面监控很多的连接。
- 我们熟悉的 select/poll/epoll 就是内核提供给用户态的多路复用系统调用,线程可以通过一个系统调用函数从内核中获取多个事件。
select/poll/epoll是如何获得网络事件的呢?
在获取事件时,先把我们要关心的连接传给内核,再由内核检测
- 如果没有事件发生,线程只需要阻塞在这个系统调用,而无需像前面的线程池方案那样轮询调用read操作来判断是否有数据
- 如果有事件发生,内核会返回产生了事件的连接,线程就会从阻塞状态返回,然后在用户态中再处理这些连接对应的业务即可
当下开源软件能够做到网络高性能的原因就是IO多路复用吗?
- 是的,基本是基于 I/O 多路复用,用过 I/O 多路复用接口写网络程序的同学,肯定知道是面向过程的方式写代码的,这样的开发的效率不高。
- 于是,大佬们基于面向对象的思想,对 I/O 多路复用作了一层封装,让使用者不用考虑底层网络 API 的细节,只需要关注应用代码的编写。有两种模型:Reactor模型和Proactor模型
思路
对高并发编程,网络连接上的消息处理,可以分为两个阶段:
- 等待消息准备好
- 消息处理
当使用默认的阻塞套接字时(例如1 个线程捆绑处理 1 个连接),往往是把这两个阶段合二为一,这样操作套接字的代码所在的线程就得睡眠等待消息准备好,这导致了高并发线程下会频繁的睡眠,唤醒,从而影响了CPU的使用效率
高并发编程方法当然就是把两个阶段分开处理:即,等待消息准备好的代码段与处理消息的代码段是分离的。当然,这也要求套接字必须是非阻塞的,否则,处理消息的代码段很容易导致条件不满足时,所在线程又进入了睡眠等待阶段
那么,问题来了,等待消息准备好的这个阶段怎么实现?它毕竟耗时等待,这意味着线程还是要睡眠的。
- 解决方法是:线程主动查询,或者让一个线程为所有连接而等待!这就是IO多路复用
- 多路复用就是处理等待消息准备好的这件事,但它可以同时处理多个连接!它也可能“”等待“,所以它也会导致线程睡眠。然而,这也不要紧,因为它一对多,可以监控所有连接。这样,当我们的线程被唤醒执行时,就一定是由一些连接准备好被我们的代码执行了。
作为一个高性能服务器通常需要考虑处理三类事件:IO事件、定时事件、信号。对此我们有两种比较高效的事件处理模式: Reactor和Proactor
- 同步IO模型通常用于实现
Reactor
模式。当然也可以模拟出Proactor
模式 - 异步IO模型通常用于实现
Proactor
模式。
服务器模型Reactor和Proactor
Reactor 模型
首先来回想一下普通函数调用的机制:
- 程序调用某函数
- 函数执行
- 程序等待
- 函数将结果和控制权返回给程序
- 程序继续处理。
Reactor 释义“反应堆”,是一种事件驱动机制。这里的反应堆得是对事件反应,也就是来了一个事件,Reactor就有相对应的反应/响应
实际上,Reactor 模式也叫 Dispatcher 模式,我觉得这个名字更贴合该模式的含义,即IO多路复用监听事件,收到事件后,根据事件类型分配给某个线程/进程
和普通函数调用的不同之处在于:应用程序不是主动地调用某个API完成处理,而是恰恰相反,Reactor 逆置了事件处理流程,应用程序需要提供相应的接口并注册到Reactor上,如果相应的事件发生,Reactor将主动调用应用程序注册的接口,这些接口又称为“回调函数”
Reactor 模式是处理并发IO比较常见的一种模式,用于同步IO:
- 中心思想是将所有要处理的IO事件注册到一种中心IO多路复用器上,同时主线程/进程阻塞到多路复用器上;一旦有IO事件到来或者是准备就绪(文件描述符或 socket 可读、写),多路复用器返回将事件注册的相应IO事件分发到对应的处理器中。
- 它要求主线程(IO处理单元)只负责监听文件描述符上是否有事件发生,有的话就立即将该事件通知工作线程(逻辑单元)。除此之外,主线程不做任何其他实质性的工作。读写事件、接受新的连接,以及处理客户请求均在工作线程中完成。
Reactor模型有三个重要的组件:
- 多路复用器:由操作系统提供,在Linux上一般是select、poll、epoll等系统调用(快递数据中心)
- 事件分发器:将多路复用器中返回的就绪事件分到对应的处理函数中(快递公司)
- 事件处理器:否则处理特定事件的处理函数(快递柜)
具体流程如下:
- 注册读就绪事件和相应的事件处理器
- 事件分离器等待事件
- 事件到来,激活分离器,分离器调用事件对应的处理器
- 事件处理器完成实际的读操作,处理读到的数据,注册新的事件,然后返还控制权
举个具体的例子,使用同步IO模式(以epoll_wait为例)实现的Reactor模式的工作流程是:
- 主线程往epoll内核事件表中注册socket上的读就绪事件
- 主线程调用
epoll_wait
等待socket
上的读就绪事件 - 当socket上有数据可读时,
epoll_wait
通知主线程。主线程则将socket可读事件放入请求队列 - 睡眠在请求队列上的某个工作线程被唤醒,它从socket上读取数据,并处理客户请求,然后往epoll内核事件中注册该socket上的写就绪事件。
- 主线程调用
epoll_wait
等待socket
可写 - 当socket可写时,
epoll_wait
通知主线程。主线程将socket
可写事件放入请求队列 - 睡眠在请求队列上的某个工作线程被唤醒,它往socket上写入服务器处理客户请求的结果
如上图:工作线程从请求队列中取出事件之后,根据事件的类型来决定如何处理它:
- 对于可读事件,执行读数据和处理请求的操作
- 对于可写事件,执行写数据的操作
所以,没有必要区分“读工作线程”和“写工作线程”
三种经典的Reactor 方案
Reactor模式是灵活多变的,可以应对不同的业务场景,灵活在于:
- Reactor 的数量可以只有一个,也可以有多个;
- 处理资源池可以是单个进程 / 线程,也可以是多个进程 /线程;
为此有多种
- 单 Reactor 单进程 / 线程;
- 单 Reactor 多进程 / 线程;
- 多 Reactor 单进程 / 线程;
- 多 Reactor 多进程 / 线程;
其中,[多 Reactor 单进程 / 线程」实现方案相比「单 Reactor 单进程 / 线程」方案,不仅复杂而且也没有性能优势,因此实际中并没有应用。
剩下的 3 个方案都是比较经典的,且都有应用在实际的项目中:
- 单 Reactor 单进程 / 线程;
- 单 Reactor 多线程 / 进程;
- 多 Reactor 多进程 / 线程;
方案具体使用进程还是线程,要看使用的编程语言以及平台有关:
- Java 语言一般使用线程,比如 Netty;
- C 语言使用进程和线程都可以,例如 Nginx 使用的是进程,Memcache 使用的是线程。
接下来,分别介绍这三个经典的 Reactor 方案。
单 Reactor 单进 / 线程
一般来说,C语言实现的是单Reactor单进程的方案,因为C语言编写完的程序,运行后就是一个独立的进程,不需要在进程中在创建线程
而Java语言实现的是单Reactor单线程的方案,因为Java程序是泡在Java虚拟机这个进程上面的,虚拟机中有很多线程,我们写的Java只是其中一个线程而已
我们来看看「单 Reactor 单进程」的方案示意图:
可以看到进程里有 Reactor、Acceptor、Handler 这三个对象:
- Reactor对象的作用是监听和分发事件
- Accept对象的作用是获取连接
- Handle对象的作用是处理业务
对象里的select、accept、read、send是系统调用,dispatch和业务处理是需要完成的操作,其中dispatch是分发事件操作。
接下来,介绍下【单Reactor单进程】这个方案
- Reactor 对象通过 select (IO 多路复用接口) 监听事件,收到事件后通过 dispatch 进行分发,具体分发给 Acceptor 对象还是 Handler 对象,还要看收到的事件类型;
- 如果是连接建立的事件,则交给Acceptor对象进行处理,Acceptor 对象会通过accept分发获取连接,并创建一个Handle对象来处理后继的响应事件
- 如果不是连接建立事件, 则交由当前连接对应的 Handler 对象来进行响应;
- Handler对象通过read->业务处理->send的流程来完成完整的业务流程
单 Reactor 单进程的方案因为全部工作都在同一个进程内完成,所以实现起来比较简单,不需要考虑进程间通信,也不用担心多进程竞争。
但是,这种方案存在 2 个缺点:
- 第一个缺点,因为只有一个进程,无法充分利用多核CPU的性能
- 第二个缺点,Handler 对象在业务处理时,整个进程是无法处理其他连接的事件的,如果业务处理耗时比较长,那么就造成响应的延迟;
所以,单 Reactor 单进程的方案不适用计算机密集型的场景,只适用于业务处理非常快速的场景。
Redis 是由 C 语言实现的,它采用的正是「单 Reactor 单进程」的方案,因为 Redis 业务处理主要是在内存中完成,操作的速度是很快的,性能瓶颈不在 CPU 上,所以 Redis 对于命令的处理是单进程的方案。
单 Reactor 多进 / 线程
如果要克服「单 Reactor 单线程 / 进程」方案的缺点,那么就需要引入多线程 / 多进程,这样就产生了单 Reactor 多线程 / 多进程的方案。
「单 Reactor 多线程」方案的示意图如下:
详细说一下这个方案:
- Reactor 对象通过 select (IO 多路复用接口) 监听事件,收到事件后通过 dispatch 进行分发,具体分发给 Acceptor 对象还是 Handler 对象,还要看收到的事件类型;
- 如果是连接建立的事件,则交由 Acceptor 对象进行处理,Acceptor 对象会通过 accept 方法 获取连接,并创建一个 Handler 对象来处理后续的响应事件;
- 如果不是连接建立事件, 则交由当前连接对应的 Handler 对象来进行响应;
上面的三个步骤和单Reactor单线程方案是一样的,接下来的步骤就开始不一样了
- Handler 对象不再负责业务处理,只负责数据的接收和发送,Handler 对象通过 read 读取到数据后,会将数据发给子线程里的 Processor 对象进行业务处理;
- 子线程里的 Processor 对象就进行业务处理,处理完后,将结果发给主线程中的 Handler 对象,接着由 Handler 通过 send 方法将响应结果发送给 client;
单 Reator 多线程的方案优势在于能够充分利用多核 CPU 的能,那既然引入多线程,那么自然就带来了多线程竞争资源的问题。
比如,子线程完成业务处理后,要把结果传递给主线程的Reactor进行发送,这里涉及到共享数据的竞争。
要避免多线程由于竞争共享资源而导致数据错乱的问题,就需要在操作共享资源前加上互斥锁,以保证任意时间里只有一个线程在操作共享资源,待该线程操作完释放互斥锁后,其他线程才有机会操作共享数据。
聊完单 Reactor 多线程的方案,接着来看看单 Reactor 多进程的方案。
事实上,单 Reactor 多进程相比单 Reactor 多线程实现起来很麻烦,主要因为要考虑子进程 <-> 父进程的双向通信,并且父进程还得知道子进程要将数据发送给哪个客户端。
而多线程间可以共享数据,虽然要额外考虑并发问题,但是这远比进程间通信的复杂度低得多,因此实际应用中也看不到单 Reactor 多进程的模式。
另外,【单Reactor】的模式还有个问题,因为一个Reactor对象承担所有事件的监听和响应,而且只能在主线程中运行,在面对瞬间高并发的场景时,容易成为性能的瓶颈的地方
多 Reactor 多进程 / 线程
要解决【单Reactor】的问题,就是将【单Reactor】实现成【多Reactor】,这样就产生了【多 Reactor 多进程 / 线程】方案
多 Reactor 多进程 / 线程方案的示意图如下(以线程为例):
方案详细说明如下:
- 主线程中的MainReactor对象通过select监控连接建立事件,收到事件后通过accept对象中的accept获取连接,将新的连接分配给某个子线程
- 子线程中的SubReactor 对象将MainReactor对象分配的连接加入select继续进行监听,并创建一个Handler用于处理连接的响应事件
- 如果有新的事件发生时,SubReactor 对象会调用当前连接对应的 Handler 对象来进行响应。
- Handler 对象通过 read -> 业务处理 -> send 的流程来完成完整的业务流程。
多 Reactor 多线程的方案虽然看起来复杂的,但是实际实现时比单 Reactor 多线程的方案要简单的多,原因如下:
- 主线程和子线程分工明确,主线程只负责接收新连接,子线程负责完成后续的业务处理。
- 主线程和子线程的交互很简单,主线程只需要把新连接传给子线程,子线程无须返回数据,直接就可以在子线程将处理结果发送给客户端。
大名鼎鼎的两个开源软件 Netty 和 Memcache 都采用了「多 Reactor 多线程」的方案。
采用了「多 Reactor 多进程」方案的开源软件是 Nginx,不过方案与标准的多 Reactor 多进程有些差异。
具体差异表现在主进程中仅仅用来初始化 socket,并没有创建 mainReactor 来 accept 连接,而是由子进程的 Reactor 来 accept 连接,通过锁来控制一次只有一个子进程进行 accept(防止出现惊群现象),子进程 accept 新连接后就放到自己的 Reactor 进行处理,不会再分配给其他子进程。
优缺点
Reactor 模式是编写高性能网络服务器的必备技术之一,它具有如下的优点:
- 响应快,不必为单个同步时间所阻塞,虽然Reactor 本身依然是同步的
- 编程相对比较简单,可以最大程序的避免复杂的多线程以及同步问题,并且避免了多线程/进程的切换开销
- 可扩展性,可以方便的通过增加Reactor实例个数来重复利用CPU资源
- 可复用性,reactor 框架本身与具体事件处理逻辑无关,具有很高的复用性;
Reactor模型开发效率比直接使用IO复用要高,它通常是单线程的,设计目标是希望单线程使用一颗CPU的全部资源:
- 优点是:每个事件处理中很多时候可以不考虑共享资源的互斥访问
- 缺点很明显:
- 现在的硬件发展,已经不再遵循摩尔定律,CPU的频率受制于材料的限制不再有大的提升,而改为从核数的增加上提升能力,当程序需要使用多核资源时,Reactor 模型就会悲剧
- 如果程序业务很简单,比如只是简单的访问了一些提供了并发访问的服务器,就可以直接开启多个反应堆,每个反应堆对应一颗CPU信号,这些反应堆上跑的请求互不相关,这是完全可以利用多核的。例如 Nginx 这样的 http 静态服务器。
reactor与http
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/epoll.h>
#include <arpa/inet.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#include <time.h>
#include <sys/stat.h>
#include <sys/sendfile.h>
#define BUFFER_LENGTH 4096
#define MAX_EPOLL_EVENTS 1024
#define SERVER_PORT 8889
#define PORT_COUNT 1
#define HTTP_WEBSERVER_HTML_ROOT "/home/oceanstar/CLionProjects/untitled"
#define HTTP_METHOD_GET 0
#define HTTP_METHOD_POSY 1
typedef int NCALLBACK (int, int , void *) ;
struct ntyevent{
int fd;
int events;
int (*callback)(int fd, int events, void *arg);
void *arg;
int status;
char buffer[BUFFER_LENGTH];
int length;
long last_active;
int method;
char resource[BUFFER_LENGTH];
int ret_code;
};
struct eventblock{
struct eventblock *next;
struct ntyevent *events;
};
struct ntyreactor{
int epfd;
int blkcnt;
struct eventblock *evblk;
};
int recv_cb(int fd, int events, void *arg);
int send_cb(int fd, int events, void *arg);
struct ntyevent *ntyreactor_idx(struct ntyreactor *reactor, int sockfd);
void nty_event_set(struct ntyevent *ev, int fd, NCALLBACK callback, void *arg){
ev->fd = fd;
ev->callback = callback;
ev->events = 0;
ev->arg = arg;
ev->last_active = time(NULL);
}
int nty_event_add(int epfd, int events, struct ntyevent *ev){
struct epoll_event ep_ev = {0, {0}};
ep_ev.data.ptr = ev;
ep_ev.events = ev->events = events;
int op;
if(ev->status == 1){
op = EPOLL_CTL_MOD;
}else{
op = EPOLL_CTL_ADD;
ev->status = 1;
}
if(epoll_ctl(epfd, op, ev->fd, &ep_ev) < 0){
printf("event add failed [fd = %d], events[%d]\n", ev->fd, events);
return -1;
}
return 0;
}
int nty_event_del(int epfd, struct ntyevent *ev){
struct epoll_event ep_ev = {0, {0}};
if(ev->status != 1){
return -1;
}
ev->status = 0;
ep_ev.data.ptr = ev;
epoll_ctl(epfd, EPOLL_CTL_DEL, ev->fd, &ep_ev);
return 0;
}
/*
* allbuf: 待处理的包裹
* idx:从包裹的第idx个字节开始查看
* linebuf:将字节拷贝到linebuf中
* */
int readline(char *allbuf, int idx, char *linebuf){
int len = strlen(allbuf);
for (; idx < len; idx++){
//idx + 1有溢出风险(待解决)
if(allbuf[idx] == '\r' && allbuf[idx + 1] == '\n'){
return idx + 2;
}else{
*(linebuf++) = allbuf[idx];
}
}
return -1;
}
// 快递公司拆包,并根据其中内容做出不同的应对
int http_request(struct ntyevent *ev){
char linebuf[1024] = {0};
int idx = readline(ev->buffer, 0, linebuf);
if(strstr(linebuf, "GET")){
ev->method = HTTP_METHOD_GET;
// url: GET / HTTP/1.1
int i = 0;
while (linebuf[sizeof("GET ") + i] != ' ') i++;
linebuf[sizeof("GET" + i)] = '\0';
sprintf(ev->resource, "%s/%s", HTTP_WEBSERVER_HTML_ROOT, linebuf+sizeof("GET "));
}else if (strstr(linebuf, "POST")) {
}
return 0;
}
// fd (clientfd)发送了数据包裹
int recv_cb(int fd, int events, void *arg){
struct ntyreactor *reactor = (struct ntyreactor *)arg; //这个包裹是由哪个快递公司处理的
struct ntyevent *ev = ntyreactor_idx(reactor, fd); //这个包裹应该存放到哪个快递柜中
//将fd要发生的数据包裹放到快递柜的缓冲区中
int len = recv(fd, ev->buffer, BUFFER_LENGTH, 0);
if(len > 0){
ev->length = len;
ev->buffer[len] = '\0';
printf("C[%d]:%s\n", fd, ev->buffer);
http_request(ev);
// 已经查看完包裹了,接下来就要给顾客发送一个回执单了
//接下来要等待顾客有空时将回执发送出去
nty_event_del(reactor->epfd, ev); //在回执之前不再等待顾客新包裹,因此将【等待顾客发送】这一事件从快递处理中心移除
nty_event_set(ev, fd, send_cb, reactor);
nty_event_add(reactor->epfd, EPOLLOUT, ev);
}else if(len == 0){
nty_event_del(reactor->epfd, ev);
close(ev->fd);
}else{
nty_event_del(reactor->epfd, ev);
close(ev->fd);
printf("recv[fd=%d] error[%d]:%s\n", fd, errno, strerror(errno));
}
return len;
}
int http_response0(struct ntyevent *ev){
if(ev == NULL) return -1;
memset(ev->buffer, 0, BUFFER_LENGTH);
const char *html = "<html><head><title>hello http</title></head><body><H1>King</H1></body></html>\r\n\r\n";
ev->length = sprintf(ev->buffer,
"HTTP/1.1 200 OK\r\nDate: Thu, 11 Nov 2021 12:28:52 GMT\r\nContent-Type: text/html;charset=ISO-8859-1\r\nContent-Length: 83\r\n\r\n%s",
html);
return ev->length;
}
// 现在空闲了,快递数据处理中心通知可以给顾客发送回执单了
int send_cb0(int fd, int events, void *arg){
struct ntyreactor *reactor = (struct ntyreactor *)arg; //是哪个快递公司
struct ntyevent *ev = ntyreactor_idx(reactor, fd); //这个fd的数据是放在快递中心reactor的哪个快递柜中的
// 快递中心填写回执单
http_response0(ev);
// 将回执单发送出去
int len = send(fd, ev->buffer, ev->length, 0);
if(len > 0){
printf("send[fd=%d], [%d]%s\n", fd, len, ev->buffer);
// 回执单发送完毕之后等待顾客新的需求
nty_event_del(reactor->epfd, ev);
nty_event_set(ev, fd, recv_cb, reactor);
nty_event_add(reactor->epfd, EPOLLIN, ev);
}else{
close(ev->fd);
nty_event_del(reactor->epfd, ev);
printf("send[fd=%d] error %s\n", fd, strerror(errno));
}
return len;
}
int http_response(struct ntyevent *ev){
if(ev == NULL) return -1;
memset(ev->buffer, 0, BUFFER_LENGTH);
printf("resource: %s\n", ev->resource);
int filefd = open(ev->resource, O_RDONLY);
if(filefd == -1){
ev->ret_code = 404;
ev->length = sprintf(ev->buffer,
"HTTP/1.1 404 Not Found\r\n"
"Date: Thu, 11 Nov 2021 12:28:52 GMT\r\n"
"Content-Type: text/html;charset=ISO-8859-1\r\n"
"Content-Length: 85\r\n\r\n"
"<html><head><title>404 Not Found</title></head><body><H1>404</H1></body></html>\r\n\r\n" );
}else{
struct stat stat_buf;
fstat(filefd, &stat_buf);
close(filefd);
if (S_ISDIR(stat_buf.st_mode)) {
ev->ret_code = 404;
ev->length = sprintf(ev->buffer,
"HTTP/1.1 404 Not Found\r\n"
"Date: Thu, 11 Nov 2021 12:28:52 GMT\r\n"
"Content-Type: text/html;charset=ISO-8859-1\r\n"
"Content-Length: 85\r\n\r\n"
"<html><head><title>404 Not Found</title></head><body><H1>404</H1></body></html>\r\n\r\n" );
} else if (S_ISREG(stat_buf.st_mode)) {
ev->ret_code = 200;
ev->length = sprintf(ev->buffer,
"HTTP/1.1 200 OK\r\n"
"Date: Thu, 11 Nov 2021 12:28:52 GMT\r\n"
"Content-Type: text/html;charset=ISO-8859-1\r\n"
"Content-Length: %ld\r\n\r\n",
stat_buf.st_size );
}
}
return ev->length;
}
// 现在空闲了,快递数据处理中心通知可以给顾客发送回执单了
int send_cb(int fd, int events, void *arg){
struct ntyreactor *reactor = (struct ntyreactor *)arg; //是哪个快递公司
struct ntyevent *ev = ntyreactor_idx(reactor, fd); //这个fd的数据是放在快递中心reactor的哪个快递柜中的
// 快递中心填写回执单
http_response(ev);
// 将回执单发送出去
int len = send(fd, ev->buffer, ev->length, 0);
if(len > 0){
printf("send[fd=%d], [%d]%s\n", fd, len, ev->buffer);
if(ev->ret_code == 200){
int filefd = open(ev->resource, O_RDONLY );
struct stat stat_buf;
fstat(filefd, &stat_buf);
sendfile(fd, filefd, NULL, stat_buf.st_size);
close(filefd);
}
// 回执单发送完毕之后等待顾客新的需求
nty_event_del(reactor->epfd, ev);
nty_event_set(ev, fd, recv_cb, reactor);
nty_event_add(reactor->epfd, EPOLLIN, ev);
}else{
close(ev->fd);
nty_event_del(reactor->epfd, ev);
printf("send[fd=%d] error %s\n", fd, strerror(errno));
}
return len;
}
// 有顾客发邮件说有包裹要发送
int accept_cb(int fd, int events, void *arg){ //对应事件发生了
struct ntyreactor *reactor = (struct ntyreactor *)arg; // 找到这个事件是由哪个快递公司处理的
if(reactor == NULL) return -1;
struct sockaddr_in client_addr;
socklen_t len = sizeof(client_addr);
//当前是哪个顾客要预定了要发送数据
int clientfd;
if((clientfd = accept(fd, (struct sockaddr *)&client_addr, &len)) == -1){
if(errno != EAGAIN && errno != EINTR){
}
printf("accept: %s\n", strerror(errno));
return -1;
}
if((fcntl(clientfd, F_SETFL, O_NONBLOCK)) < 0){
printf("%s: fcntl nonblocking failed, %d\n", __func__, MAX_EPOLL_EVENTS);
return -1;
}
// 给这个顾客分配一个快递柜
struct ntyevent *event = ntyreactor_idx(reactor, clientfd);
nty_event_set(event, clientfd, recv_cb, reactor); // 给这个快递柜贴上标签:reactor快递公司将会接收到顾客clientfd的包裹,当有包裹来了应对手段是recv_cb
nty_event_add(reactor->epfd, EPOLLIN, event); //要求处理中心(epoll)关注快递柜event的数据到来(EPOLLIN)事件,当有EPOLLIN事件到了就通知上层用户态是哪个快递柜上发生了事件
printf("new connect [%s:%d], pos[%d]\n",
inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port), clientfd);
return 0;
}
int init_sock(short port) {
int fd = socket(AF_INET, SOCK_STREAM, 0);
fcntl(fd, F_SETFL, O_NONBLOCK);
struct sockaddr_in server_addr;
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
server_addr.sin_port = htons(port);
bind(fd, (struct sockaddr*)&server_addr, sizeof(server_addr));
if (listen(fd, 20) < 0) {
printf("listen failed : %s\n", strerror(errno));
}
return fd;
}
int ntyreactor_alloc(struct ntyreactor *reactor) {
if (reactor == NULL) return -1;
if (reactor->evblk == NULL) return -1;
struct eventblock *blk = reactor->evblk;
while (blk->next != NULL){
blk = blk->next;
}
struct ntyevent *evs = (struct ntyevent *)malloc(MAX_EPOLL_EVENTS * sizeof(struct ntyevent));
if(evs == NULL){
printf("ntyreactor_alloc ntyevents failed\n");
return -2;
}
memset(evs, 0, (MAX_EPOLL_EVENTS) * sizeof(struct ntyevent));
struct eventblock *block = (struct eventblock *)malloc(sizeof(struct eventblock));
if (block == NULL) {
printf("ntyreactor_alloc eventblock failed\n");
return -2;
}
memset(block, 0, sizeof(struct eventblock));
block->events = evs;
block->next = NULL;
blk->next = block;
reactor->blkcnt ++;
return 0;
}
struct ntyevent *ntyreactor_idx(struct ntyreactor *reactor, int sockfd){
int blkidx = sockfd / MAX_EPOLL_EVENTS;
while (blkidx >= reactor->blkcnt){
ntyreactor_alloc(reactor);
}
int i = 0;
struct eventblock *blk = reactor->evblk;
while (i++ < blkidx && blk != NULL){
blk = blk->next;
}
return &blk->events[sockfd % MAX_EPOLL_EVENTS];
}
int ntyreactor_init(struct ntyreactor *reactor){
if(reactor == NULL) return -1;
memset(reactor, 0, sizeof(struct ntyreactor));
reactor->epfd = epoll_create(1);
if(reactor->epfd <= 0){
printf("create epfd in %s err %s\n", __func__, strerror(errno));
return -2;
}
struct ntyevent *evs = (struct ntyevent *)malloc(MAX_EPOLL_EVENTS * sizeof(struct ntyevent));
if(evs == NULL){
printf("ntyreactor_alloc ntyevents failed\n");
close(reactor->epfd);
return -3;
}
memset(evs, 0, (MAX_EPOLL_EVENTS) * sizeof(struct ntyevent));
struct eventblock *block = (struct eventblock *)malloc(sizeof(struct eventblock));
if (block == NULL) {
printf("ntyreactor_alloc eventblock failed\n");
return -2;
}
memset(block, 0, sizeof(struct eventblock));
block->events = evs;
block->next = NULL;
reactor->evblk = block;
reactor->blkcnt = 1;
return 0;
}
int ntyreactor_destory(struct ntyreactor *reactor){
close(reactor->epfd);
struct eventblock *blk = reactor->evblk;
struct eventblock *blk_next = NULL;
while (blk != NULL){
blk_next = blk->next;
free(blk->events);
free(blk);
blk = blk_next;
}
return 0;
}
int ntyreactor_addlistener(struct ntyreactor *reactor, int sockfd, NCALLBACK *acceptor){
if(reactor == NULL) return -1;
if(reactor->evblk == NULL) return -1;
struct ntyevent *event = ntyreactor_idx(reactor, sockfd); // 去快递公司reactor寻找sockfd对应的快递柜
nty_event_set(event, sockfd, acceptor, reactor); // 快递柜中存放的是:编号sockfd,应对手段是accept,当事情来了之后由快递公司reactor处理
nty_event_add(reactor->epfd, EPOLLIN, event); //要求快递员关注快递柜event中的EPOLLIN实践
return 0;
}
int ntyreactor_run(struct ntyreactor *reactor) {
if (reactor == NULL) return -1;
if (reactor->epfd < 0) return -1;
if (reactor->evblk == NULL) return -1;
struct epoll_event events[MAX_EPOLL_EVENTS+1];
while (1) {
int nready = epoll_wait(reactor->epfd, events, MAX_EPOLL_EVENTS, 1000);
if (nready < 0) {
printf("epoll_wait error, exit\n");
continue;
}
for (int i = 0;i < nready;i ++) {
struct ntyevent *ev = (struct ntyevent*)events[i].data.ptr;
if ((events[i].events & EPOLLIN) && (ev->events & EPOLLIN)) {
ev->callback(ev->fd, events[i].events, ev->arg);
}
if ((events[i].events & EPOLLOUT) && (ev->events & EPOLLOUT)) {
ev->callback(ev->fd, events[i].events, ev->arg);
}
}
}
}
int main(int argc, char *argv[]) {
unsigned short port = SERVER_PORT; // listen 8888
if (argc == 2) {
port = atoi(argv[1]);
}
struct ntyreactor *reactor = (struct ntyreactor *)malloc(sizeof(struct ntyreactor));
ntyreactor_init(reactor);
int sockfds[PORT_COUNT] = {0};
for (int i = 0; i < PORT_COUNT; ++i) {
sockfds[i] = init_sock(port + i);
ntyreactor_addlistener(reactor, sockfds[i], accept_cb);
}
ntyreactor_run(reactor);
for (int i = 0; i < PORT_COUNT; ++i) {
close(sockfds[i]);
}
ntyreactor_destory(reactor);
free(reactor);
}
reactor与websocket(下面代码有问题,仅能处理一次接收,待修改)
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/epoll.h>
#include <arpa/inet.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#include <time.h>
#include <openssl/sha.h>
#include <openssl/pem.h>
#include <openssl/bio.h>
#include <openssl/evp.h>
#define BUFFER_LENGTH 4096
#define MAX_EPOLL_EVENTS 1024
#define SERVER_PORT 8888
#define PORT_COUNT 100
typedef int NCALLBACK(int ,int, void*);
// 快递柜
struct ntyevent{
int fd;
int events; //当前快递柜关注的事件
void *arg;
int (*callback)(int fd, int events, void *arg); //当事件来了之后的应对手段
int status; // 当前快递柜是否已经加入控制中心的监控下
char buffer[BUFFER_LENGTH];//用于存放包裹的缓冲区
int length; //当前包裹已用空间
long last_active; //最近使用这个快递柜的时间
int status_mechaine;
};
// 快递菜鸟驿站(一个小区一家菜鸟驿站)
struct eventblock{
struct eventblock *next ; //下一家菜鸟驿站
struct ntyevent *events; //这家菜鸟驿站管理的所有快递柜
};
// 快递公司
struct ntyreactor {
/*数据中心将能够感知到【在哪个快递柜上发生了(输入、输出、错误)事件】,
* 然后根据快递柜中注册的内容,做出反应,也就是让编译器callback,
* 编译器调度callback时,能够根据参数arg知道是由哪个快递公司管理的,
* 然后根据快递公司以及顾客编号找出对应快递柜,并根据事件类型做出反应
* 【接收数据到快递柜的缓冲区、将数据放入快递柜的缓冲区中等待发送】
* */
int epfd; //快递公司被哪个数据管理中心管理
int blkcnt; //这个快递公司管理了多少菜鸟驿站
struct eventblock *evblk; //这家快递公司管理的所有的菜鸟驿站的地址
};
int recv_cb(int fd, int events, void *arg);
int send_cb(int fd, int events, void *arg);
struct ntyevent *ntyreactor_idx(struct ntyreactor *reactor, int sockfd);
int readline(char *allbuf, int idx, char *linebuf);
int base64_encode(char *in_str, int in_len, char *out_str) {
BIO *b64, *bio;
BUF_MEM *bptr = NULL;
size_t size = 0;
if (in_str == NULL || out_str == NULL)
return -1;
b64 = BIO_new(BIO_f_base64());
bio = BIO_new(BIO_s_mem());
bio = BIO_push(b64, bio);
BIO_write(bio, in_str, in_len);
BIO_flush(bio);
BIO_get_mem_ptr(bio, &bptr);
memcpy(out_str, bptr->data, bptr->length);
out_str[bptr->length-1] = '\0';
size = bptr->length;
BIO_free_all(bio);
return size;
}
/*
* allbuf: 待处理的包裹
* idx:从包裹的第idx个字节开始查看
* linebuf:将字节拷贝到linebuf中
* */
int readline(char *allbuf, int idx, char *linebuf){
int len = strlen(allbuf);
for (; idx < len; ++idx) {
if (allbuf[idx] == '\r' && allbuf[idx+1] == '\n') {
return idx + 2;
}else{
*(linebuf++) = allbuf[idx];
}
}
return -1;
}
//快递柜中存放了 是哪个顾客(fd)的、是什么时候使用这个快递柜的、当事件来了怎么应对
void nty_event_set(struct ntyevent *ev, int fd, NCALLBACK callback, void *arg){
ev->fd = fd;
ev->callback = callback;
ev->events = 0;
ev->arg = arg;
ev->last_active= time(NULL);
}
//数据中心将会关注快递柜上的events事件
int nty_event_add(int epfd, int events, struct ntyevent *ev) {
struct epoll_event ep_ev = {0, {0}};
ep_ev.data.ptr = ev;
ep_ev.events = ev->events = events;
int op;
if (ev->status == 1) {
op = EPOLL_CTL_MOD;
} else {
op = EPOLL_CTL_ADD;
ev->status = 1;
}
if (epoll_ctl(epfd, op, ev->fd, &ep_ev) < 0) {
printf("event add failed [fd=%d], events[%d]\n", ev->fd, events);
return -1;
}
return 0;
}
//数据中心不再关注快递柜上的事件
int nty_event_del(int epfd, struct ntyevent *ev) {
struct epoll_event ep_ev = {0, {0}};
if (ev->status != 1) {
return -1;
}
ep_ev.data.ptr = ev;
ev->status = 0;
epoll_ctl(epfd, EPOLL_CTL_DEL, ev->fd, &ep_ev);
return 0;
}
/*
ev->buffer :
ev->length
GET / HTTP/1.1
Host: 192.168.232.128:8888
Connection: Upgrade
Pragma: no-cache
Cache-Control: no-cache
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/95.0.4638.69 Safari/537.36
Upgrade: websocket
Origin: null
Sec-WebSocket-Version: 13
Accept-Encoding: gzip, deflate
Accept-Language: zh-TW,zh;q=0.9,en-US;q=0.8,en;q=0.7
Sec-WebSocket-Key: QWz1vB/77j8J8JcT/qtiLQ==
Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits
str = "QWz1vB/77j8J8JcT/qtiLQ==258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
sha = SHA1(str);
value = base64_encode(sha);
*/
#define GUID "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
enum {
WS_HANDSHARK = 0,
WS_TRANMISSION = 1,
WS_END = 2,
};
// WebSocket的帧结构 ---封装websocket的head包
// 19 : length of "Sec-WebSocket-Key: "
#define WEBSOCK_KEY_LENGTH 19
typedef struct _ws_ophdr{
unsigned char opcode:4,
rcv3:1,
rsv2:1,
rsv1:1,
fin:1;
unsigned char payload_len:7,
mask:1;
}ws_ophdr;
typedef struct _ws_head_126 {
unsigned short payload_length;
char mask_key[4];
} ws_head_126;
typedef struct _ws_head_127 {
long long payload_length;
char mask_key[4];
} ws_head_127;
int send_cb(int fd, int events, void *arg){
struct ntyreactor *reactor = (struct ntyreactor *)arg; //这是哪家快递公司处理的包裹
struct ntyevent *ev = ntyreactor_idx(reactor, fd); //找到fd所属reactor管理的所有机房中的某个快递柜
// 发送回应
int len = send(fd, ev->buffer, ev->length, 0);
if (len > 0) {
printf("[%s:%d] send[fd=%d], [%d]%s\n", __func__ , __LINE__, fd, len, ev->buffer);
nty_event_del(reactor->epfd, ev);
nty_event_set(ev, fd, recv_cb, reactor);
nty_event_add(reactor->epfd, EPOLLIN, ev);
} else {
close(ev->fd);
nty_event_del(reactor->epfd, ev);
printf("[%s:%d] send[fd=%d] error %s\n", __func__ , __LINE__, fd, strerror(errno));
}
return len;
}
// 拆包
void unmask(char *payload, int length, char *maskkey){
for (int i = 0; i < length; ++i) {
payload[i] ^= maskkey[i % 4];
}
}
int transmission(struct ntyevent *ev){
ws_ophdr *hdr = (ws_ophdr*)ev->buffer;
printf("[%s:%d] length: %d\n", __func__ , __LINE__, hdr->payload_len);
if(hdr->payload_len < 126){
char *payload = (ev->buffer + sizeof(ws_ophdr) + 4);
if(hdr->mask){
unmask(payload, hdr->payload_len, ev->buffer + 2);
}
printf("[%s:%d] payload : %s\n", __func__ , __LINE__, payload);
}else if(hdr->payload_len == 126){
ws_head_126 *hdr126 = (ws_head_126 *)(ev->buffer + sizeof(ws_ophdr));
}else{
ws_head_127 *hdr127 = (ws_head_127 *)(ev->buffer + sizeof(ws_ophdr));
}
return 0;
}
// 握手: 1. 拆解shark包 2. 生成升级头
int handshark(struct ntyevent *ev){
char linebuf[1024] = {0};
int idx = 0;
char sec_data[128] = {0};
char sec_accept[32] = {0};
do{
memset(linebuf, 0, 1024);
idx = readline(ev->buffer, idx, linebuf);
if (strstr(linebuf, "Sec-WebSocket-Key")) {
//linebuf: Sec-WebSocket-Key: QWz1vB/77j8J8JcT/qtiLQ==
strcat(linebuf, GUID);
//linebuf: Sec-WebSocket-Key: QWz1vB/77j8J8JcT/qtiLQ==258EAFA5-E914-47DA-95CA-C5AB0DC85B11
SHA1(reinterpret_cast<const unsigned char *>(linebuf + WEBSOCK_KEY_LENGTH), strlen(linebuf + WEBSOCK_KEY_LENGTH),
reinterpret_cast<unsigned char *>(sec_data)); // openssl
base64_encode(sec_data, strlen(sec_data), sec_accept);
memset(ev->buffer, 0, BUFFER_LENGTH);
ev->length = sprintf(ev->buffer, "HTTP/1.1 101 Switching Protocols\r\n"
"Upgrade: websocket\r\n"
"Connection: Upgrade\r\n"
"Sec-WebSocket-Accept: %s\r\n\r\n", sec_accept);
printf("[%s:%d]ws response : %s\n", __FUNCTION__ , __LINE__, ev->buffer);
break;
}
}while ((ev->buffer[idx] != '\r' || ev->buffer[idx + 1] != '\n') && idx != -1);
return 0;
}
// 根据(之前,即recv_cb中)收到的数据做出回应
int websocket_request(struct ntyevent *ev){
if(ev->status_mechaine == WS_HANDSHARK){ //如果是一个新的连接到来
handshark(ev); //那么先握手
ev->status_mechaine = WS_TRANMISSION; //握手之后就进入交谈阶段
}else if(ev->status_mechaine == WS_TRANMISSION){ //如果是交谈阶段
transmission(ev);
}else{
}
printf("[%s:%d]websocket_request --> %d\n", __func__ , __LINE__, ev->status_mechaine);
return 0;
}
//接收数据并拆解包裹
int recv_cb(int fd, int events, void *arg){
struct ntyreactor *reactor = (struct ntyreactor *)arg; //这是哪家快递公司处理的包裹
struct ntyevent *ev = ntyreactor_idx(reactor, fd); //找到fd所属reactor管理的所有机房中的某个快递柜
memset(ev->buffer, 0, BUFFER_LENGTH);
int len = recv(fd, ev->buffer, BUFFER_LENGTH, 0); //将fd发来的数据放入快递柜的空间中
if(len > 0){ //如果发来了数据
ev->length = len; //记录接收到了多少数据
ev->buffer[len] = '\0'; //表示这是一段数据
printf("C[%d]: machine: %d\n", fd, ev->status_mechaine);
//解析(处理)顾客发来的包裹(数据)
websocket_request(ev);
// 等待顾客fd有空时将回执发送给顾客
nty_event_del(reactor->epfd, ev); //在回执发送之前不在接收顾客发来的数据
nty_event_set(ev, fd, send_cb, reactor);
nty_event_add(reactor->epfd, EPOLLOUT, ev);
}else if(len == 0){ //本次数据接收完了
// nty_event_del(reactor->epfd, ev);
// close(ev->fd);
}else{
nty_event_del(reactor->epfd, ev);
close(ev->fd);
printf("recv[fd=%d] error[%d]:%s\n", fd, errno, strerror(errno));
}
return 0;
}
int accept_cb(int fd, int events, void *arg){
struct ntyreactor *reactor = (struct ntyreactor *)arg;
if(reactor == NULL){
return -1;
}
struct sockaddr_in client_addr;
socklen_t len = sizeof(client_addr);
int clientfd ;
if ((clientfd = accept(fd, (struct sockaddr*)&client_addr, &len)) == -1) {
if (errno != EAGAIN && errno != EINTR) {
}
printf("accept: %s\n", strerror(errno));
return -1;
}
//为什么epoll/select/poll要非阻塞:因为即使触发了读事件,也有可能没有数据可读(因为如果校验码出错,就会清空读缓存区中接收到的数据):
if ((fcntl(clientfd, F_SETFL, O_NONBLOCK)) < 0) {
printf("%s: fcntl nonblocking failed, %d\n", __func__, MAX_EPOLL_EVENTS);
return -1;
}
// //将clientfd加入监听
struct ntyevent *event = ntyreactor_idx(reactor, clientfd); //给clientfd分配一个对应的快递柜
/* 等待握手:
* 当有数据到来之后,也就是调用了recv,那么就读取数据,并拆包head,
* 如果发现head中有Sec-WebSocket-Key字段,那么就说明这是websocket,于是就返回给对端`HTTP/1.1 101 Switching Protocols\r\n
* Upgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept:xxxxxxxx`,表示同意升级协议
* */
event->status_mechaine = WS_HANDSHARK; //等待握手
// 告诉数据中心:监听clientfd的发送数据事件。当有数据发生时,派发给快递公司reactor处理
nty_event_set(event, clientfd, recv_cb, reactor);
nty_event_add(reactor->epfd, EPOLLIN, event);
printf("new connect [%s:%d], pos[%d]\n",
inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port), clientfd);
return 0;
}
int ntyreactor_alllistener(struct ntyreactor *reactor, int sockfd, NCALLBACK *acceptor){
if (reactor == NULL) return -1;
if (reactor->evblk == NULL) return -1;
struct ntyevent *event = ntyreactor_idx(reactor, sockfd);
nty_event_set(event, sockfd, acceptor, reactor);
nty_event_add(reactor->epfd, EPOLLIN, event);
return 0;
}
int ntyreactor_alloc(struct ntyreactor *reactor){
if (reactor == NULL) return -1;
if(reactor->evblk == NULL) return -1;
struct eventblock *blk = reactor->evblk;
while (blk->next != NULL){
blk = blk->next;
}
struct ntyevent *evs = (struct ntyevent*)malloc((MAX_EPOLL_EVENTS) * sizeof(struct ntyevent));
if(evs == NULL){
printf("ntyreactor_alloc ntyevents failed\n");
return -2;
}
memset(evs, 0, (MAX_EPOLL_EVENTS) * sizeof(struct ntyevent*));
struct eventblock *block = (struct eventblock *)malloc(sizeof(struct eventblock));
if (block == NULL) {
free(evs);
printf("ntyreactor_alloc eventblock failed\n");
return -2;
}
memset(block, 0, sizeof(struct eventblock));
block->events = evs;
block->next = NULL;
blk->next = block;
reactor->blkcnt++;
return 0;
}
struct ntyevent *ntyreactor_idx(struct ntyreactor *reactor, int sockfd){
int blkidx = sockfd / MAX_EPOLL_EVENTS;
while (blkidx >= reactor->blkcnt){
ntyreactor_alloc(reactor);
}
int i = 0;
struct eventblock *blk = reactor->evblk;
while (i++ < blkidx && blk != NULL){
blk = blk->next;
}
return &blk->events[sockfd % MAX_EPOLL_EVENTS];
}
int ntyreactor_init(struct ntyreactor *reactor){
if(reactor == NULL) return -1;
memset(reactor, 0, sizeof(struct ntyreactor));
reactor->epfd = epoll_create(1);
if (reactor->epfd <= 0) {
printf("create epfd in %s err %s\n", __func__, strerror(errno));
return -2;
}
struct ntyevent *evs = (struct ntyevent*)malloc((MAX_EPOLL_EVENTS) * sizeof(struct ntyevent));
if(evs == NULL){
close(reactor->epfd);
printf("ntyreactor_alloc ntyevents failed\n");
return -2;
}
memset(evs, 0, (MAX_EPOLL_EVENTS) * sizeof(struct ntyevent*));
struct eventblock *block = (struct eventblock *)malloc(sizeof(struct eventblock));
if (block == NULL) {
close(reactor->epfd);
free(evs);
printf("ntyreactor_alloc eventblock failed\n");
return -2;
}
memset(block, 0, sizeof(struct eventblock));
block->events = evs;
block->next = NULL;
reactor->evblk = block;
reactor->blkcnt = 1;
return 0;
}
int ntyreactor_run(struct ntyreactor *reactor){
if(reactor == NULL) return -1;
if(reactor->epfd < 0) return -1;
if(reactor->evblk == NULL) return -1;
struct epoll_event events[MAX_EPOLL_EVENTS + 1];
while (1){
int nready = epoll_wait(reactor->epfd, events, MAX_EPOLL_EVENTS, 1000);
if (nready < 0) {
printf("epoll_wait error, exit\n");
continue;
}
for (int i = 0; i < nready; ++i) {
struct ntyevent *ev = (struct ntyevent *)events[i].data.ptr;
if((events[i].events & EPOLLIN) && (ev->events && EPOLLIN) ){
ev->callback(ev->fd, events[i].events, ev->arg);
}
if ((events[i].events & EPOLLOUT) && (ev->events & EPOLLOUT)) {
ev->callback(ev->fd, events[i].events, ev->arg);
}
}
}
return 0;
}
int ntyreactor_destory(struct ntyreactor *reactor){
close(reactor->epfd);
struct eventblock * blk = reactor->evblk;
struct eventblock * blk_next = NULL;
while (blk != NULL){
blk_next = blk->next;
free(blk->events);
free(blk);
blk = blk_next;
}
return 0;
}
int init_sock(short port) {
int fd = socket(AF_INET, SOCK_STREAM, 0);
fcntl(fd, F_SETFL, O_NONBLOCK);
struct sockaddr_in server_addr;
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
server_addr.sin_port = htons(port);
bind(fd, (struct sockaddr*)&server_addr, sizeof(server_addr));
if (listen(fd, 20) < 0) {
printf("listen failed : %s\n", strerror(errno));
}
return fd;
}
int main(int argc, char *argv[]) {
unsigned short port = SERVER_PORT; // listen 8888
if (argc == 2) {
port = atoi(argv[1]);
}
struct ntyreactor *reactor = (struct ntyreactor*)malloc(sizeof(struct ntyreactor));
ntyreactor_init(reactor);
int sockfds[PORT_COUNT] = {0};
for (int i = 0; i < PORT_COUNT; ++i) {
sockfds[i] = init_sock(port + i);
ntyreactor_alllistener(reactor, sockfds[i], accept_cb);
}
ntyreactor_run(reactor);
for (int i = 0; i < PORT_COUNT; ++i) {
close(sockfds[i]);
}
ntyreactor_destory(reactor);
free(reactor);
return 0;
}
websocket.html:
<html>
<head>
<script>
let ws;
function doConnect(addr) {
ws = new WebSocket("ws://" + addr);
ws.onopen = () => {
document.getElementById("log").value += (" Connection opened\n");
};
ws.onmessage = (event) => {
document.getElementById("log").value += (" Receive: " + event.data + "\n\n"); // JSON.stringify()
};
ws.onclose = () => {
document.getElementById("log").value += (" Connection closed\n");
};
}
document.addEventListener("DOMContentLoaded", (event) => {
document.getElementById("btn_connect").onclick = () => {
let server_addr = document.getElementById("server_addr").value;
doConnect(server_addr);
};
document.getElementById("btn_send").onclick = () => {
let msg = document.getElementById("message").value;
ws.send(msg);
document.getElementById("log").value += (" Send: " + msg + "\n");
};
});
</script>
</head>
<body>
<div id="header">
<h1 align="left">WebSocket Client</h1>
Server: <input id="server_addr" type="text" value="192.168.1.100:8888">
<input id="btn_connect" type="button" value="Connect!"><br/><br/>
Message: <input id="message" type="text" value="">
<input id="btn_send" type="button" value="Send"><br/><br/>
<textarea cols="250" id="log" rows="50"></textarea>
</div>
</body>
</html>
对于websocket的三次握手:
(1)当有连接到来之后(也就是完成了三次握手之后),马上就进行websocket握手
-
等待clientfd有数据发来
-
当有数据到来之后,也就是调用了recv,那么就读取数据,并拆包head,如果发现head中有Sec-WebSocket-Key字段,那么就说明这是websocket,于是就返回给对端
HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept:xxxxxxxx
,表示同意升级协议
(2)为什么网络要用非阻塞:因为即使触发了读事件,也有可能没有数据可读(因为如果校验码出错,就会清空读缓存区中接收到的数据):
Proactor 模型
前面提到的 Reactor 是非阻塞同步网络模式,而 Proactor 是异步网络模式。
Proactor模式将所有的IO操作都交给主线程和内核处理,包括监听和数据读写。工作线程仅负责业务逻辑,不关系IO操作何时进行和如何进行。
下图是Proactor模式的工作流程(以异步IO函数aio_read和aio_write函数为例):
具体流程如下:
- 处理器发起异步操作,并关注IO完成事件
- 事件分离器等待操作完成事件
- 分离器等待过程中,内核并行执行实际的IO操作,并将结果存入用户自定义缓存区,最后通知事件分离器读操作完成
- I/O 完成后,通过事件分离器呼唤处理器
- 事件处理器处理用户自定义缓冲区中的数据
举个具体的例子,使用异步IO模式(以aio_read、aio_write为例)实现的Reactor模式的工作流程是:
- 主线程调用
aio_read
函数向内核注册socket上的读完成事件,并告诉内核用户读缓冲区的位置,以及读操作完成时如何通知应用程序 - 主线程继续处理其他逻辑
- 当socket上的数据被读入用户缓冲区之后,内核将向应用程序发送一个信号,以通知应用程序数据已经可用
- 应用程序按照预先定义好的信号处理函数选择一个工作线程来处理客户请求。工作线程处理完客户请求之后,调用
aio_write
函数向内核注册socket上的写完成事件,并告诉内核用户写缓冲区的位置,以及写操作完成时如何通知应用程序 - 主线程继续处理其他逻辑
- 当用户缓冲区的数据被写入socket之后,内核向应用程序发送一个信号,以通知应用程序数据已经发哦是那个完毕
- 应用程序预先定义好的信号处理函数选择一个工作线程来做善后处理,比如决定是否关闭socket
连接socket上的读写事件是通过aio_read/aio_write
向内核注册的,因此内核将通过信号来向应用程序报告连接socket上的读写事件。所以,主线程中的epoll_wait
调用仅能用来监听socket上的连接请求事件,而不能用来检测连接socket上的读写事件
从上面的处理流程中,我们可以发现proactor模型最大的特点就是proactor使用异步IO。所有的IO操作都交给系统提供的异步IO接口去执行。工作线程仅仅负责业务逻辑。
- 在Proactor中,用户函数启动一个异步的文件操作。同时将这个操作注册到多路复用器上,多路复用器并不关系文件是否可读或者可写而是关系这个异步读操作是否完成。
- 异步操作是操作系统完成,用户程序不需要关闭。多路复用器等待直到有完成通知到来。
- 当操作系统完成了读文件操作—将读取到的数据复制到了用户先前提供的缓冲区之后,通知多路复用器相关操作已经完成。
- 多路复用器再调用相应的处理程序,处理数据。
Proactor增加了编程的复杂度,但给工作线程带来了更高的效率。
- Proactor可以周期系统态将读写优化,利用IO并行能力,提供一个高性能单线程模型。
- 在 windows 上,由于没有 epoll 这样的机制,因此提供了 IOCP 来支持高并发, 由于操作系统做了较好的优化,windows 较常采用 Proactor 的模型利用完成端口来实现服务器。
- 在 linux 上,在2.6 内核出现了 aio 接口,但aio实际效率并不理想,它的出现,主要是解决poll性能不佳的问题,但实际上经过测试,epoll的性能高于poll+aio,并且aio不能处理accept,因此linux主要还是以Reactor模型为主。
Reactor 模拟 Proactor 模型
在不使用操作系统提供的异步IO接口的情况下,还可以使用reactor来模拟proactor,差别是:使用异步接口可以利用系统提供的读写并行能力,而在模拟的情况下,这需要在用户态实现。具体做法:
- 注册读事件(同时再提供一段缓冲区)
- 事件分离器等待可读事件
- 事件到来,激活分离器,分离器(立即读数据,写缓冲区)调用事件处理器
- 事件处理器处理数据,删除事件(需要再用异步接口注册)
我们知道,Boost.asio 库采用的即为 Proactor 模型。不过 Boost.asio 库在 Linux 平台采用epoll 实现的 Reactor 来模拟 Proactor,并且另外开了一个线程来完成读写调度。
同步 I/O 模拟 Proactor 模型
- 主线程往epoll内核事件表中注册socket上的读就绪事件
- 主线程调用epoll_wait等待socket上有数据可读
- 当socket上有事件可读时,epoll_wait通知主线程。主线程从socket循环读取书籍,直到没有更多的数据可读,然后将读取到的数据封装成一个请求对象并插入请求队列
- 睡眠在请求队列上的某个工作线程被唤醒,它获得请求对象并处理客户请求,然后往 epoll 内核事件表中注册 socket 上的写就绪事件。
- 主线程调用 epoll_wait 等待 socket 可写。
- 当 socket 可写时,epoll_wait 通知主线程。主线程往 socket 上写入服务器处理客户请求的结果。
两个模式的相同点,都是对某个 IO 事件的事件通知(即告诉某个模块,这个 IO 操作可以进行或已经完成)。在结构上两者也有相同点:demultiplexor 负责提交 IO 操作(异步)、查询设备是否可操作(同步),然后当条件满足时,就回调注册处理函数。
不同点在于,异步情况下(Proactor),当回调注册的处理函数时,表示 IO 操作已经完成;同步情况下(Reactor),回调注册的处理函数时,表示 IO 设备可以进行某个操作(canread or can write),注册的处理函数这个时候开始提交操作。
面试
说下你知道的网络模型
- 进程模型:一个连接一个线程。这种模型的优点是编程简单、缺点是并发度低
- 线程模型:连接由线程池管理,一般采用IO事件触发的方式,只有连接可读时才会将一个连接与一个线程绑定,线程处理完之后立即归还给线程池。这种模型最大的优点是可以用少量的线程就可以处理大量的客户端连接,而且编程相对比较简单(相对于非阻塞模型)
- 非阻塞模型:每个子进程是由一个单独的非阻塞线程组成,该线程采用完全非阻塞 IO 方式处理外来的大量客户端连接(类似于 nginx/squid/ircd),该模型的优点是处理效率高占用资源少,可以处理大量客户端连接,缺点是编程比较复杂
- 协程模型:虽然非阻塞服务模型可以获得大并发处理能力,但是编程复杂度较高,协程模型综合了大并发的处理能力和较低的编程复杂度的特点,使得编程人员可以按照顺序IO的编程方式来简单实现业务模型
Reactor和Proactor的区别
- reactor是非阻塞同步ID,它是被动的。
- 主线程只负责监听文件描述符上是否有事件发生。如果有的话就立即将事件通知工作线程。
- 工作线程负责读写事件、接收新得连接,以及处理客户请求
- proactor是完全异步的,它会将所有的IO操作都交给主线程和内核处理,工作线程仅仅负责业务逻辑,不关心IO操作是如何进行和何时进行,等有IO操作时内核会自动调用注册的函数