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)
+}