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等技术文章!

相关文章
相关标签/搜索