package loadsim import ( "context" "errors" "io" "net" "sync" "sync/atomic" "time" ) type DialFunc func(ctx context.Context, network, addr string) (net.Conn, error) type Runner struct { Dial DialFunc } type Stats struct { ConnectionsOpened int64 ConnectionsFailed int64 FramesWritten int64 WriteErrors int64 ResponseBytes int64 ReadErrors int64 } func (r Runner) Run(ctx context.Context, cfg Config) (Stats, error) { phoneBase := cfg.JT808PhoneBase if phoneBase <= 0 { phoneBase = DefaultJT808PhoneBase } if _, err := (&FlagConfig{ protocol: string(cfg.Protocol), addr: cfg.Addr, connections: cfg.Connections, connectRate: cfg.ConnectRatePerSecond, sendInterval: cfg.SendInterval, duration: cfg.Duration, template: cfg.Template, jt808PhoneBase: phoneBase, drainResponses: cfg.DrainResponses, }).Build(); err != nil { return Stats{}, err } var factory *FrameFactory if cfg.SendFrames { var err error factory, err = NewFrameFactoryWithJT808PhoneBase(cfg.Protocol, cfg.Template, phoneBase) if err != nil { return Stats{}, err } } dial := r.Dial if dial == nil { dialer := &net.Dialer{Timeout: 5 * time.Second, KeepAlive: 30 * time.Second} dial = dialer.DialContext } runCtx, cancel := context.WithTimeout(ctx, cfg.Duration) defer cancel() var stats Stats var wg sync.WaitGroup connectionIndex := 0 for i, batch := range ConnectionBatches(cfg.Connections, cfg.ConnectRatePerSecond) { if i > 0 { if !sleepOrDone(runCtx, time.Second) { break } } for range batch { if runCtx.Err() != nil { break } conn, err := dial(runCtx, "tcp", cfg.Addr) if err != nil { atomic.AddInt64(&stats.ConnectionsFailed, 1) continue } atomic.AddInt64(&stats.ConnectionsOpened, 1) wg.Add(1) connIndex := connectionIndex connectionIndex++ go func() { defer wg.Done() if cfg.SendFrames { var readDone chan struct{} if cfg.DrainResponses { readDone = make(chan struct{}) go func() { drainResponses(runCtx, conn, &stats) close(readDone) }() } writeLoop(runCtx, conn, factory, connIndex, cfg.SendInterval, &stats) _ = conn.Close() if readDone != nil { <-readDone } return } <-runCtx.Done() _ = conn.Close() }() } } <-runCtx.Done() wg.Wait() return stats, nil } func drainResponses(ctx context.Context, conn net.Conn, stats *Stats) { buffer := make([]byte, 4096) for { _ = conn.SetReadDeadline(time.Now().Add(time.Second)) read, err := conn.Read(buffer) if read > 0 { atomic.AddInt64(&stats.ResponseBytes, int64(read)) } if err == nil { continue } if ctx.Err() != nil || errors.Is(err, net.ErrClosed) || errors.Is(err, io.EOF) { return } if timeout, ok := err.(net.Error); ok && timeout.Timeout() { continue } atomic.AddInt64(&stats.ReadErrors, 1) return } } func writeLoop(ctx context.Context, conn net.Conn, factory *FrameFactory, connectionIndex int, interval time.Duration, stats *Stats) { for frameIndex := int64(0); ; frameIndex++ { payload, err := factory.Frame(connectionIndex, frameIndex) if err != nil { atomic.AddInt64(&stats.WriteErrors, 1) return } _ = conn.SetWriteDeadline(time.Now().Add(3 * time.Second)) if _, err := conn.Write(payload); err != nil { atomic.AddInt64(&stats.WriteErrors, 1) return } atomic.AddInt64(&stats.FramesWritten, 1) if !sleepOrDone(ctx, interval) { return } } } func sleepOrDone(ctx context.Context, duration time.Duration) bool { timer := time.NewTimer(duration) defer timer.Stop() select { case <-ctx.Done(): return false case <-timer.C: return true } }