Web是如何运行的: HTTP 和 CGI


本文翻译自How the web works: HTTP and CGI explained ,这篇翻译是按自己的理解进行翻译并不一定字字对应,如有需要请参考原文。

#介绍

##关于本文

这篇文章是为了让读者对web是如何工作的有一个基本的了解,写作这篇文章的原因是因为在不同的新闻组看到了人们想要了解这些但是却不知道从何去了解这些知识,所以我把收集到的资料整理成了这篇文章。

这篇文章涵盖了HTTP协议,一个用来传输网页的协议;还涉及到了服务器相关的知识和一些脚本技术。这篇文章假定读者基本了解一些网页知识,了解HTML和URLs.(一个连接就是一个文件的地址,你的要求就是要取得这个文件)

##一些背景

当你浏览网页时的情况基本上是这样的:你坐在你的电脑前,并希望看到在网络上某个地方的一个文档,而你有这个文档的URL。

既然你要读取的文件是在世界的其他地方,可能相距很远,所以你需要提供一些更多的细节,以便你可以得到这个文件。

第一个要告知浏览器的细节是这个文件的URL。你启动它,键入URL(至少你在某种程度上告诉浏览器你想去的地方,也许是通过点击一个链接)。

然而,画面依然没有完成,因为浏览器不能直接从存储这个文件的磁盘读取,这个磁盘在其他计算机上,而且如果这个计算机在其他大陆呢?所以,如果你要读取这个文件,那么存储这个文件的计算机必须运行一个Web服务器程序。一个web服务器是一个监听来自浏览器的请求,然后执行他们的计算机程序。

那么接下来发生的事情是:浏览器与服务器联系并请求服务器传送文件给它。然后,服务器将包含文档的响应发送给浏览器,浏览器愉快的把它显示出来给用户(你)看。而且服务器还告诉浏览器这个文件是什么样的文件,是(HTML文件、PDF文件还是ZIP文件等),然后浏览器根据它的配置,使用和该类型文档对应的程序显示出来。

浏览器会直接显示HTML文档,如果HTML文档它包含指向图像,Java小程序,声音剪辑等的内容,浏览器也会从它们所在的服务器请求这些文件(图像,Java小程序,声音剪辑等,这些文件通常在同一台服务器,但并不总是)。值得一提的是,这将是单独的请求,并给服务器和网络添加额外的负载。当用户点击另一个连接的时整个序列就又重新开始了。

这些请求和响应是使用一个叫HTTP的特殊语言作出的,HTTP是超文本传输协议HyperText Transfer Protocol的缩写。这篇文章基本上就是介绍它具体是如何工作的。其他类似比较常见的协议有FTP和Gopher(译注:文章是99年的,Gopher这个协议已经不常见了),还有那些工作方式完全不同的协议,本文都不会涉及到,可以参考其他文章。

值得一提的是,HTTP只定义了浏览器和Web服务器互相说什么内容,但没有定义他们怎么交流,也就是没有定义怎么把说的内容发送给对方。跨越网络传输比特和字节的实际工作是由 TCP和IP协议实现的,也就是说TCP/IP定义具体怎么把内容传输给对方,大多数其他互联网协议如(FTP,Gopher)也是依赖于TCP/IP实现传输的。

当你继续往下阅读的时候请注意,任何做和网页浏览器一样工作(如:从服务器检索文档)的软件程序在不同场合下被称为客户端(Client) 或者 用户代理(User Agent)。还要注意的是,服务器(server)应该指的是服务器程序(server program),而不是在其上运行一个服务器应用程序的计算机(server machine)。

#当我点击一个链接,会发生什么?

##步骤1:解析URL

首先,浏览器所要做的就是要查看新文档的URL,看如何取得这个文档。大多数URL有这样的基本形式:

protocol://server/request-URI      协议://服务器/请求的URI

http://haiyangxu.github.io/about.html

##第2步:发送请求

通常,协议是“http”的。通过HTTP检索文档的浏览器发送以下请求到服务器:

GET /request-URI HTTP/version

其中version告诉服务器用HTTP哪个版本。(通常情况下,浏览器还有更多的一些信息,后面会进行详细描述。)

这里很重要的一点是,这个请求字符串是服务器看到的所有东西。因此服务器并不关心,请求是来自一个浏览器、一个链接检查程序、搜索引擎爬虫或者是你在手动键入这个请求字符串。服务器只是执行请求,并返回结果。

##第3步:服务器响应

