C# TCP 服务端开发笔记(TcpListener/TcpClient)
一、整体功能概述
该代码基于 C# Windows Forms 框架,实现了一个基础的 TCP 服务端程序,核心功能包括:
启动 / 停止 TCP 服务
监听并接收多个客户端连接
接收客户端发送的数据并在界面显示
(注释中预留)与串口设备交互并转发数据给客户端的扩展能力
(注释中预留)将接收到的客户端数据原封不动返回给客户端的回声功能
二、核心技术点与类库依赖
1. 关键命名空间
命名空间 | 核心用途 |
---|---|
System.Net | 提供 IP 地址(IPAddress )、网络端点(IPEndPoint )等基础网络类 |
System.Net.Sockets | 提供 TCP 通信核心类(TcpListener 、TcpClient 、NetworkStream ) |
System.Threading /System.Threading.Tasks | 实现多线程与异步任务,避免 UI 线程阻塞 |
System.Windows.Forms | 提供 Windows 图形界面控件(Form、Button、RichTextBox 等) |
System.Text | 提供字符串与字节数组的编码转换(Encoding.UTF8 ) |
2. 核心组件说明
TcpListener
:TCP 服务端监听组件,负责绑定 IP 和端口、监听客户端连接请求TcpClient
:表示与客户端的连接会话,每个客户端对应一个TcpClient
实例NetworkStream
:基于TcpClient
的数据流对象,用于实际的字节数据读写CancellationTokenSource
:用于控制异步任务的取消(如停止服务时终止监听和数据接收任务)Invoke(Action)
:Windows Forms 线程安全调用,用于在非 UI 线程中更新 UI 控件
三、代码结构拆解
1. 全局变量定义
// TCP服务端监听对象 TcpListener tcpListener = null; // 任务取消令牌源(控制异步任务停止) CancellationTokenSource cts = null;
作用:在整个 Form 生命周期内维护服务端监听状态和任务取消控制,避免局部变量被回收导致功能异常
2. 构造函数
public Form1() {InitializeComponent(); // 初始化Windows Forms控件(自动生成) }
说明:默认构造函数,仅负责加载 Form 界面控件,无自定义逻辑
3. 核心功能方法详解
(1)启动服务按钮点击事件(入口方法)
private void button1_Click(object sender, EventArgs e) {try{// 根据按钮文本判断执行"启动"或"停止"逻辑if (button1.Text == "启动"){StartServer(); // 启动服务端AccepRequest(); // 开始接收客户端连接}else{StopServer(); // 停止服务端}}catch (Exception ex){MessageBox.Show(ex.Message); // 捕获并显示异常信息} }
逻辑流程:通过按钮文本状态切换服务端启停,统一异常捕获避免程序崩溃
(2)启动服务端(StartServer
)
private void StartServer() {try{// 1. 解析服务端IP地址(需替换为实际本地IP)IPAddress ip = IPAddress.Parse("172.16.0.28");// 2. 定义服务端端口(建议选择1024以上非知名端口)int port = 9999;// 3. 创建TcpListener实例并绑定IP和端口tcpListener = new TcpListener(ip, port);// 4. 启动监听(底层执行Socket.Bind和Socket.Listen)tcpListener.Start();// 5. 更新按钮文本为"关闭",提示服务已启动button1.Text = "关闭";}catch (Exception ex){throw ex; // 抛出异常,由上层按钮事件捕获处理} }
关键注意点:
IP 地址需为本地网卡实际 IP(如
192.168.1.100
),127.0.0.1
仅本地调试可用端口需确保未被其他程序占用(可通过
netstat -ano
命令查看端口占用)若需监听所有网卡,可使用
IPAddress.Any
(如new TcpListener(IPAddress.Any, 9999)
)
(3)停止服务端(StopServer
)
private void StopServer() {// 1. 取消所有异步任务(通过令牌源通知任务停止)cts?.Cancel();// 2. 停止TcpListener监听,释放端口资源tcpListener.Stop();// 3. 还原按钮文本为"启动",提示服务已停止button1.Text = "启动"; }
资源释放逻辑:先终止任务再停止监听,避免任务在监听停止后仍尝试操作导致异常
(4)接收客户端连接(AccepRequest
)
private void AccepRequest() {// 1. 初始化任务取消令牌源cts = new CancellationTokenSource();// 2. 启动异步任务(避免阻塞UI线程)Task.Run(async () =>{// 循环监听客户端连接(直到任务被取消)while (!cts.IsCancellationRequested){// 判断是否有客户端连接请求,无则跳过(非阻塞判断)if (!tcpListener.Pending()) continue;// 异步接收客户端连接(获取TcpClient实例)TcpClient tcpClient = await tcpListener.AcceptTcpClientAsync();// 为该客户端启动数据接收逻辑AccepData(tcpClient);}}, cts.Token); // 传入取消令牌,支持任务取消 }
核心逻辑:
用
Task.Run
开启后台任务,避免 UI 线程(如 Form)卡住tcpListener.Pending()
:非阻塞判断是否有连接请求,替代同步等待(AcceptTcpClient()
)每个客户端连接对应一个
TcpClient
实例,通过AccepData
单独处理数据交互
(5)接收客户端数据(AccepData
)
private void AccepData(TcpClient tcpClient) {// 启动异步任务处理该客户端的数据接收Task.Run(async () =>{// 循环接收数据(直到任务取消或客户端断开)while (!cts.IsCancellationRequested){// 判断客户端是否连接且有可用数据,无则跳过if (!tcpClient.Connected || tcpClient.Available == 0) continue;// 1. 获取客户端数据流(基于TcpClient)NetworkStream stream = tcpClient.GetStream();// 2. 创建缓冲区(大小=客户端待接收数据量)byte[] buffer = new byte[tcpClient.Available];// 3. 异步读取数据(返回实际读取的字节数)int count = await stream.ReadAsync(buffer, 0, buffer.Length);// 4. 若读取字节数为0,说明客户端断开连接,跳过后续处理if (count == 0) continue;// 5. 线程安全更新UI(显示客户端IP、端口和数据)Invoke(new Action(() =>{// 将字节数组转换为UTF8字符串string data = Encoding.UTF8.GetString(buffer);// 获取客户端端点信息(IP:Port)string clientEndPoint = tcpClient.Client.RemoteEndPoint.ToString();// 在RichTextBox中追加数据richTextBox1.Text += $"{clientEndPoint},{data}" + Environment.NewLine;}));// 【扩展预留】与串口设备交互逻辑// - 发送数据到串口设备// - 接收串口设备响应// - 将响应转发给客户端// 【回声功能预留】将接收到的数据原封不动返回给客户端// stream.Write(buffer, 0, buffer.Length);}}, cts.Token); }
关键技术点:
线程安全 UI 更新:
Invoke(Action)
确保在 UI 线程更新RichTextBox
,避免跨线程操作异常数据读取逻辑:
tcpClient.Available
:获取客户端待接收数据长度,避免缓冲区浪费stream.ReadAsync
:异步读取数据,不阻塞当前任务读取字节数
count == 0
:TCP 协议中表示客户端正常断开连接
客户端标识:通过
tcpClient.Client.RemoteEndPoint
获取客户端 IP 和端口,便于区分多客户端
四、关键问题与优化建议
1. 现有代码潜在问题
硬编码 IP:IP 地址
172.16.0.28
硬编码,更换环境需修改代码,建议改为配置项或下拉选择无异常重试:服务启动失败(如端口占用)仅抛出异常,无重试机制
客户端断开处理:未主动检测客户端断开(仅通过
count == 0
判断),长时间无数据时可能残留无效TcpClient
实例缓冲区大小:依赖
tcpClient.Available
定义缓冲区,若数据量大可能导致内存占用过高,建议使用固定大小缓冲区(如 1024 字节)循环读取资源释放:未在
Form
关闭时释放TcpListener
和CancellationTokenSource
,可能导致资源泄漏
2. 优化方向
IP 配置优化:添加
TextBox
让用户输入 IP 和端口,替代硬编码异常处理增强:
// 启动服务时增加端口占用检测 try {tcpListener.Start(); } catch (SocketException ex) when (ex.ErrorCode == 10048) {throw new Exception("端口已被占用,请更换端口后重试"); }
客户端管理:维护
List<TcpClient>
集合,跟踪所有连接的客户端,在服务停止时主动关闭所有客户端固定缓冲区读取:
byte[] buffer = new byte[1024]; // 固定1024字节缓冲区 int totalCount = 0; while (stream.DataAvailable) {int count = await stream.ReadAsync(buffer, totalCount, buffer.Length - totalCount);totalCount += count;if (totalCount == buffer.Length){// 缓冲区满,可扩展缓冲区或处理数据break;} }
Form 关闭资源释放:
private void Form1_FormClosing(object sender, FormClosingEventArgs e) {// 停止服务并释放资源StopServer();cts?.Dispose();tcpListener?.Stop(); }
五、扩展场景说明
1. 串口设备交互(预留逻辑实现)
// 1. 假设已初始化SerialPort(需配置端口、波特率等) SerialPort serialPort = new SerialPort("COM3", 9600, Parity.None, 8, StopBits.One); // 2. 发送数据到串口设备 serialPort.Write(buffer, 0, buffer.Length); // 3. 接收串口设备响应(需处理串口数据接收事件) byte[] responseBuffer = new byte[serialPort.BytesToRead]; serialPort.Read(responseBuffer, 0, responseBuffer.Length); // 4. 将响应转发给客户端 NetworkStream stream = tcpClient.GetStream(); await stream.WriteAsync(responseBuffer, 0, responseBuffer.Length);
2. 多客户端并发处理
现有代码已通过Task.Run
为每个客户端创建独立任务,支持多客户端并发连接,但需注意:
若客户端数量极多(如数百个),需考虑任务数量控制,避免系统资源耗尽
可使用线程池(
ThreadPool.QueueUserWorkItem
)替代Task.Run
,更高效管理线程资源
六、调试与测试建议
本地调试:将服务端 IP 改为
127.0.0.1
,使用Telnet
或 C# TCP 客户端测试(如TcpClient client = new TcpClient("127.0.0.1", 9999)
)局域网测试:确保服务端和客户端在同一局域网,客户端使用服务端实际 IP(如
192.168.1.100
)连接端口检测:若启动失败,通过
cmd
执行netstat -ano | findstr "9999"
查看端口占用进程,结束占用进程后重试数据格式验证:若客户端发送非 UTF8 编码数据,需统一编码格式(如
Encoding.Default
或Encoding.ASCII
)