客戶端-伺服器概述

既然你已經瞭解了伺服器端程式設計的目的和潛在好處,我們接下來將詳細探討當伺服器從瀏覽器收到“動態請求”時會發生什麼。由於大多數網站伺服器端程式碼以類似的方式處理請求和響應,這將幫助你理解在編寫大多數自己的程式碼時需要做些什麼。

預備知識 對什麼是 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.club.tw/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.club.tw/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.club.tw
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.club.tw/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.club.tw/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 框架。