# Web基础知识介绍

# 实验介绍

Web 应用在我们的生活中无处不在。看看我们日常使用的各个应用程序,它们要么是 Web 应用,要么是移动 App 这类 Web 应用的变种。无论哪一种编程语言,只要它能够开发出与人类交互的软件,它就必然会支持 Web 应用开发。对一门崭新的编程语言来说,它的开发者首先要做的一件事,就是构建与互联网(internet)和万维网(World Wide Web)交互的库(library)和框架,而那些更为成熟的编程语言还会有各种五花八门的 Web 开发工具。

Go 是一门刚开始崭露头角的语言,它是为了让人们能够简单且高效地编写后端系统(back end system)而创建的。这门语言拥有众多先进的特性,并且密切关注程序员的生产力以及各种与速度相关的事项。和其他语言一样,Go 语言也提供了对 Web 编程的支持。

# 使用 Go 语言构建 Web 应用

Go 是一门相对比较年轻的编程语言,它拥有繁荣并且仍在不断成长的社区,并且它也非常适合用来编写那些需要快速运行的服务器端程序。因为 Go 语言提供了很多过程式编程语言的特性,所以拥有过程式编程语言使用经验的程序员对 Go 应该都不会感到陌生,但与此同时,Go 语言也提供了函数式编程方面的特性。除了内置对并发编程的支持之外,Go 语言还拥有现代化的包管理系统、垃圾收集特性以及一系列包罗万象、威力强大的标准库。

虽然 Go 自带的标准库已经非常丰富和宏大了,但 Go 仍然拥有许多质量上乘的开源库,它们可以对标准库不足的地方进行补充。

使用 Go 语言进行 Web 开发正变得日益流行,很多公司都已经开始使用 Go 了,其中包括 Dropbox、SendGrid 这样的基础设施公司,Square 和 Hailo 这样的技术驱动的公司,甚至是 BBC、纽约时报这样的传统公司。

在开发大规模 Web 应用方面,Go 语言提供了一种不同于现有语言和平台但又切实可行的方案。大规模可扩展的 Web 应用通常需要具备以下特质:

  • 可扩展;
  • 模块化;
  • 可维护;
  • 高性能。

# 可扩展

大规模的 Web 应用应该是可扩展的(scalable),这意味着应用的管理者应该能够简单、快速地提升应用的性能以便处理更多请求。如果一个应用是可扩展的,那么它就是线性的,这意味着应用的管理者可以通过添加更多硬件来获得更强的请求处理能力。

有两种方式可以对性能进行扩展:

  • 垂直扩展(vertical scaling),即提升单台设备的 CPU 数量或者性能;
  • 水平扩展(horizontal scaling),即通过增加计算机的数量来提升性能。

因为 Go 语言拥有非常优异的并发编程支持,所以它在垂直扩展方面拥有不俗的表现:一个 Go Web 应用只需要使用一个操作系统线程(OS thread),就可以通过调度来高效地运行数十万个 goroutine。

跟其他 Web 应用一样,Go 也可以通过在多个 Go Web 应用之上架设代理来进行高效的水平扩展。因为 Go Web 应用都会被编译为不包含任何动态依赖关系的静态二进制文件,所以我们可以把这些文件分发到没有安装 Go 语言的系统里,从而以一种简单且一致的方式部署 Go Web 应用。

# 模块化

大规模 Web 应用应该由可替换的组件构成,这种做法能够使开发者更容易添加、移除或者修改特性,从而更好地满足程序不断变化的需求。除此之外,这种做法的另一个好处是使开发者可以通过复用模块化的组件来降低软件开发所需的费用。

尽管 Go 是一门静态类型语言,但用户可以通过它的接口机制对行为进行描述,以此来实现动态类型匹配(dynamic typing)。Go 语言的函数可以接受接口作为参数,这意味着用户只要实现了接口所需的方法,就可以在继续使用现有代码的同时向系统中引入新的代码。与此同时,因为 Go 语言的所有类型都实现了空接口,所以用户只需要创建出一个接受空接口作为参数的函数,就可以把任何类型的值用作该函数的实际参数。此外,Go 语言还实现了一些在函数式编程中非常常见的特性,其中包括函数类型、使用函数作为值以及闭包,这些特性允许用户使用已有的函数来构建新的函数,从而帮助用户构建出更为模块化的代码。

