守望者--AIR技术交流

 找回密码
 立即注册

QQ登录

只需一步,快速开始

扫一扫,访问微社区

搜索
热搜: ANE FlasCC 炼金术
查看: 743|回复: 6

[协议通信] WEB请求处理系列

[复制链接]
  • TA的每日心情
    擦汗
    7 天前
  • 签到天数: 431 天

    [LV.9]以坛为家II

    1738

    主题

    2089

    帖子

    12万

    积分

    超级版主

    Rank: 18Rank: 18Rank: 18Rank: 18Rank: 18

    威望
    447
    贡献
    16
    金币
    50896
    钢镚
    1418

    开源英雄守望者

    发表于 2016-7-27 14:34:51 | 显示全部楼层 |阅读模式
    http://my.oschina.net/xianggao/blog/667621


    0 系列目录

    最近,终于要把《WEB请求处理系列》提上日程了,一直答应小伙伴们给分享一套完整的WEB请求处理流程:从浏览器、Nginx、Servlet容 器,最终到应用程序WEB请求的一个处理流程,前段时间由于其他工作事情的安排,一直未进行整理。不过还好该系列终于启动了,给大家分享的同时,也顺便整 理下自己的思路,以便温故而知新吧。希望大家都能在此过程中得到新的收获吧。

    本系列主要分五部分:

    1.《WEB请求处理一:浏览器请求发起处理》:分析用户在浏览器中输入URL地址,浏览器如何找到服务器地址的过程,并发起请求;

    2.《WEB请求处理二:Nginx请求反向代理》:分析请求在达反向代理服务器内部处理过程;

    3.《WEB请求处理三:Servlet容器请求处理》:分析请求在Servlet容器内部处理过程,并找到目标应用程序;

    4.《WEB请求处理四:WEB MVC框架请求处理》:分析请求在应用程序内部,开源MVC框架的处理过程;

    5.《WEB请求处理五:浏览器请求响应处理》:分析请求在服务器端处理完成后,浏览器渲染响应页面过程;

    为直观明了,先上一张图,红色部分为本章所述模块:

    WEB请求处理流程

    1 B/S网络架构概述

    我们先了解下B/S网络架构是什么?B/S网络架构从前端到后端都得到了简化,都基于统一的应用层协议HTTP来交互数据,HTTP协议采用无状态的短链接的通信方式,通常情况下,一次请求就完成了一次数据交互,通常也对应一个业务逻辑,然后这次通信连接就断开了。采用这种方式是为了能够同时服务更多的用户,因为当前互联网应用每天都会处理上亿的用户请求,不可能每个用户访问一次后就一直保持住这个连接

    当一个用户在浏览器里输入www.google.com这个URL时,将会发生如下操作:

    1. 首先,浏览器会请求DNS把这个域名解析成对应的IP地址;

    2. 然后,根据这个IP地址在互联网上找到对应的服务器,建立Socket连接,向这个服务器发起一个HTTP Get请求,由这个服务器决定返回默认的数据资源给访问的用户;

    3. 在服务器端实际上还有复杂的业务逻辑:服务器可能有多台,到底指定哪台服务器处理请求,这需要一个负载均衡设备来平均分配所有用户的请求;

    4. 还有请求的数据是存储在分布式缓存里还是一个静态文件中,或是在数据库里;

    5. 当数据返回浏览器时,浏览器解析数据发现还有一些静态资源(如:css,js或者图片)时又会发起另外的HTTP请求,而这些请求可能会在CDN上,那么CDN服务器又会处理这个用户的请求;

    以上具体流程,如图所示:

    输入图片说明

    不管网络架构如何变化,但是始终有一些固定不变的原则需要遵守:

    1. 互联网上所有资源都要用一个URL来表示。URL就是统一资源定位符;

    2. 必须基于HTTP协议与服务端交互;

    3. 数据展示必须在浏览器中进行;

    2 HTTP协议解析

    B/S网络架构的核心是HTTP协议,最重要的就是要熟悉HTTP协议中的HTTP Header,HTTP Header控制着互联网上成千上万的用户的数据传输。最关键的是,它控制着用户浏览器的渲染行为和服务器的执行逻辑

    常见的HTTP请求头:

    输入图片说明

    常见的HTTP响应头:

    输入图片说明

    常见的HTTP状态码:

    输入图片说明

    2.1 浏览器缓存机制

    当我们使用Ctrl+F5组合键刷新一个页面时,首先是在浏览器端,会直接向目标URL发送请求,而不会使用浏览器缓存的数据;其次即使请求发送到服务端,也有可能访问到的是缓存的数据。所以在HTTP的请求头中会增加一些请求头,它告诉服务端我们要获取最新的数据而非缓存。最重要的是在请求Head中增加了两个请求项Pragma:no-cache和Cache-Control:no-cache

    1. Cache-Control/Pragma

      这个HTTP Head字段用于指定所有缓存机制在整个请求/响应链中必须服从的指令,如果知道该页面是否为缓存,不仅可以控制浏览器,还可以控制和HTTP协议相关的缓存或代理服务器

      Cache-Control/Pragma字段的可选值:

      输入图片说明

      Cache-Control请求字段被各个浏览器支持的较好,而且它的优先级也比较高,它和其他一些请求字段(如Expires)同时出现时,Cache-Control会覆盖其他字段

      Pragma字段的作用和Cache-Control有点类似,它也是在HTTP头中包含一个特殊的指令,使相关的服务器来遵守,最常用的就是Pragma:no-cache,它和Cache-Control:no-cache的作用是一样的

    2. Expires 缓存过期时间

      Expires通常的使用格式是Expires:Sat,25 Feb 2012 12:22:17 GMT,后面跟着一个日期和时间,超过这个值后,缓存的内容将失效,也就是浏览器在发出请求之前检查这个页面的这个字段,看该页面是否已经过期了,过期了将重新向服务器发起请求

    3. Last-Modified/Etag 最后修改时间

      Last-Modified字段一般用于表示一个服务器上的字段的最后修改时间,资源可以是静态(静态内容自动加上Last-Modified)或者动态的内容(如Servlet提供了一个getLastModified方法用于检查某个动态内容是否已经更新),通过这个最后修改时间可以判断当前请求的资源是否是最新的

      一般服务器端在响应头中返回一个Last-Modified字段,告诉浏览器这个页面的最后修改时间,如:Sat,25 Feb 2012 12:55:04 GMT,浏览器再次请求时在请求头中增加一个If-Modified-Since:Sat,25 Feb 2012 12:55:04 GMT字段,询问当前缓存的页面是否是最新的,如果是最新的就会返回304状态码,告诉浏览器是最新的,服务器也不会传输新的数据。

      与Last-Modified字段有类似功能的还有一个Etag字段,这个字段的作用是让服务端给每个页面分配一个唯一编号,然后通过这个编号来区分当前这个页面是否是最新的。这种方式比使用Last-Modified更加灵活,但是在后端的Web服务器有多台时比较难处理,因为每个Web服务器都要记住网站的所有资源编号,否则浏览器返回这个编号就没有意义了

    3 WEB工作流程

    对于正常的上网过程,系统其实是这样做的:

    浏览器本身是一个客户端,当你输入URL的时候,首先浏览器会去请求DNS服务器,通过DNS获取相应的域名对应的IP,然后通过IP地址找到IP对应的服务器后,要求建立TCP连接,等浏览器发送完HTTP Request(请求)包后,服务器接收到请求包之后才开始处理请求包,服务器调用自身服务,返回HTTP Response(响应)包;客户端收到来自服务器的响应后开始渲染这个Response包里的主体(body),等收到全部的内容随后断开与该服务器之间的TCP连接。

    输入图片说明

    Web服务器的工作原理可以简单地归纳为:

    1. 浏览器通过DNS域名解析到服务器IP;

    2. 客户机通过TCP/IP协议建立到服务器的TCP连接;

    3. 客户端向服务器发送HTTP协议请求包,请求服务器里的资源文档;

    4. 服务器向客户机发送HTTP协议应答包,如果请求的资源包含有动态语言的内容,那么服务器会调用动态语言的解释引擎负责处理“动态内容”,并将处理得到的数据返回给客户端;

    5. 客户机与服务器断开。由客户端解释HTML文档,在客户端屏幕上渲染图形结果;

    一个简单的HTTP事务就是这样实现的,看起来很复杂,原理其实是挺简单的。需要注意的是客户机与服务器之间的通信是非持久连接的,也就是当服务器发送了应答后就与客户机断开连接,等待下一次请求。

    4 DNS域名解析

    4.1 DNS域名解析过程

    当用户在浏览器中输入域名,如:www.google.com,并按下回车后,DNS解析过程大体如下:

    输入图片说明

    1. 浏览器缓存检查(本机)

      浏览器会首先搜索浏览器自身的DNS缓存(缓存时间比较短,大概只有1分钟,且只能容纳1000条缓存),看自身的缓存中是否有www.google.com对应的条目,而且没有过期,如果有且没有过期则解析到此结束

      浏览器缓存域名也是有限制的,不仅浏览器缓存大小有限制,而且缓存的时间也有限制,通常情况下为几分钟到几小时不等,域名被缓存的时间限制可以通过TTL属性来设置。 这个缓存时间太长和太短都不好,如果缓存时间太长,一旦域名被解析到的IP有变化,会导致被客户端缓存的域名无法解析到变化后的IP地址,以致该域名不能 正常解析,这段时间内有可能会有一部分用户无法访问网站。如果时间设置太短,会导致用户每次访问网站都要重新解析一次域名。

      注:我们怎么查看Chrome自身的缓存?可以使用 chrome://net-internals/#dns 来进行查看

    2. 操作系统缓存检查(本机)+hosts解析(本机)

      如果浏览器自身的缓存里面没有找到对应的条目,其实操作系统也会有一个域名解析的过程,那么Chrome会首先搜索操作系统自身的DNS缓存中是否有这个域名对应的DNS解析结果,如果找到且没有过期则停止搜索解析到此结束

      其次在Linux中可以通过/etc/hosts文件来设置,你可以将任何域名解析到任何能够访问的IP地址。如果你在这里指定了一个域名对应的IP地址,那么浏览器会首先使用这个IP地址。当解析到这个配置文件中的某个域名时,操作系统会在缓存中缓存这个解析结果,缓存的时间同样是受这个域名的失效时间和缓存的空间大小控制的。

    3. 本地区域名服务器解析(LDNS)

      如果在hosts文件中也没有找到对应的条目,浏览器就会发起一个DNS的系统调用,就会向本地配置的首选DNS服务器(LDNS一般是电信运营商提供的,也可以使用像Google提供的DNS服务器)发起域名解析请求(通过的是UDP协议向DNS的53端口发起请求,这个请求是递归的请求,也就是运营商的DNS服务器必须得提供给我们该域名的IP地址)。

      在我们的网络配置中都会有“DNS服务器地址”这一项,这个地址就用于解决前面所说的如果两个过程无法解析时要怎么办,操作系统会把这个域名发送给这里设置的LDNS,也就是本地区的域名服务器这个DNS通常都提供给你本地互联网接入的一个DNS解析服务,例如你是在学校接入互联网,那么你的DNS服务器肯定在你的学校,如果你是在一个小区接入互联网的,那这个DNS就是提供给你接入互联网的应用提供商,即电信或者联通,也就是通常所说的SPA,那么这个DNS通常也会在你所在城市的某个角落,通常不会很远。这个专门的域名解析服务器性能都会很好,它们一般都会缓存域名解析结果,当然缓存时间是受域名的失效时间控制的,一般缓存空间不是影响域名失效的主要因素。大约80%的域名解析都到这里就已经完成了,所以LDNS主要承担了域名的解析工作。

      运营商的DNS服务器首先查找自身的缓存,找到对应的条目,且没有过期,则解析成功。

      输入图片说明

    4. 根域名服务器解析(Root Server)

      如果LDNS没有找到对应的条目,则由运营商的DNS代我们的浏览器发起迭代DNS解析请求。它首先是会找根域的DNS的IP地址(这个DNS服务器都内置13台根域的DNS的IP地址),找到根域的DNS地址,就会向其发起请求(请问www.google.com这个域名的IP地址是多少啊?)。

    5. 根域名服务器返回给本地域名服务器一个所查询域的主域名服务器(gTLD Server)地址,gTLD是国际顶级域名服务器,如.com、.cn、.org等,全球只有13台左右。

      根域发现这是一个顶级域com域的一个域名,于是就告诉运营商的DNS我不知道这个域名的IP地址,但是我知道com域的IP地址,你去找它去。

    6. 本地域名服务器(Local DNS Server)再向上一步返回的gTLD服务器发送请求

      于是运营商的DNS就得到了com域的IP地址,又向com域的IP地址发起了请求(请问www.google.com这个域名的IP地址是多少?),com域这台服务器告诉运营商的DNS我不知道www.google.com这个域名的IP地址,但是我知道google.com这个域的DNS地址,你去找它去。

    7. 接受请求的gTLD服务器查找并返回此域名对应的Name Server域名服务器的地址,这个Name Server通常就是你注册的域名服务器,例如你在某个域名服务提供商申请的域名,那么这个域名解析任务就由这个域名提供商的服务器来完成。

      于是运营商的DNS又向google.com这个域名的DNS地址(这个一般就是由域名注册商提供的,像万网,新网等)发起请求(请问www.google.com这个域名的IP地址是多少?),这个时候google.com域的DNS服务器一查,果真在我这里,于是就把找到的结果发送给运营商的DNS服务器,这个时候运营商的DNS服务器就拿到了www.google.com这个域名对应的IP地址

    8. Name Server域名服务器会查询存储的域名和IP的映射关系表,正常情况下都根据域名得到目标IP记录,连同一个TTL值返回给DNS Server域名服务器。

    9. 返回该域名对应的IP和TTL值,Local DNS Server会缓存这个域名和IP的对应关系,缓存的时间由TTL值控制。

    10. 把解析的结果返回给用户,用户根据TTL值缓存在本地系统缓存中,域名解析过程结束

    通过上面的步骤,我们最后获取的是IP地址,也就是浏览器最后发起请求的时候是基于IP来和服务器做信息交互的。在实际的DNS解析过程中,可能还不止这10个步骤,如Name Server也可能有多级,或者有一个GTM来负载均衡控制,这都有可能会影响域名解析的过程。根据以上解析流程,DNS解析整个过程,分为:递归查询过程和迭代查询过程。如图所示:

    输入图片说明

    所谓 递归查询过程 就是 “查询的递交者” 更替, 而 迭代查询过程 则是 “查询的递交者”不变。

    举个例子来说,你想知道某个一起上法律课的女孩的电话,并且你偷偷拍了她的照片,回到寝室告诉一个很仗义的哥们儿,这个哥们儿二话没说,拍着胸脯告诉你,甭急,我替你查(此处完成了一次递归查询,即,问询者的角色更替)。然后他拿着照片问了学院大四学长,学长告诉他,这姑娘是xx系的;然后这哥们儿马不停蹄又问了xx系的办公室主任助理同学,助理同学说是xx系yy班的,然后很仗义的哥们儿去xx系yy班的班长那里取到了该女孩儿电话。(此处完成若干次迭代查询,即,问询者角色不变,但反复更替问询对象)最后,他把号码交到了你手里。完成整个查询过程。

    4.2 跟踪域名解析过程

    在Linux系统中还可以使用dig命名来查询DNS的解析过程,如下所示:dig +cmd +trace www.google.com

    输入图片说明

    上面清楚地显示了整个域名是如何发起和解析的,从根域名(.)到gTLD Server(.com.)再到Name Server (google.com.)的整个过程都显示出来了。还可以看出DNS的服务器有多个备份,可以从任何一台查询到解析结果。

    4.3 清除缓存的域名

    我们知道DNS域名解析后会缓存解析结果,其中主要在两个地方缓存结果,一个是Local DNS Server,另外一个是用户的本地机器。这两个缓存都是TTL值和本机缓存大小控制的,但是最大缓存时间是TTL值,基本上Local DNS Server的缓存时间就是TTL控制的,很难人工介入,但是我们的本机缓存可以通过如下方式清除。

    在Linux下可以通过/etc/init.d/nscd restart来清除缓存。如下:

    JVM缓存DNS解析结果:在Java应用中JVM也会缓存DNS的解析结果,这个缓存是在InetAddress类中完成的,而且这个缓存时间还比较特殊,它有两种缓存策略:一种是正确解析结果缓存,另一种是失败的解析结果缓存。 这两个缓存时间由两个配置项控制,配置项是在%JAVA_ HOME%\lib\security\java.security文件中配置的。两个配置项分别是networkaddress.cache.ttl 和networkaddress.cache.negative.ttl,它们的默认值分别是-1(永不失效)和10(缓存10秒)。

    要修改这两个值同样有几种方式,分别是:直接修改java.security文件中的默认值、在Java的启动参数中增加-Dsun.net.inetaddr.ttl=xxx来修改默认值、通过InetAddress类动态修改。

    在这里还要特别强调一下,如果我们需要用InetAddress类解析域名时,一定要是单例模式,不然会有严重的性能问题,如果每次都创建InetAddress实例,每次都要进行一次完整的域名解析,非常耗时,这点要特别注意。

    4.4 几种域名解析方式

    1. A记录,A代表的是Address,用来指定域名对应的IP地址

      如将item.taobao.com指定到115.238.23.241,将switch.taobao.com指定到121.14.24.241。A记录可以将多个域名解析到一个IP地址,但是不能将一个域名解析到多个IP地址

    2. MX记录,表示的是Mail Exchange,就是可以将某个域名下的邮件服务器指向自己的Mail Server

      如taobao.com域名的A记录IP地址是115.238.25.245,如果MX记录设置为115.238.25.246,是xxx@taobao.com的邮件路由,DNS会将邮件发送到115.238.25.246所在的服务器,而正常通过Web请求的话仍然解析到A记录的IP地址

    3. CNAME记录,全称是Canonical Name(别名解析),所谓的别名解析就是可以为一个域名设置一个或者多个别名

      如将taobao.com解析到xulingbo.net,将srcfan.com也解析到xulingbo.net,其中xulingbo.net分别是taobao.com和srcfan.com的别名。前面的跟踪域名解析中的“www.taobao.com. 1542 IN CNAME www.gslb.taobao.com”就是CNAME解析。

    4. NS记录,为某个域名指定DNS解析服务器,也就是这个域名有指定的IP地址的DNS服务器去解析

      前面的“google.com. 172800 IN NS ns4.google.com.”就是NS解析。

    5. TXT记录,为某个主机名或域名设置说明

      如可以为google.com设置TXT记录为“谷歌|中国”这样的说明。

    4.5 网络抓包分析

    Linux虚拟机测试,使用命令 wget www.linux178.com 来请求,发现直接使用chrome浏览器请求时,干扰请求比较多,所以就使用wget命令来请求,不过使用wget命令只能把index.html请求回来,并不会对index.html中包含的静态资源(js、css等文件)进行请求

    抓包截图如下:

    输入图片说明

    1号包,这个是那台虚拟机在广播,要获取192.168.100.254(也就是网关)的MAC地址,因为局域网的通信靠的是MAC地址,它为什么需要跟网关进行通信是因为我们的DNS服务器IP是外围IP,要出去必须要依靠网关帮我们出去才行

    2号包,这个是网关收到了虚拟机的广播之后,回应给虚拟机的回应,告诉虚拟机自己的MAC地址,于是客户端找到了路由出口。

    3号包,这个包是wget命令向系统配置的DNS服务器提出域名解析请求(准确的说应该是wget发起了一个DNS解析的系统调用),请求的域名www.linux178.com,期望得到的是IP6的地址(AAAA代表的是IPv6地址)

    4号包,这个DNS服务器给系统的响应,很显然目前使用IPv6的还是极少数,所以得不到AAAA记录的。

    5&6号包,这个还是请求解析IPv6地址,但是www.linux178.com.leo.com这个主机名是不存在的,所以得到结果就是no such name。

    7号包,这个才是请求的域名对应的IPv4地址(A记录)。

    8号包,DNS服务器不管是从缓存里面,还是进行迭代查询最终得到了域名的IP地址,响应给了系统,系统再给了wget命令,wget于是得到了www.linux178.com的IP地址,这里也可以看出客户端和本地的DNS服务器是递归的查询(也就是服务器必须给客户端一个结果)这就可以开始下一步了,进行TCP的三次握手。

    5 发起TCP的3次握手

    拿到域名对应的IP地址之后,User-Agent(一般是指浏览器)会以一个随机端口(1024 < 端口 < 65535)向服务器的WEB程序(常用的有httpd,nginx等)80端口发起TCP的连接请求。这个连接请求(原始的http请求经过 TCP/IP4层模型的层层封包)到达服务器端后(这中间通过各种路由设备,局域网内除外),进入到网卡,然后是进入到内核的TCP/IP协议栈(用于识 别该连接请求,解封包,一层一层的剥开),还有可能要经过Netfilter防火墙(属于内核的模块)的过滤,最终到达WEB程序,最终建立了 TCP/IP的连接。

    如下图所示:

    1. Client首先发送一个连接试探,ACK=0 表示确认号无效,SYN = 1 表示这是一个连接请求或连接接受报文,同时表示这个数据报不能携带数据,seq = x 表示Client自己的初始序号(seq = 0 就代表这是第0号包),这时候Client进入syn_sent状态,表示客户端等待服务器的回复。

    2. Server监听到连接请求报文后,如同意建立连接,则向Client发送确认。TCP报文首部中的 SYN 和 ACK都置1 ,ack = x + 1表示期望收到对方下一个报文段的第一个数据字节序号是x+1,同时表明x为止的所有数据都已正确收到(ack=1其实是ack=0+1,也就是期望客户 端的第1个包),seq = y 表示Server自己的初始序号(seq=0就代表这是服务器这边发出的第0号包)。这时服务器进入syn_rcvd,表示服务器已经收到Client的 连接请求,等待client的确认。

    3. Client收到确认后还需再次发送确认,同时携带要发送给Server的数据。ACK 置1 表示确认号ack= y + 1 有效(代表期望收到服务器的第1个包),Client自己的序号seq= x + 1(表示这就是我的第1个包,相对于第0个包来说的),一旦收到Client的确认之后,这个TCP连接就进入Established状态,就可以发起 http请求了。

    看抓包截图:

    输入图片说明

    TCP 为什么需要3次握手?

    举个例子:假设一个老外在故宫里面迷路了,看到了小明,于是就有下面的对话:

    老外: Excuse me,Can you Speak English?

    小明: yes 。

    老外: OK,I want ...

    在问路之前,老外先问小明是否会说英语,小明回答是的,这时老外才开始问路。

    2个计算机通信是靠协议(目前流行的TCP/IP协议)来实现,如果2个计算机使用的协议不一样,那是不能进行通信的,所以这个3次握手就相当于试探一下对方是否遵循TCP/IP协议,协商完成后就可以进行通信了,当然这样理解不是那么准确。

    为什么HTTP协议要基于TCP来实现?

    目前在Internet中所有的传输都是通过TCP/IP进行的,HTTP协议作为TCP/IP模型中应用层的协议也不例外,TCP是一个端到端的可靠的面向连接的协议,所以HTTP基于传输层TCP协议不用担心数据的传输的各种问题。

    6 建立TCP连接后发起http请求

    经过TCP3次握手之后,浏览器发起了http的请求(看第⑫包),使用的http的方法 GET 方法,请求的URL是 / ,协议是HTTP/1.0:

    输入图片说明

    下面是第12号包的详细内容:

    输入图片说明

    以上的报文是HTTP请求报文。那么HTTP请求报文和响应报文会是什么格式呢?

    起始行:如 GET / HTTP/1.0 (请求的方法 请求的URL 请求所使用的协议)

    头部信息:User-Agent Host等成对出现的值

    主体

    不管是请求报文还是响应报文都会遵循以上的格式。那么起始行中的请求方法有哪些种呢?

    GET: 完整请求一个资源 (常用)

    HEAD: 仅请求响应首部

    POST: 提交表单 (常用)

    PUT: 上传

    DELETE: 删除

    OPTIONS: 返回请求的资源所支持的方法的方法

    TRACE: 追求一个资源请求中间所经过的代理

    那什么是URL、URI、URN?

    URI Uniform Resource Identifier 统一资源标识符,如:scheme://[username:password@]HOST:port/path/to/source

    URL Uniform Resource Locator 统一资源定位符,如:http://www.magedu.com/downloads/nginx-1.5.tar.gz

    URN Uniform Resource Name 统一资源名称

    URL和URN都属于URI,为了方便就把URL和URI暂时都通指一个东西。

    请求的协议有哪些种?有以下几种:

    http/0.9: stateless

    http/1.0: MIME, keep-alive (保持连接), 缓存

    http/1.1: 更多的请求方法,更精细的缓存控制,持久连接(persistent connection) 比较常用

    下面是Chrome发起的http请求报文头部信息:

    输入图片说明

    Accept 就是告诉服务器端,接受那些MIME类型

    Accept-Encoding 这个看起来是接受那些压缩方式的文件

    Accept-Lanague 告诉服务器能够发送哪些语言

    Connection 告诉服务器支持keep-alive特性,TCP连接在发送后将仍然保持打开状态,于是,浏览器可以继续通过相同的TCP连接发送请求。保持连接节省了为每个请求建立新连接所需的时间,还节约了网络带宽。

    Cookie 每次请求时都会携带上Cookie以方便服务器端识别是否是同一个客户端

    Host 用来标识请求服务器上的那个虚拟主机,比如Nginx里面可以定义很多个虚拟主机,那这里就是用来标识要访问那个虚拟主机。

    User-Agent 用户代理,一般情况是浏览器,也有其他类型,如:wget curl 搜索引擎的蜘蛛等

    条件请求头部:If-Modified-Since是浏览器向服务器端询问某个资源文件如果自从什么时间修改过,那么重新发给我,这样就保证服务器端资源文件更新时,浏览器再次去请求,而不是使用缓存中的文件。

    安全请求头部:Authorization: 客户端提供给服务器的认证信息;

    什么是MIME?

    MIME(Multipurpose Internet Mail Extesions 多用途互联网邮件扩展)是一个互联网标准,它扩展了电子邮件标准,使其能够支持非ASCII字符、二进制格式附件等多种格式的邮件消息,这个标准被定义在 RFC 2045、RFC 2046、RFC 2047、RFC 2048、RFC 2049等RFC中。 由RFC 822转变而来的RFC 2822,规定电子邮件标准并不允许在邮件消息中使用7位ASCII字符集以外的字符。正因如此,一些非英语字符消息和二进制文件,图像,声音等非文字消 息都不能在电子邮件中传输。

    MIME规定了用于表示各种各样的数据类型的符号化方法。此外,在万维网中使用的HTTP协议中也使用了MIME的框架,标准被扩展为互联网媒体类型

    MIME 遵循以下格式:major/minor 主类型/次类型 例如:

    image/jpg

    image/gif

    text/html

    video/quicktime

    appliation/x-httpd-php

    本帖子中包含更多资源

    您需要 登录 才可以下载或查看,没有帐号?立即注册

    x
    守望者AIR技术交流社区(www.airmyth.com)
    回复

    使用道具 举报

  • TA的每日心情
    擦汗
    7 天前
  • 签到天数: 431 天

    [LV.9]以坛为家II

    1738

    主题

    2089

    帖子

    12万

    积分

    超级版主

    Rank: 18Rank: 18Rank: 18Rank: 18Rank: 18

    威望
    447
    贡献
    16
    金币
    50896
    钢镚
    1418

    开源英雄守望者

     楼主| 发表于 2016-7-27 14:40:19 | 显示全部楼层

    0 系列目录

    上一篇《WEB请求处理一:浏览器请求发起处理》,我们讲述了浏览器端请求发起过程,通过DNS域名解析服务器IP,并建立TCP连接,发送HTTP请求。本文将讲述请求到达反向代理服务器的一个处理过程,比如:在Nginx中请求的反向代理处理流程,请求都是经过了哪些模块,做了哪些处理,又是如何找到应用服务器呢?

    为直观明了,先上一张图,红色部分为本章所述模块:

    输入图片说明

    正如标题所述,Nginx功能是进行请求的反向代理,在讲解Nginx请求处理之前,首先要给大家清楚地说明下反向代理是什么?它的功能是什么?它在Nginx中又是怎么配置实现的?

    1 反向代理

    1.1 概念

    反向代理(Reverse Proxy)方式是指以代理服务器来接受internet上的连接请求,然后将请求转发给内部网络上的服务器,并将从服务器上得到的结果返回给internet上请求连接的客户端,此时代理服务器对外就表现为一个服务器。

    举个例子,比如我想访问 http://www.test.com/readme ,但www.test.com上并不存在readme页面,于是他是偷偷从另外一台服务器上取回来,然后作为自己的内容返回用户,但用户并不知情。这里所提到的 www.test.com 这个域名对应的服务器就设置了反向代理功能。

    结论就是,反向代理服务器对于客户端而言它就像是原始服务器,并且客户端不需要进行任何特别的设置。客户端向反向代理的命名空间(name-space)中的内容发送普通请求,接着反向代理服务器将判断向何处(原始服务器)转交请求,并将获得的内容返回给客户端,就像这些内容原本就是它自己的一样。

    正向代理,既然有反向代理,就肯定有正向代理。什么叫正向代理呢?

    正向代理(Forward Proxy)通常都被简称为代理,就是在用户无法正常访问外部资源,比方说受到GFW的影响无法访问twitter的时候,我们可以通过代理的方式,让用户绕过防火墙,从而连接到目标网络或者服务。

    正向代理的工作原理就像一个跳板,比如:我访问不了google.com,但是我能访问一个代理服务器A,A能访问google.com,于是我先 连上代理服务器A,告诉他我需要google.com的内容,A就去取回来,然后返回给我。从网站的角度,只在代理服务器来取内容的时候有一次记录,有时 候并不知道是用户的请求,也隐藏了用户的资料,这取决于代理告不告诉网站。

    结论就是,正向代理是一个位于客户端和原始服务器(origin server)之间的服务器。为了从原始服务器取得内容,客户端向代理发送一个请求并指定目标(原始服务器),然后代理向原始服务器转交请求并将获得的内容返回给客户端。

    反向代理VS正向代理:

    1.2 工作流程

    1. 用户通过域名发出访问web服务器的请求,该域名被DNS服务器解析为反向代理服务器的IP地址;

    2. 反向代理服务器接受用户的请求;

    3. 反向代理服务器在本地缓存中查找请求的内容,找到后直接把内容发送给用户;

    4. 如果本地缓存里没有用户所请求的信息内容,反向代理服务器会代替用户向源服务器请求同样的信息内容,并把信息内容发给用户,如果信息内容是缓存的还会把它保存到缓存中。

    1.3 优点

    1. 保护了真实的web服务器,web服务器对外不可见,外网只能看到反向代理服务器,而反向代理服务器上并没有真实数据,因此,保证了web服务器的资源安全

      通常的代理服务器,只用于代理内部网络对Internet外部网络的连接请求,客户机必须指定代理服务器,并将本来要直接发送到Web服务器上的http请求发送到代理服务器中。不支持外部网络对内部网络的连接请求,因为内部网络对外部网络是不可见的。当一个代理服务器能够代理外部网络上的主机,访问内部网络时,这种代理服务的方式称为反向代理服务。此时代理服务器对外就表现为一个Web服务器,外部网络就可以简单把它当作一个标准的Web服务器而不需要特定的配置。不同之处在于,这个服务器没有保存任何网页的真实数据,所有的静态网页或者CGI程序,都保存在内部的Web服务器上。因此对反向代理服务器的攻击并不会使得网页信息遭到破坏,这样就增强了Web服务器的安全性。

      代理服务器充当内容服务器的替身,如果您的内容服务器具有必须保持安全的敏感信息,如信用卡号数据库,可在防火墙外部设置一个代理服务器作为内容服 务器的替身。当外部客户机尝试访问内容服务器时,会将其送到代理服务器。实际内容位于内容服务器上,在防火墙内部受到安全保护。代理服务器位于防火墙外部,在客户机看来就像是内容服务器

      当客户机向站点提出请求时,请求将转到代理服务器。然后,代理服务器通过防火墙中的特定通路,将客户机的请求发送到内容服务器。内容服务器再通过该通道将结果回传给代理服务器。代理服务器将检索到的信息发送给客户机,好像代理服务器就是实际的内容服务器。如果内容服务器返回错误消息,代理服务器会先行截取该消息并更改标头中列出的任何URL,然后再将消息发送给客户机。如此可防止外部客户机获取内部内容服务器的重定向URL。

      这样,代理服务器就在安全数据库和可能的恶意攻击之间提供了又一道屏障。与有权访问整个数据库的情况相对比,就算是侥幸攻击成功,作恶者充其量也仅限于访问单个事务中所涉及的信息。未经授权的用户无法访问到真正的内容服务器,因为防火墙通路只允许代理服务器有权进行访问

      可以配置防火墙路由器,使其只允许特定端口上的特定服务器有权通过防火墙进行访问,而不允许其他任何机器进出。安全反向代理,指当代理服务器与其他机器之间有一个或多个连接使用安全套接字层 (SSL) 协议加密数据时,即会进行安全反向代理。

    2. 节约了有限的IP地址资源

      企业内所有的网站共享一个在internet中注册的IP地址,这些服务器分配私有地址,采用虚拟主机的方式对外提供服务。

    3. 减少WEB服务器压力,提高响应速度

      反向代理就是通常所说的web服务器加速,它是一种通过在繁忙的web服务器和外部网络之间增加一个高速的web缓冲服务器来降低实际的web服务器的负载的一种技术。反向代理是针对web服务器提高加速功能,作为代理缓存,它并不是针对浏览器用户,而针对一台或多台特定的web服务器,它可以代理外部网络对内部网络的访问请求

      反向代理服务器会强制将外部网络对要代理的服务器的访问经过它,这样反向代理服务器负责接收客户端的请求,然后到源服务器上获取内容,把内容返回给 用户,并把内容保存到本地,以便日后再收到同样的信息请求时,它会把本地缓存里的内容直接发给用户,以减少后端web服务器的压力,提高响应速度。因此 Nginx还具有缓存功能。

    4. 其他优点

      (1)请求的统一控制,包括设置权限、过滤规则等;

      (2)区分动态和静态可缓存内容;

      (3)实现负载均衡,内部可以采用多台服务器来组成服务器集群,外部还是可以采用一个地址访问;

      (4)解决Ajax跨域问题;

      (5)作为真实服务器的缓冲,解决瞬间负载量大的问题;

    2 Nginx常用配置

    写到这时,一直在由于要不要去开这一节Nginx配置的讲解,如果讲的话,感觉与本文的主题有所偏离,但又考虑到,如果对Nginx配置文件都不熟悉的话,下面的内容再去讲解Nginx反向代理处理流程就有点纸上谈兵了,担心大家有些云里雾里,毫无收获。

    终究旨在为了要让大家有所收获的初衷,决定还是要着重讲解Nginx的几种常见配置,其中包括:动静分离、缓存设置、负载均衡、反向代理、还有虚拟主机功能

    2.1 Nginx启动和关闭

    Mac平台,我用brew安装的:

    /usr/local/bin/nginx # 启动
    /usr/local/bin/nginx -s reload #平滑重启
    /usr/local/etc/nginx/nginx.cnf #配置文件。
    

    2.2 配置文件详解

    其实,对比,apache的配置文件,它的相对比较清晰和简单,之前觉得很难,现在沉下心来想想,其实很简单。大致的分块下,基本就分为以下几块:

    main
    events {
        ....
    }
    http {
        ....
        upstream myproject {
            .....
        }
        server  {
            ....
            location {
                ....
            }
        }
        server  {
            ....
            location {
                ....
            }
        }
        ....
    }
    

    Nginx配置文件主要分为六个区域:

    main:全局设置

    events:Nginx工作模式

    http:http设置

    server:主机设置

    location:URL匹配

    upstream:负载均衡服务器设置

    2.2.1 main模块

    下面是一个main区域,它是一个全局的设置:

    user nobody nobody;
    worker_processes 2;
    error_log /usr/local/var/log/nginx/error.log notice;
    pid /usr/local/var/run/nginx/nginx.pid;
    worker_rlimit_nofile 1024;
    

    user 来指定Nginx Worker进程运行用户以及用户组,默认由nobody账号运行。

    workerprocesses 来指定了Nginx要开启的子进程数。每个Nginx进程平均耗费10M~12M内存。根据经验,一般指定1个进程就足够了,如果是多核CPU,建议指定和CPU的数量一样的进程数即可_。我这里写2,那么就会开启2个子进程,总共3个进程。

    errorlog 来定义全局错误日志文件_。日志输出级别有debug、info、notice、warn、error、crit可供选择,其中,debug输出日志最为最详细,而crit输出日志最少。

    pid 来指定进程id的存储文件位置

    workerrlimit_nofile 来指定一个nginx进程可以打开的最多文件描述符数目_,这里是65535,需要使用命令“ulimit -n 65535”来设置。

    2.2.2 events模块

    events模块来用指定nginx的工作模式和工作模式及连接数上限,一般是这样:

    events {
        use kqueue; #mac平台
        worker_connections  1024;
    }
    

    use 用来指定Nginx的工作模式。Nginx支持的工作模式有select、poll、kqueue、epoll、rtsig和/dev/poll。其中select和poll都是标准的工作模式,kqueue和epoll是高效的工作模式,不同的是epoll用在Linux平台上,而kqueue用在BSD系统中,因为Mac基于BSD,所以Mac也得用这个模式,对于Linux系统,epoll工作模式是首选。

    workerconnections 用于定义Nginx每个进程的最大连接数_,即接收前端的最大 请求数,默认是1024。最大客户端连接数由worker_processes和worker_connections决定,即Max_clients = worker_processes * worker_connections,在作为反向代理时,Max_clients变为:Max_clients = worker_processes * worker_connections / 4。

    进程的最大连接数受Linux系统进程的最大打开文件数限制,在执行操作系统命令“ulimit -n 65536”后worker_connections的设置才能生效。

    2.2.3 http模块

    http模块可以说是最核心的模块了,它负责HTTP服务器相关属性的配置,它里面的server和upstream子模块,至关重要,等到反向代理和负载均衡以及虚拟目录等会仔细说。

    http{
        include mime.types;
        default_type application/octet-stream;
        log_format main '$remote_addr - $remote_user [$time_local] "$request" '
                        '$status $body_bytes_sent "$http_referer" '
                        '"$http_user_agent" "$http_x_forwarded_for"';
        access_log /usr/local/var/log/nginx/access.log  main;
        sendfile on;
        tcp_nopush on;
        tcp_nodelay on;
        keepalive_timeout 10;
        #gzip on;
        upstream myproject {
            .....
        }
        server {
            ....
        }
    }
    
    1. include

      用来设定文件的mime类型,类型在配置文件目录下的mime.type文件定义,来告诉nginx来识别文件类型。

    2. default_type

      设定了默认的类型为二进制流,也就是当文件类型未定义时使用这种方式,例如在没有配置asp 的locate 环境时,Nginx是不予解析的,此时,用浏览器访问asp文件就会出现下载窗口了。

    3. log_format

      用于设置日志的格式,和记录哪些参数,这里设置为main,刚好用于access_log来纪录这种类型

      main的类型日志如下:也可以增删部分参数。

      127.0.0.1 - - [21/Apr/2015:18:09:54 +0800] "GET /index.php HTTP/1.1" 200 87151 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.76 Safari/537.36"

    4. access_log

      用来纪录每次的访问日志的文件地址,后面的main是日志的格式样式,对应于log_format的main。

    5. sendfile

      用于开启高效文件传输模式。将tcp_nopush和tcp_nodelay两个指令设置为on用于防止网络阻塞。

    6. keepalive_timeout

      设置客户端连接保持活动的超时时间。在超过这个时间之后,服务器会关闭该连接。

    2.2.4 server模块

    server模块是http的子模块,它用来定一个虚拟主机,我们先讲最基本的配置,这些在后面再讲。我们看一下一个简单的server 是如何做的?

    server {
        listen       8080;
        server_name  localhost 192.168.12.10 www.yangyi.com;
        # 全局定义,如果都是这一个目录,这样定义最简单。
        root   /Users/yangyi/www;
        index  index.php index.html index.htm; 
        charset utf-8;
        access_log  usr/local/var/log/host.access.log  main;
        aerror_log  usr/local/var/log/host.error.log  error;
        ....
    }
    

    server 标志定义虚拟主机开始。

    listen 用于指定虚拟主机的服务端口。

    server_name 用来指定IP地址或者域名,多个域名之间用空格分开。

    root 表示在这整个server虚拟主机内,全部的root web根目录。注意要和locate {}下面定义的区分开来。

    index 全局定义访问的默认首页地址。注意要和locate {}下面定义的区分开来。

    charset 用于设置网页的默认编码格式。

    access_log 用来指定此虚拟主机的访问日志存放路径,最后的main用于指定访问日志的输出格式。

    2.2.5 location模块

    location模块是nginx中用的最多的,也是最重要的模块了,什么负载均衡啊、反向代理啊、虚拟域名啊都与它相关

    location根据它字面意思就知道是来定位的,定位URL,解析URL,所以,它也提供了强大的正则匹配功能,也支持条件判断匹配,用户可以通过location指令实现Nginx对动、静态网页进行过滤处理。像我们的php环境搭建就是用到了它。

    1. 我们先来看这个,设定默认首页和虚拟机目录

      location / {
        root   /Users/yangyi/www;
        index  index.php index.html index.htm;
      }
      

      location / 表示匹配访问根目录。

      root 指令用于指定访问根目录时,虚拟主机的web目录,这个目录可以是相对路径(相对路径是相对于nginx的安装目录)。也可以是绝对路径

      index 用于设定我们只输入域名后访问的默认首页地址,有个先后顺序:index.php index.html index.htm,如果没有开启目录浏览权限,又找不到这些默认首页,就会报403错误。

    2. location 还有一种方式就是正则匹配,开启正则匹配这样:location ~。后面加个~

      下面这个例子是运用正则匹配来链接php。我们之前搭建环境也是这样做:

      location ~ \.php$ {
        root           /Users/yangyi/www;
        fastcgi_pass   127.0.0.1:9000;
        fastcgi_index  index.php;
        include        fastcgi.conf;
      }
      

      .php$ 熟悉正则的我们直到,这是匹配.php结尾的URL,用来解析php文件。里面的root也是一样,用来表示虚拟主机的根目录

      fast_pass 链接的是php-fpm的地址。其他几个参数我们以后再说。

      location 还有其他用法,等讲到实例的时候,再看吧。

    2.2.6 upstream模块

    upstream 模块负责负载均衡模块,通过一个简单的调度算法来实现客户端IP到后端服务器的负载均衡。先学习怎么用,具体的使用实例以后再说。

    upstream iyangyi.com{
        ip_hash;
        server 192.168.12.1:80;
        server 192.168.12.2:80 down;
        server 192.168.12.3:8080  max_fails=3  fail_timeout=20s;
        server 192.168.12.4:8080;
    }
    

    在上面的例子中,通过upstream指令指定了一个负载均衡器的名称iyangyi.com。这个名称可以任意指定,在后面需要的地方直接调用即可。里面是ip_hash这是其中的一种负载均衡调度算法,下面会着重介绍。紧接着就是各种服务器了。用server关键字表识,后面接ip。

    Nginx的负载均衡模块目前支持4种调度算法:

    1. weight 轮询(默认)。每个请求按时间顺序逐一分配到不同的后端服务器,如果后端某台服务器宕机,故障系统被自动剔除,使用户访问不受影响。weight。指定轮询权值,weight值越大,分配到的访问机率越高,主要用于后端每个服务器性能不均的情况下。

    2. ip_hash。每个请求按访问IP的hash结果分配,这样来自同一个IP的访客固定访问一个后端服务器,有效解决了动态网页存在的session共享问题。

    3. fair(第三方)。比上面两个更加智能的负载均衡算法。此种算法可以依据页面大小和加载时间长短智能 地进行负载均衡,也就是根据后端服务器的响应时间来分配请求,响应时间短的优先分配。Nginx本身是不支持fair的,如果需要使用这种调度算法,必须 下载Nginx的upstream_fair模块。

    4. url_hash(第三方)。按访问url的hash结果来分配请求,使每个url定向到同一个后端服务器,可以进一步提高后端缓存服务器的效率。Nginx本身是不支持url_hash的,如果需要使用这种调度算法,必须安装Nginx的hash软件包。

    在HTTP Upstream模块中,可以通过server指令指定后端服务器的IP地址和端口,同时还可以设定每个后端服务器在负载均衡调度中的状态。常用的状态有:

    1. down,表示当前的server暂时不参与负载均衡。

    2. backup,预留的备份机器。当其他所有的非backup机器出现故障或者忙的时候,才会请求backup机器,因此这台机器的压力最轻。

    3. max_fails,允许请求失败的次数,默认为1。当超过最大次数时,返回proxy_next_upstream 模块定义的错误。

    4. fail_timeout,在经历了max_fails次失败后,暂停服务的时间。max_fails可以和fail_timeout一起使用。

    注意:当负载调度算法为ip_hash时,后端服务器在负载均衡调度中的状态不能是weight和backup。

    2.3 基于域名的虚拟主机

    假设我们在本地开发有3个项目,分别在hosts里映射到本地的127.0.0.1上:

    127.0.0.1 www.iyangyi.com iyangyi.com
    127.0.0.1 api.iyangyi.com
    127.0.0.1 admin.iyangyi.com
    

    有这样3个项目,分别对应于web根目录下的3个文件夹,我们用域名对应文件夹名字,这样子好记:

    /Users/yangyi/www/www.iyangyi.com/
    /Users/yangyi/www/api.iyangyi.com/
    /Users/yangyi/www/admin.iyangyi.com/
    

    每个目录下都有一个index.php文件,都是简单的输入自己的域名。

    下面我们就来搭建这3个域名的虚拟主机,很显然,我们要新建3个server来完成。建议将对虚拟主机进行配置的内容写进另外一个文件,然后通过include指令包含进来,这样更便于维护和管理。不会使得这个nginx.conf内容太多:

    main
    events {
        ....
    }
    http {
        ....
        include vhost/www.iyangyi.conf;
        include vhost/api.iyangyi.conf;
        include vhost/admin.iyangyi.conf;
        # 或者用 *.conf  包含
        # include vhost/*.conf
    }
    

    include:主模块指令,实现对配置文件所包含的文件的设定,可以减少主配置文件的复杂度。

    既然每一个conf都是一个server,前面已经学习了一个完整的server写的了。下面就开始:

    # www.iyangyi.conf
    server {
        listen 80;
        server_name www.iyangyi.com iyangyi.com;
    
        root /Users/yangyi/www/www.iyangyi.com/;
        index index.php index.html index.htm;
    
        access_log /usr/local/var/log/nginx/www.iyangyi.access.log main;
        error_log /usr/local/var/log/nginx/www.iyangyi.error.log error;
    
        location ~ \.php$ {
            fastcgi_pass   127.0.0.1:9000; 
            fastcgi_index  index.php;
            include        fastcgi.conf;
        }
    }
    
    # api.iyangyi.conf
    server {
        listen 80;
        server_name api.iyangyi.com;
    
        root /Users/yangyi/www/api.iyangyi.com/;
        index index.php index.html index.htm;
    
        access_log /usr/local/var/log/nginx/api.iyangyi.access.log main;
        error_log /usr/local/var/log/nginx/api.iyangyi.error.log error;
    
        location ~ \.php$ {
            fastcgi_pass   127.0.0.1:9000; 
            fastcgi_index  index.php;
            include        fastcgi.conf;
        }
    }
    
    # admin.iyangyi.conf
    server {
        listen 80;
        server_name admin.iyangyi.com;
    
        root /Users/yangyi/www/admin.iyangyi.com/;
        index index.php index.html index.htm;
    
        access_log /usr/local/var/log/nginx/admin.iyangyi.access.log main;
        error_log /usr/local/var/log/nginx/admin.iyangyi.error.log error;
    
        location ~ \.php$ {
            fastcgi_pass   127.0.0.1:9000; 
            fastcgi_index  index.php;
            include        fastcgi.conf;
        }
    }
    

    这样3个很精简的虚拟域名就搭建好了。重启下nginx,然后打开浏览器访问一下这3个域名,就能看到对应的域名内容了。

    2.4 反向代理

    Nginx 使用反向代理,主要是使用location模块下的proxy_pass选项。

    来个最简单的。当我访问 mac 上的nginx 的 centos.iyangyi.com 的内容时候, 就反向代理到虚拟机centos上的 apache 192.168.33.10 的index.html页面。

    192.168.33.10 中的html 是很简单的一句输出:

    centos apache2 index.html
    

    在hosts里新加上这个域名:

    #vi /etc/hosts 
    127.0.0.1 centos.iyangyi.com
    

    在vhost目录中新建一个conf server:

    #centos.iyangyi.conf
    server {
        listen 80;
        server_name centos.iyangyi.com;
    
        access_log /usr/local/var/log/nginx/centos.iyangyi.access.log main;
        error_log /usr/local/var/log/nginx/centos.iyangyi.error.log error;
    
        location / {
            proxy_pass http://192.168.33.10;
        }
    }
    

    重启下nginx:

    sudo nginx -s reload
    

    当然。proxy 还有其他的参数,比如:proxy_set_header 用来设置header头部信息参数转发等,等用了可以仔细看看。

    2.5 负载均衡

    别被这个名字给吓住了,以为是什么很牛逼的东西的。其实不然。也很简单。

    先简单说下负载均衡是干嘛的?举个例子:我们的小网站,刚开始就一台nginx服务器,后来,随着业务量增大,用户增多,一台服务器已经不够用了,我们就又多加了几台服务器。那么这几台服务器如何调度?如何均匀的提供访问?这就是负载均衡。

    负载均衡的好处是可以集群多台机器一起工作,并且对外的IP和域名是一样的,外界看起来就好像一台机器一样。

    1. 基于 weight 权重的负载

      先来一个最简单的,weight权重的:

      upstream webservers{
        server 192.168.33.11 weight=10;
        server 192.168.33.12 weight=10;
        server 192.168.33.13 weight=10;
      }
      
      server {
        listen 80;
        server_name upstream.iyangyi.com;
      
        access_log /usr/local/var/log/nginx/upstream.iyangyi.access.log main;
        error_log /usr/local/var/log/nginx/upstream.iyangyi.error.log error;
      
        location / {
            proxy_pass http://webservers;
            proxy_set_header  X-Real-IP  $remote_addr;
        }
      }
      

      我们再来继续看几个参数 : max_fails和fail_timeout

      max_fails : 允许请求失败的次数,默认为1。当超过最大次数时,返回proxy_next_upstream 模块定义的错误。

      fail_timeout : 在经历了max_fails次失败后,暂停服务的时间。max_fails可以和fail_timeout一起使用,进行健康状态检查。

      upstream webservers{
        server 192.168.33.11 weight=10 max_fails=2 fail_timeout=30s;
        server 192.168.33.12 weight=10 max_fails=2 fail_timeout=30s;
        server 192.168.33.13 weight=10 max_fails=2 fail_timeout=30s;
      }
      

      down 表示这台机器暂时不参与负载均衡。相当于注释掉了。

      backup 表示这台机器是备用机器,是其他的机器不能用的时候,这台机器才会被使用,俗称备胎 O__O "…

      upstream webservers{
        server 192.168.33.11 down;
        server 192.168.33.12 weight=10 max_fails=2 fail_timeout=30s;
        server 192.168.33.13 backup;
      }
      
    2. 基于 ip_hash 的负载

      这种分配方式,每个请求按访问IP的hash结果分配,这样来自同一个IP的访客固定访问一个后端服务器,有效解决了动态网页存在的session共享问题。

      upstream webservers{
        ip_hash;
        server 192.168.33.11 weight=1 max_fails=2 fail_timeout=30s;
        server 192.168.33.12 weight=1 max_fails=2 fail_timeout=30s;
        server 192.168.33.13 down;
      }
      

      ip_hash 模式下,最好不要设置weight参数,因为你设置了,就相当于手动设置了,将会导致很多的流量分配不均匀。

      ip_hash 模式下,backup参数不可用,加了会报错,为啥呢?因为,本身我们的访问就是固定的了,其实,备用已经不管什么作用了。

    2.6 页面缓存

    页面缓存也是日常web 开发中很重要的一个环节,对于一些页面,我们可以将其静态化,保存起来,下次请求时候,直接走缓存,而不用去请求反相代理服务器甚至数据库服务了。从而减轻服务器压力。

    nginx 也提供了简单而强大的下重定向,反向代理的缓存功能,只需要简单配置下,就能将指定的一个页面缓存起来。它的原理也很简单,就是匹配当前访问的url, hash加密后,去指定的缓存目录找,看有没有,有的话就说明匹配到缓存了。

    我们先来看一下一个简单的页面缓存的配置:

    http {
        proxy_cache_path /data/nginx/cache levels=1:2 keys_zone=cache_zone:10m inactive=1d max_size=100m;
        upstream myproject {
            .....
        }
        server  {
            ....
            location ~ *\.php$ {
                proxy_cache cache_zone; #keys_zone的名字
                proxy_cache_key $host$uri$is_args$args; #缓存规则
                proxy_cache_valid any 1d;
                proxy_pass http://127.0.0.1:8080;
            }
        }
        ....
    }
    

    下面我们来一步一步说。用到的配置参数,主要是proxy_*前缀的很多配置。

    首先需要在http中加入proxy_cache_path 它用来制定缓存的目录以及缓存目录深度制定等。它的格式如下:

    proxy_cache_path path [levels=number] keys_zone=zone_name:zone_size [inactive=time] [max_size=size];
    

    path是用来指定 缓存在磁盘的路径地址。比如:/data/nginx/cache。那以后生存的缓存文件就会存在这个目录下。

    levels用来指定缓存文件夹的级数,可以是:levels=1, levels=1:1, levels=1:2, levels=1:2:3 可以使用任意的1位或2位数字作为目录结构分割符,如 X, X:X,或 X:X:X 例如: 2, 2:2, 1:1:2,但是最多只能是三级目录。

    那这个里面的数字是什么意思呢。表示取hash值的个数。比如:现在根据请求地址localhost/index.php?a=4 用md5进行哈希,得到e0bd86606797639426a92306b1b98ad9

    levels=1:2 表示建立2级目录,把hash最后1位(9)拿出建一个目录,然后再把9前面的2位(ad)拿来建一个目录, 那么缓存文件的路径就是/data/nginx/cache/9/ad/e0bd86606797639426a92306b1b98ad9

    以此类推:levels=1:1:2表示建立3级目录,把hash最后1位(9)拿出建一个目录,然后再把9前面的1位(d)建一个目录, 最后把d前面的2位(8a)拿出来建一个目录 那么缓存文件的路径就是/data/nginx/cache/9/d/8a/e0bd86606797639426a92306b1b98ad9

    keys_zone 所有活动的key和元数据存储在共享的内存池中,这个区域用keys_zone参数指定。zone_name指的是共享池的名称,zone_size指的是共享池的大小。

    注意每一个定义的内存池必须是不重复的路径,例如:

    proxy_cache_path  /data/nginx/cache/one  levels=1      keys_zone=one:10m;
    proxy_cache_path  /data/nginx/cache/two  levels=2:2    keys_zone=two:100m;
    proxy_cache_path  /data/nginx/cache/three  levels=1:1:2  keys_zone=three:1000m;
    

    inactive 表示指定的时间内缓存的数据没有被请求则被删除,默认inactive为10分钟。inactive=1d 1小时。inactive=30m 30分钟。

    max_size 表示单个文件最大不超过的大小。它被用来删除不活动的缓存和控制缓存大小,当目前缓存的值超出max_size指定的值之后,超过其大小后最少使用数据 (LRU替换算法)将被删除。max_size=10g表示当缓存池超过10g就会清除不常用的缓存文件。

    clean_time 表示每间隔自动清除的时间。clean_time=1m 1分钟清除一次缓存。

    好。说完了这个很重要的参数。我们再来说在server模块里的几个配置参数:

    proxy_cache 用来指定用哪个keys_zone的名字,也就是用哪个目录下的缓存。上面我们指定了三个one, two,three 。比如,我现在想用one 这个缓存目录 : proxy_cache one

    proxycache_key 这个其实蛮重要的,它用来指定生成hash的url地址的格式。根据这个key映射成一个hash值,然后存入到本地文件_。 proxy_cache_key $host$uri表示无论后面跟的什么参数,都会访问一个文件,不会再生成新的文件。 而如果proxy_cache_key $is_args$args,那么传入的参数 localhost/index.php?a=4 与localhost/index.php?a=44 将映射成两个不同hash值的文件。

    proxy_cache_key 默认是 "$scheme$host$request_uri"。但是一般我们会把它设置成:$host$uri$is_args$args 一个完整的url路径。

    proxy_cache_valid 它是用来为不同的http响应状态码设置不同的缓存时间。

    proxy_cache_valid  200 302  10m;
    proxy_cache_valid  404      1m;
    

    表示为http status code 为200和302的设置缓存时间为10分钟,404代码缓存1分钟。 如果只定义时间:

    proxy_cache_valid 5m;
    

    那么只对代码为200, 301和302的code进行缓存。 同样可以使用any参数任何相响应:

    proxy_cache_valid  200 302 10m;
    proxy_cache_valid  301 1h;
    proxy_cache_valid  any 1m; #所有的状态都缓存1小时
    

    好。缓存的基本一些配置讲完了。也大致知道了怎么使用这些参数。现在开始实战!我们启动一台vagrant linux 机器 web1 (192.168.33.11) 用作远程代理机器,就不搞复杂的负载均衡了。

    先在Mac本地加一个域名cache.iyangyi.com, 然后按照上面的配置在vhost 下新建一个proxy_cache.iyangyi.conf 文件:

    proxy_cache_path /usr/local/var/cache levels=1:2 keys_zone=cache_zone:10m inactive=1d max_size=100m;
    server  {
        listen 80;
        server_name cache.iyangyi.com;
    
        access_log /usr/local/var/log/nginx/cache.iyangyi.access.log main;
        error_log /usr/local/var/log/nginx/cache.iyangyi.error.log error;
    
        add_header X-Via $server_addr;
        add_header X-Cache $upstream_cache_status;
    
        location / {
            proxy_set_header  X-Real-IP  $remote_addr;
            proxy_cache cache_zone;
            proxy_cache_key $host$uri$is_args$args;
            proxy_cache_valid 200 304 1m;
            proxy_pass http://192.168.33.11;
        }
    }
    

    打开审核元素或者firebug。看network网络请求选项,我们可以看到,Response Headers,在这里我们可以看到:

    X-Cache:MISS
    X-Via:127.0.0.1
    

    X-cache 为 MISS 表示未命中,请求被传送到后端。因为是第一次访问,没有缓存,所以肯定是未命中。我们再刷新下,就发现其变成了HIT, 表示命中。它还有其他几种状态:

    MISS 未命中,请求被传送到后端

    HIT 缓存命中

    EXPIRED 缓存已经过期请求被传送到后端

    UPDATING 正在更新缓存,将使用旧的应答

    STALE 后端将得到过期的应答

    BYPASS 缓存被绕过了

    我们再去看看缓存文件夹 /usr/local/var/cache里面是否有了文件:

    cache git:(master) cd a/1313 git:(master) ls
    5bd1af99bcb0db45c8bd601d9ee9e13a
    ➜  13 git:(master) pwd
    /usr/local/var/cache/a/13
    

    已经生成了缓存文件。

    我们在url 后面随便加一个什么参数,看会不会新生成一个缓存文件夹及文件:http://cache.iyangyi.com/?w=ww55 。因为我们使用的生成规则是全部url转换(proxy_cache_key $host$uri$is_args$args;)

    查看 X-cache 为 MISS,再刷新 ,变成HIT。再去看一下缓存文件夹 /usr/local/var/cache。

    ~cache git:(master) ls
     4 a
    

    果然又生成了一个4文件夹。

    2.7 location 正则模块

    这一小节,主要来学习nginx中的URL重写怎么做。url重写模块,主要是在location模块面来实现,我们一点一点的看。

    首先看下location 正则匹配的使用。还记得之前是如何用location来定位.php文件的吗?

    location ~ \.php$ {
        fastcgi_pass   127.0.0.1:9000; 
        fastcgi_index  index.php;
        include        fastcgi.conf;
    }
    

    我们用~来表示location开启正则匹配, 这样:location ~。还可以用这个来匹配静态资源,缓存它们,设置过期时间:

    location ~ .*\.(gif|jpg|jpeg|bmp|png|ico|txt|mp3|mp4|swf){
        expires 15d;
    }
    location ~ .*\.(css|js){
        expires 12h;
    }
    

    expires 用来设置HTTP应答中的Expires和Cache-Control的头标时间,来告诉浏览器访问这个静态文件时,不用再去请求服务器,直接从本地缓存读取就可以了。

    语法: expires [time|epoch|max|off]
    默认值: expires off
    作用域: http, server, location
    

    可以在time值中使用正数或负数。“Expires”头标的值将通过当前系统时间加上您设定的 time 值来获得。可以设置的参数如下:

    epoch 指定“Expires”的值为 1 January, 1970, 00:00:01 GMT。

    max 指定“Expires”的值为 31 December 2037 23:59:59 GMT,“Cache-Control”的值为10年。

    -1 指定“Expires”的值为 服务器当前时间 -1s,即永远过期。

    负数:Cache-Control: no-cache。

    正数或零:Cache-Control: max-age = #, # 会转换为指定时间的秒数。比如:1d、2h、3m。

    off 表示不修改“Expires”和“Cache-Control”的值。

    比如再看个例子: 控制图片等过期时间为30天

    location ~ \.(gif|jpg|jpeg|png|bmp|ico)$ {
        expires 30d;
    }
    

    我们还可以控制哪一个文件目录的时间,比如控制匹配/resource/或者/mediatorModule/里所有的文件缓存设置到最长时间。

    location ~ /(resource|mediatorModule)/ {
        root    /opt/demo;
        expires max;
    }
    

    2.8 URL重写模块

    重写模块与很多模块一起使用。先看一下是怎么用的,看2个例子,然后我们再一点一点讲每个的使用方法:

    location /download/ {
        if ($forbidden) {
            return   403;
        }
        if ($slow) {
            limit_rate  10k;
        }
        rewrite ^/(download/.*)/media/(.*)\..*$  /$1/mp3/$2.mp3 break;
        ......
    }
    
    location / {
        root   html;
        index  index.html index.htm;
        rewrite ^/bbs/(.*)$ http://192.168.18.201/forum/$1;
    }
    

    上面2个例子就是利用rewrite来完成URL重写的。我们慢慢来看它的用法。

    1. break

      break和编程语言中的用法一样,就是跳出某个逻辑。

      语法:break

      默认值:none

      使用字段:server, location, if

      if (!-f $request_filename) {
        break;
      }
      

      上面这个例子就是在if里面使用break,意思是如果访问的文件名不存在,就跳出。后续会有更多的例子。

    2. if

      if 判断一个条件,如果条件成立,则后面的大括号内的语句将执行,相关配置从上级继承。

      语法:if (condition) { … }

      默认值:none

      使用字段:server, location

      可以在判断语句中指定下列值:

      一个变量的名称;不成立的值为:空字符传”“或者一些用“0”开始的字符串。

      一个使用=或者!=运算符的比较语句。

      使用符号~*和~模式匹配的正则表达式:

      ~为区分大小写的匹配。

      ~*不区分大小写的匹配(firefox匹配FireFox)。

      !~和!~*意为“不匹配的”。

      使用-f和!-f检查一个文件是否存在。

      使用-d和!-d检查一个目录是否存在。

      使用-e和!-e检查一个文件,目录或者软链接是否存在。

      使用-x和!-x检查一个文件是否为可执行文件。

      $http_user_agent变量获取浏览器的agent,使用~ 来匹配大小写。用户如果使用的IE 浏览器,就执行if里面的操作。

      if ($http_user_agent ~ MSIE) {
        rewrite  ^(.*)$  /msie/$1  break;
      }
      

      $request_method变量获取请求的方法,使用=来判断是否等于POST 。如果复合,就执行if 里面的操作。

      if ($request_method = POST ) {
        return 405;
      }
      

      $request_filename变量获取请求的文件名,使用!-f来匹配文件,如果不是一个文件名,就执行if 里面的逻辑。

      if (!-f $request_filename) {
        break;
        proxy_pass  http://127.0.0.1;
      }
      
    3. return

      这个指令结束执行配置语句并为客户端返回状态代码,可以使用下列的值:204,400,402-406,408,410, 411, 413, 416与500-504。此外,非标准代码444将关闭连接并且不发送任何的头部。

      语法:return code

      默认值:none

      使用字段:server, location, if

    4. rewrite

      语法:rewrite regex replacement flag

      默认值:none

      使用字段:server, location, if

      rewrite用来重写url,有3个位置:

      regex 表示用来匹配的正则

      replacement 表示用来替换的

      flag 是尾部的标记

      flag可以是以下的值:

      last - url重写后,马上发起一个新的请求,再次进入server块,重试location匹配,超过10次匹配不到报500错误,地址栏url不变

      break - url重写后,直接使用当前资源,不再执行location里余下的语句,完成本次请求,地址栏url不变

      redirect - 返回302临时重定向,url会跳转,爬虫不会更新url。

      permanent - 返回301永久重定向。url会跳转。爬虫会更新url。

      为空 - URL 不会变,但是内容已经变化,也是永久性的重定向。

      上面的正则表达式的一部分可以用圆括号,方便之后按照顺序用$1-$9来引用。

      我们来看几个例子:

      需要将/photos/123456重写成/path/to/photos/12/1234/123456.png

      可以这样:

      rewrite  "/photos/([0-9] {2})([0-9] {2})([0-9] {2})" /path/to/photos/$1/$1$2/$1$2$3.png;
      

      下面是一些简单的常见的重写:

      rewrite ^/js/base.core.v3.js /js/base.core.v3.dev.js redirect;
      rewrite ^/js/comment.frame.js /js/comment.frame.dev.js redirect;
      rewrite ^/live-static/(.*)$ http://live.bilibili.com/public/$1 last;
      

    2.9 配置整理

    在此记录下Nginx服务器nginx.conf的配置文件说明, 部分注释收集与网络:

    # 运行用户
    user www-data;    
    # 启动进程,通常设置成和cpu的数量相等
    worker_processes  1;
    
    # 全局错误日志及PID文件
    error_log  /var/log/nginx/error.log;
    pid        /var/run/nginx.pid;
    
    # 工作模式及连接数上限
    events {
        use epoll; #epoll是多路复用IO(I/O Multiplexing)中的一种方式,但是仅用于linux2.6以上内核,可以大大提高nginx的性能
        worker_connections 1024; #单个后台worker process进程的最大并发链接数
        # multi_accept on; 
    }
    
    #设定http服务器,利用它的反向代理功能提供负载均衡支持
    http {
        #设定mime类型,类型由mime.type文件定义
        include       /etc/nginx/mime.types;
        default_type  application/octet-stream;
        #设定日志格式
        access_log    /var/log/nginx/access.log;
    
        #sendfile 指令指定 nginx 是否调用 sendfile 函数(zero copy 方式)来输出文件,对于普通应用,
        #必须设为 on,如果用来进行下载等应用磁盘IO重负载应用,可设置为 off,以平衡磁盘与网络I/O处理速度,降低系统的uptime.
        sendfile        on;
        #将tcp_nopush和tcp_nodelay两个指令设置为on用于防止网络阻塞
        tcp_nopush      on;
        tcp_nodelay     on;
        #连接超时时间
        keepalive_timeout  65;
    
        #开启gzip压缩
        gzip  on;
        gzip_disable "MSIE [1-6]\.(?!.*SV1)";
    
        #设定请求缓冲
        client_header_buffer_size    1k;
        large_client_header_buffers  4 4k;
    
        include /etc/nginx/conf.d/*.conf;
        include /etc/nginx/sites-enabled/*;
    
        #设定负载均衡的服务器列表
        upstream mysvr {
            #weigth参数表示权值,权值越高被分配到的几率越大
            #本机上的Squid开启3128端口
            server 192.168.8.1:3128 weight=5;
            server 192.168.8.2:80  weight=1;
            server 192.168.8.3:80  weight=6;
        }
    
    
        server {
            #侦听80端口
            listen       80;
            #定义使用www.xx.com访问
            server_name  www.xx.com;
    
            #设定本虚拟主机的访问日志
            access_log  logs/www.xx.com.access.log  main;
    
            #默认请求
            location / {
                root   /root;      #定义服务器的默认网站根目录位置
                index index.php index.html index.htm;   #定义首页索引文件的名称
    
                fastcgi_pass  www.xx.com;
                fastcgi_param  SCRIPT_FILENAME  $document_root/$fastcgi_script_name; 
                include /etc/nginx/fastcgi_params;
            }
    
            # 定义错误提示页面
            error_page   500 502 503 504 /50x.html;  
                location = /50x.html {
                root   /root;
            }
    
            #静态文件,nginx自己处理
            location ~ ^/(images|javascript|js|css|flash|media|static)/ {
                root /var/www/virtual/htdocs;
                #过期30天,静态文件不怎么更新,过期可以设大一点,如果频繁更新,则可以设置得小一点。
                expires 30d;
            }
            #PHP 脚本请求全部转发到 FastCGI处理. 使用FastCGI默认配置.
            location ~ \.php$ {
                root /root;
                fastcgi_pass 127.0.0.1:9000;
                fastcgi_index index.php;
                fastcgi_param SCRIPT_FILENAME /home/www/www$fastcgi_script_name;
                include fastcgi_params;
            }
            #设定查看Nginx状态的地址
            location /NginxStatus {
                stub_status            on;
                access_log              on;
                auth_basic              "NginxStatus";
                auth_basic_user_file  conf/htpasswd;
            }
            #禁止访问 .htxxx 文件
            location ~ /\.ht {
                deny all;
            }
    
        }
    
        #第一个虚拟服务器
        server {
            #侦听192.168.8.x的80端口
            listen       80;
            server_name  192.168.8.x;
    
            #对aspx后缀的进行负载均衡请求
            location ~ .*\.aspx$ {
                root   /root;#定义服务器的默认网站根目录位置
                index index.php index.html index.htm;#定义首页索引文件的名称
    
                proxy_pass  http://mysvr;#请求转向mysvr 定义的服务器列表
    
                #以下是一些反向代理的配置可删除.
                proxy_redirect off;
    
                #后端的Web服务器可以通过X-Forwarded-For获取用户真实IP
                proxy_set_header Host $host;
                proxy_set_header X-Real-IP $remote_addr;
                proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
                client_max_body_size 10m;    #允许客户端请求的最大单文件字节数
                client_body_buffer_size 128k;  #缓冲区代理缓冲用户端请求的最大字节数,
                proxy_connect_timeout 90;  #nginx跟后端服务器连接超时时间(代理连接超时)
                proxy_send_timeout 90;        #后端服务器数据回传时间(代理发送超时)
                proxy_read_timeout 90;         #连接成功后,后端服务器响应时间(代理接收超时)
                proxy_buffer_size 4k;             #设置代理服务器(nginx)保存用户头信息的缓冲区大小
                proxy_buffers 4 32k;               #proxy_buffers缓冲区,网页平均在32k以下的话,这样设置
                proxy_busy_buffers_size 64k;    #高负荷下缓冲大小(proxy_buffers*2)
                proxy_temp_file_write_size 64k;  #设定缓存文件夹大小,大于这个值,将从upstream服务器传
            }
        }
    }
    

    3 Nginx模块

    上面我们已经详细讲解了Nginx常用配置,从中我们已经体会到了,Nginx模块化配置的优点。其中,模块化设计类似于面向对象中的接口类,它增强了nginx源码的可读性、可扩充性和可维护性。

    所以,Nginx有五大优点:模块化、事件驱动、异步、非阻塞、多进程单线程。由内核和模块组成的,其中内核完成的工作比较简单, 仅仅通过查找配置文件将客户端请求映射到一个location block,然后又将这个location block中所配置的每个指令将会启动不同的模块去完成相应的工作。

    3.1 模块划分

    Nginx的模块从结构上分为核心模块、基础模块和第三方模块:

    核心模块:HTTP模块、EVENT模块和MAIL模块

    基础模块:HTTP Access模块、HTTP FastCGI模块、HTTP Proxy模块和HTTP Rewrite模块,

    第三方模块:HTTP Upstream Request Hash模块、Notice模块和HTTP Access Key模块。

    Nginx的模块从功能上分为如下三类:

    Core(核心模块):构建nginx基础服务、管理其他模块。

    Handlers(处理器模块):此类模块直接处理请求,并进行输出内容和修改headers信息等操作。Handlers处理器模块一般只能有一个。

    Filters (过滤器模块):此类模块主要对其他处理器模块输出的内容进行修改操作,最后由Nginx输出。

    Proxies (代理类模块):此类模块是Nginx的HTTP Upstream之类的模块,这些模块主要与后端一些服务比如FastCGI等进行交互,实现服务代理和负载均衡等功能。

    Nginx的核心模块主要负责建立nginx服务模型、管理网络层和应用层协议、以及启动针对特定应用的一系列候选模块。其他模块负责分配给web服务器的实际工作:

    (1) 当Nginx发送文件或者转发请求到其他服务器,由Handlers(处理模块)或Proxies(代理类模块)提供服务;

    (2) 当需要Nginx把输出压缩或者在服务端加一些东西,由Filters(过滤模块)提供服务。

    3.2 模块处理

    1. 当服务器启动,每个handlers(处理模块)都有机会映射到配置文件中定义的特定位置(location);如果有多个handlers(处理模块)映射到特定位置时,只有一个会“赢”(说明配置文件有冲突项,应该避免发生)。

      处理模块以三种形式返回:

      OK

      ERROR

      或者放弃处理这个请求而让默认处理模块来处理(主要是用来处理一些静态文件,事实上如果是位置正确而真实的静态文件,默认的处理模块会抢先处理)。

    2. 如果handlers(处理模块)把请求反向代理到后端的服务器,就变成另外一类的模块:load-balancers(负载均衡模块)。负载均衡模块的配置中有一组后端服务器,当一个HTTP请求过来时,它决定哪台服务器应当获得这个请求。

      Nginx的负载均衡模块采用两种方法:

      轮转法,它处理请求就像纸牌游戏一样从头到尾分发;

      IP哈希法,在众多请求的情况下,它确保来自同一个IP的请求会分发到相同的后端服务器。

    3. 如果handlers(处理模块)没有产生错误,filters(过滤模块)将被调用。多个filters(过滤模块)能映射到每个位置,所以(比如)每个请求都可以被压缩成块。它们的执行顺序在编译时决定。

      filters(过滤模块)是经典的“接力链表(CHAIN OF RESPONSIBILITY)”模型:一个filters(过滤模块)被调用,完成其工作,然后调用下一个filters(过滤模块),直到最后一个filters(过滤模块)。

      过滤模块链的特别之处在于:

      每个filters(过滤模块)不会等上一个filters(过滤模块)全部完成;

      它能把前一个过滤模块的输出作为其处理内容;有点像Unix中的流水线。

      过滤模块能以buffer(缓冲区)为单位进行操作,这些buffer一般都是一页(4K)大小,当然你也可以在nginx.conf文件中进行配置。这意味着,比如,模块可以压缩来自后端服务器的响应,然后像流一样的到达客户端,直到整个响应发送完成。

      总之,过滤模块链以流水线的方式高效率地向客户端发送响应信息。

    4. 所以总结下上面的内容,一个典型的HTTP处理周期是这样的:

      客户端发送HTTP请求 –>

      Nginx基于配置文件中的位置选择一个合适的处理模块 ->

      (如果有)负载均衡模块选择一台后端服务器 –>

      处理模块进行处理并把输出缓冲放到第一个过滤模块上 –>

      第一个过滤模块处理后输出给第二个过滤模块 –>

      然后第二个过滤模块又到第三个 –>

      依此类推 –> 最后把响应发给客户端。

      下图展示了nginx模块处理流程:

      输入图片说明

      Nginx本身做的工作实际很少,当它接到一个HTTP请求时,它仅仅是通过查找配置文件将此次请求映射到一个location block,而此location中所配置的各个指令则会启动不同的模块去完成工作,因此模块可以看做Nginx真正的劳动工作者。通常一个location中的指令会涉及一个handler模块和多个filter模块(当然,多个location可以复用同一个模块)。handler模块负责处理请求,完成响应内容的生成,而filter模块对响应内容进行处理

    4 Nginx请求处理

    Nginx在启动时会以daemon形式在后台运行,采用多进程+异步非阻塞IO事件模型来处理各种连接请求。多进程模型包括一个master进程,多个worker进程,一般worker进程个数是根据服务器CPU核数来决定的master进程负责管理Nginx本身和其他worker进程。如下图:

    从上图中可以很明显地看到,4个worker进程的父进程都是master进程,表明worker进程都是从父进程fork出来的,并且父进程的ppid为1,表示其为daemon进程。

    需要说明的是,在nginx多进程中,每个worker都是平等的,因此每个进程处理外部请求的机会权重都是一致的。

    Nginx架构及工作流程图:

    输入图片说明

    Nginx的每一个Worker进程都管理着大量的线程,真正处理请求业务的是Worker之下的线程。worker进程中有一个ngx_worker_process_cycle()函数,执行无限循环,不断处理收到的来自客户端的请求,并进行处理,直到整个Nginx服务被停止。

    worker 进程中,ngx_worker_process_cycle()函数就是这个无限循环的处理函数。在这个函数中,一个请求的简单处理流程如下:

    1. 操作系统提供的机制(例如 epoll, kqueue 等)产生相关的事件。

    2. 接收和处理这些事件,如是接收到数据,则产生更高层的 request 对象。

    3. 处理 request 的 header 和 body。

    4. 产生响应,并发送回客户端。

    5. 完成 request 的处理。

    6. 重新初始化定时器及其他事件。

    4.1 多进程处理模型

    下面来介绍一个请求进来,多进程模型的处理方式:

    首先,master进程一开始就会根据我们的配置,来建立需要listen的网络socket fd,然后fork出多个worker进程。

    其次,根据进程的特性,新建立的worker进程,也会和master进程一样,具有相同的设置。因此,其也会去监听相同ip端口的套接字socket fd

    然后,这个时候有多个worker进程都在监听同样设置的socket fd,意味着当有一个请求进来的时候,所有的worker都会感知到。这样就会产生所谓的“惊群现象”。为了保证只会有一个进程成功注册到listenfd的读事件,nginx中实现了一个“accept_mutex”类似互斥锁,只有获取到这个锁的进程,才可以去注册读事件。其他进程全部accept 失败。

    最后,监听成功的worker进程,读取请求,解析处理,响应数据返回给客户端,断开连接,结束。因此,一个request请求,只需要worker进程就可以完成。

    进程模型的处理方式带来的一些好处就是:进程之间是独立的,也就是一个worker进程出现异常退出,其他worker进程是不会受到影响的;此外,独立进程也会避免一些不需要的锁操作,这样子会提高处理效率,并且开发调试也更容易。

    如前文所述,多进程模型+异步非阻塞模型才是胜出的方案。单纯的多进程模型会导致连接并发数量的降低,而采用异步非阻塞IO模型很好的解决了这个问题;并且还因此避免的多线程的上下文切换导致的性能损失。

    worker进程会竞争监听客户端的连接请求:这种方式可能会带来一个问题,就是可能所有的请求都被一个worker进程给竞争获取了,导致其他进程都比较空闲,而某一个进程会处于忙碌的状态,这种状态可能还会导致无法及时响应连接而丢弃discard掉本有能力处理的请求。这种不公平的现象,是需要避免的,尤其是在高可靠web服务器环境下。

    针对这种现象,Nginx采用了一个是否打开acceptmutex选项的值,ngx_accept_disabled标识控制一个worker进程是否需要去竞争获取accept_mutex选项,进而获取accept事件_。

    ngx_accept_disabled值,nginx单进程的所有连接总数的八分之一,减去剩下的空闲连接数量,得到的这个ngx_accept_disabled。

    当ngxaccept_disabled大于0时,不会去尝试获取accept_mutex锁,并且将 ngx_accept_disabled减1,于是,每次执行到此处时,都会去减1,直到小于0。不去获取accept_mutex锁,就是等于让出获取 连接的机会,很显然可以看出,当空闲连接越少时,ngx_accept_disable越大,于是让出的机会就越多,这样其它进程获取锁的机会也就越大_。不去accept,自己的连接就控制下来了,其它进程的连接池就会得到利用,这样,nginx就控制了多进程间连接的平衡了。

    4.2 一个简单的HTTP请求

    从 Nginx 的内部来看,一个 HTTP Request 的处理过程涉及到以下几个阶段:

    初始化 HTTP Request(读取来自客户端的数据,生成 HTTP Request 对象,该对象含有该请求所有的信息)。

    处理请求头。

    处理请求体。

    如果有的话,调用与此请求(URL 或者 Location)关联的 handler。

    依次调用各 phase handler 进行处理。

    在建立连接过程中,对于nginx监听到的每个客户端连接,都会将它的读事件的handler设置为ngx_http_init_request函数,这个函数就是请求处理的入口。在处理请求时,主要就是要解析http请求,比如:uri,请求行等,然后再根据请求生成响应。下面看一下nginx处理的具体过程:

    在这里,我们需要了解一下 phase handler 这个概念。phase 字面的意思,就是阶段。所以 phase handlers 也就好理解了,就是包含若干个处理阶段的一些 handler

    在每一个阶段,包含有若干个 handler,再处理到某个阶段的时候,依次调用该阶段的 handler 对 HTTP Request 进行处理。

    通常情况下,一个 phase handler 对这个 request 进行处理,并产生一些输出。通常 phase handler 是与定义在配置文件中的某个 location 相关联的

    一个 phase handler 通常执行以下几项任务:

    获取 location 配置。

    产生适当的响应。

    发送 response header。

    发送 response body。

    当 Nginx 读取到一个 HTTP Request 的 header 的时候,Nginx 首先查找与这个请求关联的虚拟主机的配置。如果找到了这个虚拟主机的配置,那么通常情况下,这个 HTTP Request 将会经过以下几个阶段的处理(phase handlers):

    NGX_HTTP_POST_READ_PHASE: 读取请求内容阶段

    NGX_HTTP_SERVER_REWRITE_PHASE: Server 请求地址重写阶段

    NGX_HTTP_FIND_CONFIG_PHASE: 配置查找阶段

    NGX_HTTP_REWRITE_PHASE: Location请求地址重写阶段

    NGX_HTTP_POST_REWRITE_PHASE: 请求地址重写提交阶段

    NGX_HTTP_PREACCESS_PHASE: 访问权限检查准备阶段

    NGX_HTTP_ACCESS_PHASE: 访问权限检查阶段

    NGX_HTTP_POST_ACCESS_PHASE: 访问权限检查提交阶段

    NGX_HTTP_TRY_FILES_PHASE: 配置项 try_files 处理阶段

    NGX_HTTP_CONTENT_PHASE: 内容产生阶段

    NGX_HTTP_LOG_PHASE: 日志模块处理阶段

    在内容产生阶段,为了给一个 request 产生正确的响应,Nginx 必须把这个 request 交给一个合适的 content handler 去处理。 如果这个 request 对应的 location 在配置文件中被明确指定了一个 content handler,那么Nginx 就可以通过对 location 的匹配,直接找到这个对应的 handler,并把这个 request 交给这个 content handler 去处理。这样的配置指令包括像,perl,flv,proxy_pass,mp4等。

    如果一个 request 对应的 location 并没有直接有配置的 content handler,那么 Nginx 依次尝试:

    如果一个 location 里面有配置 random_index on,那么随机选择一个文件,发送给客户端。

    如果一个 location 里面有配置 index 指令,那么发送 index 指令指明的文件,给客户端。

    如果一个 location 里面有配置 autoindex on,那么就发送请求地址对应的服务端路径下的文件列表给客户端。

    如果这个 request 对应的 location 上有设置 gzip_static on,那么就查找是否有对应的.gz文件存在,有的话,就发送这个给客户端(客户端支持 gzip 的情况下)。

    请求的 URI 如果对应一个静态文件,static module 就发送静态文件的内容到客户端。

    内容产生阶段完成以后,生成的输出会被传递到 filter 模块去进行处理。filter 模块也是与 location 相关的。所有的 fiter 模块都被组织成一条链。输出会依次穿越所有的 filter,直到有一个 filter 模块的返回值表明已经处理完成。

    这里列举几个常见的 filter 模块,例如:

    server-side includes。

    XSLT filtering。

    图像缩放之类的。

    gzip 压缩。

    在所有的 filter 中,有几个 filter 模块需要关注一下。按照调用的顺序依次说明如下:

    copy: 将一些需要复制的 buf(文件或者内存)重新复制一份然后交给剩余的 body filter 处理。

    postpone: 这个 filter 是负责 subrequest 的,也就是子请求的。

    write: 写输出到客户端,实际上是写到连接对应的 socket 上。

    4.3 请求完整处理过程

    根据以上请求步骤所述,请求完整的处理过程如下图所示:

    输入图片说明

    本帖子中包含更多资源

    您需要 登录 才可以下载或查看,没有帐号?立即注册

    x
    守望者AIR技术交流社区(www.airmyth.com)
    回复 支持 反对

    使用道具 举报

  • TA的每日心情
    擦汗
    7 天前
  • 签到天数: 431 天

    [LV.9]以坛为家II

    1738

    主题

    2089

    帖子

    12万

    积分

    超级版主

    Rank: 18Rank: 18Rank: 18Rank: 18Rank: 18

    威望
    447
    贡献
    16
    金币
    50896
    钢镚
    1418

    开源英雄守望者

     楼主| 发表于 2016-7-27 14:42:36 | 显示全部楼层
    摘要
    本篇文章将给大家讲述Servlet容器中请求处理的过程,在给本篇文章起标题时,一直在“应用服务 器”与”Servlet容器“这两者之间拿捏不定,主要是因为要清晰的区分开这两者的关系:Servlet容器可以说是应用服务器的一个子集。又由于本文 的初衷是讲述大家平常使用比较多的Servlet为主,所以,给本篇就起了《Servlet容器请求处理》的名字。

    0 系列目录

    本篇文章将给大家讲述Servlet容器中请求处理的过程,在给本篇文章起标题时,一直在“应用服务器”与“Servlet容器”这两者之间拿捏不定,主要是因为要清晰的区分开这两者的关系:Servlet容器可以说是应用服务器的一个子集。又由于本文的初衷是讲述大家平常使用比较多的Servlet为主,所以,给本篇就起了《Servlet容器请求处理》的名字。

    先说下在整个WEB请求处理过程中,本篇文章讲述的是哪个流程模块。为直观明了,先上一张图,红色部分为本章所述模块:

    输入图片说明

    所讲述的请求流程模块,大家已经很清楚了。那怎么给大家去讲的更清晰,大家理解的更容易呢?当然是,带着问题去学习,吸收或许会更快些啦。:)

    开篇之前,给大家提以下几个问题,这些问题是本文的主体思路(也是个人学习路线):

    1. WEB服务器那么多,Apache、Tomcat、Nginx、Jetty、Resin,名词那么多,HTTP Server、Application Server、Web Server、Servlet Container,他们是什么?之间关系是什么?区别又在哪?

    2. CGI、WSGI、Servlet、JSP、FastCGI等等,他们是什么?他们之间区别又在哪?和上面WEB服务器之间关系是什么?

    3. Servlet生命周期及工作原理是什么?

    4. HTTP Request进入到Tomcat中执行,请求处理流程如何?如何找到对应的Application并进行请求处理?

    1 WEB服务器

    只要Web上的Server都叫Web Server,但是大家分工不同,解决的问题也不同,所以根据Web Server提供的功能,每个Web Server的名字也会不一样

    按功能分类,Web Server可以分为:

    |- Web Server
            |- Http Server
            |- Application Server
                |- Servlet Container
                |- CGI Server
                |- ......
    

    1.1 Http Server

    HTTP Server本质上也是一种应用程序——它通常运行在服务器之上,绑定服务器的IP地址并监听某一个tcp端口来接收并处理HTTP请求,这样客户端(一般来说是IE, Firefox,Chrome这样的浏览器)就能够通过HTTP协议来获取服务器上的网页(HTML格式)、文档(PDF格式)、音频(MP4格式)、视频(MOV格式)等等资源。下图描述的就是这一过程:

    HTTP Server

    一个HTTP Server关心的是HTTP协议层面的传输和访问控制,所以在Apache/Nginx上你可以看到代理、负载均衡等功能。

    1. 客户端通过HTTP Server访问服务器上存储的静态资源(HTML文件、图片文件等等)。
    2. 通过CGI/Servlet技术,也可以将处理过的动态内容通过HTTP Server分发,但是一个HTTP Server始终只是把服务器上的文件如实的通过HTTP协议传输给客户端。

    HTTP Server中经常使用的是Apache、Nginx两种,HTTP Server主要用来做静态内容服务、代理服务器、负载均衡等。直面外来请求转发给后面的应用服务(Tomcat,django什么的)。

    |- Http Server
        |- Apache
        |- Nginx
    

    1.1.1 Apache HTTP服务器

    Apache HTTP服务器是一个模块化的服务器,可以运行在几乎所有广泛使用的计算机平台上。Apache支持模块多,性能稳定,Apache本身是静态解析,适合静态HTML、图片等,但可以通过扩展脚本、模块等支持动态页面等。

    Apache可以支持PHPcgiperl,但是要使用Java的话,你需要Tomcat在Apache后台支撑,将Java请求由Apache转发给Tomcat处理。

    1.1.2 Nginx HTTP服务器

    Nginx是一个高性能的HTTP和反向代理服务器,同时也是一个IMAP/POP3/SMTP 代理服务器。

    其特点是占有内存少,并发能力强。Nginx代码完全用C语言从头写成。

    具有很高的稳定性。其它HTTP服务器,当遇到访问的峰值,或者有人恶意发起慢速连接时,也很可能会导致服务器物理内存耗尽频繁交换,失去响应,只能重启服务器。例如当前apache一旦上到200个以上进程,web响应速度就明显非常缓慢了。

    而Nginx采取了分阶段资源分配技术,使得它的CPU与内存占用率非常低。Nginx官方表示保持10000个没有活动的连接,它只占2.5M内存,所以类似DOS这样的攻击对nginx来说基本上是毫无用处的。就稳定性而言,Nginx比Lighthttpd更胜一筹。

    1.1.3 Nginx与Apache比较

    Nginx相对于Apache的优点:

    1. 轻量级,同样启动WEB服务,比Apache占用更少的内存以及资源;
    2. 抗并发性能高,核心区别在于Apache是同步多进程模型,一个连接对应一个进程Nginx是异步的,多个连接(万级别)可以对应一个进程
    3. Nginx模块较少,配置简单,所以Nginx可以将资源用在数据处理以及进程上面,Apache模块较多比较全,相对稳定,但在内存资源上消耗比较大;
    4. Nginx可以在不间断的情况下进行软件版本的升级
    5. Nginx处理静态页面性能比apache高3倍多

    选择高并发高性能就选择Nginx,如果要稳定,选择Apache,主要根据服务器要面临的需求而定。

    当然,两者也可以组合使用:

    1. Nginx放前端+apache放后端+MYSQL+PHP:可以提高服务器负载能力
    2. Nginx处理静态页面请求如MP3,GIF.JPG.JS,apache处理动态页面请求,充分结合了二者的优势;

    1.2 Application Server

    Application Server 是一个应用执行的服务器。它首先需要支持开发语言的 Runtime(对于 Tomcat 来说,就是 Java),保证应用能够在应用服务器上正常运行。其次,需要支持应用相关的规范,例如类库、安全方面的特性。与HTTP Server相比,Application Server能够动态的生成资源并返回到客户端。

    |- Application Server
        |- Tomcat
        |- Jetty
    

    当初在Apache Server开发时还未出现Servlet的概念,所以Apache不能内置支持Servlet。实际上,除了Apache,其他许多HTTP Server软件都不能直接支持Servlet。为了支持Servlet,通常要单独开发程序,这种程序一般称为服务器小程序容器(Servlet Container),有时也叫做服务器小程序引擎(Servlet Engine)。它是Web服务器或应用程序服务器的一部分,用于在发送的请求和响应之上提供网络服务,解码基于MIME的请求,格式化基于MIME的响应,它在Servlet的生命周期内包容和管理Servlet,是一个实时运行的外壳程序。运行时由Web服务器软件处理一般请求,并把Servlet调用传递给“容器”来处理。

    比如,对于 Tomcat 来说,就是需要提供 JSP/Sevlet 运行需要的标准类库、Interface 等。为了方便,应用服务器往往也会集成 HTTP Server 的功能,但是不如专业的 HTTP Server 那么强大,所以Application Server往往是运行在 HTTP Server 的背后,执行应用,将动态的内容转化为静态的内容之后,通过 HTTP Server 分发到客户端

    输入图片说明

    Tomcat运行在JVM之上,它和HTTP服务器一样,绑定IP地址并监听TCP端口,同时还包含以下指责:

    1. 管理Servlet程序的生命周期;
    2. 将URL映射到指定的Servlet进行处理;
    3. 与Servlet程序合作处理HTTP请求——根据HTTP请求生成HttpServletRequest/Response对象并传递给Servlet进行处理,将Servlet中的HttpServletResponse对象生成的内容返回给浏览器;

    所以 Tomcat 属于是一个「Application Server」,但是更准确的来说,是一个「Servlet/JSP」应用的容器(Ruby/Python 等其他语言开发的应用也无法直接运行在 Tomcat 上)。

    1.2.1 Servlet容器工作模式

    按照工作模式的不同,Servlet容器可以分为以下3类:

    1. 独立运行的Servlet容器

      在这种模式下,Servlet容器作为构成Web服务器的一部分而存在。当使用基于Java的Web服务器时,就属于这种情况。这种方式是Tomcat的默认模式,然而大多数Web服务器并不是基于Java的,所以就产生了下面的两种其他类型。

    2. 内置的Servlet容器

      Servlet容器由Web服务器插件和Java容器两部分组成。采用这种方式时,Web服务器插件需要在某个Web服务器内部地址空间中打开一个JVM(Java虚拟机),在此JVM上加载Java容器并运行Servlet。 如果客户端调用Servlet,Web服务器插件首先获得此请求的控制并将它传递(使用JNI技术)给Java容器,然后Java容器把此请求交给 Servlet来处理。这种方式运行速度较快,并且能够提供良好的性能,适用于单进程、多线程服务器,但是在伸缩性方面存在不足。

    3. 外置的Servlet容器

      采用这种方式时,Servlet容器运行在Web服务器外部地址空间。先由Web服务器插件在某个Web服务器外部地址空间打开一个JVM(Java虚拟机),然后加载Java容器来运行Servlet。 Web服务器插件和JVM之间使用IPC(进程间通信)机制(通常是TCP/IPSockets)。如果客户端调用Servlet,Web服务器插件首先 获得此请求的控制并将它传递(使用IPC技术)给Java容器,然后Java容器把此请求交给Servlet来处理。这种方式对客户端请求的处理速度不如 内置Servlet那样快,但是在其他方面(如可伸缩性、稳定性等)具有优势。

    Tomcat属于Servlet容器,其工作模式也分为上述3种,所以Tomcat既可被用作独立运行的Servlet引擎(便于开发和调试),又可作为一个需要增强功能的Web服务器(如当前的Apache、IIS和Netscape服务器)插件。在配置Tomcat之前,就需要确定采用哪种工作模式,工作模式(1)比较简单,直接安装Tomcat即可,工作模式(2)和(3)有些复杂,除了安装Tomcat、Web服务器之外,还需要安装连接两者的中间连接件。

    1.2.2 Apache与Tomcat整合使用

    虽然Tomcat也可以认为是HTTP服务器,但通常它仍然会和Apache/Nginx配合在一起使用:

    1. 动静态资源分离——运用Nginx的反向代理功能分发请求:所有动态资源的请求交给Tomcat,而静态资源的请求(例如图片、视频、CSS、JavaScript文件等)则直接由Nginx返回到浏览器,这样能大大减轻Tomcat的压力;

    2. 负载均衡——当业务压力增大时,可能一个Tomcat的实例不足以处理,那么这时可以启动多个Tomcat实例进行水平扩展,而Nginx的负载均衡功能可以把请求通过算法分发到各个不同的实例进行处理;

    整合的好处:

    1. 如果客户端请求的是静态页面,则只需要Apache服务器响应请求。
    2. 如果客户端请求动态页面,则是Tomcat服务器响应请求。
    3. 因为JSP是服务器端解释代码的,这样整合就可以减少Tomcat的服务开销。

    2 什么是CGI

    如上文所述,HTTP服务器是一个很简单的东西,并不负责动态网页的构建,只能转发静态网页。事物总是不断发展,网站也越来越复杂,所以出现动态技术。同时Apache也说,它能支持perl,生成动态网页。这个支持perl,其实是Apache越位了,做了一件额外的事情。

    既然HTTP Server自己不能做,外包给别人吧,但是要与第三做个约定,我给你什么,然后你给我什么,就是握把请求参数发送给你,然后我接收你的处理结果给客户端。那这个约定就是Common Gateway Interface,简称CGI。

    CGI全称是“通用网关接口”(Common Gateway Interface),是HTTP服务器与你的或其它机器上的程序进行“交谈”的一种工具,其程序须运行在网络服务器上,是一种根据请求信息动态产生响应内容的接口协议。CGI可以用任何一种语言编写,只要这种语言具有标准输入、输出和环境变量。如php,perl,tcl等。

    通过CGI,HTTP Server可以将根据请求不同启动不同的外部程序,并将请求内容转发给该程序,在程序执行结束后,将执行结果作为回应返回给客户端。 也就是说,对于每个请求,都要产生一个新的进程进行处理。因为每个进程都会占有很多服务器的资源和时间,这就导致服务器无法同时处理很多的并发请求。另外 CGI程序都是与操作系统平台相关的,虽然在互联网爆发的初期,CGI为开发互联网应用做出了很大的贡献,但是随着技术的发展,开始逐渐衰落。

    所以,CGI的定义是:外部应用程序与HTTP 服务器之间的接口协议。

    2.1 CGI工作原理

    HTTP Server与CGI程序请求处理流程:

    输入图片说明

    HTTP服务器将根据CGI程序的类型决定数据向CGI程序的传送方式,一般来讲是通过标准输入/输出流和环境变量来与CGI程序间传递数据。 如下图所示:

    输入图片说明

    CGI程序通过标准输入(STDIN)和标准输出(STDOUT)来进行输入输出。此外CGI程序还通过环境变 量来得到输入,操作系统提供了许多环境变量,它们定义了程序的执行环境,应用程序可以存取它们。HTTP服务器和CGI接口又另外设置了一些环境变量,用 来向CGI程序传递一些重要的参数。CGI的GET方法还通过环境变量QUERY-STRING向CGI程序传递Form中的数据。

    2.2 CGI环境变量

    下面是一些常用的CGI环境变量:

    输入图片说明

    每当客户请求CGI的时候,HTTP服务器就请求操作系统生成一个新的CGI解释器进程(如php-cgi.exe),CGI的一个进程则处理完一 个请求后退出,下一个请求来时再创建新进程。当然,这样在访问量很少没有并发的情况也行。可是当访问量增大,并发存在,这种方式就不适合了。于是就有了FastCGI。

    3 什么是FastCGI

    FastCGI像是一个常驻(long-live)型的CGI,它可以一直执行着,只要激活后,不会每次都要花费时间去fork一次(这是CGI最 为人诟病的fork-and-execute 模式)。它还支持分布式的运算, 即 FastCGI 程序可以在网站服务器以外的主机上执行并且接受来自其它网站服务器来的请求。

    FastCGI是语言无关的、可伸缩架构的CGI开放扩展,其主要行为是将CGI解释器进程保持在内存中并因此获得较高的性能。众所周知,CGI解释器的反复加载是CGI性能低下的主要原因,如果CGI解释器保持在内存中并接受FastCGI进程管理器调度,则可以提供良好的性能、伸缩性、Fail- Over特性等等。

    3.1 FastCGI工作原理

    1. HTTP Server启动时载入FastCGI进程管理器(IIS ISAPI或Apache Module);
    2. FastCGI进程管理器自身初始化,启动多个CGI解释器进程(可见多个php-cgi)并等待来自HTTP Server的连接;
    3. 当客户端请求到达HTTP Server时,FastCGI进程管理器选择并连接到一个CGI解释器。HTTP Server将CGI环境变量和标准输入发送到FastCGI子进程php-cgi;
    4. FastCGI子进程完成处理后将标准输出和错误信息从同一连接返回HTTP Server。当FastCGI子进程关闭连接时,请求便告处理完成。FastCGI子进程接着等待并处理来自FastCGI进程管理器(运行在HTTP Server中)的下一个连接。在CGI模式中,php-cgi在此便退出了。

    在上述情况中,你可以想象CGI通常有多慢。每一个Web请求PHP都必须重新解析php.ini、重新载入全部扩展并重初始化全部数据结构。使用 FastCGI,所有这些都只在进程启动时发生一次。一个额外的好处是,持续数据库连接(Persistent database connection)可以工作。

    3.2 FastCGI与CGI特点

    1. 如CGI,FastCGI也具有语言无关性;
    2. 如CGI,FastCGI在进程中的应用程序,独立于核心web服务器运行,提供了一个比API更安全的环境。(API把应用程序的代码与核心的 web服务器链接在一起,这意味着在一个错误的API的应用程序可能会损坏其他应用程序或核心服务器; 恶意的API的应用程序代码甚至可以窃取另一个应用程序或核心服务器的密钥。)
    3. FastCGI技术目前支持语言有:C/C++、Java、Perl、Tcl、Python、SmallTalk、Ruby等。相关模块在Apache, ISS, Lighttpd等流行的服务器上也是可用的。
    4. 如CGI,FastCGI的不依赖于任何Web服务器的内部架构,因此即使服务器技术的变化, FastCGI依然稳定不变。

    4 什么是PHP-CGI

    PHP-CGI是PHP自带的FastCGI管理器。PHP-CGI的不足:

    1. PHP-CGI变更php.ini配置后,需重启PHP-CGI才能让新的php-ini生效,不可以平滑重启;
    2. 直接杀死PHP-CGI进程,php就不能运行了。(PHP-FPM和Spawn-FCGI就没有这个问题,守护进程会平滑从新生成新的子进程。

    5 什么是PHP-FPM

    PHP-FPM是一个PHP FastCGI管理器,是只用于PHP的,使用PHP-FPM来控制PHP-CGI的FastCGI进程,它负责管理一个进程池,来处理来自Web服务器的请求。可以在 http://php-fpm.org/download 下载得到。

    相对Spawn-FCGI,PHP-FPM在CPU和内存方面的控制都更胜一筹,而且前者很容易崩溃,必须用crontab进行监控,而PHP-FPM则没有这种烦恼。

    PHP-FPM提供了更好的PHP进程管理方式,可以有效控制内存和进程、可以平滑重载PHP配置,比Spawn-FCGI具有更多优点,所以被PHP官方收录了。在PHP 5.3.3中可以直接使用PHP-FPM了。

    在./configure的时候带 –enable-fpm参数即可开启PHP-FPM。

    5.1 PHP-FPM工作原理

    Apache+PHP配合使用,会在Apache配置下面一段:

    LoadModule php5_module C:/php/php5apache2_2.dll
    

    当PHP需要在Apache服务器下运行时,一般来说,它可以模块的形式集成,此时模块的作用是接收Apache传递过 来的PHP文件请求,并处理这些请求,然后将处理后的结果返回给Apache。如果我们在Apache启动前在其配置文件中配置好了PHP模块,PHP模 块通过注册apache2的ap_hook_post_config挂钩,在Apache启动的时候启动此模块以接受PHP文件的请求。

    Apache的Hook机制是指:Apache允许模块(包括内部模块和外部模块,例如mod_php5.so,mod_perl.so等)将自定义的函数注入到请求处理循环中。换 句话说,模块可以在Apache的任何一个处理阶段中挂接(Hook)上自己的处理函数,从而参与Apache的请求处理过程。 mod_php5.so/php5apache2.dll就是将所包含的自定义函数,通过Hook机制注入到Apache中,在Apache处理流程的各 个阶段负责处理php请求。

    有人测试Nginx+PHP-FPM在高并发情况下可能会达到Apache+mod_php5的5~10倍,现在Nginx+PHP-FPM使用的人越来越多。

    6 什么是Spawn-FCGI

    Spawn-FCGI是一个通用的FastCGI管理服务器,它是lighttpd中的一部份,很多人都用Lighttpd的Spawn-FCGI 进行FastCGI模式下的管理工作,不过有不少缺点。而PHP-FPM的出现多少缓解了一些问题,但PHP-FPM有个缺点就是要重新编译,这对于一些 已经运行的环境可能有不小的风险(refer)。

    Spawn-FCGI目前已经独成为一个项目,更加稳定一些,也给很多Web 站点的配置带来便利。已经有不少站点将它与nginx搭配来解决动态网页。

    6.1 PHP-FPM与Spawn-CGI对比

    PHP-FPM、Spawn-FCGI都是守护PHP-CGI的进程管理器。

    PHP-FPM的使用非常方便,配置都是在PHP-FPM.ini的文件内,而启动、重启都可以从php/sbin/PHP-FPM中进行。更方便 的是修改php.ini后可以直接使用PHP-FPM reload进行加载,无需杀掉进程就可以完成php.ini的修改加载。使用PHP-FPM可以使PHP有不小的性能提升。PHP-FPM控制的进程 CPU回收的速度比较慢,内存分配的很均匀。

    Spawn-FCGI控制的进程CPU下降的很快,而内存分配的比较不均匀。有很多进程似乎未分配到,而另外一些却占用很高。可能是由于进程任务分配的不均匀导致的。而这也导致了总体响应速度的下降。而PHP-FPM合理的分配,导致总体响应的提到以及任务的平均。

    7 什么是Servlet

    Servlet最初是在1995年由James Gosling提出的,因为使用该技术需要复杂的Web服务器支持,所以当时并没有得到重视,也就放弃了。后来随着Web应用复杂度的提升,并要求提供更 高的并发处理能力,Servlet被重新捡起,并在Java平台上得到实现,现在提起Servlet,指的都是Java Servlet。Java Servlet要求必须运行在Web服务器当中,与Web服务器之间属于分工和互补关系。确切的说,在实际运行的时候Java Servlet与Web服务器会融为一体,如同一个程序一样运行在同一个Java虚拟机(JVM)当中。与CGI不同的是,Servlet对每个请求都是单独启动一个线程,而不是进程。这种处理方式大幅度地降低了系统里的进程数量,提高了系统的并发处理能力。另外因为Java Servlet是运行在虚拟机之上的,也就解决了跨平台问题。如果没有Servlet的出现,也就没有互联网的今天。

    在Servlet出现之后,随着使用范围的扩大,人们发现了它的一个很大的一个弊端。那就是为了能够输出HTML格式内容,需要编写大量重复代码,造成不必要的重复劳动。为了解决这个问题,基于Servlet技术产生了JavaServet Pages技术,也就是JSP。Servlet和JSP两者分工协作,Servlet侧重于解决运算和业务逻辑问题,JSP则侧重于解决展示问题。Servlet与JSP一起为Web应用开发带来了巨大的贡献,后来出现的众多Java Web应用开发框架都是基于这两种技术的,更确切的说,都是基于Servlet技术的。

    7.1 Servlet生命周期

    作为一名专业编程人员,您碰到的大多数 Java servlet 都是为响应 Web 应用程序上下文中的 HTTP 请求而设计的。因此,javax.servlet 和 javax.servlet.http 包中特定于 HTTP 的类是您应该关心的。对于Servlet容器(Tomcat)与HttpServlet是怎样进行交互的呢,看下类图:

    在此输入图片描述

    Servlet的框架是由两个Java包组成的:javax.servlet与javax.servlet.http。在javax.servlet包中定义了所有的Servlet类都必须实现或者扩展的通用接口和类。在javax.servlet.http包中定义了采用Http协议通信的HttpServlet类。Servlet的框架的核心是javax.servlet.Servlet接口,所有的Servlet都必须实现这个接口。在Servlet接口中定义了5个方法,其中3个方法代表了Servlet的生命周期:

    1. init(ServletConfig)方法:负责初始化Servlet对象,在Servlet的生命周期中,该方法执行一次;该方法执行在单线程的环境下,因此开发者不用考虑线程安全的问题;
    2. service(ServletRequest req,ServletResponse res)方法:负责响应客户的请求;为了提高效率,Servlet规范要求一个Servlet实例必须能够同时服务于多个客户端请求,即 service()方法运行在多线程的环境下,Servlet开发者必须保证该方法的线程安全性;
    3. destroy()方法:当Servlet对象退出生命周期时,负责释放占用的资源;

    编程注意事项说明:

    1. 当Server Thread线程执行Servlet实例的init()方法时,所有的Client Service Thread线程都不能执行该实例的service()方法,更没有线程能够执行该实例的destroy()方法,因此Servlet的init()方法是工作在单线程的环境下,开发者不必考虑任何线程安全的问题
    2. 当服务器接收到来自客户端的多个请求时,服务器会在单独的Client Service Thread线程中执行Servlet实例的service()方法服务于每个客户端。此时会有多个线程同时执行同一个Servlet实例的service()方法,因此必须考虑线程安全的问题
    3. 请大家注意,虽然service()方法运行在多线程的环境下,并不一定要同步该方法。而是要看这个方法在执行过程中访问的资源类型及对资源的访问方式。分析如下:

      如果service()方法没有访问Servlet的成员变量也没有访问全局的资源比如静态变量、文件、数据库连接等,而是只使用了当前线程自己的 资源,比如非指向全局资源的临时变量、request和response对象等。该方法本身就是线程安全的,不必进行任何的同步控制。

      如果service()方法访问了Servlet的成员变量,但是对该变量的操作是只读操作,该方法本身就是线程安全的,不必进行任何的同步控制。

      如果service()方法访问了Servlet的成员变量,并且对该变量的操作既有读又有写,通常需要加上同步控制语句。

      如果service()方法访问了全局的静态变量,如果同一时刻系统中也可能有其它线程访问该静态变量,如果既有读也有写的操作,通常需要加上同步控制语句。

      如果service()方法访问了全局的资源,比如文件、数据库连接等,通常需要加上同步控制语句。

    在创建一个 Java servlet 时,一般需要子类 HttpServlet。该类中的方法允许您访问请求和响应包装器(wrapper),您可以用这个包装器来处理请求和创建响应。大多数程序员都知道Servlet的生命周期,简单的概括这就分为四步:

    Servlet类加载--->实例化--->服务--->销毁;

    输入图片说明

    创建Servlet对象的时机:

    1. 默认情况下,在Servlet容器启动后:客户首次向Servlet发出请求,Servlet容器会判断内存中是否存在指定的Servlet对象,如果没有则创建它,然后根据客户的请求创建HttpRequest、HttpResponse对象,从而调用Servlet对象的service方法;
    2. Servlet容器启动时:当web.xml文件中如果<servlet>元素中指定了<load-on-startup>子元素时,Servlet容器在启动web服务器时,将按照顺序创建并初始化Servlet对象;
    3. Servlet的类文件被更新后,重新创建Servlet。Servlet容器在启动时自动创建Servlet,这是由在web.xml文件中为Servlet设置的<load-on-startup>属性决定的。从中我们也能看到同一个类型的Servlet对象在Servlet容器中以单例的形式存在;

    注意:在web.xml文件中,某些Servlet只有<serlvet>元素,没有<servlet-mapping>元素,这样我们无法通过url的方式访问这些Servlet,这种Servlet通常会在<servlet>元素中配置一个<load-on-startup>子元素,让容器在启动的时候自动加载这些Servlet并调用init(ServletConfig config)方法来初始化该Servlet。其中方法参数config中包含了Servlet的配置信息,比如初始化参数,该对象由服务器创建。

    销毁Servlet对象的时机:

    Servlet容器停止或者重新启动:Servlet容器调用Servlet对象的destroy方法来释放资源。以上所讲的就是Servlet对 象的生命周期。那么Servlet容器如何知道创建哪一个Servlet对象?Servlet对象如何配置?实际上这些信息是通过读取web.xml配置 文件来实现的。

    <servlet>
        <!-- Servlet对象的名称 -->
        <servlet-name>action<servlet-name>
        <!-- 创建Servlet对象所要调用的类 -->
        <servlet-class>org.apache.struts.action.ActionServlet</servlet-class>
        <init-param>
            <!-- 参数名称 -->
            <param-name>config</param-name>
            <!-- 参数值 -->
            <param-value>/WEB-INF/struts-config.xml</param-value>
        </init-param>
        <init-param>
            <param-name>detail</param-name>
            <param-value>2</param-value>
        </init-param>
        <init-param>
            <param-name>debug</param-name>
            <param-value>2</param-value>
        </init-param>
        <!-- Servlet容器启动时加载Servlet对象的顺序 -->
        <load-on-startup>2</load-on-startup>
    </servlet>
    <!-- 要与servlet中的servlet-name配置节内容对应 -->
    <servlet-mapping>
        <servlet-name>action</servlet-name>
        <!-- 客户访问的Servlet的相对URL路径 -->
        <url-pattern>*.do</url-pattern>
    </servlet-mapping>
    

    当Servlet容器启动的时候读取<servlet>配置节信息,根据<servlet-class>配置节信息创建 Servlet对象,同时根据<init-param>配置节信息创建HttpServletConfig对象,然后执行Servlet对象 的init方法,并且根据<load-on-startup>配置节信息来决定创建Servlet对象的顺序,如果此配置节信息为负数或者没 有配置,那么在Servlet容器启动时,将不加载此Servlet对象。当客户访问Servlet容器时,Servlet容器根据客户访问的URL地 址,通过<servlet-mapping>配置节中的<url-pattern>配置节信息找到指定的Servlet对象,并 调用此Servlet对象的service方法。

    在整个Servlet的生命周期过程中,创建Servlet实例、调用实例的init()和destroy()方法都只进行一次,当初始化完成后,Servlet容器会将该实例保存在内存中,通过调用它的service()方法,为接收到的请求服务。下面给出Servlet整个生命周期过程的UML序列图,如图所示:

    输入图片说明

    如果需要让Servlet容器在启动时即加载Servlet,可以在web.xml文件中配置<load-on-startup>元素。

    7.2 Servlet工作原理

    上面描述了Servlet的生命周期,接着我们描述一下Tomcat与Servlet是如何工作的,首先看下面的时序图:

    输入图片说明

    1. Web Client 向Servlet容器(Tomcat)发出Http请求;
    2. Servlet容器接收Web Client的请求;
    3. Servlet容器创建一个HttpRequest对象,将Web Client请求的信息封装到这个对象中;
    4. Servlet容器创建一个HttpResponse对象;
    5. Servlet容器调用HttpServlet对象的service方法,把HttpRequest对象与HttpResponse对象作为参数传给 HttpServlet对象;
    6. HttpServlet调用HttpRequest对象的有关方法,获取Http请求信息;
    7. HttpServlet调用HttpResponse对象的有关方法,生成响应数据;
    8. Servlet容器把HttpServlet的响应结果传给Web Client;

    7.3 CGI与Servlet比较

    CGI应用开发比较困难,因为它要求程序员有处理参数传递的知识,这不是一种通用的技能。CGI不可移植,为某一特定平台编写的CGI应用只能运行于这一环境中。每一个CGI应用存在于一个由客户端请求激活的进程中,并且在请求被服务后被卸载。这种模式将引起很高的内存、CPU开销,而且在同一进程中不能服务多个客户。

    Servlet对CGI的最主要优势在于一个Servlet被客户端发送的第一个请求激活,然后它将继续运行于后台,等待以后的请求。每个请求将生成一个新的线程,而不是一个完整的进程。多个客户能够在同一个进程中同时得到服务。一般来说,Servlet进程只是在Web Server卸载时被卸载。

    Servlet提供了Java应用程序的所有优势——可移植、稳健、易开发。使用Servlet Tag技术,Servlet能够生成嵌于静态HTML页面中的动态内容。

    综上,Servlet处于服务器进程中,它通过多线程方式运行其service方法,一个实例可以服务于多个请求,并且其实例一般不会销毁。 而CGI对每个请求都产生新的进程,服务完成后就销毁,所以效率上低于Servlet。

    CGI与Servlet的对比:

    对比一:当用户浏览器发出一个Http/CGI的请求,或者说调用一个CGI程序的时候,服务器端就要新启用一个进程(而且是每次都要调用),调用CGI程序越多(特别是访问量高的时候),就要消耗系统越多的处理时间,只剩下越来越少的系统资源,对于用户来说,只能是漫长的等待服务器端的返回页面了,这对于电子商务激烈发展的今天来说,不能不说是一种技术上的遗憾。

    而Servlet充分发挥了服务器端的资源并高效的利用。每次调用Servlet时并不是新启用一个进程,而是在一个Web服务器的进程中共享和分离线程,而线程最大的好处在于可以共享一个数据源,使系统资源被有效利用

    对比二:传统的CGI程序,不具备平台无关性特征,系统环境发生变化,CGI程序就要瘫痪,而Servlet具备Java的平台无关性,在系统开发过程中保持了系统的可扩展性、高效性。

    对比三:传统技术中,一般大都为二层的系统架构,即Web服务器+数据库服务器,导致网站访问量大的时候,无法克服CGI程序与数据库建立连接时速度慢的瓶颈,从而死机、数据库死锁现象频繁发生。而Servlet有连接池的概念,它可以利用多线程的优点,在系统缓存中事先建立好若干与数据库的连接,到时候若想和数据库打交道可以随时跟系统"要"一个连接即可,反应速度可想而知。

    8 Tomcat工作原理

    Tomcat 的结构很复杂,但是 Tomcat 也非常的模块化,找到了 Tomcat 最核心的模块,您就抓住了 Tomcat 的“七寸”。下面是 Tomcat 的总体结构图:

    输入图片说明

    从上图可以看出Tomcat的核心是两个组件:连接器(Connector)和容器(Container)。 Connector组件是负责生成请求对象和响应对象的,Tomcat默认的是HttpConnector,负责根据收到的Http请求报文生成 Request对象和Response对象,并把这两个对象传递给Container,然后根据Response中的内容生成相应的HTTP报文。

    Container是容器的父接口,所有子容器都必须实现这个接口,简单来说就是服务器部署的项目是运行在Container中的。Container里面的项目获取到Connector传递过来对应的的Request对象和Response对象进行相应的操作。

    Connector可以根据不同的设计和应用场景进行替换。一个Container可以选择对应多个Connector。多个Connector和一个Container就形成了一个Service,有了Service就可以对外提供服务了

    Tomcat要为一个Servlet的请求提供服务,需要做三件事:

    1. 创建一个request对象并填充那些有可能被所引用的Servlet使用的信息,如参数,头部、cookies、查询字符串等。一个 request对象就是javax.servlet.ServletRequest或 javax.servlet.http.ServletRequest接口的一个实例。
    2. 创建一个response对象,所引用的servlet使用它来给客户端发送响应。一个response对象是javax.servlet.ServletResponse或javax.servlet.http.ServletResponse接口的一个实例。
    3. 调用servlet的service方法,并传入request和response对象。这里servlet会从request对象取值,给response写值。
    4. 根据servlet返回的response生成相应的HTTP响应报文。

    既然我们已经抓到Tomcat的“七寸”,两个核心组件:连接器(Connector)和容器(Container),那这样从连接器(Connector)入手,来看下Tomcat处理HTTP请求的流程。

    很多开源应用服务器都是集成tomcat作为web container的,而且对于tomcat的servlet container这部分代码很少改动。这样,这些应用服务器的性能基本上就取决于Tomcat处理HTTP请求的connector模块的性能

    8.1 Connector种类

    Tomcat源码中与connector相关的类位于org.apache.coyote包中,Connector分为以下几类:

    Http Connector,基于HTTP协议,负责建立HTTP连接。它又分为BIO Http Connector与NIO Http Connector两种,后者提供非阻塞IO与长连接Comet支持。

    AJP Connector,基于AJP协议,AJP是专门设计用来为tomcat与http服务器之间通信专门定制的协议,能提供较高的通信速度和效率。如与Apache服务器集成时,采用这个协议。

    APR HTTP Connector,用C实现,通过JNI调用的。主要提升对静态资源(如HTML、图片、CSS、JS等)的访问性能。现在这个库已独立出来可用在任何项目中。Tomcat在配置APR之后性能非常强劲。

    8.2 Connector配置

    对Connector的配置位于conf/server.xml文件中。

    8.2.1 BIO HTTP/1.1 Connector配置

    <Connector port=”8080” protocol=”HTTP/1.1” maxThreads=”150” 
        connectionTimeout=”20000” redirectPort=”8443” />
    

    其它一些重要属性如下:

    acceptCount : 接受连接request的最大连接数目,默认值是10;

    address : 绑定IP地址,如果不绑定,默认将绑定任何IP地址;

    allowTrace : 如果是true,将允许TRACE HTTP方法;

    compressibleMimeTypes : 各个mimeType, 以逗号分隔,如text/html,text/xml;

    compression : 如果带宽有限的话,可以用GZIP压缩;

    connectionTimeout : 超时时间,默认为60000ms (60s);

    maxKeepAliveRequest : 默认值是100;

    maxThreads : 处理请求的Connector的线程数目,默认值为200;

    如果是SSL配置,如下:

    <Connector port="8181" protocol="HTTP/1.1" SSLEnabled="true" 
        maxThreads="150" scheme="https" secure="true" 
        clientAuth="false" sslProtocol = "TLS" 
        address="0.0.0.0" 
        keystoreFile="E:/java/jonas-full-5.1.0-RC3/conf/keystore.jks" 
        keystorePass="changeit" />
    

    其中,keystoreFile为证书位置,keystorePass为证书密码。

    8.2.2 NIO HTTP/1.1 Connector配置

    <Connector port=”8080” protocol=”org.apache.coyote.http11.Http11NioProtocol” 
        maxThreads=”150” connectionTimeout=”20000” redirectPort=”8443” />
    

    8.2.3 Native APR Connector配置

    1. ARP是用C/C++写的,对静态资源(HTML,图片等)进行了优化。所以要下载本地库tcnative-1.dll与openssl.exe,将其放在%tomcat%\bin目录下。

      下载地址是:http://tomcat.heanet.ie/native/1.1.10/binaries/win32/

    2. 在server.xml中要配置一个Listener,如下图。这个配置tomcat是默认配好的。

      <!--APR library loader. Documentation at /docs/apr.html --> 
      <Listener className="org.apache.catalina.core.AprLifecycleListener" SSLEngine="on" />
      
    3. 配置使用APR connector

      <Connector port=”8080” protocol=”org.apache.coyote.http11.Http11AprProtocol” 
        maxThreads=”150” connectionTimeout=”20000” redirectPort=”8443” />
      
    4. 如果配置成功,启动tomcat,会看到如下信息:

      org.apache.coyote.http11.Http11AprProtocol init

    8.3 Tomcat架构模块

    1. Server(服务器)是Tomcat构成的顶级构成元素,所有一切均包含在Server中,Server的实现类StandardServer可以包含一个到多个Services;
    2. 次顶级元素Service的实现类为StandardService调用了容器(Container)接口,其实是调用了Servlet Engine(引擎),而且StandardService类中也指明了该Service归属的Server;
    3. 接下来次级的构成元素就是容器(Container):主机(Host)、上下文(Context)和引擎(Engine)均继承自Container接口,所以它们都是容器。 但是,它们是有父子关系的,在主机(Host)、上下文(Context)和引擎(Engine)这三类容器中,引擎是顶级容器,直接包含是主机容器,而 主机容器又包含上下文容器,所以引擎、主机和上下文从大小上来说又构成父子关系,虽然它们都继承自Container接口。
    4. 连接器(Connector)将Service和Container连接起来,首先它需要注册到一个Service,它的作用就是把来自客户端的请求转发到Container(容器),这就是它为什么称作连接器的原因。

    8.4 Tomcat运行流程

    输入图片说明

    假设来自客户的请求为:http://localhost:8080/test/index.jsp

    1. 请求被发送到本机端口8080,被在那里侦听的Coyote HTTP/1.1 Connector获得;
    2. Connector把该请求交给它所在的Service的Engine来处理,并等待Engine的回应;
    3. Engine获得请求localhost:8080/test/index.jsp,匹配它所有虚拟主机Host;
    4. Engine匹配到名为localhost的Host(即使匹配不到也把请求交给该Host处理,因为该Host被定义为该Engine的默认主机);
    5. localhost Host获得请求/test/index.jsp,匹配它所拥有的所有Context;
    6. Host匹配到路径为/test的Context(如果匹配不到就把该请求交给路径名为""的Context去处理);
    7. path="/test"的Context获得请求/index.jsp,在它的mapping table中寻找对应的servlet;
    8. Context匹配到URL PATTERN为*.jsp的servlet,对应于JspServlet类;
    9. 构造HttpServletRequest对象和HttpServletResponse对象,作为参数调用JspServlet的doGet或doPost方法;
    10. Context把执行完了之后的HttpServletResponse对象返回给Host;
    11. Host把HttpServletResponse对象返回给Engine;
    12. Engine把HttpServletResponse对象返回给Connector;
    13. Connector把HttpServletResponse对象返回给客户browser;

    本帖子中包含更多资源

    您需要 登录 才可以下载或查看,没有帐号?立即注册

    x
    守望者AIR技术交流社区(www.airmyth.com)
    回复 支持 反对

    使用道具 举报

  • TA的每日心情
    擦汗
    7 天前
  • 签到天数: 431 天

    [LV.9]以坛为家II

    1738

    主题

    2089

    帖子

    12万

    积分

    超级版主

    Rank: 18Rank: 18Rank: 18Rank: 18Rank: 18

    威望
    447
    贡献
    16
    金币
    50896
    钢镚
    1418

    开源英雄守望者

     楼主| 发表于 2016-7-27 14:45:40 | 显示全部楼层

    0 系列目录

    上篇文章《WEB请求处理三:Servlet容器请求处理》讲述了Servlet容器Tomcat工作原理及请求处理过程,本章将继续讲解Tomcat配置实践。

    1 Tomcat目录结构

    要部署使用tomcat,则必须了解tomcat的目录结构以及各目录的作用。这里以tomcat7为例,进入tomcat安装目录下:

    |-- bin 执行脚本目录;
    |   |-- bootstrap.jar tomcat启动时所依赖的一个类,在启动tomcat时会发现Using CLASSPATH: 是加载的这个类;
    |   |-- catalina-tasks.xml 定义tomcat载入的库文件,类文件;
    |   |-- catalina.bat
    |   |-- catalina.sh tomcat单个实例在Linux平台上的启动/关闭脚本;
    |   |-- commons-daemon-native.tar.gz jsvc工具,可以使tomcat以守护进程方式运行,需单独编译安装;
    |   |-- commons-daemon.jar jsvc工具所依赖的java类;
    |   |-- configtest.bat
    |   |-- configtest.sh tomcat检查配置文件语法是否正确的Linux平台脚本;
    |   |-- cpappend.bat
    |   |-- daemon.sh tomcat已守护进程方式运行时的,启动,停止脚本;
    |   |-- digest.bat
    |   |-- digest.sh
    |   |-- setclasspath.bat
    |   |-- setclasspath.sh
    |   |-- shutdown.bat
    |   |-- shutdown.sh tomcat服务在Linux平台下关闭脚本;
    |   |-- startup.bat
    |   |-- startup.sh tomcat服务在Linux平台下启动脚本;
    |   |-- tomcat-juli.jar
    |   |-- tomcat-native.tar.gz 使tomcat可以使用apache的apr运行库,以增强tomcat的性能需单独编译安装;
    |   |-- tool-wrapper.bat
    |   |-- tool-wrapper.sh
    |   |-- version.bat
    |   |-- version.sh 查看tomcat以及JVM的版本信息;
    |-- conf 顾名思义,配置文件目录;
    |   |-- catalina.policy Java相关的安全策略配置文件,在系统资源级别上提供访问控制的能力,比如:配置tomcat对文件系统中目录或文件的读、写执行等权限,及对一些内存,session等的管理权限;
    |   |-- catalina.properties Tomcat内部package的定义及访问相关的控制,也包括对通过类装载器装载的内容的控制;Tomcat在启动时会事先读取此文件的相关设置;
    |   |-- context.xml tomcat的默认context容器,所有host的默认配置信息;
    |   |-- logging.properties Tomcat通过自己内部实现的JAVA日志记录器来记录操作相关的日志,此文件即为日志记录器相关的配置信息,可以用来定义日志记录的组件级别以及日志文件的存在位置等;
    |   |-- server.xml tomcat的主配置文件,包含Service, Connector, Engine, Realm, Valve, Hosts主组件的相关配置信息;
    |   |-- tomcat-users.xml Realm认证时用到的相关角色、用户和密码等信息;Tomcat自带的manager默认情况下会用到此文件;在Tomcat中添加/删除用户,为用户指定角色等将通过编辑此文件实现;
    |   |-- web.xml 为不同的Tomcat配置的web应用设置缺省值的文件,遵循Servlet规范标准的配置文件,用于配置servlet,并为所有的Web应用程序提供包括MIME映射等默认配置信息;
    |-- lib 运行需要的库文件(JARS),包含被Tomcat使用的各种各样的jar文件。在Linux/UNIX上,任何这个目录中的文件将被附加到Tomcat的classpath中;
    |-- logs 日志文件默认存放目录;
    |   |-- localhost_access_log.2013-09-18.txt 访问日志;
    |   |-- localhost.2013-09-18.log 错误和其它日志;
    |   |-- manager.2013-09-18.log 管理日志;
    |   |-- catalina.2013-09-18.log Tomcat启动或关闭日志文件;
    |-- temp 临时文件存放目录;
    |   |-- safeToDelete.tmp
    |-- webapps tomcat默认存放应用程序的目录,好比apache的默认网页存放路径是/var/www/html一样;
    |   |-- docs tomcat文档;
    |   |-- examples tomcat自带的一个独立的web应用程序例子;
    |   |-- host-manager tomcat的主机管理应用程序;
    |   |   |-- META-INF 整个应用程序的入口,用来描述jar文件的信息;
    |   |   |   |-- context.xml 当前应用程序的context容器配置,它会覆盖tomcat/conf/context.xml中的配置;
    |   |   |-- WEB-INF 用于存放当前应用程序的私有资源;
    |   |   |   |-- classes 用于存放当前应用程序所需要的class文件;
    |   |   |   |-- lib 用于存放当前应用程序所需要的jar文件;
    |   |   |   |-- web.xml 当前应用程序的部署描述符文件,定义应用程序所要加载的servlet类,以及该程序是如何部署的;
    |   |-- manager tomcat的管理应用程序;
    |   |-- ROOT 指tomcat的应用程序的根,如果应用程序部署在ROOT中,则可直接通过http://ip:port 访问到;
    |-- work 用于存放JSP应用程序在部署时编译后产生的class文件;
    

    2 Tomcat模块结构

    如下图所示,前端请求被tomcat直接接收或者由前端的代理,通过HTTP,或者AJP代理给Tomcat,此时请求被tomcat中的 connector接收,不同的connector和Engine被service组件关联起来,在一个Engine中定义了许多的虚拟主机,由Host 容器定义,每一个Host容器代表一个主机,在各自的Host中,又可以定义多个Context,用此来定义一个虚拟主机中的多个独立的应用程序。

    3 Tomcat server.xml配置

    Tomcat Server的结构图如下:

    3.1 conf/server.xml配置

    <Server port="8005" shutdown="SHUTDOWN">
        <Listener className="org.apache.catalina.core.AprLifecycleListener" SSLEngine="on" />
        <Listener className="org.apache.catalina.core.JasperListener" />
        <Listener className="org.apache.catalina.core.JreMemoryLeakPreventionListener" />
        <Listener className="org.apache.catalina.mbeans.GlobalResourcesLifecycleListener" />
        <Listener className="org.apache.catalina.core.ThreadLocalLeakPreventionListener" />
    
        <!-- 全局命名资源,来定义一些外部访问资源,其作用是为所有引擎应用程序所引用的外部资源的定义 --> 
        <GlobalNamingResources>
            <!-- 定义的一个名叫“UserDatabase”的认证资源,将conf/tomcat-users.xml加载至内存中,在需要认证的时候到内存中进行认证 -->
            <Resource name="UserDatabase" auth="Container" 
                  type="org.apache.catalina.UserDatabase" 
                  description="User database that can be updated and saved" 
                  factory="org.apache.catalina.users.MemoryUserDatabaseFactory" 
                  pathname="conf/tomcat-users.xml" /> 
        </GlobalNamingResources> 
    
        <!-- 定义Service组件,同来关联Connector和Engine,一个Engine可以对应多个Connector,每个Service中只能一个Engine -->
        <Service name="Catalina"> 
            <!-- 修改HTTP/1.1的Connector监听端口为80.客户端通过浏览器访问的请求,只能通过HTTP传递给tomcat -->
            <Connector port="80" protocol="HTTP/1.1" connectionTimeout="20000" redirectPort="8443" /> 
    
            <Connector port="8009" protocol="AJP/1.3" redirectPort="8443" /> 
    
            <!-- 修改当前Engine,默认主机是,www.test.com -->
            <Engine name="Catalina" defaultHost="test.com"> 
    
                <!-- Realm组件,定义对当前容器内的应用程序访问的认证,通过外部资源UserDatabase进行认证 -->     
                <Realm className="org.apache.catalina.realm.LockOutRealm"> 
                     <Realm className="org.apache.catalina.realm.UserDatabaseRealm" resourceName="UserDatabase"/> 
                   </Realm> 
    
                <!-- 定义一个主机,域名为:test.com,应用程序的目录是/web,设置自动部署,自动解压 -->
                <Host name="test.com"  appBase="/web" unpackWARs="true" autoDeploy="true">
                    <!-- 定义一个别名www.test.com,类似apache的ServerAlias --> 
                    <Alias>www.test.com</Alias> 
                    <!-- 定义该应用程序,访问路径"",即访问www.test.com即可访问,网页目录为:相对于appBase下的www/,即/web/www,并且当该应用程序下web.xml或者类等有相关变化时,自动重载当前配置,即不用重启tomcat使部署的新应用程序生效 -->
                    <Context path="" docBase="www/" reloadable="true" /> 
                    <!-- 定义另外一个独立的应用程序,访问路径为:www.test.com/bbs,该应用程序网页目录为/web/bbs --> 
                    <Context path="/bbs" docBase="/web/bbs" reloadable="true" /> 
    
                    <!-- 定义一个Valve组件,用来记录tomcat的访问日志,日志存放目录为:/web/www/logs如果定义为相对路径则是相当于$CATALINA_HOME,并非相对于appBase,这个要注意。定义日志文件前缀为www_access.并以.log结尾,pattern定义日志内容格式,具体字段表示可以查看tomcat官方文档 -->
                    <Valve className="org.apache.catalina.valves.AccessLogValve" directory="/web/www/logs" 
                        prefix="www_access." suffix=".log" 
                        pattern="%h %l %u %t "%r" %s %b" />
                </Host> 
    
                <!-- 定义一个主机名为manager.test.com,应用程序目录是$CATALINA_HOME/webapps,自动解压,自动部署 -->
                <Host name="manager.test.com" appBase="webapps" unpackWARs="true" autoDeploy="true"> 
                    <!-- 定义远程地址访问策略,仅允许172.23.136.*网段访问该主机,其他的将被拒绝访问 -->
                    <Valve className="org.apache.catalina.valves.RemoteAddrValve" allow="172.23.136.*" /> 
                    <!-- 定义该主机的访问日志 --> 
                    <Valve className="org.apache.catalina.valves.AccessLogValve" directory="/web/bbs/logs" 
                        prefix="bbs_access." suffix=".log" 
                        pattern="%h %l %u %t "%r" %s %b" /> 
                </Host>
            </Engine>
        </Service>
    </Server>
    

    看上去很复杂。其实,大部分都是注释。下面是一个简图说明了各组件之间的关系!

    |-- Server
    |    |-- Listener
    |    |-- GlobalNamingResources
    |    |    |-- Resource
    |    |-- Service
    |    |    |-- Connector
    |    |    |-- Engine
    |    |    |    |-- Realm
    |    |    |    |-- Host
    |    |    |    |    |-- Alias
    |    |    |    |    |-- Context
    |    |    |    |    |-- Value
    

    上面列出的比较常用的组件元素,server.xml文件中可定义的元素非常多,包括 Server, Service, Connector, Engine, Cluster, Host, Alias, Context, Realm, Valve, Manager, Listener, Resources, Resource, ResourceEnvRef, ResourceLink, WatchedResource, GlobalNameingResources, Store, Transaction, Channel, Membership, Transport, Member, ClusterListener等等。

    由以上配置不难看出存在的一个问题。如果我们想要对其中一个应用程序的配置做一些修改,那么就必须重新启动tomcat,那样势必就会影响到另外两个应用程序的正常服务。因此以上配置是不适合线上使用的,因此需要将其配置为多实例,每个实例只跑一个独立的应用程序,那样我们应用程序之间就不会在互相受到影响。但是我们将面临这样一个问题,80端口只能被一个HTTP/1.1 Connector监听,而三个tomcat实例则至少需要3个HTTP/1.1 Connector,这样我们就需要一个前端代理做分发处理,接收HTTP 80端口的请求,按域名通过每个tomcat实例的AJP/1.3 Connector传递请求。而前端的代理选择apache,基于这样的思路,我们还可以做到tomcat的负载均衡,而且apache会将接收的HTTP超文本传输报文重新封装成二进制格式通过AJP/1.3 协议传递给后端的tomcat处理,在效率上也是有明显的提升。

    3.2 server.xml组件元素

    1. <Server>元素

      这会让Tomcat启动一个server实例(即一个JVM),它监听在8005端口以接收shutdown命令。各Server的定义不能使用同 一个端口,这意味着如果在同一个物理机上启动了多个Server实例,必须配置它们使用不同的端口。这个端口的定义用于为管理员提供一个关闭此实例的便捷 途径,因此,管理员可以直接telnet至此端口使用SHUTDOWN命令关闭此实例。不过,基于安全角度的考虑,这通常不允许远程进行。

      该元素代表整个容器,是Tomcat实例的顶层元素。由org.apache.catalina.Server接口来定义。它包含一个或多个<Service>元素。并且它不能做为任何元素的子元素。

      <Server port ="8005" shutdown ="SHUTDOWN" debug ="0">
      

      className:指定实现org.apache.catalina.Server接口的类。默认值为: org.apache.catalina.core.StandardServer;

      port:指定Tomcat监听shutdown命令端口。终止服务器运行时,必须在Tomcat服务器所在的机器上发出shutdown命令。该属性是必须的;

      shutdown:指定终止Tomcat服务器运行时,发给Tomcat服务器的shutdown监听端口的字符串。该属性必须设置;

    2. <Service>元素

      Service主要用于关联一个引擎和与此引擎相关的连接器,每个连接器通过一个特定的端口和协议接收入站请求交将其转发至关联的引擎进行处理。因此,Service要包含一个引擎、一个或多个连接器。

      该元素由org.apache.catalina.Service接口定义,它包含一个<Engine>元素,以及一个或多个<Connector>,这些Connector元素共享用同一个Engine元素。

      <Service name="Catalina"> // 第一个<Service>处理所有直接由Tomcat服务器接收的web客户请求
      <Service name="Apache"> // 第二个<Service>处理所有由Apahce服务器转发过来的Web客户请求
      

      className:指定实现org.apahce.catalina.Service接口的类。默认为:org.apahce.catalina.core.StandardService;

      name:定义Service的名字,此名字也会在产生相关的日志信息时记录在日志文件当中,默认为Catalina;

    3. <Engine>元素

      Engine是Servlet处理器的一个实例,即servlet引擎,默认为定义在server.xml中的Catalina。

      每个Service元素只能有一个Engine元素。元素处理在同一个<Service>中所有<Connector>元素接收到的客户请求。由org.apahce.catalina.Engine接口定义。

      <Engine name="Catalina" defaultHost="localhost" debug="0">
      

      className:指定实现Engine接口的类,默认值为StandardEngine;

      defaultHost:指定处理客户的默认主机名,在<Engine>中的<Host>子元素中必须定义这一主机;定义一个接收所有发往非明确定义虚拟主机的请求的host组件;

      name:定义Engine的名字;

      在<Engine>可以包含如下元素<Logger>, <Realm>, <Value>, <Host>,<Listener>;

    4. <Host>元素

      位于Engine容器中用于接收请求并进行相应处理的主机或虚拟主机。

      它由Host接口定义。一个Engine元素可以包含多个<Host>元素。每个<Host>的元素定义了一个虚拟主机。它包含了一个或多个Web应用<Context>。

      <Host name="localhost" debug="0" appBase="webapps" unpackWARs="true" autoDeploy="true">
      

      className:指定实现Host接口的类。默认值为StandardHost

      appBase:指定虚拟主机的目录,可以指定绝对目录,也可以指定相对于<CATALINA_HOME>的相对目录。如果没有此项,默认为<CATALINA_HOME>/webapps;

      autoDeploy:如果此项设为true,表示Tomcat服务处于运行状态时,能够监测appBase下的文件,如果有新有web应用加入进来,会自运发布这个WEB应用;

      unpackWARs:如果此项设置为true,表示把WEB应用的WAR文件先展开为开放目录结构后再运行。如果设为false将直接运行为WAR文件;

      alias:指定主机别名,可以指定多个别名;

      deployOnStartup:如果此项设为true,表示Tomcat服务器启动时会自动发布appBase目录下所有的Web应用;

      如果Web应用中的server.xml没有相应的<Context>元素,将采用Tomcat默认的Context;

      name:定义虚拟主机的名字;

      在<Host>元素中可以包含如下子元素:<Logger>, <Realm>, <Value>, <Context>;

    5. <Context>元素

      Context在某些意义上类似于apache中的路径别名,一个Context定义用于标识tomcat实例中的一个Web应用程序。

      它由Context接口定义。是使用最频繁的元素。每个<Context>元素代表了运行在虚拟主机上的单个Web应用。一 个<Host>可以包含多个<Context>元素。每个web应用有唯一的一个相对应的Context代表web应用自身。 servlet容器为第一个web应用创建一个 ServletContext对象。

      <Context path="/sample" docBase="sample" debug="0" reloadbale="true">
      

      className:指定实现Context的类,默认为StandardContext类;

      docBase:相应的Web应用程序的存放位置;也可以使用相对路径,起始路径为此Context所属Host中appBase定义的路径;切 记,docBase的路径名不能与相应的Host中appBase中定义的路径名有包含关系,比如,如果appBase为deploy,而docBase 绝不能为deploy-bbs类的名字;

      path:指定访问Web应用的URL入口,注意/myweb,而不是myweb了事;

      reloadable:如果这个属性设为true, Tomcat服务器在运行状态下会监视在WEB-INF/classes和Web-INF/lib目录CLASS文件的改动。如果监视到有class文件被更新,服务器自重新加载Web应用;

      cookies:指定是否通过Cookies来支持Session,默认值为true;

      useNaming:指定是否支持JNDI,默认值为了true;

      在<Context>元素中可以包含如下元素:<Logger>, <Realm>, <Resource>, <ResourceParams>;

    6. <Connector>元素

      由Connector接口定义。<Connector>元素代表与客户程序实际交互的组件,它负责接收客户请求,以及向客户返回响应结果。

      进入Tomcat的请求可以根据Tomcat的工作模式分为如下两类:

      Tomcat作为应用程序服务器:请求来自于前端的web服务器,这可能是Apache, IIS, Nginx等;

      Tomcat作为独立服务器:请求来自于web浏览器;

      Tomcat应该考虑工作情形并为相应情形下的请求分别定义好需要的连接器才能正确接收来自于客户端的请求。一个引擎可以有一个或多个连接器,以适应多种请求方式。

      定义连接器可以使用多种属性,有些属性也只适用于某特定的连接器类型。一般说来,常见于server.xml中的连接器类型通常有4种:

      HTTP连接器

      SSL连接器

      AJP 1.3连接器

      proxy连接器

      <!-- 第一个Connector元素定义了一个HTTP Connector,它通过8080端口接收HTTP请求;-->
      <Connector port="8080" maxThread="50" minSpareThreads="25" maxSpareThread="75" enableLookups="false" redirectPort="8443" acceptCount="100" debug="0" connectionTimeout="20000" disableUploadTimeout="true" />
      
      <!--  第二个Connector元素定义了一个AJP Connector,它通过8009端口接收由其它服务器转发过来的请求;-->
      <Connector port="8009" enableLookups="false" redirectPort="8443" debug="0" protocol="AJP/1.3" />
      
      <!-- 第三个Connector元素定义了多个属性的SSL连接器;-->
      <Connector port="8443" maxThreads="150" minSpareThreads="25"
      maxSpareThreads="75" enableLookups="false" acceptCount="100"
      debug="0" scheme="https" secure="true" clientAuth="false" sslProtocol="TLS" />
      

      Connector元素共用属性:

      className:指定实现Connector接口的类;

      enableLookups:如果设为true,表示支持域名解析,可以把IP地址解析为主机名。WEB应用中调用request.getRemoteHost方法返回客户机主机名。默认值为true;

      redirectPort:指定转发端口。如果当前端口只支持non-SSL请求,在需要安全通信的场命,将把客户请求转发至SSL的redirectPort端口;

      HttpConnector元素的属性:

      className:指定实现Connector的类;

      port:设定Tcp/IP端口,默认值为8080,如果把8080改成80,则只要输入 http://localhost 即可,因为TCP/IP的默认端口是80;

      address:如果服务器有二个以上ip地址,此属性可以设定端口监听的ip地址。默认情况下,端口会监听服务器上所有的ip地址;

      bufferSize:设定由端口创建的输入流的缓存大小。默认值为2048byte;

      protocol:设定Http协议,默认值为HTTP/1.1;

      maxThreads:设定在监听端口的线程的最大数目,这个值也决定了服务器可以同时响应客户请求的最大数目。默认值为200;

      acceptCount:设定在监听端口队列的最大客户请求数量,默认值为10。如果队列已满,客户必须等待;

      connectionTimeout:定义建立客户连接超时的时间。如果为-1,表示不限制建立客户连接的时间;

      AJP Connector的属性:

      className:指定实现Connector的类;

      port:设定AJP端口号;

      protocol:必须设定为AJP/1.3;

    4 Tomcat一个服务,配置多个端口号

    即一个service配置多个端口,项目可以通过多个端口访问。修改tomcat-home\conf下的server.xml,在Service下配置多个<Connector>即可。

    <Service name="Catalina"> 
        <Connector connectionTimeout="20000" port="8080" protocol="HTTP/1.1" redirectPort="8443"/>
        <Connector port="8009" protocol="AJP/1.3" redirectPort="8443"/>
        <Connector port="8099" protocol="HTTP/1.1" maxThreads="150" minSpareThreads="25" maxSpareThreads="75"
            enableLookups="false" redirectPort="8443" acceptCount="100"
            debug="0" connectionTimeout="20000" URIEncoding="utf-8"
            disableUploadTimeout="true" />
    
        <Engine defaultHost="localhost" name="Catalina"> 
            <Realm className="org.apache.catalina.realm.UserDatabaseRealm" resourceName="UserDatabase"/> 
            <Host appBase="webapps" autoDeploy="true" name="localhost" unpackWARs="true" xmlNamespaceAware="false" xmlValidation="false">
            </Host>
        </Engine>
    </Service>
    

    在这个应用里,可以用8080端口号访问服务,也可以用8099端口号来访问服务; 服务放置的路径由host决定,上例中服务放在webapps下。

    5 Tomcat多个服务,配置不同端口号

    即配置多个service,每个service可以配置多个端口。**修改tomcat-home\conf下的server.xml,添加多个Service即可。

    注意:Service name、Engine name、appBase,端口号别忘了修改,以免重复。

    <Service name="Catalina">
        <Connector connectionTimeout="20000" port="8080" protocol="HTTP/1.1" redirectPort="8443"/>
        <Connector port="8009" protocol="AJP/1.3" redirectPort="8443"/>
    
        <Engine defaultHost="localhost" name="Catalina">
            <Realm className="org.apache.catalina.realm.UserDatabaseRealm" resourceName="UserDatabase"/>
            <Host appBase="webapps" autoDeploy="true" name="localhost" unpackWARs="true" xmlNamespaceAware="false" xmlValidation="false">
            </Host>
        </Engine>
    </Service>
    
    <Service name="Catalina1">
        <Connector connectionTimeout="20000" port="8099" protocol="HTTP/1.1" redirectPort="8443"/>
        <Connector port="8009" protocol="AJP/1.3" redirectPort="8443"/>
    
        <Engine defaultHost="localhost" name="Catalina1">
            <Realm className="org.apache.catalina.realm.UserDatabaseRealm" resourceName="UserDatabase"/>
            <Host appBase="webapps1" autoDeploy="true" name="localhost" unpackWARs="true" xmlNamespaceAware="false" xmlValidation="false">
            </Host>
        </Engine>
    </Service>
    
    <Service name="Catalina2">
        <Connector connectionTimeout="20000" port="8098" protocol="HTTP/1.1" redirectPort="8443"/>
        <Connector port="8009" protocol="AJP/1.3" redirectPort="8443"/>
    
        <Engine defaultHost="localhost" name="Catalina2">
            <Realm className="org.apache.catalina.realm.UserDatabaseRealm" resourceName="UserDatabase"/>
            <Host appBase="webapps2" autoDeploy="true" name="localhost" unpackWARs="true" xmlNamespaceAware="false" xmlValidation="false">
            </Host>
        </Engine>
    </Service>
    

    以上三个service,发布的路径不同,项目分别发布在webapps、webapps1、webapps2下,访问不同的项目的方法:

    http://localhost:8080/项目名称1

    http://localhost:8099/项目名称2

    http://localhost:8088/项目名称3

    6 Tomcat优化配置

    6.1 精简Tomcat和配置文件

    1. 删除不需要的管理应用和帮助应用,提高tomcat安全性

      删除webapps下所有文件:rm –fr $CATALINA_HOME/webapps/*

      删除server/webapps下所有文件:rm –fr $CATALINA_HOME/server/webapps/*

    2. 精简sever.xml配置文件

      使用tomcat发布版本中的最小配置文件,提高性能,如果有功能上的需求,在逐个的加入功能配置。

      备份原来的server.xml为server.xml_bak:mv server.xml server.xml_bak

      复制server-minimal.xml为server.xml:cp server-minimal.xml server.xml

    6.2 连接器优化

    在$CATALINA_HOME/conf/server.xml 配置文件中的Connetctor节点,和连接数相关的参数配置和优化。

    1. maxThreads

      Tomcat使用线程来处理接收的每个请求。这个值表示Tomcat可创建的最大的线程数。默认值200。 可以根据机器的时期性能和内存大小调整,一般可以在400-500。最大可以在800左右。

    2. acceptCount

      指定当所有可以使用的处理请求的线程数都被使用时,可以放到处理队列中的请求数,超过这个数的请求将不予处理。默认值10。

    3. minSpareThreads

      Tomcat初始化时创建的线程数。默认值4。

    4. maxSpareThreads

      一旦创建的线程超过这个值,Tomcat就会关闭不再需要的socket线程。默认值50。

    5. enableLookups

      是否反查域名,默认值为true。为了提高处理能力,应设置为false

    6. connnectionTimeout

      网络连接超时,默认值20000,单位:毫秒。设置为0表示永不超时,这样设置有隐患的。通常可设置为30000毫秒

    7. maxKeepAliveRequests

      保持请求数量,默认值100。

    8. bufferSize

      输入流缓冲大小,默认值2048 bytes。

    9. compression

      压缩传输,取值on/off/force,默认值off。

    其中和最大连接数相关的参数为maxThreads和acceptCount。如果要加大并发连接数,应同时加大这两个参数web server允许的最大连接数还受制于操作系统的内核参数设置,通常Windows是2000个左右,Linux是1000个左右。

    配置示例:

    <!-- AJP连接 -->
    <Connector port="8009" maxTreads="500" minSpareThreads="10" 
           maxSpareThreads="50" acceptCount="50" connectionTimeout="60000" 
           enableLookups="false" redirectPort="8443" protocol="AJP/1.3" />
    
    <!-- 通用连接 -->
    <Connector port="8080"
           maxTreads="500" minSpareThreads="10" maxSpareThreads="50"
           acceptCount="50" connectionTimeout="60000"
           enableLookups="false" redirectPort="8443" protocol="AJP/1.3"
           compression="on"
           compressionMinSize="2048"
           noCompressionUserAgents="gozilla, traviata"
           compressableMimeType="text/html,text/xml" />
    
    <!-- 主机和应用配置 -->
    <Host name="localhost" appBase=""
        unpackWARs="true" autoDeploy="true"
        xmlValidation="false" xmlNamespaceAware="false">
        <Context path="" docBase="/www/xxxx/site/web" reloadable="true" debug="0"/>
    </Host>
    

    6.3 JVM优化

    Tomcat默认可以使用的内存为128MB,Windows下,在文件{tomcat_home}/bin/catalina.bat,Unix下,在文件$CATALINA_HOME/bin/catalina.sh的前面,增加如下设置:

    JAVA_OPTS="$JAVA_OPTS -Xms[初始化内存大小] -Xmx[可以使用的最大内存]"
    # 或设置环境变量
    export JAVA_OPTS="$JAVA_OPTS -Xms[初始化内存大小] -Xmx[可以使用的最大内存]
    

    一般说来,你应该使用物理内存的 80% 作为堆大小。如果本机上有Apache服务器,可以先折算Apache需要的内存,然后修改堆大小。建议设置为70%;建议设置[初始化内存大小]等于[可以使用的最大内存],这样可以减少平凡分配堆而降低性能。

    6.4 其他优化配置

    1. Tomcat中如何禁止和允许列目录下的文件

      在$CATALINA_HOME/conf/web.xml中,把listings参数设置成false即可,如下:

      <servlet>
         <servlet-name>default</servlet-name>
         <servlet-class>org.apache.catalina.servlets.DefaultServlet</servlet-class>
         <init-param>
             <param-name>debug</param-name>
             <param-value>0</param-value>
         </init-param>
         <init-param>
             <param-name>listings</param-name>
             <param-value>false</param-value>
         </init-param>
         <load-on-startup>1</load-on-startup>
      </servlet>
      

    本帖子中包含更多资源

    您需要 登录 才可以下载或查看,没有帐号?立即注册

    x
    守望者AIR技术交流社区(www.airmyth.com)
    回复 支持 反对

    使用道具 举报

  • TA的每日心情
    擦汗
    7 天前
  • 签到天数: 431 天

    [LV.9]以坛为家II

    1738

    主题

    2089

    帖子

    12万

    积分

    超级版主

    Rank: 18Rank: 18Rank: 18Rank: 18Rank: 18

    威望
    447
    贡献
    16
    金币
    50896
    钢镚
    1418

    开源英雄守望者

     楼主| 发表于 2016-7-27 14:59:16 | 显示全部楼层

    0 系列目录

    为开发团队选择一款优秀的MVC框架是件难事儿,在众多可行的方案中决择需要很高的经验和水平。你的一个决定会影响团队未来的几年。要考虑方面太多:

    1. 简单易用,以提高开发效率。使小部分的精力在框架上,大部分的精力放在业务上。
    2. 性能优秀,这是一个最能吸引眼球的话题。
    3. 尽量使用大众的框架(避免使用小众的、私有的框架),新招聘来的开发人员有一些这方面技术积累,减低人员流动再适应的影响。

    如果你还在为这件事件发愁,本文最适合你了。选择Spring MVC吧。本篇文章主要以Spring MVC为例,基本上市面上的MVC框架处理流程都大同小异,主流程都基本相同。Spring MVC比较成熟、使用也比较广泛,设计理念也非常棒,所以,本文重点以Spring MVC讲解为主。

    先说下在整个WEB请求处理过程中,本篇文章讲述的是哪个流程模块。为直观明了,先上一张图,红色部分为本章所述模块:

    1 Spring MVC核心类与接口

    先来了解一下,几个重要的接口与类。现在不知道他们是干什么的没关系,先混个脸熟,为以后认识他们打个基础。

    1. DispatcherServlet -- 前置控制器

      Spring提供的前端控制器,所有的请求都有经过它来统一分发。在DispatcherServlet将请求分发给Spring Controller之前,需要借助于Spring提供的HandlerMapping定位到具体的Controller。

    2. HandlerMapping接口 -- 处理请求的映射

      HandlerMapping接口的实现类:

      SimpleUrlHandlerMapping 通过配置文件,把一个URL映射到Controller DefaultAnnotationHandlerMapping 通过注解,把一个URL映射到Controller类上

    3. HandlerAdapter接口 -- 处理请求的映射

      AnnotationMethodHandlerAdapter类,通过注解,把一个URL映射到Controller类的方法上

    4. Controller接口 -- 控制器

      由于我们使用了@Controller注解,添加了@Controller注解的类就可以担任控制器(Action)的职责,所以我们并没有用到这个接口。

      需要为并发用户处理请求,因此实现Controller接口时,必须保证线程安全并且可重用。 Controller将处理用户请求,这和Struts Action扮演的角色是一致的。一旦Controller处理完用户请求,则返回ModelAndView对象给DispatcherServlet前 端控制器,ModelAndView中包含了模型(Model)和视图(View)。从宏观角度考虑,DispatcherServlet是整个Web应用的控制器;从微观考虑,Controller是单个Http请求处理过程中的控制器,而ModelAndView是Http请求过程中返回的模型(Model)和视图(View)

    5. HandlerInterceptor接口 -- 拦截器

      无图,我们自己实现这个接口,来完成拦截的器的工作。

    6. ViewResolver接口的实现类

      Spring提供的视图解析器(ViewResolver)在Web应用中查找View对象,从而将相应结果渲染给客户。

      UrlBasedViewResolver类 通过配置文件,把一个视图名交给到一个View来处理 InternalResourceViewResolver类,比上面的类,加入了JSTL的支持

    7. View接口

      JstlView类

    8. LocalResolver接口

    9. HandlerExceptionResolver接口 -- 异常处理

      SimpleMappingExceptionResolver实现类

    10. ModelAndView类

    2 DispatcherServlet初始化过程

    当Web项目启动时,做初始化工作,所以我们大部分是配置在Web.xml里面,这样项目一启动,就会执行相关的初始化工作,下面是Web.xml代码:

        <servlet>  
            <servlet-name>SpringMVCDispatcher</servlet-name>  
            <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>  
            <init-param>  
                <param-name>contextConfigLocation</param-name>  
                <param-value>  
                    classpath:spring-mvc.xml  
                </param-value>  
            </init-param>  
            <load-on-startup>1</load-on-startup>  
        </servlet>  
        <servlet-mapping>  
            <servlet-name>SpringMVCDispatcher</servlet-name>  
            <url-pattern>*.jhtml</url-pattern>  
        </servlet-mapping>
    
        <servlet>  
            <servlet-name>HessianDispatcher</servlet-name>  
            <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>  
            <init-param>  
                <param-name>contextConfigLocation</param-name>  
                <param-value>  
                    classpath:hessian-service.xml  
                </param-value>  
            </init-param>  
            <load-on-startup>1</load-on-startup>  
        </servlet>  
        <servlet-mapping>  
            <servlet-name>HessianDispatcher</servlet-name>  
            <url-pattern>/service/*</url-pattern>  
        </servlet-mapping>
    

    这里配置了两个DispatcherServlet,后面会介绍到,怎么各自处理,有各自的上下文容器。

    最早我们开始学习MVC结构时,就是学servlet,都是继承了HttpServlet 类,也是重新了init、doGet、doPost、destroy方法,我这边就不介绍HttpServlet类,DispatcherServlet也是间接最高继承了HttpServlet,类继承结构如图所示:

    我们先了解项目启动,DispatcherServlet和父类都做了什么事情呢?这是我们本节的重点。

    1. 第一步:DispatcherServlet继承了FrameworkServlet,FrameworkServlet 继承了HttpServletBean,HttpServletBean继承了HttpServlet 类,而HttpServletBean类有一个入口点就是重写了init方法,如图所示:

      init方法做了什么事情呢?接下来我们来具体分析:

      1)PropertyValues:获取Web.xml里面的servlet的init-param(web.xml)

      /** 
       * Create new ServletConfigPropertyValues. 
       * @param config ServletConfig we'll use to take PropertyValues from 
       * @param requiredProperties set of property names we need, where 
       * we can't accept default values 
       * @throws ServletException if any required properties are missing 
       */  
      public ServletConfigPropertyValues(ServletConfig config, Set<String> requiredProperties)  
          throws ServletException {  
          Enumeration en = config.getInitParameterNames();  
          while (en.hasMoreElements()) {  
              String property = (String) en.nextElement();  
              Object value = config.getInitParameter(property);  
              addPropertyValue(new PropertyValue(property, value));  
          }   
      }
      

      说明:

      Enumeration en = config.getInitParameterNames();
      

      获取了init-param的param-name和param-value值,并设置配置参数到PropertyValue,如图所示:

      2)BeanWrapper:封装了bean的行为,提供了设置和获取属性值,它有对应的BeanWrapperImpl,如图所示:

      3)ResourceLoader:接口仅有一个getResource(String location)的方法,可以根据一个资源地址加载文件资源。classpath:这种方式指定SpringMVC框架bean配置文件的来源。

      ResourcePatternResolver扩展了ResourceLoader接口,获取资源:

      ResourcePatternResolver resolver =new PathMatchingResourcePatternResolver();
      resolver.getResources("classpath:spring-mvc.xml");
      

      总结:

      先通过PropertyValues获取web.xml文件init-param的参数值,然后通过ResourceLoader读取.xml配置信息,BeanWrapper对配置的标签进行解析和将系统默认的bean的各种属性设置到对应的bean属性。

    2. 第二步:在init方法里还调用了initServletBean();这里面又实现了什么。 HttpServletBean在为子类提供模版、让子类根据自己的需求实现不同的ServletBean的初始化工作,这边是由 HttpServletBean的子类FrameworkServlet来实现的,如图所示:

      this.webApplicationContext = initWebApplicationContext();初始化SpringMVC 上下文容器,servlet的上下文容器是ServletContext。对initWebApplicationContext();进行跟踪,查看这个方法做了什么事情?

       

      protected WebApplicationContext initWebApplicationContext() {  
           //1. 根节点上下文,是通过ContextLoaderListener加载的,服务器启动时,最先加载的  
           WebApplicationContext rootContext =  
                   WebApplicationContextUtils.getWebApplicationContext(getServletContext());  
           if (this.webApplicationContext != null) {
               wac = this.webApplicationContext;  
               if (wac instanceof ConfigurableWebApplicationContext) {  
                   ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext) wac;  
                   //2. 要对上下文设置父上下文和ID等  
                   if (!cwac.isActive()) {  
                       if (cwac.getParent() == null) {  
                           cwac.setParent(rootContext);  
                       }  
                       configureAndRefreshWebApplicationContext(cwac);  
                   }  
               }  
           }  
           //3. Servlet不是由编程式注册到容器中,查找servletContext中已经注册的WebApplicationContext作为上下文  
           if (wac == null) {  
               wac = findWebApplicationContext();  
           }  
           //4. 如果都没找到时,就用根上下文就创建一个上下文有ID  
           if (wac == null) {  
               wac = createWebApplicationContext(rootContext);  
           }  
           //5. 在上下文关闭的情况下调用refesh可启动应用上下文,在已经启动的状态下,调用refresh则清除缓存并重新装载配置信息  
           if (!this.refreshEventReceived) {  
               onRefresh(wac);  
           }  
          //6. 对不同的请求对应的DispatherServlet有不同的WebApplicationContext、并且都存放在ServletContext中  
           if (this.publishContext) {  
               String attrName = getServletContextAttributeName();  
               getServletContext().setAttribute(attrName, wac);  
               if (this.logger.isDebugEnabled()) {  
                   this.logger.debug("Published WebApplicationContext of servlet '" + getServletName() +  
                           "' as ServletContext attribute with name [" + attrName + "]");  
               }  
           }  
      
           return wac;  
      }
      

      总结:

      initWebApplicationContext初始化上下文,并作为值放到了ServletContext里,因为不同的 DispatherServlet有对应的各自的上下文,而且上下文有设置父上下文和id属性等。上下文项目启动时会调用 createWebApplicationContext()方法,如下图所示。

      然后会初始化,设置父上下文和id属性等,如图所示:

      1) 获取ContextLoaderListener加载的上下文并标示为根上下文,如果是编程式传入,没初始化,以根节点为父上文,并设置ID等信息,然后初始化。

      2) 如果上下文是为空的,Servlet不是由编程式注册到容器中,查找servletContext中已经注册的 WebApplicationContext作为上下文,如果都没找到时,就用根上下文就创建一个上下文ID,在上下文关闭的情况下调用refesh可启 动应用上下文,在已经启动的状态下,调用refresh则清除缓存并重新装载配置信息。

      3) 对不同的请求对应的DispatherServlet有不同的WebApplicationContext、并且都存放在ServletContext 中。以servlet-name为key保存在severtContext,前面有配置了两个DispatherServlet,都有各自的上下文容器, 如下图所示。

    3. 第三步:回调函数onRefresh还做了一些提供了SpringMVC各种编程元素的初始化工作, onRefresh在为子类提供模版、让子类根据自己的需求实现不同的onRefresh的初始化工作,这边是由FrameworkServlet的子类 DispatcherServlet来实现的,如图所示:

      我们现在来分析SpringMVC组件进行初始化,并封装到DispatcherServlet中:

           //初始化上传文件解析器
           initMultipartResolver(context);
           //初始化本地解析器
           initLocaleResolver(context);
           //初始化主题解析器
           initThemeResolver(context);
           //初始化映射处理器
           initHandlerMappings(context);
           //初始化适配器处理器
           initHandlerAdapters(context);
           //初始化异常处理器
           initHandlerExceptionResolvers(context);
           //初始化请求到视图名翻译器
           initRequestToViewNameTranslator(context);
           //初始化视图解析器
           initViewResolvers(context);
      

      1)initHandlerMappings初始化映射处理器:

      private void initHandlerMappings(ApplicationContext context) {  
         this.handlerMappings = null;  
         if (this.detectAllHandlerMappings) {  
             Map<String, HandlerMapping> matchingBeans =  
                   BeanFactoryUtils.beansOfTypeIncludingAncestors(context, HandlerMapping.class, true, false);  
             if (!matchingBeans.isEmpty()) {  
                 this.handlerMappings = new ArrayList<HandlerMapping>(matchingBeans.values());  
                 // We keep HandlerMappings in sorted order.  
                 OrderComparator.sort(this.handlerMappings);  
             }  
         }  
         else {  
             try {  
                 HandlerMapping hm = context.getBean(HANDLER_MAPPING_BEAN_NAME, HandlerMapping.class);  
                 this.handlerMappings = Collections.singletonList(hm);  
             }  
             catch (NoSuchBeanDefinitionException ex) {  
             }  
         }  
         if (this.handlerMappings == null) {  
             this.handlerMappings = getDefaultStrategies(context, HandlerMapping.class);  
             if (logger.isDebugEnabled()) {  
                 logger.debug("No HandlerMappings found in servlet '" + getServletName() + "': using default");  
             }  
         }  
      }
      

      说明:

      1)detectAllHandlerMappings默认是true,根据类型匹配机制查找上下文及父容器上下文中所有类型为HandlerMapping的bean,将它们作为该类型组件,并放到ArrayList<HandlerMapping>中。

      2)detectAllHandlerMappings如果是false时,查找key为handlerMapping的HandlerMapping类型的bean为该类组件,而且 Collections.singletonList只有一个元素的集合。

      3)List<HandlerMapping> 是为空的话,使用BeanNameUrlHandleMapping实现类创建该类的组件。

      initHandlerMapping会初始化了handlerMethods请求方法的映射,HandlerMapping是处理请求的映射的如图所示:

      2)initHandlerAdapters适配器处理器:

      private void initHandlerAdapters(ApplicationContext context) {  
           this.handlerAdapters = null;  
      
           if (this.detectAllHandlerAdapters) {  
               Map<String, HandlerAdapter> matchingBeans =  
                       BeanFactoryUtils.beansOfTypeIncludingAncestors(context, HandlerAdapter.class, true, false);  
               if (!matchingBeans.isEmpty()) {  
                   this.handlerAdapters = new ArrayList<HandlerAdapter>(matchingBeans.values());  
                   // We keep HandlerAdapters in sorted order.  
                   OrderComparator.sort(this.handlerAdapters);  
               }  
           }  
           else {  
               try {  
                   HandlerAdapter ha = context.getBean(HANDLER_ADAPTER_BEAN_NAME, HandlerAdapter.class);  
                   this.handlerAdapters = Collections.singletonList(ha);  
               }  
               catch (NoSuchBeanDefinitionException ex) {  
                               }  
           }  
           if (this.handlerAdapters == null) {  
               this.handlerAdapters = getDefaultStrategies(context, HandlerAdapter.class);  
               if (logger.isDebugEnabled()) {  
                   logger.debug("No HandlerAdapters found in servlet '" + getServletName() + "': using default");  
               }  
           }  
      }
      

      initHandlerAdapters适配器处理器初始化原理跟initHandlerMappings初始化映射处理器一样。

    当DispatcherServlet初始化后,就会自动扫描上下文的bean,根据名称或者类型匹配的机制查找自定义的组件,找不到则使用DispatcherServlet。Properties定义默认的组件。

    HttpServletBean、FrameworkServlet、DispatcherServlet三个不同的类层次,SpringMVC对三个以抽象和继承来实现不用的功能,分工合作,实现了解耦的设计原则。

    我们在回顾一下,各自做了什么事情:

    1. HttpServletBean 主要做一些初始化的工作,将web.xml中配置的参数设置到Servlet中。比如servlet标签的子标签init-param标签中配置的参数。
    2. FrameworkServlet 将Servlet与Spring容器上下文关联。其实也就是初始化FrameworkServlet的属性webApplicationContext,这个属性代表SpringMVC上下文,它有个父类上下文,既web.xml中配置的ContextLoaderListener监听器初始化的容器上下文。
    3. DispatcherServlet 初始化各个功能的实现类。比如异常处理、视图处理、请求映射处理等。

    3 DispatcherServlet处理请求过程

     

    Spring MVC请求处理流程描述:

    1. 用户向服务器发送请求,请求被Spring 前端控制Servelt DispatcherServlet捕获;
    2. DispatcherServlet对请求URL进行解析,得到请求资源标识符(URI)。然后根据该URI,调用HandlerMapping 获得该Handler配置的所有相关的对象(包括Handler对象以及Handler对象对应的拦截器),最后以 HandlerExecutionChain对象的形式返回;
    3. DispatcherServlet 根据请求获得Handler,选择一个合适的HandlerAdapter。(附注:如果成功获得HandlerAdapter后,此时将开始执行拦截器的preHandler(...)方法)
    4. 提取Request中的模型数据,填充Handler入参,开始执行Handler(Controller)。 在填充Handler的入参过程中,根据你的配置,Spring将帮你做一些额外的工作:
      HttpMessageConveter:将请求消息(如Json、xml等数据)转换成一个对象,将对象转换为指定的响应信息;
      数据转换:对请求消息进行数据转换。如String转换成Integer、Double等;
      数据根式化:对请求消息进行数据格式化。 如将字符串转换成格式化数字或格式化日期等;
      数据验证: 验证数据的有效性(长度、格式等),验证结果存储到BindingResult或Error中;
      
    5. Handler执行完成后,向DispatcherServlet 返回一个ModelAndView对象;
    6. 根据返回的ModelAndView,选择一个适合的ViewResolver(必须是已经注册到Spring容器中的ViewResolver)返回给DispatcherServlet;
    7. ViewResolver 结合Model和View,来渲染视图;
    8. 将渲染结果返回给客户端;

    HttpServlet提供了service方法用于处理请求,service使用了模板设计模式,在内部对于http get方法会调用doGet方法,http post方法调用doPost方法:

    进入processRequest方法看下:

        

    其中注册的监听器类型为ApplicationListener接口类型。继续看DispatcherServlet覆写的doService方法:

      

    最终就是doDispatch方法。doDispatch方法功能简单描述一下:

    1. 首先根据请求的路径找到HandlerMethod(带有Method反射属性,也就是对应Controller中的方法);
    2. 然后匹配路径对应的拦截器,有了HandlerMethod和拦截器构造个HandlerExecutionChain对象。HandlerExecutionChain对象的获取是通过HandlerMapping接口提供的方法中得到;
    3. 有了HandlerExecutionChain之后,通过HandlerAdapter对象进行处理得到ModelAndView对象;
    4. HandlerMethod内部handle的时候,使用各种HandlerMethodArgumentResolver实现类处理HandlerMethod的参数,使用各种HandlerMethodReturnValueHandler实现类处理返回值;
    5. 最终返回值被处理成ModelAndView对象,这期间发生的异常会被HandlerExceptionResolver接口实现类进行处理;

    本帖子中包含更多资源

    您需要 登录 才可以下载或查看,没有帐号?立即注册

    x
    守望者AIR技术交流社区(www.airmyth.com)
    回复 支持 反对

    使用道具 举报

  • TA的每日心情
    擦汗
    7 天前
  • 签到天数: 431 天

    [LV.9]以坛为家II

    1738

    主题

    2089

    帖子

    12万

    积分

    超级版主

    Rank: 18Rank: 18Rank: 18Rank: 18Rank: 18

    威望
    447
    贡献
    16
    金币
    50896
    钢镚
    1418

    开源英雄守望者

     楼主| 发表于 2016-7-27 15:03:22 | 显示全部楼层

    0 系列目录

    1 HTTP报文

    HTTP报文是面向文本的,报文中的每一个字段都是一些ASCII码串,各个字段的长度是不确定的。HTTP有两类报文:请求报文和响应报文

    1.1 HTTP请求报文解剖

    1.1.1 请求报文结构

    HTTP请求报文由3部分组成(请求行+请求头+请求体):

    下面是一个实际的请求报文:

    ①为请求方法,GET和POST是最常见的HTTP方法,除此以外还包括DELETE、HEAD、OPTIONS、PUT、TRACE。不过,当前的大多数浏览器只支持GET和POST,Spring 3.0提供了一个HiddenHttpMethodFilter,允许你通过“_method”的表单参数指定这些特殊的HTTP方法(实际上还是通过POST提交表单)。服务端配置了HiddenHttpMethodFilter后,Spring会根据_method参数指定的值模拟出相应的HTTP方法,这样,就可以使用这些HTTP方法对处理方法进行映射了。

    GET:最常见的一种请求方式,服务器将URL定位的资源放在响应报文的数据部分,回送给客户端。地址中”?”之后的部分就是通过GET发送的请求数据,各个数据之间用”&”符号隔开。显然,这种方式不适合传送私密数据。另外,由于不同的浏览器对地址的字符限制也有所不同,一般最多只能识别1024个字符,所以如果需要传送大量数据的时候,也不适合使用GET方式

    POST:对于上面提到的不适合使用GET方式的情况,可以考虑使用POST方式,因为使用POST方法可以允许客户端给服务器提供信息较多。POST方法将请求参数封装在HTTP请求数据中,以名称/值的形式出现,可以传输大量数据,这样POST方式对传送的数据大小没有限制,而且也不会显示在URL中。

    关于HTTP请求GET和POST的区别:

    1. GET提交:请求的数据会附在URL之后(就是把数据放置在HTTP协议头<request-line>中),以?分割URL和传输数据,多个参数用&连接。如果数据是英文字母/数字,原样发送,如果是空格,转换为+,如果是中文/其他字符,则直接把字符串用BASE64加密,得出如: %E4%BD%A0%E5%A5%BD,其中%XX中的XX为该符号以16进制表示的ASCII。 POST提交:把提交的数据放置在是HTTP的报文体<request-body>中。 因此,GET提交的数据会在地址栏中显示出来,而POST提交,地址栏不会改变
    2. 传输数据的大小: 首先声明,HTTP协议没有对传输的数据大小进行限制,HTTP协议规范也没有对URL长度进行限制。 而在实际开发中存在的限制主要有: GET:特定浏览器和服务器对URL长度有限制,例如IE对URL长度的限制是2083字节(2K+35)。对于其他浏览器,如Netscape、FireFox等,理论上没有长度限制,其限制取决于操作系统的支持。 因此对于GET提交时,传输数据就会受到URL长度的限制。 POST:由于不是通过URL传值,理论上数据不受限。但实际各个WEB服务器会规定对post提交数据大小进行限制,Apache、IIS6都有各自的配置。
    3. 安全性: POST的安全性要比GET的安全性高。注意:这里所说的安全性和上面GET提到的“安全”不是同个概念。上面“安全”的含义仅仅是不作数据修改,而这里安全的含义是真正的Security的含义,比如:通过GET提交数据,用户名和密码将明文出现在URL上,因为(1)登录页面有可能被浏览器缓存,(2)其他人查看浏览器的历史纪录,那么别人就可以拿到你的账号和密码了,

    ②为请求对应的URL地址,它和报文头的Host属性组成完整的请求URL。

    ③为协议名称及版本号

    ④为HTTP的报文头,报文头包含若干个属性,格式为“属性名:属性值”,服务端据此获取客户端的信息。

    ⑤为报文体,它将一个页面表单中的组件值通过param1=value1&param2=value2的键值对形式编码成一个格式化串,它承载多个请求参数的数据。不但报文体可以传递请求参数,请求URL也可以通过类似于“/chapter15/user.html?param1=value1&param2=value2”的方式传递请求参数。

    对照上面的请求报文,我们把它进一步分解,你可以看到一幅更详细的结构图:

    1.1.2 HTTP请求报文头属性

    报文头属性是什么东西呢?我们不妨以一个小故事来说明吧。

    快到中午了,张三丰不想去食堂吃饭,于是打电话叫外卖:老板,我要一份[鱼香肉丝],要12:30之前给我送过来哦,我在江湖湖公司研发部,叫张三丰。

    这里,你要[鱼香肉丝]相当于HTTP报文体,而“12:30之前送过来”,你叫“张三丰”等信息就相当于HTTP的报文头。它们是一些附属信息,帮忙你和饭店老板顺利完成这次交易

    请求HTTP报文和响应HTTP报文都拥有若干个报文关属性,它们是为协助客户端及服务端交易的一些附属信息。

    1. Accept:

      请求报文可通过一个“Accept”报文头属性告诉服务端 客户端接受什么类型的响应。

      如下报文头相当于告诉服务端,俺客户端能够接受的响应类型仅为纯文本数据啊,你丫别发其它什么图片啊,视频啊过来,那样我会歇菜的~~~:

      Accept:text/plain
      

      Accept属性的值可以为一个或多个MIME类型的值,关于MIME类型,大家请参考:http://en.wikipedia.org/wiki/MIME_type

    2. Cookie:

      客户端的Cookie就是通过这个报文头属性传给服务端的哦!如下所示:

      Cookie: $Version=1; Skin=new;jsessionid=5F4771183629C9834F8382E23BE13C4C
      

      服务端是怎么知道客户端的多个请求是隶属于一个Session呢?注意到后台的那个 jsessionid=5F4771183629C9834F8382E23BE13C4C木有?原来就是通过HTTP请求报文头的Cookie属性的 jsessionid的值关联起来的!(当然也可以通过重写URL的方式将会话ID附带在每个URL的后面哦)。

    3. Referer:

      表示这个请求是从哪个URL过来的,假如你通过google搜索出一个商家的广告页面,你对这个广告页面感兴趣,鼠标一点发送一个请求报文到商家的网站,这个请求报文的Referer报文头属性值就是http://www.google.com。

      唐僧到了西天. 
      如来问:侬是不是从东土大唐来啊? 
      唐僧:厉害!你咋知道的! 
      如来:呵呵,我偷看了你的Referer...
      

      很多貌似神奇的网页监控软件(如著名的 我要啦),只要在你的网页上放上一段JavaScript,就可以帮你监控流量,全国访问客户的分布情况等报表和图表,其原理就是通过这个Referer及其它一些HTTP报文头工作的。

    4. Cache-Control:

      对缓存进行控制,如一个请求希望响应返回的内容在客户端要被缓存一年,或不希望被缓存就可以通过这个报文头达到目的。

      如以下设置,相当于让服务端将对应请求返回的响应内容不要在客户端缓存:

      Cache-Control: no-cache
      
    5. User-Agent:

      产生请求的浏览器类型。

    6. Host:

      请求的主机名,允许多个域名同处一个IP地址,即虚拟主机。

    1.1.3 如何访问请求报文头

    由于请求报文头是客户端发过来的,服务端当然只能读取了,以下是HttpServletRequest一些用于读取请求报文头的API:

    // 获取请求报文中的属性名称  
    java.util.Enumeration<java.lang.String>   getHeaderNames();  
    
    // 获取指定名称的报文头属性的值  
    java.lang.String getHeader(java.lang.String name);
    

    由于一些请求报文头属性“太著名”了,因此HttpServletRequest为它们提供了VIP的API:

    // 获取报文头中的Cookie(读取Cookie的报文头属性) 
    Cookie[]   getCookies() ;  
    
    // 获取客户端本地化信息(读取 Accept-Language 的报文头属性)  
    java.util.Locale    getLocale()   
    
    // 获取请求报文体的长度(读取Content-Length的报文头属性)  
    int getContentLength();  
    
    // 获取请求所关联的HttpSession,其内部的机理是通过读取请求报文头中Cookie属性的JSESSIONID的值,
    // 在服务端的一个会话Map中,根据这个JSESSIONID获取对应的HttpSession的对象
    HttpSession getSession()
    

    1.2 HTTP响应报文解剖

    1.2.1 响应报文结构

    HTTP的响应报文也由三部分组成(响应行+响应头+响应体):

    以下是一个实际的HTTP响应报文:

    ①报文协议及版本;

    ②状态码及状态描述;

    ③响应报文头,也是由多个属性组成;

    ④响应报文体,即我们真正要的“干货”;

    1.2.2 响应状态码

    和请求报文相比,响应报文多了一个“响应状态码”,它以“清晰明确”的语言告诉客户端本次请求的处理结果。

    HTTP的响应状态码由5段组成:

    1xx 消息,一般是告诉客户端,请求已经收到了,正在处理,别急...。

    2xx 处理成功,一般表示:请求收悉、我明白你要的、请求已受理、已经处理完成等信息。

    3xx 重定向到其它地方。它让客户端再发起一个请求以完成整个处理。

    4xx 处理发生错误,责任在客户端,如客户端的请求一个不存在的资源,客户端未被授权,禁止访问等。

    5xx 处理发生错误,责任在服务端,如服务端抛出异常,路由出错,HTTP版本不支持等。

    以下是几个常见的状态码:

    200 OK 你最希望看到的,即处理成功!

    301 永久重定向 Location响应首部的值仍为当前URL,因此为隐藏重定向;

    302 临时重定向 显式重定向, Location响应首部的值为新的URL。

    303 See Other redirect到其它的页面,目标的URL通过响应报文头的Location告诉你。

    304 Not Modified 告诉客户端,你请求的这个资源至你上次取得后,并没有更改,你直接用你本地的缓存吧,我很忙哦,你能不能少来烦我啊!

    400 Bad Request 客户端请求有语法错误,不能被服务器所理解。

    401 Unauthorized 请求未经授权,这个状态代码必须和WWW-Authenticate报头域一起使用。

    403 Forbidden 服务器收到请求,但是拒绝提供服务。

    404 Not Found 你最不希望看到的,即找不到页面。如你在google上找到一个页面,点击这个链接返回404,表示这个页面已经被网站删除了,google那边的记录只是美好的回忆。

    500 Internal Server Error 看到这个错误,你就应该查查服务端的日志了,肯定抛出了一堆异常,别睡了,起来改BUG去吧!

    503 Server Unavailable 服务器当前不能处理客户端的请求,一段时间后可能恢复正常,举个例子:HTTP/1.1 200 OK(CRLF)。

    其它的状态码参见:http://en.wikipedia.org/wiki/List_of_HTTP_status_codes

    有些响应码,Web应用服务器会自动给生成。你可以通过HttpServletResponse的API设置状态码:

    // 设置状态码,状态码在HttpServletResponse中通过一系列的常量预定义了,如SC_ACCEPTED,SC_OK  
    void setStatus(int sc)
    

    1.2.3 HTTP响应报文头属性

    Cache-Control:响应输出到客户端后,服务端通过该报文头属告诉客户端如何控制响应内容的缓存。

    下面的设置让客户端对响应内容缓存3600秒,也即在3600秒内,如果客户再次访问该资源,直接从客户端的缓存中返回内容给客户,不要再从服务端 获取(当然,这个功能是靠客户端实现的,服务端只是通过这个属性提示客户端“应该这么做”,做不做,还是决定于客户端,如果是自己宣称支持HTTP的客户 端,则就应该这样实现)。

    Cache-Control: max-age=3600
    

    ETag:一个代表响应服务端资源(如页面)版本的报文头属性,如果某个服务端资源发生变化了,这个ETag就会相应发生变化。它是Cache-Control的有益补充,可以让客户端“更智能”地处理什么时候要从服务端取资源,什么时候可以直接从缓存中返回响应。

    关于ETag的说明,你可以参见:http://en.wikipedia.org/wiki/HTTP_ETag。 Spring 3.0还专门为此提供了一个org.springframework.web.filter.ShallowEtagHeaderFilter(实现原理很简单,对JSP输出的内容MD5,这样内容有变化ETag就相应变化了),用于生成响应的ETag,因为这东东确实可以帮助减少请求和响应的交互

    下面是一个ETag:

    ETag: "737060cd8c284d8af7ad3082f209582d"
    

    Location:在JSP中让页面Redirect到一个某个A页面中,其实是让客户端再发一个请求到A页面,这个需要Redirect到的A页面的URL,其实就是通过响应报文头的Location属性告知客户端的,如下的报文头属性,将使客户端redirect到iteye的首页中。

    Location: http://www.iteye.com
    

    Set-Cookie:服务端可以设置客户端的Cookie,其原理就是通过这个响应报文头属性实现的。

    Set-Cookie: UserID=JohnDoe; Max-Age=3600; Version=1
    
    Connection 使用keep-alive特性;
    Content-Encoding 使用gzip方式对资源压缩;
    Content-type MIME类型为html类型,字符集是 UTF-8;
    Date 响应的日期;
    Server 使用的WEB服务器;
    Transfer-Encoding:chunked 分块传输编码 是http中的一种数据传输机制,允许HTTP由网页服务器发送给客户端应用(通常是网页浏览器)的数据可以分成多个部分,分块传输编码只在HTTP协议1.1版本(HTTP/1.1)中提供;
    

    更多其它的HTTP响应头报文,参见:http://en.wikipedia.org/wiki/List_of_HTTP_header_fields

    1.2.4 如何写HTTP请求报文头

    在服务端可以通过HttpServletResponse的API写响应报文头的属性:

    // 添加一个响应报文头属性  
    void setHeader(String name, String value)
    

    像Cookie,Location这些响应都是有福之人,HttpServletResponse为它们都提供了VIP版的API:

    // 添加Cookie报文头属性  
    void addCookie(Cookie cookie)   
    
    // 不但会设置Location的响应报文头,还会生成303的状态码呢,两者天仙配呢  
    void sendRedirect(String location)
    

    2 HTTP传输处理

    在一个网络中。传输数据需要面临三个问题:

    1. 客户端如何知道所请求内容的位置
    2. 当客户端知道所请求内容的位置后,如何获取所请求的内容
    3. 所请求内容以何种形式组织以便被客户端所识别

    对于WEB来说,回答上面三种问题分别采用三种不同的技术,分别为:统一资源定位符(URI),超文本传输协议(HTTP)和超文本标记语言(HTML)。对于大多数WEB开发人员来说URI和HTML都是非常的熟悉。而HTTP协议在很多WEB技术中都被封装的过多使得HTTP反而最不被熟悉。

    HTTP作为一种传输协议,也是像HTML一样随着时间不断演进的,目前流行的HTTP1.1是HTTP协议的第三个版本。

    在Internet中所有的传输都是通过TCP/IP进行的。HTTP协议作为TCP/IP模型中应用层的协议也不例外。HTTP在网络中的层次如图所示:

    可以看出,HTTP是基于传输层TCP协议的,而TCP是一个端到端的面向连接的协议。所谓的端到端可以理解为进程到进程之间的通信。所以HTTP在开始传输之前,首先需要建立TCP连接,而TCP连接的过程需要所谓的“三次握手”。概念如图所示。

    在TCP三次握手之后,建立了TCP连接,此时HTTP就可以进行传输了。一个重要的概念是面向连接,即HTTP在传输完成之前并不断开TCP连接。在HTTP1.1中(通过Connection头设置)这是默认行为。所谓的HTTP传输完成,我们通过一个具体的例子来看。

    比如访问我的博客,使用Fiddler来截取对应的请求和响应。如图所示:

    可以看出,虽然仅仅访问了我的博客,但所获取的不仅仅是一个HTML,而是浏览器对HTML解析的过程中,如果发现需要获取的内容,会再次发起HTTP请求去服务器获取,比如上图中的那个common2.css。这上面19个HTTP请求,只依靠一个TCP连接就够了,这就是所谓的持久连接。也是所谓的一次HTTP请求完成。

    3 浏览器解析html代码,并请求html代码中的资源

    浏览器拿到index.html文件后,就开始解析其中的html代码,遇到js/css/image等静态资源时,就向服务器端去请求下载(会使用多线程下载,每个浏览器的线程数不一样),这个时候就用上keep-alive特性了,建立一次HTTP连接,可以请求多个资源,下载资源的顺序就是按照代码里的顺序,但是由于每个资源大小不一样,而浏览器又多线程请求请求资源,所以从下图看出,这里显示的顺序并不一定是代码里面的顺序。

    浏览器在请求静态资源时(在未过期的情况下),向服务器端发起一个http请求(询问自从上一次修改时间到现在有没有对资源进行修改),如果服务器端返回304状态码(告诉浏览器服务器端没有修改),那么浏览器会直接读取本地的该资源的缓存文件。

    浏览器具体渲染页面,内部工作原理,请参考:《前端必读:浏览器内部工作原理》

    本帖子中包含更多资源

    您需要 登录 才可以下载或查看,没有帐号?立即注册

    x
    守望者AIR技术交流社区(www.airmyth.com)
    回复 支持 反对

    使用道具 举报

  • TA的每日心情

    2016-7-28 13:41
  • 签到天数: 16 天

    [LV.4]偶尔看看III

    1

    主题

    12

    帖子

    330

    积分

    下士

    Rank: 3Rank: 3

    威望
    0
    贡献
    0
    金币
    237
    钢镚
    11
    发表于 2016-7-27 17:26:10 | 显示全部楼层
    很好、 借鉴 借鉴
    守望者AIR技术交流社区(www.airmyth.com)
    回复 支持 反对

    使用道具 举报

    您需要登录后才可以回帖 登录 | 立即注册

    本版积分规则

    
    关闭

    站长推荐上一条 /4 下一条

    QQ|手机版|Archiver|网站地图|小黑屋|守望者 ( 京ICP备14061876号

    GMT+8, 2017-10-18 08:23 , Processed in 1.296875 second(s), 31 queries .

    守望者AIR

    守望者AIR技术交流社区

    本站成立于 2014年12月31日

    快速回复 返回顶部 返回列表