从零构建Golang CLI压力测试工具的实现方案
1. 基础项目结构
首先创建基本的CLI框架,处理命令行参数:
// main.go
package main
import (
"flag"
"fmt"
"os"
)
type Config struct {
URL string
Requests int
Concurrency int
Timeout int
}
func main() {
config := parseFlags()
fmt.Printf("开始压力测试: %s\n", config.URL)
fmt.Printf("总请求数: %d, 并发数: %d\n", config.Requests, config.Concurrency)
// 启动压力测试
runStressTest(config)
}
func parseFlags() *Config {
config := &Config{}
flag.StringVar(&config.URL, "url", "", "目标URL (必需)")
flag.IntVar(&config.Requests, "requests", 100, "总请求数")
flag.IntVar(&config.Concurrency, "concurrency", 10, "并发数")
flag.IntVar(&config.Timeout, "timeout", 30, "超时时间(秒)")
flag.Parse()
if config.URL == "" {
fmt.Println("错误: 必须提供 -url 参数")
os.Exit(1)
}
return config
}
2. 并发HTTP压力测试核心实现
使用goroutine和channel实现并发控制:
// stress.go
package main
import (
"fmt"
"net/http"
"sync"
"time"
)
type Result struct {
Duration time.Duration
Status int
Error bool
}
type Statistics struct {
TotalRequests int
FailedRequests int
SuccessRequests int
TotalDuration time.Duration
MinDuration time.Duration
MaxDuration time.Duration
mu sync.Mutex
}
func runStressTest(config *Config) {
stats := &Statistics{
MinDuration: time.Hour,
}
startTime := time.Now()
// 创建工作池
jobs := make(chan int, config.Requests)
results := make(chan Result, config.Requests)
// 启动worker
var wg sync.WaitGroup
for i := 0; i < config.Concurrency; i++ {
wg.Add(1)
go worker(i, config, jobs, results, &wg)
}
// 分发任务
for i := 0; i < config.Requests; i++ {
jobs <- i
}
close(jobs)
// 收集结果
go func() {
wg.Wait()
close(results)
}()
// 处理结果
for result := range results {
stats.update(result)
}
elapsed := time.Since(startTime)
stats.printReport(elapsed)
}
func worker(id int, config *Config, jobs <-chan int, results chan<- Result, wg *sync.WaitGroup) {
defer wg.Done()
client := &http.Client{
Timeout: time.Duration(config.Timeout) * time.Second,
}
for range jobs {
start := time.Now()
resp, err := client.Get(config.URL)
duration := time.Since(start)
result := Result{
Duration: duration,
Error: err != nil,
}
if err == nil {
result.Status = resp.StatusCode
resp.Body.Close()
}
results <- result
}
}
func (s *Statistics) update(r Result) {
s.mu.Lock()
defer s.mu.Unlock()
s.TotalRequests++
s.TotalDuration += r.Duration
if r.Error || (r.Status >= 400 && r.Status < 600) {
s.FailedRequests++
} else {
s.SuccessRequests++
}
if r.Duration < s.MinDuration {
s.MinDuration = r.Duration
}
if r.Duration > s.MaxDuration {
s.MaxDuration = r.Duration
}
}
func (s *Statistics) printReport(elapsed time.Duration) {
avgDuration := time.Duration(0)
if s.TotalRequests > 0 {
avgDuration = s.TotalDuration / time.Duration(s.TotalRequests)
}
fmt.Println("\n=== 压力测试报告 ===")
fmt.Printf("总耗时: %v\n", elapsed)
fmt.Printf("总请求数: %d\n", s.TotalRequests)
fmt.Printf("成功请求: %d (%.1f%%)\n", s.SuccessRequests,
float64(s.SuccessRequests)/float64(s.TotalRequests)*100)
fmt.Printf("失败请求: %d (%.1f%%)\n", s.FailedRequests,
float64(s.FailedRequests)/float64(s.TotalRequests)*100)
fmt.Printf("平均延迟: %v\n", avgDuration)
fmt.Printf("最小延迟: %v\n", s.MinDuration)
fmt.Printf("最大延迟: %v\n", s.MaxDuration)
fmt.Printf("请求速率: %.1f req/s\n",
float64(s.TotalRequests)/elapsed.Seconds())
}
3. 安全验证实现
添加基本的安全检查防止滥用:
// security.go
package main
import (
"fmt"
"net/url"
"os"
"strings"
)
func validateTarget(target string) error {
u, err := url.Parse(target)
if err != nil {
return fmt.Errorf("无效的URL: %v", err)
}
// 禁止本地地址
if isLocalhost(u.Hostname()) {
return fmt.Errorf("禁止测试本地地址")
}
// 检查常见黑名单
if isBlacklisted(u.Hostname()) {
return fmt.Errorf("目标地址在黑名单中")
}
return nil
}
func isLocalhost(hostname string) bool {
localhosts := []string{
"localhost",
"127.0.0.1",
"::1",
"0.0.0.0",
}
for _, lh := range localhosts {
if hostname == lh || strings.HasPrefix(hostname, lh+".") {
return true
}
}
return false
}
func isBlacklisted(hostname string) bool {
// 这里可以添加自定义黑名单逻辑
// 例如:禁止测试特定域名
blacklist := []string{
"example.com",
"test.com",
}
for _, bl := range blacklist {
if strings.Contains(hostname, bl) {
return true
}
}
return false
}
// 在main函数中添加验证
func main() {
config := parseFlags()
// 安全验证
if err := validateTarget(config.URL); err != nil {
fmt.Printf("安全验证失败: %v\n", err)
os.Exit(1)
}
// ... 其余代码
}
4. WebSocket支持扩展
添加WebSocket测试功能:
// websocket.go
package main
import (
"flag"
"time"
"github.com/gorilla/websocket"
)
type WSConfig struct {
UseWebSocket bool
Message string
}
var wsConfig WSConfig
func init() {
flag.BoolVar(&wsConfig.UseWebSocket, "ws", false, "使用WebSocket协议")
flag.StringVar(&wsConfig.Message, "ws-message", "test", "WebSocket测试消息")
}
func runWebSocketTest(config *Config) {
// WebSocket连接实现
dialer := websocket.Dialer{
HandshakeTimeout: time.Duration(config.Timeout) * time.Second,
}
conn, _, err := dialer.Dial(config.URL, nil)
if err != nil {
// 处理错误
return
}
defer conn.Close()
// WebSocket消息测试逻辑
// ...
}
5. 构建和使用
创建go.mod文件:
go mod init stress-tool
构建工具:
go build -o stress-tool
使用示例:
# HTTP测试
./stress-tool -url https://example.com -requests 1000 -concurrency 50
# 带安全验证
./stress-tool -url http://localhost:8080 # 会被拒绝
# WebSocket测试
./stress-tool -url ws://example.com/ws -ws -requests 500
6. 性能优化建议
- 使用sync.Pool重用对象减少GC压力
- 实现连接池复用HTTP连接
- 添加速率限制控制
- 实现实时进度显示
这个实现完全使用标准库,没有外部依赖,涵盖了并发控制、错误处理、安全验证等核心概念。WebSocket部分需要gorilla/websocket库,但你可以按照类似模式实现自己的WebSocket客户端。