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 状态

相关推荐
CHANG_THE_WORLD2 小时前
Windows程序字符串处理与逆向分析
windows·stm32·单片机
A9better3 小时前
嵌入式开发学习日志30——stm32之定时器中断简单项目练习
stm32·单片机·嵌入式硬件·学习
充哥单片机设计3 小时前
【STM32项目开源】基于STM32的智能电子秤
stm32·单片机·嵌入式硬件
学不动CV了6 小时前
C语言(FreeRTOS)中堆内存管理分析Heap_1、Heap_2、Heap_4、Heap_5详细分析与解析(二)
linux·c语言·arm开发·stm32·单片机·51单片机
XINVRY-FPGA10 小时前
XCVU9P-2FLGA2104E Xilinx AMD Virtex UltraScale+ FPGA
人工智能·嵌入式硬件·fpga开发·硬件工程·dsp开发·射频工程·fpga
10001hours13 小时前
(基于江协科技)51单片机入门:7.LED点阵屏
科技·嵌入式硬件·51单片机
10001hours14 小时前
(基于江协科技)51单片机入门:9.蜂鸣器
科技·嵌入式硬件·51单片机
transuperb14 小时前
51单片机红外遥控
单片机·嵌入式硬件
Archie_IT18 小时前
嵌入式八股文篇——P1 关键字篇
c语言·开发语言·单片机·mcu·物联网·面试·职场和发展