NodeJS和TCP:一本通

Cris_冷崢子發表於2019-03-03
  • TCP簡介
    • TCP格式(Segment)
      • URG和PUSH的區別
    • TCP三次握手四次揮手
      • 三次握手
      • 四次揮手
  • Node.js的tcp實現
    • 基本介紹
    • tcp是長連線(socket)
      • 長連線注意事項
      • 設定超時
      • 關閉socket
        • 方法一:客戶端手動關閉
        • 方法二:socket.end(),伺服器讓客戶端關閉連線
      • 控制連線數
        • maxConnections
        • getConnections
    • 關閉伺服器
      • server.close()
      • server.unref()
    • socket是一個雙工流
      • 雙工流簡介
      • 關於讀取
      • 關於讀取
      • 關於pipe
    • socket的其它屬性方法
      • socket.bufferSize
    • 埠被佔用解決方案

pre-notify

參考與圖片來源

維護ing...


TCP簡介

TCP格式(Segment)

NodeJS和TCP:一本通
每一行32位

  • 源埠號(16位) --- 目的埠號(16位) 0~65535 計算機通過埠號識別訪問哪個服務,比如http服務或ftp服務

  • 32位序列號,以便達到目的後重新組裝資料包 TCP用序列號讀資料包進行標記,假設當前的序列號為s,傳送資料長度為i,則下次傳送資料時的序列號為s+i。在建立連線時通常由計算機生成一個隨機數作為序列號初始值。

  • 32位確認應答號 接收方收到資料後的答覆訊號 它等於下一次應該接受到的資料的序列號。假設傳送端的序列號為s,傳送資料長度為i,那麼接收端返回的確認應答號也是s+i。傳送端接收到這個確認應答後,可以認為這個位置以前所有的資料都已被正常接收。

  • 4位首部長度 TCP首部的長度,單位為4位元組,如果沒有可選欄位,那麼這裡的值就是5(單位為4位元組),表示TCP首部的長度為20位元組。【1代表4個位元組,4位8個狀態能代表32個位元組】

  • 6位保留位

  • 6位控制位 TCP的連線、傳輸和斷開都接受這個六個控制位的指揮

    • URG 此包包含緊急資料,先讀取緊急資料再讀取其它
    • ACK(acknowlegement) 為1表示確認號
    • PSH(push急迫位) 快取區將滿(可手動置為1),立刻傳輸資料 (因為TCP有懶啟動的概念,發一個位元組不會立馬發出去 會攢夠一個量 再發)
    • RST(reset重置) 表示連線段了要重新連線
    • SYN(synchronous) 同步序列號位 表示要建立連結 TCP建立連結時要將這個值設為1
    • FIN傳送端完成位,提出斷開連線的一方把FIN置為1,表示要斷開連線
  • 16位視窗值 客戶端和服務端溝通好每次傳送多少資料


  • 16位TCP校驗和 校驗資料是否完整 TCP校驗和的計算包括TCP首部、資料和其它填充位元組。
  • 16位緊急指標 表示標記為URG的資料在TCP資料部分中的位置。

  • 可選項

  • 資料

URG和PUSH的區別

以下引用自 TCP報文段中URG和PSH的區別

緊急URG(urgent):

當URG = 1時表明緊急指標欄位有效,他告訴系統此報文段中有緊急資料,應儘快傳送,而不要按原來的排隊順序來傳送,傳送方的TCP就把緊急資料放到本報文段資料的最前面。URG標誌位要與首部中的緊急指標欄位配合使用,緊急指標指向資料段中的某個位元組,(資料從第一個位元組到指標所指的位元組就是緊急資料)。值得注意的是即使視窗為0時也可以傳送緊急資料,緊急資料不進入接收緩衝區直接交給上層程式。

推送PSH(push):

當兩個應用程式進行互動式通訊時,有時客戶發一個請求給伺服器時希望立即能夠收到對方的響應,這種情況下,客戶應用程式通知TCP使用推送(push)操作,TCP就把PSH置為1,並立即建立一個報文段傳送過去,類似的伺服器的TCP收到一個設了PSH標誌的報文段時就儘快將所有收到的資料立即提交給服務程式,而不在等到整個快取都填滿了再向上交付。

TCP三次握手四次揮手

三次握手

Q:為什麼要握手?而且要三次?

