{{ v.name }}
{{ v.cls }}類
{{ v.price }} ¥{{ v.price }}
目前,我們處于互聯(lián)網(wǎng)時(shí)代,互聯(lián)網(wǎng)產(chǎn)品百花齊放。例如,當(dāng)打開瀏覽器,可以看到各種信息,瀏覽器是如何跟服務(wù)器進(jìn)行通信的?當(dāng)打開微信跟朋友聊天時(shí),你是如何跟朋友進(jìn)行消息傳遞的?這些都得靠網(wǎng)絡(luò)進(jìn)程之間的通信,都得依賴于socket。那什么是socket?node中有哪些跟網(wǎng)絡(luò)通信有關(guān)的模塊?這些問題是本文研究的重點(diǎn)。
1. Socket
Socket源于Unix,而Unix的基本哲學(xué)是『一些皆文件』,都可以用『打開open ==> 讀/寫(read/write) ==> 關(guān)閉(close)』模式來操作,Socket也可以采用這種方法進(jìn)行理解。關(guān)于Socket,可以總結(jié)如下幾點(diǎn):
2. node中網(wǎng)絡(luò)通信的架構(gòu)實(shí)現(xiàn)
node中的模塊,從兩種語言實(shí)現(xiàn)角度來說,存在javscript、c++兩部分,通過 process.binding 來建立關(guān)系。具體分析如下:
3. net使用
net模塊 是基于TCP協(xié)議的socket網(wǎng)路編程模塊,http模塊就是建立在該模塊的基礎(chǔ)上實(shí)現(xiàn)的,先來看看基本使用方法:
// 創(chuàng)建socket服務(wù)器 server.js const net = require('net') const server = net.createServer(); server.on('connection', (socket) => { socket.pipe(process.stdout); socket.write('data from server'); }); server.listen(3000, () => { console.log(`server is on ${JSON.stringify(server.address())}`); }); // 創(chuàng)建socket客戶端 client.js const net = require('net'); const client = net.connect({port: 3000}); client.on('connect', () => { client.write('data from client'); }); client.on('data', (chunk) => { console.log(chunk.toString()); client.end(); }); // 打開兩個(gè)終端,分別執(zhí)行`node server.js`、`node client.js`,可以看到客戶端與服務(wù)器進(jìn)行了數(shù)據(jù)通信。
使用 const server = net.createServer(); 創(chuàng)建了server對(duì)象,那server對(duì)象有哪些特點(diǎn):
// net.js exports.createServer = function(options, connectionListener) { return new Server(options, connectionListener); }; function Server(options, connectionListener) { EventEmitter.call(this); ... if (typeof connectionListener === 'function') { this.on('connection', connectionListener); } ... this._handle = null; } util.inherits(Server, EventEmitter);
上述代碼可以分為幾個(gè)點(diǎn):
再來看看connectionListener事件的回調(diào)函數(shù),里面包含一個(gè) socket 對(duì)象,該對(duì)象是一個(gè)連接套接字,是個(gè)五元組(server_host、server_ip、protocol、client_host、client_ip),相關(guān)實(shí)現(xiàn)如下:
function onconnection(err, clientHandle) { ... var socket = new Socket({ ... }); ... self.emit('connection', socket); }
因?yàn)镾ocket是繼承了 stream.Duplex ,所以Socket也是一個(gè)可讀可寫流,可以使用流的方法進(jìn)行數(shù)據(jù)的處理。
接下來就是很關(guān)鍵的端口監(jiān)聽(port),這是server與client的主要區(qū)別,代碼:
Server.prototype.listen = function() { ... listen(self, ip, port, addressType, backlog, fd, exclusive); ... } function listen(self, address, port, addressType, backlog, fd, exclusive) { ... if (!cluster) cluster = require('cluster'); if (cluster.isMaster || exclusive) { self._listen2(address, port, addressType, backlog, fd); return; } cluster._getServer(self, { ... }, cb); function cb(err, handle) { ... self._handle = handle; self._listen2(address, port, addressType, backlog, fd); ... } } Server.prototype._listen2 = function(address, port, addressType, backlog, fd) { if (this._handle) { ... } else { ... rval = createServerHandle(address, port, addressType, fd); ... this._handle = rval; } this._handle.onconnection = onconnection; var err = _listen(this._handle, backlog); ... } function _listen(handle, backlog) { return handle.listen(backlog || 511); }
上述代碼有幾個(gè)點(diǎn)需要注意:
接下來分析下listen中最重要的 _handle 了,_handle決定了server的功能:
function createServerHandle(address, port, addressType, fd) { ... if (typeof fd === 'number' && fd >= 0) { ... handle = createHandle(fd); ... } else if(port === -1 && addressType === -1){ handle = new Pipe(); } else { handle = new TCP(); } ... return handle; } function createHandle(fd) { var type = TTYWrap.guessHandleType(fd); if (type === 'PIPE') return new Pipe(); if (type === 'TCP') return new TCP(); throw new TypeError('Unsupported fd type: ' + type); }
_handle 由C++中的Pipe、TCP實(shí)現(xiàn),因而要想完全搞清楚node中的網(wǎng)絡(luò)通信,必須深入到V8的源碼里面。
4. UDP/dgram使用
跟net模塊相比,基于UDP通信的dgram模塊就簡(jiǎn)單了很多,因?yàn)椴恍枰ㄟ^三次握手建立連接,所以整個(gè)通信的過程就簡(jiǎn)單了很多,對(duì)于數(shù)據(jù)準(zhǔn)確性要求不太高的業(yè)務(wù)場(chǎng)景,可以使用該模塊完成數(shù)據(jù)的通信。
// server端實(shí)現(xiàn) const dgram = require('dgram'); const server = dgram.createSocket('udp4'); server.on('message', (msg, addressInfo) => { console.log(addressInfo); console.log(msg.toString()); const data = Buffer.from('from server'); server.send(data, addressInfo.port); }); server.bind(3000, () => { console.log('server is on ', server.address()); }); // client端實(shí)現(xiàn) const dgram = require('dgram'); const client = dgram.createSocket('udp4'); const data = Buffer.from('from client'); client.send(data, 3000); client.on('message', (msg, addressInfo) => { console.log(addressInfo); console.log(msg.toString()); client.close(); });
從源碼層面分析上述代碼的原理實(shí)現(xiàn):
exports.createSocket = function(type, listener) { return new Socket(type, listener); }; function Socket(type, listener) { ... var handle = newHandle(type); this._handle = handle; ... this.on('message', listener); ... } util.inherits(Socket, EventEmitter); const UDP = process.binding('udp_wrap').UDP; function newHandle(type) { if (type == 'udp4') { const handle = new UDP(); handle.lookup = lookup4; return handle; } if (type == 'udp6') { const handle = new UDP(); handle.lookup = lookup6; handle.bind = handle.bind6; handle.send = handle.send6; return handle; } ... } Socket.prototype.bind = function(port_ /*, address, callback*/) { ... startListening(self); ... } function startListening(socket) { socket._handle.onmessage = onMessage; socket._handle.recvStart(); ... } function onMessage(nread, handle, buf, rinfo) { ... self.emit('message', buf, rinfo); ... } Socket.prototype.send = function(buffer, offset, length, port, address, callback) { ... self._handle.lookup(address, function afterDns(ex, ip) { doSend(ex, self, ip, list, address, port, callback); }); } const SendWrap = process.binding('udp_wrap').SendWrap; function doSend(ex, self, ip, list, address, port, callback) { ... var req = new SendWrap(); ... var err = self._handle.send(req, list, list.length, port, ip, !!callback); ... }
上述代碼存在幾個(gè)點(diǎn)需要注意:
5. DNS使用
DNS(Domain Name System)用于域名解析,也就是找到host對(duì)應(yīng)的ip地址,在計(jì)算機(jī)網(wǎng)絡(luò)中,這個(gè)工作是由網(wǎng)絡(luò)層的ARP協(xié)議實(shí)現(xiàn)。在node中存在 net 模塊來完成相應(yīng)功能,其中dns里面的函數(shù)分為兩類:
依賴底層操作系統(tǒng)實(shí)現(xiàn)域名解析,也就是我們?nèi)粘i_發(fā)中,域名的解析規(guī)則,可以回使用瀏覽器緩存、本地緩存、路由器緩存、dns服務(wù)器,該類僅有 dns.lookup
該類的dns解析,直接到nds服務(wù)器執(zhí)行域名解析
const dns = require('dns'); const host = 'bj.meituan.com'; dns.lookup(host, (err, address, family) => { if (err) { console.log(err); return; } console.log('by net.lookup, address is: %s, family is: %s', address, family); }); dns.resolve(host, (err, address) => { if (err) { console.log(err); return; } console.log('by net.resolve, address is: %s', address); }) // by net.resolve, address is: 103.37.152.41 // by net.lookup, address is: 103.37.152.41, family is: 4
在這種情況下,二者解析的結(jié)果是一樣的,但是假如我們修改本地的/etc/hosts文件呢
// 在/etc/host文件中,增加: 10.10.10.0 bj.meituan.com // 然后再執(zhí)行上述文件,結(jié)果是: by net.resolve, address is: 103.37.152.41 by net.lookup, address is: 10.10.10.0, family is: 4
接下來分析下dns的內(nèi)部實(shí)現(xiàn):
const cares = process.binding('cares_wrap'); const GetAddrInfoReqWrap = cares.GetAddrInfoReqWrap; exports.lookup = function lookup(hostname, options, callback) { ... callback = makeAsync(callback); ... var req = new GetAddrInfoReqWrap(); req.callback = callback; var err = cares.getaddrinfo(req, hostname, family, hints); ... } function resolver(bindingName) { var binding = cares[bindingName]; return function query(name, callback) { ... callback = makeAsync(callback); var req = new QueryReqWrap(); req.callback = callback; var err = binding(req, name); ... return req; } } var resolveMap = Object.create(null); exports.resolve4 = resolveMap.A = resolver('queryA'); exports.resolve6 = resolveMap.AAAA = resolver('queryAaaa'); ... exports.resolve = function(hostname, type_, callback_) { ... resolver = resolveMap[type_]; return resolver(hostname, callback); ... }
上面的源碼有幾個(gè)點(diǎn)需要關(guān)注:
6. HTTP使用
在WEB開發(fā)中,HTTP作為***、最重要的應(yīng)用層,是每個(gè)開發(fā)人員應(yīng)該熟知的基礎(chǔ)知識(shí),我面試的時(shí)候必問的一塊內(nèi)容。同時(shí),大多數(shù)同學(xué)接觸node時(shí),首先使用的恐怕就是http模塊。先來一個(gè)簡(jiǎn)單的demo看看:
const http = require('http'); const server = http.createServer(); server.on('request', (req, res) => { res.setHeader('foo', 'test'); res.writeHead(200, { 'Content-Type': 'text/html', }); res.write(''); res.end(``); }); server.listen(3000, () => { console.log('server is on ', server.address()); var req = http.request({ host: '127.0.0.1', port: 3000}); req.on('response', (res) => { res.on('data', (chunk) => console.log('data from server ', chunk.toString()) ); res.on('end', () => server.close() ); }); req.end(); }); // 輸出結(jié)果如下: // server is on { address: '::', family: 'IPv6', port: 3000 } // data from server 頭
針對(duì)上述demo,有很多值得深究的地方,一不注意服務(wù)就掛掉了,下面根據(jù)node的 官方文檔 ,逐個(gè)進(jìn)行研究。
6.1 http.Agent
因?yàn)镠TTP協(xié)議是無狀態(tài)協(xié)議,每個(gè)請(qǐng)求均需通過三次握手建立連接進(jìn)行通信,眾所周知三次握手、慢啟動(dòng)算法、四次揮手等過程很消耗時(shí)間,因此HTTP1.1協(xié)議引入了keep-alive來避免頻繁的連接。那么對(duì)于tcp連接該如何管理呢?http.Agent就是做這個(gè)工作的。先看看源碼中的關(guān)鍵部分:
function Agent(options) { ... EventEmitter.call(this); ... self.maxSockets = self.options.maxSockets || Agent.defaultMaxSockets; self.maxFreeSockets = self.options.maxFreeSockets || 256; ... self.requests = {}; // 請(qǐng)求隊(duì)列 self.sockets = {}; // 正在使用的tcp連接池 self.freeSockets = {}; // 空閑的連接池 self.on('free', function(socket, options) { ... // requests、sockets、freeSockets的讀寫操作 self.requests[name].shift().onSocket(socket); freeSockets.push(socket); ... } } Agent.defaultMaxSockets = Infinity; util.inherits(Agent, EventEmitter); // 關(guān)于socket的相關(guān)增刪改查操作 Agent.prototype.addRequest = function(req, options) { ... if (freeLen) { var socket = this.freeSockets[name].shift(); ... this.sockets[name].push(socket); ... } else if (sockLen < this.maxSockets) { ... } else { this.requests[name].push(req); } ... } Agent.prototype.createSocket = function(req, options, cb) { ... } Agent.prototype.removeSocket = function(s, options) { ... } exports.globalAgent = new Agent();
上述代碼有幾個(gè)點(diǎn)需要注意:
下面可以測(cè)試下agent對(duì)請(qǐng)求本身的限制:
// req.js const http = require('http'); const server = http.createServer(); server.on('request', (req, res) => { var i=1; setTimeout(() => { res.end('ok ', i++); }, 1000) }); server.listen(3000, () => { var max = 20; for(var i=0; i var req = http.request({ host: '127.0.0.1', port: 3000}); req.on('response', (res) => { res.on('data', (chunk) => console.log('data from server ', chunk.toString()) ); res.on('end', () => server.close() ); }); req.end(); } }); // 在終端中執(zhí)行time node ./req.js,結(jié)果為: // real 0m1.123s // user 0m0.102s // sys 0m0.024s // 在req.js中添加下面代碼 http.globalAgent.maxSockets = 5; // 然后同樣time node ./req.js,結(jié)果為: real 0m4.141s user 0m0.103s sys 0m0.024s
當(dāng)設(shè)置maxSockets為某個(gè)值時(shí),tcp的連接就會(huì)被限制在某個(gè)值,剩余的請(qǐng)求就會(huì)進(jìn)入 requests 隊(duì)列里面,等有空余的socket連接后,從request隊(duì)列中出棧,發(fā)送請(qǐng)求。
6.2 http.ClientRequest
當(dāng)執(zhí)行http.request時(shí),會(huì)生成ClientRequest對(duì)象,該對(duì)象雖然沒有直接繼承Stream.Writable,但是繼承了http.OutgoingMessage,而http.OutgoingMessage實(shí)現(xiàn)了write、end方法,因?yàn)榭梢援?dāng)跟stream.Writable一樣的使用。
var req = http.request({ host: '127.0.0.1', port: 3000, method: 'post'}); req.on('response', (res) => { res.on('data', (chunk) => console.log('data from server ', chunk.toString()) ); res.on('end', () => server.close() ); }); // 直接使用pipe,在request請(qǐng)求中添加數(shù)據(jù) fs.createReadStream('./data.json').pipe(req);
接下來,看看http.ClientRequest的實(shí)現(xiàn), ClientRequest繼承了OutgoingMessage:
const OutgoingMessage = require('_http_outgoing').OutgoingMessage; function ClientRequest(options, cb) { ... OutgoingMessage.call(self); ... } util.inherits(ClientRequest, OutgoingMessage);
6.3 http.Server
http.createServer其實(shí)就是創(chuàng)建了一個(gè)http.Server對(duì)象,關(guān)鍵源碼如下:
exports.createServer = function(requestListener) { return new Server(requestListener); }; function Server(requestListener) { ... net.Server.call(this, { allowHalfOpen: true }); if (requestListener) { this.addListener('request', requestListener); } ... this.addListener('connection', connectionListener); this.timeout = 2 * 60 * 1000; ... } util.inherits(Server, net.Server); function connectionListener(socket) { ... socket.on('end', socketOnEnd); socket.on('data', socketOnData) ... }
有幾個(gè)需要要關(guān)注的點(diǎn):
6.4 http.ServerResponse
看node.org官方是如何介紹server端的response對(duì)象的:
This object is created internally by an HTTP server–not by the user. It is passed as the second parameter to the ‘request’ event.
The response implements, but does not inherit from, the Writable Stream interface.
跟http.ClientRequest很像,繼承了OutgoingMessage,沒有繼承Stream.Writable,但是實(shí)現(xiàn)了Stream的功能,可以跟Stream.Writable一樣靈活使用:
function ServerResponse(req) { ... OutgoingMessage.call(this); ... } util.inherits(ServerResponse, OutgoingMessage);
6.5 http.IncomingMessage
An IncomingMessage object is created by http.Server or http.ClientRequest and passed as the first argument to the ‘request’ and ‘response’ event respectively. It may be used to access response status, headers and data.
http.IncomingMessage有兩個(gè)地方時(shí)被內(nèi)部創(chuàng)建,一個(gè)是作為server端的request,另外一個(gè)是作為client請(qǐng)求中的response,同時(shí)該類顯示地繼承了Stream.Readable。
function IncomingMessage(socket) { Stream.Readable.call(this); this.socket = socket; this.connection = socket; ... }
util.inherits(IncomingMessage, Stream.Readable);
7. 結(jié)語
上面是對(duì)node中主要的網(wǎng)絡(luò)通信模塊,粗略進(jìn)行了分析研究,對(duì)網(wǎng)絡(luò)通信的細(xì)節(jié)有大概的了解。但是這還遠(yuǎn)遠(yuǎn)不夠的,仍然無法解決node應(yīng)用中出現(xiàn)的各種網(wǎng)絡(luò)問題,這邊文章只是一個(gè)開端,希望后面可以深入了解各個(gè)細(xì)節(jié)、深入到c++層面。