? ? 從一個同步式編程風格遷移到Node平臺(在Node架構中,持續(xù)傳遞風格和異步接口是常用方式),將是一件令人受挫的事情。編寫異步代碼可能是一種與眾不同的體驗,特別是設計到控制流的時候。一些簡單的問題,比如說:對一系列文件進行迭代、執(zhí)行連續(xù)的一系列任務、或者等待一系列執(zhí)行被完成,需要開發(fā)者采取新的方法或者技術來避免寫出低效或者可讀性差的代碼。一種常見的問題是陷入回調地域問題的陷阱,并且看到代碼規(guī)模橫向發(fā)展而不是縱向,伴隨著嵌套,使得即使是簡單的日常工作也變得難以維護和閱讀。
? ? 在這篇文章中,我們可以看到它實際上可能馴服回調,并且能夠通過設計模式和一些原則寫出干凈、可控制的異步代碼。我們將看到如何控制流庫,如異步,可以明顯簡化我們的問題,同時我們也能發(fā)現(xiàn)持續(xù)傳遞方式并不是唯一的方式來實現(xiàn)異步接口。事實上,我們將學會Promises和ECMAScript6中的generators如何成為有力而靈活的替代方案。對于每一個范例而言,我們將學習有助于幫助我們實現(xiàn)最常見控制流的模式,并且在最后一章,我們將準備有足夠的自信去編寫干凈并且高效的異步代碼的。
The difficulties of asynchronous programming
? ? 在JavaScript中,異步代碼失去控制,毫無疑問是十分容易的。閉包和就地定義匿名函數(shù)的方式允許一種平滑過渡的編程經(jīng)驗,而不需要開發(fā)者跳轉到代碼庫中的其他點。這完全吻合原理:它使代碼簡潔,保持代碼的流暢性,并且能在更短的時間內工作。不幸的是,犧牲代碼質量,比如說:模塊化、可重用性、可維護性,遲早會導致回調嵌套的失控擴散,函數(shù)規(guī)模的臃腫,并且導致糟糕的代碼組織。大多數(shù)時間下,創(chuàng)建閉包并不是函數(shù)功能的需要。所以它更像原則問題,而不是一個與異步編程相關的問題。意識到我們的代碼正在變得更丑陋或者更好,更進一步直到如何讓代碼不丑陋并采用合適的方式是區(qū)分新手和專家的標準。
Creating a simple web spider
? ? 為了解釋這個問題,我們將創(chuàng)造一個小小的網(wǎng)絡爬蟲,一個命令行應用程序,以URL作為輸入,并將其內容本地下載到一個文件當中。在本章提供的代碼中,我們將使用一些npm依賴:
?request:一個庫來精簡HTTP調用。
?mkdirp:一個小的實用應用程序來創(chuàng)建目錄遞歸。
? ? 此外,我們將經(jīng)常引用一個名為./utilities的本地模塊,其中包括一些我們將在應用程序中使用的輔助程序。我們?yōu)榱撕啙嵑雎晕募械膬热荩悄憧梢哉业酵暾膶崿F(xiàn),伴隨一個package.json包括完整的依賴列表,本書的包下載提供在:http://www.packtpub.com.。
? ? 我們的應用中的核心函數(shù)被包括在一個名為spider.js的模塊內部。我們來一起看看這段代碼是什么樣的。首先,我們先要下載我們將用到的所有依賴:
var request = require('request');
var fs = require('fs');
var mkdirp = require('mkdirp');
var path = require('path');
var utilities = require('./utilities');
? ? 接下來我將創(chuàng)造一個新的函數(shù)被稱為spider(),當下載過程完成時,將通過URL來下載并且回調callback函數(shù)將被調用。
function spider(url, callback) {
? ? var filename = utilities.urlToFilename(url);
? ? fs.exists(filename, function(exists) { ? ? //[1]
? ? ? ? if(!exists) {
? ? ? ? ? ? console.log("Downloading " + url);
? ? ? ? ? ? request(url, function(err, response, body) { ? ? //[2]
? ? ? ? ? ? ? ? if(err) {
? ? ? ? ? ? ? ? ? ?callback(err);
? ? ? ? ? ? ? ?} else {
? ? ? ? ? ? ? ? ? ?mkdirp(path.dirname(filename), function(err) { ? ? //[3]
? ? ? ? ? ? ? ? ? ? ? ?if(err) {
? ? ? ? ? ? ? ? ? ? ? ? ? callback(err);
? ? ? ? ? ? ? ? ? ? ? ?}?else {
? ? ? ? ? ? ? ? ? ? ? ? ? ?fs.writeFile(filename, body, function(err) { ? ? //[4]
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? if(err) {
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? callback(err);
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? } else {
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? callback(null, filename, true);
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?}
? ? ? ? ? ? ? ? ? ? ? ? ?});
? ? ? ? ? ? ? ? ? ? ?}
? ? ? ? ? ? ? ? ?});
? ? ? ? ? ? ?}
? ? ? ? ? });
? ? ? ? } else {
? ? ? ? ? ? ?callback(null, filename, false);
? ? ? ? }
? ? });
}
前述代碼將會執(zhí)行如下任務:
1.通過驗證相應的文件是否已經(jīng)完成創(chuàng)建,檢查URL是否已經(jīng)完成下載:
fs.exists(filecodename, function(exists) …
2.如果找不到文件,使用以下行代碼來下載URL:
request(url, function(err, response, body) …
3.然后,我們將確定包含該文件的目錄是否存在:
mkdirp(path.dirname(filename), function(err) …
4.最后我們將把HTTP響應的主體寫入文件系統(tǒng):
fs.writeFile(filename, body, function(err) …
? ? 為了完善我們的爬蟲應用程序,我們只需要通過提供一個URL作為輸入,來調用spider()(在我們的例子中,我們從命令行參數(shù)中讀取)。
spider(process.argv[2], function(err, filename, downloaded) {
? ? if(err) {
? ? ? ? console.log(err);
? ? } else if(downloaded){
? ? ? ? console.log('Completed the download of "'+ filename +'"');
? ? } else {
? ? ? ? console.log('"'+ filename +'" was already downloaded');
? ? }
});
? ? 現(xiàn)在,首先我們準備去試試我們的網(wǎng)絡爬蟲程序,首先,請確保你有utilities.js模塊,并且package.json包含你整個工程目錄下的全部依賴。接下來通過運行一下代碼來安裝所有的依賴,代碼如下:
npm install
? ? 接下,來我們將執(zhí)行spider模塊來下載網(wǎng)頁的內容,伴隨著如下命令行:
node spider http://www.example.com
? ? 我們的web爬蟲應用程序需要我們總是包括協(xié)議頭(比如:http://),在我們所提供的URL當中。另外不要期望HTML鏈接會被重寫或者圖片資源會被下載,因為這只是一個簡單的例子來演示異步編程如何工作。
Generators
? ? ES6中的Generator的引入,極大程度上改變了JavaScript程序員對迭代器的看法,并為解決callback hell提供了新方法。
? ? 迭代器模式是很常用的設計模式,但是實現(xiàn)起來,很多東西是程序化的;當?shù)?guī)則比較復雜時,維護迭代器內的狀態(tài),是比較麻煩的。 于是有了generator,何為generator?
Generators: a better way to build Iterators.
借助 yield 關鍵字,可以更優(yōu)雅的實現(xiàn)fibonacci數(shù)列:
function* fibonacci() {
? ? let a = 0, b = 1;
? ? while(true) {
? ? ? ? yield a; [a, b] = [b, a + b];
} }
yield與異步
? ? yield可以暫停運行流程,那么便為改變執(zhí)行流程提供了可能。這和Python的coroutine類似。
? ? Geneartor之所以可用來控制代碼流程,就是通過yield來將兩個或者多個Geneartor的執(zhí)行路徑互相切換。這種切換是語句級別的,而不是函數(shù)調用級別的。其本質是CPS變換。
? ? yield之后,實際上本次調用就結束了,控制權實際上已經(jīng)轉到了外部調用了generator的next方法的函數(shù),調用的過程中伴隨著狀態(tài)的改變。那么如果外部函數(shù)不繼續(xù)調用next方法,那么yield所在函數(shù)就相當于停在yield那里了。所以把異步的東西做完,要函數(shù)繼續(xù)執(zhí)行,只要在合適的地方再次調用generator 的next就行,就好像函數(shù)在暫停后,繼續(xù)執(zhí)行。
Buffer
? ? 在Node.js中,Buffer類是隨Node內核一起發(fā)布的核心庫。Buffer庫為Node.js帶來了一種存儲原始數(shù)據(jù)的方法,可以讓Nodejs處理二進制數(shù)據(jù),每當需要在Nodejs中處理I/O操作中移動的數(shù)據(jù)時,就有可能使用Buffer庫。原始數(shù)據(jù)存儲在 Buffer 類的實例中。一個 Buffer 類似于一個整數(shù)數(shù)組,但它對應于 V8 堆內存之外的一塊原始內存。
? ? Buffer 和 Javascript 字符串對象之間的轉換需要顯式地調用編碼方法來完成。以下是幾種不同的字符串編碼:
? ? ?‘a(chǎn)scii’ – 僅用于 7 位 ASCII 字符。這種編碼方法非常快,并且會丟棄高位數(shù)據(jù)。
? ? ?‘utf8’ – 多字節(jié)編碼的 Unicode 字符。許多網(wǎng)頁和其他文件格式使用 UTF-8。
? ? ?‘ucs2’ – 兩個字節(jié),以小尾字節(jié)序(little-endian)編碼的 Unicode 字符。它只能對 BMP(基本多文種平面,U+0000 – U+FFFF) 范圍內的字符編碼。
? ? ‘base64’ – Base64 字符串編碼。
? ? ‘binary’ – 一種將原始二進制數(shù)據(jù)轉換成字符串的編碼方式,僅使用每個字符的前 8 位。這種編碼方法已經(jīng)過時,應當盡可能地使用 Buffer 對象。
? ? 'hex' - 每個字節(jié)都采用 2 進制編碼。
? ? 在Buffer中創(chuàng)建一個數(shù)組,需要注意以下規(guī)則:
? ? Buffer 是內存拷貝,而不是內存共享。
? ? Buffer 占用內存被解釋為一個數(shù)組,而不是字節(jié)數(shù)組。比如,new Uint32Array(new Buffer([1,2,3,4])) 創(chuàng)建了4個 Uint32Array,它的成員為 [1,2,3,4] ,而不是[0x1020304] 或 [0x4030201]。
slab 分配
? ? 在 lib/buffer.js 模塊中,有個模塊私有變量 pool, 它指向當前的一個8K 的slab :
Buffer.poolSize = 8 * 1024;
var pool;
function allocPool() {
? ? pool = new SlowBuffer(Buffer.poolSize);
? ? pool.used = 0;
}
SlowBuffer 為 src/node_buffer.cc 導出,當用戶調用new Buffer時 ,如果你要申請的空間大于8K,node 會直接調用SlowBuffer ,如果小于8K ,新的Buffer 會建立在當前slab 之上:
? ? 新創(chuàng)建的Buffer的 parent成員變量會指向這個slab ,
? ? offset 變量指向在這個slab 中的偏移:
if (!pool || pool.length - pool.used < this.length) allocPool();
this.parent = pool;
this.offset = pool.used;
pool.used += this.length;
在 lib/_tls_legacy.js 中,SlabBuffer創(chuàng)建了一個 10MB 的 slab:
function alignPool() {
? ? // Ensure aligned slices
? ?if (poolOffset & 0x7) {
? ? ? ?poolOffset |= 0x7; poolOffset++;
? ?}
}
? ? 這里做了8字節(jié)的內存對齊處理。
? ? 如果不按照平臺要求對數(shù)據(jù)存放進行對齊,會帶來存取效率上的損失。比如32位的Intel處理器通過總線訪問(包括讀和寫)內存數(shù)據(jù)。每個總線周期從偶地址開始訪問32位內存數(shù)據(jù),內存數(shù)據(jù)以字節(jié)為單位存放。如果一個32位的數(shù)據(jù)沒有存放在4字節(jié)整除的內存地址處,那么處理器就需要2個總線周期對其進行訪問,顯然訪問效率下降很多。
? ? Node.js 是一個跨平臺的語言,第三方的C++ addon 也是非常多,避免破壞了第三方模塊的使用,比如 directIO 就必須要內存對齊。
淺拷貝
? ? Buffer更像是可以做指針操作的C語言數(shù)組。例如,可以用[index]方式直接修改某個位置的字節(jié)。 需要注意的是:Buffer#slice 方法, 不是返回一個新的Buffer, 而是返回對原 Buffer 某個區(qū)間數(shù)值的引用。
const buf1 = Buffer.allocUnsafe(26);
for (var i = 0 ; i < 26 ; i++) {
? ? buf1[i] = i + 97; // 97 is ASCII a
}
const buf2 = buf1.slice(0, 3);
buf2.toString('ascii', 0, buf2.length);
? ? // Returns: 'abc'
buf1[0] = 33;
buf2.toString('ascii', 0, buf2.length);
? ? // Returns : '!bc'
官方 API 提供的例子,buf2是對buf1前3個字節(jié)的引用,對buf2的修改就相當于作用在buf1上。
深拷貝
? ? 如果想要拷貝一份Buffer,得首先創(chuàng)建一個新的Buffer,并通過.copy方法把原Buffer中的數(shù)據(jù)復制過去。
const buf1 = Buffer.allocUnsafe(26);
const buf2 = Buffer.allocUnsafe(26).fill('!');
for (let i = 0 ; i < 26 ; i++) {
? ? buf1[i] = i + 97; // 97 is ASCII a
}
buf1.copy(buf2, 8, 16, 20);
console.log(buf2.toString('ascii', 0, 25));
? ? // Prints: !!!!!!!!qrst!!!!!!!!!!!!!
? ? 通過深拷貝的方式,buf2截取了buf1的部分內容,之后對buf2的修改并不會作用于buf1, 兩者內容獨立不共享。需要注意的事:深拷貝是一種消耗 CPU 和內存的操作,需要非常謹慎。
內存碎片
? ? 動態(tài)分配將不可避免會產(chǎn)生內存碎片的問題,那么什么是內存碎片? 內存碎片即“碎片的內存”描述一個系統(tǒng)中所有不可用的空閑內存,這些碎片之所以不能被使用,是因為負責動態(tài)分配內存的分配算法使得這些空閑的內存無法使用。
? ? 上述的 slab 分配,存在明顯的內存碎片,即 8KB 的內存并沒有完全被使用,存在一定的浪費。通用的slab實現(xiàn),會浪費約1/2的空間。
? ? 當然存在更高效,更省內存的內存管理分配,比如 tcmalloc, 但也必須承受一定的管理代價。node.js 在這方面并沒有一味的執(zhí)著于此,而是達到一種性能與空間使用的平衡。
PM2源碼淺析
? ? 近年來,大前端和全棧的思潮下,很多公司的項目轉成了node驅動,pm2做為一個帶有負載均衡功能的進程管理器,是眾多公司的主流方案。
PM2工作原理:
? ? 要理解pm2就要理解god和santan的關系:god的職責是守護進程,重啟進程。santan的職責是異常進程的退出,殺死進程,毀滅進程等工作。
god和santan通訊的方式,是RPC:
RPC(Remote Procedure Call Protocol)——遠程過程調用協(xié)議,它是一種通過網(wǎng)絡從遠程計算機程序上請求服務,而不需要了解底層網(wǎng)絡技術的協(xié)議。RPC協(xié)議假定某些傳輸協(xié)議的存在,如TCP或UDP,為通信程序之間攜帶信息數(shù)據(jù)。在OSI網(wǎng)絡通信模型中,RPC跨越了傳輸層和應用層。RPC使得開發(fā)包括網(wǎng)絡分布式多程序在內的應用程序更加容易。
總結
? ? pm2的集群,從原理是采用cluster.fork來實現(xiàn)的,深入理解cluser模塊,精度pm2的源代碼,能更好的理解pm2,更好的理解node設計思想
ES7+ES8新趨勢與異步處理:
ES7新特性:
Array.prototype.includes()方法:
? ? includes()的作用,是查找一個值在不在數(shù)組里,若在,則返回true,反之返回false。 基本用法:
['a', 'b', 'c'].includes('a') ? ? // true
['a', 'b', 'c'].includes('d') ? ? // false
Array.prototype.includes()方法接收兩個參數(shù):要搜索的值和搜索的開始索引。當?shù)诙€參數(shù)被傳入時,該方法會從索引處開始往后搜索(默認索引值為0)。若搜索值在數(shù)組中存在則返回true,否則返回false。 且看下面示例:
['a', 'b', 'c', 'd'].includes('b') ? ? ? ? // true
['a', 'b', 'c', 'd'].includes('b', 1) ? ? ?// true
['a', 'b', 'c', 'd'].includes('b', 2) ? ? ?// false
那么,我們會聯(lián)想到ES6里數(shù)組的另一個方法indexOf,下面的示例代碼是等效的:
['a', 'b', 'c'].includes('a') ? ? ? ? ?//true
['a', 'b', 'c'].indexOf('a') > -1 ? ? ?//true
*異步函數(shù)(Async functions)
為什么要引入async?
? ? 眾所周知,JavaScript語言的執(zhí)行環(huán)境是“單線程”的,那么異步編程對JavaScript語言來說就顯得尤為重要。以前我們大多數(shù)的做法是使用回調函數(shù)來實現(xiàn)JavaScript語言的異步編程。回調函數(shù)本身沒有問題,但如果出現(xiàn)多個回調函數(shù)嵌套,例如:進入某個頁面,需要先登錄,拿到用戶信息之后,調取用戶商品信息,代碼如下:
this.$http.jsonp('/login', (res) => {
? ? ? this.$http.jsonp('/getInfo', (info) => {
? ? ? ? ? // do something
? ? ? })
})
? ? 假如上面還有更多的請求操作,就會出現(xiàn)多重嵌套。代碼很快就會亂成一團,這種情況就被稱為“回調函數(shù)地獄”(callback hell)。
? ? 于是,我們提出了Promise,它將回調函數(shù)的嵌套,改成了鏈式調用。寫法如下:
var promise = new Promise((resolve, reject) => { ?
? ? this.login(resolve)
})
.then(() => this.getInfo())
.catch(() => { console.log("Error") })
? ? 從上面可以看出,Promise的寫法只是回調函數(shù)的改進,使用then方法,只是讓異步任務的兩段執(zhí)行更清楚而已。Promise的最大問題是代碼冗余,請求任務多時,一堆的then,也使得原來的語義變得很不清楚。此時我們引入了另外一種異步編程的機制:Generator。
? ? Generator 函數(shù)是一個普通函數(shù),但是有兩個特征。一是,function關鍵字與函數(shù)名之間有一個星號;二是,函數(shù)體內部使用yield表達式,定義不同的內部狀態(tài)(yield在英語里的意思就是“產(chǎn)出”)。一個簡單的例子用來說明它的用法:
function* helloWorldGenerator() {
? ? yield 'hello';
? ? yield 'world'; ?
? ? return 'ending';
}
var hw = helloWorldGenerator();
? ? 上面代碼定義了一個 Generator 函數(shù)helloWorldGenerator,它內部有兩個yield表達式(hello和world),即該函數(shù)有三個狀態(tài):hello,world 和 return 語句(結束執(zhí)行)。Generator 函數(shù)的調用方法與普通函數(shù)一樣,也是在函數(shù)名后面加上一對圓括號。不同的是,調用 Generator 函數(shù)后,該函數(shù)并不執(zhí)行,返回的也不是函數(shù)運行結果,而是一個指向內部狀態(tài)的指針對象,必須調用遍歷器對象的next方法,使得指針移向下一個狀態(tài)。也就是說,每次調用next方法,內部指針就從函數(shù)頭部或上一次停下來的地方開始執(zhí)行,直到遇到下一個yield表達式(或return語句)為止。換言之,Generator 函數(shù)是分段執(zhí)行的,yield表達式是暫停執(zhí)行的標記,而next方法可以恢復執(zhí)行。上述代碼分步執(zhí)行如下:
hw.next()
// { value: 'hello', done: false }
hw.next()
// { value: 'world', done: false }
hw.next()
// { value: 'ending', done: true }
hw.next()
// { value: undefined, done: true }
? ? Generator函數(shù)的機制更符合我們理解的異步編程思想。
? ? 用戶登錄的例子,我們用Generator來寫,如下:
var gen = function* () { ?
? ? const f1 = yield this.login()
? ? const f2 = yield this.getInfo()
};
? ? 雖然Generator將異步操作表示得很簡潔,但是流程管理卻不方便(即何時執(zhí)行第一階段、何時執(zhí)行第二階段)。此時,我們便希望能出現(xiàn)一種能自動執(zhí)行Generator函數(shù)的方法。我們的主角來了:async/await。
ES8引入了async函數(shù),使得異步操作變得更加方便。簡單說來,它就是Generator函數(shù)的語法糖。
async function asyncFunc(params) { ?
? ? const result1 = await this.login() ?
? ? const result2 = await this.getInfo()
}
更加簡潔易懂
變體,異步函數(shù)存在以下四種使用形式:
? ? 函數(shù)聲明:asyncfunctionfoo(){}
? ? 函數(shù)表達式:constfoo=asyncfunction(){}
? ? 對象的方式:letobj={asyncfoo(){}}
? ? 箭頭函數(shù):constfoo=async()=>{}
常見用法匯總:
處理單個異步結果:
async function asyncFunc() {
? ? const result = await otherAsyncFunc();
? ? ?console.log(result);
}
順序處理多個異步結果:
async function asyncFunc() { ?
? ? const result1 = await otherAsyncFunc1(); ?
? ? console.log(result1); ?
? ? const result2 = await otherAsyncFunc2();
? ? console.log(result2);
}
并行處理多個異步結果:
async function asyncFunc() { ?
? ? const [result1, result2] = await Promise.all([ ?
? ? ? ? otherAsyncFunc1(), ? ?otherAsyncFunc2()
? ? ]); ?
? ?console.log(result1, result2);
}
處理錯誤:
async function asyncFunc() { ?
? ? try { ? ?
? ? ? ?await otherAsyncFunc(); ?
? ?} catch (err) {
? ?console.error(err); ?
? }
}
網(wǎng)絡 (Net)?
網(wǎng)絡模型
? ? ISO制定的OSI參考模型的過于龐大、復雜招致了許多批評。與此對照,由技術人員自己開發(fā)的TCP/ IP協(xié)議棧獲得了更為廣泛的應用。如圖所示,是TCP/IP參考模型和OSI參考模型的對比示意圖。
UDP vs TCP
? ? TCP(Transmission Control Protocol):傳輸控制協(xié)議
? ? UDP(User Datagram Protocol):用戶數(shù)據(jù)報協(xié)議
? ? 主要在于連接性(Connectivity)、可靠性(Reliability)、有序性(Ordering)、有界性(Boundary)、擁塞控制(Congestion or Flow control)、傳輸速度(Speed)、量級(Heavy/Light weight)、頭部大小(Header size)等差異。
主要差異:
1.TCP是面向連接(Connection oriented)的協(xié)議,UDP是無連接(Connection less)協(xié)議;
? ? TCP用三次握手建立連接:1) Client向server發(fā)送SYN;2) Server接收到SYN,回復Client一個SYN-ACK;3)Client接收到SYN_ACK,回復Server一個ACK。到此,連接建成。
? ? UDP發(fā)送數(shù)據(jù)前不需要建立連接。
2.TCP可靠,UDP不可靠;
? ? TCP丟包會自動重傳,UDP不會。
3.TCP有序,UDP無序;
? ? 消息在傳輸過程中可能會亂序,后發(fā)送的消息可能會先到達,TCP會對其進行重排序,UDP不會。從程序實現(xiàn)的角度來看,可以用下圖來進行描述。
? ? 從上圖也能清晰的看出,TCP通信需要服務器端偵聽listen、接收客戶端連接請求accept,等待客戶端connect建立連接后才能進行數(shù)據(jù)包的收發(fā)(recv/send)工作。而UDP則服務器和客戶端的概念不明顯,服務器端即接收端需要綁定端口,等待客戶端的數(shù)據(jù)的到來。后續(xù)便可以進行數(shù)據(jù)的收發(fā)(recvfrom/sendto)工作。
Socket 抽象
? ? Socket 是對 TCP/IP 協(xié)議族的一種封裝,是應用層與TCP/IP協(xié)議族通信的中間軟件抽象層。它把復雜的TCP/IP協(xié)議族隱藏在Socket接口后面,對用戶來說,一組簡單的接口就是全部,讓Socket去組織數(shù)據(jù),以符合指定的協(xié)議。
? ? Socket 還可以認為是一種網(wǎng)絡間不同計算機上的進程通信的一種方法,利用三元組(ip地址,協(xié)議,端口)就可以唯一標識網(wǎng)絡中的進程,網(wǎng)絡中的進程通信可以利用這個標志與其它進程進行交互。
? ? Socket 起源于 Unix ,Unix/Linux 基本哲學之一就是“一切皆文件”,都可以用“打開(open) –> 讀寫(write/read) –> 關閉(close)”模式來進行操作。因此 Socket 也被處理為一種特殊的文件。
TCP Socket
? ? Node.js 的 Net模塊也對 TCP socket 進行了抽象封裝:
function Socket(options) {
? ? if (!(this instanceof Socket)) return new Socket(options);
? ? this._connecting = false;
? ? this._hadError = false;
? ? this._handle = null;
? ? this._parent = null;
? ? this._host = null;
? ? if (typeof options === 'number')
? ? ? ? options = { fd: options }; // Legacy interface.
? ? else if (options === undefined)
? ? ? ? options = {};
? ? stream.Duplex.call(this, options);
? ? if (options.handle) {
? ? ? ? this._handle = options.handle; // private
? ? } else if (options.fd !== undefined) {
? ? ? ? this._handle = createHandle(options.fd);
? ? ? ? this._handle.open(options.fd);
? ? ? ? if ((options.fd == 1 || options.fd == 2) &&
? ? ? ? ? ?(this._handle instanceof Pipe) &&
? ? ? ? ? ?process.platform === 'win32') {
? ? ? ? ? ?// Make stdout and stderr blocking on Windows
? ? ? ? ? var err = this._handle.setBlocking(true);
? ? ? ? ? if (err)
? ? ? ? ? ? ? throw errnoException(err, 'setBlocking');
? ? ? ? ? }
? ? ? ? ?this.readable = options.readable !== false;
? ? ? ? ?this.writable = options.writable !== false;
? ? ? ?} else {
? ? ? ? ? // these will be set once there is a connection
? ? ? ? ? this.readable = this.writable = false;
? ? ? }
? ? ?// shut down the socket when we're finished with it.
? ? ?this.on('finish', onSocketFinish);
? ? ?this.on('_socketEnd', onSocketEnd);
? ? ?initSocketHandle(this);
? ? ?// ...
}
util.inherits(Socket, stream.Duplex);
? ? 首先Socket是一個全雙工的 Stream,所以繼承了 Duplex。通過createHandle創(chuàng)建套接字并賦值到this._handle上。同時監(jiān)聽finish,_socketEnd事件:
粘包
一般所謂的TCP粘包是在一次接收數(shù)據(jù)不能完全地體現(xiàn)一個完整的消息數(shù)據(jù)。TCP通訊為何存在粘包呢?主要原因是TCP是以流的方式來處理數(shù)據(jù),再加上網(wǎng)絡上MTU的往往小于在應用處理的消息數(shù)據(jù),所以就會引發(fā)一次接收的數(shù)據(jù)無法滿足消息的需要,導致粘包的存在。處理粘包的唯一方法就是制定應用層的數(shù)據(jù)通訊協(xié)議,通過協(xié)議來規(guī)范現(xiàn)有接收的數(shù)據(jù)是否滿足消息數(shù)據(jù)的需要。
情況分析
? ? TCP粘包通常在流傳輸中出現(xiàn),UDP則不會出現(xiàn)粘包,因為UDP有消息邊界,發(fā)送數(shù)據(jù)段需要等待緩沖區(qū)滿了才將數(shù)據(jù)發(fā)送出去,當滿的時候有可能不是一條消息而是幾條消息合并在換中去內,在成粘包;另外接收數(shù)據(jù)端沒能及時接收緩沖區(qū)的包,造成了緩沖區(qū)多包合并接收,也是粘包。
解決辦法
? ? 自定義應用層協(xié)議;
? ? 不使用Nagle算法, 使用提供的 API:socket.setNoDelay。
UDP
UDP Socket:
function Socket(type, listener) {
? ? EventEmitter.call(this);
? ? if (typeof type === 'object') {
? ? ? ? var options = type;
? ? ? ? type = options.type;
? ? }
? ? var handle = newHandle(type);
? ? handle.owner = this;
? ? this._handle = handle;
? ? this._receiving = false;
? ? this._bindState = BIND_STATE_UNBOUND;
? ? this.type = type; this.fd = null; // compatibility hack
? ? // If true - UV_UDP_REUSEADDR flag will be set
? ? this._reuseAddr = options && options.reuseAddr;
? ? if (typeof listener === 'function')
? ? ? ? this.on('message', listener);
}
util.inherits(Socket, EventEmitter);
? ? UDP 繼承了EventEmitter, 同樣也支持 IPV4和 IPV6協(xié)議, 由type區(qū)分,this._reuseAddr標識是否要使用選項:SO_REUSEADDR。
? ? SO_REUSEADDR允許完全重復的捆綁:當一個IP地址和端口綁定到某個套接口上時,還允許此IP地址和端口捆綁到另一個套接口上。一般來說,這個特性僅在支持多播的系統(tǒng)上才有,而且只對UDP套接口而言(TCP不支持多播)。
總結
? ? 盡量不要嘗試去使用UDP,除非知道丟包了對于應用是沒有影響的,否則排查網(wǎng)絡丟包會很困難。