Node.js<九>——掌握http模組並開發伺服器

語言: CN / TW / HK

Web伺服器

什麼是Web伺服器?

  • 當應用程式(客戶端)需要某一個資源時,可以向一臺伺服器,通過Http請求獲取到這個資源;提供資源的這個伺服器,就是一個Web伺服器

目前有很多開源的Web伺服器:NginxApache(靜態)、Apache Tomcat(靜態、動態)、Node.js

Web伺服器初體驗

  1. 首先需要引入http模組
  2. 該模組有一個createServer方法,可以幫助我們建立一個web伺服器,其需要傳入一個回撥函式,該回調函式會在客戶端與伺服器建立連線之後被執行
  3. 剛剛我們只是創建出了伺服器,但是還沒有去啟動它。createServer方法會返回一個例項,該例項上面有一個listen方法可以監聽指定的埠號和主機,我們可以給該方法傳入伺服器成功啟動之後執行的函式,該函式在執行的時候會被傳入兩個引數:

  4. reqrequest請求引數,包含請求相關的資訊

  5. resresponse響應物件,包含我們要響應給客戶端的資訊

  6. 如果我們更改了專案程式碼,那麼必須要重新使用node重啟一下伺服器,要不然客戶端接受到的資料就不是最新的了。所以為了我們開發的方便,可以提前全域性安裝一下nodemon這個庫,它可以幫助我們在儲存之後自動重新執行某個檔案

```js const http = require('http')

// 建立一個web伺服器 const serve = http.createServer((req, res) => { // 其實這個res繼承了我們之前學的Scream.Writable類,所以其也可以使用end方法 res.end('Hello World!') })

// 啟動伺服器,並且指定埠號和主機 serve.listen(3000, '127.0.0.1', () => { console.log('伺服器成功啟動!'); }) ```

因為res.end方法會先將資料寫入後再關閉,所以客戶端是可以接受到伺服器返回的資料的

建立伺服器的方式

  1. http.createServer可以返回給我們一個伺服器物件,通過該物件上的listen屬性就可以監聽對應的埠號,同時我們也可以在主機上監聽多個埠號

```js const http = require('http')

// 建立一個web伺服器 const serve1 = http.createServer((req, res) => { res.end('Hello serve1!') })

// 啟動伺服器,並且指定埠號和主機 serve1.listen(3000, '0.0.0.0', () => { console.log('伺服器1成功啟動!'); })

// 建立一個web伺服器 const serve2 = http.createServer((req, res) => { res.end('Hello serve2!') })

// 啟動伺服器,並且指定埠號和主機 serve2.listen(3001, '0.0.0.0', () => { console.log('伺服器2成功啟動!'); }) ```

  1. http模組裡面有一個Server類,通過new http.Server也可以去建立一個伺服器,同樣是用返回的伺服器物件中的listen方法去啟動並監聽埠號

```js const http = require('http')

// 建立一個web伺服器 const serve2 = new http.Server((req, res) => { res.end('Hello serve2!') })

// 啟動伺服器,並且指定埠號和主機 serve2.listen(3001, '127.0.0.1', () => { console.log('伺服器2成功啟動!'); }) ```

其實這兩種方法的本質上是一樣的,在原始碼中,createServer其實就是包裹了一層new Serve而已

監聽方法的使用

  • Server通過listen方法來開啟伺服器,並且在某一個主機和埠上監聽網路請求

    • 也就是當通過ip:port的方式傳送到我們監聽的Web伺服器埠上時,我們就可以對其進行相關的處理
  • listen函式有三個引數

    • 埠號port:可以不傳,系統會預設分配埠號,這個隨機分配的埠號可以在伺服器物件上的address方法返回的port屬性中查詢到,後續專案中我們會寫入到環境變數中

js // 啟動伺服器,並且指定埠號和主機 serve2.listen(() => { console.log('伺服器2成功啟動!'); console.log(serve2.address().port); // 57980 })

  • 127.0.0.1:迴環地址,表達的意思其實是我們主機自己發出去的包,直接被自己接收

  • 正常的資料包是要經過 應用層 -> 傳輸層 -> 網路層 -> 資料鏈路層 -> 物理層

  • 而環回地址,是在網路層直接就被獲取到了,是不會經過資料鏈路層和物理層的

    • 所以我們監聽127.0.0.1時,在同一個網段下的主機中,通過ip地址是不能訪問的(包括自己的主機都不能用ip地址進行訪問)

    • 0.0.0.0(預設值):

      • 監聽IPv4上的所有地址,這就包括了127.0.0.1,再根據埠號找到不同的應用程式
      • 所以我們監聽0.0.0.0時,在同一個網段下的主機中,通過ip地址是可以訪問的
  • 回撥函式:伺服器啟動成功時的回撥函式

request物件

request.url

  1. 我們可以通過req.url獲取到客戶端傳遞過來的url
  2. URL是一個全域性類,其接受兩個引數,第一個是要解析的字串,使用方法new URL(input[, base])

  3. input:要解析的絕對或相對的輸入網址。 如果 input 是相對的,則需要 base。 如果 input 是絕對的,則忽略 base。 如果 input 不是字串,則先轉換成字串

  4. base:如果 input 不是絕對的,則為要解析的基本網址。 如果 base 不是字串,則先轉換成字串
  5. 最終該函式會返回一個物件,裡面包含了url引數對應的一些資訊,包括pathnamesearchParams等等

  6. searchParams是一個特殊的物件,裡面包含了我們url中的引數資訊,但我們需要通過其get屬性去獲取

