This commit is contained in:
Sky Johnson 2025-03-05 12:30:54 -06:00
parent 9295e5445e
commit 47a1b56619

108
server.go
View File

@ -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)
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),
},