This commit is contained in:
Gerasimos Maropoulos 2016-08-14 05:44:36 +03:00
parent 56bd511427
commit c6f5406c3b
7 changed files with 160 additions and 117 deletions

View File

@ -37,6 +37,12 @@ const (
contentType = "Content-Type"
// ContentLength represents the header["Content-Length"]
contentLength = "Content-Length"
// contentEncodingHeader represents the header["Content-Encoding"]
contentEncodingHeader = "Content-Encoding"
// varyHeader represents the header "Vary"
varyHeader = "Vary"
// acceptEncodingHeader represents the header key & value "Accept-Encoding"
acceptEncodingHeader = "Accept-Encoding"
// ContentHTML is the string of text/html response headers
contentHTML = "text/html"
// ContentBinary header value for binary data.
@ -111,36 +117,6 @@ func (ctx *Context) GetRequestCtx() *fasthttp.RequestCtx {
return ctx.RequestCtx
}
// Reset resets the Context with a given domain.Response and domain.Request
// the context is ready-to-use after that, just like a new Context
// I use it for zero rellocation memory
func (ctx *Context) Reset(reqCtx *fasthttp.RequestCtx) {
ctx.Params = ctx.Params[0:0]
ctx.session = nil
ctx.middleware = nil
ctx.RequestCtx = reqCtx
}
// Clone use that method if you want to use the context inside a goroutine
func (ctx *Context) Clone() context.IContext {
var cloneContext = *ctx
cloneContext.pos = 0
//copy params
p := ctx.Params
cpP := make(PathParameters, len(p))
copy(cpP, p)
cloneContext.Params = cpP
//copy middleware
m := ctx.middleware
cpM := make(Middleware, len(m))
copy(cpM, m)
cloneContext.middleware = cpM
// we don't copy the sessionStore for more than one reasons...
return &cloneContext
}
// Do calls the first handler only, it's like Next with negative pos, used only on Router&MemoryRouter
func (ctx *Context) Do() {
ctx.pos = 0
@ -521,11 +497,27 @@ func (ctx *Context) Write(format string, a ...interface{}) {
ctx.RequestCtx.WriteString(fmt.Sprintf(format, a...))
}
func (ctx *Context) clientAllowsGzip() bool {
if h := ctx.RequestHeader(acceptEncodingHeader); h != "" {
for _, v := range strings.Split(h, ";") {
if strings.Contains(v, "gzip") { // we do Contains because sometimes browsers has the q=, we don't use it atm. || strings.Contains(v,"deflate"){
return true
}
}
}
return false
}
// Gzip accepts bytes, which are compressed to gzip format and sent to the client
func (ctx *Context) Gzip(b []byte, status int) {
_, err := fasthttp.WriteGzip(ctx.RequestCtx.Response.BodyWriter(), b)
if err == nil {
ctx.RequestCtx.Response.Header.Add("Content-Encoding", "gzip")
ctx.RequestCtx.Response.Header.Add(varyHeader, acceptEncodingHeader)
if ctx.clientAllowsGzip() {
_, err := fasthttp.WriteGzip(ctx.RequestCtx.Response.BodyWriter(), b)
if err == nil {
ctx.SetHeader(contentEncodingHeader, "gzip")
}
}
}
@ -630,8 +622,9 @@ func (ctx *Context) ServeContent(content io.ReadSeeker, filename string, modtime
ctx.RequestCtx.Response.Header.Set(lastModified, modtime.UTC().Format(config.TimeFormat))
ctx.RequestCtx.SetStatusCode(StatusOK)
var out io.Writer
if gzipCompression {
ctx.RequestCtx.Response.Header.Add("Content-Encoding", "gzip")
if gzipCompression && ctx.clientAllowsGzip() {
ctx.RequestCtx.Response.Header.Add(varyHeader, acceptEncodingHeader)
ctx.SetHeader(contentEncodingHeader, "gzip")
gzipWriter := gzipWriterPool.Get().(*gzip.Writer)
gzipWriter.Reset(ctx.RequestCtx.Response.BodyWriter())
defer gzipWriter.Close()
@ -651,6 +644,7 @@ func (ctx *Context) ServeContent(content io.ReadSeeker, filename string, modtime
// gzipCompression (bool)
//
// You can define your own "Content-Type" header also, after this function call
// This function doesn't implement resuming, use ctx.RequestCtx.SendFile/fasthttp.ServeFileUncompressed(ctx.RequestCtx,path)/ServeFile(ctx.RequestCtx,path) instead
func (ctx *Context) ServeFile(filename string, gzipCompression bool) error {
f, err := os.Open(filename)
if err != nil {
@ -673,6 +667,7 @@ func (ctx *Context) ServeFile(filename string, gzipCompression bool) error {
//
// You can define your own "Content-Type" header also, after this function call
// for example: ctx.Response.Header.Set("Content-Type","thecontent/type")
// This function doesn't implement resuming, use ctx.RequestCtx.SendFile instead
func (ctx *Context) SendFile(filename string, destinationName string) error {
err := ctx.ServeFile(filename, false)
if err != nil {

View File

@ -90,9 +90,7 @@ type (
Session() Session
SessionDestroy()
Log(string, ...interface{})
Reset(*fasthttp.RequestCtx)
GetRequestCtx() *fasthttp.RequestCtx
Clone() IContext
Do()
Next()
StopExecution()

View File

@ -21,30 +21,6 @@ import (
"github.com/valyala/fasthttp"
)
func TestContextReset(t *testing.T) {
var context Context
context.Params = PathParameters{PathParameter{Key: "testkey", Value: "testvalue"}}
context.Reset(nil)
if len(context.Params) > 0 {
t.Fatalf("Expecting to have %d params but got: %d", 0, len(context.Params))
}
}
func TestContextClone(t *testing.T) {
var context Context
context.Params = PathParameters{
PathParameter{Key: "testkey", Value: "testvalue"},
PathParameter{Key: "testkey2", Value: "testvalue2"},
}
c := context.Clone()
if v := c.Param("testkey"); v != context.Param("testkey") {
t.Fatalf("Expecting to have parameter value: %s but got: %s", context.Param("testkey"), v)
}
if v := c.Param("testkey2"); v != context.Param("testkey2") {
t.Fatalf("Expecting to have parameter value: %s but got: %s", context.Param("testkey2"), v)
}
}
func TestContextDoNextStop(t *testing.T) {
var context Context
ok := false

35
http.go
View File

@ -311,9 +311,8 @@ func (s *Server) Port() int {
return 443
}
return 80
} else {
return p
}
return p
}
if s.Config.AutoTLS {
return 443
@ -559,12 +558,10 @@ func (s *ServerList) CloseAll() (err error) {
// OpenAll starts all servers
// returns the first error happens to one of these servers
// if one server gets error it closes the previous servers and exits from this process
func (s *ServerList) OpenAll() error {
func (s *ServerList) OpenAll(reqHandler fasthttp.RequestHandler) error {
l := len(s.servers) - 1
h := s.mux.ServeRequest()
for i := range s.servers {
if err := s.servers[i].Open(h); err != nil {
if err := s.servers[i].Open(reqHandler); err != nil {
time.Sleep(2 * time.Second)
// for any case,
// we don't care about performance on initialization,
@ -1332,7 +1329,6 @@ type (
}
serveMux struct {
cPool *sync.Pool
tree *muxTree
lookups []*route
@ -1355,9 +1351,8 @@ type (
}
)
func newServeMux(contextPool sync.Pool, logger *logger.Logger) *serveMux {
func newServeMux(logger *logger.Logger) *serveMux {
mux := &serveMux{
cPool: &contextPool,
lookups: make([]*route, 0),
errorHandlers: make(map[int]Handler, 0),
hostname: config.DefaultServerHostname, // these are changing when the server is up
@ -1485,16 +1480,16 @@ func (mux *serveMux) lookup(routeName string) *route {
return nil
}
func (mux *serveMux) ServeRequest() fasthttp.RequestHandler {
func (mux *serveMux) Handler() HandlerFunc {
// initialize the router once
mux.build()
// optimize this once once, we could do that: context.RequestPath(mux.escapePath), but we lose some nanoseconds on if :)
getRequestPath := func(reqCtx *fasthttp.RequestCtx) string {
return utils.BytesToString(reqCtx.Path())
getRequestPath := func(ctx *Context) string {
return utils.BytesToString(ctx.Path()) //string(ctx.Path()[:]) // a little bit of memory allocation, old method used: BytesToString, If I see the benchmarks get low I will change it back to old, but this way is safer.
}
if !mux.escapePath {
getRequestPath = func(reqCtx *fasthttp.RequestCtx) string { return utils.BytesToString(reqCtx.RequestURI()) }
getRequestPath = func(ctx *Context) string { return utils.BytesToString(ctx.RequestCtx.RequestURI()) }
}
methodEqual := func(treeMethod []byte, reqMethod []byte) bool {
@ -1511,14 +1506,11 @@ func (mux *serveMux) ServeRequest() fasthttp.RequestHandler {
}
}
return func(reqCtx *fasthttp.RequestCtx) {
context := mux.cPool.Get().(*Context)
context.Reset(reqCtx)
routePath := getRequestPath(reqCtx)
return func(context *Context) {
routePath := getRequestPath(context)
tree := mux.tree
for tree != nil {
if !methodEqual(tree.method, reqCtx.Method()) {
if !methodEqual(tree.method, context.Method()) {
// we break any CORS OPTIONS method
// but for performance reasons if user wants http method OPTIONS to be served
// then must register it with .Options(...)
@ -1555,9 +1547,8 @@ func (mux *serveMux) ServeRequest() fasthttp.RequestHandler {
context.middleware = middleware
//ctx.Request.Header.SetUserAgentBytes(DefaultUserAgent)
context.Do()
mux.cPool.Put(context)
return
} else if mustRedirect && mux.correctPath && !bytes.Equal(reqCtx.Method(), methodConnectBytes) {
} else if mustRedirect && mux.correctPath && !bytes.Equal(context.Method(), methodConnectBytes) {
reqPath := routePath
pathLen := len(reqPath)
@ -1582,7 +1573,6 @@ func (mux *serveMux) ServeRequest() fasthttp.RequestHandler {
note := "<a href=\"" + utils.HTMLEscape(urlToRedirect) + "\">Moved Permanently</a>.\n"
context.Write(note)
}
mux.cPool.Put(context)
return
}
}
@ -1590,6 +1580,5 @@ func (mux *serveMux) ServeRequest() fasthttp.RequestHandler {
break
}
mux.fireError(StatusNotFound, context)
mux.cPool.Put(context)
}
}

141
iris.go
View File

@ -153,8 +153,6 @@ type (
UseTemplate(TemplateEngine) *TemplateEngineLocation
UseGlobal(...Handler)
UseGlobalFunc(...HandlerFunc)
OnError(int, HandlerFunc)
EmitError(int, *Context)
Lookup(string) Route
Lookups() []Route
Path(string, ...interface{}) string
@ -169,6 +167,7 @@ type (
// Implements the FrameworkAPI
Framework struct {
*muxAPI
contextPool sync.Pool
Config *config.Iris
gzipWriterPool sync.Pool // used for several methods, usually inside context
sessions *sessionsManager
@ -225,7 +224,7 @@ func New(cfg ...config.Iris) *Framework {
// set the websocket server
s.Websocket = NewWebsocketServer(s.Config.Websocket)
// set the servemux, which will provide us the public API also, with its context pool
mux := newServeMux(sync.Pool{New: func() interface{} { return &Context{framework: s} }}, s.Logger)
mux := newServeMux(s.Logger)
mux.onLookup = s.Plugins.DoPreLookup
// set the public router API (and party)
s.muxAPI = &muxAPI{mux: mux, relativePath: "/"}
@ -286,6 +285,29 @@ func (s *Framework) initialize() {
}
}
func (s *Framework) acquireCtx(reqCtx *fasthttp.RequestCtx) *Context {
v := s.contextPool.Get()
var ctx *Context
if v == nil {
ctx = &Context{
RequestCtx: reqCtx,
framework: s,
}
} else {
ctx = v.(*Context)
ctx.Params = ctx.Params[0:0]
ctx.RequestCtx = reqCtx
ctx.middleware = nil
ctx.session = nil
}
return ctx
}
func (s *Framework) releaseCtx(ctx *Context) {
s.contextPool.Put(ctx)
}
// Go starts the iris station, listens to all registered servers, and prepare only if Virtual
func Go() error {
return Default.Go()
@ -295,8 +317,14 @@ func Go() error {
func (s *Framework) Go() error {
s.initialize()
s.Plugins.DoPreListen(s)
if firstErr := s.Servers.OpenAll(); firstErr != nil {
// build the fasthttp handler to bind it to the servers
h := s.mux.Handler()
reqHandler := func(reqCtx *fasthttp.RequestCtx) {
ctx := s.acquireCtx(reqCtx)
h(ctx)
s.releaseCtx(ctx)
}
if firstErr := s.Servers.OpenAll(reqHandler); firstErr != nil {
return firstErr
}
@ -665,30 +693,6 @@ func (s *Framework) UseGlobalFunc(handlersFn ...HandlerFunc) {
s.UseGlobal(convertToHandlers(handlersFn)...)
}
// OnError registers a custom http error handler
func OnError(statusCode int, handlerFn HandlerFunc) {
Default.OnError(statusCode, handlerFn)
}
// EmitError fires a custom http error handler to the client
//
// if no custom error defined with this statuscode, then iris creates one, and once at runtime
func EmitError(statusCode int, ctx *Context) {
Default.EmitError(statusCode, ctx)
}
// OnError registers a custom http error handler
func (s *Framework) OnError(statusCode int, handlerFn HandlerFunc) {
s.mux.registerError(statusCode, handlerFn)
}
// EmitError fires a custom http error handler to the client
//
// if no custom error defined with this statuscode, then iris creates one, and once at runtime
func (s *Framework) EmitError(statusCode int, ctx *Context) {
s.mux.fireError(statusCode, ctx)
}
// Lookup returns a registed route by its name
func Lookup(routeName string) Route {
return Default.Lookup(routeName)
@ -1051,6 +1055,10 @@ type (
// templates
Layout(string) MuxAPI // returns itself
// errors
OnError(int, HandlerFunc)
EmitError(int, *Context)
}
muxAPI struct {
@ -1839,3 +1847,78 @@ func (api *muxAPI) Layout(tmplLayoutFile string) MuxAPI {
})
return api
}
// OnError registers a custom http error handler
func OnError(statusCode int, handlerFn HandlerFunc) {
Default.OnError(statusCode, handlerFn)
}
// EmitError fires a custom http error handler to the client
//
// if no custom error defined with this statuscode, then iris creates one, and once at runtime
func EmitError(statusCode int, ctx *Context) {
Default.EmitError(statusCode, ctx)
}
// OnError registers a custom http error handler
func (api *muxAPI) OnError(statusCode int, handlerFn HandlerFunc) {
path := strings.Replace(api.relativePath, "//", "/", -1) // fix the path if double //
staticPath := path
// find the static path (on Party the path should be ALWAYS a static path, as we all know,
// but do this check for any case)
dynamicPathIdx := strings.IndexByte(path, parameterStartByte) // check for /mypath/:param
if dynamicPathIdx == -1 {
dynamicPathIdx = strings.IndexByte(path, matchEverythingByte) // check for /mypath/*param
}
if dynamicPathIdx > 1 { //yes after / and one character more ( /*param or /:param will break the root path, and this is not allowed even on error handlers).
staticPath = api.relativePath[0:dynamicPathIdx]
}
if staticPath == "/" {
api.mux.registerError(statusCode, handlerFn) // register the user-specific error message, as the global error handler, for now.
return
}
//after this, we have more than one error handler for one status code, and that's dangerous some times, but use it for non-globals error catching by your own risk
// NOTES:
// subdomains error will not work if same path of a non-subdomain (maybe a TODO for later)
// errors for parties should be registered from the biggest path length to the smaller.
// get the previous
prevErrHandler := api.mux.errorHandlers[statusCode]
if prevErrHandler == nil {
/*
make a new one with the standard error message,
this will be used as the last handler if no other error handler catches the error (by prefix(?))
*/
prevErrHandler = HandlerFunc(func(ctx *Context) {
ctx.ResetBody()
ctx.SetStatusCode(statusCode)
ctx.SetBodyString(statusText[statusCode])
})
}
func(statusCode int, staticPath string, prevErrHandler Handler, newHandler Handler) { // to separate the logic
errHandler := HandlerFunc(func(ctx *Context) {
if strings.HasPrefix(ctx.PathString(), staticPath) { // yes the user should use OnError from longest to lower static path's length in order this to work, so we can find another way, like a builder on the end.
newHandler.Serve(ctx)
return
}
// serve with the user-specific global ("/") pure iris.OnError receiver Handler or the standar handler if OnError called only from inside a no-relative Party.
prevErrHandler.Serve(ctx)
})
api.mux.registerError(statusCode, errHandler)
}(statusCode, staticPath, prevErrHandler, handlerFn)
}
// EmitError fires a custom http error handler to the client
//
// if no custom error defined with this statuscode, then iris creates one, and once at runtime
func (api *muxAPI) EmitError(statusCode int, ctx *Context) {
api.mux.fireError(statusCode, ctx)
}

View File

@ -151,12 +151,13 @@ func (r *responseEngineMap) render(ctx *Context, obj interface{}, options ...map
}
ctx.SetContentType(ctype)
if gzipEnabled {
if gzipEnabled && ctx.clientAllowsGzip() {
_, err := fasthttp.WriteGzip(ctx.RequestCtx.Response.BodyWriter(), finalResult)
if err != nil {
return err
}
ctx.Response.Header.Add("Content-Encoding", "gzip")
ctx.RequestCtx.Response.Header.Add(varyHeader, acceptEncodingHeader)
ctx.SetHeader(contentEncodingHeader, "gzip")
} else {
ctx.Response.SetBody(finalResult)
}

View File

@ -201,8 +201,9 @@ func (t *templateEngineWrapper) execute(ctx *Context, filename string, binding i
ctx.SetContentType(contentHTML + "; charset=" + charset)
var out io.Writer
if gzipEnabled {
ctx.Response.Header.Add("Content-Encoding", "gzip")
if gzipEnabled && ctx.clientAllowsGzip() {
ctx.RequestCtx.Response.Header.Add(varyHeader, acceptEncodingHeader)
ctx.SetHeader(contentEncodingHeader, "gzip")
gzipWriter := ctx.framework.AcquireGzip(ctx.Response.BodyWriter())
defer ctx.framework.ReleaseGzip(gzipWriter)