C# 语言进阶(十)TCP网络
本篇核心知识点:TCP 三次握手完整流程、TCP 四次挥手完整流程、阻塞 IO 缺陷、多线程解决阻塞、网络通用 DLL 库封装、服务端 / 客户端 Socket 分层封装、项目复用方案、网络异常崩溃处理、面试高频网络考点
一、TCP 三次握手(连接建立)
1. 核心概念
TCP 是面向连接可靠协议,客户端调用Connect()、服务端调用Accept()底层自动完成三次握手,对上层代码透明,无需手动发送同步包;作用确认双方收发能力,防止失效连接请求占用服务端资源。
2. 关键标识释义
SY:同步标志,发起连接请求
ACK:确认标志,回复对方收到数据
3. 分步完整流程
第一次握手(客户端→服务端)
客户端调用 Connect,发送 SY 同步报文,客户端状态变为
SY_SENT,向服务端发起连接申请。第二次握手(服务端→客户端)
服务端处于 Listen 监听状态,收到 SY 报文,回复
SY+ACK报文,同步自身连接意愿并确认收到客户端请求,服务端进入SY_RCVD状态。第三次握手(客户端→服务端)
客户端收到服务端双标志报文,单独回复 ACK 确认报文,双方切换至
ESTABLISHED稳定传输状态,握手完成,可正常收发数据。
4. 工程对应代码触发点
客户端Socket.Connect()内部封装完整三次握手;
服务端Socket.Accept()阻塞等待握手完成后,才返回客户端套接字。
5. 拓展:为什么需要三次握手
防止历史失效连接报文延迟到达服务端,建立无效连接占用端口;双向验证发送、接收通道正常,保障后续数据可靠传输。
二、TCP 四次挥手(连接断开)
1. 核心概念
TCP 全双工通道,收发通道独立,关闭时双方需分别告知对方不再发送数据,共四次报文交互;调用Socket.Close()底层自动触发挥手流程。
2. 分步完整流程
第一次挥手(主动关闭方)
客户端 / 服务端调用 Close,发送 FIN 终止报文,进入
FIN_WAIT_1状态,表示本方不再发送新数据,但仍能接收对方剩余消息。第二次挥手(被动方回复 ACK)
被动端收到 FIN,回复 ACK 确认报文,进入
CLOSE_WAIT,此时被动端可继续发送未传输完毕数据。第三次挥手(被动方发 FIN)
被动端所有数据发送完毕,发送 FIN 报文告知对方无数据再发送。
第四次挥手(主动方 ACK 确认)
主动端收到 FIN,回复 ACK,等待一段时间无报文后彻底释放套接字资源,连接完全关闭。
3. 代码层面问题
直接关闭客户端进程会强制断开,跳过完整挥手流程,服务端 Receive 读取数据返回 0,未做判断会直接抛出异常导致程序崩溃。
4. 拓展:四次挥手必要性
全双工读写分离,一方发完数据不代表另一方数据传输完毕,必须分两次告知终止发送,保证缓冲区残留数据全部接收。
三、阻塞 IO 致命缺陷(基础单线程网络问题)
1. 阻塞方法清单
Accept()、Receive()均为阻塞方法,线程执行时暂停,直到有客户端连接 / 收到数据才恢复运行。
2. 单线程致命问题
服务端主线程卡在
Accept(),已有客户端发消息无法处理;收发逻辑阻塞时,无法同时处理新客户端接入;
客户端 Receive 阻塞,无法手动输入新消息发送;
单个客户端断开会直接导致整个服务端程序崩溃。
3. 解决方案:多线程分离收发逻辑
概念
每一条客户端连接分配独立子线程处理收发,子线程内部阻塞不会干扰主线程 / 其他客户端;主线程仅负责监听新连接。
运行逻辑
主线程循环 Accept 捕获新客户端 Socket;
每拿到客户端套接字,新建线程专门处理该客户端消息收发;
子线程内循环 Receive、业务处理,阻塞仅影响自身线程。
代码片段(多线程服务端基础)
// 单个客户端独立消息处理线程 static void ClientHandle(Socket clientSocket){ byte[] buffer = new byte[1024]; while (true){ int len = clientSocket.Receive(buffer); if (len <= 0){ // 客户端断开连接 Console.WriteLine("客户端下线"); clientSocket.Close(); break; } string msg = System.Text.Encoding.UTF8.GetString(buffer, 0, len); Console.WriteLine("收到消息:" + msg); // 回发消息 byte[] sendBuf = System.Text.Encoding.UTF8.GetBytes("服务端已收到:" + msg); clientSocket.Send(sendBuf); } } // 服务主线程 static void Main(){ Socket server = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); IPEndPoint ep = new IPEndPoint(IPAddress.Any, 8080); server.Bind(ep); server.Listen(100); Console.WriteLine("服务端启动,等待连接..."); while (true){ Socket client = server.Accept(); // 主线程仅阻塞监听 // 新开线程处理客户端,互不阻塞 new Thread(() => ClientHandle(client)).Start(); Console.WriteLine("新客户端接入"); } }四、通用网络 DLL 库封装(项目复用核心)
1. 封装目的
分离网络底层代码与业务逻辑,服务端控制台、Unity 客户端共用同一套网络逻辑;
避免每个项目重复编写 Socket 连接、收发、编码代码;
统一异常处理、断线检测,减少重复 bug。
2. VS 项目区分
控制台程序:输出 exe 可执行程序,独立运行;
类库项目(Class Library):输出.dll动态链接库,无 Main 入口,仅提供可调用类;
3. 库分层规范(命名空间隔离)
MyNet.Server:TCP 服务端封装类MyNet.Client:TCP 客户端封装类
4. 服务端封装类完整结构
using System; using System.Net; using System.Net.Sockets; using System.Threading; namespace MyNet.Server{ public class TcpServer{ private Socket _serverSocket; private int _port; // 构造函数:绑定指定端口 public TcpServer(int port){ _port = port; _serverSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); IPEndPoint ep = new IPEndPoint(IPAddress.Any, _port); _server.Bind(ep); } // 启动监听,主线程阻塞监听,新连接开子线程 public void Start(){ _serverSocket.Listen(100); Console.WriteLine($"服务端启动,端口{_port}"); while (true){ Socket client = _serverSocket.Accept(); Thread t = new Thread(() => HandleClient(client)); t.IsBackground = true; t.Start(); } } // 单个客户端消息循环处理 private void HandleClient(Socket client){ byte[] buf = new byte[1024]; while (true){ int recvLen; try{ recvLen = client.Receive(buf); } catch{ break; } if (recvLen <= 0) break; string msg = Encoding.UTF8.GetString(buf, 0, recvLen); Console.WriteLine("客户端消息:" + msg); // 回包逻辑 byte[] send = Encoding.UTF8.GetBytes($"服务回复:{msg}"); client.Send(send); } client.Close(); Console.WriteLine("客户端断开连接"); } } }5. 客户端封装类核心结构
namespace MyNet.Client{ public class TcpClient{ private Socket _clientSocket; private string _ip; private int _port; public TcpClient(string ip, int port){ _ip = ip; _port = port; _clientSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); } // 建立连接(底层自动三次握手) public bool Connect(){ try{ IPEndPoint ep = new IPEndPoint(IPAddress.Parse(_ip), _port); _clientSocket.Connect(ep); // 新开线程持续接收服务端推送消息 new Thread(RecvLoop).Start(); return true; } catch{ Console.WriteLine("连接失败"); return false; } } // 发送字符串消息封装 public void SendMsg(string msg){ byte[] data = Encoding.UTF8.GetBytes(msg); _clientSocket.Send(data); } // 后台持续接收消息 private void RecvLoop(){ byte[] buf = new byte[1024]; while (true){ int len = _clientSocket.Receive(buf); if (len <= 0) break; string res = Encoding.UTF8.GetString(buf, 0, len); Console.WriteLine("服务端返回:" + res); } } // 主动关闭连接,触发四次挥手 public void Close(){ _clientSocket.Close(); } } }6. DLL 使用方式
编译类库项目生成
MyNet.dll;控制台 / Unity 项目右键引用该 dll;
头部添加
using MyNet.Server;直接实例化类调用方法。
五、字符串与字节数组转换规范
1. 转换核心类:System.Text.Encoding.UTF8
发送:字符串 → byte 数组
string text = "玩家登录"; byte[] sendBuffer = Encoding.UTF8.GetBytes(text); socket.Send(sendBuffer);接收:byte 数组 → 字符串
Receive 返回的len是实际收到字节长度,必须截取有效字节,避免多余空字符
byte[] recvBuf = new byte[1024]; int realLen = socket.Receive(recvBuf); string res = Encoding.UTF8.GetString(recvBuf, 0, realLen);拓展:统一编码规范
项目全部使用 UTF8,避免 GBK/Unicode 混用导致中文乱码。
六、网络异常与断线容错处理
1. 崩溃根源
客户端强制关闭、网络波动时Receive()、Send()会抛出 Socket 异常,无 try-catch 捕获直接程序终止。
2. 标准容错方案
所有 Socket 读写操作包裹try-catch,捕获异常后主动关闭套接字、退出循环。
容错代码示例
private void HandleClient(Socket client){ byte[] buf = new byte[1024]; while (true){ int len; try{ len = client.Receive(buf); } catch (SocketException ex){ Console.WriteLine("网络异常:" + ex.Message); break; } if (len <= 0) break; // 业务处理逻辑 } client.Close(); }七、实战交互流程(控制台长连接聊天)
启动服务端 TcpServer,开始监听端口;
客户端调用
Connect()建立 TCP 长连接,三次握手完成;客户端控制台输入文字,调用
SendMsg()发送;服务端子线程收到消息,自动回复;
客户端后台线程持续打印服务端回复;
客户端调用
Close()主动断开,执行四次挥手正常释放资源。
八、重点
TCP 三次握手每一步报文、作用,为什么不能两次握手;
TCP 四次挥手完整流程,为什么不能三次关闭;
Connect/Accept/Receive阻塞带来的单线程缺陷,多线程解决方案;类库 DLL 封装优势,服务端 / 客户端分层设计;
Socket 收发字节数组编码转换,UTF8 使用规范;
客户端异常下线程序崩溃解决方案(try-catch + 线程隔离);
IPAddress.Any 与 127.0.0.1 区别:Any 监听本机所有网卡,仅本地测试用回环地址。
