C# TCP 开发笔记(TcpListener/TcpClient)

TCP 服务端 Form3 代码笔记(基于 C# Windows Forms)

一、窗体核心功能定位

Form3 是一个独立的 TCP 服务端窗体,核心能力包括:

  1. 启动 / 停止 TCP 服务,监听指定 IP(127.0.0.1)和端口(9999)

  2. 异步接收多客户端连接,为每个客户端分配独立数据接收任务

  3. 线程安全的日志记录(区分普通信息、成功、错误等状态)

  4. 资源自动释放(服务停止、窗体关闭时释放网络资源)

  5. 附加功能:通过按钮打开 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:异步启动服务 + 监听客户端连接

功能逻辑
  1. 配置服务端地址(本地回环地址 127.0.0.1 + 端口 9999)

  2. 初始化 TcpListener 并启动(最大挂起连接数 10)

  3. 创建取消令牌源,标记服务为运行状态

  4. 启动独立后台任务,循环监听客户端连接(非阻塞 UI)

  5. 捕获 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:异步停止服务 + 资源释放

功能逻辑
  1. 取消后台任务(通过 cts.Cancel() 通知所有关联任务终止)

  2. 释放取消令牌源、TcpListener 资源

  3. 标记服务为停止状态,记录成功日志

  4. 短暂延迟(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 非编译时常量),通过方法重载实现默认日志颜色

核心设计(方法重载)
  1. 无参数重载:默认使用灰色日志,内部调用带颜色的重载

  2. 带颜色重载:检查是否跨线程,通过 BeginInvoke 异步委托到 UI 线程

  3. 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
}

四、关键注意事项

  1. 端口占用问题 :若启动服务时报 "10048 错误",需关闭占用 9999 端口的程序,或修改 serverPort 为其他未占用端口

  2. 编码一致性 :服务端用 Encoding.UTF8 解析数据,客户端需保持相同编码,否则会出现乱码

  3. 多客户端支持 :每个客户端连接会启动独立的 ReceiveClientDataAsync 任务,任务间通过缓冲区隔离,互不干扰

  4. 任务取消机制 :服务停止时通过 cts.Cancel() 终止所有后台任务,避免任务残留导致内存泄漏

  5. 控件名匹配 :确保日志控件 NamerichTextBox2,按钮 Namebutton1/button2,否则会报空引用错误

五、功能测试流程

  1. 运行程序,打开 Form3

  2. 点击 "启动服务端",日志显示 "服务端启动,监听地址 127.0.0.1:9999"(绿色)

  3. 启动 TCP 客户端(如之前的 Form2/Form4),连接 127.0.0.1:9999,Form3 日志显示 "新客户端接入"

  4. 客户端发送数据,Form3 日志显示 "接收来自 XXX 的数据"(蓝色)

  5. 点击 "停止服务端",日志显示 "服务端已停止"(绿色),所有客户端连接被释放

  6. 点击 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(); // 资源释放后再关闭
    }
}

四、关键技术点说明

  1. 异步操作 :所有网络操作都使用异步方法(如ConnectAsyncReadAsyncWriteAsync),避免阻塞 UI 线程,保证界面响应性。

  2. 任务取消机制 :使用CancellationTokenSource控制后台接收任务,确保服务端断开时能正确终止接收线程。

  3. 连接超时处理 :通过Task.WhenAny组合连接任务和延迟任务,实现 10 秒连接超时控制。

  4. 线程安全的 UI 更新 :使用BeginInvoke确保所有 UI 操作在 UI 线程执行,避免跨线程操作异常。

  5. 资源释放 :在finally块和窗体关闭事件中仔细释放所有网络资源(TcpClientNetworkStream等),避免内存泄漏。

  6. 异常处理:针对不同的网络异常(如连接超时、被拒绝、地址无效等)提供明确的错误提示。

  7. 状态管理 :使用_isClientConnected标记连接状态,避免重复连接或断开操作,确保 UI 状态与实际状态一致。

五、使用流程

  1. 确保服务端(Form3)已启动并监听 9999 端口

  2. 在 Form4 中点击 "连接服务端" 按钮,连接到服务端

  3. 在文本框中输入要发送的数据,点击 "发送" 按钮

  4. 接收服务端响应会显示在日志区域

  5. 完成后点击 "断开服务端" 按钮,或直接关闭窗体(会自动断开连接)

六、注意事项

  1. 服务端 IP 和端口需与 Form3 保持一致(127.0.0.1:9999)

  2. 编码格式使用 UTF8,需与服务端保持一致

  3. 发送数据前必须先建立连接

  4. 若端口被占用,需修改端口号并确保服务端和客户端使用相同端口

  5. 网络异常时客户端会自动断开连接并更新 UI 状态

相关推荐
清风6666661 天前
基于单片机与DAC0832的双路波形信号发生系统设计
单片机·嵌入式硬件·毕业设计·课程设计·期末大作业
azwsm1 天前
电路元器件和GPIO控制器
单片机·嵌入式硬件
kebidaixu1 天前
FreeRTOS 移植到 STM32F407VETX 记录(一)
stm32·单片机·嵌入式硬件
CSDN官方博客1 天前
「谁说嵌入式只是调包和焊板子?」—— 2026嵌入式全栈技术征锋令
嵌入式硬件·物联网·embedding
半条-咸鱼1 天前
【INACCESSIBLE_BOOT_DEVICE】安装 Config Tool 后 Windows 蓝屏,最终通过 VMware 虚拟机解决
windows·stm32·vmware·芯片
点灯小铭1 天前
基于单片机的数码管定时插座设计与定时开关功能实现
单片机·嵌入式硬件·毕业设计·课程设计·期末大作业
云栖梦泽1 天前
玩转RK3506SDK
linux·嵌入式硬件
数智工坊1 天前
机器人四大主控板系统分层选型指南:树莓派、ESP32、STM32与Arduino的能力边界与实战定位
stm32·嵌入式硬件·机器人
某林2121 天前
跨越底层与AI的鸿沟:ROS2+多模态大模型(Qwen-VL)机器人全链路排障实录
人工智能·stm32·机器人·人机交互·ros2·技术复盘
进击的小头1 天前
第8篇:IGBT 从零到精通:核心原理、关键参数、选型指南与工业级应用要点
经验分享·嵌入式硬件·学习