客戶端-伺服器概述

現在您已經瞭解了伺服器端程式設計的目的和潛在好處,我們將詳細檢查瀏覽器向伺服器傳送“動態請求”時會發生什麼。由於大多數網站伺服器端程式碼都以類似的方式處理請求和響應,因此這將幫助您瞭解在編寫大多數自己的程式碼時需要做什麼。

先決條件 對什麼是 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.club.tw/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.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應用程式捕獲的,但您可以使用 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 框架。