Go 语言也经常会被用于创建微服务(microservice)。在微服务架构中,大型应用通常由多个规模较小的独立服务组合而成,这些独立服务通常可以相互替换,并根据它们各自的功能进行组织。比如,日志记录服务会被归类为系统级服务,而开具账单、风险分析这样的服务则会被归类为应用级服务。创建多个规模较小的 Go 服务并将它们组合为单个 Web 应用,这种做法使得我们可以在有需要的时候对应用中的服务进行替换,而整个 Web 应用也会因此变得更加模块化。

# 可维护

和其他庞大而复杂的应用一样,拥有一个易于维护的代码库(codebase)对大规模的 Web 应用来说也是非常重要的。这是因为大规模的应用通常都会不断地成长和演化,所以开发者需要经常性地回顾并修改代码,而修改难懂、笨拙的代码需要花费大量的时间,并且隐含着可能会造成某些功能无法正常运作的风险。因此,确保源代码能够以适当的方式组织起来并且具有良好的可维护性对开发者来说就显得至关重要了。

Go 语言的设计鼓励良好的软件工程实践,它拥有简洁且极具可读性的语法以及灵活且清晰的包管理系统。除此之外,Go 语言还有一整套优秀的工具,它们不仅可以增强程序员的开发体验,还能够帮助他们写出更具可读性的代码,比如以标准化方式对 Go 代码进行格式化的源代码格式化程序 gofmt 就是其中一个例子。

因为 Go 语言希望文档可以和代码一同演进,所以它的文档工具 godoc 会对 Go 源代码及其注释进行语法分析,然后以 HTML、纯文本或者其他多种格式创建出相应的文档。godoc 的使用方法非常简单,开发者只需要把文档写到源代码里面,godoc 就会把这些文档以及与之相关联的代码提取出来,生成相应的文档文件。

除此之外,Go 还内置了对测试的支持:gotest 工具会自动寻找与源代码处于同一个包(package)之内的测试代码,并运行其中的功能测试和性能测试。Go 语言也提供了 Web 应用测试工具,这些工具可以模拟出一个 Web 服务器,并对该服务器生成的响应(response)进行记录。

# 高性能

高性能不仅意味着能够在短时间内处理大量请求,还意味着服务器能够快速地对客户端进行响应,并让终端用户(end user)能够快速地执行操作。

Go 语言的一个设计目标就是提供接近于 C 语言的性能,尽管这个目标目前尚未达成,但 Go 语言现在的性能已经非常具有竞争力:Go 程序会被编译为本地码(native code),这一般意味着 Go 程序可以运行得比解释型语言的程序要快,并且就像前面说过的那样,Go 语言的 goroutine 对并发编程提供了非常好的支持,这使得 Go 应用可以同时处理多个请求。

希望以上介绍能够引起你对使用 Go 语言及其平台进行 Web 开发的兴趣。但是在学习如何使用 Go 进行 Web 开发之前,我们需要先来了解一下什么是 Web 应用,以及它们的工作原理是什么,这会给我们学习之后的内容带来非常大的帮助。

# Web 应用的工作原理

如果你在一个技术会议上向在场的程序员们提出“什么是 Web 应用”这一问题,那么通常会得到五花八门的回答,有些人甚至可能还会因为你问了个如此基础的问题而感到惊讶和不解。通过不同的人对这个问题的不同回答,我们可以了解到人们对 Web 应用并没有一个十分明确的定义。

比如说,Web 服务算不算 Web 应用?因为 Web 服务通常会被其他软件调用,而 Web 应用则是为人类提供服务,所以很多人都认为 Web 服务与 Web 应用是两种不同的事物。但如果一个程序能够像 RSS feed 那样,产生出来的数据既可以被其他软件使用,又可以被人类理解,那么这个程序到底是一个 Web 服务还是一个 Web 应用呢?

同样地,如果一个应用只会返回 HTML 页面,但却并不对页面进行任何处理,那么它是一个 Web 应用吗?运行在 Web 浏览器之上的 Adobe Flash 程序是一个 Web 应用吗?对于一个纯 HTML5 编写的应用,如果它运行在一个长期驻留于计算机的浏览器中,那么它算是一个 Web 应用吗?如果一个应用在向服务器发送请求时没有使用 HTTP 协议,那么它算是一个 Web 应用吗?大多数程序员都能够从高层次的角度去理解 Web 应用是什么,但是一旦我们深入一些,尝试去探究 Web 应用的实现层次,事情就会变得含糊不清起来。

