国产xxxx99真实实拍_久久不雅视频_高清韩国a级特黄毛片_嗯老师别我我受不了了小说

資訊專欄INFORMATION COLUMN

走進Node.js 之 HTTP實現分析

April / 904人閱讀

摘要:事實上,協議確實是基于協議實現的。的可選參數用于監聽事件另外,它也監聽事件,只不過回調函數是自己實現的。并且會把本次連接的套接字文件描述符封裝成對象,作為事件的參數。過載保護理論上,允許的同時連接數只與進程可以打開的文件描述符上限有關。

作者:正龍(滬江Web前端開發工程師)
本文為原創文章,轉載請注明作者及出處

上文“走進Node.js啟動過程”中我們算是成功入門了。既然Node.js的強項是處理網絡請求,那我們就來分析一個HTTP請求在Node.js中是怎么被處理的,以及JavaScript在這個過程中引入的開銷到底有多大。

Node.js采用的網絡請求處理模型是IO多路復用。它與傳統的主從多線程并發模型是有區別的:只使用有限的線程數(1個),所以占用系統資源很少;操作系統級的異步IO支持,可以減少用戶態/內核態切換,并且本身性能更高(因為直接與網卡驅動交互);JavaScript天生具有保護程序執行現場的能力(閉包),傳統模型要么依賴應用程序自己保存現場,或者依賴線程切換時自動完成。當然,并不能說IO多路復用就是最好的并發模型,關鍵還是看應用場景。

我們來看“hello world”版Node.js網絡服務器:

require("http").createServer((req, res) => {
    res.end("hello world");
}).listen(3333);
代碼思路分析 createServer([requestListener])

createServer創建了http.Server對象,它繼承自net.Server。事實上,HTTP協議確實是基于TCP協議實現的。createServer的可選參數requestListener用于監聽request事件;另外,它也監聽connection事件,只不過回調函數是http.Server自己實現的。然后調用listen讓http.Server對象在端口3333上監聽連接請求并最終創建TCP對象,由tcp_wrap.h實現。最后會調用TCP對象的listen方法,這才真正在指定端口開始提供服務。我們來看看涉及到的所有JavaScript對象:

涉及到的C++類大多只是對libuv做了一層包裝并公布給JavaScript,所以不在這里特別列出。我們有必要提一下http-parser,它是用來解析http請求/響應消息的,本身十分高效:沒有任何系統調用,沒有內存分配操作,純C實現。

connection事件

當服務器接受了一個連接請求后,會觸發connection事件。我們可以在這個結點獲取到套接字文件描述符,之后就可以在這個文件描述符上做流式讀或寫,也就是所謂的全雙工模式。上文提到net.Server的listen方法會創建TCP對象,并且提供TCP對象的onconnection事件回調方法;這里可以利用字段net.Server.maxConnections做過載保護,后面會講到。并且會把clientHandle(本次連接的套接字文件描述符)封裝成net.Socket對象,作為connection事件的參數。我們來看看調用過程:

tcp_wrap.cc

void TCPWrap::Listen(const FunctionCallbackInfo& args) {
  int err = uv_listen(reinterpret_cast(&wrap->handle_),
                      backlog,
                      OnConnection);
  args.GetReturnValue().Set(err);
}

OnConnectionconnection_wrap.cc中定義

    // ...省略不重要的代碼
    uv_stream_t* client_handle =
        reinterpret_cast(&wrap->handle_);
    // uv_accept can fail if the new connection has already been closed, in
    // which case an EAGAIN (resource temporarily unavailable) will be
    // returned.
    if (uv_accept(handle, client_handle))
      return;

    // Successful accept. Call the onconnection callback in JavaScript land.
    argv[1] = client_obj;
  // ...省略不重要的代碼
  wrap_data->MakeCallback(env->onconnection_string(), arraysize(argv), argv);

上文提到的clientHandle實際上是uv_accept的第二個參數,指服務當前連接的套接字文件描述符。net.Server的字段 _handle 會在JavaScript側存儲該字段。最后我們上一張流程圖:

request事件

connection事件的回調函數connectionListener(lib/_http_server.js)中,首先獲取http-parser對象,設置parser.onIncoming回調(馬上會用到)。當連接套接字有數據到達時,調用http-parser.execute方法。http-parser在解析過程中會觸發如下回調函數:

on_message_begin:在開始解析HTTP消息之前,可以設置http-parser的初始狀態(注意http-parse有可能是復用的而不是重每次新創建)

on_url:解析請求的url,對響應消息不起作用

on_status, 解析狀態碼,只對http響應消息起作用

on_head_field, 頭字段名稱

on_head_value:頭字段對應值

on_headers_complete:當所有頭解析完成時

