emmmmmmm…
tcp
我們應該都知道,tcp是一種網路協議。
說起網路,在大學學計算機網路的時候,記得老師講過網路一共分7層,這7層從上到下依次是物理層、資料鏈路層、網路層、傳輸層、會話層、表示層和應用層。
我們來看張圖:
OSI是Open System Interconnection的縮寫,意為開放式系統互聯。國際標準化組織(ISO)制定了OSI模型,該模型定義了不同計算機互聯的標準,是設計和描述計算機網路通訊的基本框架。OSI模型把網路通訊的工作分為7層,分別是物理層、資料鏈路層、網路層、傳輸層、會話層、表示層和應用層
上面是基本網路模型的組成。我們再來看一下tcp/ip的參考模型
TCP/IP是傳輸控制協議/網路互聯協議的簡稱。早期的TCP/IP模型是一個四層結構,從下往上依次是網路介面層、網際網路層、傳輸層和應用層。後來在使用過程中,借鑑OSI七層參考模型,將網路介面層劃分為了物理層和資料鏈路層,形成五層結構。
我們看到了tcp/ip網路模型把osi中的應用層、表示層、會話層合併成了應用層。
Tcp協議實際上是在傳輸層。傳輸層是面向連線的、可靠的的程式到程式通訊的協議。TCP提供全雙工服務,即資料可在同一時間雙向傳播。TCP將若干個位元組構成一個分組,此分組稱為報文段(Segment)。提供了一種端到端的連線。 傳輸層的協議主要是TCP ,TCP(Transimision Control Protocal)是一種可靠的、面向連線的協議,傳輸效率低。
我們再來看一下tcp協議的格式
源埠號和目標埠號,計算機通過埠號識別訪問哪個服務,比如http服務或ftp服務,傳送方埠號是進行隨機埠,目標埠號決定了接收方哪個程式來接收。 32位序列號 TCP用序列號對資料包進行標記,以便在到達目的地後重新重灌,假設當前的序列號為s,傳送資料長度為 l,則下次傳送資料時的序列號為 s + l。在建立連線時通常由計算機生成一個隨機數作為序列號的初始值確認應答號 它等於下一次應該接收到的資料的序列號。假設傳送端的序列號為 s,傳送資料的長度為 l,那麼接收端返回的確認應答號也是 s + l。傳送端接收到這個確認應答後,可以認為這個位置以前所有的資料都已被正常接收。 首部長度:TCP 首部的長度,單位為 4 位元組。如果沒有可選欄位,那麼這裡的值就是 5。表示 TCP 首部的長度為 20 位元組。控制位 TCP的連線、傳輸和斷開都受這六個控制位的指揮 PSH(push急迫位) 快取區將滿,立刻傳輸速度 RST(reset重置位) 連線斷了重新連線 URG(urgent緊急位) 緊急訊號 ACK(acknowledgement 確認)為1表示確認號 SYN(synchronous建立聯機) 同步序號位 TCP建立連線時要將這個值設為1 FIN傳送端完成位,提出斷開連線的一方把FIN置為1表示要斷開連線 視窗值說明本地可接收資料段的數目,這個值的大小是可變的。當網路通暢時將這個視窗值變大加快傳輸速度,當網路不穩定時減少這個值可以保證網路資料的可靠傳輸。它是來在TCP傳輸中進行流量控制的 視窗大小:用於表示從應答號開始能夠接受多少個 8 位位元組。如果視窗大小為 0,可以傳送視窗探測。 效驗和: 用來做差錯控制,TCP校驗和的計算包括TCP首部、資料和其它填充位元組。在傳送TCP資料段時,由傳送端計算校驗和,當到達目的地時又進行一次檢驗和計算。如果兩次校驗 和一致說明資料是正確的,否則 將認為資料被破壞,接收端將丟棄該資料 緊急指標:盡在 URG(urgent緊急) 控制位為 1 時有效。表示緊急資料的末尾在 TCP 資料部分中的位置。通常在暫時中斷通訊時使用(比如輸入 Ctrl + C)。
三次握手
想必大家都知道這麼個東西。。那麼到底什麼是三次握手呢。。
舉個?
假如說我去咖啡店買咖啡。這時我要問店員了:請問有某某咖啡嗎?然後店員告訴我:有的。你帶錢了麼。然後我就說:我帶錢了,給來一杯把。。然後我們該給錢給錢該給咖啡給咖啡
我們來看這個例子。假如說我就是客戶端,店員是伺服器。
然後我去問伺服器,你可以建立連線嘛(一次握手)?伺服器說我可以建立連結,你可以建立連線嗎(二次握手)?這時我再回復他說,我可以建立連結(三次握手)…
emmmmm…然後我們兩個智障就愉快的建立了連結。。做一些偷偷摸摸傳東西的事情。。
為了更形象的來解讀一下三次握手,我們來看張圖:
我們可以很清楚的看到。
第一次握手: 建立連線。客戶端傳送連線請求,傳送SYN報文,將seq設定為0。然後,客戶端進入SYN_SEND狀態,等待伺服器的確認。
第二次握手: 伺服器收到客戶端的SYN報文段。需要對這個SYN報文段進行確認,傳送ACK報文,將ack設定為1。同時,自己還要傳送SYN請求資訊,將seq為0。伺服器端將上述所有資訊一併傳送給客戶端,此時伺服器進入SYN_RECV狀態。
第三次握手: 客戶端收到伺服器的ACK和SYN報文後,進行確認,然後將ack設定為1,seq設定為1,向伺服器傳送ACK報文段,這個報文段傳送完畢以後,客戶端和伺服器端都進入ESTABLISHED狀態,完成TCP三次握手。
四次揮手
我們再來舉個?
假如我到飯店去吃飯,我吃完了要買單。這時我會喊服務員買單->服務員說您要買單嗎->服務員說一共xxxx元->我給錢之後就可以走人了
比喻可能不太恰當。。但差不多這個意思,我們直接看圖
第一次揮手:客戶端向伺服器傳送一個FIN ACK報文段。此時,客戶端進入 FIN_WAIT_1狀態,這表示客戶端沒有資料要傳送伺服器了,請求關閉連線;
第二次揮手:伺服器收到了客戶端傳送的FIN報文段,向客戶端回一個ACK報文段。伺服器進入了CLOSE_WAIT狀態,客戶端收到伺服器返回的ACK報文後,進入FIN_WAIT_2狀態;
第三次揮手:伺服器會觀察自己是否還有資料沒有傳送給客戶端,如果有,先把資料傳送給客戶端,再傳送FIN報文;如果沒有,那麼伺服器直接傳送FIN報文給客戶端。請求關閉連線,同時伺服器進入LAST_ACK狀態;
第四次揮手:客戶端收到伺服器傳送的FIN報文段,向伺服器傳送ACK報文段。然後客戶端進入TIME_WAIT狀態;伺服器收到客戶端的ACK報文段以後,就關閉連線;此時,客戶端等待2MSL(大約4分鐘?)後依然沒有收到回覆,則證明Server端已正常關閉,客戶端也可以關閉連線了。
瞭解完三次握手和四次揮手後我們可能會有幾個問題
1.為什麼客戶端和伺服器需要三次握手
答:三次握手的主要目的是保證客戶端和服務端都是可以正常進行的收發資訊。
2.為什麼需要四次揮手?
答:四次揮手的目的是為了確保雙方的資料傳送完畢都可以斷開。
3.為什麼握手是三次,揮手卻是4次
答:server端收到fin報文時可能並不會立即關閉。關於這個我們想一下,有可能客戶端傳送斷開連線的fin報文,server還有資料需要傳輸,這時就不應該立即斷開連結。
nodejs中的TCP(net模組)
在Node.js中,net模組實現了基於TCP的資料通訊
我們來看一下建立一個簡單的tcp服務端的方法
建立TCP伺服器
let net = require(`net`);
let server = net.createServer(function(socket){
console.log(`客戶端已經連結`);
})
server.listen(`8080`,function(){
console.log(`server is run in 8080`);
})
複製程式碼
這樣就使用nodejs建立了一個簡單的tcp伺服器;
注意:createServer的回撥中有一個引數socket,指的是TCP伺服器監聽的socket埠物件。
設定最大連結數以及監聽客戶端的連結數量
server.getConnections((err,count)=>{//獲得當前的連結個數
console.log(`已經連結`+count+`個使用者`)
});
server.maxConnections = 2;//限制當前的最大連線個數為2
複製程式碼
socket物件
net.Socket代表一個socket埠物件,他是一個雙工流(可讀可寫)
address
獲取客戶端地址
let net = require(`net`);
let server = net.createServer(function(socket){
console.log(socket.address())
});
server.listen(8080);
複製程式碼
結果:
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(雙工流)中的東西pipe到可寫流中。
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.end(`服務端關閉`);
});
server.on(`connection`,function(){
console.log(`客戶端連結`);
})
server.listen(8080);
複製程式碼
建立client
let net = require(`net`);
//createConnection第一個引數是你要連線到伺服器的埠號
let socket = net.createConnection(8080,function(){
socket.write(`hello`); //向服務端傳送
socket.on(`data`,function(data){
console.log(data); //接受服務端資料
});
});
複製程式碼