当服务器接收到HTTP请求,并找到适当的文档就返回它。然而,HTTP响应被要求具有特定的形式。它必须看起来像这样:

HTTP/[VER] [CODE] [TEXT]
Field1: Value1
Field2: Value2

...Document content here...

第一行显示了所使用的HTTP版本,随后是3位数字(HTTP状态代码)和可以给人看的对应于状态码的短语。通常情况下[code] 和[TEXT]为200 OK,这意味着一切运行良好。第一行后面跟着一些所谓的header,包含关于文档的信息。用一个空行结束header,然后后面就跟着文档的内容。下面是一个典型的header:

HTTP/1.0 200 OK
Server: Netscape-Communications/1.1
Date: Tuesday, 25-Nov-97 01:22:04 GMT
Last-modified: Thursday, 20-Nov-97 10:44:53 GMT
Content-length: 6372
Content-type: text/html

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
<HTML>
...followed by document content...

我们从这个响应头可以的第一行看到,请求成功了。第二行是可选的,并且告诉我们,在服务器运行的Web服务器是Netscape-Communications,1.1版。然后,我们得到了什么服务器当前的日期和文件的最后修改时间,然后是文件的字节大小,最后,最重要的字段是:Content-type。

内容类型(Content-type)字段被浏览器用来判断接收到的文件格式,HTML文件格式是被定义为text/htmltext/plain为普通文本,GIF是image/gif等。这样做的优点是,该URL可以具有任意的结尾而浏览器将仍然能得到文档的正确格式并正确解析。

这里有一个重要的概念是,对于浏览器,服务器可以作为一个黑盒子。即:浏览器请求一个特定的文件,服务器要么是正确返回文件或者返回一个错误消息。服务器如何产生文档对浏览器来说仍然是未知的。这意味着服务器程序可以从一个文件中读取、运行一个程序生成,通过分析一些命令配置编译等种种方式生成这个文档。这使服务器管理员有很大的自由尝试各种不同的服务,因为用户不关心(甚至不知道)他看的的页面是如何生成的。

##服务器做了什么

