WebServe 分类
按照我个人对web服务器的理解,我将其分为两类:
1、一类是以nginx、Apache Http Server为代表的通用的Web Server
2、一类是基于语言的web Server,比如C++的Piatache、Java的Tomcat、Python的gunicorn等
区分的依据主要是,是否可以执行计算任务!
WebServe 执行流程
详细的说,一个web服务器的工作是什么呢?其实就是:
1、监听用户的请求
2、收到请求后对HTTP请求进行解析
3、根据解析的内容,返回数据
WebServe 如何返回数据
前面我说的两种服务器的区别就在第三步,也就是返回的数据,一般是一个HTML的页面。如果是是一个静态页面,换句话说,就是返回一个文本文件,那么就很容易了,直接调用send()系统调用就可以了,这是Nginx等通用WebServer可以搞定的!但是如果需要动态生成HTML呢?比如需要先查询数据库,然后执行一些操作,最终再生成数据然后返回呢?这个时候Nginx就做不到了,或者说没办法直接做到了!因为我的处理方式是java或php写的,这个时候nginx显然是没办法直接拿到结果的!
怎么办呢?
1、使用tomcat,因为他本身就是Java写的,上述webserver的1、2步骤都是由Java实现,当需要第三步动态生成数据的时候他只需要启动一个线程来处理一下就行了
2、php的解决方案,nginx会将这个请求发给PHP-FPM,后者会启动php解析器调用php脚本,然后将结果通过标准输入输出的方式发给nginx,nginx再返回解雇,这个就是FastCGI协议!
什么是NIO
关于NIO和BIO我在下面的文章中,详细的讨论过:
我在这里再阐述一下NIO:
我们详细的展开上述的webserver的监听请求的过程:
其实就是创建一个套接字,然后监听,这个套接字我们将其命名为listen-fd,是的,它是一个文件描述符!
监听是需要一个线程的。此时一个用户的一个HTTP请求到达,这个时候我们需要和用户建立一个TCP链接,然后创建一个新的套接字与用户通讯,创建的这个套接字我们称之为handle-fd!注意,一个TCP请求,可以传输多个HTTP请求!也就是长连接!
那么BIO和NIO区别就在如何在处理这个handle-fd:
- 前者直接启动一个新的线程,来接受并处理用户发来的请求,也就是每个handle-fd对应一个线程,但是由于HTTP通常是长连接的,那么在没有HTTP请求的时候,此线程就被阻塞了!而且我们需要为每个用户的TCP连接创建一个线程,这会使得服务器不敢重负!
- 而NIO使用epoll,他会监控handle-fd,当handle-fd可读时(即一个HTTP请求到达),触发一个事件,我们的线程只需要循环处理事件就可以了!这样的话一个线程可以同时处理多个TCP连接,当其中一个发来了HTTP请求,epoll就会产生一个事件,我们的线程会循环的检查有没有事件产生从而处理他!
这就是NIO的优势!
Nginx和Tomcat是如何使用NIO的?
Nginx
对于Nginx,会启动多个worker进程,所有的进程都会监听listen-fd,当TCP连接到达时,理论上所有的worker都会收到请求,也就是群惊,但是通过一定的方法可以保证只有一个人监听到了TCP的到达!
每个worker进程维护了一个epoll,epoll监听了listen-fd,当他知道TCP到达后就会产生对应的事件,然后会唤醒正在等待事件的线程,为了好理解,我们认为一个worker进程中就一个线程!然后处理这个TCP请请求,也就是调用accept系统调用创建一个handle-fd,然后将其挂到epoll中,当handle-fd接收到HTTP请求,同样epoll会产生事件,线程再处理这个事件!
可以看到线程其实是串行的处理每一个HTTP请求!多个worker实现了并行!这样线程就会一直在工作不会因为http请求没有到达而被阻塞,同时有确保了线程数目不会过多!可以认为其比较充分的利用了CPU资源!
Tomcat
Tomcat我没研究过,我用与之类似的Pistache来说明,我的主页有对于Pistache代码的全面解析,有兴趣的可以去看看!
和Nginx的Master-Worker模式略有不同,但是本质上没区别,有个名字叫Reactor 多线程模型!
首先会启动一个名为Reactor 的线程和多个handler线程,这个Reactor线程的任务就是监听listen-fd,当有TCP链接时,选择其中一个handler线程,然后handler线程创建handle-fd,然后监听handle-fd,也是通过epoll机制,本质上没有任何区别。
为什么通常认为Nginx会更快呢?
终于到了问题了!从上面的分析看Niginx,在设计上,并不存在任何优势,而且Nginx仅仅能处理静态的资源!
我认为问题就恰恰出在了这里,那就是如何将静态资源返回给用户。
其实我是不知道Nginx怎么实现的,我可以猜,我觉得是这样的:
直接通过sendfile()系统调用把静态文件的数据发从出去,这过程用C语言是很容易的。
而我们用Java,特别是使用高级的web开发框架,他的过程一般是这样的:
1、read()读文件到内存
2、修改其中的部分内容,对于静态文件则不需要改
3、调用send()发送出去
看上去好像差不多,因为都要先把数据读到内存再发送。
区别在于底层的实现,这需要一定的OS的知识,sendfile的过程,是先把读文件,OS只要你读文件,会首先把文件读pagecache中,也就是页面缓存,然后数据直接从页面缓存复制到TCP的发送缓冲中,整个过程不需要在用户空间执行任何代码!
然而你用read()的话,数据先到pagecache,然后被copy到用户空间的缓冲区,然后再赋值给TCP缓冲区,这一上一下就会浪费很多的时间!
当然你可能会疑问,这个开销和从磁盘加载文件比,岂不是小巫见大巫?的确是的,但是pagecache被加载一次之后,除非内存告急被回收,否则会一直存在,这样这个部分的时间开销两者就都没有了!
其实Nginx采用的其实就是所谓的”零拷贝“方式!甚至,可以绕过TCP缓冲区,直接从pagecache的中读数据!
至此,我必须作一个免责声明,以上都是我之前研究并发的时候对一系列的问题的调研,以及我对相关问题的理解,我没有翻阅过Nginx的源码,也没深入的了解过零拷贝,我只是从理论的角度分析,他的优势!但是对于Pistache、epoll以及pagecache我还是比较有把握的。
为什么Nginx最擅长反向代理
作为问题的结果,我想解释这个问题,其实这和NIO、Epoll是息息相关的!毕竟Nginx的最广泛应用就是反向代理。
所谓反向代理就是,请求发给Nginx,然后再把请求转发出去,等待结果的,然后再将结果返回!
Epoll即是实现NIO的核心,但是我们需要知道,Epoll最擅长的是监听socket和eventfd!也即是最擅长监听网络文件描述符,对于本地的文件,Epoll是没办法的,因此要想实现对文件的异步读写,还是得需要多线程,这也是为什么nodejs的libuv使用了两种方法实现异步,一种是Epoll,一种是多线程!
而实现反向代理,恰好就是访问网络IO,也就是Nginx创建一个client-fd,与要转发的服务器进行通讯,当目标服务器把数据写回的时候,被Nginx的epoll监听到,然后再处理这个事件!
是不是也是在NIO呢?
当然了和php通讯也是类似的,只不过并没有用网络套接字,而是用标准输入输出,这也是一个特殊的fd,只要php-fpm将准备好的数据写道标准输出,然后被Nginx感知到!也是异步IO。