云计算百科
云计算领域专业知识百科平台

一对多 TCP 服务器与客户端流量控制系统开发文档

一对多 TCP 服务器与客户端流量控制系统开发文档

1. 文档目的

本文档为作者自己的练习项目,本人是一名 C# 和 WPF 初学者,本文从创建解决方案开始,逐步完成一个仅在本机运行的“一台服务器、多台客户端”TCP 通信项目。

完成后,你可以先启动服务器,再多次启动客户端程序;每个客户端都能向服务器发送文本,服务器只把文本回显给原客户端。服务器与客户端均展示实时流量数据,且每个客户端的上传、下载流量均独立限速。

本文是第一版的完整范围。请严格按“实施顺序”逐步完成并验证,不要一开始同时编写所有文件。

ps:如有需要可向作者私聊无偿索要源码

2. 已确认的需求

项目项最终约定
开发语言与框架 C#、.NET 8、WPF
运行方式 仅本机模拟,不部署到局域网或互联网
通信协议 TCP
服务端形式 独立 WPF 程序
客户端形式 独立 WPF 程序,可多次打开
通信模式 一个服务器对应多个客户端
消息类型 UTF-8 文本
服务器处理方式 只回显给发送消息的客户端
流量控制 每个客户端独立限制上传与下载速率
修改限速 不提供运行时界面修改;修改默认值后重启程序
数据展示 数值、进度条、表格、日志
折线图 不需要
自动发送 需要,用于制造流量并验证限速
不做的功能 文件传输、群发、登录、数据库、远程部署

3. 基本概念

本项目中“上传”和“下载”的方向固定如下:

客户端 -> 服务器:上传流量
服务器 -> 原客户端:下载流量(服务器回显)

服务器不是聊天服务器。客户端 A 发送的内容只会返回给 A,不会发送给 B、C 等其他客户端。

#mermaid-svg-LX676Ddf0zMqffdG{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-LX676Ddf0zMqffdG .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-LX676Ddf0zMqffdG .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-LX676Ddf0zMqffdG .error-icon{fill:#552222;}#mermaid-svg-LX676Ddf0zMqffdG .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-LX676Ddf0zMqffdG .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-LX676Ddf0zMqffdG .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-LX676Ddf0zMqffdG .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-LX676Ddf0zMqffdG .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-LX676Ddf0zMqffdG .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-LX676Ddf0zMqffdG .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-LX676Ddf0zMqffdG .marker{fill:#333333;stroke:#333333;}#mermaid-svg-LX676Ddf0zMqffdG .marker.cross{stroke:#333333;}#mermaid-svg-LX676Ddf0zMqffdG svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-LX676Ddf0zMqffdG p{margin:0;}#mermaid-svg-LX676Ddf0zMqffdG .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-LX676Ddf0zMqffdG .cluster-label text{fill:#333;}#mermaid-svg-LX676Ddf0zMqffdG .cluster-label span{color:#333;}#mermaid-svg-LX676Ddf0zMqffdG .cluster-label span p{background-color:transparent;}#mermaid-svg-LX676Ddf0zMqffdG .label text,#mermaid-svg-LX676Ddf0zMqffdG span{fill:#333;color:#333;}#mermaid-svg-LX676Ddf0zMqffdG .node rect,#mermaid-svg-LX676Ddf0zMqffdG .node circle,#mermaid-svg-LX676Ddf0zMqffdG .node ellipse,#mermaid-svg-LX676Ddf0zMqffdG .node polygon,#mermaid-svg-LX676Ddf0zMqffdG .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-LX676Ddf0zMqffdG .rough-node .label text,#mermaid-svg-LX676Ddf0zMqffdG .node .label text,#mermaid-svg-LX676Ddf0zMqffdG .image-shape .label,#mermaid-svg-LX676Ddf0zMqffdG .icon-shape .label{text-anchor:middle;}#mermaid-svg-LX676Ddf0zMqffdG .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-LX676Ddf0zMqffdG .rough-node .label,#mermaid-svg-LX676Ddf0zMqffdG .node .label,#mermaid-svg-LX676Ddf0zMqffdG .image-shape .label,#mermaid-svg-LX676Ddf0zMqffdG .icon-shape .label{text-align:center;}#mermaid-svg-LX676Ddf0zMqffdG .node.clickable{cursor:pointer;}#mermaid-svg-LX676Ddf0zMqffdG .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-LX676Ddf0zMqffdG .arrowheadPath{fill:#333333;}#mermaid-svg-LX676Ddf0zMqffdG .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-LX676Ddf0zMqffdG .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-LX676Ddf0zMqffdG .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-LX676Ddf0zMqffdG .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-LX676Ddf0zMqffdG .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-LX676Ddf0zMqffdG .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-LX676Ddf0zMqffdG .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-LX676Ddf0zMqffdG .cluster text{fill:#333;}#mermaid-svg-LX676Ddf0zMqffdG .cluster span{color:#333;}#mermaid-svg-LX676Ddf0zMqffdG div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-LX676Ddf0zMqffdG .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-LX676Ddf0zMqffdG rect.text{fill:none;stroke-width:0;}#mermaid-svg-LX676Ddf0zMqffdG .icon-shape,#mermaid-svg-LX676Ddf0zMqffdG .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-LX676Ddf0zMqffdG .icon-shape p,#mermaid-svg-LX676Ddf0zMqffdG .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-LX676Ddf0zMqffdG .icon-shape .label rect,#mermaid-svg-LX676Ddf0zMqffdG .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-LX676Ddf0zMqffdG .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-LX676Ddf0zMqffdG .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-LX676Ddf0zMqffdG :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}

