- 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
- 端口被占用解決方案
[TOC]
## pre-notify
參考
- [深入理解TCP/IP模型](https://juejin.im/post/5a7c4ebaf265da4e81239431)
- [TCP四次揮手](https://blog.jiar.vip/2017/08/24/TCP%E5%9B%9B%E6%AC%A1%E6%8C%A5%E6%89%8B%E7%AE%80%E4%BB%8B/)
- [或許這樣能幫你了解 OSI 七層模型](https://juejin.im/post/59eb06b1f265da430f313c7f)
- [TCP報文段中URG和PSH的區別](https://blog.csdn.net/wenqiang1208/article/details/72669378)
## 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位保留位
- 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的區別](https://blog.csdn.net/wenqiang1208/article/details/72669378)
緊急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()`、`flag`、`drain事件`...
有一點要注意的是,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);
});
});
```