从纯粹且狭隘的角度来看,Web 应用应该是这样的计算机程序:它会对客户端发送的 HTTP 请求做出响应,并通过 HTTP 响应将 HTML 回传至客户端。但这样一来,Web 应用不就跟 Web 服务器一样了吗?的确如此,如果按照上面给出的定义来看,Web 服务器和 Web 应用将没有区别:一个 Web 服务器就是一个 Web 应用。下图展示 Web 应用最基本的请求与响应结构:

image-20220531170716440

将 Web 服务器看作是 Web 应用的一个问题在于,像 httpd 和 Apache 这样的 Web 服务器都会监视特定的目录,并在接收到请求时返回位于该目录中的文件(比如 Apache 就会对 docroot 目录进行监视)。与此相反,Web 应用并不会简单地返回文件:它会对请求进行处理,并执行应用程序中预先设定好的操作,Web 应用工作原理如下图:

image-20220531170722479

从以上观点来看,我们也许可以把 Web 服务器看作是一种特殊的 Web 应用,这种应用只会返 回被请求的文件。普遍来讲,很多用户都会把使用浏览器作为客户端的应用看作是 Web 应用。 这其中包括 Adobe Flash 应用、单页 Web 应用,甚至是那些不使用 HTTP 协议进行通信但却驻留在 桌面或系统上的应用。

为了在接下来的学习中讨论 Web 编程的相关技术,我们必须给这些技术一个明确的定义。首先,让我 们来给出应用 的定义。

应用(application)是一个与用户进行互动并帮助用户执行指定活动的软件程序。比如记账系统、人力资源系统、桌面出版软件等。而 Web 应用则是部署在 Web 之上,并通过 Web 来使用的应用。

换句话说,一个程序只需要满足以下两个条件,我们就可以把它看作是一个 Web 应用:

  • 这个程序必须向发送命令请求的客户端返回 HTML,而客户端则会向用户展示渲染后的 HTML;
  • 这个程序在向客户端传送数据时必须使用 HTTP 协议。

在这个定义的基础上,如果一个程序不是向用户渲染并展示 HTML,而是向其他程序返回某种非 HTML 格式的数据,那么这个程序就是一个为其他程序提供服务的 Web 服务。后面将对 Web 服务进行更详细的说明。

# HTTP

首先,让我们通过一个简单的定义来说明什么是 HTTP:

HTTP 是一种无状态、由文本构成的请求-响应(request-response)协议,这种协议使用的是客户端-服务器(client-server)计算模型。

请求-响应是两台计算机进行通信的基本方式,其中一台计算机会向另一台计算机发送请求,而接收到请求的计算机则会对请求进行响应。在客户端-服务器计算模型中,发送请求的一方(客户端)负责向返回响应的一方(服务器)发起会话,而服务器则负责为客户端提供服务。在 HTTP 协议中,客户端也被称作用户代理(user-agent),而服务器则通常会被称为 Web 服务器。在大多数情况下,HTTP 客户端都是一个 Web 浏览器。

HTTP 是一种无状态协议,它唯一知道的就是客户端会向服务器发送请求,而服务器则会向客户端返回响应,并且后续发生的请求对之前发生过的请求一无所知。相对的,像 FTP、Telnet 这类面向连接的协议则会在客户端和服务器之间创建一个持续存在的通信通道(其中 Telnet 在进行通信时使用的也是请求-响应方式以及客户端-服务器计算模型)。顺带提一下,HTTP 1.1 也可以通过持久化连接来提升性能。

跟很多互联网协议一样,HTTP 也是以纯文本方式而不是二进制方式发送和接收协议数据的。这样做是为了让开发者可以在无需使用专门的协议分析工具的情况下,弄清楚通信中正在发生的事情,从而更容易进行故障排查。

因为 HTTP 最初在设计时只用于传送 HTML,所以 HTTP 0.9 只提供了 GET 这一个方法(method),但新版本对 HTTP 的扩展使它逐渐变成了一种通用的协议,用户也得以将其应用于 Web 应用等分布式系统中,本章接下来就会对 Web 应用进行介绍。

# HTTP 请求

