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

一、整体功能概述

该代码基于 C# Windows Forms 框架,实现了一个基础的 TCP 服务端程序,核心功能包括:

  • 启动 / 停止 TCP 服务

  • 监听并接收多个客户端连接

  • 接收客户端发送的数据并在界面显示

  • (注释中预留)与串口设备交互并转发数据给客户端的扩展能力

  • (注释中预留)将接收到的客户端数据原封不动返回给客户端的回声功能

二、核心技术点与类库依赖

1. 关键命名空间

命名空间 核心用途
System.Net 提供 IP 地址(IPAddress)、网络端点(IPEndPoint)等基础网络类
System.Net.Sockets 提供 TCP 通信核心类(TcpListenerTcpClientNetworkStream
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关闭时释放TcpListenerCancellationTokenSource,可能导致资源泄漏

2. 优化方向

  1. IP 配置优化 :添加TextBox让用户输入 IP 和端口,替代硬编码

  2. 异常处理增强:

    复制代码
    // 启动服务时增加端口占用检测
    try
    {
        tcpListener.Start();
    }
    catch (SocketException ex) when (ex.ErrorCode == 10048)
    {
        throw new Exception("端口已被占用,请更换端口后重试");
    }
  3. 客户端管理 :维护List<TcpClient>集合,跟踪所有连接的客户端,在服务停止时主动关闭所有客户端

  4. 固定缓冲区读取:

    复制代码
    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;
        }
    }
  5. 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,更高效管理线程资源

六、调试与测试建议

  1. 本地调试 :将服务端 IP 改为127.0.0.1,使用Telnet或 C# TCP 客户端测试(如TcpClient client = new TcpClient("127.0.0.1", 9999)

  2. 局域网测试 :确保服务端和客户端在同一局域网,客户端使用服务端实际 IP(如192.168.1.100)连接

  3. 端口检测 :若启动失败,通过cmd执行netstat -ano | findstr "9999"查看端口占用进程,结束占用进程后重试

  4. 数据格式验证 :若客户端发送非 UTF8 编码数据,需统一编码格式(如Encoding.DefaultEncoding.ASCII

相关推荐
就叫飞六吧2 小时前
基于汇编实现led点灯-51单片机-stc89c52rc
嵌入式硬件·学习
宁静致远20212 小时前
FreeRTOS任务同步与通信--事件标志组
stm32·嵌入式·freertos
宁静致远20213 小时前
仿照STM32 HAL库设计思想使用FreeRTOS实现异步非阻塞式设备驱动
stm32·嵌入式硬件
田甲7 小时前
【STM32】墨水屏驱动开发
stm32·单片机·墨水屏
常州晟凯电子科技8 小时前
海思SS626开发笔记之环境搭建和SDK编译
人工智能·笔记·嵌入式硬件·物联网
智者知已应修善业8 小时前
【51单片机32个灯,第一次亮1,2。第二次亮2,3。第三次亮3,4。。。。】2023-2-10
c语言·经验分享·笔记·嵌入式硬件·51单片机
点灯小铭9 小时前
基于51单片机手机无线蓝牙APP控制风扇调速设计
单片机·mongodb·智能手机·毕业设计·51单片机·课程设计
JuneXcy10 小时前
C语言易错点大总结
c语言·嵌入式硬件·算法
沐欣工作室_lvyiyi11 小时前
采用AIOT技术的防疫物资监控系统的设计与开发(论文+源码)
stm32·单片机·嵌入式硬件·毕业设计·防疫物资