天天看點

[NodeJS]建立HTTP、HTTPS伺服器與用戶端

超文本傳輸協定(HTTP,HyperText Transfer Protocol)是網際網路上應用最為廣泛的一種網絡協定。所有的WWW檔案都必須遵守這個标準。設計HTTP最初的目的是為了提供一種釋出和接收HTML頁面的方法。其屬于下圖七層網路協定的“應用層”。
[NodeJS]建立HTTP、HTTPS伺服器與用戶端

HTTP伺服器

建立HTTP伺服器

建立服務

方式一:回調方式

var server = http.createServer((request, response) => {
    // 接受用戶端請求時觸發
    ...
});

server.listen(10000, 'localhost', 511, () => {
   // 開始監聽
   ...
});           

複制

方式二:事件監聽方式

var server = http.createServer();
// 接受用戶端請求時觸發
server.on('request', (request, rsponse) => {
    ...
});

server.listen(10000, 'localhost', 511);
// 開始監聽
server.on('listening', () => {
    ...
});           

複制

注意:

  • server.listen(port, [host], [backlog], [callback])

    中的backlog參數為整數,指定位于等待隊列中用戶端連接配接的最大數量,一旦超過這個長度,HTTP伺服器将開始拒絕來自新用戶端的連接配接,預設值為511。
  • 在HTTP請求伺服器時,會發送兩次請求。一次是使用者送出請求,另一次是浏覽器為頁面在收藏夾中的顯示圖示(預設為favicon.ico)而自動發出的請求。

關閉伺服器

server.close();
// 伺服器關閉時會觸發close事件
server.on('close', () => {...});           

複制

逾時

server.setTimeout(60 * 1000, () => {
   console.log('逾時了');
});
// 或者通過事件形式
server.setTimeout(60 * 1000);
server.on('timeout', () => {...});           

複制

注意:預設逾時時間為2分鐘

錯誤

server.on('error', (e) => {
    if(e.code === 'EADDRINUSE') {
        // 端口被占用
    }
});           

複制

擷取用戶端請求資訊

當從用戶端請求流中讀取到資料時會觸發data事件,當讀取完用戶端請求流中的資料時觸發end事件。

請求對象的屬性 說明
method 請求的方法Get、Post、Put、Delete
url 用戶端發送請求時使用的URL參數字元串;通常用來判斷請求頁面
headers 請求頭對象
httpVersion HTTP1.0或者HTTP1.1
trailers 用戶端發送的trailers對象
socket 伺服器用于監聽用戶端請求的socket對象

Get請求

server.on('request', (request, response) => {
    if(request.url !== '/favicon.ico') {
        /* Get請求 */
        var params = url.parse(req.url, true).query;
        // 或者
        // var params = querystring.parse(url.parse(request.url).query);
        // 根據參數做處理
        // ...
        // 結束請求
        response.end();
    }  
});           

複制

Post請求

server.on('request', (request, response) => {
    request.setEncoding('utf-8');
    if(request.url !== '/favicon.ico') {
        let result = '';
        request.on('data', (data) => {
            result += data;
        });
        request.on('end', () => {
            var params = JSON.parse(postData);
            console.log(`資料接收完畢:${result}`);
        });
        // 結束本次請求
        response.end();
    }
    // 結束本次請求
    response.end(JSON.stringify({status: 'success'}));
});           

複制

