客户端-服务器概述

既然你已经了解了服务器端编程的目的和潜在好处,我们接下来将详细探讨当服务器从浏览器收到“动态请求”时会发生什么。由于大多数网站服务器端代码以类似的方式处理请求和响应,这将帮助你理解在编写大多数自己的代码时需要做些什么。

预备知识 对什么是 Web 服务器有基本的了解。
目标 了解动态网站中的客户端-服务器交互,特别是服务器端代码需要执行哪些操作。

讨论中没有实际代码,因为我们尚未选择要用来编写代码的 Web 框架!但是,这个讨论仍然非常相关,因为无论你选择哪种编程语言或 Web 框架,所描述的行为都必须由你的服务器端代码实现。

Web 服务器和 HTTP(入门)

Web 浏览器使用 HyperText Transfer Protocol (HTTP) 与 Web 服务器通信。当你点击网页上的链接、提交表单或运行搜索时,浏览器会向服务器发送一个 HTTP 请求

此请求包括:

  • 一个 URL,用于标识目标服务器和资源(例如,HTML 文件、服务器上的特定数据点或要运行的工具)。

  • 一个方法,用于定义所需的操作(例如,获取文件或保存或更新一些数据)。下面列出了不同的方法/动词及其关联的操作:

    • GET:获取特定资源(例如,包含产品信息的 HTML 文件,或产品列表)。
    • POST:创建新资源(例如,向 Wiki 添加新文章,向数据库添加新联系人)。
    • HEAD:获取特定资源的元数据信息,但不获取正文,这与 GET 不同。例如,你可以使用 HEAD 请求来查找资源的最后更新时间,然后仅当资源已更改时才使用(更“昂贵的”)GET 请求下载资源。
    • PUT:更新现有资源(如果不存在则创建新资源)。
    • DELETE:删除指定资源。
    • TRACEOPTIONSCONNECTPATCH:这些动词用于不那么常见/高级的任务,因此我们在此不予讨论。
  • 可以在请求中编码附加信息(例如,HTML 表单数据)。信息可以编码为:

    • URL 参数:GET 请求通过在发送到服务器的 URL 末尾添加名称/值对来编码数据 — 例如 http://example.com?name=Fred&age=11。你总是有一个问号 (?) 将 URL 的其余部分与 URL 参数分开,一个等号 (=) 将每个名称与其关联的值分开,以及一个与号 (&) 将每个对分开。URL 参数本质上是“不安全的”,因为它们可以被用户更改然后重新提交。因此,URL 参数/GET 请求不用于更新服务器上数据的请求。
    • POST 数据。POST 请求添加新资源,其数据编码在请求体中。
    • 客户端 Cookie。Cookie 包含有关客户端的会话数据,包括服务器可用于确定其登录状态和对资源的权限/访问的密钥。

Web 服务器等待客户端请求消息,在收到后处理它们,并用 HTTP 响应消息回复 Web 浏览器。响应包含一个 HTTP 响应状态码,指示请求是否成功(例如,成功为 200 OK,如果找不到资源则为 404 Not Found,如果用户无权查看资源则为 403 Forbidden 等)。成功 GET 请求的响应体包含请求的资源。

当返回 HTML 页面时,它会被 Web 浏览器渲染。作为处理的一部分,浏览器可能会发现指向其他资源的链接(例如,HTML 页面通常会引用 JavaScript 和 CSS 文件),并将发送单独的 HTTP 请求来下载这些文件。

静态和动态网站(在以下部分讨论)都使用完全相同的通信协议/模式。

GET 请求/响应示例

你可以通过点击链接或在网站上(例如搜索引擎主页)搜索来进行简单的 GET 请求。例如,当你使用 MDN 搜索词语“客户端-服务器概述”时发送的 HTTP 请求将与下面显示的文本非常相似(它不会完全相同,因为消息的部分内容取决于你的浏览器/设置)。

注意: HTTP 消息的格式定义在“Web 标准”(RFC9110) 中。你不需要知道这种级别的细节,但至少现在你知道这一切的来源了!

