278 lines
7.2 KiB
Go
278 lines
7.2 KiB
Go
package speedtestdotnet
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
const (
|
|
maxDownstreamTestCount = 4
|
|
maxTransferSize = 8 * 1024 * 1024
|
|
pingTimeout = time.Second * 5
|
|
speedTestTimeout = time.Second * 10
|
|
cmdTimeout = time.Second
|
|
latencyMaxTestCount = 60
|
|
dataBlockSize = 16 * 1024 //16KB
|
|
)
|
|
|
|
var (
|
|
errInvalidServerResponse = errors.New("Invalid server response")
|
|
errPingFailure = errors.New("Failed to complete ping test")
|
|
errDontBeADick = errors.New("requested ping count too high")
|
|
startBlockSize = uint64(4096) //4KB
|
|
dataBlock []byte
|
|
)
|
|
|
|
func init() {
|
|
base := []byte("ABCDEFGHIJ")
|
|
dataBlock = make([]byte, dataBlockSize)
|
|
for i := range dataBlock {
|
|
dataBlock[i] = base[i%len(base)]
|
|
}
|
|
}
|
|
|
|
type durations []time.Duration
|
|
|
|
func (ts *Testserver) ping(count int) ([]time.Duration, error) {
|
|
var errRet []time.Duration
|
|
if count > latencyMaxTestCount {
|
|
return errRet, errDontBeADick
|
|
}
|
|
//establish connection to the host
|
|
conn, err := net.DialTimeout("tcp", ts.Host, pingTimeout)
|
|
if err != nil {
|
|
return errRet, err
|
|
}
|
|
defer conn.Close()
|
|
|
|
durs := []time.Duration{}
|
|
buff := make([]byte, 256)
|
|
for i := 0; i < count; i++ {
|
|
t := time.Now()
|
|
fmt.Fprintf(conn, "PING %d\n", uint(t.UnixNano()/1000000))
|
|
conn.SetReadDeadline(time.Now().Add(pingTimeout))
|
|
n, err := conn.Read(buff)
|
|
if err != nil {
|
|
return errRet, err
|
|
}
|
|
conn.SetReadDeadline(time.Time{})
|
|
d := time.Since(t)
|
|
flds := strings.Fields(strings.TrimRight(string(buff[0:n]), "\n"))
|
|
if len(flds) != 2 {
|
|
return errRet, errInvalidServerResponse
|
|
}
|
|
if flds[0] != "PONG" {
|
|
return errRet, errInvalidServerResponse
|
|
}
|
|
if _, err = strconv.ParseInt(flds[1], 10, 64); err != nil {
|
|
return errRet, errInvalidServerResponse
|
|
}
|
|
durs = append(durs, d)
|
|
}
|
|
if len(durs) != count {
|
|
return errRet, errPingFailure
|
|
}
|
|
return durs, nil
|
|
}
|
|
|
|
//MedianPing runs a latency test against the server and stores the median latency
|
|
func (ts *Testserver) MedianPing(count int) (time.Duration, error) {
|
|
var errRet time.Duration
|
|
durs, err := ts.ping(count)
|
|
if err != nil {
|
|
return errRet, err
|
|
}
|
|
sort.Sort(durations(durs))
|
|
ts.Latency = durs[count/2]
|
|
return durs[count/2], nil
|
|
}
|
|
|
|
//Ping will run count number of latency tests and return the results of each
|
|
func (ts *Testserver) Ping(count int) ([]time.Duration, error) {
|
|
return ts.ping(count)
|
|
}
|
|
|
|
//throwBytes chucks bytes at the remote server then listens for a response
|
|
func throwBytes(conn io.ReadWriter, count uint64) error {
|
|
var writeBytes uint64
|
|
var b []byte
|
|
buff := make([]byte, 128)
|
|
for writeBytes < count {
|
|
if (count - writeBytes) >= uint64(len(dataBlock)) {
|
|
b = dataBlock
|
|
} else {
|
|
b = dataBlock[0:(count - writeBytes)]
|
|
}
|
|
n, err := conn.Write(b)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
writeBytes += uint64(n)
|
|
}
|
|
//read the response
|
|
n, err := conn.Read(buff)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if n == 0 {
|
|
return fmt.Errorf("Failed to get OK on upload")
|
|
}
|
|
if !strings.HasPrefix(string(buff[0:n]), "OK ") {
|
|
return fmt.Errorf("Failed to get OK on upload")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
//readBytes reads until we get a newline or an error
|
|
func readBytes(rdr io.Reader, count uint64) error {
|
|
var rBytes uint64
|
|
buff := make([]byte, 4096)
|
|
for rBytes < count {
|
|
n, err := rdr.Read(buff)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
rBytes += uint64(n)
|
|
if n == 0 {
|
|
break
|
|
}
|
|
if buff[n-1] == '\n' {
|
|
break
|
|
}
|
|
}
|
|
if rBytes != count {
|
|
return fmt.Errorf("Failed entire read: %d != %d", rBytes, count)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
//Upstream measures upstream bandwidth in bps
|
|
func (ts *Testserver) Upstream(duration int) (uint64, error) {
|
|
var currBps uint64
|
|
sz := startBlockSize
|
|
conn, err := net.DialTimeout("tcp", ts.Host, speedTestTimeout)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
targetTestDuration := time.Second * time.Duration(duration)
|
|
defer conn.Close()
|
|
|
|
//we repeat the tests until we have a test that lasts at least N seconds
|
|
for i := 0; i < maxDownstreamTestCount; i++ {
|
|
//request a download of size sz and set a deadline
|
|
if err = conn.SetWriteDeadline(time.Now().Add(cmdTimeout)); err != nil {
|
|
return 0, err
|
|
}
|
|
cmdStr := fmt.Sprintf("UPLOAD %d 0\n", sz)
|
|
if _, err := conn.Write([]byte(cmdStr)); err != nil {
|
|
return 0, err
|
|
}
|
|
if err = conn.SetWriteDeadline(time.Time{}); err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
ts := time.Now() //set start time mark
|
|
if err = conn.SetWriteDeadline(time.Now().Add(speedTestTimeout)); err != nil {
|
|
return 0, err
|
|
}
|
|
if err := throwBytes(conn, sz-uint64(len(cmdStr))); err != nil {
|
|
return 0, err
|
|
}
|
|
if err = conn.SetReadDeadline(time.Time{}); err != nil {
|
|
return 0, err
|
|
}
|
|
//check if our test was a reasonable timespan
|
|
dur := time.Since(ts)
|
|
currBps = bps(sz, dur)
|
|
if dur.Nanoseconds() > targetTestDuration.Nanoseconds() || sz == maxTransferSize {
|
|
_, err = fmt.Fprintf(conn, "QUIT\n")
|
|
return bps(sz, dur), err
|
|
}
|
|
//test was too short, try again
|
|
sz = calcNextSize(sz, dur)
|
|
if sz > maxTransferSize {
|
|
sz = maxTransferSize
|
|
}
|
|
}
|
|
|
|
_, err = fmt.Fprintf(conn, "QUIT\n")
|
|
return currBps, err
|
|
}
|
|
|
|
//Downstream measures upstream bandwidth in bps
|
|
func (ts *Testserver) Downstream(duration int) (uint64, error) {
|
|
var currBps uint64
|
|
sz := startBlockSize
|
|
conn, err := net.DialTimeout("tcp", ts.Host, speedTestTimeout)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
defer conn.Close()
|
|
|
|
targetTestDuration := time.Second * time.Duration(duration)
|
|
//we repeat the tests until we have a test that lasts at least N seconds
|
|
for i := 0; i < maxDownstreamTestCount; i++ {
|
|
//request a download of size sz and set a deadline
|
|
if err = conn.SetWriteDeadline(time.Now().Add(cmdTimeout)); err != nil {
|
|
return 0, err
|
|
}
|
|
fmt.Fprintf(conn, "DOWNLOAD %d\n", sz)
|
|
if err = conn.SetWriteDeadline(time.Time{}); err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
ts := time.Now() //set start time mark
|
|
if err = conn.SetReadDeadline(time.Now().Add(speedTestTimeout)); err != nil {
|
|
return 0, err
|
|
}
|
|
//read until we get a newline
|
|
if err = readBytes(conn, sz); err != nil {
|
|
return 0, err
|
|
}
|
|
if err = conn.SetReadDeadline(time.Time{}); err != nil {
|
|
return 0, err
|
|
}
|
|
//check if our test was a reasonable timespan
|
|
dur := time.Since(ts)
|
|
currBps = bps(sz, dur)
|
|
if dur.Nanoseconds() > targetTestDuration.Nanoseconds() || sz == maxTransferSize {
|
|
_, err = fmt.Fprintf(conn, "QUIT\n")
|
|
return bps(sz, dur), err
|
|
}
|
|
//test was too short, try again
|
|
sz = calcNextSize(sz, dur)
|
|
if sz > maxTransferSize {
|
|
sz = maxTransferSize
|
|
}
|
|
}
|
|
|
|
_, err = fmt.Fprintf(conn, "QUIT\n")
|
|
return currBps, err
|
|
}
|
|
|
|
//calcNextSize takes the current preformance metrics and
|
|
//attempts to calculate what the next size should be
|
|
func calcNextSize(b uint64, dur time.Duration) uint64 {
|
|
if b == 0 {
|
|
return startBlockSize
|
|
}
|
|
target := time.Second * 5
|
|
return (b * uint64(target.Nanoseconds())) / uint64(dur.Nanoseconds())
|
|
}
|
|
|
|
//take the byte count and duration and calcuate a bits per second
|
|
func bps(byteCount uint64, dur time.Duration) uint64 {
|
|
bits := byteCount * 8
|
|
return uint64((bits * 1000000000) / uint64(dur.Nanoseconds()))
|
|
}
|
|
|
|
func (d durations) Len() int { return len(d) }
|
|
func (d durations) Less(i, j int) bool { return d[i].Nanoseconds() < d[j].Nanoseconds() }
|
|
func (d durations) Swap(i, j int) { d[i], d[j] = d[j], d[i] }
|