diff --git a/server.go b/server.go index fafe28e..abe072b 100644 --- a/server.go +++ b/server.go @@ -16,6 +16,49 @@ import ( router "git.sharkk.net/Go/Router" ) +// Pre-allocated response components +var ( + // Common strings for response headers + contentLengthHeader = []byte(HeaderContentLength + ": ") + crlf = []byte("\r\n") + colonSpace = []byte(": ") + + // Status line map for quick lookups + statusLines = map[int][]byte{ + 100: []byte("HTTP/1.1 100 Continue\r\n"), + 101: []byte("HTTP/1.1 101 Switching Protocols\r\n"), + 200: []byte("HTTP/1.1 200 OK\r\n"), + 201: []byte("HTTP/1.1 201 Created\r\n"), + 202: []byte("HTTP/1.1 202 Accepted\r\n"), + 204: []byte("HTTP/1.1 204 No Content\r\n"), + 206: []byte("HTTP/1.1 206 Partial Content\r\n"), + 300: []byte("HTTP/1.1 300 Multiple Choices\r\n"), + 301: []byte("HTTP/1.1 301 Moved Permanently\r\n"), + 302: []byte("HTTP/1.1 302 Found\r\n"), + 304: []byte("HTTP/1.1 304 Not Modified\r\n"), + 307: []byte("HTTP/1.1 307 Temporary Redirect\r\n"), + 308: []byte("HTTP/1.1 308 Permanent Redirect\r\n"), + 400: []byte("HTTP/1.1 400 Bad Request\r\n"), + 401: []byte("HTTP/1.1 401 Unauthorized\r\n"), + 403: []byte("HTTP/1.1 403 Forbidden\r\n"), + 404: []byte("HTTP/1.1 404 Not Found\r\n"), + 405: []byte("HTTP/1.1 405 Method Not Allowed\r\n"), + 406: []byte("HTTP/1.1 406 Not Acceptable\r\n"), + 409: []byte("HTTP/1.1 409 Conflict\r\n"), + 410: []byte("HTTP/1.1 410 Gone\r\n"), + 412: []byte("HTTP/1.1 412 Precondition Failed\r\n"), + 413: []byte("HTTP/1.1 413 Payload Too Large\r\n"), + 415: []byte("HTTP/1.1 415 Unsupported Media Type\r\n"), + 416: []byte("HTTP/1.1 416 Range Not Satisfiable\r\n"), + 429: []byte("HTTP/1.1 429 Too Many Requests\r\n"), + 500: []byte("HTTP/1.1 500 Internal Server Error\r\n"), + 501: []byte("HTTP/1.1 501 Not Implemented\r\n"), + 502: []byte("HTTP/1.1 502 Bad Gateway\r\n"), + 503: []byte("HTTP/1.1 503 Service Unavailable\r\n"), + 504: []byte("HTTP/1.1 504 Gateway Timeout\r\n"), + } +) + // Interface for an HTTP server. type Server interface { Get(path string, handler Handler) @@ -33,6 +76,7 @@ type Server interface { type server struct { handlers []Handler contextPool sync.Pool + bufferPool sync.Pool router *router.Router[Handler] errorHandler func(Context, error) } @@ -61,6 +105,7 @@ func NewServer() Server { } s.contextPool.New = func() any { return s.newContext() } + s.bufferPool.New = func() any { return bytes.NewBuffer(make([]byte, 0, 1024)) } return s } @@ -183,6 +228,7 @@ func (s *server) handleConnection(conn net.Conn) { url = message[space+1 : lastSpace] // Add headers until we meet an empty line + ctx.request.headers = ctx.request.headers[:0] // Reset headers without allocation for { message, err = ctx.reader.ReadString('\n') @@ -212,15 +258,20 @@ func (s *server) handleConnection(conn net.Conn) { // Read the body, if any if contentLength := ctx.request.Header(HeaderContentLength); contentLength != "" { length, _ := strconv.Atoi(contentLength) - ctx.request.body = make([]byte, length) + if cap(ctx.request.body) >= length { + ctx.request.body = ctx.request.body[:length] // Reuse existing slice if possible + } else { + ctx.request.body = make([]byte, length) + } ctx.reader.Read(ctx.request.body) + } else { + ctx.request.body = ctx.request.body[:0] // Empty the body slice without allocation } // Handle the request s.handleRequest(ctx, method, url, conn) - // Clean up the context - ctx.request.headers = ctx.request.headers[:0] + // Clean up the context - reset slices without allocation ctx.request.body = ctx.request.body[:0] ctx.response.headers = ctx.response.headers[:0] ctx.response.body = ctx.response.body[:0] @@ -230,7 +281,7 @@ func (s *server) handleConnection(conn net.Conn) { } } -// Handles the given request. +// Handles the given request with reduced allocations. func (s *server) handleRequest(ctx *context, method string, url string, writer io.Writer) { ctx.method = method ctx.scheme, ctx.host, ctx.path, ctx.query = parseURL(url) @@ -241,23 +292,44 @@ func (s *server) handleRequest(ctx *context, method string, url string, writer i s.errorHandler(ctx, err) } - tmp := bytes.Buffer{} - tmp.WriteString("HTTP/1.1 ") - tmp.WriteString(strconv.Itoa(int(ctx.status))) - tmp.WriteString("\r\n" + HeaderContentLength + ": ") - tmp.WriteString(strconv.Itoa(len(ctx.response.body))) - tmp.WriteString("\r\n") + // Get buffer from pool + buf := s.bufferPool.Get().(*bytes.Buffer) + buf.Reset() + defer s.bufferPool.Put(buf) - for _, header := range ctx.response.headers { - tmp.WriteString(header.Key) - tmp.WriteString(": ") - tmp.WriteString(header.Value) - tmp.WriteString("\r\n") + // Write status line using map lookup for efficiency + if statusLine, ok := statusLines[int(ctx.status)]; ok { + buf.Write(statusLine) + } else { + // For uncommon status codes, format the line dynamically + buf.WriteString("HTTP/1.1 ") + buf.WriteString(strconv.Itoa(int(ctx.status))) + buf.Write(crlf) } - tmp.WriteString("\r\n") - tmp.Write(ctx.response.body) - writer.Write(tmp.Bytes()) + // Write Content-Length header + buf.Write(contentLengthHeader) + buf.WriteString(strconv.Itoa(len(ctx.response.body))) + buf.Write(crlf) + + // Write all response headers + for _, header := range ctx.response.headers { + buf.WriteString(header.Key) + buf.Write(colonSpace) + buf.WriteString(header.Value) + buf.Write(crlf) + } + + // End headers + buf.Write(crlf) + + // Write headers + writer.Write(buf.Bytes()) + + // Write body directly to avoid another copy + if len(ctx.response.body) > 0 { + writer.Write(ctx.response.body) + } } // Allocates a new context with the default state. @@ -266,7 +338,7 @@ func (s *server) newContext() *context { server: s, request: request{ reader: bufio.NewReader(nil), - body: make([]byte, 0), + body: make([]byte, 0, 1024), headers: make([]Header, 0, 8), params: make([]router.Parameter, 0, 8), },