请求

请求的每一行都包含有关它的信息。第一部分称为头部,它包含有关请求的有用信息,就像 HTML 头部包含有关 HTML 文档的有用信息一样(但不包含实际内容本身,实际内容在正文中)。

http
GET /en-US/search?q=client+server+overview&topic=apps&topic=html&topic=css&topic=js&topic=api&topic=webdev HTTP/1.1
Host: developer.mozilla.org
Connection: keep-alive
Pragma: no-cache
Cache-Control: no-cache
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.116 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Referer: https://mdn.org.cn/en-US/
Accept-Encoding: gzip, deflate, sdch, br
Accept-Language: en-US,en;q=0.8,es;q=0.6
Cookie: sessionid=6ynxs23n521lu21b1t136rhbv7ezngie; csrftoken=zIPUJsAZv6pcgCBJSCj1zU6pQZbfMUAT; dwf_section_edit=False; dwf_sg_task_completion=False; _gat=1; _ga=GA1.2.1688886003.1471911953; ffo=true

第一行和第二行包含我们上面讨论的大部分信息:

  • 请求类型 (GET)。
  • 目标资源 URL (/en-US/search)。
  • URL 参数 (q=client%2Bserver%2Boverview&topic=apps&topic=html&topic=css&topic=js&topic=api&topic=webdev)。
  • 目标/主机网站 (developer.mozilla.org)。
  • 第一行的末尾还包括一个短字符串,用于标识特定协议版本 (HTTP/1.1)。

最后一行包含有关客户端 cookie 的信息——在这种情况下,你可以看到 cookie 包含一个用于管理会话的 ID (Cookie: sessionid=6ynxs23n521lu21b1t136rhbv7ezngie; …)。

