一、整体功能概述
该代码基于 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
)