TCP 服务端 Form3 代码笔记(基于 C# Windows Forms)
一、窗体核心功能定位
Form3 是一个独立的 TCP 服务端窗体,核心能力包括:
-
启动 / 停止 TCP 服务,监听指定 IP(127.0.0.1)和端口(9999)
-
异步接收多客户端连接,为每个客户端分配独立数据接收任务
-
线程安全的日志记录(区分普通信息、成功、错误等状态)
-
资源自动释放(服务停止、窗体关闭时释放网络资源)
-
附加功能:通过按钮打开 Form4 子窗体(非模态,不阻塞服务端操作)
二、核心变量定义(服务端基础配置)
变量名 | 类型 | 作用说明 |
---|---|---|
DEFAULT_LOG_COLOR_* |
const int |
日志默认颜色的 ARGB 分量(编译时常量,解决默认参数非编译时常量问题) |
DEFAULT_LOG_COLOR |
static readonly Color |
日志默认颜色(灰色),由上述 ARGB 分量合成 |
_listener |
TcpListener |
TCP 监听器对象,负责监听客户端连接 |
cts |
CancellationTokenSource |
任务取消令牌源,控制后台监听、数据接收任务的终止 |
_isServerRunning |
bool |
服务端运行状态标记(避免重复启动 / 停止操作) |
三、核心功能模块拆解(带关键代码)
模块 1:服务端启动 / 停止按钮事件(入口逻辑)
功能逻辑
-
点击按钮时先禁用按钮,防止并发操作
-
根据
_isServerRunning
状态切换:未运行则启动服务,已运行则停止服务 -
异常捕获并记录错误日志,最终恢复按钮可用性
关键代码
private async void button1_Click(object sender, EventArgs e)
{
button1.Enabled = false; // 禁用按钮防并发
try
{
if (!_isServerRunning)
{
await StartServerAsync(); // 启动服务
button1.Text = "停止服务端";
AddServerLog("操作:开始启动服务端...");
}
else
{
await StopServerAsync(); // 停止服务
button1.Text = "启动服务端";
AddServerLog("操作:开始停止服务端...");
}
}
catch (Exception ex)
{
AddServerLog($"错误:{ex.Message}", Color.Red); // 错误日志标红
// 异常后恢复按钮文本
button1.Text = _isServerRunning ? "停止服务端" : "启动服务端";
}
finally
{
button1.Enabled = true; // 恢复按钮可用
}
}
模块 2:异步启动服务 + 监听客户端连接
功能逻辑
-
配置服务端地址(本地回环地址 127.0.0.1 + 端口 9999)
-
初始化
TcpListener
并启动(最大挂起连接数 10) -
创建取消令牌源,标记服务为运行状态
-
启动独立后台任务,循环监听客户端连接(非阻塞 UI)
-
捕获
SocketException
,针对端口占用(10048 错误)给出明确提示
关键代码(监听客户端核心逻辑)
private async Task StartServerAsync()
{
try
{
IPAddress serverIp = IPAddress.Parse("127.0.0.1");
int serverPort = 9999;
_listener = new TcpListener(serverIp, serverPort);
_listener.Start(10); // 最大挂起连接数10
cts = new CancellationTokenSource();
_isServerRunning = true;
AddServerLog($"成功:服务端启动,监听地址 {serverIp}:{serverPort}", Color.Green);
// 异步循环监听客户端(独立任务,不阻塞UI)
_ = Task.Run(async () =>
{
while (!cts.Token.IsCancellationRequested)
{
if (_listener.Pending()) // 检查是否有等待连接的客户端(非阻塞)
{
TcpClient client = await _listener.AcceptTcpClientAsync(); // 接收客户端连接
string clientEndPoint = client.Client.RemoteEndPoint.ToString();
AddServerLog($"连接:新客户端接入 - {clientEndPoint}");
// 为每个客户端启动独立数据接收任务
_ = ReceiveClientDataAsync(client, cts.Token);
}
else
{
await Task.Delay(100, cts.Token); // 无连接时短暂等待,减少CPU占用
}
}
}, cts.Token);
}
catch (SocketException ex)
{
if (ex.ErrorCode == 10048)
throw new Exception($"端口 {9999} 已被占用,请关闭其他占用程序或更换端口");
throw new Exception($"服务端启动失败:{ex.Message}");
}
}
模块 3:异步接收单个客户端数据
功能逻辑
-
为每个客户端创建独立的
NetworkStream
用于数据读写 -
循环读取客户端数据(通过
DataAvailable
避免无效等待) -
读取到 0 字节表示客户端主动断开,触发资源释放
-
捕获任务取消异常(服务端停止)和通信异常,分别处理
-
最终通过
finally
块确保流和客户端连接资源释放
关键代码
private async Task ReceiveClientDataAsync(TcpClient client, CancellationToken token)
{
string clientEndPoint = client.Client.RemoteEndPoint.ToString();
NetworkStream stream = null;
byte[] buffer = new byte[4096]; // 4KB固定缓冲区,适配多数场景
try
{
stream = client.GetStream();
while (!token.IsCancellationRequested && client.Connected)
{
if (!stream.DataAvailable)
{
await Task.Delay(50, token);
continue;
}
int readCount = await stream.ReadAsync(buffer, 0, buffer.Length, token);
if (readCount == 0) // 客户端主动断开
{
AddServerLog($"断开:客户端主动断开 - {clientEndPoint}");
break;
}
// 解析数据(UTF8编码,需与客户端一致)
string receiveData = Encoding.UTF8.GetString(buffer, 0, readCount);
AddServerLog($"接收:来自 {clientEndPoint} 的数据 - {receiveData}", Color.Blue);
Array.Clear(buffer, 0, buffer.Length); // 清空缓冲区,避免残留数据
}
}
catch (OperationCanceledException)
{
AddServerLog($"停止:客户端 {clientEndPoint} 接收任务已取消");
}
catch (Exception ex)
{
AddServerLog($"错误:与客户端 {clientEndPoint} 通信异常 - {ex.Message}", Color.Red);
}
finally
{
stream?.Dispose(); // 释放流资源
client?.Dispose(); // 释放客户端连接
AddServerLog($"清理:客户端 {clientEndPoint} 连接已释放");
}
}
模块 4:异步停止服务 + 资源释放
功能逻辑
-
取消后台任务(通过
cts.Cancel()
通知所有关联任务终止) -
释放取消令牌源、
TcpListener
资源 -
标记服务为停止状态,记录成功日志
-
短暂延迟(100ms)确保资源释放完成
关键代码
private async Task StopServerAsync()
{
try
{
// 取消并释放取消令牌
if (cts != null)
{
cts.Cancel();
cts.Dispose();
cts = null;
}
// 停止监听并释放
if (_listener != null)
{
_listener.Stop();
_listener = null;
}
_isServerRunning = false;
AddServerLog("成功:服务端已停止", Color.Green);
}
catch (Exception ex)
{
throw new Exception($"服务端停止失败:{ex.Message}");
}
await Task.Delay(100); // 确保资源释放完成
}
模块 5:线程安全的日志更新(核心优化点)
解决的问题
-
Windows Forms 控件只能由 UI 线程修改,后台任务(如数据接收)直接更新日志会报跨线程错误
-
避免使用默认参数(
Color
非编译时常量),通过方法重载实现默认日志颜色
核心设计(方法重载)
-
无参数重载:默认使用灰色日志,内部调用带颜色的重载
-
带颜色重载:检查是否跨线程,通过
BeginInvoke
异步委托到 UI 线程 -
WriteLogToUI
:实际写入日志(拼接时间戳、设置颜色、自动滚动到最新行)
关键代码
// 重载1:无颜色参数(默认灰色)
private void AddServerLog(string content)
{
AddServerLog(content, Color.Gray);
}
// 重载2:带颜色参数(显式指定颜色)
private void AddServerLog(string content, Color color)
{
if (richTextBox2.InvokeRequired) // 检查是否跨线程
{
richTextBox2.BeginInvoke(new Action(() =>
{
WriteLogToUI(content, color);
}));
}
else
{
WriteLogToUI(content, color);
}
}
// 实际写入UI(仅UI线程调用)
private void WriteLogToUI(string content, Color color)
{
string log = $"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] {content}{Environment.NewLine}";
richTextBox2.SelectionColor = color;
richTextBox2.AppendText(log);
richTextBox2.ScrollToCaret(); // 自动滚动到最新日志
}
模块 6:窗体关闭时资源释放(防内存泄漏)
功能逻辑
-
窗体关闭前检查服务是否运行,若运行则先停止服务(避免强制关闭导致资源泄漏)
-
通过
e.Cancel = true
取消当前关闭操作,待服务停止后重新关闭窗体
关键代码(注意:原事件名 Form1_FormClosing
需改为 Form3_FormClosing
,与窗体名匹配)
private async void Form3_FormClosing(object sender, FormClosingEventArgs e)
{
if (_isServerRunning)
{
e.Cancel = true; // 取消当前关闭,先停止服务
await StopServerAsync();
this.Close(); // 服务停止后重新关闭窗体
}
}
模块 7:附加功能(打开 Form4 子窗体)
功能逻辑
-
通过
button2
点击事件打开 Form4,使用Show()
非模态方式 -
打开后仍可操作 Form3(如停止服务、查看日志),不阻塞服务端核心功能
关键代码
private void button2_Click(object sender, EventArgs e)
{
Form4 form4 = new Form4();
form4.Show(); // 非模态打开,不阻塞Form3
}
四、关键注意事项
-
端口占用问题 :若启动服务时报 "10048 错误",需关闭占用 9999 端口的程序,或修改
serverPort
为其他未占用端口 -
编码一致性 :服务端用
Encoding.UTF8
解析数据,客户端需保持相同编码,否则会出现乱码 -
多客户端支持 :每个客户端连接会启动独立的
ReceiveClientDataAsync
任务,任务间通过缓冲区隔离,互不干扰 -
任务取消机制 :服务停止时通过
cts.Cancel()
终止所有后台任务,避免任务残留导致内存泄漏 -
控件名匹配 :确保日志控件
Name
为richTextBox2
,按钮Name
为button1
/button2
,否则会报空引用错误
五、功能测试流程
-
运行程序,打开 Form3
-
点击 "启动服务端",日志显示 "服务端启动,监听地址 127.0.0.1:9999"(绿色)
-
启动 TCP 客户端(如之前的 Form2/Form4),连接 127.0.0.1:9999,Form3 日志显示 "新客户端接入"
-
客户端发送数据,Form3 日志显示 "接收来自 XXX 的数据"(蓝色)
-
点击 "停止服务端",日志显示 "服务端已停止"(绿色),所有客户端连接被释放
-
点击
button2
可打开 Form4,同时可移动 / 操作 Form3,无阻塞
TCP 客户端 Form4 代码笔记(基于 C# Windows Forms)
一、窗体核心功能定位
Form4 是一个独立的 TCP 客户端窗体,主要功能包括:
-
与 TCP 服务端(如 Form3)建立连接和断开连接
-
向服务端发送数据并接收服务端响应
-
线程安全的日志记录(区分不同状态的信息)
-
自动处理网络异常和资源释放
-
完全独立运行,不依赖其他窗体
二、核心变量定义
变量名 | 类型 | 作用说明 |
---|---|---|
_tcpClient |
TcpClient |
TCP 客户端实例,负责与服务端建立连接 |
_clientCts |
CancellationTokenSource |
取消令牌源,用于控制接收响应的后台任务 |
_clientStream |
NetworkStream |
网络流,作为数据读写的通道 |
_isClientConnected |
bool |
连接状态标记,避免重复操作 |
三、核心功能模块解析
1. 初始化配置(构造函数)
public Form4()
{
InitializeComponent();
// 窗体基础配置
this.Text = "TCP客户端(Form2)";
// 日志控件配置
richTextBox1.ReadOnly = true;
richTextBox1.ScrollBars = RichTextBoxScrollBars.Vertical;
richTextBox1.WordWrap = true;
// 未连接时禁用发送按钮
button2.Enabled = false;
// 绑定窗体关闭事件
this.FormClosing += Form2_FormClosing;
}
2. 连接 / 断开服务端功能
连接 / 断开按钮点击事件
private async void button1_ClickAsync(object sender, EventArgs e)
{
button1.Enabled = false; // 禁用按钮防止并发操作
try
{
if (!_isClientConnected)
{
// 连接服务端
await ConnectToServerAsync();
button1.Text = "断开服务端";
button2.Enabled = true;
AddClientLog("操作:开始连接服务端...");
}
else
{
// 断开服务端
await DisconnectFromServerAsync();
button1.Text = "连接服务端";
button2.Enabled = false;
AddClientLog("操作:开始断开服务端...");
}
}
catch (SocketException ex)
{
// 处理网络异常(使用switch-case兼容C# 7.3)
string errorMsg;
switch (ex.ErrorCode)
{
case 10060:
errorMsg = "连接超时:服务端未响应";
break;
case 10061:
errorMsg = "连接被拒绝:服务端拒绝连接";
break;
case 10049:
errorMsg = "IP地址无效:请确认IP格式正确";
break;
default:
errorMsg = $"网络错误:{ex.Message}";
break;
}
AddClientLog(errorMsg, Color.Red);
// 恢复按钮状态
button1.Text = _isClientConnected ? "断开服务端" : "连接服务端";
}
catch (Exception ex)
{
AddClientLog($"操作失败:{ex.Message}", Color.Red);
}
finally
{
button1.Enabled = true; // 恢复按钮可用性
}
}
异步连接服务端
private async Task ConnectToServerAsync()
{
try
{
// 配置服务端地址和端口
IPAddress serverIp = IPAddress.Parse("127.0.0.1");
int serverPort = 9999;
_tcpClient = new TcpClient();
// 设置10秒连接超时
var connectTask = _tcpClient.ConnectAsync(serverIp, serverPort);
var timeoutTask = Task.Delay(10000);
var completedTask = await Task.WhenAny(connectTask, timeoutTask);
if (completedTask == timeoutTask)
{
_tcpClient.Dispose();
throw new TimeoutException("连接服务端超时(10秒)");
}
// 连接成功后的初始化
_clientStream = _tcpClient.GetStream();
_clientCts = new CancellationTokenSource();
_isClientConnected = true;
AddClientLog($"成功:已连接到服务端 {serverIp}:{serverPort}", Color.Green);
// 启动接收响应的后台任务
_ = ReceiveServerResponseAsync(_clientCts.Token);
}
catch (Exception ex)
{
_tcpClient?.Dispose();
throw new Exception($"连接服务端失败:{ex.Message}");
}
}
异步断开服务端
private async Task DisconnectFromServerAsync()
{
try
{
// 取消接收任务
if (_clientCts != null)
{
_clientCts.Cancel();
_clientCts.Dispose();
_clientCts = null;
}
// 释放流资源
if (_clientStream != null)
{
_clientStream.Dispose();
_clientStream = null;
}
// 释放客户端连接
if (_tcpClient != null)
{
_tcpClient.Dispose();
_tcpClient = null;
}
_isClientConnected = false;
AddClientLog("成功:已断开与服务端的连接", Color.Green);
}
catch (Exception ex)
{
throw new Exception($"断开服务端失败:{ex.Message}");
}
await Task.Delay(100); // 确保资源释放完成
}
3. 发送数据与接收响应功能
发送数据按钮点击事件
private async void button2_ClickAsync(object sender, EventArgs e)
{
string sendData = textBox1.Text.Trim();
if (string.IsNullOrEmpty(sendData))
{
MessageBox.Show("请输入要发送的数据!", "输入为空", MessageBoxButtons.OK, MessageBoxIcon.Warning);
return;
}
if (!_isClientConnected || _tcpClient == null || !_tcpClient.Connected || _clientStream == null)
{
MessageBox.Show("未连接到服务端,请先点击「连接服务端」按钮!", "连接失效", MessageBoxButtons.OK, MessageBoxIcon.Warning);
return;
}
button2.Enabled = false;
try
{
byte[] sendBuffer = Encoding.UTF8.GetBytes(sendData);
lock (_clientStream)
{
if (!_clientStream.CanWrite)
{
throw new InvalidOperationException("网络流不可写,连接可能已断开");
}
_clientStream.WriteAsync(sendBuffer, 0, sendBuffer.Length);
}
string localEndPoint = _tcpClient.Client.LocalEndPoint.ToString();
AddClientLog($"发送:我({localEndPoint})→ 服务端:{sendData}", Color.Blue);
textBox1.Clear();
}
catch (Exception ex)
{
AddClientLog($"发送失败:{ex.Message}", Color.Red);
await DisconnectFromServerAsync();
button1.Text = "连接服务端";
button2.Enabled = false;
}
finally
{
button2.Enabled = true;
}
}
异步接收服务端响应
private async Task ReceiveServerResponseAsync(CancellationToken token)
{
byte[] receiveBuffer = new byte[4096];
try
{
while (!token.IsCancellationRequested)
{
// 检查连接状态
if (!_isClientConnected || _tcpClient == null || !_tcpClient.Connected || _clientStream == null || !_clientStream.CanRead)
{
AddClientLog("接收停止:连接已断开或流不可读", Color.Orange);
break;
}
// 检查是否有可读取的数据
if (!_clientStream.DataAvailable)
{
await Task.Delay(100, token);
continue;
}
// 读取数据
int readCount = await _clientStream.ReadAsync(receiveBuffer, 0, receiveBuffer.Length, token);
if (readCount == 0)
{
AddClientLog("接收提示:服务端已主动断开连接", Color.Orange);
await DisconnectFromServerAsync();
button1.Text = "连接服务端";
button2.Enabled = false;
break;
}
// 解析数据
byte[] validData = new byte[readCount];
Array.Copy(receiveBuffer, validData, readCount);
string responseData = Encoding.UTF8.GetString(validData);
string serverEndPoint = _tcpClient.Client.RemoteEndPoint.ToString();
AddClientLog($"接收:服务端({serverEndPoint})→ 我:{responseData}", Color.Purple);
// 清空缓冲区
Array.Clear(receiveBuffer, 0, receiveBuffer.Length);
}
}
catch (OperationCanceledException)
{
AddClientLog("接收任务:已主动取消", Color.Gray);
}
catch (Exception ex)
{
AddClientLog($"接收异常:{ex.Message}", Color.Red);
await DisconnectFromServerAsync();
button1.Text = "连接服务端";
button2.Enabled = false;
}
}
4. 线程安全的日志更新
// 日志添加方法重载
private void AddClientLog(string content)
{
AddClientLog(content, Color.Gray);
}
private void AddClientLog(string content, Color color)
{
if (richTextBox1.InvokeRequired)
{
// 跨线程时异步委托到UI线程
richTextBox1.BeginInvoke(new Action(() =>
{
WriteLogToUI(content, color);
}));
}
else
{
WriteLogToUI(content, color);
}
}
// 实际写入日志到UI
private void WriteLogToUI(string content, Color color)
{
richTextBox1.SelectionColor = color;
string logWithTime = $"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] {content}{Environment.NewLine}";
richTextBox1.AppendText(logWithTime);
richTextBox1.ScrollToCaret(); // 自动滚动到最新日志
}
5. 窗体关闭时的资源清理
private async void Form2_FormClosing(object sender, FormClosingEventArgs e)
{
if (_isClientConnected)
{
e.Cancel = true; // 取消当前关闭操作
await DisconnectFromServerAsync(); // 先断开连接
this.Close(); // 资源释放后再关闭
}
}
四、关键技术点说明
-
异步操作 :所有网络操作都使用异步方法(如
ConnectAsync
、ReadAsync
、WriteAsync
),避免阻塞 UI 线程,保证界面响应性。 -
任务取消机制 :使用
CancellationTokenSource
控制后台接收任务,确保服务端断开时能正确终止接收线程。 -
连接超时处理 :通过
Task.WhenAny
组合连接任务和延迟任务,实现 10 秒连接超时控制。 -
线程安全的 UI 更新 :使用
BeginInvoke
确保所有 UI 操作在 UI 线程执行,避免跨线程操作异常。 -
资源释放 :在
finally
块和窗体关闭事件中仔细释放所有网络资源(TcpClient
、NetworkStream
等),避免内存泄漏。 -
异常处理:针对不同的网络异常(如连接超时、被拒绝、地址无效等)提供明确的错误提示。
-
状态管理 :使用
_isClientConnected
标记连接状态,避免重复连接或断开操作,确保 UI 状态与实际状态一致。
五、使用流程
-
确保服务端(Form3)已启动并监听 9999 端口
-
在 Form4 中点击 "连接服务端" 按钮,连接到服务端
-
在文本框中输入要发送的数据,点击 "发送" 按钮
-
接收服务端响应会显示在日志区域
-
完成后点击 "断开服务端" 按钮,或直接关闭窗体(会自动断开连接)
六、注意事项
-
服务端 IP 和端口需与 Form3 保持一致(127.0.0.1:9999)
-
编码格式使用 UTF8,需与服务端保持一致
-
发送数据前必须先建立连接
-
若端口被占用,需修改端口号并确保服务端和客户端使用相同端口
-
网络异常时客户端会自动断开连接并更新 UI 状态