理解網路以前,先來理解溝通是怎麼一回事
傳紙條的故事第一集:告白篇
「傳紙條」跟「網路」的本質都是在“溝通”
傳紙條守則:
- 寫明來源
--> 才能知道紙條要傳回去給誰 - 寫明目的地
--> 才能知道紙條要發送給誰,中間的人才能幫我傳紙條 - 經過三次的前置作業,確保雙方的「接收、發送」功能都沒問題
傳紙條的故事第二集:便當篇
傳紙條守則:
- 內容的格式要標準化
--> 才能更方便去解讀 - 分為 header 跟 body
--> header 放「額外附加的資訊」,body 放「主要的資訊」 - 用狀態碼來標準化結果
--> 用狀態碼來代表不同的結果,就不用每次都要把結果整句寫出來
- 用動詞來標準化動作
傳紙條的故事第三集:發大財篇
傳紙條守則:
- 新增服務代號,一人負責一個服務
- 不同服務可以有不同的格式
- 有些服務不一定要經過三次確認
--> 有些服務的重點是「快速、即時性」,如果還需要先確認的話會拖慢速度,所以就不確認了,就一直不斷發送紙條(以量取勝) - 跨校傳紙條,只寫校名(地標)不寫地址也行,郵差也看得懂
為什麼我們需要協定(protocol)?
協定,其實就是一種標準(為了要讓彼此能夠溝通而建立的一個規範)。有了標準,才可以做「規模化」
例如:
HTTP 的全名是 Hypertext Transfer Protocol,就是一個協定
由上到下來談網路運作:從 HTTP 協定開始講起
HTTP 是什麼呢?
網頁前端在跟後端溝通時,都是透過 HTTP 協定
所以網址最前面都會有 http 或是 https(s 就是 secure,是一種更安全的連線方式)
HTTP request 的一生
透過 HTTP 協定溝通的兩端,分別是 Client 和 Server
Client 就是「自己的電腦、瀏覽器」
以這個 GitHub 的頁面 來說,我們為什麼可以看到這個頁面呢?
打開 devtool 進入 Network 頁籤
Network 頁籤這裡會印出「瀏覽器發送的每一個 request」和「收到的 response」
下圖中,在最左側的 Name 欄位,第一個「mentor-program-4th」是「整個網頁」的 request, response:
- request
- response
會根據回傳的 response 的 html 程式碼,去下載頁面所需的圖片、css、js 等等(也就是紅色框框那些)
(html 裡面會寫好要下載哪些資源,例如:圖片、css、js)
所以,HTTP request 的一生就是:
瀏覽器 --> 製造 request --> 傳給 server
server --> 處理 --> 回傳 response 給瀏覽器
我只是個計程車司機:DNS Server
DNS 的全名是 Domain Name System,負責把 domain name 轉換成「實際的 IP 位置」
下圖是 devtool 的 Network 頁籤,Remote Address 就是「經由 DNS 解析之後的 IP 位置」
nslookup github.com
使用指令 nslookup github.com
,可以查詢這個 domain name 的 IP 位置是哪裡:
可能會有多個不同的 IP 位置,對應到不同的 server(分散在世界各處的 server)
localhost 和 127.0.0.1(另一個 DNS 的展現)
在電腦裡面有一個檔案叫做 /hosts,長這樣子:
(檔案位置是在 /private/etc/hosts)
這個 /hosts 檔案的內容是 Host Database,在這個檔案裡面,可以自己新增一些規則來「把一個 domain name 對應到一個 IP 位置」。
電腦在發送 request 時,會優先到這個 /hosts 檔案查詢這些規則來找是否有對應的 IP 位置。如果規則沒有寫,電腦才會去 DNS server 查 IP 位置。
例如:
範例一
localhost
對應到的 IP 位置就是 127.0.0.1(在任何一台電腦上,127.0.0.1 就是“自己電腦的 server”的意思,是一個特殊的 IP 位置)
當我的電腦要發送 request 到 localhost
這個 domain name 時,localhost
的 IP 位置是哪裡呢?電腦就會先去 /hosts 檔案裡面查,就會查到「localhost
這個 domain name 的 IP 位置是 127.0.0.1」,因此就會去 127.0.0.1 這個位置
範例二
假設我現在在 /hosts 檔案裡面新增了 127.0.0.1 github.com
,當我「用 nslookup github.com
查詢」或者是「在瀏覽器的網址列輸入 github.com」時,電腦就會把 github.com 的 IP 位置解析為 127.0.0.1
什麼時候會用到 /hosts 檔案呢?
通常是在測試一些東西的時候會用到 /hosts 檔案
安裝盜版軟體的原理
在安裝 Adobe 的盜版軟體時,會要你去改一個叫做 /hosts 的檔案:
把一個 domain name 例如 adobe.com 對應到 127.0.0.1
為什麼要這麼做呢?
在安裝軟體時,都會有一個「檢查是否為盜版軟體」的機制。
檢查的方式是:發送一個 request 到 adobe server -> adobe server 就會回傳一個 response 說是否為盜版軟體
request -> adobe server -> response
但是,因為我要下載盜版軟體時,不希望它去做這個“是否為盜版軟體”的檢查,因此我可以這麼做:
假設 adobe server 的 domain name 是 adobe.com
那我就在 /hosts 檔案內新增這行 127.0.0.1 adobe.com
或是 127.0.0.2 adobe.com
意思就是,把 adobe.com 的 IP 位置對應到 127.0.0.1 (我的電腦)或是 127.0.0.2 (其他任意一個不存在的 IP 位置)
這樣一來,
發送到 adobe.com 的每一個 request 都會被導到一個根本沒有 adobe 服務的地方(我的電腦 or 一個不存在的 IP 位置),就不會有 response(所以也不會去做“是否為盜版”的檢查),我就不會被視為是盜版軟體了:
request -> ??? -> xxx
瀏覽器只是另一個程式
永遠不要忘記:瀏覽器只是一個程式,這個程式就是幫我發送 request -> 接收 response -> 再把 response 的 html 程式碼渲染成頁面讓我看到
因此,就算沒有瀏覽器,我們一樣可以拿到這個 response
沒有瀏覽器,要怎麼拿到 response 呢?
在 Node.js 有一套 library 叫做 Request
安裝完 Request 之後,直接從 GitHub 把這段用法貼到 index.js 裡面:
const request = require('request');
request('http://www.google.com', function (error, response, body) {
console.error('error:', error); // Print the error if one occurred
console.log('statusCode:', response && response.statusCode); // Print the response status code if a response was received
console.log('body:', body); // Print the HTML for the Google homepage.
});
- 首先,先把 request 給 require 進來
request
小括號裡面,會傳入兩個參數:
第一個參數:填入我要發送 request 的頁面網址
代表說:我要發送 request 到 https://github.com/Lidemy/mentor-program-4th 這個網址去
第二個參數:傳入一個 function
function 裡面會做一些事情(印出接收到的資訊):
console.error('error:', error)
就是:如果有出現錯誤,會把錯誤印出來console.log('statusCode:', response && response.statusCode)
就是:如果有成功收到 response,就把 status code 印出來console.log('body:', body)
就是:把 response 的 body 印出來
const request = require('request');
request('https://github.com/Lidemy/mentor-program-4th', function (error, response, body) {
console.error('error:', error); // Print the error if one occurred
console.log('statusCode:', response && response.statusCode); // Print the response status code if a response was received
// console.log('body:', body);
});
這裡,我們先不印出 body,先執行一下 node index.js
,結果印出如下,代表:沒有錯誤
error: null
statusCode: 200
接著,就可以把 body 印出來看看:
const request = require('request');
request(
'https://github.com/Lidemy/mentor-program-4th',
function (error, response, body) {
// console.error('error:', error); // Print the error if one occurred
// console.log('statusCode:', response && response.statusCode); // Print the response status code if a response was received
console.log('body:', body);
}
);
執行一下 node index.js
,結果印出如下,就是我在 Network 頁籤看到的 response(也就是:從 https://github.com/Lidemy/mentor-program-4th 這個 server 回傳的 response)
我可以用 node index.js > mtr4th.html
這個指令,把 node index.js
輸出的東西,導到一個新的檔案叫做 mtr4th.html
在資料夾裡面,就可以看到 mtr4th.html 這個檔案
接著,用 open mtr4th.html
打開 mtr4th.html,就可以看到渲染完成的網頁了!(跟原本用瀏覽器接收 response 的頁面一模一樣)
結論:瀏覽器可以做到的事情,我們都可以透過自己寫的程式去做到
Header 與 Body(不是網頁的那個)
request 和 response 都有分成兩個部分:header, body
Header:寫一些額外資訊
Header 裡面寫的就是:看 server 有要求什麼資訊,或者是我想要帶什麼資訊給 server
打開 devtool,在 Network 頁籤的 Headers,會把 response 和 request 的 header 都寫在這裡
有些 Headers 的欄位,只會出現在 Request Headers 或是 Response Headers 的其中一個
例如:
Request Method
這個欄位,只有在 Request Headers 會有,在 Response Headers 不會有
有些 Headers 的欄位,在 Request Headers 和 Response Headers 都可以有,但是他們會代表不同的意思(因為 Request 和 Response 是兩個不同的場景)
例如:content-type
這個 Header 欄位
- 在 request Header 的
content-type
是要告訴 server「我要傳送的資料內容是什麼格式」 - 在 response Header 的
content-type
是要告訴瀏覽器「這個 response 的資料內容是什麼格式」
使用 request 套件來發送 request
用 request 這個 Node.js 的套件,把 response.headers
印出來看看:
const request = require('request');
request(
'https://github.com/Lidemy/mentor-program-4th',
function (error, response, body) {
console.log(response.headers)
// console.log('body:', body);
}
);
response.headers
就會是這樣:
{
server: 'GitHub.com',
date: 'Sat, 18 Jul 2020 05:54:59 GMT',
'content-type': 'text/html; charset=utf-8',
status: '200 OK',
vary: 'X-PJAX, Accept-Encoding, Accept, X-Requested-With',
...
Body:寫主要內容
GET 與 POST
在 HTTP 的 request 裡面,使用 GET, POST 這些動詞,來決定要執行什麼動作
當我想要「取得資訊」時,Request Method 就要使用 GET
例如:
當我想看這張杯子的圖片時,就會使用 GET,「GET」背後做的事情是:
瀏覽器發送了一個「GET」的 request 到 https://avatars2.githubusercontent.com/u/2755720?s=64&v=4 這個網址去,所得到的 response 就是這張圖片,瀏覽器再把這張圖片顯示出來讓我看到
當我想要送出資訊時,Request Method 就要使用 POST
以這個 GitHub 的 Sign in 頁面來說,打開 devtool > Network 頁籤,然後隨便輸入帳號密碼按下 Sign in,在 Network 頁籤點擊 session 就可以看到:
瀏覽器發送了一個 POST 的 request 到 https://github.com/session 這個網址去
這個 Post request 的 Body
往下拉,會看到一個 Form Data,這就是這個 POST request 的 Body(我要 POST 出去的東西),會有 key 和相對應的 value
例如:
我剛剛在 Sign in 表單中填寫的帳號(login: aa)、密碼(password: bb)
上圖看到的 Form Data 是 chrome 編碼之後的樣子(用一個比較方便觀看的格式顯示出來)
在 Form Data 點擊 view source,就會看到一堆很像亂碼的東西,這就是 Form Data 實際上在 request 的 Body 裡面的樣子(在 encode 之前的樣子)
發送 request 時:
當我使用 POST,就會有 request Body
- request 的 Body 就是「我要 POST 的東西」
當我使用 GET,並不會有 request Body
- 因為 GET 只是要「跟對方取得資訊」,不需要在 request Body 寫任何東西
其他的 HTTP Method
參考資料 HTTP request methods
最常用的 HTTP Method 就是 GET, POST, DELETE, PUT
GET
用來取得資料
HEAD
不會有 response body
在有些情況,我不想知道這個 response 的內容,我只想知道這個 response 的一些資訊,因此我只需要取得 response 的 header 就好
(此 method 會用到的機率很低)
POST
用來提交東西
PUT, PATCH
改變內容
PUT 跟 PATCH 很類似,差別在於:
- 「PUT 的內容」會取代掉原本的所有內容
- PATCH 只會對指定資源做「部分修改」
DELETE
刪除資源
CONNECT
(很少用)
OPTIONS
回傳給我「這個 server 支援哪些方法」
TRACE
(很少用)
常見的 HTTP Status code
HTTP Status code(狀態碼)
通常,Status code 都是以「不同的開頭」來區分,代表不同的意思
1** (Informational) — 資訊
1** 代表:server 已收到請求,但 client 端需要去進行一些處理
例如:
101 Switching Protocols:
server 需要我去使用另一個不同的 protocol
2** (Successful) — 成功
例如:
204 No Content
server 成功處理了 request,但沒有返回任何內容
例如,我發了一個 request 是「DELETE (刪除資源)」,如果刪除成功,就會回傳 204 的狀態碼(但不會返回任何內容,因為沒有什麼資訊需要返回)
3** (Redirection) — 重新導向
最常見的例如:
301 Moved Permanently
此資源已經被永久的移到新的位置(就是轉址的意思)
例如:我用 GET 訪問 a.com 這個資源,如果是回傳 301,
在 response header 會有一個 Location 的欄位是 b.com
下一次當我再發 request 到 a.com 時,不需要再問一次「要去哪裡」,瀏覽器就會直接幫我轉址到 b.com
因為從第一次的 request 中,瀏覽器已經知道:a.com 被永久的移到 b.com 了(瀏覽器把這個關係給存起來了)
只有第一次 request 需要問「要去哪裡」
// 第一次 request
GET a.com
status code: 301
Location: b.com
// 第二次 request
GET a.com => b.com(瀏覽器會直接去 b.com)
302 Found(Moved Temporarily)
此資源暫時的被移到新的位置
例如:我用 GET 訪問 a.com 這個資源,如果是回傳 302,
代表:a.com 只是暫時的被移到 b.com
下一次當我再發 request 到 a.com 時,還是會先去 a.com 問「要去哪裡」,最後再到 b.com
每次發 request,都會先去 a.com 問「要去哪裡」
// 第一次 request
GET a.com
status code: 302
Location: b.com
// 第二次 request
GET a.com
status code: 302
Location: b.com
4** (Client Error) — 客戶端 錯誤
例如:
- 400
- 404
5** (Server Error) — 伺服端 錯誤
例如:
- 500 Internal Server Error
在搶票時,server 可能就會出現錯誤
HTTP status codes cheat sheet
1 Hold on
2 Here you go
3 Go away
4 You fucked up
5** I fucked up
實作一個超簡易 HTTP Server
在 Node.js 裡面,有一個內建的 library 叫做 http
作法如下:
注意!每次修改程式碼,就要把 server 關掉再重跑一次,不然不會更新
先把 http 給 require 進來
然後就可以宣告一個變數 server
,並使用 http.createServer()
建立一個 server
var http = require('http');
var server = http.createServer()
有了 server 後,要怎麼處理 server 裡面的資訊呢?
作法就是:傳入一個 function 叫做 handleRequest
在 handleRequest
function 裡面:
res.write('I love you')
代表:要回傳的 response 內容是 'I love you'res.end()
代表:讓 client 端知道「這個 response 結束了」
最後,要用 server.listen(5000)
給它一個 port 名稱(也就是:服務代碼),server 就會去監聽 5000
這個 port
var http = require('http');
var server = http.createServer(handleRequest);
function handleRequest(req, res){
console.log(req.url);
res.write('I love you');
res.end();
}
server.listen(5000) // port
這時,輸入指令 node index.js
會發現:
不會出現任何東西,這很正常,因為我沒有 log 任何東西(按下 ctrl + C 可以結束 server)
但是,(要在 node index.js
執行這個 server 時)當我在瀏覽器網址列輸入 localhost:5000 按下 Enter,就看到我剛剛設定的 response 內容了
(5000 就是我剛剛指定的 port,要輸入 5000 才能連接到這個服務)
這時,再回到 CLI 去看 console.log(req.url)
,會把 request 的 url 都印出來:
/
/favicon.ico
/
就是「根目錄」/favicon.ico
就是「網頁左上角的小 icon」,瀏覽器預設都會發送一個 request 問說「有沒有這個 icon 可以拿」,有的話就拿回來並顯示出來(此範例中,並沒有拿到 favicon)
打開 devtool,可以看到:
用 GET 發送 request 到 http://localhost:5000/ 這個網址去
response 的內容就是 I love you
拜訪不同的頁面
這裡的 request URL 就會是 http://localhost:5000/hey
然後,console.log(req.url)
就會印出 /hey
這個 URL:
/hey
/favicon.ico
根據 req.url
做不同的事情
我設定兩個不同的 url 會回傳不同的 response:
var http = require('http');
var server = http.createServer(handleRequest);
function handleRequest(req, res){
if (req.url === '/') {
res.write('welcome!');
res.end();
return
}
if (req.url === '/about') {
res.write('Hi, I am Harry.');
res.end();
return
}
}
server.listen(5000) // port
server 端就會根據不同的網址,回傳不同的 response
- 輸入 http://localhost:5000/ 就會顯示「welcome!」
- 輸入 http://localhost:5000/about 就會顯示「Hi, I am Harry.」
指定 Header
如果想要「指定 Header」,可以用一個 function 叫做 res.writeHead()
,裡面傳入兩個參數:
- 第一個參數:填入「回傳的狀態碼」
- 第二個參數:填入「Header 要放的內容」,要注意的是,key 不能有空格,例如:
'myBlog': 'good'
不能寫成'my Blog': 'good'
var http = require('http');
var server = http.createServer(handleRequest);
function handleRequest(req, res){
if (req.url === '/') {
res.write('welcome!');
res.end();
return
}
if (req.url === '/about') {
res.write('Hi, I am Harry.');
res.end();
return
}
// 指定 Header 的內容
if (req.url === '/page3') {
res.writeHead(200, {
'myBlog': 'good'
})
res.end();
return
}
res.writeHead(404);
res.end();
}
server.listen(5000) // port
在最後面加上 res.writeHead(404)
,因此如果輸入了一個亂打的網址 http://localhost:5000/kkk ,頁面就會顯示「This localhost page can’t be found」,並且在 devtool 顯示 404
如果輸入網址 http://localhost:5000/page3 ,點進 page3 的 Response Headers,會看到「myBlog: good」,就是我剛剛指定的 Header
重新導向
如果想要「重新導向」,狀態碼就是 301 或 302
這裡,可以用一個 function 叫做 res.writeHead()
,裡面傳入兩個參數:
- 第一個參數:填入「回傳的狀態碼」
- 第二個參數:填入「Header 要放的東西」,因為我是要「重新導向(轉址)」,因此一定要有一個叫做
Location
的 Header,後面的 value 就是「要重新導向到哪一個網址/about
」
var http = require('http');
var server = http.createServer(handleRequest);
function handleRequest(req, res){
if (req.url === '/') {
res.write('welcome!');
res.end();
return
}
if (req.url === '/about') {
res.write('Hi, I am Harry.');
res.end();
return
}
// 重新導向
if (req.url === '/redirect') {
res.writeHead(302, {
'Location': '/about'
})
res.end();
return
}
res.writeHead(404);
res.end();
}
server.listen(5000) // port
這時,輸入網址 http://localhost:5000/redirect ,瀏覽器就會幫我轉址到 http://localhost:5000/about 去了。
打開 devtool,可以看到:
- Status Code: 302 Found
- Location: /about
所以,下一個 request 就會到 /about 去了
轉址到 google.com
當然,也可以轉址到其他網址去,例如:'Location': 'https://google.com'
var http = require('http');
var server = http.createServer(handleRequest);
function handleRequest(req, res){
if (req.url === '/') {
res.write('welcome!');
res.end();
return
}
if (req.url === '/about') {
res.write('Hi, I am Harry.');
res.end();
return
}
// 重新導向
if (req.url === '/redirect') {
res.writeHead(302, {
'Location': 'https://google.com'
})
res.end();
return
}
res.writeHead(404);
res.end();
}
server.listen(5000) // port
這時,輸入網址 http://localhost:5000/redirect ,瀏覽器就會幫我轉址到 https://www.google.com/ 去了。
點進去 google.com 會發現 https://google.com/ 也有一個 301 轉址,轉到 https://www.google.com/