added vendor
This commit is contained in:
+277
@@ -0,0 +1,277 @@
|
||||
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] }
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
package speedtestdotnet
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
var (
|
||||
bits = 8
|
||||
kb = uint64(1024)
|
||||
mb = 1024 * kb
|
||||
gb = 1024 * mb
|
||||
tb = 1024 * gb
|
||||
pb = 1024 * tb
|
||||
tooDamnFast = "Too fast to test"
|
||||
)
|
||||
|
||||
func HumanSpeed(bps uint64) string {
|
||||
if bps > pb {
|
||||
return tooDamnFast
|
||||
} else if bps > tb {
|
||||
return fmt.Sprintf("%.02f Tb/s", float64(bps)/float64(tb))
|
||||
} else if bps > gb {
|
||||
return fmt.Sprintf("%.02f Gb/s", float64(bps)/float64(gb))
|
||||
} else if bps > mb {
|
||||
return fmt.Sprintf("%.02f Mb/s", float64(bps)/float64(mb))
|
||||
} else if bps > kb {
|
||||
return fmt.Sprintf("%.02f Kb/s", float64(bps)/float64(kb))
|
||||
}
|
||||
return fmt.Sprintf("%d bps", bps)
|
||||
}
|
||||
+204
@@ -0,0 +1,204 @@
|
||||
package speedtestdotnet
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/kellydunn/golang-geo"
|
||||
)
|
||||
|
||||
const (
|
||||
serversConfigUrl string = `http://www.speedtest.net/speedtest-servers-static.php`
|
||||
clientConfigUrl string = `http://www.speedtest.net/speedtest-config.php`
|
||||
getTimeout time.Duration = 2 * time.Second
|
||||
)
|
||||
|
||||
type Testserver struct {
|
||||
Name string
|
||||
Sponsor string
|
||||
Country string
|
||||
Lat float64
|
||||
Long float64
|
||||
Distance float64 //distance from server in KM
|
||||
URLs []string
|
||||
Host string
|
||||
Latency time.Duration //latency in ms
|
||||
}
|
||||
type testServerlist []Testserver
|
||||
|
||||
type Config struct {
|
||||
LicenseKey string
|
||||
IP net.IP
|
||||
Lat float64
|
||||
Long float64
|
||||
ISP string
|
||||
Servers []Testserver
|
||||
}
|
||||
|
||||
type sconfig struct {
|
||||
XMLName xml.Name `xml:"server-config"`
|
||||
Threads int `xml:"threadcount,attr"`
|
||||
IgnoreIDs string `xml:"ignoreids,attr"`
|
||||
}
|
||||
|
||||
type cconfig struct {
|
||||
XMLName xml.Name `xml:"client"`
|
||||
Ip string `xml:"ip,attr"`
|
||||
Lat float64 `xml:"lat,attr"`
|
||||
Long float64 `xml:"lon,attr"`
|
||||
ISP string `xml:"isp,attr"`
|
||||
ISPUpAvg uint `xml:"ispulavg,attr"`
|
||||
ISPDlAvg uint `xml:"ispdlavg,attr"`
|
||||
}
|
||||
|
||||
type speedtestConfig struct {
|
||||
XMLName xml.Name `xml:"settings"`
|
||||
License string `xml:"licensekey"`
|
||||
ClientConfig cconfig `xml:"client"`
|
||||
ServerConfig sconfig `xml:"server-config"`
|
||||
}
|
||||
|
||||
type server struct {
|
||||
XMLName xml.Name `xml:"server"`
|
||||
Url string `xml:"url,attr"`
|
||||
Url2 string `xml:"url2,attr"`
|
||||
Lat float64 `xml:"lat,attr"`
|
||||
Long float64 `xml:"lon,attr"`
|
||||
Name string `xml:"name,attr"`
|
||||
Country string `xml:"country,attr"`
|
||||
CC string `xml:"cc,attr"`
|
||||
Sponsor string `xml:"sponsor,attr"`
|
||||
ID uint `xml:"id,attr"`
|
||||
Host string `xml:"host,attr"`
|
||||
}
|
||||
|
||||
type settings struct {
|
||||
XMLName xml.Name `xml:"settings"`
|
||||
Servers []server `xml:"servers>server"`
|
||||
}
|
||||
|
||||
//GetServerList returns a list of servers in the native speedtest.net structure
|
||||
func GetServerList() ([]server, error) {
|
||||
//get a list of servers
|
||||
clnt := http.Client{
|
||||
Timeout: getTimeout,
|
||||
}
|
||||
//get the server configs
|
||||
req, err := http.NewRequest("GET", serversConfigUrl, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp, err := clnt.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("Invalid status %s", resp.StatusCode)
|
||||
}
|
||||
xmlDec := xml.NewDecoder(resp.Body)
|
||||
sts := settings{}
|
||||
if err := xmlDec.Decode(&sts); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return sts.Servers, nil
|
||||
}
|
||||
|
||||
//GetConfig returns a configuration containing information about our client and a list of acceptable servers sorted by distance
|
||||
func GetConfig() (*Config, error) {
|
||||
//get a client configuration
|
||||
clnt := http.Client{
|
||||
Timeout: getTimeout,
|
||||
}
|
||||
//get the server configs
|
||||
req, err := http.NewRequest("GET", clientConfigUrl, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp, err := clnt.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("Invalid status %s", resp.StatusCode)
|
||||
}
|
||||
xmlDec := xml.NewDecoder(resp.Body)
|
||||
cc := speedtestConfig{}
|
||||
if err := xmlDec.Decode(&cc); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cfg := Config{
|
||||
LicenseKey: cc.License,
|
||||
IP: net.ParseIP(cc.ClientConfig.Ip),
|
||||
Lat: cc.ClientConfig.Lat,
|
||||
Long: cc.ClientConfig.Long,
|
||||
ISP: cc.ClientConfig.ISP,
|
||||
}
|
||||
ignoreIDs := make(map[uint]bool, 1)
|
||||
strIDs := strings.Split(cc.ServerConfig.IgnoreIDs, ",")
|
||||
for i := range strIDs {
|
||||
x, err := strconv.ParseUint(strIDs[i], 10, 32)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
ignoreIDs[uint(x)] = false
|
||||
}
|
||||
srvs, err := GetServerList()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := populateServers(&cfg, srvs, ignoreIDs); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &cfg, nil
|
||||
}
|
||||
|
||||
func populateServers(cfg *Config, srvs []server, ignore map[uint]bool) error {
|
||||
for i := range srvs {
|
||||
//checking if we are ignoring this server
|
||||
_, ok := ignore[srvs[i].ID]
|
||||
if ok {
|
||||
continue
|
||||
}
|
||||
srv := Testserver{
|
||||
Name: srvs[i].Name,
|
||||
Sponsor: srvs[i].Sponsor,
|
||||
Country: srvs[i].Country,
|
||||
Lat: srvs[i].Lat,
|
||||
Long: srvs[i].Long,
|
||||
Host: srvs[i].Host,
|
||||
}
|
||||
if srvs[i].Url != "" {
|
||||
srv.URLs = append(srv.URLs, srvs[i].Url)
|
||||
}
|
||||
if srvs[i].Url2 != "" {
|
||||
srv.URLs = append(srv.URLs, srvs[i].Url2)
|
||||
}
|
||||
p := geo.NewPoint(cfg.Lat, cfg.Long)
|
||||
if p == nil {
|
||||
return errors.New("Invalid client lat/long")
|
||||
}
|
||||
sp := geo.NewPoint(srvs[i].Lat, srvs[i].Long)
|
||||
if sp == nil {
|
||||
return errors.New("Invalid server lat/long")
|
||||
}
|
||||
srv.Distance = p.GreatCircleDistance(sp)
|
||||
cfg.Servers = append(cfg.Servers, srv)
|
||||
}
|
||||
sort.Sort(testServerlist(cfg.Servers))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (tsl testServerlist) Len() int { return len(tsl) }
|
||||
func (tsl testServerlist) Swap(i, j int) { tsl[i], tsl[j] = tsl[j], tsl[i] }
|
||||
func (tsl testServerlist) Less(i, j int) bool { return tsl[i].Distance < tsl[j].Distance }
|
||||
Reference in New Issue
Block a user