初步研究node中的網絡通信模塊

分類:技術 時間:2017-01-13

目前,我們處于互聯網時代,互聯網產品百花齊放。例如,當打開瀏覽器,可以看到各種信息,瀏覽器是如何跟服務器進行通信的?當打開微信跟朋友聊天時,你是如何跟朋友進行消息傳遞的?這些都得靠網絡進程之間的通信,都得依賴于socket。那什么是socket?node中有哪些跟網絡通信有關的模塊?這些問題是本文研究的重點。

1. Socket

Socket源于Unix,而Unix的基本哲學是『一些皆文件』,都可以用『打開open ==gt; 讀/寫(read/write) ==gt; 關閉(close)』模式來操作,Socket也可以采用這種方法進行理解。關于Socket,可以總結如下幾點:

  • 可以實現底層通信,幾乎所有的應用層都是通過socket進行通信的,因此『一切且socket』
  • 對TCP/IP協議進行封裝,便于應用層協議調用,屬于二者之間的中間抽象層
  • 各個語言都與相關實現,例如C、C 、node
  • TCP/IP協議族中,傳輸層存在兩種通用協議: TCP、UDP,兩種協議不同,因為不同參數的socket實現過程也不一樣

2. node中網絡通信的架構實現

node中的模塊,從兩種語言實現角度來說,存在javscript、c 兩部分,通過 process.binding 來建立關系。具體分析如下:

  • 標準的node模塊有net、udp、dns、http、tls、https等
  • V8是chrome的內核,提供了javascript解釋運行功能,里面包含tcp_wrap.h、udp_wrap.h、tls_wrap.h等
  • OpenSSL是基本的密碼庫,包括了MD5、SHA1、RSA等加密算法,構成了node標準模塊中的 crypto
  • cares模塊用于DNS的解析
  • libuv實現了跨平臺的異步編程
  • http_parser用于http的解析

3. net使用

net模塊 是基于TCP協議的socket網路編程模塊,http模塊就是建立在該模塊的基礎上實現的,先來看看基本使用方法:

// 創建socket服務器 server.js
const net = require('net')
const server = net.createServer();
server.on('connection', (socket) =gt; {
  socket.pipe(process.stdout);
  socket.write('data from server');
});
server.listen(3000, () =gt; {
  console.log(`server is on ${JSON.stringify(server.address())}`);
});

// 創建socket客戶端 client.js
const net = require('net');
const client = net.connect({port: 3000});
client.on('connect', () =gt; {
  client.write('data from client');
});
client.on('data', (chunk) =gt; {
  console.log(chunk.toString());
  client.end();
});
// 打開兩個終端,分別執行`node server.js`、`node client.js`,可以看到客戶端與服務器進行了數據通信。

使用 const server = net.createServer(); 創建了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,具有事件的相關方法
  • _handle是server處理的句柄,屬性值最終由c 部分的 TCPPipe 類創建
  • connectionListener也是語法糖,作為connection事件的回調函數

再來看看connectionListener事件的回調函數,里面包含一個 socket 對象,該對象是一個連接套接字,是個五元組(server_host、server_ip、protocol、client_host、client_ip),相關實現如下:

function onconnection(err, clientHandle) {
  ...
  var socket = new Socket({
    ...
  });
  ...
  self.emit('connection', socket);
}

因為Socket是繼承了 stream.Duplex ,所以Socket也是一個可讀可寫流,可以使用流的方法進行數據的處理。

接下來就是很關鍵的端口監聽(port),這是server與client的主要區別,代碼:

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);
}

上述代碼有幾個點需要注意:

  • 監聽的對象可以是端口、路徑、定義好的server句柄、文件描述符
  • 當通過cluster創建工作進程(worker)時,exclusive判斷是否進行socket連接的共享
  • 事件監聽最終還是通過TCP/Pipe的listen來實現
  • backlog規定了socket連接的限制,默認最多為511

接下來分析下listen中最重要的 _handle 了,_handle決定了server的功能:

function createServerHandle(address, port, addressType, fd) {
  ...
  if (typeof fd === 'number' amp;amp; fd gt;= 0) {
    ...
    handle = createHandle(fd);
    ...
  } else if(port === -1 amp;amp; 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實現,因而要想完全搞清楚node中的網絡通信,必須深入到V8的源碼里面。

4. UDP/dgram使用

跟net模塊相比,基于UDP通信的dgram模塊就簡單了很多,因為不需要通過三次握手建立連接,所以整個通信的過程就簡單了很多,對于數據準確性要求不太高的業務場景,可以使用該模塊完成數據的通信。

// server端實現
const dgram = require('dgram');
const server = dgram.createSocket('udp4');
server.on('message', (msg, addressInfo) =gt; {
  console.log(addressInfo);
  console.log(msg.toString());
  const data = http://www.tuicool.com/articles/Buffer.from('from server');
  server.send(data, addressInfo.port);
});
server.bind(3000, () =gt; {
  console.log('server is on ', server.address());
});
// client端實現
const dgram = require('dgram');
const client = dgram.createSocket('udp4');
const data = http://www.tuicool.com/articles/Buffer.from('from client');
client.send(data, 3000);
client.on('message', (msg, addressInfo) =gt; {
  console.log(addressInfo);
  console.log(msg.toString());
  client.close();
});

從源碼層面分析上述代碼的原理實現:

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,后續的所有操作都是基于事件的方式
  • UDP在創建的時候需要注意ipv4和ipv6
  • UDP的_handle是由UDP類創建的
  • 通信過程中可能需要進行DNS查詢,解析出ip地址,然后再進行其他操作

5. DNS使用

DNS(Domain Name System)用于域名解析,也就是找到host對應的ip地址,在計算機網絡中,這個工作是由網絡層的ARP協議實現。在node中存在 net 模塊來完成相應功能,其中dns里面的函數分為兩類:

  • 依賴底層操作系統實現域名解析,也就是我們日常開發中,域名的解析規則,可以回使用瀏覽器緩存、本地緩存、路由器緩存、dns服務器,該類僅有 dns.lookup
  • 該類的dns解析,直接到nds服務器執行域名解析
const dns = require('dns');
const host = 'bj.meituan.com';
dns.lookup(host, (err, address, family) =gt; {
  if (err) {
    console.log(err);
    return;
  }
  console.log('by net.lookup, address is: %s, family is: %s', address, family);
});

dns.resolve(host, (err, address) =gt; {
  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

在這種情況下,二者解析的結果是一樣的,但是假如我們修改本地的/etc/hosts文件呢

// 在/etc/host文件中,增加:
10.10.10.0 bj.meituan.com

// 然后再執行上述文件,結果是:
by net.resolve, address is: 103.37.152.41
by net.lookup, address is: 10.10.10.0, family is: 4

接下來分析下dns的內部實現:

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);
  ...
}

上面的源碼有幾個點需要關注:

  • lookup與resolve存在差異,使用的時候需要注意
  • 不管是lookup還是resolve,均依賴于cares庫
  • 域名解析的type很多: resolve4、resolve6、resolveCname、resolveMx、resolveNs、resolveTxt、resolveSrv、resolvePtr、resolveNaptr、resolveSoa、reverse

6. HTTP使用

在WEB開發中,HTTP作為最流行、最重要的應用層,是每個開發人員應該熟知的基礎知識,我面試的時候必問的一塊內容。同時,大多數同學接觸node時,首先使用的恐怕就是http模塊。先來一個簡單的demo看看:

const http = require('http');
const server = http.createServer();
server.on('request', (req, res) =gt; {
  res.setHeader('foo', 'test');
  res.writeHead(200, {
    'Content-Type': 'text/html',
  });
  res.write('lt;!doctypegt;');
  res.end(`lt;htmlgt;lt;/htmlgt;`);
});

server.listen(3000, () =gt; {
  console.log('server is on ', server.address());
  var req = http.request({ host: '127.0.0.1', port: 3000});
  req.on('response', (res) =gt; {
    res.on('data', (chunk) =gt; console.log('data from server ', chunk.toString()) );
    res.on('end', () =gt; server.close() );
  });
  req.end();
});
// 輸出結果如下:
// server is on  { address: '::', family: 'IPv6', port: 3000 }
// data from server  lt;!doctypegt;
// data from server  lt;htmlgt;lt;/htmlgt;

針對上述demo,有很多值得深究的地方,一不注意服務就掛掉了,下面根據node的 官方文檔 ,逐個進行研究。

6.1 http.Agent

因為HTTP協議是無狀態協議,每個請求均需通過三次握手建立連接進行通信,眾所周知三次握手、慢啟動算法、四次揮手等過程很消耗時間,因此HTTP1.1協議引入了keep-alive來避免頻繁的連接。那么對于tcp連接該如何管理呢?http.Agent就是做這個工作的。先看看源碼中的關鍵部分:

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);
// 關于socket的相關增刪改查操作
Agent.prototype.addRequest = function(req, options) {
  ...
  if (freeLen) {
    var socket = this.freeSockets[name].shift();
    ...
    this.sockets[name].push(socket);
    ...
  } else if (sockLen lt; 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連接數量的上限(Infinity)
  • 連接池管理的核心是對 socketsfreeSockets 的增刪查
  • globalAgent會作為http.ClientRequest的默認agent

下面可以測試下agent對請求本身的限制:

// req.js
const http = require('http');

const server = http.createServer();
server.on('request', (req, res) =gt; {
  var i=1;
  setTimeout(() =gt; {
    res.end('ok ', i  );
  }, 1000)
});

server.listen(3000, () =gt; {
  var max = 20;
  for(var i=0; ilt;max; i  ) {
    var req = http.request({ host: '127.0.0.1', port: 3000});
    req.on('response', (res) =gt; {
      res.on('data', (chunk) =gt; console.log('data from server ', chunk.toString()) );
      res.on('end', () =gt; server.close() );
    });
    req.end();
  }
});
// 在終端中執行time node ./req.js,結果為:
// real  0m1.123s
// user  0m0.102s
// sys 0m0.024s

// 在req.js中添加下面代碼
http.globalAgent.maxSockets = 5;
// 然后同樣time node ./req.js,結果為:
real  0m4.141s
user  0m0.103s
sys 0m0.024s

當設置maxSockets為某個值時,tcp的連接就會被限制在某個值,剩余的請求就會進入 requests 隊列里面,等有空余的socket連接后,從request隊列中出棧,發送請求。

6.2 http.ClientRequest

當執行http.request時,會生成ClientRequest對象,該對象雖然沒有直接繼承Stream.Writable,但是繼承了http.OutgoingMessage,而http.OutgoingMessage實現了write、end方法,因為可以當跟stream.Writable一樣的使用。

var req = http.request({ host: '127.0.0.1', port: 3000, method: 'post'});
req.on('response', (res) =gt; {
  res.on('data', (chunk) =gt; console.log('data from server ', chunk.toString()) );
  res.on('end', () =gt; server.close() );
});
// 直接使用pipe,在request請求中添加數據
fs.createReadStream('./data.json').pipe(req);

接下來,看看http.ClientRequest的實現, 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其實就是創建了一個http.Server對象,關鍵源碼如下:

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)
  ...
}

有幾個需要要關注的點:

  • 服務的創建依賴于net.server,通過net.server在底層實現服務的創建
  • 默認情況下,服務的超時時間為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,但是實現了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有兩個地方時被內部創建,一個是作為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. 結語

上面是對node中主要的網絡通信模塊,粗略進行了分析研究,對網絡通信的細節有大概的了解。但是這還遠遠不夠的,仍然無法解決node應用中出現的各種網絡問題,這邊文章只是一個開端,希望后面可以深入了解各個細節、深入到c 層面。

參考

https://yjhjstz.gitbooks.io/deep-into-node/content/chapter9/chapter9-1.html

http://nodejs.org/api/http.html


Tags: Socket

文章來源:http://zhenhua-lee.github.io/node/socket.html


ads
ads

相關文章
ads

相關文章

ad