轉換URL字元串與查詢字元串

  • querystring子產品:轉換URL中的查詢字元串(URL中?之後,#之前)

    querystring.stringify(obj, [sep], [eq]) querystring.parse(str, [sep], [eq], [option])

    • sep:分割符,預設&
    • eq:配置設定字元,預設=
    • options:

      {maxKeys: number}

      指定轉換後對象中的屬性個數
    let str = querystring.stringify({name: 'ligang', age: 27}); console.log(str); // name=ligang&age=27 let obj = querystring.parse(str); console.log(obj); // { name: 'ligang', age: '27' }
  • url子產品:轉換完整URL字元串

    url.parse(urlStr, [parseQueryString])

    • parseQueryString:如果為true,将查詢字元通過querystring轉換為對象;預設false。

    url.resolve(from, to);

    将二者結合成一個路徑,from、to既可以是相對路徑也可以是絕對路徑。

    // http://ligangblog.com/javascript/a?a=1 url.resolve('http://ligangblog.com/javascript/', 'a?a=1'); // http://ligangblog.com/a?a=1 url.resolve('http://ligangblog.com/javascript/', '/a?a=1');

    注意:具體合并規則,請檢視《Node權威指南》— 8.1HTTP伺服器。

屬性 含義
href 原URL字元串
protocol 協定
slashes 在協定與路徑中間是否使用“//”分隔符
host URL完整位址及端口号,可能是一個IP位址
hostname URL完整位址,可能是一個IP位址
port 端口号
path URL字元串中的路徑,包含查詢字元串
pathname URL字元串中的路徑,不包含查詢字元串
search 查詢字元串,包含起始字元“?”
query 查詢字元串,不包含起始字元“?”
hash hash值,包含起始字元“#”
var urlStr = 'http://ligangblog.com/javascript/?name=lg&uid=1#a/b/c';
console.log(url.parse(urlStr, true));
/*
Url {
    protocol: 'http:',
    slashes: true,
    auth: null,
    host: 'ligangblog.com',
    port: null,
    hostname: 'ligangblog.com',
    hash: '#a/b/c',
    search: '?name=lg&uid=1',
    query: { name: 'lg', uid: '1' },
    pathname: '/javascript/',
    path: '/javascript/?name=lg&uid=1',
    href: 'http://ligangblog.com/javascript/?name=lg&uid=1#a/b/c' 
}
*/           

複制

發送伺服器端響應流

response.writeHead(statusCode, [reasonPhrase], [headers]);
// 或者
response.setHeader(name, value);           

複制

響應頭中包含的一些常用字段:

字段 說明
content-type 用于指定内容類型
location 用于将用戶端重定向到另一個URL位址
content-disposition 用于指定一個被下載下傳的檔案名
content-length 用于指定伺服器端響應内容的位元組數
set-cookie 用于在用戶端建立一個cookie
content-encoding 用于指定伺服器端響應内容的編碼方式
Cache-Control 用于開啟緩存機制
Expires 用于指定緩存過期時間
Tag 用于指定當伺服器響應内容沒有變換時不重新下載下傳資料

示例:

response.writeHead(200, {'Content-Type': 'text/plain', 
                         'Access-Control-Allow-Origin': 'http://localhost'});
// 或者
response.statusCode = 200;
response.setHeader('Content-Type', 'text/plain');
response.setHeader('Access-Control-Allow-Origin', 'http://localhost');           

複制

writeHead和setHeader差別:

  • writeHead:該方法被調用時發送響應頭
  • setHeader:write方法第一次被調用時發送響應頭
/* 擷取響應頭中的某個字段值 */
response.getHeader(name);
/* 删除一個響應字段值 */
response.removeHeader(name);
/* 該屬性表示響應頭是否已發送 */
response.headersSent;
/* 在響應資料的尾部增加一個頭資訊 */
response.addTrailers(headers);           

複制

示例:

// 必須再響應頭中添加Trailer字段,并且其值設定為追加的響應頭中所指定的字段名
response.write(200, {'Content-Type': 'text/plain', 'Trailer': 'Content-MD5'});
response.write('....');
response.addTrailers({'Content-MD5', '...'});
response.end();           

複制

特别說明:

當再快速網路且資料量很小的情況下,Node将資料直接發送到作業系統核心緩存區中,然後從該核心緩存區中取出資料發送給請求方;如果網速很慢或者資料量很大,Node通常會将資料緩存在記憶體中,在對方可以接受資料的情況下将記憶體中的資料通過作業系統核心緩存區發送給請求方。

response.write

傳回true,說明直接寫到了作業系統核心緩存區中;傳回false,說明暫時緩存的記憶體中。每次需要通過調用

response.end()

來結束響應。

[NodeJS]建立HTTP、HTTPS伺服器與用戶端

響應逾時會觸發

timeout

事件;

response.end()

方法調用之前,如果連接配接中斷,會觸發

close

事件。

/* 設定請求逾時時間2分鐘 */
response.setTimeout(2 * 60 * 1000, () => {
  console.error('請求逾時!'); 
});
// 或者
response.setTimout(2 * 60 * 1000);
response.on('timeout', () => {
  console.error('請求逾時!');
});

/* 連接配接中斷 */
response.on('close', () => {
  console.error('連接配接中斷!');
});           

複制

/**
 * HTTP服務端
 * Created by ligang on 17/5/28.
 */

import http from 'http';

var server = http.createServer();
// 接受用戶端請求時觸發
server.on('request', (request, response) => {
    if(request.url !== '/favicon.ico') {
        response.setTimeout(2 * 60 * 1000, () => {
           console.error('請求逾時!');
        });
        response.on('close', () => {
            console.error('請求中斷!');
        });
        let result = '';
        request.on('data', (data) => {
            result += data;
        });
        request.on('end', () => {
            console.log(`伺服器資料接收完畢:${result}`);
            response.statusCode = 200;
            response.write('收到!');
            response.end(); // 結束本次請求
        });
    }
});

server.listen(10000, 'localhost', 511);
// 開始監聽
server.on('listening', () => {
    console.log('開始監聽');
});

server.on('error', (e) => {
    if(e.code === 'EADDRINUSE') {
        console.log('端口被占用');
    }else {
        console.log(`發生錯誤:${e.code}`);
    }
});           

複制

HTTP用戶端

Node.js可以輕松向任何網站發送請求并讀取網站的響應資料。
var req = http.request(options, callback);
// get請求
var req = http.get(options, callback);
// 向目标網站發送資料
req.write(chunk, [encoding]);
// 結束本次請求
req.end([chucnk], [encoding]);
// 中止本次請求
req.abort();           

複制

其中,options用于指定目标URL位址,如果該參數是一個字元串,将自動使用url子產品中的parse方法轉換為一個對象。注意:

http.get()

方法隻能使用Get方式請求資料,且無需調用

req.end()

方法,Node.js會自動調用。

/**
 * HTTP用戶端
 * Created by ligang on 17/5/30.
 */
import http from 'http';

const options = {
        hostname: 'localhost',
        port: 10000,
        path: '/',
        method: 'post'
    },
    req = http.request(options);

req.write('你好,伺服器');
req.end();

req.on('response', (res) => {
    console.log(`狀态碼:${res.statusCode}`);
    let result = '';
    res.on('data', (data) => {
        result += data;
    });
    res.on('end', () => {
        console.log(`用戶端接受到響應:${result}`);
    })
});
req.setTimeout(60* 1000, () => {
    console.log('逾時了');
    req.abort();
});
req.on('error', (error) => {
    if(error.code === 'ECONNERSET') {
        console.log('socket端口逾時');
    }else {
        console.log(`發送錯誤:${error.code}`);
    }
});           

複制

代理伺服器

/**
 * HTTP代理
 * Created by ligang on 17/5/30.
 */

import http from 'http';
import url from 'url';

/**
 * 服務端
 */
const server = http.createServer(async (req, res) => {
    // req.setEncoding('utf-8');
    /* 逾時 2分鐘 */
    res.setTimeout(2 * 60 * 1000, () => {
        // ...
    });
    /* 連接配接中斷 */
    res.on('close', () => {
        // ...
    });

    let options = {},
        result = "";

    options = await new Promise((resolve, reject) => {
        if(req.method === 'GET') {
            options = url.parse('http://localhost:10000' + req.url);
            resolve(options);
        }else if(req.method === 'POST') {
            req.on('data', (data) => {
                result += data;
            });

            req.on('end', () => {
                options = url.parse('http://localhost:10000' + req.url);
                // post請求必須制定
                options.headers = {
                    'content-type': 'application/json',
                };
                resolve(options);
            });
        }
    });
    options.method = req.method;

    let content = await clientHttp(options, result ? JSON.parse(result) : result);
    res.setHeader('Content-Type', 'text/html');
    res.write('<html><head><meta charset="UTF-8" /></head>')
    res.write(content);
    res.write('</html>');
    // 結束本次請求
    res.end();
});
server.listen(10010, 'localhost', 511);
/* 開始監聽 */
server.on('listening', () => {
    // ...
});
/* 監聽錯誤 */
server.on('error', (e) => {
    console.log(e.code);
    // ...
});

/**
 * 用戶端
 * @param options 請求參數
 * @param data 請求資料
 */
async function clientHttp(options, data) {
    let output = new Promise((resolve, reject) => {
        let req = http.request(options, (res) => {
            let result = '';
            res.setEncoding('utf8');
            res.on('data', function (chunk) {
                result += chunk;
            });
            res.on('end', function () {
                resolve(result);
            });
        });
        req.setTimeout(60000, () => {
            console.error(`連接配接背景逾時 ${options.href}`);
            reject();
            req.abort();
        });
        req.on('error', err => {
            console.error(`連接配接背景報錯 ${err}`);
            if (err.code === 'ECONNRESET') {
                console.error(`socket逾時 ${options.href}`);
            } else {
                console.error(`連接配接背景報錯 ${err}`);
            }
            reject();
            req.abort();
        });
        // 存在請求資料,發送請求資料
        if (data) {
            req.write(JSON.stringify(data));
        }
        req.end();
    });
    return await output;
}           

複制

注意:

  • POST請求必須指定headers資訊,否則會報錯

    socket hang up

  • 擷取到options後需要重新指定其method

    options.method = req.method;

HTTPS伺服器

  • HTTPS使用https協定,預設端口号44;
  • HTTPS需要向證書授證中心申請證書;
  • HTTPS伺服器與用戶端之間傳輸是經過SSL安全加密後的密文資料;

建立公鑰、私鑰及證書

(1)建立私鑰

openssl genrsa -out privatekey.pem 1024           

複制

(2)建立證書簽名請求

openssl req -new -key privatekey.pem -out certrequest.csr           

複制

(3)獲驗證書,線上證書需要經過證書授證中心簽名的檔案;下面隻建立一個學習使用證書

openssl x509 -req -in certrequest.csr -signkey privatekey.pem -out certificate.pem           

複制

(4)建立pfx檔案

openssl pkcs12 -export -in certificate.pem -inkey privatekey.pem -out certificate.pfx           

複制

HTTPS服務

建立HTTPS伺服器同HTTP伺服器大緻相同,需要增加證書,建立HTTPS伺服器時通過options參數設定。

import https from 'https';
import fs from 'fs';

var pk = fs.readFileSync('privatekey.pem'),
    pc = fs.readFileSync('certificate.pem');

var opts = {
    key: pk,
    cert: pc
};

var server = https.createServer(opts);           

複制

opts參數為一個對象,用于指定建立HTTPS伺服器時配置的各種選項,下面隻描述幾個必要選項:

屬性名 說明
pff 用于指定從pfx檔案讀取出的私鑰、公鑰以及證書(指定該屬性後,無需再指定key、cert、ca)
key 用于指定字尾名為pem的檔案,讀出私鑰
cert 用于指定字尾名為pem的檔案,讀出公鑰
ca 用于指定一組證書,預設值為幾個著名的證書授證中心

HTTPS用戶端

const options = {
        hostname: 'localhost',
        port: 1443,
        path: '/',
        method: 'post',
        key: fs.readFileSync('privatekey.pem'),
        cert: fs.readFileSync('certificate.pem'),
        rejectUnhauthorized: false,
        agent: false // 從連接配接池中指定挑選一個目前連接配接狀态為關閉的https.Agent
    },
    req = https.request(options);

// 或者
const options = {
        hostname: 'localhost',
        port: 1443,
        path: '/',
        method: 'post',
        key: fs.readFileSync('privatekey.pem'),
        cert: fs.readFileSync('certificate.pem'),
        rejectUnhauthorized: false,
    };
// 顯示指定https.Agent對象
options.agent = new https.Agent(options);
var req = https.request(options);           

複制

說明: 普通的 HTTPS 服務中,服務端不驗證用戶端的證書(但是需要攜帶證書),中間人可以作為用戶端與服務端成功完成 TLS 握手; 但是中間人沒有證書私鑰,無論如何也無法僞造成服務端跟用戶端建立 TLS 連接配接。 當然如果你擁有證書私鑰,代理證書對應的 HTTPS 網站當然就沒問題了,是以這裡的私鑰和公鑰隻是格式書寫,沒有太大意義,隻要将請求回來的資料原原本本交給浏覽器來解析就算完成任務。

關于代理的兩篇文章:HTTP 代理原理及實作(一)、HTTP 代理原理及實作(二)