1. 程式人生 > >從底層看看HTTP模組的構建之net模組深入理解

從底層看看HTTP模組的構建之net模組深入理解

問題1:如何建立一個TCP伺服器?

net.createServer([options][, connectionListener])

建立一個Server物件,引數connectionListener作為'connection'事件的監聽函式,這個options有如下的預設值:
	{
	  allowHalfOpen: false,//自己不會自動傳送FIN欄位
	  pauseOnConnect: false//允許在不同程序之間傳遞socket
	}

如果allowHalfOpen設定為true,那麼socket當另一端傳送了FIN報文的時候不會自己自動傳送FIN。這時候socket不是可讀的了,但是是可寫的(因為對方傳送了FIN,所以不會有資料傳送了,因此不可讀,但是因為自己沒有傳送FIN,所以還是可以傳送的,也就是可寫的)。當然你可以顯示呼叫end()方法,這時候就相當於自己也傳送FIN了,也就是不可寫了。如果pauseOnConnect為true,那麼傳送訊息的socket就會暫停,無法從其讀取資料。這就允許在不同的程序之間傳遞連線,而不會在原始的連線中讀取到資料。但是如果你需要從暫停的socket中讀取資料就必須顯示的呼叫resume方法。

我們看看TCP伺服器內部的結構,也就是net.Server物件的內部簽名:

Server {
  domain: null,
  _events: { connection: [Function] },
  _eventsCount: 1,
  _maxListeners: undefined,
  _connections: 0,
  _handle: null,
  _usingSlaves: false,
  _slaves: [],
  _unref: false,
  allowHalfOpen: false,
  pauseOnConnect: false }
該物件有如下的方法

問題2:什麼是Class: net.Server


用於建立一個本地的伺服器或者TCP伺服器。他是一個EventEmiter物件,但是有以下額外的事件:
close事件:
當伺服器關閉的時候觸發,如果還有連線存在那麼這個事件不會觸發,除非所有的連線都關閉了
connection
當有新連線建立的時候觸發,這個事件的回撥的引數是一個net.Socket例項
error
當有錯誤觸發的時候觸發,這個事件被觸發了表示有錯誤產生,這時候馬上會呼叫close事件
listening
當net.Server物件呼叫了server.listen時候會觸發
server.address()方法
返回繫結的地址,地址的型別,同時返回埠號。返回的物件簽名如:{ port: 12346, family: 'IPv4', address: '127.0.0.1' }

var server = net.createServer((socket) => {
  socket.end('goodbye\n');
	}).on('error', (err) => {
	  // handle errors here
	  throw err;
	});
	// grab a random port.
	server.listen(() => {
	  address = server.address();
	  console.log('opened server on %j', address);
	});
注意:在listening事件觸發之前不要呼叫server.address方法
server.close([callback])
停止接受新連線,但是儲存已經存在的連線。這個方法是非同步的,伺服器當所有的連線已經關閉同時伺服器觸發了'close'事件的時候會關閉。這個回撥函式當'close'事件觸發的時候會被觸發一次。這個回撥函式接受一個錯誤物件,如果伺服器沒有開啟但是呼叫close方法的時候會觸發
server.connections
伺服器可以併發的連線的數量。用 child_process.fork()這種方式把socket物件傳遞給子程序這時候就是null。如果獲取當前活動的連線用server.getConnections代替
server.getConnections(callback)
獲取伺服器物件上面的併發的連線數量。Works when sockets were sent to forks。回撥函式中有兩個引數err,count
server.listen(handle[, backlog][, callback])
handle物件可以是一個server或者socket(任何具有_handle屬性的物件),也可以是{fd: <n>} 物件。這就會讓伺服器在一個特定的handle上接受連線,但是前提是檔案描述符或者handle已經被繫結到一個埠號或者domain socket了!在windows平臺上無法監聽檔案描述符。這個方法是非同步的,當伺服器被訪問,那麼'listening'事件會被觸發,callback就是'listening'事件的回撥函式。backlog和 server.listen(port[, hostname][, backlog][, callback]).中作用一樣
server.listen(options[, callback])
options接受如下的引數。path用於指定Unix平臺上的socket。如果exclusive設定為false,那麼所有的子程序就會使用相同的handle,這時候子程序和父程序共同處理。如果設定為true,那麼handle不會共享,這時候埠號共享都會導致錯誤。
server.listen({
  host: 'localhost',
  port: 80,
  exclusive: true
});
server.listen(path[, backlog][, callback])
通過指定的path來開啟一個本地socket連線監聽。這個方法是非同步的,如果伺服器被訪問那麼就會觸發'listening'事件,最後的callback就是'listening'事件的回撥函式。在Unix中,local domain通常代表Uinx domian。其中path是檔案系統的地址的名稱,他在建立檔案時候受到相同的命名轉換規則以及許可權監測的限制,在作業系統中是可見的,知道unlinked後才不可見。在windows上,本地domain是通過使用命名管道的實現的。地址必須指代一個入口\\?\pipe\ or \\.\pipe。任何字元都是允許的,但是後續可能會對管道名稱做一些處理,例如解析'..'等。管道當最後一個引用關閉的時候會被移除。注意:javascript中字串轉義需要使用雙斜槓。
net.createServer().listen(
   path.join('\\\\?\\pipe', process.cwd(), 'myctl'))