上传文本

仅回显

客户端 A(WPF)

服务器(WPF)

客户端 B(WPF)

客户端 C(WPF)

4. 开发前准备

4.1 操作系统和软件

WPF 只能在 Windows 上开发和运行。请准备:

  • Windows 10 或 Windows 11。
  • Visual Studio 2022 Community 或更高版本。
  • 安装 Visual Studio 时勾选“.NET 桌面开发”工作负载。
  • 已安装 .NET 8 SDK。

4.2 本项目不需要安装的内容

第一版不需要 NuGet 包,不需要数据库,不需要 IIS,也不需要 Docker。只使用 .NET 自带的 System.Net.Sockets、WPF 和基础集合类。

★ 关键:三个项目的目标框架必须兼容。共享类库使用 net8.0,两个 WPF 项目使用 net8.0-windows。

5. 从零创建解决方案

5.1 创建空白解决方案

  • 打开 Visual Studio。
  • 点击“创建新项目”。
  • 搜索并选择“空白解决方案”。
  • 名称输入 OneToManyTcpDemo。
  • 选择一个自己容易找到的目录。
  • 点击“创建”。
  • 此时解决方案中还没有任何项目。

    5.2 添加共享类库项目

  • 在“解决方案资源管理器”中右键解决方案 OneToManyTcpDemo。
  • 选择“添加” -> “新建项目”。
  • 搜索“类库”。
  • 选择“类库(.NET)”。
  • 项目名称填写 TcpDemo.Shared。
  • 框架选择 .NET 8.0。
  • 点击“创建”。
  • 创建后删除 Visual Studio 自动生成的 Class1.cs,后续使用清晰的文件名代替它。

    5.3 添加服务器 WPF 项目

  • 右键解决方案 -> “添加” -> “新建项目”。
  • 搜索“WPF 应用”。
  • 选择“WPF 应用”。
  • 项目名称填写 TcpDemo.Server。
  • 框架选择 .NET 8.0。
  • 点击“创建”。
  • 5.4 添加客户端 WPF 项目

    重复服务器项目的创建过程,项目名称填写:

    TcpDemo.Client

    完成后,解决方案应有三个项目:

    OneToManyTcpDemo
    ├── TcpDemo.Shared
    ├── TcpDemo.Server
    └── TcpDemo.Client

    5.5 添加项目引用

    服务器和客户端需要使用共享项目中的报文、限速和统计代码。

  • 右键 TcpDemo.Server -> “添加” -> “项目引用”。
  • 勾选 TcpDemo.Shared。
  • 点击“确定”。
  • 对 TcpDemo.Client 重复以上操作。
  • ★ 关键:不要把 FrameCodec、RateLimiter 等代码分别复制到服务器和客户端。两端必须引用同一个共享项目,才能保证协议一致。

    5.6 首次生成

    点击:

    生成 -> 生成解决方案

    在“错误列表”中确认:

    错误:0

    如果此时有错误,先解决错误,不要继续下一阶段。

    6. 总体项目结构

    完成后建议采用以下文件结构。文件夹可在项目上右键“添加” -> “新建文件夹”创建;类文件可右键相应文件夹“添加” -> “类”创建。

    OneToManyTcpDemo

    ├── TcpDemo.Shared
    │ ├── Configuration
    │ │ └── TrafficSettings.cs
    │ ├── Protocol
    │ │ ├── MessageType.cs
    │ │ ├── TcpMessage.cs
    │ │ └── FrameCodec.cs
    │ └── Traffic
    │ ├── TrafficSnapshot.cs
    │ ├── TrafficCounter.cs
    │ └── RateLimiter.cs

    ├── TcpDemo.Server
    │ ├── Models
    │ │ └── ClientSessionInfo.cs
    │ ├── Services
    │ │ ├── TcpServerService.cs
    │ │ └── ClientSession.cs
    │ ├── ViewModels
    │ │ ├── ViewModelBase.cs
    │ │ ├── RelayCommand.cs
    │ │ └── ServerViewModel.cs
    │ ├── MainWindow.xaml
    │ ├── MainWindow.xaml.cs
    │ ├── App.xaml
    │ └── App.xaml.cs

    └── TcpDemo.Client
    ├── Services
    │ └── TcpClientService.cs
    ├── ViewModels
    │ ├── ViewModelBase.cs
    │ ├── RelayCommand.cs
    │ └── ClientViewModel.cs
    ├── MainWindow.xaml
    ├── MainWindow.xaml.cs
    ├── App.xaml
    └── App.xaml.cs

    7. 共享项目:每个文件要实现什么

    TcpDemo.Shared 不包含任何 WPF 界面,也不直接连接服务器。它只负责两端共用的规则和工具。

    7.1 Configuration/TrafficSettings.cs

    职责: 集中保存项目固定配置。

    应定义的内容:

    配置项建议初始值作用
    DefaultHost 127.0.0.1 客户端默认连接地址
    DefaultPort 5000 服务器默认监听端口
    UploadBytesPerSecond 10 * 1024 单个客户端上传限速
    DownloadBytesPerSecond 10 * 1024 单个客户端下载限速
    MaxMessageLength 64 * 1024 一条文本消息允许的最大字节数
    RateRefreshIntervalMs 500 或 1000 界面刷新速率的时间间隔

    此文件不应该包含 TCP 收发逻辑,也不应该包含 WPF 控件代码。

    ★ 关键:限速单位统一为“字节/秒”。10 KB/s 的代码值应为 10 * 1024,而不是 10。

    7.2 Protocol/MessageType.cs

    职责: 用枚举标识每条 TCP 消息的用途。

    至少定义:

    枚举名称建议数值发送方向作用
    TextRequest 1 客户端 -> 服务器 普通文本请求
    EchoResponse 2 服务器 -> 客户端 对请求文本的回显
    SystemMessage 3 服务器 -> 客户端 系统提示或错误说明

    此文件只定义类型,不执行网络操作。

    7.3 Protocol/TcpMessage.cs

    职责: 表示一条已经被完整解析的应用层消息。

    应包含:

    Type:MessageType
    Content:string

    示例:

    Type = TextRequest
    Content = "你好,服务器"

    此文件只负责存数据;不在这里进行编码、发送、日志或限速。

    7.4 Protocol/FrameCodec.cs

    职责: 解决 TCP 的“半包”和“粘包”问题,统一服务器与客户端之间的字节格式。

    TCP 只保证字节顺序,不保证一次读取就是一条消息。因此本项目规定报文格式:

    [4 字节:正文长度][1 字节:消息类型][UTF-8 文本内容]

    其中“正文长度”至少应覆盖“消息类型 + 文本内容”的字节数。

    建议实现的方法:

    方法要做什么
    WriteMessageAsync 把 TcpMessage 编码成字节,并完整写入 NetworkStream
    ReadMessageAsync 从 NetworkStream 读取并解析一条完整报文,返回 TcpMessage
    ReadExactlyAsync 循环读取,直到缓冲区指定长度全部填满

    ReadMessageAsync 的执行步骤:

  • 先读取 4 字节长度。
  • 将长度转换成整数。
  • 检查长度是否大于 0 且不超过 MaxMessageLength。
  • 按该长度读取剩余正文。
  • 读取第 1 个字节作为消息类型。
  • 剩余字节按 UTF-8 解码为文本。
  • 返回 TcpMessage。
  • ★ 关键:ReadAsync 一次可能只得到部分字节,所以 ReadExactlyAsync 必须循环读取。没有这一步,自动发送时很容易解析错误。

    7.5 Traffic/TrafficSnapshot.cs

    职责: 表示某一时刻用于界面显示的流量快照。

    建议属性:

    TotalBytes
    BytesPerSecond

    可增加格式化后的属性或在 ViewModel 内格式化:

    TotalText,例如“12.50 KB”
    SpeedText,例如“9.82 KB/s”

    这个文件的作用是把“统计结果”与“统计过程”分开,便于界面读取。

    7.6 Traffic/TrafficCounter.cs

    职责: 累加流量,并计算实时速率。

    建议维护的数据:

    累计总字节数
    当前统计周期新增字节数
    上次刷新时间
    当前每秒速率

    建议公开的方法:

    方法功能
    AddBytes(int count) 每次成功发送或接收后累加字节数
    GetSnapshot() 读取当前总量和当前速率
    RefreshSpeed() 根据上一个周期内新增字节数计算速率

    计数规则:

    • 发送成功后,增加发送方向字节数。
    • 接收成功后,增加接收方向字节数。
    • 建议统计实际经过 NetworkStream 的全部字节,包括 4 字节长度头,保证统计与限速规则一致。

    7.7 Traffic/RateLimiter.cs

    职责: 在不阻塞 UI 的情况下限制传输速度。

    每一个客户端会使用独立的限速器;上传和下载各一个。

    建议字段:

    RateBytesPerSecond
    下一次允许发送/处理的时间
    用于保护时间计算的锁

    建议公开方法:

    WaitAsync(int byteCount, CancellationToken token)

    该方法的含义是:当前这批 byteCount 数据在指定速率下需要等待多长时间;等待结束后,调用方才可真正读写这批数据。

    例如:

    限速:10 KB/s
    本次:1 KB
    理论最少时间:约 100 ms

    ★ 关键:必须使用 await Task.Delay,不能使用 Thread.Sleep。Thread.Sleep 会造成窗口失去响应。

    ★ 关键:不要让所有客户端共用一个 RateLimiter。每个 ClientSession 都要创建自己的上传、下载限速器,否则会变成“服务器总流量限速”。

    8. 服务器项目:每个文件要实现什么

    8.1 Models/ClientSessionInfo.cs

    职责: 表示服务器窗口中客户端表格的一行数据。

    建议属性:

    属性作用
    ClientId 服务端递增生成的客户端编号,例如 1、2、3
    RemoteEndPoint 客户端地址与端口,例如 127.0.0.1:53214
    ConnectedTime 建立连接的时间
    Status 已连接、已断开、异常断开
    UploadTotalText 此客户端上传总量
    UploadSpeedText 此客户端当前上传速度
    DownloadTotalText 此客户端下载总量
    DownloadSpeedText 此客户端当前下载速度
    UploadProgress 上传进度条百分比,范围 0 到 100
    DownloadProgress 下载进度条百分比,范围 0 到 100

    这个类应该实现 INotifyPropertyChanged,以便数值变化后 DataGrid 自动刷新。

    8.2 Services/ClientSession.cs

    职责: 管理“服务器与某一个客户端”的完整生命周期。

    每连接一个客户端,就创建一个 ClientSession。它不能被其他客户端共用。

    建议持有:

    TcpClient
    NetworkStream
    ClientSessionInfo
    上传限速器
    下载限速器
    上传统计器
    下载统计器
    CancellationTokenSource

    建议方法:

    方法功能
    RunAsync 循环接收该客户端的消息
    HandleMessageAsync 按消息类型处理一条消息
    SendAsync 向当前客户端发送一条完整消息
    Close 或 CloseAsync 关闭该连接、释放资源

    RunAsync 中应执行:

  • 调用 FrameCodec.ReadMessageAsync 读取一条完整消息。
  • 读取到的字节按该客户端上传限速处理。
  • 更新该客户端上传统计。
  • 如果消息类型是 TextRequest,创建同内容的 EchoResponse。
  • 调用 SendAsync 回显给当前客户端。
  • 如果连接关闭、取消或发生异常,通知 TcpServerService 移除当前会话。
  • SendAsync 中应执行:

  • 使用下载限速器等待。
  • 调用 FrameCodec.WriteMessageAsync。
  • 更新下载统计。
  • ★ 关键:这个类中执行“回显”时,目标只能是自己的 NetworkStream,不能访问其他客户端的网络流。

    8.3 Services/TcpServerService.cs

    职责: 管理服务器监听器和全部客户端会话。

    建议字段:

    TcpListener
    ConcurrentDictionary<int, ClientSession>
    CancellationTokenSource
    递增客户端编号

    建议方法:

    方法功能
    StartAsync(string host, int port) 创建监听器并开始接收连接
    AcceptClientsAsync 持续调用 AcceptTcpClientAsync
    RemoveClient 删除断开的客户端会话
    StopAsync 停止监听、关闭所有会话

    启动流程:

    创建 TcpListener
    -> Start
    -> 启动 AcceptClientsAsync 循环
    -> 接受一个 TcpClient
    -> 分配 ClientId
    -> 创建 ClientSession
    -> 加入字典
    -> 通知界面添加一行客户端数据
    -> 启动该 ClientSession.RunAsync

    停止流程:

    停止接受新客户端
    -> 取消后台任务
    -> 逐个关闭 ClientSession
    -> 清空客户端字典
    -> 通知界面更新客户端数量为 0

    建议使用事件向 ViewModel 通知:

    客户端已连接
    客户端已断开
    收到日志
    客户端数据已更新

    这样网络服务不需要直接依赖 WPF 控件。

    8.4 ViewModels/ViewModelBase.cs

    职责: 为服务器和客户端的 ViewModel 提供属性变化通知能力。

    至少实现:

    INotifyPropertyChanged
    OnPropertyChanged(…)

    本文件不放 TCP 代码、不放按钮业务代码。

    8.5 ViewModels/RelayCommand.cs

    职责: 将 WPF 按钮与 ViewModel 方法绑定。

    应实现 ICommand,支持:

    执行方法
    是否可执行判断(可选)

    服务器至少需要两个命令:

    StartServerCommand
    StopServerCommand

    8.6 ViewModels/ServerViewModel.cs

    职责: 连接服务器服务与服务器界面,是服务器窗口的核心。

    建议属性:

    属性作用
    ServerIp 默认 127.0.0.1
    ServerPort 默认 5000
    ServerStatus 未启动、运行中、已停止
    ClientCount 当前连接总数
    Clients ObservableCollection<ClientSessionInfo>
    Logs ObservableCollection<string>
    StartServerCommand 启动按钮命令
    StopServerCommand 停止按钮命令

    应实现的业务:

  • 调用 TcpServerService.StartAsync。
  • 接到客户端连接事件后,添加到 Clients。
  • 接到断开事件后,删除或标记客户端。
  • 每次变更后更新 ClientCount。
  • 接到日志事件后追加到 Logs。
  • 使用 DispatcherTimer 每 500 或 1000 毫秒刷新客户端流量显示。
  • ★ 关键:网络线程不能直接操作 ObservableCollection。需要通过 WPF 的 Dispatcher 切回 UI 线程后再添加日志、添加客户端或更新列表。

    8.7 MainWindow.xaml(服务器)

    职责: 定义服务器窗口的视觉布局和数据绑定。

    推荐布局:

    第一行:监听地址、端口、启动按钮、停止按钮
    第二行:服务器状态、当前连接数量
    中间:客户端 DataGrid
    底部:日志 ListBox

    客户端 DataGrid 列应包括:

    编号
    远程地址
    连接状态
    连接时间
    上传总量
    上传速率
    下载总量
    下载速率

    可在上传、下载速率列旁显示进度条,进度条最大值为 100。

    绑定要求:

    地址输入框 -> ServerIp
    端口输入框 -> ServerPort
    启动按钮 -> StartServerCommand
    停止按钮 -> StopServerCommand
    连接数量文本 -> ClientCount
    客户端表格 -> Clients
    日志列表 -> Logs

    8.8 MainWindow.xaml.cs(服务器)

    职责: 窗口初始化与关闭时资源释放。

    应做的事情:

  • 创建 ServerViewModel。
  • 设置 DataContext。
  • 在窗口关闭事件中调用停止服务器逻辑,避免端口未释放。
  • 不要把 TCP 接收循环、限速算法、复杂业务代码直接写到这里。

    8.9 App.xaml 与 App.xaml.cs(服务器)

    第一版通常无需修改。它们负责 WPF 程序启动。若要添加全局异常记录,可后续在 App.xaml.cs 中处理,但不属于第一阶段必须内容。

    9. 客户端项目:每个文件要实现什么

    9.1 Services/TcpClientService.cs

    职责: 管理一个客户端窗口与服务器之间的 TCP 连接。

    建议字段:

    TcpClient
    NetworkStream
    CancellationTokenSource
    上传限速器
    上传统计器
    下载统计器
    SemaphoreSlim 发送锁

    建议方法:

    方法功能
    ConnectAsync(host, port) 连接服务器并启动接收循环
    SendTextAsync(text) 发送一条文本请求
    ReceiveLoopAsync 持续接收服务器回显
    DisconnectAsync 主动断开并释放资源

    ConnectAsync 的执行流程:

    创建 TcpClient
    -> ConnectAsync
    -> 获取 NetworkStream
    -> 创建取消令牌
    -> 启动 ReceiveLoopAsync
    -> 通知界面状态变为“已连接”

    SendTextAsync 的执行流程:

    检查是否已连接
    -> 等待发送锁
    -> 创建 TextRequest
    -> 根据本条消息字节数执行上传限速
    -> FrameCodec.WriteMessageAsync
    -> 更新上传统计
    -> 释放发送锁

    ReceiveLoopAsync 的执行流程:

    循环调用 FrameCodec.ReadMessageAsync
    -> 收到 EchoResponse
    -> 更新下载统计
    -> 触发“收到回显”事件
    -> ViewModel 将文本追加到界面日志

    ★ 关键:客户端的接收循环必须独立运行。不能在每次发送后才临时读取一次,否则自动发送和断开处理容易卡住。

    ★ 关键:手动发送和自动发送会同时调用发送功能,必须用 SemaphoreSlim 保证一条完整报文写入完成后,下一条才可写入。

    9.2 ViewModels/ViewModelBase.cs

    与服务器项目作用相同:实现 INotifyPropertyChanged 和 OnPropertyChanged。

    为了初学阶段清晰,可以在客户端项目复制同样实现;后期可再把它移到共享项目。

    9.3 ViewModels/RelayCommand.cs

    与服务器项目作用相同:实现 ICommand,将按钮绑定到 ViewModel 方法。

    客户端需要的命令:

    ConnectCommand
    DisconnectCommand
    SendCommand
    StartAutoSendCommand
    StopAutoSendCommand

    9.4 ViewModels/ClientViewModel.cs

    职责: 保存客户端窗口显示的数据,并调用 TcpClientService。

    建议属性:

    属性作用
    Host 默认 127.0.0.1
    Port 默认 5000
    ConnectionStatus 未连接、连接中、已连接、已断开
    InputText 用户输入待发送的文本
    Messages 发送、回显、系统提示日志
    UploadTotalText 客户端上传总量
    UploadSpeedText 当前上传速度
    DownloadTotalText 客户端下载总量
    DownloadSpeedText 当前下载速度
    UploadProgress 上传速度占限速比例
    DownloadProgress 下载速度占限速比例
    IsAutoSending 是否正在自动发送

    应实现的业务:

  • 点击连接时调用 ConnectAsync。
  • 点击发送时调用 SendTextAsync(InputText)。
  • 收到服务器回显时追加 [回显] 日志。
  • 使用 DispatcherTimer 定期读取统计快照并更新流量属性。
  • 开始自动发送时创建专用 CancellationTokenSource。
  • 停止自动发送或断开连接时取消该令牌。
  • 自动发送逻辑建议如下:

    只要未取消且连接正常:
    生成“自动消息序号 + 时间 + 填充字符”的文本
    调用已有的 SendTextAsync
    等待一个较短时间

    填充字符用于提高单条消息大小,例如每条约 1 KB。发送频率应高于限速允许的频率,这样才能看出限速器的效果。

    ★ 关键:自动发送不能重新编写一套 TCP 写入代码,必须复用 SendTextAsync,确保手动发送与自动发送使用同一个协议、锁、限速和统计逻辑。

    9.5 MainWindow.xaml(客户端)

    职责: 定义客户端窗口界面。

    推荐分区:

    连接区
    流量统计区
    文本发送区
    自动发送控制区
    消息日志区

    连接区:

    服务器地址输入框
    端口输入框
    连接按钮
    断开按钮
    连接状态文本

    流量统计区:

    上传总量、上传速率、上传限速、上传进度条
    下载总量、下载速率、下载限速、下载进度条

    文本发送区:

    多行或单行 TextBox
    发送按钮

    自动发送控制区:

    开始自动发送按钮
    停止自动发送按钮
    当前自动发送状态

    消息日志区使用 ListBox 或只读多行 TextBox,显示:

    [系统] 已连接到 127.0.0.1:5000
    [发送] 你好
    [回显] 你好
    [系统] 已断开连接

    推荐绑定:

    地址输入框 -> Host
    端口输入框 -> Port
    发送文本框 -> InputText
    连接按钮 -> ConnectCommand
    断开按钮 -> DisconnectCommand
    发送按钮 -> SendCommand
    自动发送按钮 -> StartAutoSendCommand / StopAutoSendCommand
    日志列表 -> Messages

    9.6 MainWindow.xaml.cs(客户端)

    职责: 设置 DataContext,并在窗口关闭时主动断开。

    应做:

  • 创建 ClientViewModel。
  • 将其赋值给窗口 DataContext。
  • 在窗口关闭事件中调用断开逻辑。
  • 不要把自动发送循环、TCP 收发、统计计算直接放在此文件中。

    9.7 App.xaml 与 App.xaml.cs(客户端)

    第一版通常无需额外逻辑,保持 Visual Studio 自动生成内容即可。

    10. 服务器和客户端的数据流

    一次手动发送的完整过程如下:

    客户端输入文本
    -> ClientViewModel 调用 TcpClientService.SendTextAsync
    -> 客户端上传限速
    -> FrameCodec 组装 TCP 报文并写出
    -> 客户端增加上传统计
    -> 服务器 ClientSession 读取完整报文
    -> 服务器对该客户端执行上传限速并增加上传统计
    -> 服务器创建 EchoResponse
    -> 服务器对该客户端执行下载限速
    -> 服务器写回原客户端
    -> 服务器增加下载统计
    -> 客户端接收循环读取 EchoResponse
    -> 客户端增加下载统计
    -> ClientViewModel 在日志中显示回显文本

    11. 界面更新规则

    11.1 为什么不能每收到一个字节就刷新界面

    频繁刷新 WPF 控件会造成界面卡顿。因此网络线程只负责累加统计;界面通过定时器每 500 或 1000 毫秒读取一次统计快照。

    11.2 推荐刷新方式

    服务器和客户端 ViewModel 各自使用一个 DispatcherTimer:

    每 500/1000 ms:
    获取各方向 TrafficCounter 快照
    格式化字节数与速率
    更新绑定属性
    计算进度条百分比

    格式化参考:

    原始字节数显示文本
    800 800 B
    2048 2.00 KB
    1048576 1.00 MB

    12. 必须处理的异常情况

    情况期望行为
    端口已被占用 服务器显示启动失败日志,不崩溃
    客户端连接不到服务器 客户端显示连接失败日志,不崩溃
    客户端主动关闭 服务器移除该会话并更新数量
    服务器主动停止 所有客户端变为已断开状态
    收到非法长度 服务端记录异常并断开该客户端
    消息超过 64 KB 不处理该消息,记录异常并断开或提示
    用户点击停止自动发送 自动循环尽快停止
    窗口关闭 取消任务并关闭 TCP 资源

    ★ 关键:网络异常属于正常情况的一部分。所有 await 网络操作都应有 try/catch/finally,避免任意一个客户端断开导致服务器程序整体退出。

    13. 实施顺序与阶段验收

    阶段 1:项目骨架

    完成内容:创建三个项目、添加引用、创建文件夹和空类。

    验收:整个解决方案可以生成,错误数为 0。

    阶段 2:报文协议

    完成内容:MessageType、TcpMessage、FrameCodec。

    验收:可正确编码、解码中文、英文和较长文本。

    阶段 3:单客户端通信

    完成内容:服务器监听、客户端连接、文本回显。

    验收:启动一个客户端,发送“你好”,能收到同样内容的回显。

    ★ 关键:阶段 3 成功前,不要加入限速和自动发送。

    阶段 4:多客户端通信

    完成内容:服务器接受多个连接,每个连接独立 ClientSession。

    验收:至少启动 3 个客户端;A 发送消息时,只有 A 收到回显。

    阶段 5:服务器统计界面

    完成内容:连接数量、客户端表格、日志。

    验收:客户端连接和断开时,数量和列表正确更新。

    阶段 6:客户端统计界面

    完成内容:客户端上传下载总量、实时速率、进度条、消息日志。

    验收:发送文本后,客户端和服务器对应数据均变化。

    阶段 7:独立限速

    完成内容:每个 ClientSession 独立上传下载限速;客户端发送端也执行上传限速。

    验收:自动发送 10 秒后,单客户端速率稳定在约 10 KB/s,多个客户端互不抢占限额。

    阶段 8:自动发送与异常收尾

    完成内容:开始/停止自动发送、服务器停止、客户端断开、异常日志。

    验收:所有“必须处理的异常情况”均不会造成程序崩溃。

    14. 最终验收用例

    用例 1:启动服务器

  • 打开服务器程序。
  • 地址为 127.0.0.1,端口为 5000。
  • 点击“启动服务器”。
  • 预期:显示“运行中”,连接数量为 0,日志显示启动成功。

    用例 2:多客户端连接

  • 多次启动客户端程序。
  • 每个客户端点击“连接”。
  • 预期:服务器列表显示每个客户端,连接数量与客户端窗口数一致。

    用例 3:只回显给原客户端

  • 客户端 A 发送“测试消息”。
  • 查看 A、B、C 的日志。
  • 预期:仅 A 显示 [回显] 测试消息。

    用例 4:验证限速

  • 在客户端 A 点击“开始自动发送”。
  • 保持至少 10 秒。
  • 观察客户端 A 和服务器客户端 A 行。
  • 预期:上传、下载总量不断增加;实时速率接近配置限速,长期不会明显超过限速。

    用例 5:验证独立性

  • 客户端 A、B 同时自动发送。
  • 查看服务器表格。
  • 预期:A、B 的上传下载速率分别显示;A 的流量不会占用 B 的限额。

    用例 6:断开

  • 关闭客户端 A,或点击其“断开”按钮。
  • 预期:服务器连接数量减少 1,日志记录断开;服务器继续服务其他客户端。

    15. 如何启动多个客户端进行演示

    开发时可先在 Visual Studio 中调试服务器项目。生成成功后,客户端程序通常位于:

    TcpDemo.Client\\bin\\Debug\\net8.0-windows\\TcpDemo.Client.exe

    双击该文件可打开一个客户端窗口。重复双击即可打开多个客户端实例。

    建议演示顺序:

  • 启动服务器。
  • 打开 3 个客户端并连接。
  • 展示服务器连接数量为 3。
  • 在 A 手动发送文本,证明只回显给 A。
  • 在 A、B 开启自动发送,展示两者独立限速。
  • 关闭一个客户端,展示服务器连接数量变为 2。
  • 16. 初学者最容易出现的问题

    问题原因处理方式
    中文乱码 两端编码不一致 两端统一使用 UTF-8
    接收文本不完整 把一次 ReadAsync 当作完整消息 必须使用 ReadExactlyAsync
    服务器卡住 在 UI 线程执行网络循环或 Thread.Sleep 使用 async/await 与 Task.Delay
    WPF 跨线程异常 后台线程直接更新控件或集合 使用 Dispatcher 或 DispatcherTimer
    自动发送后无法停止 没有取消令牌 使用 CancellationTokenSource
    两客户端消息混在一起 写入 TCP 流时无发送锁 客户端发送使用 SemaphoreSlim
    多客户端共用限速 限速器定义在服务器全局 每个会话各自创建上传、下载限速器
    端口无法再次启动 上次监听器未关闭 在停止和窗口关闭时释放 TcpListener

    17. 第一版完成标准

    当下列项目全部满足时,第一版即完成:

    • 服务器能启动和停止。
    • 多个客户端能同时连接。
    • 服务器显示正确连接数量。
    • 客户端可发送 UTF-8 文本。
    • 服务器只回显给发送者。
    • 服务器显示每个客户端的上传、下载总量与速率。
    • 客户端显示自己的上传、下载总量与速率。
    • 每个客户端上传和下载均独立限速。
    • 客户端可自动发送文本。
    • 关闭客户端或服务器不会导致另一端崩溃。
    • 不包含折线图、文件传输、广播和远程部署功能。
    赞(0)
    未经允许不得转载:网硕互联帮助中心 » 一对多 TCP 服务器与客户端流量控制系统开发文档
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!