Skip to content

Instantly share code, notes, and snippets.

@na--
Last active April 14, 2018 05:38
Show Gist options
  • Select an option

  • Save na--/b205af9648155b477757183dac68b135 to your computer and use it in GitHub Desktop.

Select an option

Save na--/b205af9648155b477757183dac68b135 to your computer and use it in GitHub Desktop.
HTTP testing
package main
import (
"bytes"
"flag"
"io"
"log"
"net"
"net/http"
"net/url"
"strings"
"sync"
"sync/atomic"
"time"
"github.com/valyala/fasthttp"
)
const allTestNames = "stdlib-simple,stdlib-optimized,fasthttp,telnet"
var vus uint
var duration, pause time.Duration
var urlToTest, testNames string
var expectedResponse string
var expectedResponseBytes []byte
var expectedResponseLen int64
func init() {
flag.UintVar(&vus, "u", 8, "Number of virtual users (concurrent goroutines)")
flag.DurationVar(&duration, "d", 5*time.Second, "Test duration")
flag.DurationVar(&pause, "p", 3*time.Second, "Time to sleep between tests")
flag.StringVar(&expectedResponse, "er", "Hello", "Expected response")
flag.StringVar(&urlToTest, "url", "http://127.0.0.1:8000", "The URL to hammer")
flag.StringVar(&testNames, "t", allTestNames, "A comma separated list of tests to perform")
flag.Parse()
expectedResponseBytes = []byte(expectedResponse)
expectedResponseLen = int64(len(expectedResponseBytes))
}
func getTest(name string) func() bool {
switch name {
case "stdlib-simple":
return func() bool {
resp, err := http.Get(urlToTest)
if err != nil {
return false
}
defer resp.Body.Close()
buf := bytes.NewBuffer([]byte{})
respLen, err := io.Copy(buf, resp.Body)
return err == nil &&
respLen == expectedResponseLen &&
bytes.Equal(expectedResponseBytes, buf.Bytes())
}
case "stdlib-optimized":
transport := &http.Transport{
Proxy: nil,
DialContext: (&net.Dialer{
Timeout: 100 * time.Millisecond,
KeepAlive: 30 * time.Second,
DualStack: false,
}).DialContext,
DisableKeepAlives: false,
DisableCompression: true,
MaxIdleConns: 0, // no limit
IdleConnTimeout: 1 * time.Second,
ResponseHeaderTimeout: 100 * time.Millisecond,
TLSHandshakeTimeout: 1 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
}
req, err := http.NewRequest("GET", urlToTest, nil)
if err != nil {
panic(err)
}
respBodyBuffer := make([]byte, 0, expectedResponseLen)
return func() bool {
resp, err := transport.RoundTrip(req)
if err != nil {
log.Println(err)
return false
}
defer resp.Body.Close()
buf := bytes.NewBuffer(respBodyBuffer)
respLen, err := io.Copy(buf, resp.Body)
return err == nil &&
respLen == expectedResponseLen &&
bytes.Equal(expectedResponseBytes, buf.Bytes())
}
case "fasthttp":
req := fasthttp.AcquireRequest()
req.SetRequestURI(urlToTest)
client := &fasthttp.Client{}
return func() bool {
resp := fasthttp.AcquireResponse()
err := client.Do(req, resp)
return err == nil && bytes.Equal(expectedResponseBytes, resp.Body())
}
case "telnet":
parsedURL, err := url.Parse(urlToTest)
if err != nil {
panic(err)
}
tcpAddr, err := net.ResolveTCPAddr("tcp4", parsedURL.Host)
if err != nil {
panic(err)
}
request := []byte("GET / HTTP/1.0\r\nConnection: keep-alive\r\n\r\n")
respBuffer := make([]byte, 1000+expectedResponseLen)
var conn *net.TCPConn
return func() bool {
if conn == nil {
//log.Printf("Dialing tcp directly...")
conn, err = net.DialTCP("tcp4", nil, tcpAddr)
if err != nil {
return false
}
}
_, err := conn.Write(request)
if err != nil {
conn.Close()
conn = nil
return false
}
pos := int64(0)
for {
// Read until expectedResponse
//TODO: add some timeouts
n, err := conn.Read(respBuffer[pos:])
if err != nil {
conn.Close()
conn = nil
return false
}
pos += int64(n)
if bytes.Equal(expectedResponseBytes, respBuffer[pos-expectedResponseLen:pos]) {
return true
}
}
}
default:
panic("No such test type")
}
}
func execTest(name string) float64 {
var requests, errors uint64
wg := sync.WaitGroup{}
log.Printf("[%s] Start hammering %s with %d VUs for %s!", name, urlToTest, vus, duration)
for i := uint(0); i < vus; i++ {
wg.Add(1)
go func(vu uint, test func() bool) {
defer wg.Done()
var localReqCountCopy, newReqCount uint64
//log.Printf("[%s] VU %d started!", name, vu)
for {
if !test() {
atomic.AddUint64(&errors, 1)
}
newReqCount = atomic.AddUint64(&requests, 1)
if localReqCountCopy > newReqCount {
//log.Printf("[%s] VU %d finished!", name, vu)
return
}
localReqCountCopy = newReqCount
}
}(i, getTest(name))
}
log.Printf("[%s] Launched %d VUs, sleeping for %s...", name, vus, duration)
time.Sleep(duration)
log.Printf("[%s] Done!", name)
errCount := atomic.SwapUint64(&errors, 0)
reqCount := atomic.SwapUint64(&requests, 0) - errCount
rps := float64(reqCount) / (float64(duration) / float64(time.Second))
log.Printf("[%s] Made %d requests with %d errors; RPS=%.0f", name, reqCount, errCount, rps)
log.Printf("[%s] Waiting for VUs to finish...", name)
wg.Wait()
return rps
}
func main() {
log.SetFlags(log.Lmicroseconds)
for n, testName := range strings.Split(testNames, ",") {
if n > 0 {
log.Printf("Sleeping for %s before the next test...\n\n", pause)
time.Sleep(pause)
}
log.Printf("Starting test %s...", testName)
execTest(testName)
}
}
package main
import (
"net/http"
)
func sayHello(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Hello"))
}
func main() {
http.HandleFunc("/", sayHello)
if err := http.ListenAndServe(":8000", nil); err != nil {
panic(err)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment