参考:

    http://www.ietf.org/rfc/rfc2616.txt (http协议)

    https://en.wikipedia.org/wiki/Endianness(大端法和小端法)

    http://www.cnblogs.com/gaojing/archive/2012/02/04/2413626.html(url和uri)

    http://www.cnblogs.com/kissdodog/archive/2013/01/11/2856335.html(http报文)

    http://baike.baidu.com/item/通用网关接口  (cgi)

    http://www.showerlee.com/archives/807 (apache的prefork、work、event工作模式)


1.http(hypertext transfer protocol)协议

    http协议最早用来传输超文本(http/0.9版本),什么是超文本呢,其实是用超链接的方法,将各种不同空间的文字信息组织在一起的网状文本。http/1.0引入了MIME机制,使得非文本类信息也可以利用http协议传输。但是http/1.0对长连接、缓存机制、虚拟主机等的支持并不是很好,所以最后http/1.1解决了这些问题,也成为我们现在最常用的http协议。


    超链接:简单来讲就是内容链接,一个页面位置可以跳到其他地方

    html(hyper text marked language):我们知道http可以传输文件数据,但是我们又希望这些数据传输过来后按我们希望的样子展示,于是就给这些文字加上标签,通过这些标签,客户端(一般是浏览器)可以知道该以什么样的方式去展示页面给我们。这些标签与原始数据的结合就是html。

    MIME(Multipurpose Internet Mail Extensions):我们知道,每个人都有自己的不同点,计算的制造也是一样。这就导致了计算机存储数据的时候存在两种不同的方法:大端法和小端法。 简单来说,存储“protocol”单词的时候,一个从小到大存储为 pr、ot、oc、ol,另一个存储为ol、oc、ot、pr(ascii编码,一个字母只需1字节表示),这就导致了程序读取数据流,会因为计算机硬件的不同而导致意外结果。解决这个问题的方法之一是不使用二进制流,而使用文本流,先把二进制流编码为文本流,再进行程序处理。因为文本流具有易读性等优点,且不会因为硬件的不同而发生变化,所以很多程序对数据的处理都采用文本流。当然,它的缺点也很明显,转码增加了额外开销,降低了效率。 同样地,图片和视频等的编码和文本是不一样的,因为http/0.9协议只支持文本传输,所以不能传输图片,而http/1.0 MIME机制引入则改变了这一现状,MIME机制把图片等格式转码为对应的文本流并标识之,使之可以传输到远端,远端根据标识还原为原来的格式并展示,所以我们看到在http协议头部有Content-Type标识。


url和uri:

    uri(uniform resource identifier):统一资源标识符,用来唯一的标识一个资源,它是种抽象概念,只要是能唯一标识资源的符号即可。

    url(uniform resource locator):可以看做互联网上对uri的一种实现,可以唯一某一资源,它的格式一般如下:

        protocol://host[:port]/path/to/file [ "?" query ]

            protocol:协议,比如http、ftp等

            host:主机,可以是ip也可以是fqdn

            port:对应的端口,可省略

            path/to/file:在主机上具体的位置,nginx中把这个当做uri

            ?query:url附带的信息,比如查询某个用户等,可省略


http报文:

