[ 筆記 ] 網路基礎 - HTTP, Request, Response


Posted by krebikshaw on 2020-07-02

實用參考網站:
Chrome開發者工具詳解(2):Network面板

HyperText Transfer Protocol

中文翻譯為 超文本傳輸協定

  • 要先了解網路協定的標準是什麼,才能依照標準來實作網站

Client & Server

client:用戶端,我們從自己的電腦網頁上面使用網路,我們就是用戶端。

  • client 端會發送 請求 request 到 server 端

server: 伺服器,收到你的 請求request 之後,會回傳對應的 回應 response 給你

Domain Name System (DNS)

「把網域(domain)轉換成 IP 位置」這件事,我們把它稱之為 DNS 解析(resolve DNS)

  • 例如說:
    我想知道 google.com 的 IP 位置
    因此我發一個 request 給 DNS Server
    DNS Server 回覆:172.217.160.78
    我現在知道它在哪裡了

  • DNS 是直接使用 IP 位置來表示的:

    • 因為 DNS Server 也是一台 Server,所以它也是有位置的!但是因為他要負責解析 DNS,如果這個主機也是用一個 domain 來表示,就變成雞生蛋,蛋生雞的問題了。因此,DNS server 都是直接用 IP 位置來表示。

中華電信的 DNS Server IP 位置通常是 168.95.1.1 與 168.95.192.1,而 Google 也有提供免費的 DNS Server,IP 位置是 8.8.8.8,另一個網站 Cloudflare 也有一個,位置是 1.1.1.1,都非常好記。

  • 假設我們已經在 google.com 了,做 google 搜尋還需要再詢問一次 DNS google 在哪裡嗎?

    • 這段我直接引用 Huli 老師做的說明:

      答案是或許需要,也或許不需要。在電腦科學的領域中有一個詞你會很常聽到,英文叫做 Cache(發音像是 Cash 不是 Catch),台灣的翻譯是「快取」,中國那邊的翻譯則是「緩存」,簡單來說就是一個暫存資訊的地方。對於有些可能沒那麼常變動的東西,就會把結果放在這邊,加快存取的速度。

      而這個快取在很多地方都會有,不只一個地方。

      先以瀏覽器為例,瀏覽器有自己的 DNS Cache。所以假設你要問的網址已經在瀏覽器的 DNS Cache 裡了,那就不會再問一次,瀏覽器就會直接知道說 Request 要發到哪個 IP 位置。Cache 如果有資料我們叫做 Hit,沒有的話叫做 Miss。
      而作業系統也有自己的 DNS Cache,如果瀏覽器的快取 miss 了,就會去看作業系統的 Cache,hit 的話就把結果傳回去,一樣不會去問 DNS Server。但如果作業系統的也 miss 了,就會真的發 request 去 DNS Server,問說到底這個網域是對應到哪個 IP 位置。

      在我們又理解的更深以後,就可以把流程改成這樣:

      1. 瀏覽器送出關鍵字「JavaScript」到 google.com
      2. 瀏覽器檢查 dns cache 有沒有 google.com 有的話直接發送 request 給那個位置,沒有的話呼叫 C 語言提供的 function(例如說 gethostbyname)C 語言呼叫作業系統,作業系統檢查 dns cache 有沒有 google.com
        有的話直接回傳位置,沒有的話去 DNS Server(8.8.8.8)問說 google.com 在哪裡
      3. DNS Server(8.8.8.8)回傳 172.217.160.78
      4. 瀏覽器發送 request 給 172.217.160.78
      5. Google server 收到資料,去資料庫查詢關鍵字,並且取得搜尋結果
      6. Google server 把搜尋結果回傳
      7. 瀏覽器顯示搜尋結果
  • 更多 DNS 相關參考資料:
    从输入 URL 到页面加载完成的过程中都发生了什么事情?
    从输入URL到页面加载的过程?如何由一道题完善自己的前端知识体系!
    An attempt to answer the age old interview question "What happens when you type google.com into your browser and press enter?"
    上面這個的簡體中文版

瀏覽器只是一個程式

這個程式可以幫助我們發送 request 及接收 response,並且把指令渲染成我們看得懂的畫面。

若是不使用瀏覽器,依然可以用 terminal 來發送 request 及接收 response,只是不會顯示美美的畫面出來。

  • 利用 Node.js 的 library - request ( Simplified HTTP client ),模擬瀏覽器發送 request,步驟如下:

    1. 在 terminal 輸入安裝指令 npm install request
    2. 建立一個 js 檔,在裡面寫下固定格式的內容
    3. 將發送的目的地更改成自己想要的 URL (下面以 google 為例)
      const request = require('request');
      request('http://www.google.com', function (error, response, body) {
      console.error('error:', error);
      console.log('statusCode:', response && response.statusCode);
      console.log('body:', body);
      });
      
    4. 執行後會得到 response 的內容
  • 在用 node 執行的時候,可以安裝 process 模組,就可以在執行程式的同時傳入參數

    1. 引入模組 const process = require('process')
    2. 在程式中撈取參數 process.argv
    3. 傳入範例:node index.js 2

Http Request Method

依據 request 功用的不同,有查詢、新增、修改或者是刪除,稱為「 Request Method 」,是為了讓 Server 能夠清楚辨別 request 的目的。

GET:單純的跟 server 要一個連結或圖片,通常網頁都是 Get 的 request 比較多(獲取、查詢 或 檢索 (retrieval))

  • 例如要去某個網址、看某張圖片
  • 不會有 request body 因為只是要獲取資訊

POST:需要執行一些動作時(遞交、新增 或 發佈),會傳送 Post 的 request

  • 例如登入會員
  • 遞交 一個 資料區塊 (表單)。
    在 公告欄、討論區 或 部落格…,發佈 訊息 or 文章。
    新增 或 建立 資源至 Server
  • 獲取「 指定的 」資訊,放在 request body ( Form data ) 裡面

PUT:修改掉整個 request
PATCH:修改掉部分 request (較推薦)
DELETE:刪除資源
HEAD:獲取 response 的 header 不要 body
OPTION:了解 server 提供哪些溝通方式

Http Code 狀態碼

用數字表示 response 的狀態

  • 通常從百位數字就可以判斷相應的情況為何:

    • 1xx 稍等
      • 100 Continue:Server 成功接收、但 Client 還要進行一些處理
    • 2xx 成功
      • 200 OK
      • 204 No Content:成功,但沒有回傳的內容( 例如當你發出 Delete 的 request )
    • 3xx 重新導向
      • 301 Moved Permanently:資源「 永久 」移到其他位置,再下一次發出 request 時,瀏覽器會直接到新位置
      • 302 Found(Moved Temporarily):資源「 暫時 」移到其他位置
      • 304 Not Modified:東西跟之前長一樣,可以從快取拿就好
    • 4xx Client 端錯誤
      • 400 Bad Request:請求語法錯誤、或資源太大…等等
      • 401 Unauthorized:未認證,可能需要登入或 Token
      • 403 Forbidden:沒有權限
      • 404 Not Found:找不到資源
    • 5xx Server 端錯誤
      • 500 Internal Server Error:伺服器出錯,搶票時很可能發生
      • 501 Not Implemented
      • 502 Bad Gateway:通常是伺服器的某個服務沒有正確執行
  • 參考資料:
    常見與不常見的 HTTP Status Code

實作一個簡單的 Http Server

  • 利用 Node.js 內建的 http 這個 library
  • 建立一個新檔案: server.js,輸入下列程式碼:
let http = require('http'); // 引用 library: http 
let server = http.createServer(handleRequest);

function handleRequest(req, res) {
  console.log('request url: ', req.url);
  if (req.url === '/') {
    res.writeHead(200, { // 更改 response header
      'info': 'index'
    })
    res.write('index'); // 更改 response body
    res.end();
    return;
  }
  if (req.url === '/redirect') {
    res.writeHead(301, {
      // 'Location': '/category' // 轉址到 /category
      'Location': 'https://google.com' // 轉址到 google.com
    });
    res.end();
    return;
  }
  if (req.url === '/category') {
    res.writeHead(200, {
      'info': 'category'
    })
    res.write('category')
    res.end();
    return;
  }
  res.writeHead(404);
  res.end();
  return;
}

server.listen(5000); // 監聽 5000 這個 port

// 打開瀏覽器
輸入:http://localhost:5000
會看到畫面有 index
當網址加上 '/category'
會看到畫面有 category
當網址加上 '/redirect'
會跳轉網頁到 https://google.com
當網址隨意輸入 '/dakldf'
一片空白, 404 Not Found
  • 執行 Server : node server.js
  • Server 就會一直執行到你按 ctrl + C 停止

HTTP v.s HTTPS

在HTTP協議裏,Web端與Server端進行通訊是使用"明文"的方式,因為HTTP協議本身並不具備加密的功能,所以無法對請求端以及響應端的內容進行加密。

HTTP協議不管是web端還是server端都不會對雙方的身份來進行驗證(例如server端收到請求時,只會要求訊息正確,卻不會去驗證這是不是真的是對應到的web端發出來的,而且server只會對請求做出一次響應),也無法驗證內文的完整性,所以內容很有可能被竊聽或是竄改

任何人都可以發送請求,而Server端在接受到每個請求時,也都會做出相對應的回應(當然取決在發送端的ip沒有被server限制),所以如果搭配傳紙條的例子來做搭配,今天在補習班小明想要傳紙條給喜歡的小美時,會有下列幾種狀況:

  1. 無法確認請求發送是否真的發送到Server,因為中間可能被偽裝的Server響應回去:小明傳紙條給小美,結果被途中的小華攔截亂回。
  2. 無法確認Server的回應是否真的給目標的Client端,因為中間一樣會被偽裝的Client收到:小美傳紙條回去,結果被途中的小華看光,變成小華跟小美聊天,但小美跟小明都不知道。

HTTPS 的 S 是 Secure 的意思,就是透過了SSL/TSL去做了一道安全鎖,透過前述提到的andshake(交握)、公鑰基礎設施(也就是公私鑰加密)、CA(第三方身分認證機構)等,來解決前面我們HTTP無法解決的問題。一樣是透過 HTTP 進行通訊,但通訊過程會使用 SSL/TLS 進行加密,在 HTTP 之上相對安全的資料傳輸方法。

參考資料:差一個字差很多,HTTP 不等於 HTTPS

race condition 處理

擷取自 Huli 提供的範例

假設我們有一個「留言板」的 API,有兩個方法可以用,一個是新增留言,一個是抓取所有留言。我們想在新增留言完以後,重新抓取所有留言,很多人可能就會這樣寫:

const request = require('request');

// 新增留言
request.post({
  url: 'https://example.com/api/messages',
  form: { content: '新留言' }
}, (err, res) => {
  console.log('新增成功!');
})

// 抓取所有留言
request('https://example.com/api/messages', (err, res, body) => {
  console.log('所有留言:', body)
})

你第一個 Request 先發歸先發,但「先發不代表會先到達」,這點超級重要。所以兩個 Request 如果第二個先到了,那你拿到的就還是舊的留言。

再來,儘管第一個先到,但你其實是「立刻」就發了第二個 Request,兩個相差的時間可能只有 1ms 而已,這根本不是什麼差距。而 Server 處理第一個新增留言的時間很有可能大於這個差距,因此你新增了留言沒錯,但你第二個 Request 拿到的東西依然是舊的。

簡單來說好了,「從你電腦發 Request 到 Server 的時間」跟「Server 的處理時間」以及「從 Server 發 Response 傳到你電腦的時間」這三者都是「無法估計」的,所以什麼事都有可能發生,有可能快有可能慢。

所以上面那段程式碼就會有 bug 出現。

假設我照順序發了三個 request a, b, c,沒有人可以保證 response 回傳的順序也是 a, b, c

接下來我們直接假設幾種狀況就好。

狀況一
先發第一個 request,並且第一個 request 先抵達
第二個 request 抵達 server 的時候,第一個 request 已經處理完成並且傳回 response
第二個 response 抵達
這是你原本心裡所想的情況,也是最理想的狀況。

這種情況的話,會在新增完留言之後才抓取留言列表,所以拿到的會是最新的列表,有你剛剛新增的留言。

狀況二
先發第一個 request,並且第一個 request 先抵達
第二個 request 抵達 server 的時候,第一個 request 還在處理
回傳第二個 response,拿到當下的留言
留言新增完成
在這種情形下,因為第二個 response 處理的時候,你其實是還沒有新增留言的,因此拿到的結果會是舊的,不會有你剛剛新增的留言。

除了以上兩種,還可以再假設超級多種,而且每一種情況都有可能發生。

那怎麼避免?最好的方法當然就是:「確保第一個 request 處理完成時,我才發送第二個 request」。

那我怎樣才知道第一個處理完了?當然就是從我拿到第一個的 response 的時候,我自然就知道第一個處理完了,不然我不可能拿得到 response 嘛。

所以呢,你要這樣寫才是對的,確保新增留言成功,再去抓取留言:

const request = require('request');

// 新增留言
request.post({
  url: 'https://example.com/api/messages',
  form: { title: '新留言' }
}, (err, res) => {
  console.log('新增成功!');

  // 確保新增成功,才去抓取所有留言
  request('https://example.com/api/messages', (err, res, body) => {
    console.log('所有留言:', body)
  })
})

牽扯的網路的東西都是非同步的,而非同步就代表著順序是無法被預知的。你只能靠著自己寫 code 來掌握正確的順序。


#HTTP







Related Posts

寫一個簡單堪用的 ESLint plugin

寫一個簡單堪用的 ESLint plugin

MTR_0928

MTR_0928

[第六週]  CSS  Part2 -  裝飾起來吧

[第六週] CSS Part2 - 裝飾起來吧


Comments