当服务器被配置好的时候,它通常被配置到磁盘上的某个地方使用一个目录作为其根目录,并且每个目录会有一个默认的文件名(例如index.html)。这意味着,如果你向服务器请求文件“/”(如http://www.domain.tld/),你会获得在服务器根目录下的index.html文件。通常情况下,请求/foo/bar.html,服务器会给你服务器根目录下的foo的目录中的bar.html文件。

通常情况下,是那样。但是也可以将服务器的/foo/映射到其他磁盘或者其他目录上,甚至可以使用服务器端程序来回答对这个目录中的所有请求。服务器甚至可以完全不映射请求到一个目录结构,而可以使用一些其他的方案。

##HTTP版本

到目前为止,有三个版本的HTTP。第一个是HTTP/0.9,这是最原始,从来没有真正成为任何标准的规定。而HTTP/1.0,它已作为在RFC1945标准发布。HTTP/1.0是HTTP版本是今天普遍使用(通常与一些1.1的扩展),而HTTP/0.9基本没有被浏览器使用。

RFC 2068描述了HTTP/1.1,它延伸并在若干方面改进HTTP/1.0 ,和1.0的主要区别是默认采用持久连接,让客户端请求的连接打开后进行保持,使其不必重新建立下一个请求,如果几次请求必须迅速发出,这样可以节省一些等待时间和服务器负载。

HTTP/1.1相较于HTTP/1.0协议的区别主要体现在

本文主要介绍了HTTP/1.0,除了一些部分涵盖了HTTP/1.1的扩展,这些将被明确标示。

##客户端发送请求

###请求的格式

基本上,所有的请求看起来像这样:

[METH] [REQUEST-URI] HTTP/[VER]
[fieldname1]: [field-value1]
[fieldname2]: [field-value2]

[request body, if any]

METH(用于请求方法)给出了请求使用的方法,这里有几种请求方法,并且都做不同的事情。在上述的例子中使用的GET,但下面还有一些说明。REQUEST-URI是文档的服务器上的标识,如/index.html或什么的。VER是HTTP版本,就像在上面介绍的服务器的响应头中,下面的header和之前说服务器响应中一样的

请求体仅用于将数据传输到服务器的请求,比如POST和PUT。(在下面描述)。

###GET:取得一个文档

有几个请求的类型,最常见的一种是GET。GET请求基本意思是“给我这个文件”,看起来像这样:GET document_path HTTP/VER。对于URL http://www.yahoo.com/ 的document_path将是/,对于 http://www.w3.org/Talks/General.html 它是/Talks/General.html

不过,这第一行通常不是一个客户端(UA)唯一发送的请求,但它是唯一要紧的,必不可少的。UA可以包括多个报头字段(header field)的请求给服务器发送信息。这些字段的格式为fieldname: value,并都放在第一个请求行之后单独的行中。

一些GET可以使用的报头字段是:

把所有这些碎片拼凑起来:下面是一个典型的GET请求,由我的浏览器(Opera)的发放:

GET / HTTP/1.0
User-Agent: Mozilla/3.0 (compatible; Opera/3.0; Windows 95/NT4)
Accept: */*
Host: birk105.studby.uio.no:81

###HEAD:检查文件

有人有时候可能要查看特定文件由服务器返回的头,而无需实际下载的文件。这正是HEAD请求方法提供。HEAD工作原理和GET完全一样,区别只在于服务器只返回头而不是文件内容。

这对像连接检查器这样的程序来说来说就很有用,他们只需要服务器返回的响应头和不需要文件内容。

###动手写HTTP请求

实际上,我们可以通过telnet连接到web服务器的80端口,自己手动发送HTTP请求。这在Unix上很好用而在windows上并不怎么好使。详细操作流程请看另一篇博文HTTP协议Hands On

##由服务器返回的响应

###概要

服务器返回的响应包括含状态码的一行,报头字段的列表,一个空行,然后是请求的文档(如果有的话)。有点像这样:

HTTP/1.0 code text
Field1: Value1
Field2: Value2

...Document content here...

###状态代码

###响应头字段

下面列出了一些常用的字段,更多的请参考相关文档

##缓存:服务器和客户端之间的代理

###浏览器缓存The browser cache

你可能已经注意到,当你点击你不太久之前浏览过的页面时候,打开速度比之前更快很多。这是因为浏览器第一次下载这个页面时就在本地存储了一个副本。这个被保存的本地副本就称之为缓存。通常浏览器会设置有浏览器缓存的最大尺寸和文件的最大缓存时间。

这意味着,当一个新的网页被浏览器访问、并存储在缓存中的时候,如果缓存满了(接近最大限制),浏览器就会删除它认为近期不太可能被再次被访问的一些文件,腾出缓存空间。此外,如果你去访问存储在缓存中的网页,浏览器可能会发现,这个页面设置的最大缓存时间是7天,而从上次访问到现在已经过去了8天,所以这个网页需要重新从服务器加载,即使缓存中有它的副本。

究竟缓存是如何工作的,浏览器之间有些差异,但是上面是基本的想法,而且也是一个很好的想法,因为它不仅为用户节省了时间,也减少了网络流量。浏览器的缓存涉及到一些HTTP细节,将稍后介绍。

###代理缓存Proxy caches

浏览器缓存是一个不错的功能,但是当很多用户访问同一个网站,就会有很多浏览器从这个网站不断的下载更新缓存,显然这不是最优的方法。

解决的办法是让用户共享缓存,这正是代理缓存做的事情。浏览器还是有它自己的缓存,只不过不在浏览器缓存里的文件的HTTP请求并不是直接发送到服务器了,而是发送到代理缓存。如果代理缓存里面存储了这个文件,它会直接返回给浏览器,如果没有代理缓存会把这个请求转发到服务器请求下载这个文件到代理缓存中然后返回给浏览器。

所以,代理缓存就是很多用户共享的一个共同的缓存,可以显著的减少网络流量压力。但是代理缓存会扭曲基于日志的分析。

一个更好的解决办法就是用一个层级架构的代理缓存系统,而不仅仅只使用单独的一个代理缓存。想象一下,一个大型的ISP为国家的每个区域设立一个代理缓存,让这些区域级的代理缓存共享一个国家级的代理缓存,而不是直接访问源服务器;这种解决方案可以更进一步的减少网络流量。

##服务器端编程

###它是什么和为什么这样做?

服务器端的脚本或程序就是运行在Web服务器上,响应来自客户端请求的程序。这些脚本产生正常的HTML(有时也可以是HTTP头)作为输出,然后反馈给客户端,就好像客户端是请求一个普通页面一样。其实,没有办法让客户端软件判断服务器是否使用了脚本程序。

如JavaScript、VBSctrip和Java Applet等运行于客户端的技术,并不是服务器端脚本的例子。服务器端的脚本和客户端脚本的很大区别是客户端程序和服务器程序运行在不同的机器上,所以,如果一个程序处理的数据全都位于服务器机器上,那么它就应该是服务器端的脚本而不是客户端脚本。如果程序需要经常和用户进行交互,那么使用基于客户端的技术更好,这样可以减少向服务器发送请求。

所以,一般情况下,需要大量数据处理和很少交换的程序应该基于服务器开发;而处理少量数据和频繁交互的程序应该放在客户端。

还有一种情况经常被遗漏的就是,如果你需要一个程序处理数据也需要很多用户交互该如何做?近在眼前的有一种叫XML的技术,关于此请参考其他文档了解更多。

###它是如何工作的

根据使用的技术的不同,服务器端脚本的具体细节和工作原理有很大不同,而且有很多技术可以使用。但是,有些东西是不变的:服务器收到一个请求,但是需要注意的是,请求的URL并不对应于一个通常的文件,而是映射到一个脚本程序。

然后服务器启动这个脚本程序,把请求头提供信息和URL全部传给脚本程序,脚本程序运行并产生HTML文档和HTTP响应头,然后服务器把它返回给客户端。

###CGI

通用网关接口(CGI,Common Gateway Interface)是一种Web服务器和服务器端编程进行交互的方式。CGI完全独立于编程语言,操作系统和Web服务器。目前,它是最常见的服务器端编程技术,几乎每一个Web服务器都支持。此外,所有服务器用几乎同样的方式实现它,这样你可以为一个服务器编写CGI脚本,然后分发到任何Web服务器上运行。

就像上面说的,服务器需要一种方法来知道哪些URL映射到脚本而其中另一些URL只映射到普通的HTML文件。对于CGI通常是通过在服务器上创建CGI目录。具体做是在服务器进行设置,并告诉服务器,当请求一个特定的顶层目录下的文件时就执行这些CGI脚本(位于磁盘上的某个地方)。(缺省目录通常是/ cgi-bin/,所以一看就知道,像这样的URL:http://www.varsity.edu/cgi-bin/search指向一个CGI脚本,需要注意的是可以任意设定该目录。)有些服务器也可以设置为不使用CGI目录,而要求所有的CGI程序都用已.cgi结尾的文件名。

CGI程序只是普通的可执行程序(或解释性程序,比如Perl或Python,只要服务器知道如何启动程序),因此你可以使用几乎任何你想要的编程语言。在CGI程序被Web服务器启动之前,web服务器定义了一些包含从请求中接收到的信息的变量。这方面的例子有客户端的IP地址,请求头等,如果请求的URL中包含一个问号(?),那么问号之后的一切都会设置成变量。

这意味着,关于请求的额外信息可以被放入该URL的链接。这是像点击计数器用来判断是哪些项目被点击的常用方法之一。因此,用户可以在他/她的页面插入一个图像,并具有SRC属性是一个链接到这样的CGI脚本:SRC =http://stats.vendor.com/cgi-bin/counter.pl?username 。那么脚本就可以知道哪些用户被击中,增量和显示正确的计数。

CGI输出其返回(HTTP头和HTML文档)到服务器的方式是非常简单的:它把它写到标准输出。换句话说,在一个Perl或Python的脚本中,你只需要使用print语句。在C语言中使用printf或者一些等效的函数(C++使用cout«),而Java将使用System.out.println。

###其他技术

CGI是肯定不会是进行服务器端编程的唯一途径,并一直为人诟病低效率。有时候确实是那样,因为每次请求CGI程序就会重新被加载到内存然后执行。一个更快的替代方案是直接使用服务器API本身进行编程,即让程序成为服务器进程的一部分,直接利用提供的API来完成工作,这样当然是服务器相关的,而且如果使用像c/c++(通常都是)导致的程序错误可能使整个服务器崩溃。

基于服务器API编程的主要优点在于,当请求到达时,程序和需要的数据已经在内存中,这样确实会快很多。

有些服务器允许不会导致崩溃的脚本语言。一个例子是AOLserver的,它采用TCL。也有可用的服务器,比如Apache,它让你可以使用Perl或Python进行服务器的API编程,有效地消除了因编程错误引起服务器崩溃的风险。

也有很多很多专利的(和非专有)脚本语言和技术,以及各种web服务器。一些最有名的是ASP,MetaHTML和PHP3。

[本文是翻译的第一部分]


Haiyang Xu 11 May 2014