里程碑?NodeJS實現 HTTP服務器(實現簡易爬蟲)

前言

在 NodeJS 中用來建立服務的模塊是 http 核心模塊,本篇就來介紹關於使用 http 模塊搭建 HTTP 服務器和客戶端的方法,以及模塊的基本 API。html

HTTP 服務器

一、建立 HTTP 服務器

在 NodeJS 中,建立 HTTP 服務器能夠與 net 模塊建立 TCP 服務器對比,建立服務器有也兩種方式。瀏覽器

方式 1:bash

1
2
3
4
5
6
7
複製代碼
const http = require("http");

const server = http.createServer(function(req, res) {
    // ......
});

server.listen(3000);
複製代碼

方式 2:服務器

1
2
3
4
5
6
7
8
9
複製代碼
const http = require("http");

const server = http.createServer();

server.on("request", function(req, res) {
    // ......
});

server.listen(3000);
複製代碼

createServer 的回調和 request 事件的回調函數中有兩個參數,req(請求)、res(響應),基於 socket,這兩個對象都是 Duplex 類型的可讀可寫流。併發

http 模塊是基於 net 模塊實現的,因此 net 模塊原有的事件在 http 中依然存在。socket

1
2
3
4
5
6
7
8
9
10
複製代碼
const http = require("http");

const server = http.createServer();

// net 模塊事件
server.on("connection", function(socket) {
    console.log("鏈接成功");
});

server.listen(3000);
複製代碼

二、獲取請求信息

在請求對象 req 中存在請求的方法、請求的 url(包含參數,即查詢字符串)、當前的 HTTP 協議版本和請求頭等信息。函數

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
複製代碼
const http = require("http");

const server = http.createServer();

server.on("request", function(req, res) {
    console.log(req.method); // 獲取請求方法
    console.log(req.url); // 獲取請求路徑(包含查詢字符串)
    console.log(req.httpVersion); // 獲取 HTTP 協議版本
    console.log(req.headers); // 獲取請求頭(對象)

    // 獲取請求體的內容
    let arr = [];

    req.on("data", function(data) {
        arr.push(data);
    });

    req.on("end", function() {
        console.log(Buffer.concat(arr).toString());
    });
});

server.listen(3000, function() {
    console.log("server start 3000");
});
複製代碼

經過 req 對應的屬性能夠拿到請求行和請求首部的信息,請求體內的內容經過流操做來獲取,其中 url 中存在多個有用的參數,咱們本身處理會很麻煩,能夠經過 NodeJS 的核心模塊 url 進行解析。ui

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
複製代碼
const url = require("url");
let str = "http://user:pass@www.pandashen.com:8080/src/index.html?a=1&b=2#hash";

// parse 方法幫助咱們解析 url 路徑
let obj = url.parse(str, true);

console.log(obj);

// {
//     protocol: 'http:',
//     slashes: true,
//     auth: 'user:pas',
//     host: 'www.pandashen.com:8080',
//     port: '8080',
//     hostname: 'www.pandashen.com',
//     hash: '#hash',
//     search: '?a=1&b=2',
//     query: '{ a: '1', b: '2' }',
//     pathname: '/src/index.html'
//     path: '/src/index.html?a=1&b=2',
//     href: 'http://user:pass@www.pandashen.com:8080/src/index.html?a=1&b=2#hash' }
複製代碼

在被解析路徑返回的對象中有幾個屬性被常常使用:編碼

  • host:主機(域名 + 端口號);
  • hostname:主機名;
  • query:請求參數(查詢字符串或參數對象);
  • pathname:資源路徑(根據不一樣的路徑返回不一樣的資源)。

咱們使用 urlparse 方法來幫咱們解析請求路徑,在真實的服務器中傳入的第一個參數爲 req.url,第二個參數不傳時,query 會被解析成 a=1&b=2 的形式,第二個參數傳入 truequery 屬性的查詢字符串會被解析成對象的形式。url

url 模塊中,將查詢字符串 a=1&b=2 轉換爲對象 { a: '1', b: '2' } 的實現方式實際上是使用正則替換實現的。

模擬查詢字符串轉換對象的核心邏輯:

1
2
3
4
5
6
7
8
複製代碼
let str = "a=1&b=2&c=3";
let obj = {};

str.replace(/([^=&]+)=([^=&]+)/g, function() {
    obj[arguments[1]] = arguments[2];
});

console.log(obj); // { a: '1', b: '2', c: '3' }
複製代碼

在上面代碼的 replace 方法的回調函數中參數集合的第一項爲匹配到的字符串,第二項爲第一個分組的值,第三項爲第二個分組的值,依次類推,倒數第二項爲分組匹配的索引,最後一項爲原字符串。

三、設置響應信息

咱們能夠經過 req 來獲取請求信息,天然也能夠經過 res 來設置響應信息返回給客戶端。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
複製代碼
const http = require("http");

const server = http.createServer();

