168 lines
4.3 KiB
Go
168 lines
4.3 KiB
Go
package agent
|
|
|
|
import (
|
|
"context"
|
|
"crypto/rand"
|
|
"encoding/json"
|
|
"errors"
|
|
"io"
|
|
"net/http"
|
|
"time"
|
|
|
|
"github.com/x-grk/longpolling-remote-access/internal/proto"
|
|
)
|
|
|
|
const (
|
|
// speedWindow is how long each direction is measured for, unless the
|
|
// server asks for another window; speedWindowMax bounds what it may ask.
|
|
speedWindow = 3 * time.Second
|
|
speedWindowMax = 30 * time.Second
|
|
// speedWait is how long the download may take to start: in a relayed test
|
|
// its bytes come from the browser.
|
|
speedWait = 10 * time.Second
|
|
// speedUpCap bounds the upload, whatever the link does in the window.
|
|
speedUpCap = 48 << 20
|
|
)
|
|
|
|
// doSpeed measures the agent's HTTP link to the server: round-trip time of an
|
|
// empty request, then how much flows each way in the window. In a relayed
|
|
// test the server pipes both streams to the browser, so they run end to end.
|
|
func (a *Agent) doSpeed(ctx context.Context, env proto.TaskEnvelope) proto.TaskResult {
|
|
var args proto.SysSpeedArgs
|
|
if len(env.Args) > 0 {
|
|
_ = json.Unmarshal(env.Args, &args)
|
|
}
|
|
window := speedWindow
|
|
if args.WindowMs > 0 {
|
|
window = min(time.Duration(args.WindowMs)*time.Millisecond, speedWindowMax)
|
|
}
|
|
res, err := a.measureSpeed(ctx, args.Relay, window)
|
|
if err != nil {
|
|
return proto.TaskResult{Error: err.Error()}
|
|
}
|
|
data, _ := json.Marshal(res)
|
|
return proto.TaskResult{OK: true, Data: data}
|
|
}
|
|
|
|
func (a *Agent) measureSpeed(ctx context.Context, relay bool, window time.Duration) (proto.SysSpeedResult, error) {
|
|
res := proto.SysSpeedResult{Relay: relay}
|
|
streams := "/api/agent/speed"
|
|
if relay {
|
|
streams += "/relay"
|
|
}
|
|
// Ping: the first request may pay for a new connection, so keep the best.
|
|
for i := 0; i < 4; i++ {
|
|
start := time.Now()
|
|
resp, err := a.speedRequest(ctx, http.MethodGet, "/api/agent/speed/down?bytes=0", nil)
|
|
if err != nil {
|
|
return res, err
|
|
}
|
|
io.Copy(io.Discard, resp.Body)
|
|
resp.Body.Close()
|
|
ms := float64(time.Since(start).Microseconds()) / 1000
|
|
if i == 0 || ms < res.PingMs {
|
|
res.PingMs = ms
|
|
}
|
|
}
|
|
|
|
down, err := a.speedDown(ctx, streams+"/down", window)
|
|
if err != nil {
|
|
return res, err
|
|
}
|
|
res.DownBps = down
|
|
|
|
// Up: send until the window closes or the cap is hit.
|
|
body := &speedSource{deadline: time.Now().Add(window), left: speedUpCap}
|
|
start := time.Now()
|
|
resp, err := a.speedRequest(ctx, http.MethodPost, streams+"/up", body)
|
|
if err != nil {
|
|
return res, err
|
|
}
|
|
io.Copy(io.Discard, resp.Body)
|
|
resp.Body.Close()
|
|
res.UpBps = bps(int64(speedUpCap-body.left), time.Since(start))
|
|
return res, nil
|
|
}
|
|
|
|
// speedDown reads what the server streams for the window, counted from the
|
|
// first byte, and returns the rate.
|
|
func (a *Agent) speedDown(ctx context.Context, path string, window time.Duration) (float64, error) {
|
|
ctx, cancel := context.WithCancel(ctx)
|
|
defer cancel()
|
|
t := time.AfterFunc(speedWait, cancel)
|
|
defer t.Stop()
|
|
resp, err := a.speedRequest(ctx, http.MethodGet, path, nil)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
defer resp.Body.Close()
|
|
var (
|
|
n int64
|
|
start time.Time
|
|
buf = make([]byte, 64<<10)
|
|
)
|
|
for {
|
|
k, err := resp.Body.Read(buf)
|
|
if k > 0 && start.IsZero() {
|
|
start = time.Now()
|
|
t.Reset(window)
|
|
}
|
|
n += int64(k)
|
|
if err != nil {
|
|
break
|
|
}
|
|
}
|
|
if start.IsZero() {
|
|
return 0, errors.New("нет данных для замера")
|
|
}
|
|
return bps(n, time.Since(start)), nil
|
|
}
|
|
|
|
func (a *Agent) speedRequest(ctx context.Context, method, path string, body io.Reader) (*http.Response, error) {
|
|
req, err := http.NewRequestWithContext(ctx, method, a.cfg.Host+path, body)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
a.authHeaders(req)
|
|
resp, err := a.http.Do(req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusNoContent {
|
|
resp.Body.Close()
|
|
return nil, errStatus(resp.StatusCode)
|
|
}
|
|
return resp, nil
|
|
}
|
|
|
|
func bps(n int64, d time.Duration) float64 {
|
|
if d <= 0 {
|
|
return 0
|
|
}
|
|
return float64(n) * 8 / d.Seconds()
|
|
}
|
|
|
|
// speedSource is an upload body of incompressible bytes that ends at the
|
|
// deadline or after `left` bytes.
|
|
type speedSource struct {
|
|
deadline time.Time
|
|
left int
|
|
block []byte
|
|
}
|
|
|
|
func (s *speedSource) Read(p []byte) (int, error) {
|
|
if s.left <= 0 || !time.Now().Before(s.deadline) {
|
|
return 0, io.EOF
|
|
}
|
|
if s.block == nil {
|
|
s.block = make([]byte, 64<<10)
|
|
rand.Read(s.block)
|
|
}
|
|
n := copy(p, s.block)
|
|
if n > s.left {
|
|
n = s.left
|
|
}
|
|
s.left -= n
|
|
return n, nil
|
|
}
|