其余行包含有关所用浏览器及其可以处理的响应类型的信息。例如,你可以在这里看到:

  • 我的浏览器 (User-Agent) 是 Mozilla Firefox (Mozilla/5.0)。
  • 它可以接受 gzip 压缩信息 (Accept-Encoding: gzip)。
  • 它可以接受指定的语言 (Accept-Language: en-US,en;q=0.8,es;q=0.6)。
  • Referer 行表示包含此资源链接的网页的地址(即请求的来源,https://mdn.org.cn/en-US/)。

HTTP 请求也可以有正文,但在此示例中为空。

响应

此请求响应的第一部分如下所示。头部包含以下信息:

  • 第一行包含响应码 200 OK,它告诉我们请求成功。
  • 我们可以看到响应是 text/html 格式 (Content-Type)。
  • 我们还可以看到它使用 UTF-8 字符集 (Content-Type: text/html; charset=utf-8)。
  • 头部还告诉我们它有多大 (Content-Length: 41823)。

在消息的末尾,我们看到正文内容——它包含请求返回的实际 HTML。

http
HTTP/1.1 200 OK
Server: Apache
X-Backend-Server: developer1.webapp.scl3.mozilla.com
Vary: Accept, Cookie, Accept-Encoding
Content-Type: text/html; charset=utf-8
Date: Wed, 07 Sep 2016 00:11:31 GMT
Keep-Alive: timeout=5, max=999
Connection: Keep-Alive
X-Frame-Options: DENY
Allow: GET
X-Cache-Info: caching
Content-Length: 41823

<!doctype html>
<html lang="en-US" dir="ltr" class="redesign no-js" data-ffo-opensanslight=false data-ffo-opensans=false >
<head prefix="og: http://ogp.me/ns#">
  <meta charset="utf-8">
  <meta http-equiv="X-UA-Compatible" content="IE=Edge">
  <script>(function(d) { d.className = d.className.replace(/\bno-js/, ''); })(document.documentElement);</script>
  …

响应头的其余部分包括有关响应(例如,生成时间)、服务器以及它期望浏览器如何处理页面(例如,X-Frame-Options: DENY 行告诉浏览器不允许此页面嵌入在另一个站点的 <iframe> 中)的信息。

POST 请求/响应示例

当你提交包含要保存到服务器的信息的表单时,会发出 HTTP POST 请求。

请求

下面的文本显示了当用户在此站点上提交新的个人资料详细信息时发出的 HTTP 请求。请求的格式与之前显示的 GET 请求示例几乎相同,但第一行将此请求标识为 POST

http
POST /en-US/profiles/hamishwillee/edit HTTP/1.1
Host: developer.mozilla.org
Connection: keep-alive
Content-Length: 432
Pragma: no-cache
Cache-Control: no-cache
Origin: https://mdn.org.cn
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.116 Safari/537.36
Content-Type: application/x-www-form-urlencoded
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Referer: https://mdn.org.cn/en-US/profiles/hamishwillee/edit
Accept-Encoding: gzip, deflate, br
Accept-Language: en-US,en;q=0.8,es;q=0.6
Cookie: sessionid=6ynxs23n521lu21b1t136rhbv7ezngie; _gat=1; csrftoken=zIPUJsAZv6pcgCBJSCj1zU6pQZbfMUAT; dwf_section_edit=False; dwf_sg_task_completion=False; _ga=GA1.2.1688886003.1471911953; ffo=true

csrfmiddlewaretoken=zIPUJsAZv6pcgCBJSCj1zU6pQZbfMUAT&user-username=hamishwillee&user-fullname=Hamish+Willee&user-title=&user-organization=&user-location=Australia&user-locale=en-US&user-timezone=Australia%2FMelbourne&user-irc_nickname=&user-interests=&user-expertise=&user-twitter_url=&user-stackoverflow_url=&user-linkedin_url=&user-mozillians_url=&user-facebook_url=

主要区别在于 URL 没有参数。如你所见,表单中的信息编码在请求的正文中(例如,新用户全名使用 &user-fullname=Hamish+Willee 设置)。

响应

请求的响应如下所示。状态码 302 Found 告诉浏览器 POST 成功,并且它必须发出第二个 HTTP 请求以加载 Location 字段中指定的页面。其他信息与 GET 请求的响应信息相似。

http
HTTP/1.1 302 FOUND
Server: Apache
X-Backend-Server: developer3.webapp.scl3.mozilla.com
Vary: Cookie
Vary: Accept-Encoding
Content-Type: text/html; charset=utf-8
Date: Wed, 07 Sep 2016 00:38:13 GMT
Location: https://mdn.org.cn/en-US/profiles/hamishwillee
Keep-Alive: timeout=5, max=1000
Connection: Keep-Alive
X-Frame-Options: DENY
X-Cache-Info: not cacheable; request wasn't a GET or HEAD
Content-Length: 0

注意: 这些示例中显示的 HTTP 响应和请求是使用 Fiddler 应用程序捕获的,但你可以使用网络嗅探器(例如,WebSniffer)或数据包分析器(例如,Wireshark)获取类似信息。你可以自己尝试一下。使用任何链接的工具,然后浏览站点并编辑个人资料信息以查看不同的请求和响应。大多数现代浏览器也具有监控网络请求的工具(例如,Firefox 中的 网络监控器 工具)。

静态网站

静态站点是指每当请求特定资源时,服务器都会返回相同的硬编码内容。因此,例如,如果你有一个位于 /static/my-product1.html 的产品页面,则所有用户都将返回此相同的页面。如果你向站点添加另一个类似产品,你需要添加另一个页面(例如,my-product2.html),依此类推。这可能会变得非常低效——当你有数千个产品页面时会发生什么?你将在每个页面上重复大量代码(基本页面模板、结构等),如果你想更改页面结构的任何内容——例如添加一个新的“相关产品”部分——那么你将不得不单独更改每个页面。

注意: 当你拥有少量页面并希望向每个用户发送相同内容时,静态站点非常出色。但是,随着页面数量的增加,维护成本可能会很高。

让我们回顾一下它是如何工作的,再次查看我们在上一篇文章中看到的静态站点架构图。

A simplified diagram of a static web server.

当用户想要导航到页面时,浏览器会发送一个 HTTP GET 请求,指定其 HTML 页面的 URL。服务器从其文件系统检索请求的文档,并返回一个包含文档和 200 OKHTTP 响应状态码(表示成功)的 HTTP 响应。服务器可能会返回不同的状态码,例如,如果文件不在服务器上,则返回 404 Not Found;如果文件存在但已重定向到其他位置,则返回 301 Moved Permanently

静态站点的服务器只需要处理 GET 请求,因为服务器不存储任何可修改的数据。它也不会根据 HTTP 请求数据(例如 URL 参数或 cookie)更改其响应。

然而,了解静态站点的工作原理对于学习服务器端编程很有用,因为动态站点以完全相同的方式处理对静态文件(CSS、JavaScript、静态图像等)的请求。

动态网站

动态站点是一种可以根据特定请求 URL 和数据生成并返回内容的站点(而不是始终为特定 URL 返回相同的硬编码文件)。以产品站点为例,服务器将产品“数据”存储在数据库中,而不是单独的 HTML 文件中。当收到产品的 HTTP GET 请求时,服务器确定产品 ID,从数据库中获取数据,然后通过将数据插入 HTML 模板来构建响应的 HTML 页面。这比静态站点具有主要优势:

使用数据库可以高效地存储产品信息,并且易于扩展、修改和搜索。

使用 HTML 模板可以非常轻松地更改 HTML 结构,因为这只需要在一个地方、一个模板中完成,而不是在可能数千个静态页面中完成。

动态请求的剖析

本节提供了“动态”HTTP 请求和响应周期的分步概述,在上一篇文章的基础上进行了更详细的阐述。为了“保持真实”,我们将使用体育队经理网站的上下文,教练可以在 HTML 表单中选择他们的队名和队员人数,然后获得他们下一场比赛的建议“最佳阵容”。

下图显示了“球队教练”网站的主要元素,以及教练访问其“最佳球队”列表时操作序列的编号标签。使网站动态的部分是 Web 应用程序(我们将其称为处理 HTTP 请求并返回 HTTP 响应的服务器端代码)、包含有关球员、球队、教练及其关系的信息的数据库以及 HTML 模板

This is a diagram of a simple web server with step numbers for each of step of the client-server interaction.

教练提交包含队名和球员人数的表单后,操作序列如下:

  1. Web 浏览器使用资源的基本 URL (/best) 创建对服务器的 HTTP GET 请求,并将团队和球员数量编码为 URL 参数(例如,/best?team=my_team_name&show=11)或作为 URL 模式的一部分(例如,/best/my_team_name/11/)。使用 GET 请求是因为请求仅用于获取数据(不修改数据)。
  2. Web 服务器检测到请求是“动态的”,并将其转发给 Web 应用程序进行处理(Web 服务器根据其配置中定义的模式匹配规则确定如何处理不同的 URL)。
  3. Web 应用程序根据 URL (/best/) 识别出请求的意图是获取“最佳团队列表”,并从 URL 中找出所需的团队名称和球员数量。然后,Web 应用程序从数据库中获取所需信息(使用额外的“内部”参数来定义哪些球员是“最佳的”,并且可能还从客户端 cookie 中获取已登录教练的身份)。
  4. Web 应用程序通过将数据(来自数据库)放入 HTML 模板中的占位符来动态创建 HTML 页面。
  5. Web 应用程序将生成的 HTML 返回给 Web 浏览器(通过Web 服务器),同时返回 HTTP 状态码 200(“成功”)。如果任何因素阻止 HTML 返回,则Web 应用程序将返回另一个代码——例如“404”表示团队不存在。
  6. Web 浏览器将开始处理返回的 HTML,发送单独的请求以获取它引用的任何其他 CSS 或 JavaScript 文件(参见步骤 7)。
  7. Web 服务器从文件系统加载静态文件并直接将它们返回给浏览器(同样,正确的文件处理基于配置规则和 URL 模式匹配)。

更新数据库中记录的操作也将以类似方式处理,不同之处在于,像任何数据库更新一样,来自浏览器的 HTTP 请求应编码为 POST 请求。

做其他工作

Web 应用程序的工作是接收 HTTP 请求并返回 HTTP 响应。虽然与数据库交互以获取或更新信息是非常常见的任务,但代码可能同时做其他事情,或者根本不与数据库交互。

一个 Web 应用程序可能执行的额外任务的一个很好的例子是向用户发送电子邮件以确认他们在网站上的注册。该网站可能还会执行日志记录或其他操作。

返回 HTML 以外的内容

服务器端网站代码不必在响应中返回 HTML 片段/文件。它可以动态创建并返回其他类型的文件(文本、PDF、CSV 等)甚至数据(JSON、XML 等)。

这对于那些通过 JavaScript 从服务器获取内容并动态更新页面而不是在显示新内容时总是加载新页面的网站尤其重要。有关这种方法的动机以及从客户端角度看这种模型是什么样子的更多信息,请参阅使用 JavaScript 发送网络请求

Web 框架简化了服务器端 Web 编程

服务器端 Web 框架使得编写代码来处理上述操作变得容易得多。

它们执行的最重要的操作之一是提供简单的机制,将不同资源/页面的 URL 映射到特定的处理函数。这使得将与每种资源类型相关的代码分开更容易。它还在维护方面具有优势,因为你可以在一个地方更改用于提供特定功能的 URL,而无需更改处理函数。

例如,考虑以下 Django (Python) 代码,它将两个 URL 模式映射到两个视图函数。第一个模式确保资源 URL 为 /best 的 HTTP 请求将传递给 views 模块中名为 index() 的函数。模式为 /best/junior 的请求将转而传递给 junior() 视图函数。

python
# file: best/urls.py
#

from django.conf.urls import url

from . import views

urlpatterns = [
    # example: /best/
    url(r'^$', views.index),
    # example: /best/junior/
    url(r'^junior/$', views.junior),
]

注意: url() 函数中的第一个参数可能看起来有点奇怪(例如,r'^junior/$'),因为它们使用一种称为“正则表达式”(RegEx 或 RE)的模式匹配技术。此时你不需要知道正则表达式是如何工作的,除了它们允许我们匹配 URL 中的模式(而不是上面硬编码的值)并将其用作视图函数中的参数。例如,一个非常简单的 RegEx 可能会说“匹配一个大写字母,后跟 4 到 7 个小写字母。”

Web 框架还使视图函数能够轻松地从数据库中获取信息。我们数据结构在模型中定义,模型是定义要存储在底层数据库中的字段的 Python 类。如果我们有一个名为 Team 的模型,其中包含一个“team_type”字段,那么我们可以使用简单的查询语法来获取所有具有特定类型的所有团队。

以下示例获取所有具有确切(区分大小写)team_type 为“junior”的团队列表——请注意格式:字段名(team_type)后跟双下划线,然后是要使用的匹配类型(在此示例中为 exact)。还有许多其他类型的匹配,我们可以将它们串联起来。我们还可以控制返回结果的顺序和数量。

python
#best/views.py

from django.shortcuts import render

from .models import Team

def junior(request):
    list_teams = Team.objects.filter(team_type__exact="junior")
    context = {'list': list_teams}
    return render(request, 'best/index.html', context)

junior() 函数获取少年队列表后,它会调用 render() 函数,传递原始的 HttpRequest、一个 HTML 模板以及一个定义要包含在模板中的信息的“上下文”对象。render() 函数是一个便捷函数,它使用上下文和 HTML 模板生成 HTML,并在 HttpResponse 对象中返回它。

显然,Web 框架可以在许多其他任务上帮助你。我们将在下一篇文章中讨论更多好处和一些流行的 Web 框架选择。

总结

至此,你应该对服务器端代码必须执行的操作有了很好的概述,并且了解了服务器端 Web 框架可以使其更容易的一些方法。

在后续模块中,我们将帮助你为你的第一个网站选择最佳 Web 框架。