答:握手是因為要確保真正開始傳送資料之前,彼此(客戶端,服務端)收、發資料皆正常,而之所以要三次,嗯。。。請接著往下看

接下來我們來看詳細的過程


注意:[]中的為1位的訊號,後面帶=的是16位的序列號和確認號,是具體的編號。

01:客戶端 [SYN]seq=0---> 服務端

****** ****** ******

02:客戶端 <---[SYN,ACK]seq=0,ack=1 服務端

****** ****** ******

03:客戶端 [ACK]seq=1,ack=1---> 服務端


第一次握手,服務端接收到了客戶端發來的請求同步的資訊,服務端就知道了客戶端的傳送是正常的。(嘿,我我好喜歡你)

第二次握手,客戶端接收到了服務端發來的確認資訊和同步資訊,客戶端就知道了服務端的收發(兩樣)是正常的。(我也好喜歡你,我們結婚吧)

第三次握手,服務端接收到了客戶端發來的確認資訊,服務端就知道了客戶端的接收也是正常的。(嗯,我們結婚)

以上,就確保了彼此的收發訊息都是正常的。

四次揮手

Q:為什麼要揮手?而且要四次? 答:揮手是因為要和平分手,嗯。。。給對方以示意,有什麼還沒做完的搞快做,做完就了事。至於為什麼要四次,嗯。。老套路,請看詳細過程


首先和同步不一樣,分手時哪邊都可以提出分手

01:A方 [FIN,ACK]seq=xxx,ack=yyy---> B方

****** ****** ******

02:A方 <---[ACK]seq=yyy,ack=xxx+1 B方

****** ****** ******

03:A方 <---[FIN,ACK]seq=yyy,ack=xxx+1 B方

****** ****** ******

04:A方 [ACK]seq=xxx+1,ack=yyy+1---> B方

注意: 如果B方接受到A方的FIN時,恰巧也沒資料要傳送給A方了,那麼02和03會合併為一次

第一次揮手,A方表示自己已經沒有什麼要傳送給B方了,我要斷開連線了

第二次揮手,B方表示我已經知道到你(A方)要斷開連線了,稍等一下,我把剩下的資料發完

第三次揮手,B方表示我已經沒有資料要傳送了,你可以斷開連線了

第四次揮手,A方表示我已經收到你最後傳送的資料了,並且我已真正斷開連線,這是我的遺言,此時若B方接受到就會關閉自己的這邊

關於第四次揮手,A方揮手完畢後,還會等待2MSL(4min),如果此間又接收到B方傳送的FIN,則表示最後次揮手傳送的ACK對方沒有收到,就會重新傳送,並重新整理等待時間,直到2MSL內不再收到B放發來的FIN(表示B放已收到最後的ACK並且關閉),A方徹底斷開。

Node.js的tcp實現

基本介紹

Node.js 中用內建的 net 模組實現了 TCP 連線

let net = require('net');
let server = net.createServer(function(socket){
	...
}).lieten(8080);
複製程式碼

其中的 socket 俗稱為套接字,en...為嘛叫套接字?

我們通過socket能讀取到客戶端的輸入以及能向客戶端寫入資料。

注意: 預設連結最大個數(backlog)為511 server.listen(handle[, backlog][, callback])

tcp是長連線

長連線注意事項

需要注意的是socket是長連線,這意味著它會一直保持連線直到我們手動去關閉客戶端或則服務端表示要關閉連線。

另外因為是長連線,所以即使你每隔一段時間通過tcp連線向服務端傳送資訊, createServer 裡註冊的回撥函式也只會執行一次。(不像http,一次請求就會執行一次),所以我們一般還會在createServer裡包一層on('data')來實時監控客戶端的輸入以便做出響應。

net.createServer(function(socket){
    socket.on('data',function(buffer){
        console.log(socket._readableState.length);
    })
});
複製程式碼

設定超時

因為tcp連線並不像http連線一樣會自動中斷,So有可能存在一個socket長期不使用卻佔著位置的情況,一般這種時候我們就會規定一個超時時間來做出一些操作,比如詢問下人在不在啊(防掛機),要不要shuttdown啊什麼的。

socket.setTimeout(5000);
socket.on('timeout',function(){
	socket.write('喂喂,有人嗎?');
});
複製程式碼

