1.2 什么是HTTP
前面章节没有对HTTP工作方式的细节做过多描述,这样我们才能对HTTP如何适用于广阔的因特网有自己的理解。本节,将简单介绍HTTP的动作方式,以及如何使用它。
如上所述,HTTP代表超文本传输协议。如其名字所表示的,HTTP最初是用来传输超文本文档(文档中包含指向其他文档的链接)的。HTTP的第一个版本,除这些文档以外不支持任何其他的类型。很快,开发者们意识到这个协议可以用来传输其他文件类型(如图片),所以如今超文本作为HTTP的第一个H,已经有点“名不符实”了。但是考虑到HTTP的广泛应用,现在也来不及给它改名字了。
HTTP基于可靠的网络连接,通常由TCP/IP提供,而TCP/IP建立在某种物理连接之上(如以太网、WiFi等)。因为通信协议被分到不同的层,所以每一层可以专注做好当前层的事情。HTTP不关心如何建立网络连接的底层细节。基于HTTP的应用应该关注如何处理网络错误或者连接错误,而协议本身并不考虑这些事情。
OSI模型(Open System Interconnection,开放式系统互联通信参考模型)是经常用来描述网络分层的概念模型。此模型包含7层,但这些层并不严格对应所有的网络,尤其是因特网。TCP跨越至少两层(有可能是三层),这取决于你如何定义这些层。图1.2简单地描述了此模型如何和Web流量对应,HTTP处在这个模型的哪个位置。
图1.2 网络数据传输的分层
对于每一层的具体定义,有一些争论。在因特网这样的复杂系统中,不是每件事情都能像开发者喜欢的那样简单定义。事实上,IETF(Internet Engineering Task Force,因特网工程任务组)提醒过,不要太过于纠结网络分层[4]。但是,如果我们想对HTTP处在这个模型的哪个位置,以及它是如何基于下层的协议工作的,有个概括性的理解,OSI模型对我们还是很有帮助的。许多Web应用基于HTTP构建,所以我们在说应用层的时候,更多的是指网络而不是JavaScript应用。
从本质上来讲,HTTP是一个请求-响应协议。Web浏览器使用HTTP协议,向服务器发送一个请求。服务器响应一个消息,这个消息包含了浏览器所请求的资源。HTTP成功的关键在于简单。在随后的章节中我们会看到,这种简单性反而成了HTTP/2要解决的问题。为了效率,HTTP/2牺牲了一些简单性。
创建一个连接后,HTTP请求的基本语法如下:
这里的符号代表一个回车/换行(Enter或者Return键)。HTTP的基本形式就是如此简单。使用HTTP的其中一个方法(这里是GET),后跟所请求的资源地址(/page.html)。此刻已经连接到了对应的服务器,这里使用了叫作TCP/IP的技术。所以我们可以很简单地从服务器请求资源,而不用关心连接的创建和管理。
HTTP协议的首个版本(0.9)仅支持这种简单的语法,并且只有一个GET方法。这时候你可能会问,为什么在HTTP/0.9请求中还需要声明GET,不显得多此一举吗?后来的HTTP版本中引入了其他方法,所以请给HTTP的发明者们送上掌声,他们预见到了会有更多的请求方法。在下一节,会讨论HTTP的多个版本,但本语法作为HTTP GET请求的格式依然可被识别。
举一个实际的例子。由于Web服务器仅需要一个TCP/IP连接来接收HTTP请求,所以可以使用像Telnet这样的应用程序来模拟浏览器。Telnet是一个简单的应用程序,它可以创建到服务器的TCP/IP连接,然后你就能以文本形式输入命令,并查看响应。这个程序正是我们研究HTTP所需要的,不过在本章末尾,我们会介绍一些更好的查看HTTP的工具。但很不幸的是,一些技术面临过时,Telnet刚好是其中之一,许多操作系统默认都不包含Telnet客户端了。为了尝试一些简单的HTTP请求,我们可能需要手动安装一个Telnet客户端,或者可以使用一个类似的命令,如nc。这个命令是netcat的简写版,很多Linux系的操作系统中都有,包括macOS。下面给出一个它的简单示例,效果和使用Telnet几乎一样。
对于Windows用户来说,推荐使用PuTTY[5](通常需要手动安装),而不建议使用Windows自带的客户端,自带的客户端经常会有显示问题,比如不显示正在输入的内容,或者覆盖掉之前的内容。安装、启动PuTTY之后,会看到配置窗口,可以在此输入主机名(www.google.com)、端口号(80)和连接类型(Telnet)。在Close window on exit(退出时关闭窗口)选项区,确保选中Never(永不)选项,否则将看不到结果,如图1.3所示。如果在输入下面的命令时遇到了问题,提示输入了错误的请求格式,则可能需要将Connection→Telnet→Telnet Negotiation Mode修改为Passive。
图1.3 PuTTY连接Google的细节
如果你使用的是Apple Macintosh或者Linux主机,也许可以直接从命令行调用Telnet,前提是Telnet已经安装好:
或者,像我们之前提到的,使用nc命令:
开启了一个Telnet会话并建立连接后,会看到屏幕上一片空白,或者出现如下提示(具体取决于Telnet程序):
无论此消息有没有显示,我们都可以输入HTTP命令。所以,输入GET/并按下回车键,是在告诉Google的服务器,请求默认页(/),并且(因为没指定HTTP版本号)使用默认的HTTP/0.9。注意,有一些Telnet客户端默认不会回显你正在输入的内容(特别是Windows自带的Telnet客户端),所以我们看不到自己输入了什么内容。但是依旧可以发送这些命令。
通过公司代理使用Telnet
如果你的电脑不能直接访问因特网,就不能通过Telnet直接连上Google。在使用代理来限制直接访问的公司环境中,这种情况经常发生(第3章讨论代理)。这时,可以使用一台内部的Web服务器(比如内部网站)观看示例,不能使用Google。在1.5.3节,讨论可以使用代理的其他工具,但是现在,只需接着读后面的内容,不用照着做。
Google的服务器通常使用HTTP/1.0响应,它会忽略你发送的HTTP/0.9请求(因为已经没有服务器在使用HTTP/0.9了)。响应中包含一个HTTP响应码200(说明请求成功)或者302(说明服务器想让你跳转到另外一页),随后关闭连接。下一节我们介绍过程的细节,所以现在不要太关注其中的细节。
如下是一台Linux终端上的响应,响应行加粗显示。为了简洁,返回的HTML内容没有完全显示:
如果你不在美国,可能会看到一个指向当地Google服务器的跳转。比如,你在爱尔兰,Google会发送一个302响应,建议浏览器访问Google Ireland(http://www.google.ie)。如下所示:
在每个例子的末尾,我们都会看到,连接被关闭。想要发送另一个HTTP请求,需要再次创建连接。为了省略此步骤,可以使用HTTP/1.1(默认保持连接打开,后面会讲),在请求资源之后输入HTTP/1.1:
注意,如果你使用HTTP/1.0或者HTTP/1.1,一定要按两下Return键,告诉Web服务器HTTP请求发送完了。在下一节,我们解释为什么对于HTTP/1.0和HTTP/1.1来说,必须按两下Return键。
在服务器响应之后,可以再次使用GET命令获取页面。实际上,Web浏览器通常使用这个未关闭的连接去请求其他的资源(而不是像我们一样再次请求同样的资源),但是概念上是一样的。
从技术上来讲,为了遵守HTTP/1.1规范,在HTTP/1.1请求中还要指定host首部,理由我们还是放到后面再讲。对于这个简单的例子,不用太担心这个要求,因为Google看起来并不一定要求这样(但是如果使用其他的网站,结果可能不同)。
如你所见,HTTP基本的语法很简单,是一种基于文本的请求-响应格式,但是在HTTP/2下它变成了二进制格式。
如果请求非文本数据,比如一张图片,使用像Telnet这样的程序就不太方便了。在终端会话中会显示一堆乱码,Telnet会尝试将二进制图片格式转换为有意义的文本,但是失败了。如:
现在笔者已经不使用Telnet了,有更好的工具可以使用,但是这个练习依然很有用,它说明了HTTP消息的格式,并展示了协议的初始版本是多么地简单。
如之前提到的,HTTP成功的关键在于它的简单,这使它在服务层面实现起来相对简单。所以,似乎所有可以上网的电脑(从复杂的服务器到IoT中的灯泡)都可以实现HTTP,并直接通过网络提供灵活的命令。实现完全符合HTTP规范的Web服务器是一项更加艰巨的任务。同样,网页浏览器也非常复杂,在通过HTTP获取网页(包括HTML、CSS和用于显示网页的JavaScript)后,还有无数其他协议需要处理。但是,创建一个简单的服务来监听HTTP GET请求,以及响应数据,并不困难。HTTP的简单性也促进了微服务体系结构的繁荣,在该结构中,通常基于较轻的应用服务器,如Node.js(Node),应用程序被分成许多独立的Web服务。