理解與掌握TCP的三次握手與四次分手是每個程序開發人員的基本功,讓咱們先從TCP首部開始吧。node
TCP工做在傳輸層,提供應用程序到應用程序之間的可靠傳輸。學習TCP協議,首先從TCP協議頭部開始: ios
TCP協議頭部每一個字段說明一下以下:好,下面進入正題。c++
爲了分析TCP握手與分手的細節,咱們編寫了服務端代碼和客戶端代碼,運行下面的程序,並進行抓包,經過抓包分析上面的握手與分手的過程。下面是用於分析TCP三次握手與四次分手過程用的程序代碼:windows
Rust編寫的服務端程序代碼:後端
use std::net::{TcpListener, TcpStream};
use std::io::prelude::*;
use std::thread;
fn main() {
{
let listener = TcpListener::bind("127.0.0.1:33333").unwrap();
let (mut stream, addr) = listener.accept().unwrap();
println!("tcp accept from {:?}", addr);
let mut buf = [0; 1024];
let size = stream.read(&mut buf).unwrap();
println!("receive from remote {} bytes data.", size);
thread::sleep_ms(1000);
}
thread::sleep_ms(6*1000);
}
複製代碼
或者c++編寫的服務端(windows)程序代碼:安全
// TcpServerSimple.cpp: 定義控制檯應用程序的入口點。
#include "stdafx.h"
#include<WinSock2.h>
#include<stdlib.h>
#include<WS2tcpip.h>
#include<string>
#include<iostream>
using namespace std;
#pragma comment(lib, "ws2_32.lib")
#define _WINSOCK_DEPRECATED_NO_WARNINGS
int main() {
WSADATA wsaData;
if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) {
cout << "Failed to load Winsock" << endl;
return -1;
}
SOCKET sockServer = socket(AF_INET, SOCK_STREAM, 0);
SOCKADDR_IN addrServer;
addrServer.sin_family = AF_INET;
addrServer.sin_port = htons(33333);
addrServer.sin_addr.S_un.S_addr = htonl(INADDR_ANY);
if (SOCKET_ERROR == bind(sockServer, (LPSOCKADDR)&addrServer, sizeof(SOCKADDR_IN))) {
cout << "Failed bind:" << WSAGetLastError() << endl;
return -1;
}
if (SOCKET_ERROR == listen(sockServer, 10)) {
cout<<"Listen failed:"<< WSAGetLastError() << endl;
return -1;
}
SOCKADDR_IN addrClient;
int len = sizeof(SOCKADDR);
SOCKET sockConn = accept(sockServer, (SOCKADDR*)&addrClient, &len);
if (SOCKET_ERROR == sockConn) {
cout << "Accept failed:" << WSAGetLastError() << endl;
return -1;
}
char addrBuf[20] = { '\0' };
inet_ntop(AF_INET, (void*)&addrClient.sin_addr, addrBuf, 16);
cout << "Accept from " << addrBuf << endl;
char recvBuf[1024];
memset(recvBuf, 0, sizeof(recvBuf));
int size = recv(sockConn, recvBuf, sizeof(recvBuf), 0);
cout << "received " << size << " from remote" << endl;
Sleep(1000);
closesocket(sockConn);
closesocket(sockServer);
WSACleanup();
system("pause");
return 0;
}
複製代碼
Rust實現以下:服務器
use std::io::prelude::*;
use std::net::TcpStream;
use std::thread;
fn main() {
{
let mut stream = TcpStream::connect("192.168.2.210:33333").unwrap();
let n = stream.write(&[1,2,3,4,5,6,7,8,9,10]).unwrap();
println!("send {} bytes to remote node, waiting for end.", n);
thread::sleep_ms(1000);
}
thread::sleep_ms(10*60*1000);
}
複製代碼
TCP是面向鏈接的,不管哪一方向另外一方發送數據以前,都必須先在雙方之間創建一條鏈接。在TCP/IP協議中,TCP協議提供可靠的鏈接服務,鏈接是經過三次握手進行初始化的。三次握手的目的是同步鏈接雙方的序列號和確認號並交換TCP窗口大小信息。下面經過上面給出的程序和wireshark抓包工具對TCP鏈接過程進行分析。微信
運行服務端程序,運行後服務端程序進入監聽狀態LISTEN
。 網絡
第一次: 客戶端向服務端發送SYN(創建鏈接請求),客戶端進入SYN_SENT
狀態(握手中的中間狀態都很是短,很難看到,大部分看到的是LISTEN
和ESTABLISH
)。以下圖所示: 併發
第二次: 服務端接收到SYN
,迴應SYN+ACK
進入SYN+RCVD
狀態(這個狀態很是短很難看到) 抓包(SYN+ACK):
第三次: 客戶端收到SYN+ACK
後,迴應ACK
進入ESTABLISH
狀態。 抓包(ACK):
服務端收到ACK
後,進入ESTABLISH
狀態,握手完成,鏈接創建。
當客戶端和服務器經過三次握手創建了TCP鏈接之後,當數據傳送完畢,確定是要斷開TCP鏈接的啊。那對於TCP的斷開鏈接,這裏就有了神祕的「四次分手」。
第一次: 主機A(能夠是客戶端也能夠是服務端,這裏主機A是客戶端首先發起斷開鏈接)發送鏈接釋放報文FIN
,此時,主機A進入FIN_WAIT_1
狀態,表示主機A沒有數據要發送給主機B了。 抓包FIN
:
第二次: 主機B收到了主機A發送的FIN
報文,向主機A回一個ACK
報文。主機A收到ACK
後進入FIN_WAIT_2
狀態,主機B進入CLOSE_WAIT
狀態。 抓包ACK
:
FIN
,請求關閉鏈接,同時主機B進入
LAST_ACK
狀態。 抓包
FIN
:
第四次: 主機A收到主機B發送的FIN
報文,向主機B發送ACK
報文,而後主機A進入TIME_WAIT
狀態;主機B收到主機A的ACK
報文之後,就關閉鏈接。此時,主機A 等待2MSL
(最大報文存活時間)後依然沒有收到回覆,則證實Server端已正常關閉,此時,主機A關閉鏈接。 抓包ACK
:
至此,TCP的四次分手完成,斷開鏈接。
最後,從代碼看抓包結果:
能夠明確的看到,首先是進行了3次握手,鏈接創建後進行了1次發送數據過程,最後是4次分手,結束。TCP創建鏈接的三次握手,爲何非要三次呢?兩次不行嗎?在謝希仁的《計算機網絡》中是這樣說的:
爲了防止已失效的鏈接請求報文段忽然又傳送到了服務端,於是產生錯誤。
在書中同時舉了一個例子,以下:
「已失效的鏈接請求報文段」的產生在這樣一種狀況下:client發出的第一個鏈接請求報文段並無丟失,而是在某個網絡結點長時間的滯留了,以至延誤到鏈接釋放之後的某個時間纔到達server。原本這是一個早已失效的報文段。但server收到此失效的鏈接請求報文段後,就誤認爲是client再次發出的一個新的鏈接請求。因而就向client發出確認報文段,贊成創建鏈接。假設不採用「三次握手」,那麼只要server發出確認,新的鏈接就創建了。因爲如今client並無發出創建鏈接的請求,所以不會理睬server的確認,也不會向server發送數據。但server卻覺得新的鏈接已經創建,並一直等待client發來數據。這樣,server的不少資源就白白浪費掉了。採用「三次握手」的辦法能夠防止上述現象發生。例如剛纔那種狀況,client不會向server的確認發出確認。server因爲收不到確認,就知道client並無要求創建鏈接。」
這就很明白了,防止了服務器端的一直等待而浪費資源。
可是三次握手的過程不是天衣無縫,也有一個問題,就是SYN Flood攻擊,主要攻擊手段是向服務端發送大量SYN請求鏈接,服務端響應SYN請求向客戶端發送SYN+ACK,可是,此時客戶端卻再也不向服務端發送最後的ACK,致使佔用了服務端大量的資源。這裏再也不細述。
TCP是全雙工模式,這是理解4次分手的關鍵,這就意味着,當A發出FIN
報文時,只是表示A已經沒有數據要發送了,並不意味着B不須要發送數據給A了,這個時候A仍是能夠接收來自B的數據;當B返回ACK
報文時,表示它已經知道A沒有數據發送了,可是B仍是能夠發送數據到A的。因此2次分手是不能夠的。當B再也不須要向A發送數據時,向A發送FIN
報文,告訴A,我也沒有數據要發送了,以後彼此就會中斷此次TCP鏈接。
四次分手過程當中的狀態:
狀態 | 解釋 |
---|---|
FIN_WAIT_1 | 這個狀態要好好解釋一下,其實FIN_WAIT_1和FIN_WAIT_2狀態的真正含義都是表示等待對方的FIN報文。而這兩種狀態的區別是:FIN_WAIT_1狀態其實是當SOCKET在ESTABLISHED狀態時,它想主動關閉鏈接,向對方發送了FIN報文,此時該SOCKET即進入到FIN_WAIT_1狀態。而當對方迴應ACK報文後,則進入到FIN_WAIT_2狀態,固然在實際的正常狀況下,不管對方何種狀況下,都應該立刻迴應ACK報文,因此FIN_WAIT_1狀態通常是比較難見到的,而FIN_WAIT_2狀態還有時經常能夠用netstat看到。(主動方) |
FIN_WAIT_2 | 上面已經詳細解釋了這種狀態,實際上FIN_WAIT_2狀態下的SOCKET,表示半鏈接,也即有一方要求close鏈接,但另外還告訴對方,我暫時還有點數據須要傳送給你(ACK信息),稍後再關閉鏈接。(主動方) |
CLOSE_WAIT | 這種狀態的含義實際上是表示在等待關閉。怎麼理解呢?當對方close一個SOCKET後發送FIN報文給本身,你係統毫無疑問地會迴應一個ACK報文給對方,此時則進入到CLOSE_WAIT狀態。接下來呢,實際上你真正須要考慮的事情是察看你是否還有數據發送給對方,若是沒有的話,那麼你也就能夠 close這個SOCKET,發送FIN報文給對方,也即關閉鏈接。因此你在CLOSE_WAIT狀態下,須要完成的事情是等待你去關閉鏈接。(被動方) |
LAST_ACK | 這個狀態仍是比較容易好理解的,它是被動關閉一方在發送FIN報文後,最後等待對方的ACK報文。當收到ACK報文後,也便可以進入到CLOSED可用狀態了。(被動方) |
TIME_WAIT | 表示收到了對方的FIN報文,併發送出了ACK報文,就等2MSL後便可回到CLOSED可用狀態了。若是FIN_WAIT_1狀態下,收到了對方同時帶FIN標誌和ACK標誌的報文時,能夠直接進入到TIME_WAIT狀態,而無須通過FIN_WAIT_2狀態。(主動方) |
CLOSED | 表示鏈接中斷。 |
爲何TIME_WAIT狀態要等待2MSL? 客戶端接收到服務器端的 FIN
報文後進入此狀態,此時並非直接進入 CLOSED
狀態,還須要等待一個時間計時器設置的時間 2MSL
。這麼作有兩個理由:
歡迎關注微信公衆號,按期推送TCP/IP、後端開發、區塊鏈、分佈式、Linux、Rust等技術文章!