on_body:解析http消息中包含的payload

on_message_complete:解析工作結束

Node.js中Parser類是對http-parser的包裝,它會注冊上面所有的回調函數。同時,暴露給JavaScript5個事件:
kOnHeaders,kOnHeadersComplete,kOnBody,kOnMessageComplete,kOnExecute。在lib/_http_common.js中監聽了這些事件。其中,當需要強制把頭字段回傳到JavaScript時會觸發kOnHeaders;例如,頭字段個數超過32,或者解析結束時仍然有頭字段沒有回傳給JavaScript。當調用完http_parser_execute后觸發kOnExecute。kOnHeadersComplete事件觸發時,會調用parser的onIncoming回調函數。僅僅HTTP頭解析完成之后,就會觸發request事件。執行流程如下:

總結

說了那么多,其實仍然離不開最基礎的套接字編程步驟,對于服務器端依次是:create、bind,listen、accept和close。客戶端會經歷create、bind、connect和close。想了解更多套接字編程的同學可以參考《UNIX網絡編程》。

HTTP場景分析

上面提到的Node.js版hello world只涵蓋了HTTP處理最基本的情況,但是也足以說明Node.js處理得非常簡潔。現在,我們來分析一些典型的HTTP場景。

1. keep-alive

對于前端應用,HTTP請求瞬間數量比較多,但每個請求傳輸的數據一般不大;這時,用同一個TCP連接處理同一個用戶發出的HTTP請求可以顯著提高性能。但是keep-alive也不是萬能的,如果用戶每次只發起一個請求,它反而會因為延長連接的生存時間,浪費服務器資源。

針對同一個連接,Node.js會維持一個incoming隊列和一個outgoing隊列。應用程序通過監聽request事件,可以訪問ServerResponse和IncomingMessage對象,當請求處理完成之后(調用response.end()),ServerResponse會響應finish事件。如果它是本次連接上最后一個response對象,則準備關閉連接;否則,繼續觸發request事件。每個連接最長超時時間默認為2分鐘,可以通過http.Server.setTimeout調整。
現在把我們的Node.js版hello world修改一下

var delay = [2000, 30, 500];
var i = 0;
require("http").createServer((req, res) => {
    // 為了讓請求模擬更真實,會調整每個請求的響應時間
    setTimeout(() => {
        res.end("hello world");
    }, delay[i]);
    i = (i+1)%(delay.length);
}).listen(3333, () => {
    // listen的回調函數
    console.log("listen at 3333");
});

客戶端代碼如下:

var http = require("http");

// 設置HTTP agent開啟keep-alive模式
// 套接字的打開時間維持1分鐘
var agent = new http.Agent({
    keepAlive: true,
    keepAliveMsecs: 60000
});

// 每次請求結束之后,都會再發起一次請求
// doReq每調用一次只會觸發2次請求
function doReq(again, iter) {
    let request = http.request({
        hostname: "192.168.1.10",
        port: 3333,
        agent:agent
    }, (res) => {
        console.log(`${new Date().valueOf()} ${iter} ${again} Headers: ${JSON.stringify(res.headers)}`);
        console.log(request.socket.localPort);
        // 設置解析響應的編碼格式
        res.setEncoding("utf8");
        // 接收響應
        res.on("data", (chunk) => {
            console.log(`${new Date().valueOf()} ${iter} ${again} Body: ${chunk}`);
        });
        if (again) doReq(false, iter);
    });
    // 發起請求
    request.end();
}

for (let i = 0; i < 3; i++) {
    doReq(true, i);
}

套接字復用的時序如下:

2. Expect頭

如果客戶端在發送POST請求之前,由于傳輸的數據量比較大,期望向服務器確認請求是否能被處理;這種情況下,可以先發送一個包含頭Expect:100-continue的http請求。如果服務器能處理此請求,則返回響應狀態碼100(Continue);否則,返回417(Expectation Failed)。默認情況下,Node.js會自動響應狀態碼100;同時,http.Server會觸發事件checkContinue和checkExpectation來方便我們做特殊處理。具體規則是:當服務器收到頭字段Expect時:如果其值為100-continue,會觸發checkContinue事件,默認行為是返回100;如果值為其它,會觸發checkExpectation事件,默認行為是返回417。

例如,我們通過curl發送HTTP請求:

curl -vs --header "Expect:100-continue" http://localhost:3333

交互過程如下