HTTP 是一种请求-响应协议,协议涉及的所有事情都以一个请求开始。HTTP 请求跟其他所有 HTTP 报文(message)一样,都由一系列文本行组成,这些文本行会按照以下顺序进行排列:

  1. 请求行(request-line);
  2. 零个或任意多个请求首部(header);
  3. 一个空行;
  4. 可选的报文主体(body)。

一个典型的 HTTP 请求看上去是这个样子的:

GET host: www.shiyanlou.com
HTTP/1.1
Host: www.shiyanlou.com
User-Agent: Mozilla/5.0
1
2
3
4

这个请求中的第一个文本行就是请求行,请求行中的第一个单词为请求方法(request method),之后跟着的是统一资源标识符(Uniform Resource Identifier,URI)以及所用的 HTTP 版本。位于请求行之后的两个文本行为请求的首部。注意,这个报文的最后一行为空行,即使报文的主体部分为空,这个空行也必须存在,至于报文是否包含主体则需要根据请求使用的方法而定。

# 请求方法

请求方法是请求行中的第一个单词,它指明了客户端想要对资源执行的操作。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 指定的资源进行修改。

安全的请求方法

如果一个 HTTP 方法只要求服务器提供信息而不会对服务器的状态做任何修改,那么这个方法就是安全的(safe)。GET、HEAD、OPTIONS 和 TRACE 都不会对服务器的状态进行修改,所以它们都是安全的方法。与此相反,POST、PUT 和 DELETE 都能够对服务器的状态进行修改(比如说,在处理 POST 请求时,服务器存储的数据就可能会发生变化),因此这些方法都不是安全的方法。

幂等的请求方法

如果一个 HTTP 方法在使用相同的数据进行第二次调用的时候,不会对服务器的状态造成任何改变,那么这个方法就是幂等的(idempotent)。根据安全的方法的定义,因为所有安全的方法都不会修改服务器状态,所以它们天生就是幂等的。

PUT 和 DELETE 虽然不安全,但却是幂等的,这是因为它们在进行第二次调用时都不会改变服务器的状态:因为服务器在执行第一个 PUT 请求之后,URI 指定的资源已经被更新或者创建出来了,所以针对同一个资源的第二次 PUT 请求只会执行服务器已经执行过的动作;与此类似,虽然服务器对于同一个资源的第二次 DELETE 请求可能会返回一个错误,但这个请求并不会改变服务器的状态。

相反,因为重复的 POST 请求是否会改变服务器状态是由服务器自身决定的,所以 POST 方法既不安全也非幂等。幂等性是一个非常重要的概念,在介绍 Web 服务时将再次提及这个概念。

浏览器对请求方法的支持

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 发送。

# 请求首部

HTTP 请求方法定义了发送请求的客户端想要执行的动作,而 HTTP 请求的首部则记录了与请求本身以及客户端有关的信息。请求的首部由任意多个用冒号分隔的纯文本键值对组成,最后以回车(CR)和换行(LF)结尾。

作为 HTTP 1.1 RFC 的一部分,RFC 7231 对主要的一些 HTTP 请求字段(request field)进行了标准化。过去,非标准的 HTTP 请求通常以 X-作为前缀,但标准并没有沿用这一惯例。

大多数 HTTP 请求首部都是可选的,宿主(Host)首部字段是 HTTP 1.1 唯一强制要求的首部。根据请求使用的方法不同,如果请求的报文中包含有可选的主体,那么请求的首部还需要带有内容长度(Content-Length)字段或者传输编码(Transfer-Encoding)字段。

以下为一些常见的请求首部:

首部字段 作用描述
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 当请求包含主体的时候,这个首部用于记录主体内容的类型。在发送 POSTPUT 请求时,内容的类型默认为 x-www-form-urlencoded,但是在上传文件时,内容的类型应该设置为 multipart/form-data(上传文件这一操作可以通过将 input 标签的类型设置为 file 来实现)
Host 服务器的名字以及端口号。如果这个首部没有记录服务器的端口号,就表示服务器使用的是 80 端口
Referrer 发起请求的页面所在的地址
User-Agent 对发起请求的客户端进行描述

# 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>
1
2
3
4
5
6
7
8
9
10
11

HTTP 响应的第一行为状态行,这个文本行包含了状态码(status code)和相应的原因短语(reason phrase),原因短语对状态码进行了简单的描述。除此之外,这个例子中的 HTTP 响应还包含了一个 HTML 格式的报文主体。

HTTP 响应中的状态码表明了响应的类型,响应状态码共有五种类型,它们分别以不同的数字作为前缀,如下:

状态码类型 作用描述
1XX 情报状态码。服务器通过这些状态码来告知客户端,自己已经接收到了客户端发送的请求,并且已经对请求进行了处理
2XX 成功状态码。这些状态码说明服务器已经接收到了客户端发送的请求,并且已经成功地对请求进行了处理。这类状态码的标准响应为“200 OK”
3XX 重定向状态码。这些状态码表示服务器已经接收到了客户端发送的请求,并且已经成功处理了请求,但为了完成请求指定的动作,客户端还需要再做一些其他工作。这类状态码大多用于实现 URL 重定向
4XX 客户端错误状态码。这类状态码说明客户端发送的请求出现了某些问题。在这一类型的状态码中,最常见的就是“404 Not Found”了,这个状态码表示服务器无法从请求指定的 URL 中找到客户端想要的资源
5XX 服务器错误状态码。当服务器因为某些原因而无法正确地处理请求时,服务器就会使用这类状态码来通知客户端。在这一类状态码中,最常见的就是“500 Internal Server Error”状态码了

# 响应首部

响应首部跟请求首部一样,都是由冒号分隔的纯文本键值对组成,并且同样以回车(CR)和换行(LF)结尾。正如请求首部能够告诉服务器更多与请求相关或者与客户端诉求相关的信息一样,响应首部也能够向客户端传达更多与响应相关或者与服务器(对客户端的)诉求相关的信息。

下表展示了一些常见的响应首部:

首部字段 作用描述
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 描述的基本和摘要访问认证模式)

# URL

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。

URL 的一般格式为:

