TCP三次握手與四次分手

理解與掌握TCP的三次握手與四次分手是每個程序開發人員的基本功,讓咱們先從TCP首部開始吧。node

TCP首部

TCP工做在傳輸層,提供應用程序到應用程序之間的可靠傳輸。學習TCP協議,首先從TCP協議頭部開始: ios

這裏寫圖片描述
TCP協議頭部每一個字段說明一下以下:

  • Source Port和Destination Port:分別佔用16位,表示源端口號和目的端口號;用於區別主機中的不一樣進程,而IP地址是用來區分不一樣的主機的,源端口號和目的端口號配合上IP首部中的源IP地址和目的IP地址就能惟一的肯定一個TCP鏈接;
  • Sequence Number:用來標識從TCP發端向TCP收端發送的數據字節流,它表示在這個報文段中的的第一個數據字節在數據流中的序號;主要用來解決網絡報亂序的問題
  • Acknowledgment Number:32位確認序列號包含發送確認的一端所指望收到的下一個序號,所以,確認序號應當是上次已成功收到數據字節序號加1。不過,只有當標誌位中的ACK標誌(下面介紹)爲1時該確認序列號的字段纔有效。主要用來解決不丟包的問題
  • Offset:給出首部中32 bit字的數目,須要這個值是由於任選字段的長度是可變的。這個字段佔4bit(最多能表示15個32bit的的字,即4*15=60個字節的首部長度),所以TCP最多有60字節的首部。然而,沒有任選字段,正常的長度是20字節;
  • TCP Flags:TCP首部中有6個標誌比特,它們中的多個可同時被設置爲1,主要是用於操控TCP的狀態機的,依次爲URG,ACK,PSH,RST,SYN,FIN。每一個標誌位的意思以下:
    • URG:此標誌表示TCP包的緊急指針域(後面立刻就要說到)有效,用來保證TCP鏈接不被中斷,而且督促中間層設備要儘快處理這些數據;
    • ACK:此標誌表示應答域有效,就是說前面所說的TCP應答號將會包含在TCP數據包中;有兩個取值:0和1,爲1的時候表示應答域有效,反之爲0;
    • PSH:這個標誌位表示Push操做。所謂Push操做就是指在數據包到達接收端之後,當即傳送給應用程序,而不是在緩衝區中排隊;
    • RST:這個標誌表示鏈接復位請求。用來複位那些產生錯誤的鏈接,也被用來拒絕錯誤和非法的數據包;
    • SYN:表示同步序號,用來創建鏈接。SYN標誌位和ACK標誌位搭配使用,當鏈接請求的時候,SYN=1,ACK=0;鏈接被響應的時候,SYN=1,ACK=1;這個標誌的數據包常常被用來進行端口掃描。掃描者發送一個只有SYN的數據包,若是對方主機響應了一個數據包回來 ,就代表這臺主機存在這個端口;可是因爲這種掃描方式只是進行TCP三次握手的第一次握手,所以這種掃描的成功表示被掃描的機器不很安全,一臺安全的主機將會強制要求一個鏈接嚴格的進行TCP的三次握手;
    • FIN: 表示發送端已經達到數據末尾,也就是說雙方的數據傳送完成,沒有數據能夠傳送了,發送FIN標誌位的TCP數據包後,鏈接將被斷開。這個標誌的數據包也常常被用於進行端口掃描。
  • Window:窗口大小,也就是有名的滑動窗口,用來進行流量控制;

好,下面進入正題。c++

TCP三次握手與四次分手

爲了分析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是面向鏈接的,不管哪一方向另外一方發送數據以前,都必須先在雙方之間創建一條鏈接。在TCP/IP協議中,TCP協議提供可靠的鏈接服務,鏈接是經過三次握手進行初始化的。三次握手的目的是同步鏈接雙方的序列號和確認號並交換TCP窗口大小信息。下面經過上面給出的程序和wireshark抓包工具對TCP鏈接過程進行分析。微信

在這裏插入圖片描述

運行服務端程序,運行後服務端程序進入監聽狀態LISTEN網絡

在這裏插入圖片描述
啓動客戶端,開始TCP握手。

第一次: 客戶端向服務端發送SYN(創建鏈接請求),客戶端進入SYN_SENT狀態(握手中的中間狀態都很是短,很難看到,大部分看到的是LISTENESTABLISH)。以下圖所示: 併發

在這裏插入圖片描述
抓包(SYN):
在這裏插入圖片描述

第二次: 服務端接收到SYN,迴應SYN+ACK進入SYN+RCVD狀態(這個狀態很是短很難看到) 抓包(SYN+ACK):

在這裏插入圖片描述

第三次: 客戶端收到SYN+ACK後,迴應ACK進入ESTABLISH狀態。 抓包(ACK):

在這裏插入圖片描述

服務端收到ACK後,進入ESTABLISH狀態,握手完成,鏈接創建。

TCP斷開鏈接的過程——四次分手

當客戶端和服務器經過三次握手創建了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

在這裏插入圖片描述
第三次: 當主機B再也不須要鏈接時,向主機A發送鏈接釋放報文 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。這麼作有兩個理由:

  • 其一,確保最後一個確認報文可以到達。若是 B 沒收到 A 發送來的確認報文,那麼就會從新發送鏈接釋放請求報文,A 等待一段時間就是爲了處理這種狀況的發生。
  • 其二,等待一段時間是爲了讓本鏈接持續時間內所產生的全部報文都從網絡中消失,使得下一個新的鏈接不會出現舊的鏈接請求報文。

歡迎關注微信公衆號,按期推送TCP/IP、後端開發、區塊鏈、分佈式、Linux、Rust等技術文章!

相關文章
相關標籤/搜索