TCP 网络编程笔记:TcpListener、TcpClient 与核心用法
TCP 网络编程笔记:TcpListener、TcpClient 与核心用法
一、核心类定位与关系
TCP 网络编程中,TcpListener
、TcpClient
是基于 Socket
的高层封装类,三者定位不同但协同实现 TCP 通讯,核心关系如下:
类名 | 核心作用 | 底层依赖 | 适用角色 |
---|---|---|---|
Socket | 底层网络通讯基础类,支持 TCP/UDP 等协议 | 直接操作网络底层 | 需精细控制网络细节的场景 |
TcpListener | 简化 TCP 服务器端监听逻辑,处理客户端连接请求 | 封装 Socket(TCP 协议) | TCP 服务器 |
TcpClient | 简化 TCP 客户端连接逻辑,处理数据收发 | 封装 Socket(TCP 协议) | TCP 客户端 |
二、TcpListener 与 TcpClient 核心差异(vs Socket)
对比维度 | Socket(底层) | TcpListener / TcpClient(高层封装) |
---|---|---|
创建连接 | 需手动指定协议(TCP/UDP)、IP、端口,代码繁琐 | TcpClient 直接调用 Connect() 建立连接,无需关注底层协议细节 |
服务器监听 | 需手动调用 Bind() 绑定端口、Listen() 开启监听、Accept() 接收连接 | TcpListener 直接调用 Start() 开启监听,AcceptTcpClient() 接收连接,流程简化 |
复杂性 | 需手动处理数据分包 / 组包、错误捕获、缓冲区管理 | 隐藏底层细节,提供 NetworkStream 简化数据读写,开发专注业务逻辑 |
API 友好度 | 接口偏底层,需理解网络协议细节 | 接口高层化,如 GetStream() 直接获取数据流,降低学习成本 |
三、核心 API 与使用流程
1. 基础准备:IP 与端口控制(EndPoint 家族)
TCP 通讯需明确 IP 地址和端口,依赖以下类实现:
EndPoint:抽象基类,定义网络端点的基本结构。
IPEndPoint:
EndPoint
的子类,具体实现 IP 地址(IPAddress
)和端口(int
)的绑定,格式:new IPEndPoint(IPAddress.Parse("127.0.0.1"), 8888)
。关键属性:
TcpListener.LocalEndpoint
:获取 TcpListener(服务器)绑定的本地 IP 和端口。TcpClient.Client.RemoteEndPoint
:获取 TcpClient 连接的远程端(服务器 / 客户端)IP 和端口。TcpClient.Client.LocalEndPoint
:获取 TcpClient 本地端(客户端 / 服务器)IP 和端口。
2. TcpListener(服务器端)使用流程
步骤 1:实例化 TcpListener
指定监听的 IP 地址和端口,两种方式:
// 方式 1:直接传入 IPAddress 和端口 IPAddress ip = IPAddress.Parse("127.0.0.1"); // 本地回环地址,仅本地访问 int port = 8888; TcpListener server = new TcpListener(ip, port); // 方式 2:传入 IPEndPoint(更灵活,支持多 IP 绑定) IPEndPoint endPoint = new IPEndPoint(ip, port); TcpListener server = new TcpListener(endPoint);
步骤 2:启动监听
调用 Start()
开启服务器监听,等待客户端连接:
server.Start(); // 启动监听,可传入参数限制最大连接数(如 server.Start(10)) Console.WriteLine("服务器已启动,等待客户端连接...");
步骤 3:接收客户端连接
调用 AcceptTcpClient()
(阻塞式)或 AcceptTcpClientAsync()
(异步)接收连接,返回与客户端通讯的 TcpClient
对象:
// 同步接收(阻塞当前线程) TcpClient client = server.AcceptTcpClient(); Console.WriteLine($"客户端已连接:{client.Client.RemoteEndPoint}"); // 异步接收(推荐,不阻塞线程) TcpClient client = await server.AcceptTcpClientAsync();
步骤 4:数据读写(通过 NetworkStream)
通过 TcpClient.GetStream()
获取 NetworkStream
,实现数据收发:
NetworkStream stream = client.GetStream(); // 获取数据流 // 读取客户端数据(需先判断是否有可用数据) byte[] buffer = new byte[1024]; // 缓冲区(大小根据业务调整) if (stream.DataAvailable) // 或 client.Available > 0,判断是否有数据 {int readLen = stream.Read(buffer, 0, buffer.Length); // 读取数据到缓冲区string recvMsg = Encoding.UTF8.GetString(buffer, 0, readLen); // 转字符串Console.WriteLine($"收到客户端消息:{recvMsg}"); } // 向客户端发送数据 string sendMsg = "Hello Client!"; byte[] sendBuffer = Encoding.UTF8.GetBytes(sendMsg); stream.Write(sendBuffer, 0, sendBuffer.Length); // 发送数据
步骤 5:关闭资源
通讯结束后,需释放 NetworkStream
、TcpClient
和 TcpListener
:
stream.Close(); // 关闭数据流 client.Close(); // 关闭与客户端的连接 server.Stop(); // 停止服务器监听(释放端口)
3. TcpClient(客户端)使用流程
步骤 1:实例化 TcpClient
直接创建对象,无需提前指定 IP 和端口:
TcpClient tcpClient = new TcpClient();
步骤 2:连接服务器
调用 Connect()
(同步)或 ConnectAsync()
(异步)连接服务器:
// 同步连接(阻塞线程) string serverIp = "127.0.0.1"; int serverPort = 8888; tcpClient.Connect(serverIp, serverPort); // 异步连接(推荐) await tcpClient.ConnectAsync(serverIp, serverPort); Console.WriteLine($"已连接服务器:{tcpClient.Client.RemoteEndPoint}");
步骤 3:数据读写(与服务器端一致)
通过 GetStream()
获取 NetworkStream
,实现数据收发,逻辑与服务器端完全相同:
NetworkStream stream = tcpClient.GetStream(); // 发送数据到服务器 string sendMsg = "Hello Server!"; byte[] sendBuffer = Encoding.UTF8.GetBytes(sendMsg); stream.Write(sendBuffer, 0, sendBuffer.Length); // 读取服务器返回数据 byte[] recvBuffer = new byte[1024]; int readLen = stream.Read(recvBuffer, 0, recvBuffer.Length); string recvMsg = Encoding.UTF8.GetString(recvBuffer, 0, readLen); Console.WriteLine($"收到服务器消息:{recvMsg}");
步骤 4:关闭资源
stream.Close(); tcpClient.Close();
4. 进阶:异步与取消(Task + CancellationTokenSource)
为避免同步操作阻塞线程,推荐使用异步方法,并通过 CancellationTokenSource
(CTS)实现任务取消(如手动关闭连接):
// 创建取消令牌源 CancellationTokenSource cts = new CancellationTokenSource(); CancellationToken token = cts.Token;// 异步接收客户端连接(支持取消) async Task AcceptClientAsync(TcpListener server) {while (!token.IsCancellationRequested) // 未取消则持续监听{try{TcpClient client = await server.AcceptTcpClientAsync(token); // 传入取消令牌// 处理客户端通讯(建议开启新线程/任务,避免阻塞监听)_ = HandleClientAsync(client, token);}catch (OperationCanceledException){Console.WriteLine("服务器监听已取消");break;}} }// 取消任务(如用户点击“关闭服务器”按钮) cts.Cancel(); // 触发取消,AcceptTcpClientAsync 会抛出 OperationCanceledException
5. 跨线程访问控件(WinForm/WPF)
若在 UI 线程外(如异步接收数据的线程)操作 UI 控件(如 TextBox),需使用 Invoke()
或 BeginInvoke()
切换到 UI 线程:
// WinForm 示例:更新 TextBox 显示收到的消息 private void UpdateTextBox(string msg) {if (textBox1.InvokeRequired) // 判断是否跨线程{// 跨线程,通过委托调用textBox1.Invoke(new Action<string>(UpdateTextBox), msg);}else{// 同一线程,直接更新textBox1.AppendText(msg + Environment.NewLine);} }// 在数据接收线程中调用 UpdateTextBox($"收到消息:{recvMsg}");
四、TCP 通讯核心流程(补充)
TCP 是面向连接的可靠协议,完整通讯流程如下(服务器 - 客户端交互):
服务器启动:
TcpListener
绑定 IP 和端口 → 调用Start()
开启监听。客户端连接:
TcpClient
调用Connect()
→ 向服务器发送 SYN 报文(TCP 三次握手第一步)。三次握手:服务器响应 SYN+ACK 报文 → 客户端响应 ACK 报文 → 连接建立。
数据交互:双方通过
NetworkStream
读写数据(TCP 保证数据有序、不丢失)。连接关闭:一方调用
Close()
→ 发送 FIN 报文(TCP 四次挥手)→ 双方释放连接。服务器停止:
TcpListener
调用Stop()
→ 释放监听端口。
五、关键注意点
端口占用:若启动
TcpListener
时报 “地址已在使用”,需确认端口是否被其他程序占用(可通过netstat -ano | findstr "端口号"
查看)。数据缓冲区:
Read()
方法读取的是当前缓冲区中的数据,需根据实际读取长度(readLen
)解析数据,避免缓冲区残留旧数据。异常处理:需捕获
SocketException
(网络错误)、IOException
(流操作错误)、OperationCanceledException
(任务取消)等异常,保证程序稳定性。连接限制:
TcpListener.Start(int backlog)
中的backlog
参数指定等待队列的最大长度,超过则新连接会被拒绝,需根据业务需求设置。