编辑
2025-12-16
C#
00

目录

🎯 为什么要自己造轮子?
痛点分析
🚀 核心技术架构解析
技术栈选择
🚩 架构流程
🔧 关键实现要点
1. 异步并发请求核心算法
2. ScottPlot 5.0+ 图表更新机制
3. 高性能HTTP请求处理
💡 实战应用场景
1. 接口性能基准测试
2. 系统稳定性测试
3. 负载均衡效果验证
📊 完整项目配置
NuGet包依赖
UI设计要点
🧑‍💻 完整代码
⚡ 性能优化秘籍
1. 图表更新频率控制
2. 内存占用优化
3. 线程安全保障
🎯 核心要点总结

在现代Web开发中,你是否遇到过这样的困扰:网站上线后突然崩溃,用户投诉不断,但却不知道系统的真实承载能力? 作为开发者,我们经常听到产品经理问:"这个接口能同时处理多少用户请求?",而你却只能凭经验给出一个模糊的答案。

今天,我将分享如何用C#从零打造一个专业级网站压力测试工具,不仅能够精确模拟高并发场景,还能通过可视化图表直观展现系统性能数据。无需付费工具,无需复杂配置,只要掌握核心技术,你就能拥有媲美商业级的性能测试利器!

🎯 为什么要自己造轮子?

痛点分析

市面上的压力测试工具要么功能单一,要么价格昂贵。作为C#开发者,我们需要的是:

  • 高度可定制化的测试参数
  • 实时可视化的性能数据
  • 轻量级且易于扩展的架构
  • 完全免费的解决方案

🚀 核心技术架构解析

技术栈选择

  • WinForms + ScottPlot 5.0+:专业图表
  • HttpClient + 异步编程:高性能HTTP请求处理
  • SemaphoreSlim:智能并发控制
  • TableLayoutPanel:响应式布局

🚩 架构流程

download_01.png

🔧 关键实现要点

1. 异步并发请求核心算法

C#
private async Task ProcessRequestAsync(SemaphoreSlim semaphore, int requestId) { await semaphore.WaitAsync(cancellationTokenSource.Token); try { var testResult = await SendHttpRequestAsync(requestId); lock (lockObject) { testResults.Add(testResult); completedRequests++; if (testResult.IsSuccess) { successRequests++; totalResponseTime += testResult.ResponseTime; } else { failedRequests++; } } // 在UI线程中更新图表 if (InvokeRequired) { Invoke(new Action(() => UpdateChart(testResult))); } else { UpdateChart(testResult); } } finally { semaphore.Release(); } }

核心亮点

  • 使用SemaphoreSlim精确控制并发数量
  • lock机制确保线程安全的数据更新
  • Invoke保证UI更新在主线程执行

2. ScottPlot 5.0+ 图表更新机制

⚠️ 重要坑点:ScottPlot 5.0版本API发生重大变化,网上90%的教程都已过时!

C#
private void UpdateScottPlotData() { formsPlotResponseTime.Plot.Remove(responsePlot); double[] xData = requestIndexData.ToArray(); double[] yData = responseTimeData.ToArray(); responsePlot = formsPlotResponseTime.Plot.Add.Scatter(xData, yData); responsePlot.Color = ScottColor.FromHex("#007AFF"); responsePlot.MarkerSize = 3; responsePlot.LineWidth = 1.5f; responsePlot.ConnectStyle = ConnectStyle.Straight; responsePlot.LegendText = "响应时间"; if (requestIndexData.Count > 0 && responseTimeData.Count > 0) { var xMin = requestIndexData.Min(); var xMax = requestIndexData.Max(); var yMin = Math.Max(0, responseTimeData.Min() - 50); var yMax = responseTimeData.Max() + 100; formsPlotResponseTime.Plot.Axes.SetLimitsX(xMin - 5, xMax + 5); formsPlotResponseTime.Plot.Axes.SetLimitsY(yMin, yMax); } formsPlotResponseTime.Refresh(); }

关键技巧

  • 使用Plot.Remove()而非已废弃的Clear()方法
  • 避免频繁更新图表,采用批量更新策略提升性能
  • 别名处理颜色类型冲突:using ScottColor = ScottPlot.Color;

3. 高性能HTTP请求处理

C#
private async Task<TestResult> SendHttpRequestAsync(int requestId) { var result = new TestResult { RequestId = requestId, StartTime = DateTime.Now }; var stopwatch = Stopwatch.StartNew(); try { using var request = new HttpRequestMessage(); request.Method = new HttpMethod(cmbMethod.Text); request.RequestUri = new Uri(txtUrl.Text); if (!string.IsNullOrWhiteSpace(rtbHeaders.Text)) { var headers = rtbHeaders.Text.Split('\n'); foreach (var header in headers) { if (string.IsNullOrWhiteSpace(header)) continue; var parts = header.Split(':', 2); if (parts.Length == 2) { request.Headers.TryAddWithoutValidation(parts[0].Trim(), parts[1].Trim()); } } } if ((cmbMethod.Text == "POST" || cmbMethod.Text == "PUT") && !string.IsNullOrWhiteSpace(rtbRequestBody.Text)) { request.Content = new StringContent(rtbRequestBody.Text, Encoding.UTF8, "application/json" } using var cts = new CancellationTokenSource(TimeSpan.FromSeconds((double)nudTimeout.Value)); using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource( cancellationTokenSource.Token, cts.Token); using var response = await httpClient.SendAsync(request, linkedCts.Token); stopwatch.Stop(); result.ResponseTime = stopwatch.ElapsedMilliseconds; result.StatusCode = (int)response.StatusCode; result.IsSuccess = response.IsSuccessStatusCode; result.ResponseSize = response.Content.Headers.ContentLength ?? 0; result.EndTime = DateTime.Now; } catch (Exception ex) { stopwatch.Stop(); result.ResponseTime = stopwatch.ElapsedMilliseconds; result.IsSuccess = false; result.ErrorMessage = ex.Message; result.EndTime = DateTime.Now; } return result; }

💡 实战应用场景

1. 接口性能基准测试

场景:新接口上线前的性能评估

  • 设置并发数50,总请求1000
  • 观察平均响应时间和成功率曲线
  • 找出性能拐点,确定最佳并发配置

2. 系统稳定性测试

场景:长时间高并发场景验证

  • 启用实时图表监控
  • 设置较长的测试周期
  • 观察内存泄漏和性能衰减情况

3. 负载均衡效果验证

场景:验证负载均衡器配置效果

  • 配置多个目标URL轮询测试
  • 对比不同服务器的响应时间分布
  • 优化负载分发策略

📊 完整项目配置

NuGet包依赖

XML
<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <OutputType>WinExe</OutputType> <TargetFramework>net8.0-windows</TargetFramework> <UseWindowsForms>true</UseWindowsForms> </PropertyGroup> <ItemGroup> <PackageReference Include="ScottPlot.WinForms" Version="5.0.21" /> </ItemGroup> </Project>

UI设计要点

C#
// 统计信息卡片式设计 this.lblTotalRequests.BackColor = Color.FromArgb(248, 249, 250); this.lblSuccessRequests.BackColor = Color.FromArgb(212, 237, 218); this.lblFailedRequests.BackColor = Color.FromArgb(248, 215, 218); // 扁平化按钮设计 this.btnStart.FlatStyle = FlatStyle.Flat; this.btnStart.FlatAppearance.BorderSize = 0; this.btnStart.BackColor = Color.FromArgb(0, 122, 255);

🧑‍💻 完整代码

C#
using System; using System.Collections.Generic; using System.ComponentModel; using System.Diagnostics; using System.Drawing; using System.IO; using System.Linq; using System.Net.Http; using System.Text; using System.Threading; using System.Threading.Tasks; using System.Windows.Forms; using ScottPlot; using ScottPlot.WinForms; using DrawingColor = System.Drawing.Color; using ScottColor = ScottPlot.Color; using Timer = System.Windows.Forms.Timer; namespace AppWebStressTester { public partial class FrmMain : Form { private HttpClient httpClient; private CancellationTokenSource cancellationTokenSource; private List<TestResult> testResults; private Stopwatch testStopwatch; private Timer updateTimer; private int totalRequests; private int completedRequests; private int successRequests; private int failedRequests; private double totalResponseTime; private readonly object lockObject = new object(); private ScottPlot.Plottables.Scatter responsePlot; private List<double> responseTimeData; private List<double> requestIndexData; private bool isRealtimeEnabled = true; private int chartUpdateCounter = 0; public FrmMain() { InitializeComponent(); InitializeApplication(); } private void InitializeApplication() { httpClient = new HttpClient(); httpClient.Timeout = TimeSpan.FromSeconds(30); testResults = new List<TestResult>(); testStopwatch = new Stopwatch(); responseTimeData = new List<double>(); requestIndexData = new List<double>(); updateTimer = new Timer(); updateTimer.Interval = 500; updateTimer.Tick += UpdateTimer_Tick; InitializeScottPlot(); txtUrl.Text = "https://httpbin.org/get"; cmbMethod.SelectedIndex = 0; nudConcurrency.Value = 10; nudTotalRequests.Value = 100; nudTimeout.Value = 30; btnStop.Enabled = false; } private void InitializeScottPlot() { formsPlotResponseTime.Plot.Clear(); formsPlotResponseTime.Plot.FigureBackground.Color = ScottColor.FromHex("#FFFFFF"); formsPlotResponseTime.Plot.DataBackground.Color = ScottColor.FromHex("#FAFAFA"); formsPlotResponseTime.Plot.Axes.Bottom.Label.FontName = "SimSun"; formsPlotResponseTime.Plot.Axes.Left.Label.FontName = "SimSun"; formsPlotResponseTime.Plot.Axes.Top.Label.FontName = "SimSun"; formsPlotResponseTime.Plot.Axes.Title.Label.FontName = "SimSun"; formsPlotResponseTime.Plot.Title("响应时间分析"); formsPlotResponseTime.Plot.XLabel("请求序号"); formsPlotResponseTime.Plot.YLabel("响应时间 (ms)"); formsPlotResponseTime.Plot.Grid.MajorLineColor = ScottColor.FromHex("#E0E0E0"); formsPlotResponseTime.Plot.Grid.MinorLineColor = ScottColor.FromHex("#F0F0F0"); formsPlotResponseTime.Plot.Grid.MajorLineWidth = 1; formsPlotResponseTime.Plot.Grid.MinorLineWidth = 0.5f; double[] emptyX = new double[0]; double[] emptyY = new double[0]; responsePlot = formsPlotResponseTime.Plot.Add.Scatter(emptyX, emptyY); responsePlot.Color = ScottColor.FromHex("#007AFF"); responsePlot.MarkerSize = 3; responsePlot.LineWidth = 1.5f; responsePlot.ConnectStyle = ConnectStyle.Straight; formsPlotResponseTime.Plot.Legend.FontName = "SimSun"; responsePlot.LegendText = "响应时间"; formsPlotResponseTime.Plot.Legend.IsVisible = true; formsPlotResponseTime.Plot.Legend.Alignment = Alignment.UpperRight; formsPlotResponseTime.Plot.Axes.SetLimitsX(0, 100); formsPlotResponseTime.Plot.Axes.SetLimitsY(0, 1000); formsPlotResponseTime.Refresh(); } private async void btnStart_Click(object sender, EventArgs e) { if (string.IsNullOrWhiteSpace(txtUrl.Text)) { MessageBox.Show("请输入有效的URL", "错误", MessageBoxButtons.OK, MessageBoxIcon.Warning); return; } try { var uri = new Uri(txtUrl.Text); } catch { MessageBox.Show("URL格式不正确", "错误", MessageBoxButtons.OK, MessageBoxIcon.Warning); return; } ResetStatistics(); btnStart.Enabled = false; btnStop.Enabled = true; grpSettings.Enabled = false; cancellationTokenSource = new CancellationTokenSource(); testStopwatch.Start(); updateTimer.Start(); await RunLoadTest(); } private void btnStop_Click(object sender, EventArgs e) { StopTest(); } private void StopTest() { if (cancellationTokenSource != null) { cancellationTokenSource.Cancel(); } testStopwatch.Stop(); updateTimer.Stop(); btnStart.Enabled = true; btnStop.Enabled = false; grpSettings.Enabled = true; UpdateStatistics(); MessageBox.Show($"测试完成!\n总请求数: {completedRequests}\n成功: {successRequests}\n失败: {failedRequests}", "测试结果", MessageBoxButtons.OK, MessageBoxIcon.Information); } private async Task RunLoadTest() { var concurrency = (int)nudConcurrency.Value; var totalRequestsCount = (int)nudTotalRequests.Value; totalRequests = totalRequestsCount; var semaphore = new SemaphoreSlim(concurrency, concurrency); var tasks = new List<Task>(); for (int i = 0; i < totalRequestsCount; i++) { if (cancellationTokenSource.Token.IsCancellationRequested) break; var requestId = i; tasks.Add(ProcessRequestAsync(semaphore, requestId)); } try { await Task.WhenAll(tasks); } catch (OperationCanceledException) { } finally { StopTest(); } } private async Task ProcessRequestAsync(SemaphoreSlim semaphore, int requestId) { await semaphore.WaitAsync(cancellationTokenSource.Token); try { var testResult = await SendHttpRequestAsync(requestId); lock (lockObject) { testResults.Add(testResult); completedRequests++; if (testResult.IsSuccess) { successRequests++; totalResponseTime += testResult.ResponseTime; } else { failedRequests++; } } if (InvokeRequired) { Invoke(new Action(() => UpdateChart(testResult))); } else { UpdateChart(testResult); } } finally { semaphore.Release(); } } private async Task<TestResult> SendHttpRequestAsync(int requestId) { var result = new TestResult { RequestId = requestId, StartTime = DateTime.Now }; var stopwatch = Stopwatch.StartNew(); try { using var request = new HttpRequestMessage(); request.Method = new HttpMethod(cmbMethod.Text); request.RequestUri = new Uri(txtUrl.Text); if (!string.IsNullOrWhiteSpace(rtbHeaders.Text)) { var headers = rtbHeaders.Text.Split('\n'); foreach (var header in headers) { if (string.IsNullOrWhiteSpace(header)) continue; var parts = header.Split(':', 2); if (parts.Length == 2) { request.Headers.TryAddWithoutValidation(parts[0].Trim(), parts[1].Trim()); } } } if ((cmbMethod.Text == "POST" || cmbMethod.Text == "PUT") && !string.IsNullOrWhiteSpace(rtbRequestBody.Text)) { request.Content = new StringContent(rtbRequestBody.Text, Encoding.UTF8, "application/json"); } using var cts = new CancellationTokenSource(TimeSpan.FromSeconds((double)nudTimeout.Value)); using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource( cancellationTokenSource.Token, cts.Token); using var response = await httpClient.SendAsync(request, linkedCts.Token); stopwatch.Stop(); result.ResponseTime = stopwatch.ElapsedMilliseconds; result.StatusCode = (int)response.StatusCode; result.IsSuccess = response.IsSuccessStatusCode; result.ResponseSize = response.Content.Headers.ContentLength ?? 0; result.EndTime = DateTime.Now; } catch (Exception ex) { stopwatch.Stop(); result.ResponseTime = stopwatch.ElapsedMilliseconds; result.IsSuccess = false; result.ErrorMessage = ex.Message; result.EndTime = DateTime.Now; } return result; } private void UpdateChart(TestResult result) { if (!isRealtimeEnabled) return; requestIndexData.Add(result.RequestId + 1); responseTimeData.Add(result.ResponseTime); if (requestIndexData.Count > 2000) { requestIndexData.RemoveAt(0); responseTimeData.RemoveAt(0); } chartUpdateCounter++; if (chartUpdateCounter % 10 == 0 || requestIndexData.Count < 50) { UpdateScottPlotData(); } } private void UpdateScottPlotData() { formsPlotResponseTime.Plot.Remove(responsePlot); double[] xData = requestIndexData.ToArray(); double[] yData = responseTimeData.ToArray(); responsePlot = formsPlotResponseTime.Plot.Add.Scatter(xData, yData); responsePlot.Color = ScottColor.FromHex("#007AFF"); responsePlot.MarkerSize = 3; responsePlot.LineWidth = 1.5f; responsePlot.ConnectStyle = ConnectStyle.Straight; responsePlot.LegendText = "响应时间"; if (requestIndexData.Count > 0 && responseTimeData.Count > 0) { var xMin = requestIndexData.Min(); var xMax = requestIndexData.Max(); var yMin = Math.Max(0, responseTimeData.Min() - 50); var yMax = responseTimeData.Max() + 100; formsPlotResponseTime.Plot.Axes.SetLimitsX(xMin - 5, xMax + 5); formsPlotResponseTime.Plot.Axes.SetLimitsY(yMin, yMax); } formsPlotResponseTime.Refresh(); } private void UpdateTimer_Tick(object sender, EventArgs e) { UpdateStatistics(); if (isRealtimeEnabled && chartUpdateCounter % 10 != 0 && requestIndexData.Count > 0) { UpdateScottPlotData(); } } private void UpdateStatistics() { if (InvokeRequired) { Invoke(new Action(UpdateStatistics)); return; } lock (lockObject) { lblTotalRequests.Text = $"总请求数: {completedRequests}"; lblSuccessRequests.Text = $"成功请求: {successRequests}"; lblFailedRequests.Text = $"失败请求: {failedRequests}"; if (successRequests > 0) { lblAvgResponseTime.Text = $"平均响应时间: {(totalResponseTime / successRequests):F2} ms"; lblSuccessRate.Text = $"成功率: {((double)successRequests / completedRequests * 100):F2}%"; } else { lblAvgResponseTime.Text = "平均响应时间: 0 ms"; lblSuccessRate.Text = "成功率: 0%"; } if (testStopwatch.IsRunning && completedRequests > 0) { var rps = completedRequests / testStopwatch.Elapsed.TotalSeconds; lblRequestsPerSecond.Text = $"QPS: {rps:F2} req/s"; } if (totalRequests > 0) { var progress = (int)((double)completedRequests / totalRequests * 100); pgbProgress.Value = Math.Min(progress, 100); } if (responseTimeData.Count > 0) { var minResponse = responseTimeData.Min(); var maxResponse = responseTimeData.Max(); var avgResponse = responseTimeData.Average(); lblChartInfo.Text = $"响应时间统计: 最小 {minResponse:F0}ms, 最大 {maxResponse:F0}ms, 平均 {avgResponse:F1}ms | " + $"数据点: {responseTimeData.Count} | 实时更新: {(isRealtimeEnabled ? "启用" : "暂停")}"; } else { lblChartInfo.Text = "提示: 鼠标左键拖拽平移,右键拖拽缩放,滚轮快速缩放,双击重置视图"; } } } private void ResetStatistics() { lock (lockObject) { testResults.Clear(); requestIndexData.Clear(); responseTimeData.Clear(); totalRequests = 0; completedRequests = 0; successRequests = 0; failedRequests = 0; totalResponseTime = 0; chartUpdateCounter = 0; } formsPlotResponseTime.Plot.Clear(); InitializeScottPlot(); pgbProgress.Value = 0; UpdateStatistics(); } private void btnExportResults_Click(object sender, EventArgs e) { if (testResults.Count == 0) { MessageBox.Show("没有可导出的测试结果", "提示", MessageBoxButtons.OK, MessageBoxIcon.Information); return; } using var saveDialog = new SaveFileDialog(); saveDialog.Filter = "CSV文件|*.csv|PNG图片|*.png|SVG矢量图|*.svg|文本文件|*.txt"; saveDialog.Title = "导出测试结果"; saveDialog.FileName = $"load_test_result_{DateTime.Now:yyyyMMdd_HHmmss}"; if (saveDialog.ShowDialog() == DialogResult.OK) { try { switch (saveDialog.FilterIndex) { case 2: ExportChartPNG(saveDialog.FileName); break; case 3: ExportChartSVG(saveDialog.FileName); break; default: ExportResults(saveDialog.FileName); break; } MessageBox.Show("导出成功!", "提示", MessageBoxButtons.OK, MessageBoxIcon.Information); } catch (Exception ex) { MessageBox.Show($"导出失败: {ex.Message}", "错误", MessageBoxButtons.OK, MessageBoxIcon.Error); } } } private void ExportResults(string fileName) { var csv = new StringBuilder(); csv.AppendLine("请求ID,开始时间,结束时间,响应时间(ms),状态码,是否成功,响应大小,错误信息"); lock (lockObject) { foreach (var result in testResults) { csv.AppendLine($"{result.RequestId},{result.StartTime:yyyy-MM-dd HH:mm:ss.fff}," + $"{result.EndTime:yyyy-MM-dd HH:mm:ss.fff},{result.ResponseTime}," + $"{result.StatusCode},{result.IsSuccess},{result.ResponseSize}," + $"\"{result.ErrorMessage}\""); } } File.WriteAllText(fileName, csv.ToString(), Encoding.UTF8); } private void ExportChartPNG(string fileName) { UpdateScottPlotData(); formsPlotResponseTime.Plot.Save(fileName, 1920, 1080); } private void ExportChartSVG(string fileName) { UpdateScottPlotData(); formsPlotResponseTime.Plot.Save(fileName, 1920, 1080); } private void btnClearChart_Click(object sender, EventArgs e) { lock (lockObject) { requestIndexData.Clear(); responseTimeData.Clear(); chartUpdateCounter = 0; } formsPlotResponseTime.Plot.Clear(); InitializeScottPlot(); } private void btnRealtimeChart_Click(object sender, EventArgs e) { isRealtimeEnabled = !isRealtimeEnabled; btnRealtimeChart.Text = isRealtimeEnabled ? "暂停实时更新" : "启用实时更新"; btnRealtimeChart.BackColor = isRealtimeEnabled ? DrawingColor.FromArgb(220, 53, 69) : DrawingColor.FromArgb(40, 167, 69); if (isRealtimeEnabled && requestIndexData.Count > 0) { UpdateScottPlotData(); } } private void btnResetView_Click(object sender, EventArgs e) { if (requestIndexData.Count > 0 && responseTimeData.Count > 0) { var xMin = requestIndexData.Min(); var xMax = requestIndexData.Max(); var yMin = Math.Max(0, responseTimeData.Min() - 50); var yMax = responseTimeData.Max() + 100; formsPlotResponseTime.Plot.Axes.SetLimitsX(xMin - 5, xMax + 5); formsPlotResponseTime.Plot.Axes.SetLimitsY(yMin, yMax); } else { formsPlotResponseTime.Plot.Axes.SetLimitsX(0, 100); formsPlotResponseTime.Plot.Axes.SetLimitsY(0, 1000); } formsPlotResponseTime.Refresh(); } private void btnAutoScale_Click(object sender, EventArgs e) { formsPlotResponseTime.Plot.Axes.AutoScale(); formsPlotResponseTime.Refresh(); } protected override void OnFormClosing(FormClosingEventArgs e) { if (cancellationTokenSource != null) { cancellationTokenSource.Cancel(); } httpClient?.Dispose(); updateTimer?.Dispose(); base.OnFormClosing(e); } } public class TestResult { public int RequestId { get; set; } public DateTime StartTime { get; set; } public DateTime EndTime { get; set; } public long ResponseTime { get; set; } public int StatusCode { get; set; } public bool IsSuccess { get; set; } public long ResponseSize { get; set; } public string ErrorMessage { get; set; } = string.Empty; } }

image.png

image.png

⚡ 性能优化秘籍

1. 图表更新频率控制

C#
// 每10个数据点更新一次图表,避免界面卡顿 chartUpdateCounter++; if (chartUpdateCounter % 10 == 0 || requestIndexData.Count < 50) { UpdateScottPlotData(); }

2. 内存占用优化

C#
// 限制显示数据点数量,避免内存溢出 if (requestIndexData.Count > 2000) { requestIndexData.RemoveAt(0); responseTimeData.RemoveAt(0); }

3. 线程安全保障

C#
private readonly object lockObject = new object(); lock (lockObject) { testResults.Add(testResult); completedRequests++; // 关键数据更新操作 }

🎯 核心要点总结

1. 技术选型要前瞻性:ScottPlot 5.0+的API变化提醒我们,选择技术栈时要考虑版本兼容性,并及时跟进最新文档。

2. 异步编程是王道:通过SemaphoreSlimHttpClient的完美结合,实现了高效的并发控制,这是现代C#开发的标准模式。

3. 用户体验至关重要:实时图表更新、响应式布局、现代化UI设计,技术实力最终要通过用户体验来体现价值。


**你在项目中是如何进行性能测试的?**遇到过哪些有趣的性能瓶颈问题?欢迎在评论区分享你的实战经验,让我们一起探讨更多性能优化的黑科技!

觉得这个压力测试工具有用的话,请转发给更多需要的同行! 下期我们将深入探讨C#中的内存优化技巧,敬请期待!

关注我,获取更多C#开发实战干货!

相关信息

通过网盘分享的文件:AppWebStressTester.zip 链接: https://pan.baidu.com/s/14gILepUxBrudG3zk9bsanQ?pwd=mpnr 提取码: mpnr --来自百度网盘超级会员v9的分享

本文作者:技术老小子

本文链接:

版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!