server.listen(port[, hostname][, backlog][, callback])
如果沒有指定hsotname那麼就會在IPV6下監聽'::',在IPV4下監聽'0.0.0.0',如果把port設定為0那麼就是一個隨機的介面。backlog指的是最大的等待連線的佇列長度。最大的長度通過作業系統的sysctl設定決定,如tcp_max_syn_backlog and somaxconn(linux)。預設的值為511而不是512。當用戶遇到EADDRINUSE 錯誤的時候,表示有另外一個伺服器執行在同樣的埠號上,一個處理方式就是通過等待
	server.on('error', (e) => {
	  if (e.code == 'EADDRINUSE') {
	    console.log('Address in use, retrying...');
	    setTimeout(() => {
	      server.close();
	      //首先關閉然後繼續呼叫listen來監聽
	      server.listen(PORT, HOST);
	    }, 1000);
	  }
	});
server.maxConnections
設定伺服器接收的最多的連線。當吧socket傳送給 child_process.fork().時候不建議使用這個選項
server.unref()
如果在事件系統中只有一個活動的伺服器的時候允許程式退出。如果伺服器已經unrefed,這時候再次呼叫無效,返回net.Server物件
server.ref()
和unref相反
問題四:什麼是Class: net.Socket(和net.Server一樣,也是一個EventEmitter例項)
這個物件是一個TCP或者本地的socket。net.Socket實現了雙工的流,可以被使用者建立也可以作為客戶端使用(connect方法),也可以讓Node.js建立完成以後通過'connection'事件傳遞給net.Server物件。
new net.Socket([options])
其中引數的預設值如下:
{
  fd: null,//為socket指定一個存在的檔案描述符
  allowHalfOpen: false,
  readable: false,//設定為true那麼這個流可讀
  writable: false//設定為流那麼這個流可寫(只有當fd存在的時候才會可寫)
}
Event: 'close'
當socket被關閉的時候觸發,引數是一個布林值表示socket是否由於傳輸錯誤而關閉
Event: 'connect'
當連線被完全建立的時候觸發
Event: 'data'
當有資料接收到的時候被觸發。引數data可以是Buffer也可以是一個String。這裡的資料編碼是通過socket.setEncoding來完成的。注意:如果沒有指定這個事件那麼當有資料傳輸過來的時候就會丟失
Event: 'drain'
當write buffer為空的時候觸發,可以用於截止上傳。如果在A端呼叫write方法只會在A端觸發這個事件
Event: 'end'
當另一端傳送一個FIN報文的時候觸發。預設情況下allowHalfOpen == false(也就是不允許單方面的連線存在),那麼socket把所有的待發送的資料傳送完成以後就會銷燬他的檔案描述符。但是把allowHalfOpen設定為true,那麼socket就不會自動呼叫end方法。這時候就可以允許使用者傳遞任意數量的資料,這時候用於必須手動呼叫end方法
Event: 'error'
呼叫這個事件後就會馬上呼叫'close'事件
Event: 'lookup'
當從域名解析出IP地址以後,但是還沒有連線的時候觸發。在Unitx socket中不可用。第一個引數為err,第二個為address,第三個為family
Event: 'timeout'
如果socket超時了就會呼叫。這時候我們知道socket已經空閒了,所以必須手動關閉連線
socket.address()
返回地址,返回型別為{ port: 12346, family: 'IPv4', address: '127.0.0.1' }
socket.bufferSize
我們知道在net.Socket中,socket.write一直可以呼叫。這種快取的方式會導致記憶體消耗猛漲,這個屬性可以獲取當前已經快取的資料量。這時候使用者可以通過pause,resume方法來控制資料流
socket.bytesRead
已經獲取到資料量
socket.bytesWritten
已經發送的資料量
socket.connect(options[, connectListener])
為一個socket開啟一個連線。對於TCP的socket可以使用如下引數:port.host,lcoalAddress,localPort,family,lookup。對於local domain socket必須要指定path。通常情況下,這個方法不是必須的,因為net.createConnection自動打開了socket。但是當你需要實現自己的自定義的Socket的時候才需要。這個方法是非同步的,如果'connect'事件觸發了,socket就會建立。如果在連線時候出錯了,那麼'connect'事件不會觸發。
socket.destroy()
這時候socket上面不會觸發任何的I/O行為,只有當出錯的時候下需要
socket.end([data][, encoding])
傳送一個FIN報文,這時候伺服器可能還會發送一些資料。如果指定了data,相當於呼叫socket.write(data, encoding) followed by socket.end().
socket.localAddress
連線者的IP地址
socket.localPort
本地的埠號
socket.pause()
暫停讀取資料,這時候'data'事件就不會觸發。當檔案上傳時候有用
socket.unref()
在socket中呼叫unref,那麼當事件系統中只有一個活動的socket的時候就會退出。多次呼叫這個方法無效
socket.ref()
和unref相反
socket.remoteAddress,socket.remoteFamily,socket.remotePort
返回伺服器端的地址,地址型別,遠端埠
socket.resume()
當呼叫pause方法後呼叫這個方法繼續讀取資料
socket.setEncoding([encoding])
設定可讀流的編碼方法
socket.setKeepAlive([enable][, initialDelay])
啟動/關閉keep-alive功能,initialDelay用於設定最後一次接收到資料到first keepalive probe之間的時間。把這個initialDelay設定為0就會導致預設的值或者前一次設定的值不變,也就是重新載入上一次的值。預設為0,返回一個socket。enable預設為false。關於keep-alive設定的值:以前每一個http下面都需要建立一個TCP socket,而且通訊過馬上關閉,這樣就會存在建立socket時候的開銷,使用keep-alive表示在伺服器傳送完連線以後不會馬上關閉而是等待timeout時間。keep-alive並不是免費的午餐,長時間的tcp連線容易導致系統資源無效佔用
socket.setNoDelay([noDelay])
注意:Node.js中預設啟動了Nagle演算法,因此呼叫socket.setNoDelay(true)就是去掉Nagle演算法。使用write方法就會立即傳送資料到網路中。Nagle演算法表示:如果每次只是傳送一個位元組的內容而不優化,網路中就會充滿只有極少數有效資料的資料包,將十分浪費網路資源。Nagle演算法針對這種情況,要求緩衝區的資料達到一定的量或者一定時間才會傳送,所以小資料包就會被Nagle演算法合併,以此優化網路。但是資料可能延遲傳送

