真正“搞”懂HTTP協議05之What's HTTP?

  前面幾篇文章,我從縱向的空間到橫向的時間,再到一個具體的小栗子,可以說是全方位,無死角的覆蓋了HTTP的大部分基本框架,但是我聊的都太寬泛了,很多內容都是一筆帶過,再加上一句後面再說就草草結束了。並且我還漏了一點東西,就是HTTP本身。

  所以那,這一章,我們回到我們的核心論點,來聊一聊HTTP的特性以及起始行中的核心內容。

一、What is HTTP?

  這個問題如果大家看過前面幾篇文章,肯定能很輕易的回答:HTTP是應用層協議,用來傳輸超文本,或者可以說是用來傳輸超媒體的一種協議,HTTP是無狀態的基於請求-響應模型的。你說的沒錯,接下來我也可能會聊到你想到的這些。但是還有呢?還有呢?下面,我們就來捋一捋HTTP的特點,來說一下這“還有”的部分是什麼。

1)靈活且容易擴展

  靈活易擴展,可以說是HTTP這個協議的最大的特點,也是最有價值的特點。爲什麼這麼說呢?大家如果跟着這個系列閱讀到了這裏,肯定能理解,因爲這個特點是HTTP可以發展至今經久不衰的原因。我們先來看一點點圖片,要不然都是字挺無聊的。

  

   上圖,是RFC1945也就是HTTP/1.0的Additional Features部分,這部分增加了一些請求方法和頭字段規定,注意我之前說過,HTTP/1.0還算不上是標準,只是個備忘錄,但是在這個備忘錄的最後,已經確定了一部分未來的標準的內容。

  我們再來看張圖:

   這是RFC2616也就是HTTP/1.1標準的頭字段的部分定義,是不是有些字段看上去很熟悉。沒錯,有些1945中的附加內容,在2616中就已經變成標準了。這也算是可以體現HTTP協議靈活且易擴展的一個方面了吧。

  這些RFC文檔,實際上更像是對於已有擴展的承認和標準化,實現了“從實踐中來,到實踐中去”的良性循環。這也是爲啥HTTP可以經久不衰的原因了。

  靈活且易擴展這個特性也會在接下來的文章中一再的強調,因爲你會逐漸的發現除了極少的部分,HTTP幾乎沒有不能擴展的部分。

2)可靠傳輸:值得依賴的背後的人

  其實說HTTP協議可靠協議並不是特別的準確,HTTP的可靠性一部分來自於HTTP協議自身,一部分來自於它所依賴的上層TCP協議。

  TCP本身就是一個可靠的傳輸協議,它的可靠性其實就是來自於三次握手,通過客戶端與服務器的溝通來確定是否建立連接,UDP就沒這麼麻煩,傳過去就完事了,我管你收沒收到。所以基於這樣的因素,TCP就不可避免的要比UDP慢不少。犧牲了速度,確保了穩定。

  而HTTP可靠的另外一部分,則是來自於它本身的數據包的形式,也就是它加上的那一層HTTP頭。

  我們說HTTP是可靠的協議,但是它的可靠,並不是絕對的純粹的可靠,只是一種理論場景下的可靠。也就是說在正常網絡環境和物理設備的情況下,HTTP會盡可能的把數據準確傳遞到另一端。只是儘可能。

  比如,光纖斷了,你啥玩意來了都傳輸不過去。哈哈哈哈

3)應用層協議

  這個東西顯而易見啦~但是我還得磨嘰兩句,如果你讀了空間穿梭就會知道其實應用層協議有很多。爲啥只有HTTP應用如此廣泛和出名呢?嗯……因爲它比較能打,怎麼用都行,又因爲具有靈活可擴展的特點,所以它幾乎可以傳輸一切內容。

  所以像比如FTP、SSH啥的,只能在特定的領域使用,固然就沒有HTTP應用這麼廣泛,這麼知名了。

4)請求-應答模型

  這個我之前也說過。HTTP是基於請求-應答模型的應用層協議。簡單來說就是一來一回,有來有回。

  請求-應答模型還有一點就是:明確的規定了HTTP協議裏雙方的定位,永遠是請求方先發起連接和請求,是主動的,應答方只能在接收到請求後進行答覆,是被動的。注意我說的話,我說的是請求方和應答方,而沒有絕對的說是瀏覽器方和服務器方,因爲發起請求的一方不一定僅僅只是瀏覽器,服務器也可以作爲請求的發起方,比如代理服務器。

  另外,HTTP的請求-應答模式,也恰好契合了傳統的 C/S(Client/Server)系統架構,請求方作爲客戶端、應答方作爲服務器。所以,隨着互聯網的發展就出現了 B/S(Browser/Server)架構,用輕量級的瀏覽器代替笨重的客戶端應用,實現零維護的“瘦”客戶端,而服務器則擯棄私有通信協議轉而使用 HTTP 協議。

  最後,請求 - 應答模式也完全符合 RPC(Remote Procedure Call)的工作模式,可以把 HTTP 請求處理封裝成遠程函數調用,導致了 WebService、RESTful 和 gRPC 等的出現。

  嗯,這段話是我抄的~

5)無狀態

  這個東西想必大家也很熟悉了吧,我印象中我之前也提到過這個無狀態。那我想問你個問題,TCP是有狀態還是無狀態的呢?你猜一猜。答案我放在文末了。

  HTTP的無狀態主要體現在數據包的一次性上,我發完了就不管了,收到信息就處理,客戶端和服務器都不會記錄之前發送過的包的任何信息。HTTP報文本身沒有互相的聯繫,這一次的包和上一次的包也沒有任何關聯,它僅有的聯繫在發送和響應的過程中也顯得微不足道,嗯……我說的這個聯繫其實指的僅僅是發過去了,收到了,返回了,結束了,這樣的聯繫(好像跟沒聯繫也差不多~)。

  當然,這種無狀態也可以通過一點點小手段來解決,哈哈哈哈,我猜你猜到是啥了,嗯,就是cookie。

  最後,要着重說的特點就這些,但是你發現一個問題沒有,其實好多特點,都算是靈活可擴展的子特點,那麼還可以說出有關靈活可擴展所延伸出的哪些特點呢?

二、HTTP的優缺點:其實我並沒有你想象得那麼好。

  其實這一小節可以和上一小節合併一下,但是計劃都已經寫到這了,就這樣吧。

  HTTP的特點有很多,但是特點不代表優點,其中也有不少缺點,其實整個HTTP的歷史,實際上在做的事情就是揚長避短,發揮它的長處,儘量彌補它的短處。唉……果然到哪裏都要做到全方位多面體發展,就連一個協議都要這麼卷,心累~。

1)HTTP的優點

  我們先來稍微說下HTTP的優點吧。

靈活易擴展

  這肯定沒毛病吧,要是沒有這個優點,你現在都不一定用學HTTP,可能在學另外一個應用層協議,幻想一下,可能叫做ZakingTTP。哈哈哈哈。

  正是因爲靈活,所以才容易擴展,而容易擴展正好驗證了HTTP的靈活,所以其實靈活與易擴展是相輔相成的,互爲表裏的,不可拆分的一個優點。HTTP裏幾乎每一個核心要素都沒有被硬性的要求一定要怎麼樣怎麼樣,給予了開發者極大的便利和自由,這個我們再後面也會再次強調。

  靈活易擴展的特性不僅僅應用其自身,它不限制具體的下層協議,你隨便,只要你理論上是可靠的就行。

簡單

  簡單這個優點可能有點讓人無語,HTTP簡單麼?簡單爲什麼我學了這麼久感覺還是差點什麼。

  其實我個人理解,HTTP是簡單的,但是正是因爲它的簡單,所以它纔沒有那麼容易。簡單的是它的設計,是它的學習門檻,誇張一點說,你甚至不用學習HTTP,僅僅看報文的名字都能猜到個一二。

  也正是因爲它的簡單,所以纔可以靈活易擴展,這兩個優點,就意味着HTTP有無限的可能。無限的可能,就意味着它的內容會很多,很雜,甚至可以自行設計,對於初學者,就或許沒那麼友好了。

應用廣泛、環境成熟

  HTTP的另一大特點就是應用廣泛,軟硬件環境都非常成熟。你幾乎可以在任意的互聯網通信場景中找到HTTP的影子,比如Web頁面,比如臺式機的瀏覽器,手機上的APP等等,從你看到新聞、視頻、手機上的遊戲,都離不開HTTP。

  不僅僅是在應用領域,在開發領域,HTTP協議也得到了廣泛的支持。它並不限定某種編程語言或者操作系統,所以天然具有跨平臺,跨語言的優越性。而且因爲HTTP本身的簡單性,幾乎所有的編程語言都有HTTP的調用庫和相關的測試工具。

  所以你現在知道HTTP有多重要了吧。

2)HTTP的缺點

無狀態

  其實無狀態並不算是確切的缺點,但是我把它歸類到缺點裏了。因爲我個人覺得,無狀態所帶來的好處並不足以覆蓋它所帶來的問題。

  無狀態的好處是它減少了服務器因爲需要“記憶”而產生的額外的存儲需要和性能耗費。

  另外一個好處就是,正是因爲它是無狀態的,所以它可以很容易的進行組合,讓負載均衡把請求轉發到任意一臺服務器,不會因爲狀態不一致而導致出錯。

  那麼我們繼續說說無狀態所帶來的問題,沒有狀態就意味着我無法關聯兩次請求,無法支持多個相互關聯且連續的“事務”。比如購物車買東西,你是要知道用戶身份的,無狀態我怎麼能知道是誰買的呢?再比如網站的登錄系統,也是一定要攜帶用戶標識加以區分是“你”還是“我”的問題。

  而正是因爲這個問題,所以纔有了cookie,這個小餅乾。

明文

  這肯定是實打實的缺點了,明文意味着誰都可以獲取發送的報文,甚至隨意修改和更換。它的明文雖然可以方便閱讀和調試,但是所帶來的安全性問題實在是無法被忽視的。

不安全

  這個缺點,其實一部分來自於明文,而其它的不安全的地方則是由於HTTP自身的欠缺。明文只是機密性方面的一個缺點,而在身份認證和完整性校驗上面,HTTP也是欠缺的。

  身份認證,簡單來說就是你怎麼證明你是你。那在現實生活中你是怎麼證明你是你得呢?身份證!沒錯,HTTP缺了一個類似身份證一樣的東西。

  而完整性校驗則是說,HTTP的數據在傳輸的過程中如果被篡改了是無法被感知且無法驗證其真僞的。雖然我們或許可以加上一些數字摘要,但是又由於它是明文的,還是可以被第三方獲取和篡改,本質並沒有什麼區別。

  所以,正是由於明文和不安全的問題,纔出現了HTTPS,甚至是現在幾乎要求所有的網站都使用HTTPS,無一例外。這個我們聊到HTTPS的時候再說哈。

性能一般

  爲什麼HTTP的性能一般呢?實際上本質的問題就是隊頭阻塞,前面的卡住了,後面的就要一直等一直等。很好理解,對吧?

  HTTP的發展,一直致力於解決這樣的性能問題,換句話說就是解決隊頭阻塞的問題,雖然在HTTP/1.1,HTTP/2中一定程度上解決了HTTP的隊頭阻塞問題,但是卻無法解決TCP的隊頭阻塞,所以纔有了HTTP/3的終極方案,直接不用TCP了。

三、HTTP方法簡介:最熟悉的陌生人

  我相信你很熟悉HTTP的方法,天天都在用,怎麼可能會不熟悉。但是你真的熟悉HTTP的方法了麼?我覺得並沒有,看完這一小節,我相信你就會真正的熟悉了你每天都要面對的最熟悉的陌生人。

  首先,RFC1945的規定的方法只有三個:GET、HEAD、POST,而2616則在此基礎上又多了五個:PUT、DELETE、TRACE、CONNECT、OPTIONS,還有安全和冪等。要注意,這些單詞都必須是大寫的。

  我們簡單來看下這些方法都代表了什麼意思:

  1. GET:獲取資源,主要的目的是從目標Request-URI獲取資源。
  2. HEAD:獲取資源的頭信息。
  3. POST:就是向Request-URI上傳數據,或者提交數據。
  4. PUT:類似於POST。
  5. DELETE:刪除目標資源。
  6. TRACE:追蹤請求-響應的路徑。
  7. CONNECT:建立一個特殊的連接隧道。
  8. OPTIONS:列出允許對該資源使用的方法。

  我們簡單的羅列了一下HTTP所規定的請求方法。其中前四個比較常用,GET和POST這兩個是最常用的。後面的四個用的就很少了,甚至有些實踐中幾乎沒有使用。我們先看看這些方法,它都對資源做了哪些事情?增刪改查!

  沒毛病,你的觀察力很強。那我有個問題,我向服務器提交了一個DELETE請求,希望刪除服務器的某個資源,服務器一定會按照我的請求刪除該資源麼?答案是你請求你的,我聽不聽的決定權在我。所以,客戶端只有建議,無法要求。

  舉個小例子,你希望可以通過GET請求獲取服務器的私密文件,這個文件保存了所有用戶的賬號和密碼,地址是:https://www.zaking.com/users/password.txt。然後你開開心心的發出了你的請求,服務器接收到你的請求一看,臥槽,這逼要請求這個文件,這是來黑我服務器了吧?果斷甩個500就不搭理你了。

  我們簡單的理解了下方法有哪些,方法能幹啥。那麼下面,我們就來詳細的解釋下這些方法。

1)GET

  GET方法,不用說,是HTTP中最古老的方法,沒有之一,從HTTP誕生一直輝煌至今,無人可以替代。

  GET的含義就是從服務器獲取資源,這個資源既可以是靜態的文本、頁面、圖片、視頻等媒體資源,也可以是由後端語言比如PHP、JAVA等動態生成的各種類型數據。

  但是GET方法並不單純,在規範中,還有兩種類型的GET:conditional GET 和 partial GET。也就是有條件的GET請求和部分GET請求。什麼意思呢,當GET請求與If-Modified-Since字段配合使用,就變成了conditional GET,僅當資源被修改的時候纔會執行GET操作。而partial GET則是Range字段和GET一起使用的情況下產生的,只會返回整體數據的一部分。

  這樣做的目的,其實就是爲了節省資源。

2)HEAD

  HEAD方法與GET方法是完全相同的,也是從服務器獲取資源,服務器的處理機制也與GET一樣,只不過不會返回body,只會傳回響應頭,也就是資源的“元信息”。

  HEAD作用是爲了解決某些無需body的場景下使用GET請求造成的資源浪費,比如我只需要確定我是否可以對服務器上對應的資源做某些指定的操作,那我們直接使用HEAD方法就可以了。

3)POST

  POST方法與GET正好相反,是向服務器傳輸數據,傳輸的數據就放在body裏。

  POST方法算是在日常工作實踐中使用頻率僅次於GET請求的HTTP方法,甚至在某些個性化約束下,項目中所有的請求都使用POST,連GET都不用。

4)PUT

  PUT方法和POST方法十分類似,也可以向服務器提交數據。但是這兩者其實有一點點微妙的不同,PUT方法目的在於“修改”,而POST則是“新建”。

  好吧,你說我知道這個東西有啥用嘛~其實我個人覺得用處不大,因爲大多數的實踐用由於兩者太過相似,壓根就不適用PUT,一個POST解決所有的問題。

5)DELETE

  DELETE方法用於刪除服務器的資源,但是由於這個操作對於服務器來說太過危險,所以服務器往往都會忽略這個方法,友好一點的就做個假刪除,給資源打個刪除的標記。甚至強硬一點的,直接不搭理你。

6)TRACE

  該方法,本意是用於對HTTP請求鏈路的測試和診斷,可以顯示請求-響應的路徑,出發點是好的,但是無奈存在漏洞,會泄漏網站的信息,所以也是會被服務器拒絕的。

7)CONNECT

  這個方法比較特殊,要求服務器爲客戶端和另外一臺服務器建立一條通道,這時候,被請求的服務器就充當了代理的角色。

8)OPTIONS

  該方法要求服務器列出可對該資源使用的方法列表,會在響應頭的Allow字段中返回。它的功能有限,用處不大,所以有些服務器根本就沒有對他的實現,比如Nginx。

9)擴展

  這八個方法我們大致瞭解了一下,總結來說就是常用的經常用,不常用的幾乎沒用,好吧~又廢話了。

  還記得我們之前說過,HTTP是靈活且易擴展的,所以,對於HTTP的方法來說,也是可以擴展的。只要你和服務器都做好了約定,你可以隨意擴展你的HTTP方法。不知道大家還有沒有印象,有一個著名的愚人節玩笑,官方發佈了一個基於HTTP的協議,叫做HTCPCP協議,即超文本咖啡壺協議,爲HTTP協議增加了用來煮咖啡的BREW方法,哈哈哈哈。大家有興趣可以看一下,還有RFC文檔呢,編號是2324

  除了HTCPCP對HTTP的玩笑性擴展,還有一些在實際應用中擴展的請求方法。比如PATCH、LOCK、UNLOCK等,如果有合適的場景,你也完全可以使用這些方法,當然,注意要獲得服務器的支持。

10)安全與冪等

  這兩個概念,還是挺重要的,安全是指請求方法不會破壞服務器上的資源,冪等則是指多次執行相同請求,每次返回的結果也應該是相同的。

  那麼在HTTP的方法裏,GET和HEAD方法是安全的,POST、PUT、DELETE則是不安全的。GET、HEAD、DELETE、PUT則是冪等的,POST則不是冪等的。爲啥POST不是冪等的呢,因爲多次提交數據會創建多個資源,還記得我們之前說過POST是創建,PUT是更新吧,而多次使用PUT更新一個資源,還是第一次更新的狀態,所以PUT是冪等的。

   好啦,方法我們大概都理解了。我們繼續~

四、URI:爸爸和雙胞胎兄弟

  額,其實我在之前的一些文章中有過對URI的詳細的講解,比如瀏覽器原理之跨域?跨站?你真的不懂我!真正“搞”懂HTTP協議01之背景故事中都聊過URI。我在這裏簡單的說說吧,更詳細的可以去查看這兩篇內容。

  其實URI是一個總稱,算是爸爸的角色,而URL和URN則是一個雙胞胎兄弟,URI叫做統一資源標識符,說白了就是用來在網上找資源的。而URL則是統一資源定位符,用資源在互聯網上的地址作爲標識,URN呢則是叫做統一資源名稱,通過文件名來定位網絡上的資源。

  但是大家再接觸這些東西的時候爲啥會覺得混亂呢?其實就是一個兒子太出名了,一個兒子不咋出名,所以漸漸的就忘了那個不咋出名的兒子,把出名的兒子和爸爸搞混了。嗯……就這樣~

  這裏我要額外的強調一點,不知道大家在日常的工作中,針對拼接URL往往會有這樣的規範:“/”加頭不加尾。什麼意思呢?比如我們要拼接一個URL字符串:

// ok
/data-center/queryList 

// 不ok
data-center/queryList/

  爲啥會這樣要求呢。是因爲關於URI的組成的規範。我們簡單來了解下。

  一個URI通常由五部分組成:

 

   其實你也可以理解爲四部分,"://"是個固定操作。我們分別來解釋下。

1)scheme

  其實就是協議名,標識資源應該使用哪種協議來訪問。最常見的就是http,當然還有https、ftp等等。

  緊隨着scheme後面的就是“://”,它把協議和後面的部分分隔開。這個東東,其實並不是必要的,這個不必要是指從發明者的設計角度來說的,現在,你不得不接受,所以它在使用它是必須的。

2)authority

  在“://”之後,就是authority部分,通常就是主機名加端口號的形式,主機名可以是ip或者域名的形式,必須要有。但是端口號可以省略,會有一個默認值,你知道這個默認值是啥麼?

3)path

  有了協議、主機名、端口號,再加上這個path,就可以訪問互聯網上的資源了。而這個path,就對應了我們最開始所說的那個實踐中拼接URL字符串的規範的原因了。

  URI裏的path採用了類似文件系統的“目錄”、“路徑”的表示方式,因爲早起的互聯網上的計算機多是UNIX系統,所以就採用了UNIX的“/”風格。它與“://”後面的部分是一致的。這裏要尤其注意的是,URI的path部分必須與“/”開始,也就是必須包含“/”,“/”是path的部分,不是authority的部分。這回你理解了爲啥會這樣制定開發規範了吧。

  我們再來看個例子:

file:///D:/baidu_download/file

  這是我編的啊~~怎麼有三個“/”?這是file協議的特例,它中間省略了主機名,默認就是localhost。你訪問本地的機器,自然可以在該協議下省略主機名,但是在互聯網上的可絕對不行噢。

五、HTTP狀態碼:媽媽,他騙我!

  這是本篇的最後一節了,我們來聊聊狀態碼。狀態碼是響應頭中最爲重要的一個概念,在RFC1945中就定義了一些,當然只有十來個,到了2616,隨着頭字段的擴展,狀態碼也變得多了起來,大概有四十多個,那~~我們需要記住所有的狀態碼麼?甚至於那些在實踐中幾乎用不到的那些?顯然,沒必要,咱們只記住一些核心狀態碼的應用就好了,不是還有Reason呢嘛?如果你記不住數字含義,Reason還可以起到一定的輔助作用。

  狀態碼有三十多個,但是在實際的開發應用中,真正最常見的,差不多就是200、500還有301、302、404這幾個。甚至於剛入門的同學們只知道200和500,再甚至於服務器對一些場景響應的狀態碼,不管什麼原因,直接拋個500就完事了。

  狀態碼的核心作用,是告知客戶端服務器對該次請求的處理結果,是表達服務器對數據的處理狀態,客戶端可以依據狀態碼來進行後續的處理。

  狀態碼很多,但是好在它是有分類的。我們接下來就先來了解一下狀態碼的分類。

  RFC標準把狀態碼分爲了五類,從100到599,別問爲啥,問就是標準規定的。這五類的具體含義,大概是這樣的:

  1. 1xx:提示信息,表示請求處理的中間狀態,還需要後續的操作。
  2. 2xx:成功,報文已經收到並被正確處理。
  3. 3xx:重定向,資源位置發生了變動,需要客戶端重新發送請求。
  4. 4xx:客戶端錯誤,請求報文有問題,服務器無法處理。
  5. 5xx:服務器錯誤,服務器在處理請求時內部發生了問題。

  總共就這五種,但是其實在客戶端與服務器的請求-應答的模型中,正確的理解這些狀態碼並不僅僅是單方的責任,是服務器和客戶端都必須要達成一致的理解的。

  客戶端作爲請求的發起方,在獲取響應報文後,需要正確的理解狀態碼,才能知道後續如何操作,是發送新的請求還是拋出一個錯誤?是獲取本地緩存資源還是拉去服務器數據?等等等等,都需要雙方正確理解狀態碼的含義。

  而服務器作爲請求的接收方,也要正確的理解運用狀態碼,選擇正確的狀態碼返回給客戶端,指示客戶端下一步要如何操作,特別是在出錯的時候,返回正確的狀態碼就顯得更爲重要。

  好啦,我們現在對狀態碼有了一個整體的認識和了解,接下來,我們來具體學習一下在實踐中比較容易接觸到的狀態碼,其它沒有聊到的,大家可以自行查閱RFC標準或者來看我之前的翻譯也可以,完全翻譯自RFC標準。

1)1xx

  在100這個分類下面,最常見的就是101了。101的含義是Switching Protocol,也就是選擇協議。它需要配合Upgrade字段,要求在HTTP協議的基礎上改成其它協議繼續通信,比如WebSocket。如果服務器同意變更,就會發送101狀態碼,之後的數據傳輸就不會再使用HTTP協議了。

2)2xx

  2xx這個分類表示請求成功,在1945的時候,只有200(OK)、201(Created)、202(Accept)和204(No Content)這四個,到了2616則又擴展了三個:203(Non-Authoritative Information)、205(Reset Content)、206(Partial Content),所以在HTTP/1.1的標準中,成功的狀態碼一共有七個。

  我們挑兩個重要的來看看。

  首先就是200,200這個狀態碼錶示服務器成功收到了客戶端發送的消息,並且成功處理了客戶端的請求,並且成功的返回了客戶端想要的數據。反正就是一切順利~

  其次是204,它與200的含義基本上是一模一樣的,也是一切順利,沒啥問題,但是與200的區別是,204不會返回body。對於服務器來說,正確的區分200和204是很有必要的。

  最後是206,這個東西很重要,大家要額外注意一下,206是部分傳輸,是HTTP分塊傳輸或者斷點續傳的基礎,當客戶端發送“範圍請求”、要求獲取整體資源的部分數據時會出現。它與200的內在含義並無區別,只是服務器會返回部分數據。206的狀態碼通常會伴隨着“Content-Range”頭字段,表示響應報文裏數據的具體範圍。