server.on("request", function(req, res) {
    // 設置響應頭(過去的用法),不能屢次調用,見到要認識
    res.writeHead(200, { "Content-Type": "text", a: "hello world" });

    // 設置響應頭(如今的用法,經常使用),能夠屢次調用,每次設置一個響應頭
    res.setHeader("Content-Type", "text");

    // 設置狀態碼,不設置默認爲 200
    res.statusCode = 200;

    // 不發送 Date(日期)響應頭
    res.sendDate = false;

    // 返回內容
    res.write("hello world"); // 不會關閉鏈接
    res.end("hello world"); // 將內容返回後關閉鏈接
});

server.listen(3000, function() {
    console.log("server start 3000");
});
複製代碼

返回給客戶端的信息主要分爲兩部分,分別爲響應頭和返回給瀏覽器的內容,在不設置響應頭的狀況下,默認會設置響應頭 Content-LengthDate ,表明當前返回給客戶端的內容長度和日期。

返回給瀏覽器的內容能夠經過 reswrite 方法和 end 方法進行發送,write 方法不會斷開鏈接(一般在響應後須要斷開與客戶端的鏈接),end 方法會斷開鏈接,在 end 方法存在參數時,會在內部調用 write 將參數內容返回給客戶端,並斷開鏈接。

HTTP 客戶端

net 模塊中能夠經過 net.createConnection 來建立客戶端,併發送請求到服務端,在 http 模塊一樣能夠建立客戶端,並向 http 服務器發送請求。

客戶端:client.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
複製代碼
const http = require("http");

// 發送請求的配置
let config = {
    host: "localhost",
    port: 3000,
    method: "get",
    headers: {
        a: 1
    }
};

// 建立客戶端
let client = http.request(config, function(res) {
    // 接收服務端返回的數據
    let arr = [];

    res.on("data", function(data) {
        arr.push(data);
    });

    res.on("end", function() {
        console.log(Buffer.concat(arr).toString());
    });
});

// 發送請求
client.end();
複製代碼

http 模塊中經過 request 方法建立客戶端,該方法第一個參數爲發送請求的配置,包含請求地址、端口號、請求方法以及請求頭等,第二個參數爲回調函數,在請求被響應後執行,回調函數的參數爲服務器的響應對象 res,建立的客戶端經過 end 方法將請求發出與服務端進行通訊。

使用 NodeJS 實現的 「爬蟲」 其實就能夠經過 http 模塊建立的客戶端來實現,客戶端幫咱們向咱們要抓取數據的地址發送請求,並拿到響應的數據進行解析。

同時使用 HTTP 客戶端和服務器

咱們使用本身建立的客戶端訪問本身的服務端,並體會請求響應的過程,就是用上面 client.js 做爲客戶端,啓動 server.js 後再啓動 client.js 查看效果。

服務器:server.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
複製代碼
const http = require("http");

http.createServer(function(req, res) {
    console.log("The request came");

    // 獲取客戶端請求信息
    console.log(req.method);
    console.log(req.headers);

    // 返回數據
    res.write("hello world");
}).listen(3000, function() {
    console.log("server start 3000");
});
複製代碼

簡易爬蟲

咱們結合 http 模塊建立的服務端和客戶端實現一個簡易版的 「爬蟲」 去抓取百度新聞頁全部 li 標籤內的文章標題。

簡易爬蟲:crawl.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
複製代碼
const http = require("http");

// 建立服務器
const server = http.createServer();

// 監聽請求
server.on("request", function(req, res) {
    let client = http.request(
        {
            host: "news.baidu.com",
            method: "get",
            port: 80
        },
        function(r) {
            // 接收百度新聞返回的數據
            let arr = [];

            r.on("data", function(data) {
                arr.push(data);
            });

            r.on("end", function() {
                // 處理數據
                let result = Buffer.concat(arr).toString();
                let matches = result.match(/<li class="bold-item">([\s\S*?])<\/li>/gm);

                // 設置返回給瀏覽器的文檔類型和編碼格式
                res.setHeader("Content-Type", "text/html;charset=utf8");

                // 響應瀏覽器
                res.end(matches.join(""));
            });
        }
    );

    client.end();
});

server.listen(3000);
複製代碼

上面的正則匹配中 ([\s\S*?]) 表明匹配 <li class="bold-item"><\/li> 之間全部內容(多個字符、非貪婪模式),gm 表明全局並多行匹配。

上面爬取百度新聞數據的過程當中,咱們本身的 Node 服務器扮演了一個 「中間層」 的角色,咱們經過瀏覽器訪問本身的服務器 localhost:3000 觸發 request 事件,執行了回調,在回調中建立客戶端向 news.baidu.com 發送了請求,並在客戶端的回調中處理了響應(百度新聞頁返回的數據),將處理後的內容經過咱們本身 Node 服務器的 res 對象返回給了瀏覽器。

總結

相信在讀過本篇文章以後對搭建一個 Node 服務應該已經有了思路,爲將來經過 Node 服務實現複雜的業務場景及數據的處理打下了一個基礎,但願初學 Node 的小夥伴在看了這篇文章後能有所收穫。

原文出自:https://www.pandashen.com

相關文章
相關標籤/搜索