第一章 Go与Web应用
Go学习群:415660935
1.1 Web应用
在计算机的世界里,应用(application)是一个与用户进行交互,并完成用户特定任务的软件程序。而Web应用则是部署在Web之上,并通过Web来使用的软件程序。一程序满足以下两个条件,我们可以把它看做是一个Web应用:
1.这个程序必须向发送命令请求的客户端返回html,而客户端则会向用户展示渲染后的HTML。
2.这个程序在向客户端传输数据时必须使用HTTP协议。
在这个定义的基础上,如果一个程序不是向用户渲染并展示HTML,而是向其他程序返回某种非HTML格式的数据(例如XML和JSON),那么这个程序就是一个为其他程序提供服务的Web服务。
1.2 HTTP简介
协议,是网络协议的简称,网络协议是指通信计算机双方必须共同遵从的一组约定。HTTP(Hypertext transfer protocol)即超文本传输协议,定义了Web浏览器和Web服务器之间进行通信的规则。Web程序中的所有数据都是通过这个看似简单功能却异常强大的文本协议传输的。这个协议自 20 世纪 90 年代定义以来,至今只进行了3次迭代修改,其中 HTTP 1.1 是目前使用为广泛的一个版本,而新的一个版本 则是 HTTP 2.0,又称 HTTP/2。
HTTP 是一种无状态、由文本构成的请求-响应(request-response)协议,这种协议使用的是客户端-服务器(client-server)模型。
请求-响应是两台计算机进行通信的基本方式,其中一台计算机会向另一台计算机发送请求, 而接收到请求的计算机则会对请求进行响应。在客户端-服务器计算模型中,发送请求的一方(客户端)负责向返回响应的一方(服务器)发起会话,而服务器则负责为客户端提供服务。在 HTTP 协议中,客户端也被称作用户代理(user-agent),而服务器则通常会被称为 Web 服务器。在大多数情况下,HTTP客户端都是一个Web浏览器。
HTTP 是一种无状态协议,它唯一知道的就是客户端会向服务器发送请求,而服务器则会向客户端返回响应,并且后续发生的请求对之前发生过的请求一无所知。
和很多互联网协议一样,HTTP也是以纯文本方式而不是二进制方式发送和接收协议数据的。 这样做是为了让开发者可以在无需使用专门的协议分析工具的情况下,弄清楚通信中正在发生的 事情,从而更容易进行故障排查。
1.3 HTTP请求
HTTP是一种请求-响应协议,协议涉及的所有事情都以一个请求开始。HTTP请求由一系列文本行组成,这些文本行会按照以下顺序进行排列:
(1) 请求行(request-line);
(2) 零个或任意多个请求首部(header);
(3) 一个空行;
(4) 可选的报文主体(body)。
一个典型的 HTTP 请求看上去是这个样子的:
GET /Protocols/rfc2616/rfc2616.html HTTP/1.1 Host: www.w3.org User-Agent: Mozilla/5.0 (empty line) |
这个请求中的第一个文本行就是请求行:
GET /Protocols/rfc2616/rfc2616.html HTTP/1.1 |
请求行中的第一个单词为请求方法(request method),之后跟着的是统一资源标识符(Uniform Resource Identifier,URI)以及所用的 HTTP 版本。位于请求行之后的两个文本行为请求的首部。 注意,这个报文的后一行为空行,即使报文的主体部分为空,这个空行也必须存在,至于报文是否包含主体则需要根据请求使用的方法而定。
1.3.1 请求方法
请求方法是请求行中的第一个单词,它指明了客户端想要对资源执行的操作。HTTP 0.9 只有 GET一个方法,HTTP 1.0 添加了POST方法和HEAD方法,而HTTP 1.1 则添加了PUT、DELETE、 OPTIONS、TRACE和CONNECT这 5 个方法,并允许开发者自行添加更多方法——很多人立即就把这个功能付诸实践了。
HTTP 1.1 要求必须实现的只有GET方法和HEAD方法,而其他方法的实现则是可选的,甚至连POST方法也是可选的。 各个 HTTP 方法的作用说明如下:
GET——命令服务器返回指定的资源。
HEAD——与 GET 方法的作用类似,唯一的不同在于这个方法不要求服务器返回报文的主体。这个方法通常用于在不获取报文主体的情况下,取得响应的首部。
POST——命令服务器将报文主体中的数据传递给 URI 指定的资源,至于服务器具体会对这些数据执行什么动作则取决于服务器本身。
PUT——命令服务器将报文主体中的数据设置为 URI 指定的资源。如果 URI 指定的位置上已经有数据存在,那么使用报文主体中的数据去代替已有的数据。如果资源尚未存在, 那么在 URI 指定的位置上新创建一个资源。
DELETE——命令服务器删除 URI 指定的资源。
TRACE——命令服务器返回请求本身。通过这个方法,客户端可以知道介于它和服务器之间的其他服务器是如何处理请求的。
OPTIONS——命令服务器返回它支持的 HTTP 方法列表。
CONNECT——命令服务器与客户端建立一个网络连接。这个方法通常用于设置 SSL 隧道 以开启 HTTPS 功能。
PATCH——命令服务器使用报文主体中的数据对 URI 指定的资源进行修改。
1.3.2 安全的请求方法
如果一个 HTTP方法只要求服务器提供信息而不会对服务器的状态做任何修改,那么这个方 法就是安全的。GET、HEAD、OPTIONS 和 TRACE 都不会对服务器的状态进行修改,所以它们都是安全的方法。与此相反,POST、PUT和DELETE都能够对服务器的状态进行修改(比如说,在处理 POST 请求时,服务器存储的数据就可能会发生变化),因此这些方法都不是安全的方法。
1.3.3 幂等的请求方法
如果一个 HTTP 方法在使用相同的数据进行第二次调用的时候,不会对服务器的状态造成任 何改变,那么这个方法就是幂等的(idempotent)。根据安全的方法的定义,因为所有安全的方法 都不会修改服务器状态,所以它们天生就是幂等的。
PUT和DELETE虽然不安全,但却是幂等的,这是因为它们在进行第二次调用时都不会改变 服务器的状态:因为服务器在执行第一个PUT请求之后,URI指定的资源已经被更新或者创建出来了,所以针对同一个资源的第二次PUT请求只会执行服务器已经执行过的动作;与此类似,虽然服务器对于同一个资源的第二次DELETE请求可能会返回一个错误,但这个请求并不会改变服务器的状态。
相反,因为重复的POST请求是否会改变服务器状态是由服务器自身决定的,所以POST方法既不安全也非幂等。
1.3.4 浏览器对请求方法的支持
GET方法是基本的 HTTP方法,它负责从服务器上获取内容,所有浏览器都支持这个方法。POST方法从HTML 2.0开始可以通过添加HTML表单来实现:HTML的form标签有一个名为method的属性,用户可以通过将这个属性的值设置为get或者post来指定要使用哪 种方法。HTML 不支持除GET和POST之外的其他 HTTP 方法:在 HTML5 规范的早期草案中,HTML表单的method属性曾经添加过对PUT方法和DELETE方法的支持,但这些支持在之后又被删除了。
话虽如此,但流行的浏览器通常都不会只支持 HTML一种数据格式——用户可以使用 XMLHttpRequest(XHR)来获得对PUT方法和DELTE方法的支持。XHR是一系列浏览器API,这些API通常由javascript包裹(实际上XHR就是一个名为 XMLHttpRequest的浏览器对象)。 XHR允许程序员向服务器发送HTTP请求,并且跟“XMLHttpRequest”这个名字所暗示的不一样,这项技术并不仅仅局限于 XML 格式——包括 JSON 以及纯文本在内的任何格式的请求和响 应都可以通过XHR发送。
1.3.5 请求的首部
HTTP请求方法定义了发送请求的客户端想要执行的动作,而HTTP请求的首部则记录了与 请求本身以及客户端有关的信息。请求的首部由任意多个用冒号分隔的纯文本键值对组成,后以回车(CR)和换行(LF)结尾。大多数 HTTP请求首部都是可选的,宿主(Host)首部字段是 HTTP 1.1 唯一强制要求的首部。根据请求使用的方法不同,如果请求的报文中包含有可选的主体,那么请求的首部还需要带 有内容长度(Content-Length)字段或者传输编码(Transfer-Encoding)字段。表 1-1 展示了一些 常见的请求首部。
首部字段 |
作用描述 |
Accept |
客户端在 HTTP 响应中能够接收的内容类型。比如说,客户端可以通过 Accept: text/html这个首部,告知服务器自己希望在响应的主体中收到 HTML 类型的内容 |
Accept-Charset |
客户端要求服务器使用的字符集编码。比如说,客户端可以通过 Accept-Charset: utf-8这个首部,告知服务器自己希望响应的主体使用 UTF-8 字符集 |
Authorization |
这个首部用于向服务器发送基本的身份验证证书 |
Cookie |
客户端应该在这个首部中把服务器之前设置的所有 cookie 回传给服务器。比如说,如 果服务器之前在浏览器上设置了 3 个 cookie,那么 Cookie 首部字段将在一个字符串里 面包含这 3 个 cookie,并使用分号对这些 cookie 进行分隔。以下是一个 Cookie 首部示 例:Cookie: my_first_cookie=hello; my_second_cookie=world |
Content-Length |
请求主体的字节长度 |
Content-Type |
当请求包含主体的时候,这个首部用于记录主体内容的类型。在发送 POST 或 PUT 请 求时,内容的类型默认为 x-www-form-urlen-coded,但是在上传文件时,内容的 类型应该设置为 multipart/form-data(上传文件这一操作可以通过将 input 标 签的类型设置为file来实现) 异 |
Host |
服务器的名字以及端口号。如果这个首部没有记录服务器的端口号,就表示服务器使用 的是 80 端口 |
Referrer |
发起请求的页面所在的地址 |
User-Agent |
对发起请求的客户端进行描述 |
1.4 HTTP响应
HTTP 响应报文是对 HTTP 请求报文的回复。跟 HTTP 请求一样,HTTP 响应也是由一系列 文本行组成的,其中包括:
1) 一个状态行;
2) 零个或任意数量的响应首部;
3) 一个空行;
4) 一个可选的报文主体。
HTTP 响应的组织方式跟 HTTP 请求的组织方式是完全相同的。以下是 一个典型的 HTTP 响应的样子(为了节省篇幅,我们省略了报文主体中的部分内容):
200 OK Date: Sat, 22 Nov 2014 12:58:58 GMT Server: Apache/2 Last-Modified: Thu, 28 Aug 2014 21:01:33 GMT Content-Length: 33115 Content-Type: text/html; charset=iso-8859-1
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/ TR/xhtml1/DTD/xhtml1-strict.dtd"> <html xmlns=‘http://www.w3.org/1999/ xhtml‘> <head><title>Hypertext Transfer Protocol -- HTTP/1.1</title></ head><body>...</body></html> |
HTTP 响应的第一行为状态行,这个文本行包含了状态码(status code)和相应的原因短语 (reason phrase),原因短语对状态码进行了简单的描述。除此之外,这个例子中的 HTTP 响应还 包含了一个 HTML 格式的报文主体。
1.4.1 响应状态码
正如之前所说,HTTP 响应中的状态码表明了响应的类型。HTTP 响应状态码共有 5 种类型, 它们分别以不同的数字作为前缀,如表 1-2 所示。
状态码类型 |
作用描述 |
1XX |
情报状态码。服务器通过这些状态码来告知客户端,自己已经接收到了客户端发送 的请求,并且已经对请求进行了处理 |
2XX |
成功状态码。这些状态码说明服务器已经接收到了客户端发送的请求,并且已经成 功地对请求进行了处理。这类状态码的标准响应为“200 OK” |
3XX |
重定向状态码。这些状态码表示服务器已经接收到了客户端发送的请求,并且已经 成功处理了请求,但为了完成请求指定的动作,客户端还需要再做一些其他工作。 这类状态码大多用于实现 URL 重定向 |
4XX |
客户端错误状态码。这类状态码说明客户端发送的请求出现了某些问题。在这一类 型的状态码中,最常见的就是“404 Not Found”了,这个状态码表示服务器无 法从请求指定的 URL 中找到客户端想要的资源 |
5XX |
服务器错误状态码。当服务器因为某些原因而无法正确地处理请求时,服务器就会 使用这类状态码来通知客户端。在这一类状态码中,最常见的就是“500 Internal Server Error”状态码了 |
1.4.2 响应首部
响应首部跟请求首部一样,都是由冒号分隔的纯文本键值对组成,并且同样以回车(CR) 和换行(LF)结尾。正如请求首部能够告诉服务器更多与请求相关或者与客户端诉求相关的信息 一样,响应首部也能够向客户端传达更多与响应相关或者与服务器(对客户端的)诉求相关的信 息。表 1-3 展示了一些常见的响应首部。
首部字段 |
作用描述 |
Allow |
告知客户端,服务器支持哪些请求方法 |
Content-Length |
响应主体的字节长度 |
Content-Type |
如果响应包含可选的主体,那么这个首部记录的就是主体内容的类型 |
Date |
以格林尼治标准时间(GMT)格式记录的当前时间 |
Location |
这个首部仅在重定向时使用,它会告知客户端接下来应该向哪个 URL 发送请求 |
Server |
返回响应的服务器的域名 |
Set-Cookie |
在客户端里面设置一个 cookie。一个响应里面可以包含多个 Set-Cookie 首部 |
WWW-Authenticate |
服务器通过这个首部来告知客户端,在Authorization 请求首部中应该提供哪种类型的 身份验证信息。服务器常常会把这个首部与“401 Unauthorized”状态行一同发 送。除此之外,这个首部还会向服务器许可的认证授权模式(schema)提供验证信息 (challenge information)(比如 RFC 2617 描述的基本和摘要访问认证模式) |
1.5 URI
Tim Berners-Lee 在创建万维网的同时,也引入了使用位置字符串表示互联网资源的概念。他在 1994 年发表的 RFC 1630 中对统一资源标识符(Uniform Resource Identifier,URI)进行了定义。在这篇 RFC 中,他描述了一种使用字符串表示资源名字的方法,以及一种使用字符串表示 资源所在位置的方法,其中前一种方法被称为统一资源名称(Uniform Resource Name,URN), 而后一种方法则被称为统一资源定位符(Uniform Resource Location,URL)。URI 是一个涵盖性 术语,它包含了 URN 和 URL,并且这两者也拥有相似的语法和格式。因为本书只会对 URL 进 行讨论,所以本书中提及的 URI 指代的都是 URL。 URI 的一般格式为:
<方案名称>:<分层部分>[ ? <查询参数> ] [ # <片段> ]
URI 中的方案名称(scheme name)记录了 URI 正在使用的方案,它定义了 URI 其余部分的结构。因为 URI 是一种非常常用的资源标识方式,所以它拥有大量的方案可供使用,不过本书在大多数情况下只会使用HTTP方案。 URI 的分层部分(hierarchical part)包含了资源的识别信息,这些信息会以分层的方式进行 组织。如果分层部分以双斜线(//)开头,那么说明它包含了可选的用户信息,这些信息将以@ 符号结尾,后跟分层路径。不带用户信息的分层部分就是一个单纯的路径,每个路径都由一连串 的分段(segment)组成,各个分段之间使用单斜线(/)分隔。
在 URI 的各个部分当中,只有“方案名称”和“分层部分”是必需的。以问号(?)为前缀 的查询参数(query)是可选的,这些参数用于包含无法使用分层方式表示的其他信息。多个查询参数会被组织成一连串的键值对,各个键值对之间使用&符号分隔。URI 的另一个可选部分为片段(fragment),片段使用井号(#)作为前缀,它可以对 URI 定义的资源中的次级资源(secondary resource)进行标识。当 URI 包含查询参数时,URI的片段将被放到查询参数之后。因为 URI 的片段是由客户端负责处理的,所以 Web 浏览器在将URI发送给服务器之前,一般都会先把 URI 中的片段移除掉。如果程序员想要取得URI片段,那么可以通过 JavaScript 或者某个 HTTP 客户端库,将 URI 片段包含在一个 GET请求里面。让我们来看一个使用 HTTP 方案的URI示例:http://sausheong:[email protected] com/ docs/file?name=sausheong&location=singapore#summary。 这个 URI 使用的是 http 方案,跟在方案名之后的是一个冒号。位于@符号之前的分段 sausheong:password 记录的是用户名和密码,而跟在用户信息之后的 www.example.com/docs/file 就是分层部分的其余部分。位于分层部分高层的是服务器的域名 www.example.com,之后 跟着的两个层分别为 doc 和 file,每个分层之间都使用单斜线分隔。跟在分层部分之后的是以问号(?)为前缀的查询参数,这个部分包含了name=sausheong 和 location=singapore 键值对,键值对之间使用一个&符号连接。最后,这个URI的末尾还带有一个以井号(#)为 前缀的片段。 因为每个URL都是一个单独的字符串,所以URL里面是不能够包含空格的。此外,因为问号 (?)和井号(#)等符号在 URL 中具有特殊的含义,所以这些符号是不能够用于其他用途的。为了避开这些限制,我们需要使用URL 编码来对这些特殊符号进行转换(URL编码又称百分号编码)。 RFC 3986 定义了 URL 中的保留字符以及非保留字符,所有保留字符都需要进行 URL 编 码:URL 编码会把保留字符转换成该字符在 ASCII 编码中对应的字节值(byte value),接着把这个字节值表示为一个两位长的十六进制数字,后再在这个十六进制数字的前面加上一个百分号(%)。 比如说,空格在 ASCII 编码中的字节值为 32,也就是十六进制中的 20。因此,经过 URL 编码处理的空格就成了%20,URL 中的所有空格都会被替换成这个值。比如在接下来展示的这个 URL 里面,用户名 sau 和 sheong 之间的空格就被替换成了%20:http://www.example.com/docs/file? name=sau%20sheong&location=singapore。
1.6 HTTP/2简介
HTTP/2 是 HTTP 协议的新版本,这一版本对性能非常关注。HTTP/2 协议由 SPDY/2 协议 改进而来,后者最初是 Google 公司为了传输 Web 内容而开发的一种开放的网络协议。
与使用纯文本方式表示的 HTTP 1.x 不同,HTTP/2 是一种二进制协议:二进制表示不仅能够 让 HTTP/2 的语法分析变得更为高效,还能够让协议变得更为紧凑和健壮;但与此同时,对那些习惯了使用 HTTP 1.x 的开发者来说,他们将无法再通过 telnet 等应用程序直接发送 HTTP/2 报文来进行调试。
跟 HTTP 1.x 在一个网络连接里面每次只能发送单个请求的做法不同,HTTP/2 是完全多路复用的(fully multiplexed),这意味着多个请求和响应可以在同一时间内使用同一个连接。除此之 外,HTTP/2 还会对首部进行压缩以减少需要传送的数据量,并允许服务器将响应推送(push)至客户端,这些措施都能够有效地提升性能。
因为 HTTP 的应用范围是如此的广泛,对语法的任何贸然修改都有可能会对已有的 Web 造成破坏,所以尽管 HTTP/2 对协议的通信性能进行了优化,但它并没有对 HTTP 协议本身的语法 进行修改:在 HTTP/2 中,HTTP 方法和状态码等功能的语法还是跟 HTTP 1.1 时一样。
在 Go 1.6 版本中,用户在使用 HTTPS 时将自动使用 HTTP/2,而 Go 1.6 之前的版本则在 golang.org/x/net/http2包里面实现了HTTP/2协议。
1.7 Web应用程序的组成部分
通过前面的介绍,我们知道了 Web 应用就是一个执行以下任务的程序:
(1)通过 HTTP 协议,以 HTTP 请求报文的形式获取客户端输入;
(2)对 HTTP 请求报文进行处理,并执行必要的操作;
(3)生成 HTML,并以 HTTP 响应报文的形式将其返回给客户端。
为了完成这些任务,Web 应用被分成了处理器(handler)和模板引擎(template engine)这两个部分。
1.7.1 处理器
Web 应用中的处理器除了要接收和处理客户端发来的请求,还需要调用模板引擎,然后由模 板引擎生成 HTML 并把数据填充至将要回传给客户端的响应报文当中。
1.7.2 模板引擎
通过 HTTP 响应报文回传给客户端的 HTML 是由模板(template)转换而成的,模板里面可 能会包含 HTML,但也可能不会,而模板引擎(template engine)则通过模板和数据来生成终 的 HTML。
模板可以分为静态模板和动态模板两种,这两种模板都有各自的设计哲学。
1.静态模板是一些夹杂着占位符的 HTML,静态模板引擎通过将静态模板中的占位符替换 成相应的数据来生成终的 HTML,这种做法和 SSI 技术的概念非常相似。因为静态模 板通常不包含任何逻辑代码,又或者只包含少量逻辑代码,所以这种模板也称为无逻辑 模板。CTemplate 和 Mustache 都属于静态模板引擎。
2.动态模板除了包含 HTML 和占位符之外,还包含一些编程语言结构,如条件语句、迭代 语句和变量。JavaServer Pages(JSP)、Active Server Pages(ASP)和 Embedded Ruby(ERB) 都属于动态模板引擎。php 刚诞生的时候看上去也像是一种动态模板,它是之后才逐渐演变成一门编程语言的。