<方案名称>:<分层部分>[ ? <查询参数> ] [ # <片段> ]
1

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:password@www.example.com/docs/file?name=sausheong&location=singapore#summary:
1

这个 URI 使用的是 http 方案,跟在方案名之后的是一个冒号。位于 @ 符号之前的分段 sausheong:password 记录的是用户名和密码,而跟在用户信息之后的 www.example.com/docs/file 就是分层部分的其余部分。位于分层部分最高层的是服务器的域名 www.example.com,之后跟着的两个层分别为 doc 和 file,每个分层之间都使用单斜线分隔。跟在分层部分之后的是以问号(?)为前缀的查询参数,这个部分包含了 name=sausheonglocation=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

# 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)至客户端,这些措施都能够有效地提升性能。

在 Go 1.6 版本中,用户在使用 HTTPS 时将自动使用 HTTP/2,而 Go 1.6 之前的版本则在 golang.org/x/net/http2 包里面实现了 HTTP/2 协议。之后将会介绍如何使用 HTTP/2。

# Web 应用的各个组成部分

通过前面的介绍,我们知道了 Web 应用就是一个执行以下任务的程序:

  1. 通过 HTTP 协议,以 HTTP 请求报文的形式获取客户端输入;
  2. 对 HTTP 请求报文进行处理,并执行必要的操作;
  3. 生成 HTML,并以 HTTP 响应报文的形式将其返回给客户端。

为了完成这些任务,Web 应用被分成了处理器(handler)和模板引擎(template engine)这两个部分。

# 处理器

Web 应用中的处理器除了要接收和处理客户端发来的请求,还需要调用模板引擎,然后由模板引擎生成 HTML 并把数据填充至将要回传给客户端的响应报文当中。

用 MVC 模式来讲,处理器既是控制器(controller),也是模型(model)。在理想的 MVC 模式实现中,控制器应该是“苗条的”,它应该只包含路由(routing)代码以及 HTTP 报文的解包和打包逻辑;而模型则应该是“丰满的”,它应该包含应用的逻辑以及数据。

模型-视图-控制器(Model-View-Controller,MVC)模式是编写 Web 应用时常用的模式,这个模式是如此的流行,以至于人们有时候会错误地把这一模式当成了 Web 应用开发本身。

实际上,MVC 模式最初是在 20 世纪 70 年代末的施乐帕罗奥多研究中心(Xerox PARC)被引入到 Smalltalk 语言里面的,这一模式将程序分成了模型、视图和控制器 3 个部分,其中模型用于表示底层的数据,而视图则以可视化的方式向用户展示模型,至于控制器则会根据用户的输入对模型进行修改。每当模型发生变化时,视图都会自动进行更新,从而展现出模型的最新状态。

尽管 MVC 模式起源于桌面开发,但它在编写 Web 应用方面也流行了起来——包括 Ruby on Rails、CodeIgniter、Play 和 Spring MVC 在内的很多 Web 应用框架都把 MVC 用作它们的基本模式。在这些框架里面,模型一般都会通过结构(struct)或对象(object)映射(map)到数据库,而视图则会被渲染为 HTML,至于控制器则负责对请求进行路由,并管理对模型的访问。

使用 MVC 框架进行 Web 应用开发的新手程序员常常会误以为 MVC 模式是开发 Web 应用的唯一方法,但 Web 应用本质上只是一个通过 HTTP 协议与用户互动的程序,只要能够实现这种互动,程序本身可以使用任何一种模式开发,甚至不使用模式也是可以的。

为了防止模型变得过于臃肿,并且出于代码复用的需要,开发者有时候会使用服务对象(service object)或者函数(function)对模型进行操作。尽管服务对象严格来说并不是 MVC 模式的一部分,但是通过把相同的逻辑放置到服务对象里面,并将同一个服务对象应用到不同的模型之上,可以有效地避免在多个模型里面复制相同代码的窘境。

正如之前所说,Web 应用并不是一定要用 MVC 模式进行开发——通过将控制器和模型进行合并,然后由处理器直接执行所有操作并向客户端返回响应的做法不仅是可行的,而且也是十分合理的。

# 模板引擎

通过 HTTP 响应报文回传给客户端的 HTML 是由模板(template)转换而成的,模板里面可能会包含 HTML,但也可能不会,而模板引擎(template engine)则通过模板和数据来生成最终的 HTML。正如之前所说,模板引擎是经由早期的 SSI 技术演变而来的。

模板可以分为静态模板和动态模板两种,这两种模板都有各自的设计哲学:

  • 静态模板是一些夹杂着占位符的 HTML,静态模板引擎通过将静态模板中的占位符替换成相应的数据来生成最终的 HTML,这种做法和 SSI 技术的概念非常相似。因为静态模板通常不包含任何逻辑代码,又或者只包含少量逻辑代码,所以这种模板也称为无逻辑模板。CTemplate 和 Mustache 都属于静态模板引擎。
  • 动态模板除了包含 HTML 和占位符之外,还包含一些编程语言结构,如条件语句、迭代语句和变量。JavaServer Pages(JSP)、Active Server Pages(ASP)和 Embedded Ruby(ERB)都属于动态模板引擎。PHP 刚诞生的时候看上去也像是一种动态模板,它是之后才逐渐演变成一门编程语言的。

到目前为止,我们已经介绍了很多 Web 应用背后的基础知识以及原理。初看上去,这些内容可能会显得过于琐碎了,但随着读者对本书内容的不断深入,理解这些基础知识的重要性就会慢慢地显现出来。在了解了 Web 应用开发所需的基本知识之后,现在是时候进入下一个阶段——开始实际地进行 Go 编程了。

# Hello Go

在这一节,我们将开始学习如何实际地使用 Go 语言构建 Web 应用。本节在构建 Web 应用时将会用到 Go 的 net/http 包。目前来说,你只需要在计算机上键入本节展示的代码,编译它,然后观察这些代码是如何运行的就可以了。习惯了使用大小写无关编程语言的读者请注意,因为 Go 语言是区分大小写的,所以在键入本书展示的代码时请务必注意代码的大小写。

# 实验准备

# 实验环境说明

本课程使用 WebIDE 来进行相关的演示操作,若要了解更多关于 WebIDE 的内容,请参阅 WebIDE 环境使用指南 (opens new window)

我们在当前文件夹下新建一个名为 first_webapp 的目录,并在这个子目录里面创建一个 server.go 文件:

image-20220531170737308

输入以下代码:

package main

import (
    "fmt"
    "net/http"
)

func handler(writer http.ResponseWriter, request *http.Request){
    fmt.Fprintf(writer, "Hello World, %s!", request.URL.Path[1:])
}

func main(){
    http.HandleFunc("/", handler)
    http.ListenAndServe(":8080", nil)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

在一切就绪之后,在终端执行以下命令:

$ cd $GOPATH/src
$ go install first_webapp
1
2

运行结果如下:

image-20220531170743126

可以看到运行结果如下:

image-20220531170800666

# 分析

让我们来仔细地分析一下这个 Web 应用的代码:

  • 第一行代码声明了这个程序所属的包,跟在 package 关键字之后的 main 就是包的名字。Go 语言要求可执行程序必须位于 main 包当中,Web 应用也不例外。如果你曾经使用过 Ruby、Python 或者 Java 等其他编程语言来开发 Web 应用,那么你可能已经发现了 Go 和这些语言之间的区别:其他语言通常需要将 Web 应用部署到应用服务器上面,并由应用服务器为 Web 应用提供运行环境,但是对 Go 来说,Web 应用的运行环境是由 net/http 包直接提供的,这个包和应用的源代码会一起被编译成一个可以快速部署的独立 Web 应用。
  • 位于 package 语句之后的 import 语句用于导入所需的包,被导入的包分别为 fmt 包和 http 包,前者使得程序可以使用 Fprintf 等函数对 I/O 进行格式化,而后者则使得程序可以与 HTTP 进行交互。顺带一提,Go 的 import 语句不仅可以导入标准库里面的包,还可以从第三方库里面导入包。
  • 出现在导入语句之后的是一个函数定义,这 3 行代码定义了一个名为 handler 的函数。处理器(handler)这个名字通常用来表示在指定事件被触发之后,负责对事件进行处理的回调函数,这也正是我们如此命名这个函数的原因(不过从技术上来说,至少在 Go 语言里面,这个函数并不是一个处理器,而是一个处理器函数,处理器和处理器函数之间的区别将在后续介绍)。 - 这个处理器函数接受两个参数作为输入,第一个参数为 ResponseWriter 接口,第二个参数则为指向 Request 结构的指针。handler 函数会从 Request 结构中提取相关的信息,然后创建一个 HTTP 响应,最后再通过 ResponseWriter 接口将响应返回给客户端。至于 handler 函数内部的 Fprintf 函数在被调用时则会使用一个 ResponseWriter 接口、一个带有单个格式化指示符(%s)的格式化字符串以及从 Request 结构里面提取到的路径信息作为参数。
  • 最后一个 main 函数,Go 语言规定,每个需要被编译为二进制可执行文件的程序都必须包含一个 main 函数,用作程序执行时的起点.

这个 main 函数的作用非常直观,它首先把之前定义的 handler 函数设置成根(root)URL(/)被访问时的处理器,然后启动服务器并让它监听系统的 8080 端口(按下 Ctrl+C 可以停止这个服务器)。至此,这个使用 Go 语言编写的 Hello World Web 应用就算顺利完成了。

# 实验总结

本节以介绍 Web 应用的基础知识开始,并最终走马观花地编写了一个简单却没什么用处的 Go Web 应用作为结束。在接下来的实验中,我们将会看到更多代码,并学习如何使用 Go 语言以及它的标准库去编写更真实的 Web 应用(不过这些应用距离真正生产级别的应用还有一定距离)。尽管下一个实验出现的大量代码可能会让学员有一种囫囵吞枣的感觉,但我们将会从中学习到一个典型的 Go Web 应用是如何组织的。

  • 使用 Go 开发的 Web 应用不仅具有可扩展、模块化和可维护等特性,而且使用 Go 还能够更容易地开发出性能更高的应用,因此 Go 是一门非常适合进行 Web 开发的编程语言;
  • 因为 Web 应用是一种通过 HTTP 协议向客户端返回 HTML 的程序,所以理解 HTTP 协议对学习 Web 应用开发来说是相当重要的;
  • HTTP 是一种简单、无状态、纯文本的客户端-服务器协议,它用于在客户端和服务器之间进行数据交换;
  • HTTP 的请求和响应都以相同的格式进行组织——它们首先以一个请求行或者响应行作为开始,接着后跟一个或多个首部,最后还有一个可选的主体;
  • 每个 HTTP 请求都有一个请求行,请求行里面包含一个 HTTP 方法,HTTP 方法标示了请求想要让服务器执行的动作。GET 方法和 POST 方法是最常用的两个 HTTP 方法;
  • 每个 HTTP 响应都有一个响应行,响应行会告知客户端请求的执行状态;
  • 任何 Web 应用都包含处理器和模板引擎,这两个主要部分分别与 HTTP 协议的请求和响应相对应;
  • 处理器负责接收 HTTP 请求并处理它们;
  • 模板引擎负责生成 HTML,这些 HTML 之后会作为 HTTP 响应的其中一部分被回传至客户端。