http协议基础及IO模型
Nginx是一个Web服务器程序提供的开源解决方案,它既是一个Web Server同时又是一个著名的web proxy称为web的代理。但是作为代理来讲,它更多的应用场景是在反向代理上面,所以我们有时候将其称为web reverse proxy。
在讲Nginx之前,上面已经强调了两个术语,一个是web server一个是web reverse proxy。所以简要回顾之前讲过的web服务。
如果不考虑我们说过的lamp的话,只说一个web服务器,那么任何时候一个web服务器无非就是能够提供http应用层协议,能够基于所谓的请求响应报文完成Web事务的这么一个服务应用程序。像我们此前讲到的Apache就是一个非常著名的代表。
对于Web服务来讲,它的最核心的就是http协议。
Web服务相关概念回顾
http协议
默认情况下工作在tcp的80端口,其全称为HyperText Transfer Protocol(超文本传输协议)。他主要就是传输超文本的,超文本就是由HTML语言所开发的文本或者拥有HTML标记的文本,那么HTML又指的是什么呢?
HTTP:HyperText Mark Language 超文本标记语言,这也是一种开发语言。
各位一定要注意的是,html所能够传输的内容是文本内容。早期http/0.9也只能传输文本,但是从http/1.0后,他引入了MIME机制,从而能够实现对多媒体内容的支持的。
MIME:Multipurpose Internet Mail Extension 多功能互联网邮件扩展。
MIME其实是早期smtp引入进来的一种用于实现基于文本格式的smtp协议传输非文本信息的一种将非文本信息编码成文本,而且在接收方接收完成之后又能够还原为原有媒体格式的这么一种编码方案。在http/1.0后,它被引入了http,所以从而使得http协议也能够支持多媒体内容的传输了。那么MIME在实现其内容类型标记时,有两种符号,它有主类型(major)和次类型(minor)之分,比如像文本中的纯文本信息text/plain,再比如说像jpeg格式的图片下载image/jpeg等等类似于这种格式,所以使得我们浏览器接收到相关的对应内容以后,知道使用何种应用程序来解析此类内容以及如何进行还原的。
这就是MIME,也正是MIME机制的引入才使得它能够传输非文本信息的。
大家又应该能够知道,互联网上的WEB服务器成千上万,数以万亿计。那么众多的服务器上的每一个服务器都有可能有着许多资源,那么我们如何去访问每一个资源,如何去定位每一个资源,这靠的是URL的标记。
URL的基本语法:
schema(协议)://server[:port]/path/to/source
通过URL来标记众多资源,因此互联网上的每一个资源都应该至少有一个标识,有些资源标识的路径可能不止一个,是因为我们服务器的名字可能不止一个。但是每一个资源都一个能够被某一个URL所唯一的进行标识。
但是当我们的用户代理(User Agent)跟我们的服务器端进行交互时,一般而言用户代理是一个浏览器,它跟我们的服务器之间需要通过相应的报文格式来完成其对应的资源交互的。
所以我们说,这一次请求响应过程叫一个http事务,我们有请求报文叫做request,响应报文叫做response组成。对一个事务而言,他无非就是请求、响应、请求、响应,不断的来通过这种方式进行。请求一般而言是由用户代理发起的,响应则由服务器端进行响应,那么为了能够让二者的请求响应能够通信,他们要借助于底层的TCP/IP通信子网完成通信,但http自己的工作流程则在应用层协议http协议的响应报文中得以维持和包含。
各位是否记得请求报文的报文格式(请求报文的http首部的格式)是什么?
request:
<method> <URL> <version>
<HEADERS>
<body>
响应报文(response)格式:
<version> <status> <reason phrase>
<HEADERS>
<body>
需要注意的是,我们的应用层协议格式通常有两种:
- 文本格式
文本格式一般来讲要麻烦一点,但容易开发,它的交互过程和解析要较为麻烦。使用文本格式的协议像smtp协议,http协议等都是较为著名的。
- 二进制格式
二进制格式解析简单但是理解起来却没那么直观。使用二进制格式的协议像后续的很多其它协议像memacached等它们彼此之间互相通信的协议有可能用的是二进制格式。
协议格式取决于那个应用层协议的设计者是如何去归类,如何去理解协议的。
在http事务中我们提到了这几个基本概念,URL已经解释过了,那METHOD是什么?HEADERS有哪些?STATUS常见的有哪些呢?我们再做一次梳理和回顾。
method:(几个最为常见也是最基本的method,这是常识!!!)
- GET:从服务器上去请求一个资源;
- HEAD:请求资源,服务器端无需响应资源实体部分,只需要告诉这个资源是否存在,给一个响应首部信息即可;所以叫做只返回响应首部而不用返回body部分
- POST:User Agent提交表单的;
- PUT:上传资源;
- DELETE:删除资源;
- TRACE:跟踪资源中间所经过的代理服务器;
- OPTIONS:看看这个资源支持哪些请求方法;
status,响应的状态码:
- 1xx:信息类;
- 2xx:成功类;200 --> OK
- 3xx:重定向类;301、302、304(当客户端发出条件式请求时,服务器端发现资源并未发生改变,此时可以响应一个304)
- 4xx:客户端错误类;403、401、404
- 5xx:服务器端错误类;500、502、504
每一种状态码都应该有一个与之对应的reason phrase(原因短语),用于描述他为什么会发生这种相关信息。
HEADER:
- 通用首部:请求响应报文都可以使用的;
- 请求首部:只用在请求报文中;
特定的两个条件式请求:If-Modified-Since、If-None-Match;
将来在讲到反向代理的缓存功能时,基于反向代理实现Web服务请求的缓存功能时,这项是至关重要的。在讲到对应的WEB系统构建的时候,各位要了解,缓存在现今的互联网时代是至关重要的一个组件,有一句话叫做Cache is king(缓存为王的时代);现在互联网在实现加速很多组件彼此之间的结合度或者其速率是不匹配的,甚至是相去甚远的,那在这种场景当中,我们如何去实现让整个系统的响应性能更好,一般而言都是在速度不匹配的组件之间添加缓存来实现。而If-Modified-Since、If-None-Match在Web Cache方面用处还是比较大的。
- 响应首部:只用在响应报文中;
- 实体首部:用在body部分,用来描述body中编码格式、编码语言等等;
- 扩展首部:这些扩展首部未必是标准的;
我们又说过,对于任何一个Web服务器的Web页面而言,用户能够直接请求的通常都是某一个页面而不会是某一个图片。在Web上每一个资源都有自己的独有的URL,无论是图片、声音、甚至影像这些资源都有。但是任何时候我们都很少直接打开一个单个的图片或者一个影像资源而是通过某个Web页面去载入后在某一页面的某个位置来进行显示,所以我们说过很多次,一个页面当中通常由多个资源组成,很少像我们自己写的测试页一样。所以大多数的WEB页面都由多个资源组成。
所以浏览器加载一个页面的之后页面中很可能有众多资源组成,那这些资源有可能是来自于同一个服务器也有可能来自于不同服务器,但无论来自于哪些服务器,这个浏览器在经过分析后必须把引用到的每一个资源都加载到本机,说白了就是把每一个都要重新单独请求一次,而后才能够予以完整展示的,所以,此时浏览器为了尽可能快速的去加载这些内容,它其实引入了两种机制,一种机制是浏览器自己有缓存称为private cache,是浏览器自己的私有缓存,也就意味着说如果此前曾经访问过这些页面,里边有些内容是静态生成的话,即便是动态生成它也允许缓存的话,这个时候会把这些内容缓存在浏览器所在的这个主机的浏览器自己的私有缓存空间当中,从而使得第二次再请求同样的内容时,它就可以发出条件式请求或者是如果发现资源未过期就直接使用本地的内容了。因为像Google或百度这样的网站,它们有可能会把某一个资源的缓存时长调整为1年,即1年之内都是有效的。
这是第一点,我们通过缓存来加速打开资源的过程,不会像真正服务器发请求而是用到本地缓存。第二种是由于这里的资源引用可能过多,因此浏览器则有可能会多线程并行发请求,比如:我们浏览器有的是双线程的,有的是四线程,甚至有的是8线程的。如果我们现在的主机大多数都是多核心的话,那么启动多个线程同时去加载资源也是可以的,只要带宽足够可用,也是一种的的确确能够提升其加载速度的一种方案。所以这个浏览器分析以后发现有众多资源,则有可能同时,第一次发起请求时它可能只请求一个资源,但第二次再次请求里边所引用到的资源时则有可能根据浏览器自己工作的线程模式同时发起两个请求,那这个时候最多请求到两个资源,这两个请求再发起两个请求后面两个资源,依次类推,指到所有资源都加载完后整个页面才能打开。
作为服务器来讲,它有可能同时接收到n个请求进来,但是n个请求并不是同时对应n个用户,因为任何一个用户的主机它有可能打开一个浏览器进程,而这个浏览器进程可能会并行发起多个线程同时进行请求。所以每一个连接未必对应一个用户,因为它的双线程每一个线程都会发起一个单独的请求,这点需要注意。
对于服务器端来讲,他有可能承载的连接数量跟请求的用户个数本身并没有一一对应关系。而且对于一个非常繁忙的站点来讲,同时发起请求的用户数量很多,虽然第一次请求时,只是发一个请求但随后可能会有大量请求随之都涌过来了,这时候服务器可能会承受很大的并发压力,所以后来一直讲,对于有些站点来讲,它们承载的用户数量可能是非常大的,这就要求一个服务器所能够承载的请求量是非常大的。
事实上现在的互联网站点为了应付这样的请求,它们有各种各样的加载优化方式。不过不管怎么优化,单台服务器所能够面临的并发用户请求总量总然是有限的,因此在后期会提到用户量非常大时,并发可以达到几万个级别的时候,甚至几十万级别时,如何扩展这个架构,让更多的用户接进来提供服务等等。
那在统计PV是统计的是什么?是不是每一个资源请求都是一个PV?显然不是,对于这整个主页的浏览,它可能加载n个资源但是这只能算一次的页面浏览量,因此我们的站点上有多少个页面入口,我们在统计PV的时候只能统计多少个PV。说白了,在同一个时间内来自于同一个用户的刷新不能被统计为两个PV,因为他仍然是同一个。
pv:page view
其统计方式应该是这整个页面中虽然由众多页面组成每一个资源都要单独请求通过日志分期请求的pv时,只能把这个入口的请求当做是PV,而后续的其实是对这个入口所引用的每一个资源再次请求以达到完全展示的目的。而我们的整个网站有多少个可以作为入口单独请求的页面我们自己应该是心中有数的,因此我们在实现日志分析来实现PV统计的时候,它也必然是有一套方案在里头,但是现在有各种各样的站长统计工具能够帮助我们去统计的,不过对于大型站点来讲必然是要做自行分析的。用日志分析来分析、判断用户行为甚至是做有的放矢的广告推广这都是一个基本的能力,在很多像电商站点或者是社交站点,它们都在做用户行为分析、做数据挖掘、甚至于说用户行为量很大的话,可能做一年的用户行为分析可能还要构建大数据分析平台。如果要做实时分析的话,还要做流式数据实时分析平台。这其中包含了许许多多复杂的组件,这也是一个非常复杂的生态。
除了pv这样的术语之外,偶尔还可能用到uv这样的概念。
uv:user view
我们在统计的时候并不是真正按照用户名来进行统计的,因为有些站点本来访问的是匿名的,它不可能能够根据用户名进行统计,那怎么去统计uv呢?那就需要通过独立IP来统计了。如日pv与日uv就是一天内我们的页面浏览量有多少个,一天内有多少个独立的IP地址对我们的网站发起了请求和访问。那也有月pv、月uv等等几个术语。
刚才我们提到过,我们为了能够帮助客户端打开页面时尽可能能够快速的打开页面,能够有较好的用户体验,通常有两种常见的方案:引入缓存、并行请求。现在的完成的成熟的浏览器大多数都是支持多线程的,但这种多线程需要提醒的是任何一个浏览器对于单个域名来讲,它的多线程是有上限的,就像刚才所讲,浏览器可以支持两线程、可以支持四线程,这种两线程和四线程是相对于单个域名而言的。也就是说我们打开一个浏览器后,现在浏览器能够支持标签式浏览,我们访问a网站它会打开两个线程去加载a网站的资源,我们又同时打开一个标签页面访问b网站,那么对于b网站来讲,他也会同时打开两个线程去加载b网站上的内容。那于是有些网站为了优化用户体验,有可能在同一个网站上它们会使用不同的域名,比如说打开主站时使用的是www.a.com,而主站内部对于这些图片的引用则有可能在另外一个域名下的,比如图片就有可能有一个www.image.com,如果是视频的话就在www.video.com这样的域名下,这样就使得如果在同一个网站的页面上我们自己作为网站拥有方注册了n个域名,分别将图片放在了一个域名下,把视频放在了域名下,把文本放在了一个域名下。这样客户端浏览器在加载时对于单个域名都可以启动两个线程,那因此一个页面中如果有80个资源的话而我们又使用了四个域名这就意味着它同时可以启动8线程同时加载,这也是网站打开速度优化的一种方案或思路。
因此浏览器自身的限制是针对于单个域名做限制的,它最终能打开几个线程。而对于多个域名来讲,每一个域名都可以同时打开多个线程同时访问的。所以这就是为什么很多站点上它们不同的资源却发现完全是属于不同域名的原因。并不是说不属于不同域名它就盗用了别人的,而是说它有可能是同一个网站上为了实现用户加载时加速策略而有意为之的。
这是我们在提到多个WEB页面资源加载时所讲到的概念,我们在这里提到了页面的访问入口以及资源引用的概念以及浏览器在引用时的多线程、浏览器应用缓存来及进行加速等相关概念。
Web服务器认证:
WEB服务器在使用时还可以做认证的,认证有两种方式:
- 基于IP认证;
- 基于用户认证;
基于用户认证又分为两种:
- basic
- digest
而用户访问一个需要认证的资源时服务器端可能会返回一个特殊的状态码以实现认证质询,要求客户端自行必须要打开浏览器弹出一个对话框输入账号密码以后再次向服务器端确认后才能获得资源。
在Web服务器中还有所谓的资源映射的概念,什么叫资源映射呢?比如用户通过浏览器输入URL所访问的每一个资源有可能基于DocmentRoot指定了本地文件系统下某个位置的路径。这就是一种映射,还有像Alias即路径别名,这也是资源映射的方案。
httpd的MPM:
再回顾一下httpd,它有自己的MPM(多道处理模块),MPM在Linux主机上有三种类型:
- prefork
prefork的工作方式:有一个主进程,主进程生成多个子进程,而后每个子进程处理一个请求;主进程是以管理员的身份启动的,所以它能够监听在80端口上。此前说过,端口小于1024的称为特权端口只有管理员才有权限使用的。这就是为什么它能够监听在特权端口上,而后又能够以普通用户运行的原因。
- worker
有一个主进程,主进程生成多个子进程,而后每一个子进程再生成多个线程,每个线程响应一个请求;
- event
有一个主进程,生成多个子进程,可以理解为每一个子进程响应多个请求,也可以理解为生成多个线程。在Linux系统上,子进程与线程并没有严格意义上的区分。而event模型当中最典型的特性就是被称为的事件驱动机制。
那什么叫事件驱动机制呢?接下来就说一下I/O模型;
二、I/O模型
I/O类型:
I/O的类型从不同的角度来划分,它有两种不同的方式:
- 同步IO和异步IO:synchronous,asynchronous
那什么是同步什么是异步?首先同步其实更关注的是消息通知机制,说白了就是如何通知调用者的,可以这么理解,IO无非就是一方能够提供服务一方需要调用别人的服务,所以IO请求就是调用方向被调用方运行一个库调用或函数调用或系统调用。假如说是一个系统调用,所以调用方向被调用方发起系统调用请求,被调用方本地要把这个内容给它运行完成,所以在本地要做处理,处理结束了,把处理的结果响应给调用方。问题是调用方什么时候知道自己的请求结束了呢?自己的请求对方响应了呢?所以就有同步和异步两种模式,所谓同步指的是调用发出之后不会立即返回,但一旦返回,则返回的即是最终结果。那异步指的就是调用发出之后,被调用方立即返回消息,但返回的并非最终结果;被调用者最后通过状态、通知机制等来通知调用者,或通过回调函数来处理结果。
调用者一旦发出调用以后被调用者不会立即给予响应,有可能对方已经收到调用请求了,那于是去处理,处理到最后,结果才返回过去。这种称为同步调用,如下图:
异步调用就是当调用者发出请求后,被调用者立即就告诉调用者请求已收到,需要等待一段时间。这就是立即返回结果但不是最终结果,当对方该处理这个请求时,处理完成后才再次通知调用者。如下图:
- 阻塞IO和非阻塞IO:block,nonboloc
阻塞和非阻塞其实在同步和异步上比较难以区分开来,因为它们在所实现的意义描述上用汉语进行描述可能并不是特别能体会到它们之间的区别。阻塞和非阻塞关注的是调用者等待被调用者返回调用结果时(即是这个中间过程)的状态;
阻塞指的是调用结果返回之前,调用者会被挂起(所谓挂起就是有可能转为不可中断睡眠状态);调用者只有在得到返回结果之后才能继续。
注意:异步和同步关注的是消息是如何通知的,关注的是消息通知机制,说白了就是被调用者如何把调用已经完成的结果通知给调用者。而阻塞则关注的是调用者的状态。同步和异步关注的是被调用者如何把调用结果拿到以后通知给调用者;一个关注的是调用者自己在发出调用以后自己是如何等待结果的。
而对于阻塞而言,调用结果返回之前,它怎么等待?在自己所期望请求的事没结束之前,不再干其它事,所以这就一直处于等待状态。
非阻塞指的是调用者在结果返回之前,不会被挂起,即调用不会被阻塞调用者。
举例:在吃面时,等待面好的过程当中有两种不同的选择,一种是就在面馆等待,其它什么事也不干。另一种是,在等待面好的过程当中,可以再去做其它事,当觉得面快要好的时候再去面馆。
所以这就是所谓的I/O模型,通过两种不同的角度划分它有同步/异步、阻塞/非阻塞这几种类型,而这几种类型当中,同步/异步和阻塞/非阻塞看上去在有些地方很相像,但事实上它们一个关注的是调用者如何等待结果,一个关注的是被调用者如何通知调用者结果已完成的。所以它们压根就不是一回事。
站在这个角度划分的话,I/O其实可以分为五种模型。目前来讲,常用的I/O模型有五种。
常用的五种I/O模型:
- blocking IO(阻塞式IO)
- nonblocking IO(非阻塞式IO)
- IO mutilplexing(复用型IO)
select(),poll()
- signal driven IO(事件驱动式IO)
引入了通知机制,有两种通知方案:
-
- 水平触发;多次通知;
指的是通知一次你没来处理,我再通知一次,没处理再通知一次直到处理为止。多次通知虽然看上去更可靠了,更有保证了,但是一遍一遍通知资源就被浪费了;
-
- 边缘触发;只通知一次;
如果对方没有过来接收,可以将这个通知事件通过回调函数让调用者自行获取或者将通知信息放在某处;
- asynchronous IO(异步IO)
为了能够解释IO,我们通过磁盘IO来进行解释。
注意:下述概念特别关键,对于后续但凡提到的IO时都应该有这么一个直观印象。
我们就以read为例,例如:从磁盘上做一次read操作;
一次read操作大体上由两步组成:
-
- (1)等待数据准备好,从磁盘到内核内存;
- (2)从内核内存复制到进程内存;
此前说过,用户空间的进程是没有权限直接访问硬件的。当某一个应用程序或者一个进程发起IO调用时,这个IO大体上分为两步。它需要向内核请求说要读取某数据,因为它没法直接访问硬盘,因此接下来要由内核完成,这就是一次IO调用,但是这个调用大体由两步组成。当它发起请求之后,内核收到请求之后,内核自己没有数据的。数据在磁盘上,因此内核要怎么处理呢?
第一步,内核要把数据从磁盘加载至内存中。内核能直接访问用户空间的进程,但是一般不建议让它直接访问,所以内核加载这个数据至内核自己的内存空间中,注意叫内核内存。
但是我们说过,加载至内核内存,即便是从磁盘加载完了,这个进程也是不能访问到的,我们不可能让进程直接访问内核内存。
第二步,将这个数据从内核内存复制一份到进程内存中。
所以它由两步组成,即一次IO调用发起请求后,它要等待两个阶段,第一阶段,数据要从磁盘到内核内存,第二步,数据要从内核内存复制一次到进程内存。因为我们的进程有多个所以这个进程内存一定是进程自己所特定的内存空间。两个进程之间一般而言它不能去共享这些数据的。
所以再次说明,一个进程发起IO调用后,一次IO将有两个阶段组成,此处指的是磁盘IO。而这个过程当中真正被称为叫IO的那一步,其实就是第二步,因为底下那个过程只是我们内核自己处理数据的过程。真正被称为IO的步骤是数据从内核内存到进程内存的过程,或者才是真正执行IO过程的阶段。
prefork和worker用到的都是复用型IO,所以它们的并发能力很有限,select()最多只能1024个。而event则用到的是事件驱动IO,事件驱动IO从本质上来讲,它是一个进程直接响应n个请求的。事件驱动型IO在实现IO处理时依然有可能会被阻塞,它只是第一段没有被阻塞,但是第二段有可能被阻塞的,所以性能会比较低。
所以在事件驱动式IO的基础上再一次进行改进,就出现了异步式IO。虽然事件驱动式IO和异步式IO对于并发能力支持较好,但是其编程复杂度也是较高的。
这就是为什么基于后面两种IO模型来提供服务的Web服务器程序其性能较好的原因。Nginx在最初设计时用到的就是事件驱动型IO,而且基于边缘触发来实现。更重要的是,Nginx还支持异步IO,而且他还能基于内存映射(MMAP)机制来完成数据的发放。所以Nginx是尽可能用到了近些年最新的服务器端编程技术来支持较好的并发。
总结
五种IO的比较:
- 同步异步关注的是通知机制,同步为无通知机制即前三种,异步为有通知机制即后两种。
- 一个WEB服务器的一个进程接收请求后一旦被阻塞了就没办法继续处理其它请求了,因此它只能以串行的方式进行。