初步研究node中的網(wǎng)絡(luò)通信模塊
目前,我們處于互聯(lián)網(wǎng)時代,互聯(lián)網(wǎng)產(chǎn)品百花齊放。例如,當打開瀏覽器,可以看到各種信息,瀏覽器是如何跟服務(wù)器進行通信的?當打開微信跟朋友聊天時,你是如何跟朋友進行消息傳遞的?這些都得靠網(wǎng)絡(luò)進程之間的通信,都得依賴于socket。那什么是socket?node中有哪些跟網(wǎng)絡(luò)通信有關(guān)的模塊?這些問題是本文研究的重點。
1. Socket
Socket源于Unix,而Unix的基本哲學是『一些皆文件』,都可以用『打開open ==> 讀/寫(read/write) ==> 關(guān)閉(close)』模式來操作,Socket也可以采用這種方法進行理解。關(guān)于Socket,可以總結(jié)如下幾點:
- 可以實現(xiàn)底層通信,幾乎所有的應用層都是通過socket進行通信的,因此『一切且socket』
- 對TCP/IP協(xié)議進行封裝,便于應用層協(xié)議調(diào)用,屬于二者之間的中間抽象層
- 各個語言都與相關(guān)實現(xiàn),例如C、C++、node
- TCP/IP協(xié)議族中,傳輸層存在兩種通用協(xié)議: TCP、UDP,兩種協(xié)議不同,因為不同參數(shù)的socket實現(xiàn)過程也不一樣
2. node中網(wǎng)絡(luò)通信的架構(gòu)實現(xiàn)
node中的模塊,從兩種語言實現(xiàn)角度來說,存在javscript、c++兩部分,通過 process.binding 來建立關(guān)系。具體分析如下:
- 標準的node模塊有net、udp、dns、http、tls、https等
- V8是chrome的內(nèi)核,提供了javascript解釋運行功能,里面包含tcp_wrap.h、udp_wrap.h、tls_wrap.h等
- OpenSSL是基本的密碼庫,包括了MD5、SHA1、RSA等加密算法,構(gòu)成了node標準模塊中的 crypto
- cares模塊用于DNS的解析
- libuv實現(xiàn)了跨平臺的異步編程
- http_parser用于http的解析
3. net使用
net模塊 是基于TCP協(xié)議的socket網(wǎng)路編程模塊,http模塊就是建立在該模塊的基礎(chǔ)上實現(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();
- });
- // 打開兩個終端,分別執(zhí)行`node server.js`、`node client.js`,可以看到客戶端與服務(wù)器進行了數(shù)據(jù)通信。
使用 const server = net.createServer(); 創(chuàng)建了server對象,那server對象有哪些特點:
- // 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);
上述代碼可以分為幾個點:
- createServer 就是一個語法糖,幫助new生成server對象
- server對象繼承了EventEmitter,具有事件的相關(guān)方法
- _handle是server處理的句柄,屬性值最終由c++部分的 TCP 、 Pipe 類創(chuàng)建
- connectionListener也是語法糖,作為connection事件的回調(diào)函數(shù)
再來看看connectionListener事件的回調(diào)函數(shù),里面包含一個 socket 對象,該對象是一個連接套接字,是個五元組(server_host、server_ip、protocol、client_host、client_ip),相關(guān)實現(xiàn)如下:
- function onconnection(err, clientHandle) {
- ...
- var socket = new Socket({
- ...
- });
- ...
- self.emit('connection', socket);
- }
因為Socket是繼承了 stream.Duplex ,所以Socket也是一個可讀可寫流,可以使用流的方法進行數(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);
- }
上述代碼有幾個點需要注意:
- 監(jiān)聽的對象可以是端口、路徑、定義好的server句柄、文件描述符
- 當通過cluster創(chuàng)建工作進程(worker)時,exclusive判斷是否進行socket連接的共享
- 事件監(jiān)聽最終還是通過TCP/Pipe的listen來實現(xiàn)
- backlog規(guī)定了socket連接的限制,默認最多為511
接下來分析下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實現(xiàn),因而要想完全搞清楚node中的網(wǎng)絡(luò)通信,必須深入到V8的源碼里面。
4. UDP/dgram使用
跟net模塊相比,基于UDP通信的dgram模塊就簡單了很多,因為不需要通過三次握手建立連接,所以整個通信的過程就簡單了很多,對于數(shù)據(jù)準確性要求不太高的業(yè)務(wù)場景,可以使用該模塊完成數(shù)據(jù)的通信。
- // server端實現(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端實現(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();
- });
從源碼層面分析上述代碼的原理實現(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);
- ...
- }
上述代碼存在幾個點需要注意:
- UDP模塊沒有繼承stream,僅僅繼承了EventEmit,后續(xù)的所有操作都是基于事件的方式
- UDP在創(chuàng)建的時候需要注意ipv4和ipv6
- UDP的_handle是由UDP類創(chuàng)建的
- 通信過程中可能需要進行DNS查詢,解析出ip地址,然后再進行其他操作
5. DNS使用
DNS(Domain Name System)用于域名解析,也就是找到host對應的ip地址,在計算機網(wǎng)絡(luò)中,這個工作是由網(wǎng)絡(luò)層的ARP協(xié)議實現(xiàn)。在node中存在 net 模塊來完成相應功能,其中dns里面的函數(shù)分為兩類:
依賴底層操作系統(tǒng)實現(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)部實現(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);
- ...
- }
上面的源碼有幾個點需要關(guān)注:
- lookup與resolve存在差異,使用的時候需要注意
- 不管是lookup還是resolve,均依賴于cares庫
- 域名解析的type很多: resolve4、resolve6、resolveCname、resolveMx、resolveNs、resolveTxt、resolveSrv、resolvePtr、resolveNaptr、resolveSoa、reverse
6. HTTP使用
在WEB開發(fā)中,HTTP作為***、最重要的應用層,是每個開發(fā)人員應該熟知的基礎(chǔ)知識,我面試的時候必問的一塊內(nèi)容。同時,大多數(shù)同學接觸node時,首先使用的恐怕就是http模塊。先來一個簡單的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 頭
針對上述demo,有很多值得深究的地方,一不注意服務(wù)就掛掉了,下面根據(jù)node的 官方文檔 ,逐個進行研究。
6.1 http.Agent
因為HTTP協(xié)議是無狀態(tài)協(xié)議,每個請求均需通過三次握手建立連接進行通信,眾所周知三次握手、慢啟動算法、四次揮手等過程很消耗時間,因此HTTP1.1協(xié)議引入了keep-alive來避免頻繁的連接。那么對于tcp連接該如何管理呢?http.Agent就是做這個工作的。先看看源碼中的關(guān)鍵部分:
- function Agent(options) {
- ...
- EventEmitter.call(this);
- ...
- self.maxSockets = self.options.maxSockets || Agent.defaultMaxSockets;
- self.maxFreeSockets = self.options.maxFreeSockets || 256;
- ...
- self.requests = {}; // 請求隊列
- 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();
上述代碼有幾個點需要注意:
- maxSockets默認情況下,沒有tcp連接數(shù)量的上限(Infinity)
- 連接池管理的核心是對 sockets 、 freeSockets 的增刪查
- globalAgent會作為http.ClientRequest的默認agent
下面可以測試下agent對請求本身的限制:
- // 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
當設(shè)置maxSockets為某個值時,tcp的連接就會被限制在某個值,剩余的請求就會進入 requests 隊列里面,等有空余的socket連接后,從request隊列中出棧,發(fā)送請求。
6.2 http.ClientRequest
當執(zhí)行http.request時,會生成ClientRequest對象,該對象雖然沒有直接繼承Stream.Writable,但是繼承了http.OutgoingMessage,而http.OutgoingMessage實現(xiàn)了write、end方法,因為可以當跟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請求中添加數(shù)據(jù)
- fs.createReadStream('./data.json').pipe(req);
接下來,看看http.ClientRequest的實現(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其實就是創(chuàng)建了一個http.Server對象,關(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)
- ...
- }
有幾個需要要關(guān)注的點:
- 服務(wù)的創(chuàng)建依賴于net.server,通過net.server在底層實現(xiàn)服務(wù)的創(chuàng)建
- 默認情況下,服務(wù)的超時時間為2分鐘
- connectionListener處理tcp連接后的行為,跟net保持一致
6.4 http.ServerResponse
看node.org官方是如何介紹server端的response對象的:
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,但是實現(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有兩個地方時被內(nèi)部創(chuàng)建,一個是作為server端的request,另外一個是作為client請求中的response,同時該類顯示地繼承了Stream.Readable。
- function IncomingMessage(socket) {
- Stream.Readable.call(this);
- this.socket = socket;
- this.connection = socket;
- ...
- }
util.inherits(IncomingMessage, Stream.Readable);
7. 結(jié)語
上面是對node中主要的網(wǎng)絡(luò)通信模塊,粗略進行了分析研究,對網(wǎng)絡(luò)通信的細節(jié)有大概的了解。但是這還遠遠不夠的,仍然無法解決node應用中出現(xiàn)的各種網(wǎng)絡(luò)問題,這邊文章只是一個開端,希望后面可以深入了解各個細節(jié)、深入到c++層面。