前端進階-讓你升級的網絡知識

在正文以前,我想問你們一個問題:
問:親,你有基礎嗎?
答: 有啊,你說前端嗎? 不就是HTML,JS,CSS 嗎? so easy~
問: oh-my-zsh... 好吧,那問題來了,挖掘機技術哪家強... 開玩笑。
如今纔是問題的正內容。php

  • 你知道TCP的基本內容嗎?(母雞啊~)前端

  • 好吧,那你知道TCP的3次握手,4次揮手嗎?(知道一點點)node

  • 恩,好,那什麼是進程呢?什麼是線程呢?(母雞啊。。)程序員

  • 那併發和並行又是什麼呢?(母雞啊)web

  • OMG, 那nodeJS多進程實現你會嗎?(不會呀~~~ md ...這都是些shenmegui)算法

其實,說多了都是淚,這些都是程序員的基本素質呀。。。 面tencent的時候,被一個總監,罵的阿彌陀佛麼麼噠. 今天在這裏和你們分享一下,個人血淚史。npm

TCP內容

工欲善其事,必先利其器segmentfault

一個程序員境界的提高,並不在於你寫的一首好代碼,更在於你能說出代碼背後的故事。ok~ 雞湯灌完了。咱們開始說方法了。
首先這幅圖你們必須記得很是清楚才行。
TCP
對了還有,
OSI七層模型你們應該爛熟於心的。
此處輸入圖片的描述
其中TCP處理transport層,主要是用來創建可靠的鏈接。 而創建鏈接的基礎,是他豐富的報文內容(md~超級多).咱們先來解釋一下。 首先,咱們TCP3次握手用的報文就是綠色的"TCP Flags"內容。 經過發送ACK,SYN包實現。具體涉及的Tag詳見:安全


  • Source Port / Destination Port:這個就是客戶端口(源端口)和服務器端口(目的端口). 端口就是用來區別主機中的不一樣進程,經過結合源IP和目的IP結合,得出惟一的TCP鏈接。服務器

  • Sequence Number(seqNumber): 通常由 客戶端發送,用來表示報文段中第一個數據字節在數據流中的序號,主要用來解決網絡包亂序的問題。

  • Acknowledgment Number(ACK): 即就是用來存放客戶端發來的seqNumber的下一個信號(seqNumber+1). 只有當 TCP flags中的ACK爲1時纔有效. 主要是用來解決不丟包的問題。

  • TCP flags: TCP中有6個首部,用來控制TCP鏈接的狀態.取值爲0,1.這6個有:URG,ACK,PSH,RST,SYN,FIN.

    • URG 當爲1時,用來保證TCP鏈接不被中斷, 而且將該次TCP內容數據的緊急程度提高(就是告訴電腦,你丫趕快把這個給resolve了)

    • ACK 一般是服務器端返回的。 用來表示應答是否有效。 1爲有效,0爲無效

    • PSH 表示,當數據包獲得後,立馬給應用程序使用(PUSH到最頂端)

    • RST 用來確保TCP鏈接的安全。 該flag用來表示 一個鏈接復位的請求。 若是發生錯誤鏈接,則reset一次,從新連。固然也能夠用來拒絕非法數據包。

    • SYN 同步的意思,一般是由客戶端發送,用來創建鏈接的。第一次握手時: SYN:1 , ACK:0. 第二次握手時: SYN:1 ACK:1

    • FIN 用來表示是否結束該次TCP鏈接。 一般當你的數據發送完後,會自動帶上FIN 而後斷開鏈接

恩,基本的TCP內容,你們應該掌握了吧。OK, go on.

What's TCP 3次握手

仍是同樣, 先上張圖,讓你們先看一下。 上面你們已經基本瞭解了TCP裏面相應的字段,如今看看圖裏面的是否是以爲有些親切嘞?

此處輸入圖片的描述

