一对多 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 创建空白解决方案
此时解决方案中还没有任何项目。
5.2 添加共享类库项目
创建后删除 Visual Studio 自动生成的 Class1.cs,后续使用清晰的文件名代替它。
5.3 添加服务器 WPF 项目
5.4 添加客户端 WPF 项目
重复服务器项目的创建过程,项目名称填写:
TcpDemo.Client
完成后,解决方案应有三个项目:
OneToManyTcpDemo
├── TcpDemo.Shared
├── TcpDemo.Server
└── TcpDemo.Client
5.5 添加项目引用
服务器和客户端需要使用共享项目中的报文、限速和统计代码。
★ 关键:不要把 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 的执行步骤:
★ 关键: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 中应执行:
SendAsync 中应执行:
★ 关键:这个类中执行“回显”时,目标只能是自己的 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 | 停止按钮命令 |
应实现的业务:
★ 关键:网络线程不能直接操作 ObservableCollection。需要通过 WPF 的 Dispatcher 切回 UI 线程后再添加日志、添加客户端或更新列表。
8.7 MainWindow.xaml(服务器)
职责: 定义服务器窗口的视觉布局和数据绑定。
推荐布局:
第一行:监听地址、端口、启动按钮、停止按钮
第二行:服务器状态、当前连接数量
中间:客户端 DataGrid
底部:日志 ListBox
客户端 DataGrid 列应包括:
编号
远程地址
连接状态
连接时间
上传总量
上传速率
下载总量
下载速率
可在上传、下载速率列旁显示进度条,进度条最大值为 100。
绑定要求:
地址输入框 -> ServerIp
端口输入框 -> ServerPort
启动按钮 -> StartServerCommand
停止按钮 -> StopServerCommand
连接数量文本 -> ClientCount
客户端表格 -> Clients
日志列表 -> Logs
8.8 MainWindow.xaml.cs(服务器)
职责: 窗口初始化与关闭时资源释放。
应做的事情:
不要把 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 | 是否正在自动发送 |
应实现的业务:
自动发送逻辑建议如下:
只要未取消且连接正常:
生成“自动消息序号 + 时间 + 填充字符”的文本
调用已有的 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,并在窗口关闭时主动断开。
应做:
不要把自动发送循环、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:启动服务器
预期:显示“运行中”,连接数量为 0,日志显示启动成功。
用例 2:多客户端连接
预期:服务器列表显示每个客户端,连接数量与客户端窗口数一致。
用例 3:只回显给原客户端
预期:仅 A 显示 [回显] 测试消息。
用例 4:验证限速
预期:上传、下载总量不断增加;实时速率接近配置限速,长期不会明显超过限速。
用例 5:验证独立性
预期:A、B 的上传下载速率分别显示;A 的流量不会占用 B 的限额。
用例 6:断开
预期:服务器连接数量减少 1,日志记录断开;服务器继续服务其他客户端。
15. 如何启动多个客户端进行演示
开发时可先在 Visual Studio 中调试服务器项目。生成成功后,客户端程序通常位于:
TcpDemo.Client\\bin\\Debug\\net8.0-windows\\TcpDemo.Client.exe
双击该文件可打开一个客户端窗口。重复双击即可打开多个客户端实例。
建议演示顺序:
16. 初学者最容易出现的问题
| 中文乱码 | 两端编码不一致 | 两端统一使用 UTF-8 |
| 接收文本不完整 | 把一次 ReadAsync 当作完整消息 | 必须使用 ReadExactlyAsync |
| 服务器卡住 | 在 UI 线程执行网络循环或 Thread.Sleep | 使用 async/await 与 Task.Delay |
| WPF 跨线程异常 | 后台线程直接更新控件或集合 | 使用 Dispatcher 或 DispatcherTimer |
| 自动发送后无法停止 | 没有取消令牌 | 使用 CancellationTokenSource |
| 两客户端消息混在一起 | 写入 TCP 流时无发送锁 | 客户端发送使用 SemaphoreSlim |
| 多客户端共用限速 | 限速器定义在服务器全局 | 每个会话各自创建上传、下载限速器 |
| 端口无法再次启动 | 上次监听器未关闭 | 在停止和窗口关闭时释放 TcpListener |
17. 第一版完成标准
当下列项目全部满足时,第一版即完成:
- 服务器能启动和停止。
- 多个客户端能同时连接。
- 服务器显示正确连接数量。
- 客户端可发送 UTF-8 文本。
- 服务器只回显给发送者。
- 服务器显示每个客户端的上传、下载总量与速率。
- 客户端显示自己的上传、下载总量与速率。
- 每个客户端上传和下载均独立限速。
- 客户端可自动发送文本。
- 关闭客户端或服务器不会导致另一端崩溃。
- 不包含折线图、文件传输、广播和远程部署功能。
网硕互联帮助中心





评论前必须登录!
注册