在现代Web开发中,你是否遇到过这样的困扰:网站上线后突然崩溃,用户投诉不断,但却不知道系统的真实承载能力? 作为开发者,我们经常听到产品经理问:"这个接口能同时处理多少用户请求?",而你却只能凭经验给出一个模糊的答案。
今天,我将分享如何用C#从零打造一个专业级网站压力测试工具,不仅能够精确模拟高并发场景,还能通过可视化图表直观展现系统性能数据。无需付费工具,无需复杂配置,只要掌握核心技术,你就能拥有媲美商业级的性能测试利器!
市面上的压力测试工具要么功能单一,要么价格昂贵。作为C#开发者,我们需要的是:

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更新在主线程执行⚠️ 重要坑点: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;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;
}
场景:新接口上线前的性能评估
场景:长时间高并发场景验证
场景:验证负载均衡器配置效果
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>
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;
}
}


C#// 每10个数据点更新一次图表,避免界面卡顿
chartUpdateCounter++;
if (chartUpdateCounter % 10 == 0 || requestIndexData.Count < 50)
{
UpdateScottPlotData();
}
C#// 限制显示数据点数量,避免内存溢出
if (requestIndexData.Count > 2000)
{
requestIndexData.RemoveAt(0);
responseTimeData.RemoveAt(0);
}
C#private readonly object lockObject = new object();
lock (lockObject)
{
testResults.Add(testResult);
completedRequests++;
// 关键数据更新操作
}
1. 技术选型要前瞻性:ScottPlot 5.0+的API变化提醒我们,选择技术栈时要考虑版本兼容性,并及时跟进最新文档。
2. 异步编程是王道:通过SemaphoreSlim和HttpClient的完美结合,实现了高效的并发控制,这是现代C#开发的标准模式。
3. 用户体验至关重要:实时图表更新、响应式布局、现代化UI设计,技术实力最终要通过用户体验来体现价值。
**你在项目中是如何进行性能测试的?**遇到过哪些有趣的性能瓶颈问题?欢迎在评论区分享你的实战经验,让我们一起探讨更多性能优化的黑科技!
觉得这个压力测试工具有用的话,请转发给更多需要的同行! 下期我们将深入探讨C#中的内存优化技巧,敬请期待!
关注我,获取更多C#开发实战干货!
相关信息
通过网盘分享的文件:AppWebStressTester.zip 链接: https://pan.baidu.com/s/14gILepUxBrudG3zk9bsanQ?pwd=mpnr 提取码: mpnr --来自百度网盘超级会员v9的分享
本文作者:技术老小子
本文链接:
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!