VB.net 2010 视频教程 VB.net 2010 视频教程 python基础视频教程
SQL Server 2008 视频教程 c#入门经典教程 Visual Basic从门到精通视频教程
当前位置:
首页 > 编程开发 > c#编程 >
  • TCP协议深度解析(三次握手、四次挥手、滑动窗口)

第一部分:网络编程基础
第1章 网络编程核心概念
1.5 TCP协议深度解析(三次握手、四次挥手、滑动窗口)
一、为什么要啃TCP底层?
我刚做网络编程时,遇到过一个诡异的问题:客户端明明显示“连接成功”,但服务器就是收不到数据。查了三天,最后用Wireshark抓包发现——客户端发送的SYN报文被服务器的防火墙拦截了,三次握手只完成了两次。不懂TCP底层,遇到问vb.net教程C#教程python教程SQL教程access 2010教程题就是“瞎蒙”;懂了底层,就是“精准定位”。这节我把自己踩过的坑、调优的经验都揉进去,用大白话讲透三次握手、四次挥手、滑动窗口,再结合C#代码和抓包实战,让你不仅“背得下来”,更“用得起来”。
二、三次握手:建立连接的“敲门逻辑”
TCP是“面向连接”的协议——就像你去公司面试,必须先敲门(SYN)、HR开门回应(SYN+ACK)、你进门(ACK),这三步走完,才算“连接建立”,才能开始“交流”(传输数据)。

  1. 三次握手的底层流程(带抓包截图逻辑)
步骤 角色 动作 TCP报文标志 核心字段 实际场景类比
1 客户端 发起连接请求 SYN 序列号(Seq=x) 你敲门:“有人吗?我来面试”
2 服务器 确认收到请求,同步自己的序列号 SYN+ACK 序列号(Seq=y)、确认号(Ack=x+1) HR开门:“听到了,进来吧”
3 客户端 确认收到服务器的同步 ACK 确认号(Ack=y+1) 你进门:“好的,我进来了”

核心字段必须搞懂:
序列号(Seq):TCP给每个字节的数据都编了号,比如你发送“Hello”,H的Seq是100,e是101,以此类推。作用是保证数据不重复、不丢失、有序到达。
确认号(Ack):表示“我已经收到了Seq为x及之前的所有数据,下一个我要收Seq为x+1的”。比如服务器收到客户端的Seq=100,就回复Ack=101,意思是“我收到100之前的了,你继续发101及以后的”。
SYN/ACK/FIN:TCP报文的“标志位”,相当于报文的“类型标签”:
SYN(Synchronize):同步序列号,用于发起连接;
ACK(Acknowledgment):确认收到数据;
FIN(Finish):结束连接。
2. 为什么必须是三次握手?(踩过的坑)
我之前面试时被问过这个问题,当时答“因为要同步序列号”,面试官追问“两次不行吗?”,我就懵了。后来做项目时遇到过“历史连接残留”的问题,才彻底懂了:
两次握手的隐患:如果客户端发送的SYN报文在网络中“迷路”了,客户端超时后重新发了一个SYN,建立了连接并完成通信。但过了很久,之前迷路的SYN报文突然到了服务器,服务器会以为是新的连接请求,就回复SYN+ACK,但客户端已经不需要这个连接了,不会回复ACK,服务器就会一直等ACK,浪费资源。
三次握手的作用:第三次握手可以让服务器确认客户端的“接收能力”正常——如果服务器收到了第三次握手的ACK,就知道客户端能正常接收自己的报文,不会出现“服务器发了数据,客户端收不到”的情况。
3. C#实战:查看TCP连接状态(抓包前的准备)
在客户端连接服务器的瞬间,用C#代码查看TCP连接的状态,可以看到三次握手的中间状态:
csharp

	using System;
	using System.Net;
	using System.Net.NetworkInformation;
	using System.Threading;
	
	namespace TcpHandshakeDemo;
	
	class Program
	{
	static void Main(string[] args)
	{
	// 启动TCP服务器(后台线程)
	new Thread(StartServer).Start();
	Thread.Sleep(1000); // 等服务器启动
	
	// 启动TCP客户端,发起连接
	var client = new TcpClient();
	client.BeginConnect("127.0.0.1", 8888, null, null);
	
	// 每隔100ms查看一次TCP连接状态
	for (int i = 0; i < 10; i++)
	{
	ShowTcpConnections();
	Thread.Sleep(100);
	}
	
	client.Close();
	}
	
	static void StartServer()
	{
	var listener = new TcpListener(IPAddress.Any, 8888);
	listener.Start();
	listener.AcceptTcpClient(); // 阻塞等待连接
	}
	
	static void ShowTcpConnections()
	{
	var connections = IPGlobalProperties.GetIPGlobalProperties().GetActiveTcpConnections();
	Console.WriteLine($"===== 第{DateTime.Now.Millisecond}ms 连接状态 =====");
	foreach (var conn in connections)
	{
	if (conn.LocalEndPoint.ToString().Contains(":8888"))
	{
	Console.WriteLine($"本地地址:{conn.LocalEndPoint}");
	Console.WriteLine($"远程地址:{conn.RemoteEndPoint}");
	Console.WriteLine($"连接状态:{conn.State}");
	Console.WriteLine("------------------------");
	}
	}
	}
	}

代码逐行讲:
1.StartServer:后台线程启动TCP服务器,调用AcceptTcpClient()阻塞等待连接;
2.client.BeginConnect:客户端异步发起连接,避免阻塞主线程;
3.ShowTcpConnections:用IPGlobalProperties获取系统所有TCP连接,筛选出端口为8888的连接,打印状态;
4.运行结果:你会看到连接状态从SynSent(客户端发了SYN,等服务器的SYN+ACK)变成Established(三次握手完成,连接建立)。
4. 抓包实战:用Wireshark看三次握手
1.打开Wireshark,选择“Loopback: lo0”(本地回环网卡);
2.设置过滤规则:tcp.port == 8888,只看8888端口的TCP报文;
3.运行上面的C#代码,你会看到三个TCP报文:
第一个:客户端→服务器,SYN标志,Seq=x;
第二个:服务器→客户端,SYN+ACK标志,Seq=y,Ack=x+1;
第三个:客户端→服务器,ACK标志,Ack=y+1;
4.这就是完整的三次握手!
三、四次挥手:断开连接的“告别逻辑”
TCP是“全双工”通信——就像你和朋友打电话,你可以说话,同时也能听朋友说话。所以断开连接时,需要分别关闭“发送通道”和“接收通道”,这就是为什么要四次挥手,而不是三次。

  1. 四次挥手的底层流程(带抓包截图逻辑)
步骤 角色 动作 TCP报文标志 核心字段 实际场景类比
1 客户端 关闭自己的发送通道,通知服务器 FIN+ACK 序列号(Seq=u)、确认号(Ack=v) 你说:“我说完了,挂了啊”
2 服务器 确认收到关闭请求 ACK 确认号(Ack=u+1) 朋友说:“好的,我知道了”
3 服务器 关闭自己的发送通道,通知客户端 FIN+ACK 序列号(Seq=v)、确认号(Ack=u+1) 朋友说:“我也说完了,挂吧”
4 客户端 确认收到服务器的关闭请求 ACK 确认号(Ack=v+1) 你说:“好的,挂了”