socket.setTimeout(timeout[, callback])
預設情況下net.Socket沒有timeout。必須注意:當觸發socket的timeout事件的時候,連線不會強行關閉,用於必須手動呼叫end或者destroy方法銷燬socket。把timeout設定為0那麼就不存在timeout了。
socket.write(data[, encoding][, callback])
如果所有的資料都成功傳送到核心緩衝區就會返回true。如果有一部分資料或者全部的資料都儲存在使用者的記憶體中等待發送這時候就會返回false。'drain'事件當buffer變成空的時候會觸發。

問題五:常見的net模組的方法有哪些?

net.connect(options[, connectListener])
一個工廠方法返回一個net.Socket例項,同時使用提供的選項自動連線。這個選項會同時新增到<net.Socket的建構函式>和<socket.connect方法>中。第二個引數被作為一個'connect'事件監聽器。
const net = require('net');

	const client = net.connect({port: 8124}, () => {
	  // 'connect' listener
	  console.log('connected to server!');
	  //連線上了伺服器
	  client.write('world!\r\n');
	});
	client.on('data', (data) => {
	  console.log(data.toString());
	  client.end();
	});
	client.on('end', () => {
	  console.log('disconnected from server');
	});
net.createConnection(options[, connectListener])
一個工廠方法,返回一個net.Socekt,同時使用提供的選項自動連線。這個選項會同時新增到<net.Socket的建構函式>和<socket.connect方法>中。
const net = require('net');
const client = net.createConnection({port: 8124}, () => {
  //'connect' listener
  console.log('connected to server!');
  client.write('world!\r\n');
});
client.on('data', (data) => {
  console.log(data.toString());
  client.end();
});
client.on('end', () => {
  console.log('disconnected from server');
});
net.createServer([options][, connectionListener])

這個方法返回一個net.Server物件,請參見該文章最前面的內容

問題6:一個可能很簡單的例子?

var net = require('net');
var server = net.createServer(function (socket) {
  socket.write('Echo server\r\n');
  socket.pipe(socket);
});
server.listen(1337, '127.0.0.1');
注意:上面的socket.pipe(socket)的作用就是把客戶端傳送的結果原樣傳送到客戶端。我們知道這裡的socket是雙向的流,然後查閱API知道readable.pipe(destination[, options]),而且destination必須是Stream.Writtable,而對於伺服器端的socket來說是一個全雙工的流,因此結果就是把socket的讀取的流資料原樣傳送到客戶端。

問題7:TCP客戶端和服務端通訊的過程是怎麼?

伺服器端:

var net=require('net');
//回撥函式就是connect事件回撥函式,也就是隻要有客戶端就會觸發
var server=net.createServer(function(socket){
	socket.on('data',function(data){
		console.log('接收到客戶端資料'+data);
		socket.write('你好');
	});
	socket.on('end',function(){
		console.log('連線斷開');
	});
	socket.write('歡迎光臨Node.js伺服器端');
});
//這裡繫結的是listening觸發,只要伺服器啟動就會觸發這個事件
server.listen(8888,function(){
	console.log('伺服器被繫結,listening事件會被觸發!');
});
客戶端程式碼:
var net=require('net');
//只要socket連線被成功建立就會呼叫這裡的connect事件
var client=net.connect({port:8888},function(){
	 console.log('客戶端被連線');
	 client.write('客戶端資料');
});
//客戶端data事件
client.on('data',function(data){
	console.log(data.toString());
});
client.on('end',function(){
	console.log('客戶端斷開');
});
原始碼地址見github