其實,你們看上面的圖,差很少都已經可以摸清楚,每次發送請求的內容。其實,TCP3次握手是爲了創建 穩定可靠的鏈接。因此也就不存在神馬 2次鏈接等的怪癖。
(圖中flag說明:SYN包表示標誌位syn=1,ACK包表示標誌位ack=1,SYN+ACK包表示標誌位syn=1,ack=1)
如今,咱們來正式進入3次握手環節。

  • 第一次握手. 客戶端向服務器發送一個SYN包,而且添加上seqNumber(假設爲x),而後進入SYN_SEND狀態,而且等待服務器的確認。

  • 第二次握手: 服務器接受SYN包,而且進行確認,若是該請求有效,則將TCP flags中的ACK 標誌位置1, 而後將AckNumber置爲(seqNumber+1),而且再添加上本身的seqNumber(y), 完成後,返回給客戶端.服務器進入SYN_RECV狀態.(這裏服務端是發送SYN+ACK包)

  • 第三次握手 客戶端接受ACK+SYN報文後,獲取到服務器發送seqNumber(y), 而且 將新頭部的AckNumber變爲(y+1).而後發送給服務器,完成TCP3次鏈接。此時服務器和客戶端都進入ESTABLISHED狀態.

回答一下這個比較尷尬的問題,爲何只有3次握手,而不是4次,或者2次?
很簡單呀,由於3次就夠了,幹嗎用4次。23333. 舉個例子吧,假如是2次的話, 可能會出現這樣一個狀況。

  • 當客戶端發送一次請求A後,可是A在網絡延遲了好久, 接着客戶端又發送了一次B,可是此時A已經無效了。 接着服務器相應了B,並返回TCP鏈接頭,創建鏈接(這裏就2次哈)。 而後,A 歷經千山萬水終於到服務器了, 服務器一看有請求來了,則接受,因爲一開始A帶着的TCP格式都是正確的,那麼服務器,理所應當的也返回成功鏈接的flag,可是,此時客戶端已經判斷該次請求無效,廢棄了。 而後服務器,就這麼一直掛着(浪費資源),形成的一個問題是,md, 這個鍋是誰的? 因此,爲了保險起見,再補充一次鏈接就能夠了。因此3次是最合適的。在Chinese中,以3爲起稱爲,若是你用4,5,6,7,8...次的話,這不更浪費嗎?

TCP4次揮手

TCP4次揮手,是比較簡單的。你們對照上面那個圖,咱們一步一步進行一下講解。

  • 第一次揮手: A機感受此時若是keep-alive比較浪費資源,則他提出了分手的請求。設置SeqNumberAckNumber以後,向B機發送FIN包, 表示我這已經沒有數據給你了。而後A機進入FIN_WAIT_1狀態

  • 第二次揮手:B機收到了A機的FIN包,已經知道了A機沒有數據再發送了。此時B機會給A機發送一個ACK包,而且將AckNumber 變爲 A機傳輸來的SeqNumber+1. 當A機接受到以後,則變爲FIN_WAIT_2狀態。表示已經獲得B機的許可,能夠進行關閉操做。不過此時,B機仍是能夠向A機發送請求的。

  • 第三次揮手 B機向A機發送FIN包,請求關閉,至關於告訴A機,我這裏也沒有你要的數據了。而後B機進入CLOSE_WAIT狀態.(這裏還須要帶上SeqNumber,你們看圖說話就能夠了)

  • 第四次揮手 A機接收到B機的FIN包以後,而後一樣,發送一個ACK包給B機。 B機接受到以後,就斷開了。 而A機 會等待2MSL以後,若是沒有回覆,確保服務器端確實是關閉了。而後A機也能夠關閉鏈接。A,B都進入了CLOSE狀態.

明白了嗎?
大哥~ 等等,什麼是2MSL呀~
哦,對哦。 這個還麼說...
2MSL=2*MSL. 而MSL其實就是Maximum Segment Lifetime,中文意思就是報文最大生存時間。RFC 793中規定MSL爲2分鐘,實際應用中經常使用的是30秒,1分鐘和2分鐘等。 一樣上面的TIME_WAT狀態其實也就是2MSL狀態。 若是超過改時間,則會將該報文廢棄,而後直接進入CLOSED狀態.

進程?線程?

親,請問php是一門什麼語言? (提示,關於進程)
官方回答: php是一門基於多線程的語言
親,請問nodeJS是一門什麼語言?(提示,關於線程)
官方回答: Node.js是單線程!異步!非阻塞!(不過早已能夠實現多進程交互了)
那php和nodeJS區別在哪呢?具體能夠見圖:
PHP
PHP
NodeJS

ok~ 簡單吧。
親,那進程和線程區別是什麼嘞?
go die /(ㄒoㄒ)/~~
這算是計算機的基本知識吧。 首先咱們須要記住的是,進程包括線程。這很是重要。

進程就是系統分配資源的基本單位(好比CPU,內存等)
線程就是程序執行的最小單位

