C# TCP 开发笔记(TcpListener/TcpClient)
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 状态