转自http://www.iteye.com/topic/1116710,文章很好,是一系列文章。
《编程机制探析》第二十三章 HTTP
HTTP是Web应用开发中最为重要的协议。但是,在实际的Web应用开发中,相当多的程序员根本就不了解HTTP是怎么回事,也照样编写Web程序。我就曾经是其中的一员。这种现象的产生,与现代软件业的开发模式大有关系。
这事儿,说起来话就长了。软件开发管理,一向是管理界的一大难题。因为,没有哪个程序员愿意被管理。每一个程序员都觉得自己是人才,而不是人力资源。人才,尤其是智力人才,应该是有发挥自由度的,怎么能像体力工人一样被管理呢?
但是,胳膊扭不过大腿。公司管理层就不信这个邪,非要解决这个管理上的难题。随着软件工程管理的发展,大量的程序员都被管理起来了,成为办公室蓝领工人,甚至更惨,成为码农。怎奈心比天高,却命比纸薄。呜呼哀哉。
为了照顾程序员们的情绪,有人专门著书立说,为程序员力争更多的自由和权益,比如,有一本叫做《人件》(peoplesoft)的书,就很有名。程序员们都希望,这本书能够摆在公司老板的案头。但据我所知,公司老板一般对这些东西都不感兴趣,人家有更高层次的精神追求,早就超越了这个执着于管理流程细节的层次。人家喜欢看的是《狼图腾》这类宣扬狼性企业文化的书籍。这样能够强化他们在“商场如战场”的竞争心态和优胜心理。我这么写可能会有失偏颇。因为我的样本很有限。我只偶然接触过那么几个软件业老板,但奇怪的是,他们都推崇《狼图腾》。
这是为啥子呢?很值得深究。不过,那不是本书的内容。我这里只提一点,到了那个层次,他们面对的真实世界和我们工薪阶层是不同的。他们直面市场,直面社会上的三道九流、黑白两道。他们的世界观和道德观,和我们这些生活圈子狭窄的工薪阶层是大为不同的。
这一点,我们要理解。理解万岁。虽然他们不一定需要理解我们,但我们一定需要理解他们。这就是真实的世界。我们不用直面市场,但我们需要面对上司和老板。我们想的是,如何证明自己的价值,从老板口袋中掏出更多的薪水。老板想的是,我们如何任劳任怨地跟在他的后面,跟他一起向竞争对手撕咬。
话题扯远了,我们回到软件工程管理的话题上来。在现代的软件开发模式中,大量的程序员都在固定的开发框架中进行开发工作,每个人面对的都是一个很狭窄的领域。大部分程序员就如同一个巨大的装配线上的螺丝钉,随时可以被替换。老板不用担心这样的程序员能够带走公司的核心技术。
不得不说,这是软件管理上的巨大进步。尤其是对于做项目的公司来说,尤其如此。设身处地想一想,如果我们处在老板的位置,是不是也希望采用这样的模式?
但是,这种开发管理模式对于程序员的技术全面发展来说,却是有阻碍的。怎么克服呢?公司不是学校,可能会提供必要的职业培训,但没有义务保证程序员的全面发展。程序员只能靠自己来弥补一些必要的重要的知识。
对于HTTP的重要性,我一开始是没有认识的。我那时候主要专注于各种Web开发框架的设计理念。我觉得,那些程序设计相关的东西,才是程序员的主页。至于HTTP,只是一种基础的协议,没必要深究。
当我开始尝试自己设计Web开发框架的时候,我发现,我绕不开HTTP。我必须深入了解HTTP,才能够继续前进。我还是不愿意在HTTP协议上花时间,我只是借鉴其他开源框架的相关代码。结果,我很快就发现,这样的做法效率极低,事倍功半,很多代码我不明其真意。于是,我转换了做法,开始认真地学习HTTP。思路一下子打开了。再看之前的代码,豁然开朗。
然后,我再回头看以前写的代码,包括自己写的和其他人写的代码,越看越心惊。我发现了大量的地雷代码。比如,有些人为了方便起见,在代码中滥用HTTP Session来存储数据。这些代码在开发环境中是测不出问题的。在生产环境中,如果不遇到极端环境,也是测不出问题的。只有在某些极端环境下,才可能会导致问题。一个人这么用,没出过问题,其他人觉得方便,也跟着这么用。
类似于这样的误用,比比皆是。究其原因,都是因为对编程模型没有真正的理解。比如,我在前面的《线程》章节中提到的,有些人把线程同步锁加在局部变量上,是因为没有理解线程同步模型所致。
要避免这类很难测出问题的地雷代码,有两种方法。
第一种方法是把这类问题全都列出来,放在编程规范中作为反面例子,要求所有程序员注意。这种方法治标不治本,知其然不知其所以然。
第二种方法就是深入理解编程模型,这样从根本上就避免了问题的发生。
第二种方法花的时间长,费的功夫多,但是非常值得。对于Web应用开发人员来说,HTTP就是值得深研的Web通信协议。
HTTP的基本结构,是非常简单的。HTTP的官方定义在W3网站上,请参阅:
http://www.w3.org/Protocols/
HTTP协议包括两种消息包,一种是HTTP Request(请求),一种是HTTP Response(响应,回应)。
服务端不能主动连接客户端,只能被动等待并答复客户端请求。客户端连接服务端,发出一个HTTP Request;服务端处理请求,并且返回一个HTTP Response给客户端;本次HTTP Request-Response Cycle(请求-回应周期)结束。
HTTP Request和HTTP Response的基本结构很简单。HTTP Request主要包括网址、用户信息等内容。HTTP Response主要包括状态码、HTML页面等信息。
但是,对于Web开发人员来说,追踪HTTP Request和HTTP Response的内容却非常重要。我们可以在App Server端,打开HTTP日志,就可以追踪HTTP Request和HTTP Response。我们还可以在浏览器端追踪HTTP内容。方法是使用各种HTTP监视工具。我这里提供一些关键字,供读者借鉴:Fiddler,HTTP Watch,HTTP Debugger,HTTP Sniff,Fire-ware,Network Monitor,HTTP Monitor。
有了这些工具,我们就可以清楚地看到HTTP协议的来来去去,获得直观上的认识。
HTTP本身是一种无状态的协议,一次请求/响应周期完成之后,TCP/连接就断了,事情就结束了。浏览器下次再访问同一个网站的时候,就如同访问一个新的网站,而网站也如同接受一个新的请求。这样就达到了一种日久常新、每次都是新面孔的效果。
关于婚姻家庭,有一种说法,叫做七年之痒。就是喜新厌旧的意思。对于已经到了“相看两厌”状态的夫妻来说,HTTP的这种无状态机制是非常有效的,每天都跟新婚一样。
在网站还是提供内容(content)的时代,HTTP的无状态特性工作得很好。但是,好景不长。随着Web领域的蓬勃发展,网站不再限于提供内容,而是开始向应用(application)进军。
应用程序本来是C/S结构(Client/Server,客户端/服务器端)的天下。但是,C/S结构的弱点在于吞吐量太小,能够同时响应的用户请求数量太少,因为C/S架构的应用程序总是保持着长期的TCP/IP连接,而一个系统能够支持的TCP/IP连接数量是有限的。
HTTP是一次请求/回应完成就立刻断开TCP/IP连接的协议。因此,基于HTTP协议的网站能够支持更大的用户访问量。一些应用开发人员就把目光转向了HTTP协议,希望能够开发出基于HTTP协议的Web应用程序(即Web app)。
但是,这就有一个矛盾。Web app需要保持用户和服务器端的会话(session)状态,而HTTP协议是无状态的。如何解决这个矛盾呢?如何让无状态的HTTP协议支持会话(session)状态呢?
聪明的研发人员想出一个简单易行的方案:发放临时号码牌。
这个模型很简单,却很有效。充分体现了设计人员的聪明才智。我们来看一下这个模型的工作机理。
假设App Server是一个大型游乐场的存包处,浏览器发出的HTTP Request是一个顾客。
当顾客第一次来到存包处的时候,存包处管理员为顾客分配一个储物柜,相当于web server用来存放用户会话(Session)状态的存储空间。然后,管理员把一个该储物柜对应的号码牌交给这个顾客,作为取包凭证。这个号码牌有个学名,叫做Session ID。
管理员在顾客回去的时候——即HTTP Response返回到浏览器的时候,会要求顾客随身带着这个号码牌。并嘱托顾客下次来的时候,一定要记着带着号码牌。
顾客下一次来的时候,要把随身带着的这个号码牌,交到存包处。管理员一看到号码牌,就知道,这是个老顾客了,就根据号码牌(Session ID)找到相应的储物柜,即存放用户(Session)会话状态的存储空间。管理员根据顾客的要求,更新储物柜里的Session状态。
同现实世界中的储物柜一样,App Server中的Session存储空间也会过时的。如果相隔时间太久,Session就会过期了,号码牌(Session ID)就会失效了。App Server就会给顾客重新分配一个Session存储空间和一个Session ID。
就这样,利用一个很简单的模型,就实现了用户会话(Session)状态的保持。
我们下面来看具体实现方案。无论是Session存储空间,还是Session ID,都是服务器端分配的。Session ID的形式,Session存储空间的位置,都是由服务器自行决定的。这部分的实现很简单,没什么可说的。
我们关心的问题是:HTTP Response如何把Session ID带回到浏览器;浏览器如何存放这个Session ID;浏览器下次访问这个网站的时候,又如何让HTTP Request带上这个Session ID。
我们先来考虑最主要的问题:浏览器如何存放和使用这个Session ID?
当浏览器初次访问一个网站,获取到HTTP Response的时候,同时也就获取了一个Session ID。浏览器需要把这个Session ID存储起来。下次,再访问这个网站的时候,要把Session ID放在HTTP Request里面发过去。
清楚了这个流程之后,浏览器存储Session ID的方案就呼之欲出了。浏览器只需要在本进程内存中维护一个“网站地址->Session ID”的映射表,就可以完成这个需求。
浏览器每次访问网站的时候,都去查这个“网站地址->Session ID”映射表。如果该网址存在于映射表中,就把对应的Session ID取出来,放到HTTP Request里面,发送给网站服务器。如果该网址不存在于映射表中,就直接发送没有Session ID的HTTP Request。网站服务器就会给这个浏览器新分配一个Session ID,放在HTTP Response里面,发给浏览器。浏览器接收到HTTP Response里面的Session ID,就放到“网站地址->Session ID”映射表里。
解决了这个主干问题之后,我们再来看枝节问题:HTTP Response是如何把网站服务器分配的Session ID带给到浏览器的;HTTP Request又是如何把Session ID带给网站服务器的。
要解答这两个问题,我们需要了解HTTP Request和HTTP Response的更具体的结构。
HTTP Request包括Request Line、Request Headers、Message Body等三个部分。
(1)Request Line
这一行由HTTP Method(如GET或POST)、URL、和HTTP版本号组成。
例如,GET http://www.w3.org/pub/WWW/TheProject.html HTTP/1.1
GET http://www.google.com/search?q=Tomcat HTTP/1.1
POST http://www.google.com/search HTTP/1.1
GET http://www.somsite.com/menu.do;jsessionid=1001 HTTP/1.1
(2)Request Headers
这部分定义了一些重要的头部信息,如,浏览器的种类,语言,类型。Request Headers中还可以包括一种叫做Cookie的Request Header。例如:
User-Agent: Mozilla/4.0 (compatible; MSIE 5.5; Windows NT 5.0)
Accept-Language: en-us
Cookie: jsessionid=1001
(3)Message Body
如果HTTP Method是GET,那么Message Body为空。
如果HTTP Method是POST,说明这个HTTP Request是submit(提交)一个HTML Form(表单)的结果,那么Message Body为HTML Form里面定义的Input属性。例如,
user=guest
password=guest
jsessionid=1001
如果你不知道什么叫做HTML Form(表单)的话,请回忆一下你登陆某个网站时的对话框,你需要输入用户名和密码,那些输入框就属于一个HTML Form。这些都是HTML的基本元素。对于Web开发程序员来说,对于HTML的基本了解也是必要的。
注意,如果把HTML Form元素的Method属性改为GET。那么,Message Body为空,所有的Input属性都会加在URL的后面。你在浏览器的URL地址栏中会看到这些属性,类似于
http://www.somesite/login.do?user=guest&password=guest&jsessionid=1001
从理论上来说,这3个部分(Request URL,Cookie Header, Message Body)都可以用来存放Session ID。由于Message Body方法必须要求一个包含Session ID的HTML Form,所以这种方法不通用。
我们再来看HTTP Response。
HTTP Response包括Response Status、Response Headers、Message Body两个部分。
(1)Response Status
这就是一个数字,表示成功与否的状态码。这个状态码有特定的含义,不能用于传输Session ID。
(2)Response Headers
这部分可以做文章。可以加一个叫做“set-cookie”的response header,比如,“set-cookie: jsessionid=XXXX”
(3)Message Body
这部分就是HTML文本了。这里面也可以做文章。
可以在HTML中所有的网址链接后面都加上“;jessionid=XXXX”的后缀。比如,<h ref=”\mailbox;jessionid” />。
如果页面中存在HTML Form,还可以在action的网址链接后面加上“;jessionid=XXXX”的后缀。还可以加一个隐藏的hidden input。比如,< hidden name=”jessionid”, value=”XXXX” />。
我们分析上面的Request和Response结构,可以把它们的结构对应起来。
Request Headers <-> Response Headers
Request Line <-> Response HTML
这正是HTTP Session的两种主要实现方案——cookie header和URL Rewriting(URL重写)。
(1)Cookie Header
Web Server在返回Response的时候,在Response的Header部分,加入一个“set-cookie: jsessionid=XXXX”的header属性,把jsessionid放在Cookie里传到浏览器。
浏览器会把会把Cookie存放,下一次访问Web Server的时候,再把Cookie的信息放到HTTP Request的“Cookie”header属性里面,“cookie: jsessionid=XXXX”,这样jsessionid就随着HTTP Request返回给Web Server。
(2)URL重写
Web Server在返回Response的时候,检查页面中所有的URL,包括所有的连接,和HTML Form的Action属性,在这些URL后面加上“;jsessionid=XXX”。
下一次,用户访问这个页面中的URL。jsessionid就会传回到Web Server。
URL重写需要处理整个HTML里面的所有URL链接,效率很低,这种方案很少采用。
最通用的HTTP Session实现方案是Cookie Header。这种方案需要浏览器一方的支持。
Cookie到底是什么?这可能是困扰很多人许久的问题。这个词经常出现,却很容易引起混淆。我很早就知道cookie这个词,但是,我对cookie的概念一直很模糊。直到清楚了HTTP协议之后,我才知道cookie是怎么回事。
cookie的原意是小饼干,在web领域中,引申为一小段信息的意思,相当于在服务器端传给浏览器、并要求浏览器每次都回传的小纸条。
Cookie分为两种,Session Cookie(会话cookie)和Persistent Cookie(持久cookie)。两者的格式、使用模式都是一样的。两者的区别主要在于生命周期上。
Cookie是由服务器端首先发出的小纸条,由HTTP Response里面的set-cookie header来指定。浏览器只负责在每次访问某个网站的时候,把网站服务器端发来的cookie原封不动地发回去。Cookie的内容,包括生命周期和作用域,都是由服务器端指定的。
在HTTP Response的set-cookie header里面,可以包括Expires和Max-Age这样的定义Cookie生命周期的属性,还可以包括Domain和Path这样的定义作用域和存储位置的属性。
这些属性的含义都很简单,都是些资料性知识。读者感兴趣的话,可以自行查阅HTTP规范。其中,Domain的含义需要稍微说明一下。一般来讲,Domain(域名)就是主机名,Host Name。比如,blog.myweb.com。这个blog.myweb.com就是一个网站域名。这个网站还可以把cookie的Domain定义为比本网站域名更上一级的域名,比如,myweb.com。
Session Cookie的生命周期等同于浏览器。浏览器一关,Session Cookie也就消失了。
Persistent Cookie的生命周期大于浏览器。浏览器关闭了,Persistent Cookie还在。下一次浏览器打开,还可以用到Persistent Cookie。
Session Cookie通常用来存放Session ID,因此而得名。
Session Cookie的内容是什么?前面我们讲到了浏览器内存中的“网站地址->Session ID”映射表。那只是一种简化的表达。实际上,对于浏览器来说,它根本就不关心什么Session ID。它把服务器端发来的cookie看做一个整体,它把cookie直接放在“网站地址->cookie”映射表中。这个映射表就是Session Cookie。因此,Session Cookie的内容就是“网站地址->cookie”。
浏览器访问网站的时候,会把整个cookie放到HTTP Request里面一起发过去。
由于Session Cookie的生命周期等同于浏览器的生命周期,因此,Session Cookie一般是存放在浏览器进程的内存中的。也有的浏览器把Session Cookie存放在文件中,等到浏览器关闭时,再把Session Cookie文件删除。
Persistent Cookie的生命周期大于浏览器的生命周期,一般是存储在文件系统中的。
Persistent Cookie的使用方式和同Session Cookie类似。当浏览器访问某个网站的时候,浏览器会在某一个文件目录下查找是否存在该网站的文件cookie。如果存在,就把该文件cookie的内容发送到网站服务器。
同Session Cookie的映射表一样,每一个Persistent Cookie也是对应一个网站地址的。
根据对应网站地址的性质的不同,Persistent Cookie又分为两种——主站Cooke和他站Cookie。
当浏览器访问一个网站(称为主站)的时候,主站可以通过HTTP Response把一个Persistent Cooke发给浏览器。这种Persistent Cookie叫做主站Cookie。主站Cookie里面通常包含用户名、密码等信息,以便自动登录。
浏览器得到的主站页面中有时候会包含其他网站的资源链接(称为他站),在访问他站资源的过程中,他站也有可能通过HTTP Response把一个Persistent Cookie发给浏览器。这种Persistent Cookie叫做他站Cookie。他站通常是一些广告网站。他站Cookie通常都是一些用来统计用户访问行为的Cookie。
综上所述,浏览器支持的Cookie有三种:Session Cookie,主站Persistent Cookie,他站Persistent Cooke。
浏览器一般都提供了这三种Cookie的配置界面,用户可以控制浏览器支持哪种类型的Cookie。
一般来讲,Session Cookie是一定要打开的。大部分(如果不是所有的)需要登录的网站,都需要浏览器支持Session Cookie。
如果需要自动登录的话,主站Persistent Cookie是要打开的。至于他站Persistent Cookie,除非你乐于提供自己的网站访问行为模式,否则,不要支持他站Persistent Cookie。
可以看到,HTTP Session的工作模型说起来简单,但是,实现起来,还真是挺麻烦的。这些麻烦都是Session ID这个东西带来的。
现在,我们重新思索这个问题,我们真的需要Session ID这个东西吗?Session ID不过是服务器端用来识别浏览器身份的一个随机号码牌。难道我们非得用这种方式来识别浏览器吗?我们就没有别的方法来识别浏览器身份了吗?
方法是有的。而且更加简单直观。前面章节中详细讲解了一次网站访问的过程。我们知道,浏览器发出的网络协议包里面是包含了本机IP地址和本浏览器TCP端口信息的,而且还包括了该机所在局域网的路由器公网IP地址。服务器完全可以把这些信息组合作为Session ID存放起来,也可以根据该信息组合识别出浏览器的身份。
在这种方案中,根本就不用分配什么临时的Session ID,也不用让HTTP Response和HTTP Request把Session ID传来传去。浏览器也不用考虑如何存储和安排Session ID。显然,这个方案更优。
那么,为什么不采用这种方案呢?我们仔细想一想,就可以发现这种方案的问题所在。
TCP端口和IP地址信息,是属于TCP/IP层的内容,并不是HTTP协议层的内容。这就意味着,Session状态的保持,必须在TCP/IP层上实现。
这就和HTTP协议层没什么关系了。Session不叫做HTTP Session,而叫做TCP/IP Session了。而且,Session的实现绑定在TCP/IP层上,不太符合网络协议分层的初衷。
浏览器第一次访问一个网站的时候,服务器就分配了一个Session ID给浏览器。这时候,Session已经通过Session ID建立了。分配了Session ID不等于分配了存储空间。
servlet中的getSession() API是用来获取服务端Session存储空间的。这个存储空间(一般用来存放用户名和角色等认证信息)只有在用到的时候才会分配。
服务器重启,Session肯定会被销毁的。不过,Session销毁的情况一般都是因为Time Out(过期)。用户长期不反应,Session ID就会过期。对应的Session存储空间也会释放。