進程有本身的空間,若是一個進程崩潰不會引發其它進程的崩潰。
線程,沒有本身獨立的空間,多個線程共享的是進程的地址空間,固然處理一些基本的如程序計數器,一組寄存器和棧等。
若是一個線程崩潰,它所在的進程就崩潰了。 雖說,多進程很穩定,可是進程切換時,耗費的資源也是很大的。 因此對於大併發的nodeJS來講,使用多線程的效果要遠遠比多進程快,穩定。

線程的優點

1.系統在啓動一個進程的時候,會首先在資源中獨立一塊出來,在後臺創建一些列表進行維護。 而,線程是比進程低一個level的,因此建立線程所耗費的資源要遠遠比,建立進程的資源少。

  1. 因爲進程自己就比較複雜,因此若是進行進程切換的話,形成的性能損耗也是不言而喻的(由於多個進程獨立,在切換的時候還須要保證各自的獨立性)。 而線程切換就不一樣了,由於在處在同一進程下面,對於其餘的進程都是透明化的(內存共享),因此在進行進程切換時,所耗費的資源遠遠比進程切換的小。

  2. 在Linux和window下,CPU的分配是根據線程數來的,若是

    總線程數<= CPU數量:並行運行
    總線程數> CPU數量:併發運行

    並行指的是,當你的CPU核數比線程數多的話,則會將每一個線程都分在一個CPU核裏進行處理。

併發指的是,當你的CPU核數比線程數少的話,則會利用「時間片輪轉進程調度算法」,對每一個線程進行同等的運行。

4.細化進程的處理,一般一個進程能夠拆分爲多個線程進行處理,就和模塊化處理是相似的,使用模塊化書寫的效果要遠遠比使用單main入口方式書寫 清晰,穩定。

併發,並行原理

親, 併發和並行有什麼共同點嗎?
恩~ 有的, 他們都有個‘並’子,字面上看起來都是同時執行的意思。
沒錯,固然只是字面上而已。
實際上,併發和並行是徹底不一樣的概念。 這裏主要和CPU核數有關。這裏爲了理解,拿線程來做爲參考吧。
當你的

總線程數<= CPU數量:並行運行
總線程數> CPU數量:併發運行

很明顯,並行實際上是真正意義上的同時執行。 當線程數< CPU核數時,每一個線程會獨立分配到一個CPU裏進行處理。
你們看過火影忍者嗎?
沒錯,就是鳴人 出關 口遁九尾以後。 他使用影分身,跑去各地支援同伴,對抗斑。 這裏類比來講,就能夠理解爲, 每一個CPU 都是鳴人的一個影分身,他們執行這各自不一樣的工做,可是,在同一時間上,他們都在運行。 這就是並行
那併發嘞?
其實,併發有點難以理解,他作的工做其實,就是利用一系列算法實現,並行作的事。一個比較容易理解的就是「時間片輪轉進程調度算法」。
即: 在系統控制下,每一個線程輪流使用CPU,並且,每一個線程使用時間必須很短(好比10ms), 因此這樣切換下來。咱們(愚蠢的人類,哈哈哈), 天真的覺得任務,真的是在"並行"執行.

nodeJS的進程實現

一開始nodeJS最使人詬病的就是他的單線程特性。既是絕招也是死穴,不過nodeJS發展很快,在v0.8版本就已經添加了cluster做爲內置模塊,實現多核的利用。
關於nodeJS的進程模塊,最主要的固然仍是cluster. 經過調用child_process.fork()函數來開啓進程。 先看一個具體的demo(from 官網)

var cluster = require('cluster');
var http = require('http');
var numCPUs = require('os').cpus().length;

if (cluster.isMaster) {
    console.log("master start...");

    // Fork workers.
    for (var i = 0; i < numCPUs; i++) {
        cluster.fork();
    }
    //用來監聽子worker建立監聽服務
    cluster.on('listening',function(worker,address){
        console.log('listening: worker ' + worker.process.pid +', Address: '+address.address+":"+address.port);
    });

    cluster.on('exit', function(worker, code, signal) {
        console.log('worker ' + worker.process.pid + ' died');
    });
} else {
    http.createServer(function(req, res) {
        res.writeHead(200);
        res.end("hello world\n");
    }).listen(0);
}

存放爲app.js 而後運行node app.js就能夠實現一個簡單的多進程效果。
結果可能爲下:

master start...
listening: worker 1559, Address: null:57803
listening: worker 1556, Address: null:57803
listening: worker 1558, Address: null:57803
listening: worker 1557, Address: null:57803