3)3xx

  這個類別含義就是重定向,以前的資源找不到了,你得向新的地址重新發送請求。3xx的狀態碼一共也有八個,但是其中廢棄了一個306,所以只剩下7個了,其中重要的如301、302、304。不那麼重要的比如300——多種選擇(Multiple Choices)、303——參見其他(See Other)、305——使用代理(Use Proxy)、307——臨時重定向(Temporary Redirect)。

  我們來着重瞭解下重要的三個3xx狀態碼。

  首先是301,永久重定向,意味着你訪問的資源已經不存在了,需要通過Location頭字段指明後續要跳轉到的URI。

  與301相似的是302,臨時重定向,它與301最終的結果是一樣的,都會跳轉到新的Location地址,但是兩者在語義和使用場景上有着核心的區別。301是永久,302是臨時。比如你網站的資源位置發生了永久性的變動,或者再比如從http升級到了https協議,但是你不能直接刪除原來的地址,因爲老客戶還要用,爲了可以直接使用新的https,你需要把請求到舊的地址的請求重定向到新的地址。而302,則意味着資源的位置只是臨時變化,比如午夜的緊急維護,這個時候有睡不着的客戶點進來之後,就可以讓他臨時跳轉到一個新的維護頁面,但是到了第二天維護好了,這個臨時重定向也就不需要了。

  304,這個東東稍微有點意思,它的含義是Not Modified,沒有修改。它主要用於If-Modified-Since等條件請求,用於緩存控制。它通常不具有任何跳轉含義,但是可以理解爲跳轉到了緩存,也即緩存重定向。

4)4xx

  這個分類,意味着客戶端錯誤,整個4xx下的狀態碼有18個之多,我們挑一些來稍微詳細的聊一聊。

  400:Bad Request,這個意味着客戶端錯誤,但是也僅僅是客戶端錯誤,服務器無法理解這次請求,但是具體是什麼錯誤,並沒有詳細準確的說明,只是告訴你錯了,但是卻不告訴你哪裏錯了,實在是讓人惱火,所以,在實踐應用中,儘可能不要出現400這個狀態碼。

  403:Forbidden,實際上不是客戶端的請求出錯,而是服務器拒絕了對該資源的訪問。拒絕的原因可能有很多,完全由服務器控制,如果服務器友善一點,可能會給你返回原因,但是實踐中大多數都懶得告訴你。

  404:Not Found,它的本意是資源不存在,無法在服務器上找到,所以無法返回給客戶端。但是,在實踐中,不管啥錯誤都可能會被服務器返回個404,挺煩人的,一點不友好。

  你發現了沒有,其實大多數場景下的決定權都在服務器手裏,客戶端只能對之作出響應,要求,建議,而無法左右。好啦,稍稍跑了下題,我們繼續。剩下的都比較好理解,Reason都把語義說的很清楚。

  比如:

  405:Method Not Allowed,請求行中指定的方法不允許用於請求URI中標識的資源。響應必須包含一個Allow頭字段,其中包含對請求資源有效的方法列表。

  406:Not Acceptable,資源無法滿足客戶端的條件,比如要請求一箇中文的內容,但是目前只有英文的。

  408:Request Timeout,請求超時,很好理解吧。

5)5xx

   服務器錯誤意味着客戶端的請求是沒問題的,但是服務器在處理時內部發生了錯誤,無法返回應有的響應數據。在2616中,500的狀態碼有6個,我們來看下:

  500:Internal Server Error,服務器錯誤,跟400類似,也是一個通用的服務器錯誤狀態碼,沒有明確表明到底服務器到底出了什麼錯,也是我們最常見的服務器錯誤狀態碼。但是與400不同的是,500的狀態碼可以很有效的避免服務器泄漏可能的隱私信息,所以在實踐中幾乎只要報錯就是500就完事了。

  501:Not Implemented,大概意思就是客戶端請求的功能還不支持,敬請期待,期待到啥時候,那不知道。

  502:Bad Gateway,通常是服務器作爲網關或者代理時出現的錯誤,表示服務器沒問題,但是訪問後端服務出了問題,具體啥問題不告訴你。

  503:Service Unavailable,表示服務器當前正忙,暫時無法響應服務,503是一個臨時的狀態,很可能過一會兒就不忙了,可以通過Retry-After字段可以在多久後再次嘗試。

6)DIY

  最後,你猜599這個狀態碼是什麼含義麼?嗯……這個不能留做問題,我得告訴你答案,答案就是,這是我自己定義的狀態碼,Reason叫做Zaking Received。換句話說,HTTP協議是可擴展的,所以狀態碼也是可以擴展的,只要你自己定義的狀態碼沒有和標準規定的衝突,那麼你就可以使用,當然,還有個前提,我們之前聊方法的時候也說過,就是必須客戶端和服務器都可以理解,要做好約定。

  然後,最新的RFC9110其實新增了點狀態碼。

六、一個POST請求的小栗子

  我們先來寫個客戶端的代碼,就是個index.html,代碼如下:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Zaking HTTP Demo -05</title>
  </head>
  <body>
    <button id="btn">點我發起請求</button>
  </body>
  <script>
    const btnDom = document.getElementById("btn");
    function requestFn() {
      const xhr = new XMLHttpRequest();
      const url = "http://www.zaking.com:8090";

      xhr.open("POST", url);
      xhr.onreadystatechange = function () {
        if (xhr.readyState === XMLHttpRequest.DONE && xhr.status === 200) {
          console.log(xhr);
          console.log(xhr.responseText);
        }
      };
      xhr.send();
    }
    btnDom.addEventListener("click", requestFn);
  </script>