核心细节必须搞懂:
为什么是四次挥手?:因为TCP是全双工,客户端关闭发送通道时,服务器可能还在发送数据,所以服务器需要先回复ACK确认收到关闭请求,等自己的数据发完了,再发送FIN关闭自己的发送通道,客户端再回复ACK确认。
TIME_WAIT状态:客户端发送最后一个ACK后,会进入TIME_WAIT状态,等待2MSL(最大报文生存时间,默认2分钟)。作用是:
1.确保服务器能收到最后一个ACK,如果服务器没收到,会重发FIN,客户端在TIME_WAIT状态下能再次回复ACK;
2.避免历史连接的残留报文干扰新连接——2MSL足够让网络中所有残留的报文都消失。
2. C#实战:主动断开TCP连接(触发四次挥手)
csharp

	using System;
	using System.Net.Sockets;
	using System.Threading.Tasks;
	
	namespace TcpDisconnectDemo;
	
	class Program
	{
	static async Task Main(string[] args)
	{
	var client = new TcpClient();
	await client.ConnectAsync("127.0.0.1", 8888);
	Console.WriteLine("连接已建立");
	
	// 先关闭发送通道,再关闭接收通道(模拟四次挥手的完整流程)
	client.Client.Shutdown(SocketShutdown.Send); // 发送FIN报文
	Console.WriteLine("已关闭发送通道,等待服务器响应");
	
	// 读取服务器的FIN报文
	var buffer = new byte[1024];
	int bytesRead = await client.GetStream().ReadAsync(buffer, 0, buffer.Length);
	if (bytesRead == 0)
	{
	Console.WriteLine("收到服务器的FIN报文,连接即将断开");
	}
	
	client.Client.Shutdown(SocketShutdown.Receive); // 关闭接收通道
	client.Close();
	Console.WriteLine("连接已断开");
	}
	}

代码逐行讲:
1.client.Client.Shutdown(SocketShutdown.Send):关闭Socket的发送通道,底层会发送FIN报文,通知服务器“我不发数据了”;
2.client.GetStream().ReadAsync:读取服务器的FIN报文,当服务器关闭发送通道时,会发送一个长度为0的报文,bytesRead返回0;
3.client.Client.Shutdown(SocketShutdown.Receive):关闭Socket的接收通道,完成四次挥手;
4.抓包验证:用Wireshark抓包,你会看到四个TCP报文:FIN+ACK、ACK、FIN+ACK、ACK,这就是完整的四次挥手!
四、滑动窗口:控制传输速度的“流量阀”
我之前做文件传输工具,用默认的滑动窗口,传1GB文件要10分钟;后来把滑动窗口调大,只用了1分钟。滑动窗口是TCP性能优化的核心——就像你给朋友搬箱子,朋友一次只能拿5个(接收窗口大小),你就不能一次递10个,否则朋友接不住,箱子会掉地上(数据丢失)。

  1. 滑动窗口的底层原理(大白话版)
    接收窗口:朋友一次能拿的箱子数量,由朋友的“体力”(接收缓冲区大小)决定。比如朋友的接收缓冲区是1024字节,接收窗口就是1024。
    发送窗口:你一次能递的箱子数量,等于朋友的接收窗口大小。比如朋友说“我一次能拿5个”,你就一次递5个,不能多。
    滑动逻辑:朋友每拿完5个,就告诉你“我拿完了,再递5个”,你的发送窗口就“滑动”一下,继续递下5个。
    TCP报文里的“Window Size”字段:就是接收方告诉发送方“我的接收窗口还有这么大,你最多能发这么多数据”。比如接收方的接收缓冲区还剩1024字节,就回复Window Size=1024,发送方就知道最多能发1024字节的数据。
  2. 滑动窗口的优势(为什么比“停等协议”快?)
    停等协议:发一个包,等对方确认,再发下一个。比如发1000个包,要等1000次确认,速度慢得要死。
    滑动窗口:可以连续发N个包,等N个包的确认,再继续发。比如发送窗口是100,发1000个包只需要等10次确认,速度提升10倍。
  3. C#实战:调整滑动窗口大小(性能优化)
    TCP的滑动窗口大小依赖于Socket的发送缓冲区和接收缓冲区大小,默认是8KB左右,我们可以手动调大:
    csharp
	using System;
	using System.Net;
	using System.Net.Sockets;
	using System.Text;
	using System.Threading.Tasks;
	
	namespace TcpWindowDemo;
	
	class Program
	{
	static async Task Main(string[] args)
	{
	var server = new TcpListener(IPAddress.Any, 8888);
	server.Start();
	Console.WriteLine("服务器已启动");
	
	// 调整服务器的接收缓冲区大小(影响接收窗口)
	server.Server.ReceiveBufferSize = 1024 * 1024; // 1MB
	Console.WriteLine($"服务器接收缓冲区大小:{server.Server.ReceiveBufferSize}");
	
	var client = new TcpClient();
	// 调整客户端的发送缓冲区大小(影响发送窗口)
	client.SendBufferSize = 1024 * 1024; // 1MB
	Console.WriteLine($"客户端发送缓冲区大小:{client.SendBufferSize}");
	
	await client.ConnectAsync("127.0.0.1", 8888);
	var stream = client.GetStream();
	
	// 发送1MB数据
	var buffer = new byte[1024 * 1024];
	Encoding.UTF8.GetBytes("Test Data").CopyTo(buffer, 0);
	await stream.WriteAsync(buffer, 0, buffer.Length);
	Console.WriteLine("已发送1MB数据");
	
	client.Close();
	server.Stop();
	}
	}

