前端er,什麼時候,你想寫一個 HTTP 伺服器?

楊成功發表於2021-11-30

前端 er,什麼時候,你想寫一個 HTTP 伺服器?

當你第一次接觸工程化的專案時,看到專案控制檯正在 building,過一會突然跳出一個 URL 地址,你點開它居然是你剛寫好的網頁,好神奇。

當你接後端同伴的介面時,你把資料帶去,介面竟然給你返回 500 錯誤;你去找後端,後端說這樣傳不行,你不知道為啥不行,反正按照他說的改完,返回 200 成功了。

有時候你的請求莫名其妙的就跨域了,後端說讓你們自己處理,你就找呀找解決方案。但是為什麼會跨域?後端怎麼配置的,你也不清楚。

終於有一天,你痛定思痛,決定痛改前非,一定要自己搭一個 HTTP 伺服器,徹底理清這裡面的彎彎繞繞,從此拒絕被忽悠,拒絕做只聽命令的大頭兵。

但是話說回來了,怎麼入手呢?

別急,這都給您備好啦。寫 HTTP 伺服器需要後端語言,不用說,自然首選 Node.js。

下面我們基於 Node.js 的 http 模組,一起搭建一個的 HTTP 伺服器。

http 模組

一個超簡單的 HTTP web 伺服器的示例:

const http = require('http')

const server = http.createServer((request, response) => {
  response.statusCode = 200
  response.end('hello world')
})

server.listen(3000)

這裡引入了 http 模組,提供了 createServer 方法,傳入一個回撥函式,建立了一個伺服器。

現在把程式碼寫進 index.js ,再超簡單的把它執行起來:

$ node index.js

開啟瀏覽器,輸入 http://localhost:3000,就能看到網頁顯示的 hello world 了。

程式碼剖析

http.createServer 方法的引數是一個回撥函式,這個回撥函式有兩個引數 —— 它們是 HTTP 伺服器的核心。

第一個引數是請求物件 request,第二個引數是響應物件 response。你可以把它們看作兩個袋子,一個袋子裡裝著請求相關的資料,一個袋子裡裝著響應相關的操作。

request 包含了詳細的請求資料,也就是我們前端調介面傳遞過來的資料。通過它可以獲取請求頭,請求引數,請求方法等等。

response 主要用於響應相關的設定和操作。什麼是響應?就是我收到了客戶端的請求,我可以設定狀態碼為 200 並返給前端資料;或者設定狀態碼為 500 並返給前端錯誤。

總之一句話,呼叫介面返回什麼,是由 response 決定的。

事實上,createServer 返回的是一個 EventEmitter,因此上面的寫法等同於這樣:

const http = require('http')
const server = http.createServer()

server.on('request', (request, response) => {
  response.statusCode = 200
  response.end('hello world')
}).listen(3000)

request 解析

使用者發起請求的相關資料,都包含在 request 物件中。

這些資料包含常用的請求方法,請求頭,url,請求體等等資料。

const { method, url, headers } = request

method 表示請求方法可直接使用,headers 返回請求頭物件,使用也比較簡便:

const { headers } = request
const userAgent = headers['user-agent'] // 請求頭全是小寫字母

唯獨 url 字串不好解析,裡面包含了協議,hostname,path,query 等等。

所幸 Node.js 提供了 urlquerystring 兩個模組解析 url 字串。

URL 解析

先看一個 url 模組的例子:

const url = require('url') // 解析url字串
var string = 'http://localhost:8888/start?foo=bar&hello=world'

var url_object = url.parse(string)
// { protocol: 'http:', host:'localhost:8888', pathname: '/start', query: 'foo=bar&hello=world' }

看到了吧,url 模組可以將一個完整的 URL 地址字串,拆分成一個包含各部分屬性的物件。

但是美中不足,其他部分都解析出來了,唯獨 query 還是一個字串。

query 需要二次解析。怎麼辦呢?這時候第二個模組 querystring 出場了:

const querystring = require('querystring') // 解析query字串
var string = 'http://localhost:8888/start?foo=bar&hello=world'

var url_object = url.parse(string) // { query: 'foo=bar&hello=world' }
var query_object = querystring.parse(url_object.query)
// { foo: 'bar', hello: 'world' }

這下就完美了。用 url + querystring 組合,可以完整解析你的 URL。

請求體解析

對於 POST 或者 PUT 請求,我們需要接收請求體的資料。