能夠從上面的demo中看出,經過cluster.isMaster來區分master和worker. 而master和worker之間使用listen(0)進行通訊.

  • server.listen(0):在master和worker通訊過程,集羣中的worker會打開一個隨機端口共用,經過socket通訊像上例中的57803

固然你也能夠手動打開一個端口共享監聽。像這樣.

http.createServer(function(req, res) {
        res.writeHead(200);
        res.end("hello world\n");
    }).listen(3000);

cluster對應API

cluster對象的屬性和函數

  • cluster.setttings:配置集羣參數對象

  • cluster.isMaster:判斷是否是master節點*

  • cluster.isWorker:判斷是否是worker節點*

  • Event: 'fork': 監聽建立worker進程事件

  • Event: 'online': 監聽worker建立成功事件

  • Event: 'listening': 監聽worker開啓的http.listen

  • Event: 'disconnect': 監聽worker斷線事件

  • Event: 'exit': 監聽worker退出事件

  • Event: 'setup': 監聽setupMaster事件

  • cluster.setupMaster([settings]): 設置集羣參數

  • cluster.fork([env]): 建立worker進程

  • cluster.disconnect([callback]): 關閉worket進程*

  • cluster.worker: 得到當前的worker對象*

  • cluster.workers: 得到集羣中全部存活的worker對象*

經過cluster.worker得到的worker對象和相應的參數

  • worker.id: 進程ID號

  • worker.process: ChildProcess對象*

  • worker.suicide: 在disconnect()後,判斷worker是否自殺*

  • worker.send(message, [sendHandle]):* master給worker發送消息。注:worker給發master發送消息要用process.send(message)

  • worker.kill([signal='SIGTERM']): 殺死指定的worker,別名destory()*

  • worker.disconnect(): 斷開worker鏈接,讓worker自殺

  • Event: 'message': 監聽master和worker的message事件

  • Event: 'online': 監聽指定的worker建立成功事件

  • Event: 'listening': 監聽master向worker狀態事件

  • Event: 'disconnect': 監聽worker斷線事件

  • Event: 'exit': 監聽worker退出事件

這些就是cluster的所有內容。不過這僅僅只是內容而已,若是使用cluster,這即是咱們程序員要作的事了。

進程通訊

因爲nodeJS 只能實現單進程的效果,因此他的進程數只能爲一個,可是經過引用cluster模塊,能夠開啓多個子進程實現CPU的利用。
簡單進程交互
運行後的結果爲:

[master] start master...
[master] fork: worker1
[master] fork: worker2
[master] fork: worker3
[master] fork: worker4
[master] online: worker1
[master] online: worker4
[master] online: worker2
[master] online: worker3
[worker] start worker ...1
[worker] start worker ...4
[worker] start worker ...2
[master] listening: worker4,pid:990, Address:null:3000
[master] listening: worker1,pid:987, Address:null:3000
[master] listening: worker2,pid:988, Address:null:3000
[worker] start worker ...3
[master] listening: worker3,pid:989, Address:null:3000

參照註釋代碼和上述的結果,咱們能夠很容易的獲得一個觸發邏輯。
運行過程是:

  • 首先fork子進程

  • 觸發fork事件

  • 建立成功,觸發online事件

  • 而後從新執行一遍app.js,經過isWorker判斷子進程

  • 建立子進程服務->觸發master上的listening

st=>start: 首先fork子進程
op1=>operation: 觸發fork事件
op2=>operation: 建立成功,觸發online事件
op3=>operation: 而後從新執行一遍app.js,經過isWorker判斷子進程
op4=>operation: 建立子進程服務->觸發master上的listening
e=>end

st->op1->op2->op3->op4->e

上面只是建立滿負載子進程的流程。 但怎樣實現進程間的交互呢? 很簡單,master和worker監聽message事件,經過傳遞參數,進行交互。

  • cluster.worker.send(message[,handleFn]) master向worker發送信息

  • process.send(message[,handleFn]); worker向master發送信息

這個是多進程之間的通訊
communication
咱們來分解一下代碼塊:

//開啓master監聽worker的通訊
cluster.workers[id].on('message', function(msg){
          //...
        });
        
//開啓worker監聽master的通訊
process.on('message', function(msg) {
       //...
    });

運行上面的demo. 這裏就不細說,整個流程,只看一下信息通訊這一塊了。

  • 建立子進程,觸發listening事件

  • 使用process.on監聽message

  • 接受master發送過來的消息

  • 再向master返回消息