> GET / HTTP/1.1
> Host: localhost:3333
> User-Agent: curl/7.49.1
> Accept: */*
> Expect:100-continue
>
< HTTP/1.1 100 Continue
< HTTP/1.1 200 OK
< Date: Mon, 03 Apr 2017 14:15:47 GMT
< Connection: keep-alive
< Content-Length: 11
<

我們接收到2個響應,分別是狀態碼100和200。前一個是Node.js的默認行為,后一個是應用程序代碼行為。

3. HTTP代理

在實際開發時,用到http代理的機會還是挺多的,比如,測試說線上出bug了,觸屏版頁面顯示有問題;我們一般第一時間會去看api返回是否正常,這個時候在手機上設置好代理就能輕松捕獲HTTP請求了。老牌的代理工具有fiddler,charles。其實,nodejs下也有,例如node-http-proxy,anyproxy。基本思路是監聽request事件,當客戶端與代理建立HTTP連接之后,代理會向真正請求的服務器發起連接,然后把兩個套接字的流綁在一起。我們可以實現一個簡單的代理服務器:

var http = require("http");
var url = require("url");

http.createServer((req, res) => {
    // request回調函數
    console.log(`proxy request: ${req.url}`);
    var urlObj = url.parse(req.url);
    var options = {
        hostname: urlObj.hostname,
        port: urlObj.port || 80,
        path: urlObj.path,
        method: req.method,
        headers: req.headers
    };
    // 向目標服務器發起請求
    var proxyRequest = http.request(options, (proxyResponse) => {
        // 把目標服務器的響應返回給客戶端
        res.writeHead(proxyResponse.statusCode, proxyResponse.headers);
        proxyResponse.pipe(res);
    }).on("error", () => {
        res.end();
    });
    // 把客戶端請求數據轉給中間人請求
    req.pipe(proxyRequest);
}).listen(8089, "0.0.0.0");

驗證下是否真的起作用,curl通過代理服務器訪問我們的“hello world”版Node.js服務器:

curl -x http://192.168.132.136:8089 http://localhost:3333/
優化策略

Node.js在實現HTTP服務器時,除了利用高性能的http-parser,自身也做了些性能優化。

1. http_parser對象緩存池

http-parser對象處理完一個請求之后不會被立即釋放,而是被放入緩存池(/lib/internal/freelist),最多緩存1000個http-parser對象。

2. 預設HTTP頭總數

HTTP協議規范并沒有限定可以傳輸的HTTP頭總數上限,http-parser為了避免動態分配內存,設定上限默認值是32。其他web服務器實現也有類似設置;例如,apache能處理的HTTP請求頭默認上限(LimitRequestFields)是100。如果請求消息中頭字段真超過了32個,Node.js也能處理,它會把已經解析的頭字段通過事件kOnHeaders保存到JavaScript這邊然后繼續解析。 如果頭字段不超過32個,http-parser會直接處理完并觸發on_headers_complete一次性傳遞所有頭字段;所以我們在利用Node.js作為web服務器時,應盡量把頭字段控制在32個之內。

3. 過載保護

理論上,Node.js允許的同時連接數只與進程可以打開的文件描述符上限有關。但是隨著連接數越來越多,占用的系統資源也越來越多,很有可能連正常的服務都無法保證,甚至可能拖垮整個系統。這時,我們可以設置http.Server的maxConnections,如果當前并發量大于服務器的處理能力,則服務器會自動關閉連接。另外,也可以設置socket的超時時間為可接受的最長響應時間。

性能實測

為了簡單分析下Node.js引入的開銷,現在基于libuv和http_parser編寫一個純C的HTTP服務器。基本思路是,在默認事件循環隊列上監聽指定TCP端口;如果該端口上有請求到達,會在隊列上插入一個一個的任務;當這些任務被消費時,會執行connection_cb。見核心代碼片段:

int main() {
    // 初始化uv事件循環
    loop = uv_default_loop();
    uv_tcp_t server;
    struct sockaddr_in addr;
    // 指定服務器監聽地址與端口
    uv_ip4_addr("192.168.132.136", 3333, &addr);

    // 初始化TCP服務器,并與默認事件循環綁定
    uv_tcp_init(loop, &server);
    // 服務器端口綁定
    uv_tcp_bind(&server, (const struct sockaddr*)&addr, 0);
    // 指定連接處理回調函數connection_cb
    // 256為TCP等待隊列長度
    int r = uv_listen((uv_stream_t*)&server, 256, connection_cb);

    // 開始處理默認時間循環上的消息
    // 如果TCP報錯,事件循環也會自動退出
    return uv_run(loop, UV_RUN_DEFAULT);
}

connection_cb調用uv_accept會負責與發起請求的客戶端實際建立套接字,并注冊流操作回調函數read_cb:

void connection_cb(uv_stream_t* server, int status) {
    uv_tcp_t* client = (uv_tcp_t*)malloc(sizeof(uv_tcp_t));
    uv_tcp_init(loop, client);
    // 與客戶端建立套接字
    uv_accept(server, (uv_stream_t*)client);
    uv_read_start((uv_stream_t*)client, alloc_buffer, read_cb);
}

上文中read_cb用于讀取客戶端請求數據,并發送響應數據:

void read_cb(uv_stream_t* stream, ssize_t nread, const uv_buf_t* buf) {
    if (nread > 0) {
        memcpy(reqBuf + bufEnd, buf->base, nread);
        bufEnd += nread;
        free(buf->base);
        // 驗證TCP請求數據是否是合法的HTTP報文
        http_parser_execute(parser, &settings, reqBuf, bufEnd);
        uv_write_t* req = (uv_write_t*)malloc(sizeof(uv_write_t));
        uv_buf_t* response = malloc(sizeof(uv_buf_t));
        // 響應HTTP報文
        response->base = "HTTP/1.1 200 OK
Connection:close
Content-Length:11

hello world

";
        response->len = strlen(response->base);
        uv_write(req, stream, response, 1, write_cb);
    } else if (nread == UV_EOF) {
        uv_close((uv_handle_t*)stream, close_cb);
    }
}

全部源碼請參見simple HTTP server。我們使用apache benchmark來做壓力測試:并發數為5000,總請求數為100000。

ab -c 5000 -n 100000 http://192.168.132.136:3333/

測試結果如下: 0.8秒(C) vs??5秒(Node.js)

我們再看看內存占用,0.6MB(C) vs??51MB(Node.js)

Node.js雖然引入了一些開銷,但是從代碼實現行數上確實要簡潔很多。

iKcamp原創新書《移動Web前端高效開發實戰》已在亞馬遜、京東、當當開售。

文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。

轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/91745.html

相關文章

  • 系列3|走進Node.js多進程模型

    摘要:例如,在方法中,如果需要主從進程之間建立管道,則通過環境變量來告知從進程應該綁定的相關的文件描述符,這個特殊的環境變量后面會被再次涉及到。 文:正龍(滬江網校Web前端工程師)本文原創,轉載請注明作者及出處 之前的文章走進Node.js之HTTP實現分析中,大家已經了解 Node.js 是如何處理 HTTP 請求的,在整個處理過程,它僅僅用到單進程模型。那么如何讓 Web 應用擴展到...

    snowell 評論0 收藏0
  • 干貨 | 走進Node.js啟動過程剖析

    摘要:具體調用鏈路如圖函數主要是解析啟動參數,并過濾選項傳給引擎。查閱文檔之后發現,通過指定參數可以設置線程池大小。原來的字節碼編譯優化還有都是通過多線程完成又繼續深入調查,發現環境變量會影響的線程池大小。執行過程如下調用執行。 作者:正龍 (滬江Web前端開發工程師)本文原創,轉載請注明作者及出處。 隨著Node.js的普及,越來越多的開發者使用Node.js來搭建環境,也有很多公司開始把...

    luck 評論0 收藏0
  • 干貨剖析 | 走進Node.js啟動過程

    摘要:具體調用鏈路如圖函數主要是解析啟動參數,并過濾選項傳給引擎。查閱文檔之后發現,通過指定參數可以設置線程池大小。原來的字節碼編譯優化還有都是通過多線程完成又繼續深入調查,發現環境變量會影響的線程池大小。執行過程如下調用執行。 作者:正龍 (滬江Web前端開發工程師)本文原創,轉載請注明作者及出處。 隨著Node.js的普及,越來越多的開發者使用Node.js來搭建環境,也有很多公司開始把...

    Simon 評論0 收藏0
  • 2017-07-12 前端日報

    摘要:前端日報精選借助和緩存及離線開發中和走進之實現分析總是一知半解的中個常見的陷阱發布核心成員發布了免費的學習視頻中文譯的函數式編程是一種反模式掘金譯更好的表單設計每一頁,一件事實例研究掘金打印龍墨并不簡單結合實現簡單的加載動畫 2017-07-12 前端日報 精選 借助Service Worker和cacheStorage緩存及離線開發JavaScript中toString()和valu...

    zhoutk 評論0 收藏0
  • 2017-09-13 前端日報

    摘要:前端日報精選中的垃圾收集,圖文指南十個免費的前端開發工具專題之遞歸如何在鏈中共享變量基于的爬蟲框架中文譯十六進制顏色揭秘掘金掘金小書基本環境安裝小書教程中間件對閉包的一個巧妙使用簡書源碼分析掘金組件開發練習焦點圖切換前端學 2017-09-13 前端日報 精選 V8 中的垃圾收集(GC),圖文指南十個免費的web前端開發工具JavaScript專題之遞歸 · Issue #49 · m...

    BWrong 評論0 收藏0

發表評論

0條評論

最新活動
閱讀需要支付1元查看
<