NodeJS和TCP:一本通

  • 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...bash


TCP簡介

TCP格式(Segment)

每一行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位保留位socket

  • 6位控制位 TCP的鏈接、傳輸和斷開都接受這個六個控制位的指揮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的區別post

緊急URG(urgent):ui

當URG = 1時代表緊急指針字段有效,他告訴系統此報文段中有緊急數據,應儘快傳送,而不要按原來的排隊順序來傳送,發送方的TCP就把緊急數據放到本報文段數據的最前面。URG標誌位要與首部中的緊急指針字段配合使用,緊急指針指向數據段中的某個字節,(數據從第一個字節到指針所指的字節就是緊急數據)。值得注意的是即便窗口爲0時也能夠發送緊急數據,緊急數據不進入接收緩衝區直接交給上層進程。this

推送PSH(push):spa

當兩個應用進程進行交互式通訊時,有時客戶發一個請求給服務器時但願當即可以收到對方的響應,這種狀況下,客戶應用程序通知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); //讀
  });
});
複製代碼
相關文章
相關標籤/搜索