關閉連線(socket)

方法一:客戶端手動關閉
方法二:socket.end(),伺服器讓客戶端關閉連線

此時就相當於四次揮手中服務端向客戶端提出分手[FIN,ACK]seq=xxx,ack=yyy

當客戶端接到後一般會將第二第三次揮手合併到一起,向服務端回覆[FIN,ACK]seq=yyy,ack=xxx+1,並且觸發socket.on('end')註冊的事件。

[warning] 注意: 這貨並不像ws.end,臨死之前還有遺言,會直接關掉socket套接字。

控制連線數

maxConnections

設定一個伺服器最大的連結數

server.maxConnections = 111;
複製程式碼
getConnections
server.getConnections(function(err,count){  //count為當前連線數
    console.log(`當前連線人數${count}人,最大容納${server.maxConnections}`)
})
複製程式碼

關閉伺服器

server.close()

呼叫server.close()後,server並不會立刻關閉所有連線,close只是表示服務端不再接受新的請求了,當前的連線(socket)還能繼續用。當所有客戶端(socket)全部關閉後伺服器才會關閉並觸發close事件。

server.unref()

通過呼叫 server.unref()方法, 當伺服器所有連線都關閉後,能讓伺服器自主關閉。這個方法和server.close的區別在於unref並不阻止新socket的進駐。

socket是一個雙工流

雙工流簡介

socket繼承自 Duplex(雙工流),Duplex是一個可讀可寫的流

Duplex長這樣

let {Duplex} = require('stream');
let d = Duplex({
    read(){
    	this.push('hello'); //不停止會一直以'hello'作為讀取值讀取
        this.push(null); //表示停止讀取
    }
    ,write(chunk,encoding,callback){
    	console.log(chunk);
        callback(); //clearBuffer
    }
})
複製程式碼

So,socket能使用一切可寫流和可讀流的方法進行讀取和寫入。

關於讀取

我們通過客戶端向服務端傳送資料照理說很像寫入,但在 socket 看來其實是讀取。(類似於process.stdin.pipe(transform1).pipe(transform2),其中stdin也是讀取

我們可以通過監聽 on('data') 事件來讀取客戶端的輸入。

socket.on('data',function(){});
複製程式碼

也可以通過socket.pause暫停可讀流,以及通過socket.resume繼續讀。

關於寫入

socket的可寫流層面和一般的可寫流一般無二,可寫流有的socket都有,write()flagdrain事件...

有一點要注意的是,socket的end,上面也說過,它是沒有遺言的,即是你end('something'),也不會有輸出。

關於pipe

let ws = fs.createWriteStream(path.join(__dirname,'./1.txt'));

let server = net.createServer(function(socket){
  socket.pipe(ws,{end:false}); // 第二個引數讓檔案不自動關閉
  setTimeout(function(){
    ws.end(); //關閉可寫流
    socket.unpipe(ws); //取消管道
  },15000);
});
複製程式碼

socket的其它屬性方法

socket.bufferSize

write()的緩衝區實時大小

埠被佔用解決方案

let port = 8080;
server.listen(port,'localhost',function(){
  console.log(`server is running at ${port}`);
})
server.on('error',function(err){
  if(err.code === 'EADDRINUSE'){
    server.listen(++port);
  }
});
複製程式碼

server和client

建立一個server

let net = require('net');

let server = net.createServer(function(socket){
  socket.setEncoding('utf8');

  socket.on('data',function(data){
    console.log(data); //讀
  })
  
  socket.write('ok'); //寫
  socket.end(); //關閉socket
});

server.on('connection',function(){ //注意這個事件和getConnections事件很相似,但getConnections有err和count引數
  console.log('客戶端連結');
})

server.listen(8080);
複製程式碼

建立一個server

  • net.createConnection(port[, host][, connectListener]) 預設host為localhost
  • net.connect(port[, host][, connectListener]) 是第一種的別名形式

不同於建立tcp伺服器時socket是作為回撥函式中的引數,建立客戶端的的時候,createConnection的返回值才是一個socket

let net = require('net');

// port 要連線到host的哪個埠
let socket = net.createConnection(8080,function(){
  socket.write('hello'); //寫
  socket.on('data',function(data){
    console.log(data); //讀
  });
});
複製程式碼

相關文章