client.go 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570
  1. package bwtest
  2. import (
  3. "bufio"
  4. "context"
  5. "encoding/json"
  6. "errors"
  7. "fmt"
  8. "math/rand"
  9. "net"
  10. "sync"
  11. "sync/atomic"
  12. "time"
  13. )
  14. var (
  15. ErrTestAlreadyRunning = errors.New("a bandwidth test is already in progress")
  16. ErrServerRejected = errors.New("remote server rejected bandwidth test request")
  17. )
  18. // BwTestClient manages executing bandwidth tests against a remote BwTest server
  19. type BwTestClient struct {
  20. mu sync.RWMutex
  21. running bool
  22. currentCancel context.CancelFunc
  23. status TestStatus
  24. samples []*LiveSample
  25. sampleCh chan LiveSample
  26. }
  27. // NewClient creates a new BwTestClient instance
  28. func NewClient() *BwTestClient {
  29. return &BwTestClient{
  30. sampleCh: make(chan LiveSample, 100),
  31. }
  32. }
  33. // GetStatus returns the current or most recent test state
  34. func (c *BwTestClient) GetStatus() TestStatus {
  35. c.mu.RLock()
  36. defer c.mu.RUnlock()
  37. st := c.status
  38. if len(c.samples) > 0 {
  39. st.CurrentSample = c.samples[len(c.samples)-1]
  40. st.RecentSamples = c.samples
  41. }
  42. return st
  43. }
  44. // Subscribe returns a channel of live telemetry samples
  45. func (c *BwTestClient) Subscribe() <-chan LiveSample {
  46. return c.sampleCh
  47. }
  48. // Stop terminates any currently active bandwidth test
  49. func (c *BwTestClient) Stop() {
  50. c.mu.Lock()
  51. defer c.mu.Unlock()
  52. if c.running && c.currentCancel != nil {
  53. c.currentCancel()
  54. }
  55. }
  56. // Run executes a bandwidth test asynchronously
  57. func (c *BwTestClient) Run(ctx context.Context, req SessionRequest) error {
  58. c.mu.Lock()
  59. if c.running {
  60. c.mu.Unlock()
  61. return ErrTestAlreadyRunning
  62. }
  63. if req.Target == "" {
  64. req.Target = "127.0.0.1"
  65. }
  66. if req.Port <= 0 {
  67. req.Port = 2000
  68. }
  69. if req.Protocol != "tcp" {
  70. req.Protocol = "udp"
  71. }
  72. if req.Direction != "tx" && req.Direction != "rx" && req.Direction != "both" {
  73. req.Direction = "both"
  74. }
  75. if req.PacketSize <= 0 {
  76. req.PacketSize = 1472
  77. }
  78. if req.Streams <= 0 {
  79. req.Streams = 1
  80. }
  81. if req.Streams > 8 {
  82. req.Streams = 8
  83. }
  84. if req.Duration <= 0 {
  85. req.Duration = 10
  86. }
  87. testCtx, cancel := context.WithCancel(context.Background())
  88. c.currentCancel = cancel
  89. c.running = true
  90. c.samples = make([]*LiveSample, 0, 120)
  91. sessionID := fmt.Sprintf("bw-%d-%d", time.Now().Unix(), rand.Intn(10000))
  92. c.status = TestStatus{
  93. Running: true,
  94. SessionID: sessionID,
  95. Target: req.Target,
  96. Port: req.Port,
  97. Protocol: req.Protocol,
  98. Direction: req.Direction,
  99. BandwidthLimit: req.BandwidthLimit,
  100. PacketSize: req.PacketSize,
  101. Streams: req.Streams,
  102. Duration: req.Duration,
  103. StartTime: time.Now(),
  104. }
  105. c.mu.Unlock()
  106. go c.executeTest(testCtx, cancel, req, sessionID)
  107. return nil
  108. }
  109. func (c *BwTestClient) executeTest(ctx context.Context, cancel context.CancelFunc, req SessionRequest, sessionID string) {
  110. defer func() {
  111. cancel()
  112. c.mu.Lock()
  113. c.running = false
  114. c.status.Running = false
  115. c.finalizeSummary()
  116. c.mu.Unlock()
  117. }()
  118. targetAddr := fmt.Sprintf("%s:%d", req.Target, req.Port)
  119. dialer := net.Dialer{Timeout: 5 * time.Second}
  120. controlConn, err := dialer.DialContext(ctx, "tcp", targetAddr)
  121. if err != nil {
  122. c.mu.Lock()
  123. c.status.Error = fmt.Sprintf("Failed to connect to control port %s: %v", targetAddr, err)
  124. c.mu.Unlock()
  125. return
  126. }
  127. defer controlConn.Close()
  128. // If UDP, bind a local UDP socket
  129. var udpConn *net.UDPConn
  130. var clientUDPPort int
  131. if req.Protocol == "udp" {
  132. uaddr, err := net.ResolveUDPAddr("udp", ":0")
  133. if err == nil {
  134. udpConn, err = net.ListenUDP("udp", uaddr)
  135. }
  136. if err != nil {
  137. c.mu.Lock()
  138. c.status.Error = fmt.Sprintf("Failed to create local UDP socket: %v", err)
  139. c.mu.Unlock()
  140. return
  141. }
  142. defer udpConn.Close()
  143. _ = udpConn.SetReadBuffer(8 * 1024 * 1024)
  144. if la, ok := udpConn.LocalAddr().(*net.UDPAddr); ok {
  145. clientUDPPort = la.Port
  146. }
  147. }
  148. // Send handshake request
  149. hs := SessionHandshake{
  150. Type: "request",
  151. SessionID: sessionID,
  152. Protocol: req.Protocol,
  153. Direction: req.Direction,
  154. BandwidthLimit: req.BandwidthLimit,
  155. PacketSize: req.PacketSize,
  156. Streams: req.Streams,
  157. Duration: req.Duration,
  158. UDPPort: clientUDPPort,
  159. }
  160. b, _ := json.Marshal(hs)
  161. if _, err := controlConn.Write(append(b, '\n')); err != nil {
  162. c.mu.Lock()
  163. c.status.Error = fmt.Sprintf("Handshake send failed: %v", err)
  164. c.mu.Unlock()
  165. return
  166. }
  167. // Read accept response
  168. reader := bufio.NewReader(controlConn)
  169. line, err := reader.ReadBytes('\n')
  170. if err != nil {
  171. c.mu.Lock()
  172. c.status.Error = fmt.Sprintf("Server response error: %v", err)
  173. c.mu.Unlock()
  174. return
  175. }
  176. var acceptResp SessionHandshake
  177. if err := json.Unmarshal(line, &acceptResp); err != nil || acceptResp.Type != "accept" {
  178. c.mu.Lock()
  179. c.status.Error = fmt.Sprintf("Server rejected test: %s", acceptResp.Message)
  180. c.mu.Unlock()
  181. return
  182. }
  183. serverUDPPort := acceptResp.UDPPort
  184. if serverUDPPort <= 0 {
  185. serverUDPPort = req.Port
  186. }
  187. // Statistics counters
  188. var (
  189. localTxBytes atomic.Int64
  190. localTxPackets atomic.Int64
  191. localRxBytes atomic.Int64
  192. localRxPackets atomic.Int64
  193. localLostPackets atomic.Int64
  194. serverRxBytes atomic.Int64
  195. serverRxPackets atomic.Int64
  196. serverLostPackets atomic.Int64
  197. curJitterMs atomic.Uint64 // float64 in bits
  198. jitterMu sync.Mutex
  199. jitterCalc JitterCalculator
  200. maxSeqRecv atomic.Uint64
  201. )
  202. sessionHash := HashSessionID(sessionID)
  203. // Background worker for server feedback reports over TCP control socket
  204. go func() {
  205. for {
  206. line, err := reader.ReadBytes('\n')
  207. if err != nil {
  208. return
  209. }
  210. var rep SessionHandshake
  211. if err := json.Unmarshal(line, &rep); err == nil && rep.Type == "report" {
  212. serverRxBytes.Store(rep.RxBytes)
  213. serverRxPackets.Store(rep.RxPackets)
  214. serverLostPackets.Store(rep.LostPackets)
  215. }
  216. }
  217. }()
  218. var wg sync.WaitGroup
  219. // UDP Data path
  220. if req.Protocol == "udp" {
  221. serverUDPAddr, err := net.ResolveUDPAddr("udp", fmt.Sprintf("%s:%d", req.Target, serverUDPPort))
  222. if err != nil {
  223. c.mu.Lock()
  224. c.status.Error = fmt.Sprintf("Failed to resolve server UDP address: %v", err)
  225. c.mu.Unlock()
  226. return
  227. }
  228. // Transmitter (TX or Both)
  229. if req.Direction == "tx" || req.Direction == "both" {
  230. for s := 0; s < req.Streams; s++ {
  231. wg.Add(1)
  232. streamID := uint8(s)
  233. go func() {
  234. defer wg.Done()
  235. limiter := NewRateLimiter(req.BandwidthLimit / int64(req.Streams))
  236. buf := make([]byte, req.PacketSize)
  237. var seq uint64
  238. hdr := PacketHeader{
  239. Magic: MagicBytes,
  240. Version: ProtoVer,
  241. PacketType: PacketTypeData,
  242. StreamID: streamID,
  243. SessionHash: sessionHash,
  244. PayloadLength: uint32(req.PacketSize - HeaderSize),
  245. }
  246. for {
  247. select {
  248. case <-ctx.Done():
  249. return
  250. default:
  251. }
  252. if limiter != nil {
  253. limiter.Wait(req.PacketSize)
  254. }
  255. seq++
  256. hdr.Sequence = seq
  257. hdr.TimestampNano = time.Now().UnixNano()
  258. EncodeHeader(buf, hdr)
  259. n, err := udpConn.WriteToUDP(buf, serverUDPAddr)
  260. if err == nil {
  261. localTxBytes.Add(int64(n))
  262. localTxPackets.Add(1)
  263. }
  264. }
  265. }()
  266. }
  267. }
  268. // Receiver (RX or Both)
  269. if req.Direction == "rx" || req.Direction == "both" {
  270. wg.Add(1)
  271. go func() {
  272. defer wg.Done()
  273. rxBuf := make([]byte, 65535)
  274. for {
  275. select {
  276. case <-ctx.Done():
  277. return
  278. default:
  279. }
  280. n, _, err := udpConn.ReadFromUDP(rxBuf)
  281. if err != nil {
  282. select {
  283. case <-ctx.Done():
  284. return
  285. default:
  286. time.Sleep(10 * time.Millisecond)
  287. continue
  288. }
  289. }
  290. if n < HeaderSize {
  291. continue
  292. }
  293. hdr, err := DecodeHeader(rxBuf[:n])
  294. if err != nil || hdr.SessionHash != sessionHash {
  295. continue
  296. }
  297. nowNano := time.Now().UnixNano()
  298. localRxBytes.Add(int64(n))
  299. localRxPackets.Add(1)
  300. // Jitter calculation
  301. jitterMu.Lock()
  302. j := jitterCalc.Update(hdr.TimestampNano, nowNano)
  303. jitterMu.Unlock()
  304. curJitterMs.Store(uint64(j * 1000)) // ms * 1000
  305. // Packet loss tracking
  306. lastMax := maxSeqRecv.Load()
  307. if hdr.Sequence > lastMax {
  308. if lastMax > 0 && hdr.Sequence > lastMax+1 {
  309. gap := int64(hdr.Sequence - lastMax - 1)
  310. localLostPackets.Add(gap)
  311. }
  312. maxSeqRecv.Store(hdr.Sequence)
  313. }
  314. }
  315. }()
  316. }
  317. } else if req.Protocol == "tcp" {
  318. // TCP Data path
  319. if req.Direction == "tx" || req.Direction == "both" {
  320. wg.Add(1)
  321. go func() {
  322. defer wg.Done()
  323. chunk := make([]byte, 64*1024)
  324. limiter := NewRateLimiter(req.BandwidthLimit)
  325. for {
  326. select {
  327. case <-ctx.Done():
  328. return
  329. default:
  330. }
  331. if limiter != nil {
  332. limiter.Wait(len(chunk))
  333. }
  334. n, err := controlConn.Write(chunk)
  335. if err != nil {
  336. return
  337. }
  338. localTxBytes.Add(int64(n))
  339. }
  340. }()
  341. }
  342. if req.Direction == "rx" || req.Direction == "both" {
  343. wg.Add(1)
  344. go func() {
  345. defer wg.Done()
  346. buf := make([]byte, 64*1024)
  347. for {
  348. select {
  349. case <-ctx.Done():
  350. return
  351. default:
  352. }
  353. n, err := controlConn.Read(buf)
  354. if err != nil {
  355. return
  356. }
  357. localRxBytes.Add(int64(n))
  358. }
  359. }()
  360. }
  361. }
  362. // Telemetry sampling loop (every 500ms)
  363. sampleTicker := time.NewTicker(500 * time.Millisecond)
  364. defer sampleTicker.Stop()
  365. var lastTxBytes, lastRxBytes int64
  366. lastTime := time.Now()
  367. startTime := lastTime
  368. timeoutChan := make(<-chan time.Time)
  369. if req.Duration > 0 {
  370. timer := time.NewTimer(time.Duration(req.Duration) * time.Second)
  371. defer timer.Stop()
  372. timeoutChan = timer.C
  373. }
  374. for {
  375. select {
  376. case <-ctx.Done():
  377. // Notify remote server to stop
  378. stopMsg := SessionHandshake{Type: "stop", SessionID: sessionID}
  379. sb, _ := json.Marshal(stopMsg)
  380. _, _ = controlConn.Write(append(sb, '\n'))
  381. return
  382. case <-timeoutChan:
  383. stopMsg := SessionHandshake{Type: "stop", SessionID: sessionID}
  384. sb, _ := json.Marshal(stopMsg)
  385. _, _ = controlConn.Write(append(sb, '\n'))
  386. return
  387. case now := <-sampleTicker.C:
  388. intervalSec := now.Sub(lastTime).Seconds()
  389. if intervalSec <= 0 {
  390. intervalSec = 0.5
  391. }
  392. lastTime = now
  393. elapsedSec := now.Sub(startTime).Seconds()
  394. var curTxBytes, curRxBytes int64
  395. var curLostPackets int64
  396. if req.Direction == "tx" {
  397. curTxBytes = localTxBytes.Load()
  398. curRxBytes = serverRxBytes.Load()
  399. curLostPackets = serverLostPackets.Load()
  400. } else if req.Direction == "rx" {
  401. curRxBytes = localRxBytes.Load()
  402. curLostPackets = localLostPackets.Load()
  403. } else { // both
  404. curTxBytes = localTxBytes.Load()
  405. curRxBytes = localRxBytes.Load()
  406. curLostPackets = localLostPackets.Load() + serverLostPackets.Load()
  407. }
  408. deltaTx := curTxBytes - lastTxBytes
  409. deltaRx := curRxBytes - lastRxBytes
  410. lastTxBytes = curTxBytes
  411. lastRxBytes = curRxBytes
  412. txBps := int64(float64(deltaTx*8) / intervalSec)
  413. rxBps := int64(float64(deltaRx*8) / intervalSec)
  414. txMbps := float64(txBps) / 1e6
  415. rxMbps := float64(rxBps) / 1e6
  416. // Total packets & loss %
  417. totalExpectedPkts := localRxPackets.Load() + localLostPackets.Load()
  418. var lossPct float64
  419. if totalExpectedPkts > 0 {
  420. lossPct = (float64(curLostPackets) / float64(totalExpectedPkts)) * 100.0
  421. if lossPct > 100.0 {
  422. lossPct = 100.0
  423. }
  424. }
  425. jitter := float64(curJitterMs.Load()) / 1000.0
  426. sample := &LiveSample{
  427. Timestamp: now,
  428. ElapsedSec: elapsedSec,
  429. TxBps: txBps,
  430. RxBps: rxBps,
  431. TxMbps: txMbps,
  432. RxMbps: rxMbps,
  433. TxPackets: localTxPackets.Load(),
  434. RxPackets: localRxPackets.Load(),
  435. TxBytes: curTxBytes,
  436. RxBytes: curRxBytes,
  437. LostPackets: curLostPackets,
  438. LossPct: lossPct,
  439. JitterMs: jitter,
  440. }
  441. c.mu.Lock()
  442. c.samples = append(c.samples, sample)
  443. if len(c.samples) > 200 {
  444. c.samples = c.samples[len(c.samples)-200:]
  445. }
  446. c.status.ElapsedSec = elapsedSec
  447. c.status.CurrentSample = sample
  448. c.mu.Unlock()
  449. // Non-blocking publish to subscriber channel
  450. select {
  451. case c.sampleCh <- *sample:
  452. default:
  453. }
  454. }
  455. }
  456. }
  457. func (c *BwTestClient) finalizeSummary() {
  458. if len(c.samples) == 0 {
  459. return
  460. }
  461. first := c.samples[0]
  462. last := c.samples[len(c.samples)-1]
  463. duration := last.ElapsedSec
  464. var sumTxMbps, sumRxMbps float64
  465. var peakTxMbps, peakRxMbps float64
  466. var sumLossPct, sumJitter float64
  467. for _, s := range c.samples {
  468. sumTxMbps += s.TxMbps
  469. sumRxMbps += s.RxMbps
  470. sumLossPct += s.LossPct
  471. sumJitter += s.JitterMs
  472. if s.TxMbps > peakTxMbps {
  473. peakTxMbps = s.TxMbps
  474. }
  475. if s.RxMbps > peakRxMbps {
  476. peakRxMbps = s.RxMbps
  477. }
  478. }
  479. count := float64(len(c.samples))
  480. avgTx := sumTxMbps / count
  481. avgRx := sumRxMbps / count
  482. avgLoss := sumLossPct / count
  483. avgJitter := sumJitter / count
  484. c.status.Summary = &TestSummary{
  485. DurationSec: duration,
  486. StartTime: c.status.StartTime,
  487. EndTime: time.Now(),
  488. TotalTxBytes: last.TxBytes,
  489. TotalRxBytes: last.RxBytes,
  490. AvgTxMbps: avgTx,
  491. AvgRxMbps: avgRx,
  492. PeakTxMbps: peakTxMbps,
  493. PeakRxMbps: peakRxMbps,
  494. TotalTxPackets: last.TxPackets,
  495. TotalRxPackets: last.RxPackets,
  496. TotalLostPackets: last.LostPackets,
  497. AvgLossPct: avgLoss,
  498. AvgJitterMs: avgJitter,
  499. }
  500. _ = first
  501. }