diff --git a/ctx.go b/ctx.go index f5c426b8..83185773 100644 --- a/ctx.go +++ b/ctx.go @@ -15,12 +15,12 @@ import ( "log" "mime/multipart" "net/http" - "net/url" "os" "path/filepath" "regexp" "strconv" "strings" + "sync" "text/template" "time" @@ -235,10 +235,10 @@ func (ctx *Ctx) BodyParser(out interface{}) error { return xml.Unmarshal(ctx.Fasthttp.Request.Body(), out) case MIMEApplicationForm: // application/x-www-form-urlencoded schemaDecoder.SetAliasTag("form") - data, err := url.ParseQuery(getString(ctx.Fasthttp.PostBody())) - if err != nil { - return err - } + data := make(map[string][]string) + ctx.Fasthttp.PostArgs().VisitAll(func(key []byte, val []byte) { + data[getString(key)] = append(data[getString(key)], getString(val)) + }) return schemaDecoder.Decode(out, data) } @@ -266,19 +266,26 @@ func (ctx *Ctx) BodyParser(out interface{}) error { return fmt.Errorf("bodyparser: cannot parse content-type: %v", ctype) } +// queryDecoderPool helps to improve QueryParser's performance +var queryDecoderPool = &sync.Pool{New: func() interface{} { + var decoder = schema.NewDecoder() + decoder.SetAliasTag("query") + decoder.IgnoreUnknownKeys(true) + return decoder +}} + // QueryParser binds the query string to a struct. func (ctx *Ctx) QueryParser(out interface{}) error { if ctx.Fasthttp.QueryArgs().Len() > 0 { - var schemaDecoderQuery = schema.NewDecoder() - schemaDecoderQuery.SetAliasTag("query") - schemaDecoderQuery.IgnoreUnknownKeys(true) + var decoder = queryDecoderPool.Get().(*schema.Decoder) + defer queryDecoderPool.Put(decoder) data := make(map[string][]string) ctx.Fasthttp.QueryArgs().VisitAll(func(key []byte, val []byte) { data[getString(key)] = append(data[getString(key)], getString(val)) }) - return schemaDecoderQuery.Decode(out, data) + return decoder.Decode(out, data) } return nil } @@ -496,13 +503,20 @@ func (ctx *Ctx) IP() string { } // IPs returns an string slice of IP addresses specified in the X-Forwarded-For request header. -func (ctx *Ctx) IPs() []string { - // TODO: improve with for iteration and string.Index -> like in Accepts - ips := strings.Split(ctx.Get(HeaderXForwardedFor), ",") - for i := range ips { - ips[i] = utils.Trim(ips[i], ' ') +func (ctx *Ctx) IPs() (ips []string) { + header := ctx.Fasthttp.Request.Header.Peek(HeaderXForwardedFor) + ips = make([]string, bytes.Count(header, []byte(","))+1) + var commaPos, i int + for { + commaPos = bytes.IndexByte(header, ',') + if commaPos != -1 { + ips[i] = getString(header[:commaPos]) + header, i = header[commaPos+2:], i+1 + } else { + ips[i] = getString(header) + return + } } - return ips } // Is returns the matching content type, @@ -822,7 +836,7 @@ func (ctx *Ctx) Render(name string, bind interface{}, layouts ...string) (err er return err } } - // Set Contet-Type to text/html + // Set Content-Type to text/html ctx.Set(HeaderContentType, MIMETextHTMLCharsetUTF8) // Set rendered template to body ctx.SendBytes(buf.Bytes()) @@ -909,7 +923,7 @@ func (ctx *Ctx) SendFile(file string, compress ...bool) error { file += "/" } } - // Set new URI for filehandler + // Set new URI for fileHandler ctx.Fasthttp.Request.SetRequestURI(file) // Save status code status := ctx.Fasthttp.Response.StatusCode() diff --git a/ctx_test.go b/ctx_test.go index 5d6aa191..9a7484fd 100644 --- a/ctx_test.go +++ b/ctx_test.go @@ -288,15 +288,35 @@ func Test_Ctx_BodyParser(t *testing.T) { app := New() ctx := app.AcquireCtx(&fasthttp.RequestCtx{}) defer app.ReleaseCtx(ctx) + type Demo struct { Name string `json:"name" xml:"name" form:"name" query:"name"` } - ctx.Fasthttp.Request.SetBody([]byte(`{"name":"john"}`)) - ctx.Fasthttp.Request.Header.SetContentType(MIMEApplicationJSON) - ctx.Fasthttp.Request.Header.SetContentLength(len([]byte(`{"name":"john"}`))) - d := new(Demo) - utils.AssertEqual(t, nil, ctx.BodyParser(d)) - utils.AssertEqual(t, "john", d.Name) + + testDecodeParser := func(contentType, body string) { + ctx.Fasthttp.Request.Header.SetContentType(contentType) + ctx.Fasthttp.Request.SetBody([]byte(body)) + ctx.Fasthttp.Request.Header.SetContentLength(len(body)) + d := new(Demo) + utils.AssertEqual(t, nil, ctx.BodyParser(d)) + utils.AssertEqual(t, "john", d.Name) + } + + testDecodeParser(MIMEApplicationJSON, `{"name":"john"}`) + testDecodeParser(MIMEApplicationXML, `john`) + testDecodeParser(MIMEApplicationJSON, `{"name":"john"}`) + testDecodeParser(MIMEApplicationForm, "name=john") + testDecodeParser(MIMEMultipartForm+`;boundary="b"`, "--b\r\nContent-Disposition: form-data; name=\"name\"\r\n\r\njohn\r\n--b--") + + testDecodeParserError := func(contentType, body string) { + ctx.Fasthttp.Request.Header.SetContentType(contentType) + ctx.Fasthttp.Request.SetBody([]byte(body)) + ctx.Fasthttp.Request.Header.SetContentLength(len(body)) + utils.AssertEqual(t, false, ctx.BodyParser(nil) == nil) + } + + testDecodeParserError("invalid-content-type", "") + testDecodeParserError(MIMEMultipartForm+`;boundary="b"`, "--b") type Query struct { ID int @@ -311,7 +331,102 @@ func Test_Ctx_BodyParser(t *testing.T) { utils.AssertEqual(t, 2, len(q.Hobby)) } -// TODO Benchmark_Ctx_BodyParser +// go test -v -run=^$ -bench=Benchmark_Ctx_BodyParser_JSON -benchmem -count=4 +func Benchmark_Ctx_BodyParser_JSON(b *testing.B) { + app := New() + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + type Demo struct { + Name string `json:"name"` + } + body := []byte(`{"name":"john"}`) + c.Fasthttp.Request.SetBody(body) + c.Fasthttp.Request.Header.SetContentType(MIMEApplicationJSON) + c.Fasthttp.Request.Header.SetContentLength(len(body)) + d := new(Demo) + + b.ReportAllocs() + b.ResetTimer() + + for n := 0; n < b.N; n++ { + _ = c.BodyParser(d) + } + utils.AssertEqual(b, nil, c.BodyParser(d)) + utils.AssertEqual(b, "john", d.Name) +} + +// go test -v -run=^$ -bench=Benchmark_Ctx_BodyParser_XML -benchmem -count=4 +func Benchmark_Ctx_BodyParser_XML(b *testing.B) { + app := New() + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + type Demo struct { + Name string `xml:"name"` + } + body := []byte("john") + c.Fasthttp.Request.SetBody(body) + c.Fasthttp.Request.Header.SetContentType(MIMEApplicationXML) + c.Fasthttp.Request.Header.SetContentLength(len(body)) + d := new(Demo) + + b.ReportAllocs() + b.ResetTimer() + + for n := 0; n < b.N; n++ { + _ = c.BodyParser(d) + } + utils.AssertEqual(b, nil, c.BodyParser(d)) + utils.AssertEqual(b, "john", d.Name) +} + +// go test -v -run=^$ -bench=Benchmark_Ctx_BodyParser_Form -benchmem -count=4 +func Benchmark_Ctx_BodyParser_Form(b *testing.B) { + app := New() + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + type Demo struct { + Name string `form:"name"` + } + body := []byte("name=john") + c.Fasthttp.Request.SetBody(body) + c.Fasthttp.Request.Header.SetContentType(MIMEApplicationForm) + c.Fasthttp.Request.Header.SetContentLength(len(body)) + d := new(Demo) + + b.ReportAllocs() + b.ResetTimer() + + for n := 0; n < b.N; n++ { + _ = c.BodyParser(d) + } + utils.AssertEqual(b, nil, c.BodyParser(d)) + utils.AssertEqual(b, "john", d.Name) +} + +// go test -v -run=^$ -bench=Benchmark_Ctx_BodyParser_MultipartForm -benchmem -count=4 +func Benchmark_Ctx_BodyParser_MultipartForm(b *testing.B) { + app := New() + c := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(c) + type Demo struct { + Name string `form:"name"` + } + + body := []byte("--b\r\nContent-Disposition: form-data; name=\"name\"\r\n\r\njohn\r\n--b--") + c.Fasthttp.Request.SetBody(body) + c.Fasthttp.Request.Header.SetContentType(MIMEMultipartForm + `;boundary="b"`) + c.Fasthttp.Request.Header.SetContentLength(len(body)) + d := new(Demo) + + b.ReportAllocs() + b.ResetTimer() + + for n := 0; n < b.N; n++ { + _ = c.BodyParser(d) + } + utils.AssertEqual(b, nil, c.BodyParser(d)) + utils.AssertEqual(b, "john", d.Name) +} // go test -run Test_Ctx_Context func Test_Ctx_Context(t *testing.T) { @@ -1320,6 +1435,25 @@ func Test_Ctx_Render_Engine(t *testing.T) { utils.AssertEqual(t, "

Hello, World!

", string(ctx.Fasthttp.Response.Body())) } +func Benchmark_Ctx_Render_Engine(b *testing.B) { + engine := &testTemplateEngine{} + err := engine.Load() + utils.AssertEqual(b, nil, err) + app := New() + app.Settings.Views = engine + ctx := app.AcquireCtx(&fasthttp.RequestCtx{}) + defer app.ReleaseCtx(ctx) + b.ReportAllocs() + b.ResetTimer() + for n := 0; n < b.N; n++ { + err = ctx.Render("index.tmpl", Map{ + "Title": "Hello, World!", + }) + } + utils.AssertEqual(b, nil, err) + utils.AssertEqual(b, "

Hello, World!

", string(ctx.Fasthttp.Response.Body())) +} + // go test -run Test_Ctx_Render_Go_Template func Test_Ctx_Render_Go_Template(t *testing.T) { t.Parallel() diff --git a/prefork.go b/prefork.go index 03bd417a..49da3f7b 100644 --- a/prefork.go +++ b/prefork.go @@ -19,7 +19,10 @@ const ( envPreforkChildVal = "1" ) -var testPreforkMaster = false +var ( + testPreforkMaster = false + dummyChildCmd = "date" +) // IsChild determines if the current process is a result of Prefork func (app *App) IsChild() bool { @@ -86,7 +89,7 @@ func (app *App) prefork(addr string, tlsconfig ...*tls.Config) (err error) { // When test prefork master, // just start the child process // a cmd on all os is best - cmd = exec.Command("date") + cmd = exec.Command(dummyChildCmd) } cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr @@ -116,9 +119,5 @@ func (app *App) prefork(addr string, tlsconfig ...*tls.Config) (err error) { } // return error if child crashes - for sig := range channel { - return sig.err - } - - return + return (<-channel).err } diff --git a/prefork_test.go b/prefork_test.go index d06117a2..f640c220 100644 --- a/prefork_test.go +++ b/prefork_test.go @@ -12,11 +12,12 @@ func Test_App_Prefork_Child_Process(t *testing.T) { utils.AssertEqual(t, nil, os.Setenv(envPreforkChildKey, envPreforkChildVal)) defer os.Setenv(envPreforkChildKey, "") - app := New(&Settings{ - DisableStartupMessage: true, - }) + app := New() app.init() + err := app.prefork("invalid") + utils.AssertEqual(t, false, err == nil) + go func() { time.Sleep(1000 * time.Millisecond) utils.AssertEqual(t, nil, app.Shutdown()) @@ -28,9 +29,7 @@ func Test_App_Prefork_Child_Process(t *testing.T) { func Test_App_Prefork_Main_Process(t *testing.T) { testPreforkMaster = true - app := New(&Settings{ - DisableStartupMessage: true, - }) + app := New() app.init() go func() { @@ -39,4 +38,9 @@ func Test_App_Prefork_Main_Process(t *testing.T) { }() utils.AssertEqual(t, nil, app.prefork("127.0.0.1:")) + + dummyChildCmd = "invalid" + + err := app.prefork("127.0.0.1:") + utils.AssertEqual(t, false, err == nil) } diff --git a/reuseport.go b/reuseport.go new file mode 100644 index 00000000..5afded6a --- /dev/null +++ b/reuseport.go @@ -0,0 +1,41 @@ +// +build !windows + +package fiber + +import ( + "net" + + tcplisten "github.com/valyala/tcplisten" +) + +// reuseport provides TCP net.Listener with SO_REUSEPORT support. +// +// SO_REUSEPORT allows linear scaling server performance on multi-CPU servers. +// See https://www.nginx.com/blog/socket-sharding-nginx-release-1-9-1/ for more details :) +// +// The package is based on https://github.com/kavu/go_reuseport . + +// Listen returns TCP listener with SO_REUSEPORT option set. +// +// The returned listener tries enabling the following TCP options, which usually +// have positive impact on performance: +// +// - TCP_DEFER_ACCEPT. This option expects that the server reads from accepted +// connections before writing to them. +// +// - TCP_FASTOPEN. See https://lwn.net/Articles/508865/ for details. +// +// Use https://github.com/valyala/tcplisten if you want customizing +// these options. +// +// Only tcp4 and tcp6 networks are supported. +// +// ErrNoReusePort error is returned if the system doesn't support SO_REUSEPORT. +func reuseport(network, addr string) (net.Listener, error) { + cfg := &tcplisten.Config{ + ReusePort: true, + DeferAccept: true, + FastOpen: true, + } + return cfg.NewListener(network, addr) +}