st=>start: 建立子進程,觸發listening事件
op1=>operation: 使用process.on監聽message
op2=>operation: 接受master發送過來的消息
op3=>operation: 再向master返回消息
op4=>operation: others
e=>others

st->op1->op2->op3->op4

nodeJS負載均衡

如今,nodeJS負載均衡應該是最容易實現的,其內部已經幫咱們封裝好了,咱們直接調用就over了。
其中,實現負載均衡的模塊就是cluster。之前cluster確實很累贅。負載均衡的算法實現的不是很好,致使的下場就是npm2的興起。不過如今已經實現了負載均衡,官方說法就是用round-robin,來進行請求分配。 round-robin其實就是一個隊列的循環,灰常容易理解。先看一下,cluster封裝好實現的負載均衡.

var cluster = require('cluster');
var http = require('http');
var numCPUs = require('os').cpus().length;

if (cluster.isMaster) {
    console.log('[master] ' + "start master...");

    for (var i = 0; i < numCPUs; i++) {
         cluster.fork();
    }

    cluster.on('listening', function (worker, address) {
        console.log('[master] ' + 'listening: worker' + worker.id + ',pid:' + worker.process.pid + ', Address:' + address.address + ":" + address.port);
    });

} else if (cluster.isWorker) {
     console.log('[worker] ' + "start worker ..." + cluster.worker.id);
    var num = 0;
    http.createServer(function (req, res) {
        num++;
        console.log('worker'+cluster.worker.id+":"+num);
        res.end('worker'+cluster.worker.id+',PID:'+process.pid);
    }).listen(3000);
}

(哥哥,你騙人,這哪裏實現了負載均衡,這不就是上面的算法麼?)
是呀,,, 我又沒說負載均衡不是這個。
負載均衡就是幫你解決請求的分配問題。ok~ 爲了證實,我沒有騙你,咱們來進行測試一下。
使用brew安裝siege測試,固然你也可使用其餘測試工具,不過在MAC 上面最好使用siege和webbench或者ab,我這裏使用siege

brew install siege

使用的測試語法就是

siege -c 併發數 -t 運行測試時間 URL

測試的時間後面須要帶上單位,好比s,m,h,d等。默認單位是m(分鐘). 舉個例子吧.

siege -c 100 -t 10s http://girls.hustonline.net

對女生節網頁進行 100次併發測試,持續時間是10s.
固然siege裏還有其餘的參數.

  • -c NUM 設置併發的數量.eg: -c 100; //設置100次併發

  • -r NUM 設置發送幾輪的請求,即,總的請求數爲: -cNum*-rNum可是, -r不能和-t一塊兒使用(爲何呢?你猜).eg: -r 20

  • -t NUM 測試持續時間,指你運行一次測試須要的時間,在timeout後,結束測試.

  • -f file. 用來測試file裏面的url路徑.file的尾綴須要爲.url. eg: -f girls.url.

  • -b . 就是詢問開不開啓基準測試(benchmark)。 這個參數不過重要,有興趣的同窗,能夠下去學習一下。

siege經常使用的就是這幾個. 一般咱們是搭配 -c + -r 或者-c + -t.
OK,如今咱們開始咱們的測試 procedure.
首先開啓多進程NodeJS. node app.js
使用siege -c 100 -t 10s 127.0.0.1:3000. (Ps: 固然也可使用http://localhost:3000進行代替)
獲得的結果爲

Transactions:                 600 hits
Availability:              100.00 %
Elapsed time:                6.08 secs
Data transferred:            0.01 MB
Response time:                0.01 secs
Transaction rate:           98.68 trans/sec
Throughput:                0.00 MB/sec
Concurrency:                0.88
Successful transactions:         600
Failed transactions:               0
Longest transaction:            0.04
Shortest transaction:            0.00

在10s內,發起了600次請求,最大的峯值是98.68 trans/sec。 經過統計分析,獲得每一個worker的分發量.

worker1:162
worker2:161
worker3:167
worker4:170

能夠看出,基本上每一個負載上分配的請求的數目都差很少。這就已經達到了負載均衡的效果。
下一篇會對nodeJS已經相關的測試工具作一些介紹哦。
盡請期待。
ending~

你們若是感興趣,給我一杯咖啡喝喝吧~

轉載請註明出處和做者
原文鏈接:http://www.javashuo.com/article/p-xiidelvz-g.html

相關文章
相關標籤/搜索