本系列之前的測試紀錄參考 :
# 關於 Node.js
# 如何更新樹莓派的 Node.js
# Node.js 學習筆記 (一) : 在 Windows 安裝 Node.js
以下測試主要參考下面幾本書 :
- 深入淺出 Node.js (博碩, 朴靈)
- Node.js 實戰手冊 (碁峰, 楊仁和)
- Google 御用語言 Node.js (第二版, 佳魁, 郭家寶)
- 網頁應用程式 : 使用 Node 和 Express (歐萊禮)
- PhoneGap + Node.js 整合實作 (PCuSER, 彭祖乙)
- Javascript+jQuery Mobile+Node.js 跨平台網頁設計範例教本 (碁峰, 陳會安)
Node 標準函式庫提供 http 模組, 此核心模組以 Javascript 封裝了一個底層由 C++ 實作的高效率 HTTP 伺服器 (http.server 物件) 與簡易的 HTTP 客戶端 (http.request 物件), 兼顧了高性能與使用簡易的雙重需求. Node 的設計哲學是只維持一個小而強的網路 API 核心 (如 http, net, 與 querryString 等), 而非自行實作高階的框架, 這些都交給第三方模組如 Connect 與 Express 等框架提供. 因此在 Node 核心看不到 Session 與 Cookies 等高階元素.
Node 的 http 模組是一個實作 HTTP 協定的低階事件驅動 API, 可以用來建立網頁伺服器以處理 HTTP 請求 (Request) 與回應 (Response) 之串流 (Stream) 資料, 將其剖析成本文 (Body) 與標頭 (Header) 兩大部分 (但不會進一步解析 Body 內容), 大大簡化了串流資料編解碼與訊息處理的困難. http 模組也可以用來建立客戶端程式, 模擬瀏覽器與伺服器連線, 參考 :
# https://nodejs.org/api/http.html
使用 http 模組建立 Web 應用前須用 require 載入此模組並指定一個變數接收傳回之 http 物件 :
var http=require('http');
傳回值是一個 http 物件, 用 console.log(http) 顯示物件內容 :
> console.log(JSON.stringify(http, null, 4));
http 物件主要的兩個屬性 :
- STATUS_CODES : 定義回應碼
- METHODS : 定義 GET, POST, HEAD, DELETE, UPDATE 等 HTTP 協定方法
> var http=require("http")
undefined
> http.STATUS_CODES
{ '100': 'Continue',
'101': 'Switching Protocols',
'102': 'Processing',
'103': 'Early Hints',
'200': 'OK',
'201': 'Created',
'202': 'Accepted',
'203': 'Non-Authoritative Information',
'204': 'No Content',
'205': 'Reset Content',
'206': 'Partial Content',
'207': 'Multi-Status',
'208': 'Already Reported',
'226': 'IM Used',
'300': 'Multiple Choices',
'301': 'Moved Permanently',
'302': 'Found',
'303': 'See Other',
'304': 'Not Modified',
'305': 'Use Proxy',
'307': 'Temporary Redirect',
'308': 'Permanent Redirect',
'400': 'Bad Request',
'401': 'Unauthorized',
'402': 'Payment Required',
'403': 'Forbidden',
'404': 'Not Found',
'405': 'Method Not Allowed',
'406': 'Not Acceptable',
'407': 'Proxy Authentication Required',
'408': 'Request Timeout',
'409': 'Conflict',
'410': 'Gone',
'411': 'Length Required',
'412': 'Precondition Failed',
'413': 'Payload Too Large',
'414': 'URI Too Long',
'415': 'Unsupported Media Type',
'416': 'Range Not Satisfiable',
'417': 'Expectation Failed',
'418': "I'm a Teapot",
'421': 'Misdirected Request',
'422': 'Unprocessable Entity',
'423': 'Locked',
'424': 'Failed Dependency',
'425': 'Unordered Collection',
'426': 'Upgrade Required',
'428': 'Precondition Required',
'429': 'Too Many Requests',
'431': 'Request Header Fields Too Large',
'451': 'Unavailable For Legal Reasons',
'500': 'Internal Server Error',
'501': 'Not Implemented',
'502': 'Bad Gateway',
'503': 'Service Unavailable',
'504': 'Gateway Timeout',
'505': 'HTTP Version Not Supported',
'506': 'Variant Also Negotiates',
'507': 'Insufficient Storage',
'508': 'Loop Detected',
'509': 'Bandwidth Limit Exceeded',
'510': 'Not Extended',
'511': 'Network Authentication Required' }
> http.METHODS
[ 'ACL',
'BIND',
'CHECKOUT',
'CONNECT',
'COPY',
'DELETE',
'GET',
'HEAD',
'LINK',
'LOCK',
'M-SEARCH',
'MERGE',
'MKACTIVITY',
'MKCALENDAR',
'MKCOL',
'MOVE',
'NOTIFY',
'OPTIONS',
'PATCH',
'POST',
'PROPFIND',
'PROPPATCH',
'PURGE',
'PUT',
'REBIND',
'REPORT',
'SEARCH',
'SOURCE',
'SUBSCRIBE',
'TRACE',
'UNBIND',
'UNLINK',
'UNLOCK',
'UNSUBSCRIBE' ]
http 物件主要的三個方法如下 :
http 物件方法 | 說明 |
createServer([callback]) | 建立 HTTP 伺服器, 傳回 Server 物件 callback=監聽 HTTP 請求事件之回呼函數, 攜帶 2 個參數 ServerRequest (請求物件), ServerResponse (回應物件) |
request(options, callback) | 建立 HTTP 客戶端連線 options=伺服器選項 (URL, port, 請求方法等) callback=攜帶回應物件 ClientResponse 的回呼函數 |
get(options, callback) | 以 GET 方法建立 HTTP 客戶端連線 options=伺服器選項 (URL, port, 請求方法等) callback=攜帶回應物件 ClientResponse 為參數的回呼函數 |
其中 http.createServer() 用在伺服端建立 HTTP 伺服器 (回呼函數是被選參數); 而 http.request() 與 http.get() 則用來建立客戶端 HTTP 連線. 呼叫 http.createServer() 會傳回伺服器物件 http.Server, 其常用方法如下:
Server 物件之方法 | 說明 |
listen(port, [host], [backlog], [callback]) | 監聽指定主機埠的事件 host 預設為 'localhost' 或 '127.0.0.1' |
on(event, callback(req, res)) | 監聽指定事件 event (字串), 觸發時呼叫 callback() 函數 常用事件 : 'request' : 接到客戶端請求時觸發 'connection' : TCP 連線建立時觸發 'close' : 關閉伺服器時觸發 |
http.Server 是以事件為基礎的伺服器, 所有來自客戶端的請求都會被封裝成獨立之事件物件 (繼承自 EventEmitter), 只要針對觸發之事件撰寫對應之回呼函數即可實作 HTTP 伺服器之所有功能. 注意, server.on() 的回呼函數與 http.createServer() 的回呼函數是一樣的, 均傳入 req 與 res 物件當參數.
Node 的 http 模組是一個封裝等級很低階的 API, 僅僅是低層的串流控制與訊息剖析, 實務上通常不直接使用 http 模組來建構網頁應用程式, 而是使用較高階的第三方框架如 Express 等.
呼叫 http 物件的 createServer() 方法可來建立 HTTP 伺服器 (傳回 http.Server 物件), 此方法須指定一個帶有兩個參數 req 與 res 的回呼函數來處理回應, 當請求標頭被解析完成後, 它會被打包成 http.ServerRequest 物件傳入回呼函數的第一參數 req 中 :
function(req, res) {
res.writeHead(200, {'ContentType': 'text/plain'});
res.end();
};
第一個參數 req 代表來自客戶端之資源請求物件 http.ServerRequest, 這是後端程式開發最重要的資訊, 主要內容是請求標頭, 其屬性如下表 :
Request 物件之屬性 | 說明 |
url | 原始之請求路徑 |
method | HTTP 請求方法, 如 GET, PUT, POST, DELETE 等 |
httpVersion | HTTP 協定版本 (1.0 或 1.1) |
headers | HTTP 請求標頭 (物件) |
trailers | HTTP 請求尾部 (物件) |
complete | 客戶端請求是否已發送完畢 (true/false) |
connection | HTTP 連線 socket, 為 net.Socket 物件 (物件) |
socket | 同 connection, 為其別名 (物件) |
client | 客戶端別名 (物件) |
第二個參數 res 是伺服器用來發出回應的 http.ServerResponse 物件. 在第一個 res.write() 或 res.end() 出現之前可以任何順序新增或移除回應標頭, 當第一個 res.write() 或 res.end() 出現時, Node 就會送出 (flush) 設定好的回應標頭.
在回呼函數中可以用下列 Response 物件的方法來回應客戶端要求 :
Response 物件之方法 | 說明 |
writeHead(code, header) | 送出回應碼 code (3 位數整數) 與 HTTP 回應標頭物件 header |
setHeader(attr, value) | 設定 HTTP 標頭屬性 attr 之值為 value |
getHeader(attr) | 傳回指定標頭屬性之值 |
removeHeader(attr) | 刪除指定標頭屬性 |
write(data [, encoding]) | 將 data 碼寫入回應串流, 若為字串以 encoding 編碼 (預設 utf8) |
end([data, encoding]) | 將 data 寫入回應串流並結束回應訊息, 若為字串以 encoding 編碼 (預設 utf8) |
此處 data 可以是 buffer 或字串. 方法 end() 會結束回應訊息, 可以傳入要輸出至回應之資料, 也可以直接結束回應訊息. 如果是字串應指定 encoding 編碼. 方法 writeHead() 可以一次將全部標頭資訊 (物件) 與回應碼寫入緩衝區; 而方法 setHeader() 則是只能一次寫入一個標頭屬性. 若使用 setHeader(), 則回應碼要用 res.statusCode 屬性設定.
注意, writeHead() 方法在回呼函數內只能使用一次, 而 write() 可以多次呼叫, 但都必須在 end() 之前呼叫.
一. 基本的 HTTP 伺服器 :
Node.js 打造的最簡單 HTTP 伺服器如下, 是直接在程式中輸出回應訊息 :
測試 1 : 回應 HelloWorld 的 HTTP 伺服器 (1)
var http=require('http')
http.createServer(function(req, res) {
res.writeHead(200, {'ContentType': 'text/plain'});
res.end('Hello World!\n');
}).listen(8080);
console.log('HTTP 伺服器正在監聽 8080 埠 ...');
對於輸出純文字而言, 也可以不用 res.writeHead() 指定回應碼與 ContentType 標頭屬性 , 因為伺服器預設回應 200 OK 與 text/plain 內容型態; 但如果是回應 HTML 內容時必須指定 text/html, 這樣瀏覽器才知道要將內容渲染為 HTML. 將此程式存成 js 檔例如 test1.js, 然後開啟命令提示字元視窗, 以 node 指令執行此 js :
D:\Node.js\test>node test1.js
HTTP 伺服器正在監聽 8080 埠 ...
可見伺服器已經啟動, 按 Ctrl+C 可停止程式. 這時 Windows 防火牆會彈出警告視窗, 要按 "允許存取" 程式才會有效 :
然後開啟瀏覽器連線 localhost:8080 或者 127.0.0.1:8080 即可看到伺服器輸出的網頁 :
上面測試 1 程式也可以寫成如下 :
測試 2 : 回應 HelloWorld 的 HTTP 伺服器 (2)
var http=require('http')
var server=http.createServer(function(req, res) {
res.setHeader('ContentType', 'text/plain');
res.statusCode=200;
res.write('Hello World!\n');
res.end();
});
server.listen(8080);
console.log('HTTP 伺服器正在監聽 8080 埠 ...');
注意, 上述範例中呼叫 http.createServer() 時有傳入一個回呼函數作為請求事件監聽器, 但那不是必要的參數, 也可以呼叫 server.on() 來添加事件監聽器, 測試 1 程式改寫如下 :
測試 3 : 呼叫 server.on() 來添加事件監聽器 [看原始碼]
var http=require('http');
var server=http.createServer(); //傳回 http.Server 物件
server.on('request', function(req, res) { //監聽請求事件
res.writeHead(200, {'ContentType': 'text/plain'});
res.end('Hello World!\n');
});
server.listen(8080, 'localhost');
console.log('HTTP 伺服器正在監聽 8080 埠 ...');
此例在呼叫 createServer() 不傳入回呼函數, 而是以 server 變數紀錄所建立之 http.Server 物件, 然後呼叫其 on() 函數並傳入回呼函數來監聽 request 請求事件, 效果與測試 1 一樣.
若要回應 HTML 網頁, 則 Content-Type 要改為 'text/html', 如下面範例 :
測試 4 : 回應 HelloWorld 的 HTTP 伺服器 (HTML) [看原始碼]
var http=require('http')
var server=http.createServer(function(req, res) {
res.writeHead(200, {'ContentType': 'text/html'});
res.write('<!DOCTYPE html>\n');
res.write('<head>\n');
res.write('<title>Hello</title>\n');
res.write('</head>\n');
res.write('<body>\n');
res.write('<b>Hello World!</b>\n');
res.write('</body>\n');
res.write('</html>');
res.end();
});
server.listen(8080);
console.log('HTTP 伺服器正在監聽 8080 埠 ...');
顯示網頁原始碼如下 :
注意, res.write() 中 HTML 碼以 \n 結尾的目的是要讓 HTML 原始碼的可讀性高, 若無 \n 則全部 HTML 會串在一起.
下面範例主要是檢視請求物件 req 的內容 :
測試 5 : 顯示 req 物件之屬性 (1) [看原始碼]
var http=require('http')
http.createServer(function(req, res) {
console.log("req.url=" + req.url + "\r\n");
console.log("req.method=" + req.method + "\r\n");
console.log("req.httpVersio=" + req.httpVersion + "\r\n");
console.log("req.complete=" + req.complete + "\r\n");
console.log("req.headers=" + req.headers + "\r\n");
console.log("req.trailers=" + req.trailers + "\r\n");
console.log("req.connection=" + req.connection + "\r\n");
console.log("req.socket=" + req.socket + "\r\n");
console.log("req.client=" + req.client + "\r\n");
res.write('Hello World!\n');
res.end();
}).listen(8080);
console.log('HTTP 伺服器正在監聽 8080 埠 ... \r\n');
執行結果如下 :
HTTP 伺服器正在監聽 8080 埠 ...
req.url=/
req.method=GET
req.httpVersio=1.1
req.complete=false
req.headers=[object Object]
req.trailers=[object Object]
req.connection=[object Object]
req.socket=[object Object]
req.client=[object Object]
req.url=/favicon.ico
req.method=GET
req.httpVersio=1.1
req.complete=falsereq.headers=[object Object]
req.trailers=[object Object]
req.connection=[object Object]
req.socket=[object Object]
req.client=[object Object]
結果有兩個請求出現, 除了瀏覽首頁這個請求外, 還有一個 favicon.ico 圖檔資源的請求. 可見 headers, socket, connection 等後面 5 個屬性值為物件, 其中 headers 與 trailers 可用 JSON.stringify() 將其轉成字串來顯示內容, 例如 :
測試 6 : 顯示 req 物件之屬性 (2) [看原始碼]
var http=require('http')
http.createServer(function(req, res) {
console.log("req.url=" + req.url + "\r\n");
console.log("req.method=" + req.method + "\r\n");
console.log("req.httpVersio=" + req.httpVersion + "\r\n");
console.log("req.complete=" + req.complete + "\r\n");
console.log("req.headers=" + JSON.stringify(req.headers) + "\r\n");
console.log("req.trailers=" + JSON.stringify(req.trailers) + "\r\n");
console.log("req.connection=" + req.connection + "\r\n");
console.log("req.socket=" + req.socket + "\r\n");
console.log("req.client=" + req.client + "\r\n");
res.write('Hello World!\n');
res.end();
}).listen(8080);
console.log('HTTP 伺服器正在監聽 8080 埠 ... \r\n');
結果如下 :
HTTP 伺服器正在監聽 8080 埠 ...
req.url=/
req.method=GET
req.httpVersio=1.1
req.complete=false
req.headers={"host":"localhost:8080","connection":"keep-alive","cache-control":"max-age=0","upgrade-insecure-requests":"1","user-agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36","accept":"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8","accept-encoding":"gzip, deflate, br","accept-language":"zh-TW,zh;q=0.9,en-US;q=0.8,en;q=0.7"}
req.trailers={}
req.connection=[object Object]
req.socket=[object Object]
req.client=[object Object]
req.url=/favicon.ico
req.method=GET
req.httpVersio=1.1
req.complete=false
req.headers={"host":"localhost:8080","connection":"keep-alive","pragma":"no-cache","cache-control":"no-cache","user-agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36","accept":"image/webp,image/apng,image/*,*/*;q=0.8","referer":"http://localhost:8080/","accept-encoding":"gzip, deflate, br","accept-language":"zh-TW,zh;q=0.9,en-US;q=0.8,en;q=0.7"}
req.trailers={}
req.connection=[object Object]
req.socket=[object Object]
req.client=[object Object]
可見 headers 屬性儲存了客戶端請求標頭, 而 trailers 為空的物件. 注意, connection, socket, 與 client 三者為 circular 結構不能用 JSON.stringify(), 否則執行錯誤.
二. HTTP 客戶端程式 :
上面的範例全部都是以瀏覽器作為 HTTP 客戶端代理, 實際上 http 模組除了可用 createServer() 建立 HTTP 伺服器外, 還可以用 request() 建立客戶端程式, 取代瀏覽器來與伺服器連線.
http 的 request() 方法須傳入兩個參數, 第一參數是關於遠端伺服器的連線選項物件 options, 包含下列屬性 :
- host : 伺服器之主機位址
- port : 伺服器之主機埠號
- path : 資源路徑
- method : HTTP 請求方法 (GET/POST/PUT/DELETE 等)
- headers : HTTP 請求標頭
測試 7 : HTTP 客戶端程式 [看原始碼]
var http=require('http');
var options={
hostname: 'localhost',
port: 8080,
path: '/',
method: 'POST'
}
var client=http.request(options, function(res) {
console.log('Status code : ' + res.statusCode);
res.setEncoding('utf8');
res.on('data',function(chunk) { //監視 data 事件 (收到 body)
console.log('BODY:' + chunk);
});
});
client.on('error', function(e) {
console.log('Errror : ' + e.message);
});
client.write('ABC\n');
client.end();
先開啟 DOS 視窗執行上面測試 1 或測試 2 程式, 然後開啟另一個 DOS 視窗執行上面的 HTTP 客戶端程式. 結果如下 :
D:\Node.js\test>node client_request.js
Status code : 200
Headers : {"contenttype":"text/plain","date":"Fri, 04 Jan 2019 03:35:18 GMT","connection":"close","transfer-encoding":"chunked"}
BODY:Hello World!
可見同樣是收到伺服端回應的 "Hello World!".
三. 取得 HTTP 請求內容 :
http 模組處理 HTTP 請求的程序與其他伺服端框架如 PHP 有些不同, PHP 在執行應用邏輯之前, header 與 body 均已被解析完畢; 然而 Node 卻不是這樣, 當 Node 伺服器收到 HTTP 請求時, request 事件會被觸發, Node 只會解析請求標頭, 並將結果封裝成 http.ServerRequest 物件傳給回呼函數, Node 預設不會對正文進行解析處理, 但提供下列三個事件讓使用者自行處理正文之接收 :
Request 物件之事件 | 說明 |
data | 收到請求本文時觸發 data 事件, 並傳入 chunk 參數給回呼函數. |
end | 請求本文全部傳送完成時觸發 end 事件 |
close | 使用者強制停止請求時觸發 close 事件 |
這是因為請求正文 (body) 可能很長 (例如檔案上傳), 等待 body 傳輸可能很耗時而拖累伺服器效能之故. Node 作法的好處是, 若有需要可以在 body 被解析之前利用 data 事件的回呼函數先進行一些處理, 此回呼函數會傳入所收到的正文內容 chunk, 這是一個 Buffer 物件 (Byte 陣列).
1. 取得 GET 方法之請求內容 :
HTTP 1.1 定義的 8 種請求方法中常用的只有 GET 與 POST 方法 (但在採用 RESTful 架構的設計中則使用了較多其他請求方法來簡化設計). GET 請求的內容直接嵌入於 URL 路徑內而非本文 (body) 中, 例如執行上面測試 6 的伺服器程式後, 在瀏覽器輸入下列網址 :
http://127.0.0.1:8080/user?name=tony&phone=0912345678
GET 請求的內容就是 ? 後面以 & 串起來的參數, 經過 http 模組剖析後會放在 req 物件的 url 屬性中, 伺服端程式執行結果中的 req.url 輸出如下 :
D:\Node.js\test>node http_test_6.js
HTTP 伺服器正在監聽 8080 埠 ...
req.url=/user?name=tony&phone=0912345678
使用 Node 核心模組 url 的 parse() 方法可以解析 req.url 屬性, 此方法可傳入三個參數, 第一個參數為 URL 字串, 其他為備選參數, 預設為 false, 如果要進一步解析查詢字串, 則第二參數要傳入 true :
# url.parse(urlString[, parseQueryString[, slashesDenoteHost]])
url.parse() 會傳回一個 URL 物件, 其中有三個屬性與路徑以及 GET 請求內容有關 :
GET 請求的內容就是 ? 後面以 & 串起來的參數, 經過 http 模組剖析後會放在 req 物件的 url 屬性中, 伺服端程式執行結果中的 req.url 輸出如下 :
D:\Node.js\test>node http_test_6.js
HTTP 伺服器正在監聽 8080 埠 ...
req.url=/user?name=tony&phone=0912345678
使用 Node 核心模組 url 的 parse() 方法可以解析 req.url 屬性, 此方法可傳入三個參數, 第一個參數為 URL 字串, 其他為備選參數, 預設為 false, 如果要進一步解析查詢字串, 則第二參數要傳入 true :
# url.parse(urlString[, parseQueryString[, slashesDenoteHost]])
url.parse() 會傳回一個 URL 物件, 其中有三個屬性與路徑以及 GET 請求內容有關 :
- pathname : 路徑
- path : 路徑 + 參數
- query : 參數物件
測試 8 : 讀取 GET 請求內容 [看原始碼]
var http=require('http');
var url=require('url');
http.createServer(function(req, res) {
res.writeHead(200, {'ContentType': 'text/plain'});
console.log(req.url);
console.log(url.parse(req.url));
console.log(url.parse(req.url, true).query);
res.end('Hello World!');
}).listen(8080);
console.log('HTTP 伺服器正在監聽 8080 埠 ...');
var http=require('http');
var url=require('url');
http.createServer(function(req, res) {
res.writeHead(200, {'ContentType': 'text/plain'});
console.log(req.url);
console.log(url.parse(req.url));
console.log(url.parse(req.url, true).query);
res.end('Hello World!');
}).listen(8080);
console.log('HTTP 伺服器正在監聽 8080 埠 ...');
注意, 上面 url.parse() 第二參數傳入 true 表示要對參數進行解析, 其傳回值為一物件, 在瀏覽器輸入如下網址 :
http://127.0.0.1:8080/user?name=tony&phone=0912345678
伺服端程式輸出如下 :
D:\Node.js\test>node http_test_8.js
HTTP 伺服器正在監聽 8080 埠 ...
/user?name=tony&phone=0912345678
Url {
protocol: null,
slashes: null,
auth: null,
host: null,
port: null,
hostname: null,
hash: null,
search: '?name=tony&phone=0912345678',
query: 'name=tony&phone=0912345678',
pathname: '/user',
path: '/user?name=tony&phone=0912345678',
href: '/user?name=tony&phone=0912345678' }
[Object: null prototype] { name: 'tony', phone: '0912345678' }
從最後一列輸出可知, 查詢參數已被解析成物件. 在 RESTful 風格的架構中, 會將路徑當作控制器, 行為, 與判斷參數的組合, 例如 :
/controller/action/a/b/c
controller : 控制器
action : 控制器要採取的動作
a/b/c : 判斷用的參數
利用 url 模組的 parse() 方法可解析出 pathname 字串, 再用 split() 即可取得判斷用的參數 :
var url=require('url');
var pathname=url.parse(req.url).pathname
var paths=pathname.split()
改用如下的 URL :
http://127.0.0.1:8080/controller/action/a/b/c?who=tony&gender=male
則伺服端程式輸出如下 :
D:\Node.js\test>node http_test_8.js
HTTP 伺服器正在監聽 8080 埠 ...
/controller/action/a/b/c?who=tony&gender=male
Url {
protocol: null,
slashes: null,
auth: null,
host: null,
port: null,
hostname: null,
hash: null,
search: '?who=tony&gender=male',
query: 'who=tony&gender=male',
pathname: '/controller/action/a/b/c',
path: '/controller/action/a/b/c?who=tony&gender=male',
href: '/controller/action/a/b/c?who=tony&gender=male' }
[Object: null prototype] { who: 'tony', gender: 'male' }
2. 取得 POST 方法之請求內容 :
POST 請求內容放在本文 (Body) 中, 如上所述, Node 的 http 模組不解析 Body 本文, 而是提供 req 物件的 data 事件由開發者自行接收解析. 例如下列範例中, 利用上面測試 7 的 HTTP Client 程式送出本文內容為 'ABC' 的 POST 請求, 伺服端則利用監聽 req 物件的 data 事件來取得 HTTP 的請求訊息本文 :
測試 9 : 利用 data 事件讀取 POST 請求內容 (1) [看原始碼]
var http=require('http')
var server=http.createServer(function(req, res) {
req.on('data', function(chunk) {
console.log(chunk);
});
req.on('end', function() {
console.log('Request ends');
res.end();
});
});
server.listen(8080, 'localhost');
console.log('HTTP 伺服器正在監聽 8080 埠 ...');
執行上面測試 7 的 HTTP 客戶端程式 (不要用瀏覽器, 因為此處需要送出 HTTP Body 本文), 則此伺服端程式輸出如下 :
D:\Node.js\test>node test10.js
HTTP 伺服器正在監聽 8080 埠 ...
<Buffer 41 42 43 0a>
Request ends
此處輸出之位元組陣列 41 42 43 分別是 'ABC' 的 ASCII 碼, 如果要顯示 'ABC', 必須設定 req 物件的編碼為 'utf8', 例如 :
測試 10 : 利用 data 事件讀取請求內容 (2) [看原始碼]
var http=require('http')
var server=http.createServer(function(req, res) {
req.setEncoding('utf8');
req.on('data', function(chunk) {
console.log(chunk);
});
req.on('end', function() {
console.log('Request ends');
res.end();
});
});
server.listen(8080, 'localhost');
console.log('HTTP 伺服器正在監聽 8080 埠 ...');
同樣用 HTTP 客戶端程式提出 POST 請求, 伺服端輸出就變成字串 ABC, 而不是 Buffer 物件了 :
D:\Node.js\test>node server_request_data_event_1.js
HTTP 伺服器正在監聽 8080 埠 ...
ABC
其實不設定編碼, 呼叫 Buffer 物件 (chunk) 的 toString() 方法也會轉換成可讀字串 (預設 utf8 編碼), 例如 :
測試 11 : 利用 data 事件讀取請求內容 (2) [看原始碼]
var http=require('http')
var server=http.createServer(function(req, res) {
req.on('data', function(chunk) {
console.log(chunk.toString());
});
req.on('end', function() {
console.log('Request ends');
res.end();
});
});
server.listen(8080, 'localhost');
console.log('HTTP 伺服器正在監聽 8080 埠 ...');
結果與用 setEncoder() 是一樣的.
3. 依據請求方法解析請求內容 :
當 http 模組的剖析器分析請求封包抽取標頭時, 其中第一行的第一個單字就是請求方法 (通常為大寫), 並將其設定為 req.method 屬性之值, 在伺服端程式的回呼函數中可利用 req.method 來判斷對此請求進行適當之操作. 架構如下 :
function(req, res) {
switch (req.method) {
case 'POST' :
update(req, res);
break;
case 'GET' :
default :
get(req, res);
break;
}
}
測試 12 : 利用 req.metod 判斷請求方法 [看原始碼]
var http=require('http');
var url=require('url');
var server=http.createServer(function(req, res) {
res.writeHead(200, {'ContentType': 'text/plain'});
switch (req.method) {
case 'POST' :
req.setEncoding('utf8');
req.on('data', function(chunk) {
console.log(chunk);
});
req.on('end', function() {
console.log('Request ends');
res.end();
});
break;
case 'GET' :
default :
console.log(url.parse(req.url, true).query);
res.end('Hello World!');
break;
}
});
server.listen(8080, 'localhost');
console.log('HTTP 伺服器正在監聽 8080 埠 ...');
先在瀏覽器輸入 :
然後執行測試 7 的 HTTP 客戶端程式, 結果如下 :
HTTP 伺服器正在監聽 8080 埠 ...
[Object: null prototype] { who: 'tony', gender: 'male' }
[Object: null prototype] {}
您好
Request ends
可見不論是 GET 或 POST 都能順利取得請求內容了. 這個功能在 PHP 非常簡單, 只要用 $_GET 與 $_POST 屬性即可取得, 但在 Node 卻如此麻煩, 因為 http 模組是 Node 的核心低階模組之故.
以上的測試範例都是將欲輸出之訊息直接寫死在後端程式中, 這樣缺乏彈性, 理想作法應該是根據客戶端請求之網址路徑, 利用檔案模組 fs 讀取儲存於伺服器檔案系統中的靜態網頁檔案後回應給客戶端, 伺服器的處理程序如下 :
- 建立 HTTP 伺服器並監聽指定的 port
- 等待客戶端連線
- 收到客戶端連線
- 分析網址取得請求資源之檔案路徑
- 檢查檔案是否存在, 是則讀取檔案回應給客戶端, 否則回應不存在
# Node.js 學習筆記 (三) : 檔案模組 fs 測試
而 path 模組主要是用來解析與合成檔案路徑, 因為不同的作業系統採用的路徑分隔符號不同 (Windows 用左斜 '\', Linux 用右斜 '/'), 使用 path 模組可以免除這個平台移植性問題, 用法參考 :
# https://nodejs.org/api/path.html
主要會用到 path 模組的兩個方法 :
- path.basename(path [,ext]) :
傳回路徑的最後部分 (目錄或檔案名稱), 不包含副檔名 ext - path.join(seg1, seg2, ....)
將路徑片段 seg1, seg2, ... 以系統路徑分隔符號依序串成完整路徑.
測試 13 : 利用 fs 讀取檔案並回應客戶端 [看原始碼]
var http=require('http');
var path=require('path');
var fs=require('fs');
http.createServer(function(req, res) {
var filename=path.basename(req.url); //請求之檔名
//串接檔案路徑, 若無要求資源檔預設 index.htm
var filepath=path.join(__dirname, 'public', (filename || 'index.htm'));
console.log(__dirname);
console.log(filename);
console.log(filepath);
fs.exists(filepath, function(exists) {
if (exists) { //網頁存在
fs.readFile(filepath, function(err, data) {
if (!err) {
res.writeHead(200, {'ContentType': 'text/html'});
res.end(data);
}
else {console.log(err);}
});
}
else { //網頁不存在
res.writeHead(404, {'ContentType': 'text/plain'});
res.end('Not Found');
}
});
}).listen(8080, 'localhost');
console.log('HTTP 伺服器正在監聽 8080 埠 ...\n');
此例我先在目前程式目錄 D:\Node.js\test 下建立一個存放網頁檔的資料夾 public, 然後製作一個簡單的首頁 index.htm 放在 public 下. 程式先用 path.basename() 從 req.url 取得請求網址的最後部分 (即檔名, 若無為空字串), 然後用path.join() 串接伺服器程式所在目錄 __dirname, 網頁檔所在目錄 public, 以及網頁檔名. 此處請求中若無網頁檔名, 則 filename 為空字串, 這時應該回應首頁檔, 故用 || 與 index.htm 做 or 運算. 如果不這麼做, 當客戶端不指定網頁檔名時 (例如 http://localhost:8080 時), join() 會出現如下錯誤 :
{ [Error: EISDIR: illegal operation on a directory, read] errno: -4068, code: 'EISDIR', syscall: 'read' }
執行上述瀏覽器程式後, 在瀏覽器網址列分別輸入 :
http://127.0.0.1:8080
http://127.0.0.1:8080/index.htm
http://127.0.0.1:8080/a.htm
在伺服器視窗輸出如下 :
D:\Node.js\test>node test12.js
HTTP 伺服器正在監聽 8080 埠 ...
D:\Node.js\test (請求 http://127.0.0.1:8080)
D:\Node.js\test\public\index.htm
D:\Node.js\test (請求 http://127.0.0.1:8080/index.htm)
index.htm
D:\Node.js\test\public\index.htm
D:\Node.js\test (請求 http://127.0.0.1:8080/a.htm)
a.htm
D:\Node.js\test\public\a.htm
由於 a.htm 不存在, 因此第三次請求會得到 "Not Found" 回應.
上面測試 13 根據 URL 最後面的檔名來從指定目錄 public 下讀取網頁檔回應客戶端, 但對於 RESTful 風格的架構而言, 不會把資源檔名放在 URL 中, 而是用路由器來對應 controller 與實際檔案, 例如 :
http://127.0.0.1:8080
http://127.0.0.1:8080/about
對應 controller 為 index 與 about, 伺服器需分別讀取 index.htm 與 about.htm 回應客戶端. 在下面範例中, 在伺服器程式所在目錄的 public 子目錄下新增 about.htm 與 page_not_found.htm 兩個網頁, 實作 URL 路由器如下 :
測試 14 : 利用 fs 讀取檔案並回應客戶端-實作路由器 [看原始碼]
var http=require('http');
var path=require('path');
var url=require('url');
var fs=require('fs');
http.createServer(function(req, res) {
var folder=path.join(__dirname, 'public'); //串接目錄
var url_obj=url.parse(req.url); //解析 URL
console.log(folder);
console.log(url_obj.pathname); //取得路徑名稱 (controller)
switch(url_obj.pathname) { //依據 controller 選取路由
case '/':
filepath=folder + "/index.htm";
break;
case '/about':
filepath=folder + "/about.htm";
break;
default:
filepath=folder + '/page_not_found.htm';
break;
}
fs.readFile(filepath, function(err, data) {
if (!err) {
res.writeHead(200, {'ContentType': 'text/html'});
res.end(data);
}
else {console.log(err);}
});
}).listen(8080, 'localhost');
console.log('HTTP 伺服器正在監聽 8080 埠 ...\n');
此例省略了檔案存在與否的檢查, 而且因為有 page_not_found.htm, 所以全部都用 200 狀態碼回應. 凡是沒有定義在 Switch 路由器中的 pathname (controller) 都會以 page_not_found.htm 回應.
上例的路由器做法也可擴展到有表單 (form) 提交的網頁, 例如登入表單網頁 可賦予 controller 名稱 login, 處理此路由時就直接用 fs 讀取 login.htm, 而處理程式可賦予 controller 名稱 check, 主要是核對帳號密碼. 在下面範例中, 為求簡單不進行實際檢查, 而是直接回應用戶所提交之帳號密碼. 登入表單網頁如下 :
<!doctype html>
<html>
<head>
<meta charset="UTF-8">
<title>登入</title>
</head>
<body>
<form method="POST" action="/check">
<h1>請登入</h1>
<p>請輸入帳號</p>
<input type="text" name="account">
<p>請輸入密碼</p>
<input type="text" name="password">
<p><button>登入</butto></p>
</form>
</body>
</html>
此表單中有 account 與 password 兩個欄位, 按登入鈕會向控制器 /check 以 POST 方法提交此表單. 伺服器程式修改為如下 :
測試 15 : 利用 fs 讀取檔案並回應客戶端-實作路由器 [看原始碼]
var http=require('http');
var path=require('path');
var url=require('url');
var fs=require('fs');
var qstr=require('querystring');
http.createServer(function(req, res) {
var folder=path.join(__dirname, 'public');
var url_obj=url.parse(req.url);
console.log(folder);
console.log(url_obj.pathname);
switch(url_obj.pathname) {
case '/':
filepath=folder + "/index.htm";
break;
case '/about':
filepath=folder + "/about.htm";
break;
case '/login':
filepath=folder + "/login.htm";
break;
case '/check':
if (req.method=="POST") { //處理登入表單
req.setEncoding('utf8');
var body='';
req.on('data', function(chunk) { //監聽 data 事件 (接收 body)
body += chunk;
});
req.on('end', function() {
res.writeHead(200, {'Content-Type': 'text/html'});
res.write("<html><head><title>Check</title></head><body>");
res.write("Content-Type:: " + req.headers["content-type"] + '<br>');
res.write("BODY: " + body + '<br>');
res.write("account: " + qstr.parse(body).account + '<br>');
res.write("password: " + qstr.parse(body).password + '<br>');
res.end("</body></html>");
});
}
else { //不是 POST 方法
filepath=folder + '/page_not_found.htm';
}
break;
default:
filepath=folder + '/page_not_found.htm';
break;
}
fs.readFile(filepath, function(err, data) {
if (!err) {
res.writeHead(200, {'ContentType': 'text/html'});
res.end(data);
}
else {console.log(err);}
});
}).listen(8080, 'localhost');
console.log('HTTP 伺服器正在監聽 8080 埠 ...\n');
此程式使用 querystring 模組的 parse() 方法來解析表單提交請求內容中所帶的參數 (欄位), 只要將參數字串 p1=v1&p2=v2&p3=v3 ... 傳入, 傳回值是一個物件, {p1:v1, p2:v2, p3:v3, ...}, 用鍵當索引即可取得參數值, 其介面如下 :
querystring.parse(str[, sep[, eq[, options]]])
瀏覽器網址輸入 :
http://127.0.0.1:8080/login
結果如下 :
輸入帳號密碼按登入結果如下 :
OK, 終於把 http 測完啦! 雖然一般不會用 http 模組進行開發, 但走一遍會對 HTTP 底層運作較清楚.
參考 :
# node.js伺服器實戰(4) - 內建模組與http伺服器開發的必備知識
# 基於 Node.js 的智慧家庭
# Node.js 在企業系統開發應用
# Node.js 與 Express 開發實戰 (Jollen 課程)
# Node.js 雲端技術與軟體思惟 (Jollen 的電子書)
# How to make an HTTP POST request in node.js?
# How can I get the full object in Node.js's console.log(), rather than '[Object]'?
# 淺談 HTTP Method:表單中的 GET 與 POST 有什麼差別?
# HTTP POST GET 本質區別詳解 (轉載)
沒有留言 :
張貼留言