客户端-服务器概述

现在您已经了解了服务器端编程的目的和潜在好处,我们将详细检查浏览器向服务器发送“动态请求”时会发生什么。由于大多数网站服务器端代码都以类似的方式处理请求和响应,因此这将帮助您了解在编写大多数自己的代码时需要做什么。

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

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

Web 服务器和 HTTP(入门)

Web 浏览器使用超文本传输协议HTTP)与Web 服务器通信。当您单击网页上的链接、提交表单或运行搜索时,浏览器会向服务器发送一个HTTP 请求

此请求包括

  • 标识目标服务器和资源的 URL(例如,HTML 文件、服务器上的特定数据点或要运行的工具)。
  • 定义所需操作的方法(例如,获取文件或保存或更新某些数据)。不同的方法/动词及其关联的操作列在下面
    • GET:获取特定资源(例如,包含有关产品的信息的 HTML 文件或产品列表)。
    • POST:创建新资源(例如,向维基添加新文章,向数据库添加新联系人)。
    • HEAD:获取有关特定资源的元数据信息,而不获取像GET那样获取主体。例如,您可以使用HEAD请求找出资源上次更新的时间,然后仅在资源已更改时使用(更“昂贵”的)GET请求下载资源。
    • PUT:更新现有资源(如果不存在则创建新资源)。
    • DELETE:删除指定的资源。
    • TRACEOPTIONSCONNECTPATCH:这些动词用于不太常见/高级的任务,因此我们这里不介绍它们。
  • 可以使用请求对其他信息进行编码(例如,HTML 表单数据)。信息可以编码为
    • URL 参数:GET请求通过在 URL 末尾添加名称/值对来对发送到服务器的 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-Charset: ISO-8859-1,UTF-8;q=0.7,*;q=0.7
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-Charset: ISO-8859-1,UTF-8;q=0.7,*;q=0.7)和语言(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应用程序捕获的,但您可以使用 Web 嗅探器(例如WebSniffer)或像Wireshark这样的数据包分析器获取类似的信息。您可以自己尝试一下。使用任何链接的工具,然后浏览网站并编辑个人资料信息以查看不同的请求和响应。大多数现代浏览器也具有监视网络请求的工具(例如,Firefox 中的网络监视器工具)。

静态网站

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

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

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

A simplified diagram of a static web server.

当用户想要导航到某个页面时,浏览器会发送一个 HTTP GET请求,指定其 HTML 页面的 URL。服务器从其文件系统检索请求的文档,并返回一个包含文档和“200 OK”(指示成功)的HTTP 响应状态代码的 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)并对团队和球员数量进行编码(作为 URL 参数(例如 /best?team=my_team_name&show=11)或作为 URL 模式的一部分(例如 /best/my_team_name/11/))向服务器创建 HTTP GET 请求。使用 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 从服务器获取内容并动态更新页面来工作的网站尤其相关,而不是在要显示新内容时始终加载新页面。有关此方法的动机以及从客户端的角度来看此模型的外观,请参阅从服务器获取数据

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

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

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

例如,考虑以下将两个 URL 模式映射到两个视图函数的 Django(Python)代码。第一个模式确保具有资源 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 中的模式(而不是上面的硬编码值)并在我们的视图函数中将它们用作参数。例如,一个非常简单的正则表达式可能表示“匹配一个大写字母,后跟 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 框架。