文章目錄
- 一、什麼是Web伺服器?
-
- 1.1 web伺服器初體驗
- 1.2 另外一種建立方式
- 1.3 listen方法的參數詳解
- 二、request對象解析
-
- 2.1 基本使用
- 2.2 進階使用
- 2.3 method 應用
- 2.4 headers 屬性
-
- 2.4.1 content-type
- 2.4.2 content-length
- 2.4.3 keep-alive
- 2.4.4 accept-encoding
- 2.4.5 accept
- 2.4.6 user-agent
- 三、response 對象解析
-
- 3.1 設定響應體
- 3.2 設定響應狀态碼
- 3.3 設定響應頭
- 四、發送網絡請求
-
- 4.1 http發送GET請求
- 4.2 http發送POST請求
- 五、使用原生http子產品實作檔案上傳
- 六、進階express
-
- 6.1 解析query 和 params
- 6.2 十分好用的工具中間件推薦
一、什麼是Web伺服器?
當應用程式(用戶端)需要某一個資源時,可以向一台伺服器,通過Http請求擷取到這個資源;提供資源的這個伺服器,就是一個Web伺服器。
目前有很多開源的Web伺服器:Nginx、Apache(靜态)、Apache Tomcat(靜态、動态)、Node.js
1.1 web伺服器初體驗
const http = require('http')
// 1.建立一個web伺服器
const server = http.createServer((req, res) => {
// res.write('heelo world)
// res.close() 下面方法是上述兩個方法的簡寫
res.end('hello world')
})
// 2.啟動伺服器 并指定端口号和主機
// port:0-65535,
// hostname:主機 ['localhost','127.0.0.1','0.0.0.0']
server.listen(8080,'0.0.0.0',()=>{
console.log('server start');
})
1.2 另外一種建立方式
const http = require('http')
const server2 = new http.Server((req, res) => {
res.end('server 2')
})
server2.listen(8002,()=>{
console.log('server2啟動成功');
})
本質上是同一種,源碼層面都是去
new
了
Server
這個類。
Node源碼:
1.3 listen方法的參數詳解
官網直達:http://nodejs.cn/api/net.html#net_server_listen_port_host_backlog_callback
listen函數有三個參數:
- 端口port: 可以不傳, 系統會預設配置設定端, 後續項目中我們會寫入到環境變量中;
- 如果 port 省略或是 0,系統會随意配置設定
- 主機host: 通常可以傳入
、ip位址localhost
、或者ip位址127.0.0.1
,預設是0.0.0.0;0.0.0.0
- localhost:本質上是一個域名,通常情況下會被解析成127.0.0.1;
- 127.0.0.1:回環位址(Loop Back Address),表達的意思其實是我們主機自己發出去的包,直接被自己接收;
- 正常的資料庫包經常 應用層 - 傳輸層 - 網絡層 - 資料鍊路層 - 實體層 ;
- 而回環位址,是在網絡層直接就被擷取到了,是不會經常資料鍊路層和實體層的;
- 比如我們監聽 127.0.0.1時,在同一個網段下的主機中,通過ip位址是不能通路的;
- 0.0.0.0:
- 監聽IPV4上所有的位址,再根據端口找到不同的應用程式;
- 比如我們監聽 0.0.0.0時,在同一個網段下的主機中,通過ip位址是可以通路的;
- 回調函數:伺服器啟動成功時的回調函數
const http = require('http')
const server = new http.Server((req, res) => {
res.end('hello world!')
})
// port:不寫動态生成,const result = server.address().port 可以拿到
// address:不寫預設是 0.0.0.0
server.listen(8000,() => {
console.log('伺服器啟動成功');
// 拿到伺服器資訊
const result = server.address()
console.log('result: ', result);
})
二、request對象解析
2.1 基本使用
request 對象中常用的有三個屬性,分别是:
req.url
req.method
req.headers
const http = require('http')
const server = http.Server((req,res)=>{
// request對象中封裝了用戶端給我們伺服器傳遞過來的所有資訊
console.log('req.url: ', req.url);
console.log('req.method: ', req.method);
console.log('req.headers: ', req.headers);
res.end('hello world!')
})
server.listen(8000,()=>{
console.log('server start');
})
output:
server start
req.url: /login?username=sir&password=123
req.method: POST
req.headers: {
'user-agent': 'PostmanRuntime/7.26.10',
accept: '*/*',
host: 'localhost:8000',
'accept-encoding': 'gzip, deflate, br',
connection: 'keep-alive',
cookie: 'NMTID=00OtVDuIBEAhUMivkXAgeFb15wM0PUAAAF4kDtcOQ',
'content-length': '0'
}
2.2 進階使用
可以發現上述使用方法在處理我們的請求參數時不是很友善,node為我們提供了一些内置庫,幫助我們提取出url和query資訊。
const http = require('http')
const url = require('url')
const qs = require('querystring')
const server = http.Server((req, res) => {
// 1.基本使用
// if(req.url === '/login'){
// res.end('歡飲回來')
// }else if(req.url === '/users'){
// res.end('使用者清單~')
// }else{
// res.end('錯誤請求,請檢查')
// }
console.log(req.url);
// 2.進階方式使用
// 通過内置的庫 url 解析出我們需要的 包含在url裡的資訊
const ret = url.parse(req.url)
console.log('ret: ', ret);
const { pathname, query } = url.parse(req.url)
if (pathname === '/login') {
console.log('query: ', query);
// 通過内置庫 querystring 解析出query裡我們需要的資料
// query => username=sir&password=123
const { username, password } = qs.parse(query)
console.log('username: ', username);
console.log('password: ', password);
}
res.end('登入成功!')
})
server.listen(8000, () => {
console.log('server start');
})
output:
server start
/login?username=sir&password=123
ret: Url {
protocol: null,
slashes: null,
auth: null,
host: null,
port: null,
hostname: null,
hash: null,
search: '?username=sir&password=123',
query: 'username=sir&password=123',
pathname: '/login',
path: '/login?username=sir&password=123',
href: '/login?username=sir&password=123'
}
query: username=sir&password=123
username: sir
password: 123
2.3 method 應用
const http = require('http')
const url = require('url')
const server = http.Server((req, res) => {
const { pathname } = url.parse(req.url)
if (pathname === '/login') {
// 拿到body中的資料
// 1. 拿到的是字元串(手動設定編碼)
req.setEncoding('utf-8')
req.on('data', (data) => {
console.log('data: ', data);
console.log('typeof data: ', typeof data); // string
// 2. 拿到的是 Buffer 調用 toString解碼
// console.log('data: ', data.toString());
const { username, password } = JSON.parse(data)
console.log('username: ', username);
console.log('password: ', password);
})
console.log('req.method: ', req.method);
res.end('hello server')
}
})
server.listen(8000, () => {
console.log('server start');
})
output:
server start
req.method: POST
data: {
"username":"foo",
"password":"8888"
}
typeof data: string
username: foo
password: 8888
2.4 headers 屬性
在request對象的header中也包含很多有用的資訊,用戶端會預設傳遞過來一些資訊:
詞彙:accept 接受,encoding 編碼,content 内容,keep-alive 持續服務
2.4.1 content-type
content-type是這次請求攜帶的資料的類型:
- application/json表示是一個json類型;
- text/plain表示是文本類型;
- application/xml表示是xml類型;
- multipart/form-data表示是上傳檔案;
- text/html 表示伺服器傳回的是 html文檔。
2.4.2 content-length
content-length:檔案的大小和長度
2.4.3 keep-alive
keep-alive:
- http是基于TCP協定的,但是通常在進行一次請求和響應結束後會立刻中斷;
- 在http1.0中,如果想要繼續保持連接配接:
- 浏覽器需要在請求頭中添加 connection: keep-alive;
- 伺服器需要在響應頭中添加 connection:keey-alive;
- 當用戶端再次放請求時,就會使用同一個連接配接,直至一方中斷連接配接;
- 在http1.1中,所有連接配接預設是 connection: keep-alive的;
- 不同的Web伺服器會有不同的保持 keep-alive的時間;
- Node中預設是5s
2.4.4 accept-encoding
accept-encoding:告知伺服器,用戶端支援的檔案壓縮格式,比如js檔案可以使用gzip編碼,對應 .gz檔案;
2.4.5 accept
accept:告知伺服器,用戶端可接受檔案的格式類型;
2.4.6 user-agent
user-agent:用戶端相關的資訊;
三、response 對象解析
3.1 設定響應體
如果我們希望給用戶端響應結果資料,可以通過兩種方式:
- write方法:這種方式是直接寫出資料,但是并沒有關閉流;
- end方法:這種方式是寫出最後的資料,并且寫出後會關閉流;
如果我們沒有調用 end,用戶端将會一直等待結果:
是以用戶端在發送網絡請求時,都會設定逾時時間。
3.2 設定響應狀态碼
設定狀态碼常見的有兩種方式:
res.statusCode = 200
res.writeHead(200)
3.3 設定響應頭
-
res.setHeader("key","value")
-
res.writeHead({"key":"value"})
示例:
const http = require('http')
// 1.建立一個web伺服器
const server = http.createServer((req, res) => {
// 設定響應頭
// 1.setHeader
// res.setHeader('Content-Type','text/plain;charset=utf8')
// res.setHeader('Content-Type','text/html;charset=utf8')
// 2.writeHead
res.writeHead(200, {
// "Content-Type": "application/json;charset=utf8",
"Content-Type": "text/html;charset=utf8",
})
res.end('<h1>Hello,world!</h1>')
})
// 2.啟動伺服器 并指定端口号和主機
server.listen(8000, '0.0.0.0', () => {
console.log('server start');
})
四、發送網絡請求
4.1 http發送GET請求
const http = require('http')
// http發送get請求
http.get('http://localhost:8000', (res) => {
res.setEncoding('utf-8')
res.on('data', (data) => {
// 預設是 Buffer
// console.log(data.toString());
console.log(data);
})
res.on('end', () => {
console.log('傳輸完成');
})
})
4.2 http發送POST請求
const http = require('http')
// http發送post請求
const request = http.request({
method: 'POST',
hostname: 'localhost',
port: '8000'
}, res => {
res.setEncoding('utf-8')
res.on('data', (data) => {
// 預設是 Buffer
// console.log(data.toString());
console.log(data);
})
res.on('end', () => {
console.log('傳輸完成');
})
})
// 需要調用該方法表示 POST請求已經配置完成,可以發送了
request.end()
五、使用原生http子產品實作檔案上傳
準備:
- 準備
發送一個post請求,請求體是一個postman
類型的表單資料,檔案類型選擇file,上傳一張圖檔。form-data
注意: 不能直接把拿到的資料,寫入到檔案中,因為拿到的資料包含請求頭,檔案名稱大小等資料,不是純粹的圖檔二進制資料, 我需要我們人工拆解出真實的二進制圖檔資料。
下圖中圈出來的才是圖檔的真實 二進制資料。
server.js
const http = require('http')
const url = require('url')
const fs = require('fs')
const qs = require('querystring')
const server = http.createServer((req, res) => {
// 解析該url,解構出我們需要的資料
const { pathname } = url.parse(req.url)
if (pathname === '/upload') {
if (req.method === 'POST') {
// 預設是utf8編碼,必須要先設定編碼 圖檔資料是二進制編碼
req.setEncoding('binary')
let body = ''
const boundary = req.headers['content-type'].split(';')[1].replace(' boundary=', '')
console.log('boundary: ', boundary);
req.on('data', (data) => {
// data是Buffer 相加的時候會自動轉化成字元串再相加
body += data
// console.log(data);
})
req.on('end', () => {
// console.log('body: ', body);
// 處理資料
// 1.擷取image/png的位置
const payload = qs.parse(body, '\r\n', ': ')
console.log(payload['Content-Type']);
const type = payload['Content-Type']
// 2.開始再 image/png的位置進行截取
const typeIndex = body.indexOf(type)
const typeLength = type.length
let imageData = body.substring(typeIndex + typeLength)
// 3.将中間的兩個空格去掉
imageData = imageData.replace(/^\s\s*/, '')
// 4.将最後的 boundary去除掉
imageData = imageData.substring(0, imageData.indexOf(`--${boundary}--`))
// 5.寫入到檔案
fs.writeFile('./bar.png', imageData, { encoding: 'binary' }, (err) => {
console.log(err);
console.log('檔案上傳成功!');
res.end('檔案上傳成功!')
})
})
} else {
res.statusCode = '403'
res.end('請求方法不正确')
}
} else {
res.end('ok')
}
})
server.listen(8000, () => {
console.log('server start');
})
六、進階express
6.1 解析query 和 params
假如現在我們将接收如下兩個請求,該怎麼拿到附帶在請求中的資料呢?
// params方式
http://localhost:3000/products/123/why
// query方式
http://localhost:3000/login?username=why&pasword=123
其實在
express
中非常簡單,
req.params
可以拿到第一種方式的請求資料,
req.query
可以拿到第二種。
示例:
const express = require('express')
const app = express()
// params方式
// 請求:http://localhost:3000/products/123/why
app.get('/products/:id/:name', (req, res, next) => {
console.log('req.params: ', req.params);
// req.params: { id: '123', name: 'why' }
res.send('商品的詳情資料!')
})
// query方式
// 請求 http://localhost:3000/login?username=why&pasword=123
app.get('/login', (req, res, next) => {
console.log('req.query: ', req.query);
// req.query: { username: 'why', pasword: '123' }
console.log("登入成功!");
res.end('登入成功!')
})
app.listen(3000, () => console.log(`Example app listening on 3000 port!`))
6.2 十分好用的工具中間件推薦
本章主要講解以下内容:自動解析,自動解析
application/json
,自動解析
x-www-form-urlencoded
form-data
假如現在前端給我們發送了一個
application/json
類型的資料,如下:
{
"username":"test",
"password":"12346"
}
在
express
中我們如何拿到該資料呢?
開始測試:
Postman:
http://localhost:8080/login
普通做法:
const express = require('express')
const app = express()
app.post('/login', (req, res, next) => {
req.on('data', data => {
const result = JSON.parse(data.toString())
console.log('result: ', result);
// result: { username: 'test', password: '12346' }
})
req.on('end', () => {
res.end('welcome back')
})
})
app.listen(8080, () => {
console.log('server start');
})
進階做法:
const express = require('express')
const app = express()
// 隻解析 application/josn 成對象形式資料,儲存到 req.body 中
app.use((req, res, next) => {
if (req.headers["content-type"] === 'application/json') {
req.on('data', data => {
const info = JSON.parse(data.toString())
req.body = info
})
req.on('end', () => {
next()
})
} else {
next()
}
})
app.post('/login', (req, res, next) => {
console.log('req.body: ', req.body);
// req.body: { username: 'test', password: '12346' }
res.end('welcome back')
})
app.listen(8080, () => {
console.log('server start');
})
可以發現在進階做法中,我們注冊了一個全局的中間件,并且它實作了隻監測我們的
application/json
資料的處理,解析出來的資料放到了
req.body
中。
這種方式十分的巧妙且靈活。
但是每次新項目都需要寫這樣寫一短代碼是不是有點麻煩呢?于是誕生了
body-parser
,它就是專門幹這個的。
用法示例:
const express = require('express')
const app = express()
// 1. 公用中間件 用于處了解析出我們需要的資料
// 解析 application/josn 成對象形式資料
// app.use((req, res, next) => {
// if (req.headers["content-type"] === 'application/json') {
// req.on('data', data => {
// const info = JSON.parse(data.toString())
// req.body = info
// })
// req.on('end', () => {
// next()
// })
// } else {
// next()
// }
// })
// 有一個庫專門幹這個:body-parser
// body-parser:express 3.x 内置于express架構
// body-parser:express 4.x 被分離出去
// body-parser類似功能:express 4.16.x 内置成函數
// 2. 目前express版本 4.17.1,直接使用内置的 body-parser 解析我們需要的資料
// 解析 application/json
app.use(express.json())
// 解析 x-www-form-urlencoded
// extended:true:那麼對urlencoded進行解析時,它使用的是第三方庫,qs
// extended:false:那麼對urlencoded進行解析時,使用的是Node内置的querystring
app.use(express.urlencoded({ extended: true }))
//不加{ extended: true }抛出警告:不推薦使用body解析器
app.post('/login', (req, res, next) => {
console.log('req.body: ', req.body);
res.end('welcome back')
})
app.post('/products', (req, res, next) => {
console.log('req.body: ', req.body);
res.end('upload products info success')
})
app.listen(8080, () => {
console.log('server start');
})
此外,還有一個庫
multer
,專門解析
form-data
。
用法示例:解析普通資料(非file)
const express = require('express')
const multer = require('multer')
const app = express()
const upload = multer()
// 解析 application/json
app.use(express.json())
// 解析 x-www-form-urlencoded
app.use(express.urlencoded({ extended: true }))
// 解析 form-data
// upload.any() 會傳回一個函數,直接把他注冊為中間件,他會幫我們完成解析
app.post('/login', upload.any(),(req, res, next) => {
console.log("使用者登入成功");
res.end("使用者登入成功")
console.log('req.body: ', req.body);
})
app.listen(8080, () => {
console.log('form-data解析伺服器啟動成功~');
})
解析上傳的檔案:
const path = require('path')
const express = require('express')
const multer = require('multer')
const app = express()
// 解析 application/json
app.use(express.json())
// 解析 x-www-form-urlencoded
app.use(express.urlencoded({ extended: true }))
// 解析 form-data
// 構造檔案上傳的配置對象
const storage = multer.diskStorage({
// destination 目的地
destination: (req, file, cb) => {
cb(null, './uploads/')
},
filename: (req, file, cb) => {
// file.originalname 能拿到原始檔案的名稱
cb(null, Date.now() + path.extname(file.originalname))
}
})
const upload = multer({
// storage 表示自定義上傳的檔案資訊,如檔案名稱、字尾名等
storage
})
// upload.any() 會傳回一個函數,直接把他注冊為中間件,他會幫我們解析
app.post('/login', upload.any(), (req, res, next) => {
console.log("使用者登入成功");
res.end("使用者登入成功")
console.log('req.body: ', req.body);
})
// form-data 上傳檔案的中間價
// 該中間件:擷取這次上傳的檔案,并且進行儲存
// 單個檔案upload.single('key') 多個:upload.array
app.post('/upload', upload.array('file'), (req, res, next) => {
console.log('req.body: ', req.body);
// 拿到儲存的files
console.log('req.files: ', req.files);
res.end('檔案上傳成功!')
})
app.listen(8080, () => {
console.log('form-data解析伺服器啟動成功~');
})