```js const http = require('http')

// 建立一個web伺服器 const serve1 = http.createServer((req, res) => { const { pathname, searchParams } = new URL(req.url, 'http://localhost:3000') console.log(pathname); const username = searchParams.get('username') const pwd = searchParams.get('pwd') console.log(username, pwd); res.end(${username} ${pwd}) })

// 啟動伺服器,並且指定埠號和主機 serve1.listen(3000, '127.0.0.1', () => { console.log('伺服器1成功啟動!'); }) ```

request.method

  1. 我們可以通過req.method獲取到客戶端傳遞過來的請求方法
  2. 請求體中的資料是通過流的形式傳遞到伺服器的。req本身是一個Stream的例項,所以其身上是有on方法去監聽請求體中的資料有沒有讀取完全的,但讀取到的資料是一個二進位制的buffer物件
  3. req物件上面有一個setEncoding方法,設定了它之後,我們在後面讀取到的資料就會自動按照指定的編碼進行解析,如果我們設定了utf-8編碼的話,讀取到的就是一個我們能夠識別的資料了
  4. 如果讀取到的是一個json字串,我們想要獲取到裡面的資料的話,就需要先利用JSON.parse方法將其轉換成一個js物件,然後才能夠拿到使用者傳遞給我們的對應的引數值

```js const http = require('http')

// 建立一個web伺服器 const serve1 = http.createServer((req, res) => { if (req.method === 'POST') { req.setEncoding('utf-8') req.on('data', data => { const { username, pwd } = JSON.parse(data) console.log(username, pwd); }) } })

// 啟動伺服器,並且指定埠號和主機 serve1.listen(3000, '127.0.0.1', () => { console.log('伺服器1成功啟動!'); }) ```

Restful規範(設計風格)中,我們對於資料的增刪查改應該通過不同的請求方式

  • GET:查詢資料
  • POST:新建資料
  • PATCH:更新部分資料
  • PUT:更新整條資料
  • DELETE:刪除資料

所以,我們可以通過判斷不同的請求方式進行不同的處理

  • 比如建立一個使用者
  • 請求介面為/users
  • 請求方式為POST請求
  • 攜帶資料usernamepassword

request.headers

request物件的header中也包含很多有用的資訊,客戶端會預設傳遞過來一些資訊

  1. content-type是這次請求攜帶資料的型別:

  2. application/json表示是一個json型別

  3. text/plain表示是文字型別
  4. application/xml表示是xml型別
  5. multipart/form-data表示是上傳檔案

  6. content-length:檔案的大小和長度,因為資料是以流的形式進行傳遞的,在服務端是用req.on('data',(data) => {})的形式讀取資料,但每次讀取的資料長度有限,我們需要根據資料長度來判斷資料有沒有讀取完全

  7. keep-alive

  8. http是基於TCP協議的,通常在進行一次請求和響應結果後會立即中斷,下次再進行通訊的時候需要重新建立連線

  9. http1.0中,如果想要繼續保持連線

    • 瀏覽器需要在請求頭中新增connection:keep-alive
    • 伺服器需要在響應頭中新增connection:keep-alive
    • 當客戶端再次發請求時,就會使用同一個連線,直到一方中斷連線
  • http1.1中,所有連線預設是connection:keeep-alive

    • 不同的Web伺服器會有不同的保持keep-alive的時間
    • Node中預設是5s
  • accept-encoding:告知伺服器,客戶端支援的檔案壓縮格式,比如js檔案可以使用gzip編碼,對應.gz檔案

  • 這個在效能優化方面是一個很重要的東西,比如我們利用react開發了一個專案,webpack打包時會對我們的專案進行醜化,但這其實只是相對來說比較小的優化,但如果我們在webpack進行了相關的配置,打包時把對應的js檔案壓縮成gzip格式,瀏覽器在接受到檔案之後在進行解壓就行,這樣就可以有效的提高傳輸速率

  • accept:告知伺服器,客戶端可接受檔案的格式型別

response物件

返回響應結果

我們通常使用res.end方法來返回響應結果:res.end('hello')相當於是做了兩部操作,首先先執行了res.write('hello'),然後再執行了一次res.end(),注意:res物件中是沒有close方法的

返回狀態碼

Http狀態碼是用來表示Http響應狀態的數字程式碼

  • Http狀態碼非常多,可以根據不同的情況,給客戶端返回不同的狀態碼

  1. 設定狀態碼方式1:直接給res物件上的statusCode屬性賦值

const serve1 = http.createServer((req, res) => { res.statusCode = 401 res.end() })

  1. 設定狀態碼方式2:通過res.writeHead方法和響應頭一起設定,但我們可以單獨只設置狀態碼

const serve1 = http.createServer((req, res) => { res.writeHead(400) res.end() })

設定響應頭

  1. 設定方式一:res.setHeaders方法可以通過傳參的方式設定我們的響應頭部,但是它一次性只能設定一個欄位

js const serve1 = http.createServer((req, res) => { res.setHeader('content-type', 'application/html') res.end() })

  1. 設定方式二:通過res.writeHead連同狀態碼一起設定,我們可以在第二個引數中以鍵值對的方式設定響應頭

js const serve1 = http.createServer((req, res) => { res.writeHead(403, { // 我們還可以指定對應的字元編碼,否則瀏覽器不知道該以哪種編碼進行解析 "content-type": 'text/html;charset=utf8' }) res.end('<h2>你好!</h2>') })

瀏覽器會根據不同的content-type去解析伺服器響應的內容,即使是html標籤也可以被識別