代码逐行讲:
1.server.Server.ReceiveBufferSize:设置服务器的接收缓冲区大小为1MB,这样服务器的接收窗口最大可以达到1MB;
2.client.SendBufferSize:设置客户端的发送缓冲区大小为1MB,这样客户端的发送窗口最大可以达到1MB;
3.性能对比:用默认的8KB缓冲区,发送1MB数据需要约100次系统调用;用1MB缓冲区,只需要1次系统调用,性能提升明显;
4.注意事项:缓冲区大小不能超过操作系统的限制(Windows默认最大为8MB,Linux默认最大为16MB),过大的缓冲区会浪费内存,反而降低性能。
五、基础知识拓展(面试/调优必懂)

  1. TCP的可靠传输机制(为什么不会丢包?)
    TCP通过以下4个机制保证数据100%可靠到达:
    序列号与确认号:每个字节都有编号,接收方通过确认号确认收到的数据,没收到的会要求重传;
    重传机制:如果发送方在超时时间内没收到ACK,就重传报文;如果收到3个重复的ACK(比如连续收到Ack=101三次),就立即重传Seq=101的报文(快速重传);
    校验和:每个TCP报文都有校验和,接收方会验证数据是否损坏,损坏的会要求重传;
    滑动窗口:控制发送速率,避免接收方处理不过来,导致数据丢失。
  2. TCP的拥塞控制(为什么不会把网络堵死?)
    TCP通过以下4个算法避免网络拥堵:
    慢启动:初始发送窗口很小(比如1个报文),每次收到ACK就翻倍,直到达到慢启动阈值(默认65535字节);
    拥塞避免:达到慢启动阈值后,每次收到ACK就增加1个报文,避免网络拥堵;
    快速重传:收到3个重复的ACK,立即重传丢失的报文,不需要等待超时;
    快速恢复:重传后,把慢启动阈值设置为当前窗口的一半,继续拥塞避免,而不是回到慢启动。
  3. 常见TCP问题排查工具
    Wireshark:抓包神器,能看到每个TCP报文的标志位、序列号、确认号、窗口大小等;
    netstat:Windows/Linux都有,查看TCP连接状态、端口占用情况(netstat -ano | findstr :8888);
    tcpdump:Linux下的抓包工具,命令行版Wireshark(tcpdump -i lo port 8888);
    Ping/Traceroute:排查网络连通性,Ping看是否能通,Traceroute看数据包走了哪些路由。
    六、总结:TCP是“可靠”的代名词
    三次握手:建立可靠连接,同步序列号,避免历史连接残留;
    四次挥手:断开全双工连接,确保双方都能完成数据传输;
    滑动窗口:实现流量控制,提高传输效率,是TCP性能优化的核心;
    可靠传输:通过序列号、确认号、重传机制、校验和等,保证数据100%可靠到达。
    下一节我们会用C#实战TCP的高级特性:自定义协议(解决粘包问题)、心跳检测(维持长连接)、高并发处理(支持百万级连接),把这节的底层原理落地成可复用的代码。

 本站原创,转载请注明出处:https://www.xin3721.com/ArticlecSharp/c49512.html


相关教程