理解与掌握TCP的三次握手与四次分手是每个程序开发人员的基本功,让咱们先从TCP首部开始吧。node
TCP工做在传输层,提供应用程序到应用程序之间的可靠传输。学习TCP协议,首先从TCP协议头部开始: ios
好,下面进入正题。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的四次分手完成,断开链接。
最后,从代码看抓包结果:
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等技术文章!
![]()