</html>

  很簡單,就是一個原生的ajax請求。然後我們需要提供了一個clinet.js服務,通過讀取index.html返回到頁面上:

const http = require("http");
const fs = require("fs");
const path = require("path");
const hostname = "127.0.0.1";
const port = 9000;

const server = http.createServer((req, res) => {
  let sourceCode = fs.readFileSync(
    path.resolve(__dirname, "./index.html"),
    "utf8"
  );
  res.end(sourceCode);
});

server.listen(port, hostname, () => {
  console.log(`Server running at http://${hostname}:${port}/`);
});

  也特別簡單,就是讀一下文件,返回文件。最後,我們稍稍修改下我們之前用過的那個server.js:

const http = require("http");

const hostname = "127.0.0.1";
const port = 8090;

const server = http.createServer((req, res) => {
  res.setHeader("Access-Control-Allow-Origin", "*");
  res.end("Hello Zaking World!This is Node");
});

server.listen(port, hostname, () => {
  console.log(`Server running at http://${hostname}:${port}/`);
});

  因爲兩個端口號,所以會有跨域的問題,我們除了加了一個CORS的頭,其他的跟上一小節的那個小小栗子的代碼沒有任何的區別。

  當然,其實這塊也可以通過一個服務來解決問題,通過path來返回不同的內容。一般情況下“/”這個path下就是我們的前端頁面的靜態文件,然後會通過一些特定的path,比如/api來作爲接口的路徑,這樣就沒有跨域的問題,只需要一個服務。只不過我們這裏選擇了另外一種方式作爲示例,不重要哈。

  然後,見證奇蹟的時刻,我們需要打開兩個命令行工具,分別輸入這樣的命令:

node ./05/server.js 

node ./05/client.js    

  當然,這個具體的命令可能跟你的文件所在位置的不同稍微有點不同,不重要,理解了就好了。然後,打開http://www.zaking.com:9000/這個地址,你會看到這樣的界面:

 

 

   然後,別忘了打開控制檯的Network,點擊發起請求的按鈕:

   這就是這次請求的所有內容,我們不關注它的字段都是幹啥的哈。在Response中,我們也可以拿到正常返回的字符串:

   或者,我們之前的index.html裏也打印了:

   所以,無論從什麼角度來說,這次實驗的請求都是完美成功!

  但是,不知道到了這裏你有沒有什麼疑問?我有~~~我server的代碼明明什麼都沒改啊?怎麼就POST請求也沒啥問題呢。那是不是意味着我用啥請求都行?嗯……我們試試唄,反正代碼都在這呢,隨便我們怎麼玩,我們把requestFn裏的POST改成PUT:

xhr.open("PUT", url);

  然後重新啓動下客戶端的服務,再點下發送請求:

  報錯了?額。。怪我怪我,這個報錯跟我們的核心內容沒關係,還是跨域的問題,我們再給server.js加一行代碼:

const server = http.createServer((req, res) => {
  res.setHeader("Access-Control-Allow-Origin", "*");
  res.setHeader("Access-Control-Allow-Methods", "*");

  res.end("Hello Zaking World!This is Node");
});

  再啓動下server.js,然後點下按鈕:

   沒問題~~~,再試下DELETE?竟然還是一樣。那再試下HEAD、OPTIONS?你會發現HEAD沒有返回body,OPTIONS跟前面幾個的返回一樣。最後既然都到這裏了,我們就都試一下,也不差這兩個了試下TRACE和CONNECT。我們可以看到TRACE方法報錯了:

  還記得我們之前說過的TRACE方法會暴露服務器數據,不安全,所以注意這裏:是瀏覽器直接報錯了,那這個請求發沒發到服務器呢?我們在sever.js中打印一下:

const server = http.createServer((req, res) => {
  console.log("received");
  res.setHeader("Access-Control-Allow-Origin", "*");
  res.setHeader("Access-Control-Allow-Methods", "*");
  res.end("Hello Zaking World!This is Node");
});

  然後再重複下之前的步驟。我們發現服務器壓根沒有打印,所以這個請求直接被瀏覽器攔截,沒有到達服務器。最後,試試CONNECT。竟然也報錯了,跟TRACE的報錯一樣。好吧。

  經過以上的測試,我們發現TRACE和CONNECT方法在瀏覽器中竟然都不支持,OPTIONS、GET、POST、PUT、DELETE等方法如果服務器沒有去處理這些方法,從感知上來說,竟然沒有區別。HEAD則按照規範要求,沒有返回body。

  那我們說TRACE方法,可能會造成問題,所以不被支持,這個還可以理解,但是CONNECT方法爲啥也不支持啊?嗯……因爲我們試驗的場景是瀏覽器環境下的http請求,如果是服務器對服務器發起請求,是否就可以使用CONNECT了呢?再有,GET請求是不能發送body的,但是在服務器作爲發起方的時候是否就可以發送body了呢?

  我們先來看看GET能不能發body,我們創建一個request-server.js,代碼如下,也是從官網複製下來的:

const http = require("http");

const postData = JSON.stringify({
  msg: "Hello World!",
});