http协议的报文只有两种:请求和响应报文。


    请求报文格式如下:

        //请求行

    

                                                 //消息报头

                                                                    //空格行,必备,好像是报文头部和主体区分用,待查明

    [Entity]                                                      //报文主体 put和post方法会用到

    method:表示请求的方法,常见如下:

        get:请求获取request-uri所标识的资源

        post:在资源后面附加新的数据

        head:仅获取响应报文头部

        put:请求服务器存储一个资源,用request-uri作为其标识,和post的区别在于,post是修改文件内容,put是创建文件

        delete:请求删除资源

        trace:测试用,请求服务器回送收到的请求信息

        connect:保留

        options:查询服务器的性能,或者查询与资源相关的选项和需求

    request-uri:同url

    http version:有0.9、 1.0 、1.1三个版本

    header:头部信息,有很多种类头部,缓存相关的,编码相关的等等,这里只是常见几个

        host:主机名,很多人会疑惑,http被tcp/ip协议封装,而tcp/ip已经包含目的ip了,为什么这里还需要一个host头部? 其实这个是为了实现虚拟主机,正是因为有了这个头部,web才可以利用这个头部对不同的请求转发给不同的虚拟主机。

        Cache-Control:缓存控制指令,常见的如下:

            no-cache:这个是很经常被误解的缓存指令,并非不能使用缓存,而是使用缓存前必须跟服务器确认缓存没有过期。

            no-store:这个才是大家理解上的不使用缓存

            max-age:最大缓存时长,(这个可以覆盖max-age头部,有优先级,Pragram>Cache Control>Expires>其他

        Accept:可接受的类型

        Accept-Encoding:可接受的编码

        Referer:这个头部用来记录请求是从哪个网页跳转过来的,经常利用这个字段来防盗链。

例子:

GET /5bd1bjqh_Q23odCf/static/wiseindex/js/superframe_0467623d.js HTTP/1.1

Host: gss0.bdstatic.com

Connection: keep-alive

User-Agent: Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2490.76 Mobile Safari/537.36

Accept: */*

Referer: https://m.baidu.com/?from=844b&vit=fps

Accept-Encoding: gzip, deflate, sdch, br

Accept-Language: zh-CN,zh;q=0.8


响应报文格式如下:

    

    


    

    http-version:和请求报文一样

    status-code:状态码,描述请求资源的状态,分为以下五类:

        1xx:信息类,不常用

        2xx:成功类状态码,比如200 (ok)

        3xx:重定向类状态码

        301:永久重定向,即从这个页面完全转移到另一个页面

        302:临时重定向,即只是临时去跳去某个页面而已,主体还在,对网络蜘蛛有用

        304:no modify,表示没修改过,可以使用本地的缓存

        4xx:客户端类错误

        400:请求有语法错误

        401:unauthorized未授权

        403:forbidden 禁止,服务器收到请求,但拒绝提供服务

        404:not found没有这个资源

        5xx:服务端类错误,比如502(网关错误)等

    reason-phrase:即对状态码的简单说明,比如403的“forbidden”就是这个字段

    header:和客户端的header有些相同,有些不一样,但格式都一样,具体参考rfc2616

    Entity:即报文的数据内容,没啥好说的。


为什么使用缓存?

    每个url称作一个资源或一个对象,我们知道,一个index.html页面里面可能包含很多个对象,而在最开始,每个请求对象都是独立发起一个请求的(这样是为什么我们ss -tn同一时间能看到多个相同ip的请求的原因),我们知道http是应用层协议,在tcp/ip之上,而tcp协议的连接需要“三次握手,四次断开”,这就意味着如果一个页面包含10个对象的话,用这种方法,我们访问一个页面就要30次握手,40次断开。这显然是不理想的。

    于是,http中引入了一种长连接机制,当客户端和服务器建立tcp链接,并传送好对象的时候,并不马上断开,而是等待下一个对象的请求和传送。当然,这个并不是一个一个地发起请求,那样达不到并发效果,因为浏览器一般是多线程的,所以它是两者的结合,利用算法先把请求对象合理分批,同时发起并建立长连接,这样效率大大提高。当然,长连接也有它的缺点,当并发请求数很大的时候,因为进程能打开的文件句柄有限,所以,长连接会导致链接释放过慢,相比普通链接能处理的请求更少。

    页面的请求是如此消耗资源,尤其是图片、压缩包等对象更是大大占用了带宽,而这些对象,都有很少更改的特点,所以,缓存就派上用场了。


    http/1.1中增加了很多头部用以控制缓存。

    比如,我们明确知道我们将在2017年10月23日 10:10修改1.jpg,而在此之前都不会修改,那么我们就可以让服务器返回一个If-Modified-Since头部,在此之前都可以使用缓存(客户端请求对象时会收到一个date头部,通过这个头部和If-Modified-Since对比即可判断是否该使用缓存),其他方式比如max-age等等。


public和private缓存:

    public缓存:简单地说就是允许大家访问的访问,比如无关紧要的图片等。

    private缓存:个人的隐私的缓存,比如个人cookie(可能包含用户登陆信息)等


2.web的请求处理工作模式:

一个web处理请求的过程应该大致包括以下几个步骤:

    1.接收请求

    2.分析请求

    3.读取请求内容数据

    4.封装请求内容数据为响应报文

    5.发送响应报文

    

    这里要注意到的一点是一个纯粹意义上的web只能处理静态内容,而类似于php脚本需要在访问是动态执行的内容,要获取它们的内容,则需要借助额外的机制。web本身并不参与其中。这种机制的实现方法有多种,比如httpd就把php做成自己的子模块,需要执行php相关的内容的时候则调用这个模块。当然它也支持更为流行的cgi。cgi是种接口,它可以把页面相关的动态内容转发给有能力处理它们的程序,并返回处理结果。

    还有一点是这里的封装请求内容数据,并不是指tcp/ip对http报文的封装,而是因为我们访问的内容都是一些数据,我们需要给它们打上html标签,这样客户端才知道我们希望给它展示为什么样子。

    

    那么,当请求到来的时候,web是怎么工作的呢?

  最简单的模型是单进程模型,一个进程顺序处理完一个请求的所有步骤后,再处理下一个请求报文

http协议和web处理请求相关_第1张图片

    这种模型的好处的是程序逻辑十分简单,但是缺点也很明显,当有多个请求链接并发进来的时候,只有一个请求能得到响应,其他请求有可能因为无法及时响应而超时断开,尤其是使用长连接的时候,所以现在应该没有人用这种。

    因此,出现了第二种模型。

  web使用一个主进程接受请求但是自己并不响应,而是创建一个子进程,交给子进程去响应(比如主进程只完成步骤1接受请求,子进程完成剩下步骤),这样,当同时进来多个请求的时候,只要为其都创建个子进程便可同时响应,解决了因等待太久而超时断开的问题。

    但是,这种模型的缺点也很明显,进程是个重量级的数据结构单位,这就意味着,大量的进程会占用大量的系统资源,比如一个进程占用2M内存,那么1000个请求就要2G的内存空间,而且因为单个cpu一次只能运行一个进程,为了达到同时的效果,它进行进程上下文切换,这就意味着,多个进程光是进行上下文切换就要耗费大量的系统开销。

    所以,这种模型只适合在并发不高的场景下工作,并且有它最适合的进程个数,一旦超过这个个数,这种模型的优势会被系统资源消耗的劣势所抵消,甚至一个请求都响应不了。apache的prefork就是类似这种模型,不同的是,它总是预留一些空闲进程出来,这比请求进来的时候临时fork效率高得多。

    于是,有了第三种模型。

    这种模型使用了比进程更小的单位--线程去处理请求,因为只有一个进程,而且线程是轻量级的单位,所以对内存的资源消耗大大降低,又因为可以同时使用多个线程,所以也达到了并发的目的。又因为线程在同一进程内,所以它们可以共享内存空间,当多个请求访问相同的内容的时候,这是一个极大的优点,进程只需读取一次数据,执行一次i/o操作,把数据缓存在内存空间中响应下一次相同请求内容的链接即可。大家知道i/o操作比内存操作慢上几个数量级,所以这个会大大提高系统的吞吐能力,适合在高并发的情况下使用。

    但是,这种模型也有它的缺点。首先,多个线程使用相同的进程内存空间,这就意味着,当有线程对进程内存空间的内容进行修改的时候,它必须对其加锁,用以防止多个线程同时修改带来的数据崩溃。而且,进程需要管理线程,这使得进程变得复杂,此时进程对于线程类似cpu对于进程,进程必须知道哪个请求交给哪些线程,涉及到i/o操作还需要知道,哪些线程已经完成,可以发送响应报文,哪些报文尚未完成,因此,进程对线程的管理也有最优上限,一个进程根据应该场景不同,可以最有效地管理不同的线程数量。根据这个结合前面几个模型优缺点,有了第四种模型:

http协议和web处理请求相关_第2张图片

    这种模型下,主进程事先fork几个空闲进程出来,当请求进来的时候,交给子进程,子进程生成线程进行管理,每个子进程只响应有限个请求,这样,每个进程都能发挥它的最优性能,当并发量的时候,这种模型的好处便体现出来。当然,缺点也显而易见,当访问量很小简单模型足以应付的时候,这种模型由于比较复杂,相对于简单的模型,反而效率更低。


进程对线程的管理

   前面提到,进程必须知道线程的相关状态才能对其进行管理。假设现在有100个线程在进程i/o操作,进程怎么知道哪些处理好,哪些没有呢?

      一种方法是定时对其轮询,每隔一段时间把所有线程轮询一遍。 显而易见,这种方法虽然能满足需求,但是每次轮询势必浪费大量时间。于是有了第二种方法。

     第二种方法是给每个线程设置一个标志位,类似于磁盘文件系统管理中的位图。每次有哪些线程i/o操作好了,就把标志位置1,默认为0.进程逐一轮询,而是扫描这些标志位。

     有人会好奇,这不是也要轮询一遍吗?怎么效率更高了?  其实是因为同样是轮询,但是扫描的单位大小变化了,第一种方法的轮询,就好比一个班级里,你一个个地问,xxx你作业做好了没?  而第二种则是把每个人的名字写在本子上,如果谁写了作业就打个勾,想知道谁好了没就只需要看这个本子即可。

     第二种方法也是http的work模型,然而,这种模型仍然解决不了一种问题: 因为我们轮询是定期执行的,这就会导致一种场景,当前面线程的标志位被扫描过后,前面的线程却刚好在此时完成了i/o操作,但是因为轮询只扫一次,所以它只能等待下一次轮询周期的到来,于是催生了第三种方法。

    第三种方法不使用轮询,而是改用通知机制,(进程允许线程进行中断,这点是我猜的,没验证,当然整篇都是我个人理解,不权威),当线程完成i/o操作后,通知进程数据准备好了,进程收到后及时把数据发出去,这样,大大提高了同时响应请求的能力,这也是apache的event模型

    

    最后,我要声明,这些都是我瞎写的j_0057.gif