這裡請求體比較特殊,它不是一次性傳過來的資料,而是通過 Stream 流的方式流式傳遞來的,因此要通過監聽 dataend 事件一點點的接收。

獲取方法如下:

server.on('request', (request, response) => {
  let body = []
  request.on('data', chunk => {
    // 這裡的 chunk 是一個 Buffer
    body.push(chunk)
  })
  request.on('end', () => {
    body = Buffer.concat(body)
  })
  console.log(body.toString())
})

response 設定

伺服器收到客戶端請求,要通過 response 設定如何響應給客戶端。

響應設定,主要就是狀態碼,響應頭,響應體三部分。

首先是狀態碼,比如 404:

response.statusCode = 404

再有是響應頭

response.setHeader('Content-Type', 'text/plain')

最後是響應體

response.end('找不到資料')

這三部分也可以合在一起:

response
  .writeHead(404, {
    'Content-Type': 'text/plain',
    'Content-Length': 49
  })
  .end('找不到資料')

傳送 http 請求

http 模組除了接受客戶端的請求,還可以作為客戶端去傳送請求。

傳送 http 請求是指,在 Node.js 中請求其他介面獲取資料。

傳送請求主要通過 http.request 方法來實現。

GET

下面是一個傳送 GET 請求的簡單示例:

const http = require('http')
const options = {
  hostname: 'nodejs.cn',
  port: 80,
  path: '/learn',
  method: 'GET'
}

const req = http.request(options, res => {
  console.log(`狀態碼: ${res.statusCode}`)
  res.on('data', d => {
    process.stdout.write(d)
  })
  res.on('end', () => {})
})

req.on('error', error => {
  console.error(error)
})

req.end()

使用 http.request 傳送請求後,必須顯示呼叫 req.end() 來表示完成請求傳送。

POST

與上面 GET 請求基本一致,區別是看請求體怎麼傳

const http = require('http')
const options = {
  hostname: 'nodejs.cn',
  port: 80,
  path: '/learn',
  method: 'POST'
}

const body = {
  sex: 'man',
  name: 'ruims'
}

const req = http.request(options, res => {
  console.log(`狀態碼: ${res.statusCode}`)
  res.on('data', d => {
    process.stdout.write(d)
  })
  res.on('end', () => {})
})

req.on('error', error => {
  console.error(error)
})

req.write(JSON.stringify(body)) // 傳遞 body 引數寫法

req.end()

詭異之處

看到這裡,如果你對 nodejs 理解不深,可能會發現幾處詭異的地方。

比如,正常情況下 POST 請求傳遞 body 引數可能是這樣的:

var body = { desc: '請求體引數' }
var req = http.request({
  path: '/',
  method: 'POST',
  data: body
})

而上面說到的正確姿勢是這樣的:

var body = { desc: '請求體引數' }
var req = http.request({
  path: '/',
  method: 'POST'
})
req.write(JSON.stringify(body))

還有上面獲取請求體也是如此。不能直接通過 request.body 獲取,非得這樣:

let body = []
request.on('data', chunk => {
  body.push(chunk)
})
request.on('end', () => {
  body = Buffer.concat(body)
})

這幾處應該是大家理解 http 模組最困惑的地方。其實刨根問底,這不屬於 http 的難點,而是 Node.js 中 Stream 流的特有語法。

事實上,http 模組的核心 ——— requestresponse 都屬於 Stream,一個是可讀流,一個是可寫流。

因此,徹底理解 http 模組,還需要深入瞭解 Stream 流的相關知識。

總結

本篇基於最基礎的 http 模組搭建了簡單的 HTTP 伺服器,並且實現了簡單的接收請求傳送請求

不過呢,真正的應用場景一般不會這麼搭。社群有成熟穩定的 express 框架更適合寫 Node.js 服務;傳送請求,可以用我們最熟悉的 axios ——— 沒錯,axios 也可以在 Node.js 中使用。

但是你可能不知道,express 和 axios 的核心功能,都是基於 http 模組。

因此,基礎很重要。地基不牢,地動山搖。掌握了 http 模組,即使你在 express 中見到 Stream 的用法,也不至於不明所以。

這篇就到這裡,下一篇我們繼續探索 Stream 流,記得關注我哦。

往期精彩

專欄會長期輸出前端工程與架構方向的文章,已釋出如下:

如果喜歡我的文章,請點贊支援我吧!也歡迎關注我的專欄。

宣告: 本文原創,如有轉載需求,請加微信 ruidoc 聯絡授權。

相關文章