const options = {
  hostname: "www.zaking.com",
  port: 8090,
  path: "/",
  method: "GET",
  headers: {
    "Content-Type": "application/json",
    "Content-Length": Buffer.byteLength(postData),
  },
};

const req = http.request(options, (res) => {
  console.log(`STATUS: ${res.statusCode}`);
  console.log(`HEADERS: ${JSON.stringify(res.headers)}`);
  res.setEncoding("utf8");
  res.on("data", (chunk) => {
    console.log(`BODY: ${chunk}`);
  });
  res.on("end", () => {
    console.log("No more data in response.");
  });
});

req.on("error", (e) => {
  console.error(`problem with request: ${e.message}`);
});

// Write data to request body
req.write(postData);
req.end();

  啥也沒動哈,就是官網的代碼。然後,我們直接執行這個服務,怎麼啓動就不多說了。結果是這樣的:

  是可以的對吧?

  CONNECT有點複雜,我也不會了,就這樣吧。哈哈哈哈。我猜測CONNECT和TRACE其實都可以在服務器端得到支持,只是我不知道怎麼搞這個例子。

  快完事了~我們在來看看返回一些狀態碼是什麼樣的。就先看看500吧。回到之前的代碼,我們稍微修改下server.js:

const server = http.createServer((req, res) => {
  console.log("received");
  res.setHeader("Access-Control-Allow-Origin", "*");
  res.setHeader("Access-Control-Allow-Methods", "*");
  res.statusCode = 500;
  res.end("Hello Zaking World!This is Node");
});

  然後把index.html中的請求方法改成GET。我們發現一個問題:

   返回了500,沒問題,但是~

 

   結果還是返回到瀏覽器了,但是控制檯卻沒有打印,只有個500的錯誤:

   這說明數據還是從服務器返回到瀏覽器了,但是因爲瀏覽器發現是500,就沒有把body再傳給javascript引擎去處理了。當然,還有其他的各種方法,這裏就不再多說,大家有興趣可以自己用代碼試一下。還有一些特殊的狀態碼要配合頭字段使用的,後面我還會詳細的聊。

  我們再試一個有趣的我之前說過的,叫做599的狀態碼。代碼呢這樣改:

const server = http.createServer((req, res) => {
  console.log("received");
  res.setHeader("Access-Control-Allow-Origin", "*");
  res.setHeader("Access-Control-Allow-Methods", "*");
  res.statusCode = 599;
  res.statusMessage = "Zaking Not Know";
  res.end("Hello Zaking World!This is Node");
});

  結果是這樣的:

  是不是,很有趣?

  終於~這一章結束啦~

七、總結

  這一章我們聊的內容不少,需要大家好好消化一下,是我們後面學習的重要的基礎。我們稍微回顧一下我們都學了什麼。首先我們先學了下HTTP的優缺點,讓大家對HTTP有一點稍微詳細的認識。

  再往後,我們學了下HTTP的方法,HTTP的方法不少,但是實際上我們在實際使用中的就那麼幾個,大家要注重學習,另外要額外關注下安全和冪等的含義。

  然後,我們還簡單學了下URI,這部分其實很重要,但是我之前寫過了,所以不想再重複的寫,大家要深入的瞭解可以再去看看我之前寫的文章。

  最後,理論的部分就只剩下狀態碼了,狀態碼很多,有40多個。我簡單的講解了其中一些常見的狀態碼,還有一部分,會在後續的學習中更加深入的去學習。

  在理論結束之後,我們還寫了個小例子,這個例子測試了瀏覽器發起的各種請求方法。還試了一下500狀態碼的樣子。

  最後,按照慣例,留點小問題給大家:

  1、其實我在第四節所畫出來的URI是簡寫形式,你知道它的完全體是什麼樣的麼?爲什麼完全體很少使用呢?

  2、URI後的query部分可以傳輸Object或者Array類型的數據麼?或者說,GET方法可以傳Object或者Array類型的數據麼?

  3、服務器發起的GET請求可以傳送body麼?瀏覽器呢?

  4、HTTP的狀態碼一定符合它的描述麼?

  5、爲什麼在狀態碼的那一小節,我會起一個那樣的標題呢?

  本篇所有內容,只是限定在RFC2616,其實說起來有點過時了,但是重要的內容確實也還是這些,後續可能會因爲講解深入再涉及到,也可能會涉及不到,關於方法和狀態碼的最新的額外的內容,大家其實完全可以自行學習了。

填坑

  1. TCP是有狀態的協議,他需要通過狀態來維護通信雙方的連接狀態,可以理解爲連接雙方的一個標誌,用來記錄連接建立到了什麼階段,後續的情況如何處理等等。詳情可以參考之前聊過的三次握手和四次揮手。順帶說一句,TCP是有連接有狀態的,而UDP是無連接無狀態的。
  2. HTTP關於靈活可擴展延伸的特點有:比如,壓縮,國際化,緩存,身份認證也就是HTTPS啦,等等,等等。
  3. 最後,其實本篇留下了個connect的那個坑,但是那個坑我覺得不太重要,我又確實不會,所以就不填了。😏😏

參考鏈接:

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章