Back to Projects

Raw-HTTP - HTTP/1.1 Server Built from TCP Sockets

Go

Raw-HTTP - HTTP/1.1 Server Built from TCP Sockets

Raw-HTTP is a lightweight HTTP/1.1 server built entirely from raw TCP sockets in Go. No frameworks, no libraries - just socket programming and HTTP protocol implementation from scratch. Features path parameters, graceful shutdown, buffer pooling, and production-grade performance: 11,000+ RPS.

View on GitHub → | Live Demo →

Built With Raw-HTTP

I built a URL shortener service using this server to prove it works in real applications:

Snip - URL Shortener →

The URL shortener demonstrates path parameters, static file serving, form handling, and production deployment - all running on raw TCP sockets.

Why I Built This

Most developers use frameworks like Express or Flask without understanding what happens underneath. I built this to learn HTTP at the protocol level - parsing requests from TCP sockets, managing connections, implementing TLS encryption, buffer pooling, graceful shutdown, and optimizing performance from first principles.

Core Features

  • Raw TCP Handling: Parses HTTP requests directly from socket connections
  • Path Parameters: Dynamic routing with /users/:id syntax
  • Query String Parsing: Automatic extraction of URL parameters
  • Static File Serving: Proper MIME type detection and delivery
  • Keep-Alive Support: HTTP/1.1 connection reuse for 2x performance
  • Buffer Pooling: sync.Pool optimization reduces GC pressure
  • Form & JSON Parsing: Handles both URL-encoded and JSON bodies
  • Graceful Shutdown: Handles SIGINT/SIGTERM with connection draining
  • Panic Recovery: Handler crashes don't take down the server
  • HTTPS/TLS Support: Optional encrypted connections with certificate-based security
  • Response Helpers: Built-in helpers for 201, 204, 400, 401, 403, 405, 429, 500, 502, 503
  • Custom 404 Pages: Serve custom HTML for not found routes
  • Security Basics: Path traversal protection and request limits

Installation

As a dependency:

go get github.com/codetesla51/raw-http@v1.0.1

Build from source:

git clone https://github.com/codetesla51/raw-http.git
cd raw-http
go build -o server main.go
./server

Quick Start

package main

import (
    "log"
    "github.com/codetesla51/raw-http/server"
)

func main() {
    srv := server.NewServer(":8080")
    
    srv.Register("GET", "/ping", func(req *server.Request) ([]byte, string) {
        return server.CreateResponseBytes("200", "text/plain", "OK", []byte("pong"))
    })
    
    srv.Register("GET", "/users/:id", func(req *server.Request) ([]byte, string) {
        userID := req.PathParams["id"]
        return server.CreateResponseBytes("200", "text/plain", "OK", 
            []byte("User: "+userID))
    })
    
    srv.Register("POST", "/api/data", func(req *server.Request) ([]byte, string) {
        name := req.Body["name"]
        if name == "" {
            return server.Serve400("name is required")
        }
        return server.Serve201("created: " + name)
    })
    
    if err := srv.ListenAndServe(); err != nil {
        log.Fatal(err)
    }
}

Usage Examples

curl http://localhost:8080/ping
# Returns: pong

curl http://localhost:8080/users/42
# Returns: User: 42

curl -X POST -d "name=john" http://localhost:8080/api/data
# Returns: created: john

curl https://localhost:8443/ping -k
# HTTPS request (use -k to skip certificate verification)

Advanced Features

Path Parameters

srv.Register("GET", "/users/:id", func(req *server.Request) ([]byte, string) {
    userID := req.PathParams["id"]  // "123" from /users/123
    return server.CreateResponseBytes("200", "text/plain", "OK", []byte(userID))
})

srv.Register("GET", "/posts/:postId/comments/:commentId", 
    func(req *server.Request) ([]byte, string) {
        postID := req.PathParams["postId"]
        commentID := req.PathParams["commentId"]
        // ...
    })

Query Parameters

srv.Register("GET", "/search", func(req *server.Request) ([]byte, string) {
    q := req.Query["q"]           // /search?q=golang
    page := req.Query["page"]     // /search?q=golang&page=2
    // ...
})

Configuration

cfg := &server.Config{
    ReadTimeout:     60 * time.Second,
    WriteTimeout:    30 * time.Second,
    MaxBodySize:     50 * 1024 * 1024,  // 50MB
    EnableKeepAlive: true,
    EnableLogging:   true,
}

srv := server.NewServerWithConfig(":8080", cfg)
srv.Register("GET", "/ping", handler)
srv.ListenAndServe()

Response Helpers

srv.Register("POST", "/login", func(req *server.Request) ([]byte, string) {
    if req.Body["password"] == "" {
        return server.Serve400("password required")
    }
    if !authenticate(req.Body["user"], req.Body["password"]) {
        return server.Serve401("invalid credentials")
    }
    return server.Serve201("logged in")
})

Available helpers: Serve201, Serve204, Serve400, Serve401, Serve403, Serve405, Serve429, Serve500, Serve502, Serve503

HTTPS/TLS Support

srv := server.NewServer(":8080")
srv.EnableTLS(":8443", "server.crt", "server.key")
srv.Register("GET", "/ping", handler)
srv.ListenAndServe()  // Serves HTTP on 8080 and HTTPS on 8443

Self-signed certificates included for development. For production, use Let's Encrypt:

certbot certonly --standalone -d yourdomain.com
cp /etc/letsencrypt/live/yourdomain.com/fullchain.pem server.crt
cp /etc/letsencrypt/live/yourdomain.com/privkey.pem server.key

Performance Benchmarks

Benchmarks on 8-core system:

Scenario           Concurrency    Requests/sec    Latency
GET /ping          100            5,601           17.9ms
GET /ping          500            11,042          45.3ms
POST with body     100            5,773           17.3ms

Key insights: HTTP/1.1 keep-alive nearly doubles throughput (5k → 11k RPS). Buffer pooling reduces GC overhead. Optimal concurrency range: 100-500 connections.

Technical Implementation

  • Buffer Pooling: Three sync.Pool instances (chunk, request, response buffers) reduce GC pressure
  • TCP Connection Management: HTTP/1.1 keep-alive for connection reuse
  • Custom HTTP Parser: Zero-dependency request parsing with chunked reading
  • Graceful Shutdown: SIGINT/SIGTERM handling with 2-second grace period
  • Panic Recovery: Per-connection panic handling prevents server crashes
  • TLS/SSL Layer: Optional HTTPS encryption with goroutine-per-connection model
  • Path Traversal Protection: Blocks directory traversal attacks on static files
  • Goroutine-per-connection: Leverages Go's concurrency model
  • MIME Detection: Comprehensive content-type handling
  • Memory Efficient: Streams request bodies instead of full buffering

Architecture

┌─────────────────────────────────────────────────────────────┐
│                      Server Struct                          │
│  ┌─────────────┐  ┌─────────────┐  ┌─────────────────────┐  │
│  │   Router    │  │  TLS Config │  │  Graceful Shutdown  │  │
│  └──────┬──────┘  └─────────────┘  └─────────────────────┘  │
└─────────┼───────────────────────────────────────────────────┘
          │
          ▼
┌─────────────────────────────────────────────────────────────┐
│                    Connection Handler                        │
│  ┌─────────────┐  ┌─────────────┐  ┌─────────────────────┐  │
│  │ Buffer Pool │  │  Request    │  │  Keep-Alive Loop    │  │
│  │  (sync.Pool)│  │  Parser     │  │                     │  │
│  └─────────────┘  └─────────────┘  └─────────────────────┘  │
└─────────────────────────────────────────────────────────────┘

Buffer Pooling Details

Three sync.Pool instances reduce garbage collection pressure:

Pool                Buffer Size    Purpose
chunkBufferPool     4KB           Reading from TCP connection
requestBufferPool   8KB           Accumulating request headers
responseBufferPool  Dynamic       Building HTTP responses

Buffers larger than 16KB are discarded to prevent memory bloat.

Request Object

Handlers receive *server.Request:

Field         Type                   Description
Method        string                 HTTP method (GET, POST, PUT, DELETE)
Path          string                 Request path without query string
PathParams    map[string]string      URL parameters from route (:id)
Query         map[string]string      Query string parameters
Body          map[string]string      Parsed request body
Headers       map[string]string      HTTP headers
Browser       string                 Detected browser name

Project Structure

raw-http/
├── server/
│   ├── server.go          Core HTTP server logic
│   └── server_test.go     Test suite (21 tests)
├── pages/                 Static files and templates
│   ├── index.html
│   ├── 404.html          Custom 404 page
│   └── styles.css
├── server.crt             Self-signed certificate
├── server.key             Private key
└── main.go               Example application

Testing

go test ./server/... -v

21 tests cover HTTP parsing, route matching, path parameters, response formatting, error handling, and integration tests with real TCP connections.

What I Learned

  • How HTTP requests are structured at the protocol level
  • TCP connection lifecycle and keep-alive mechanics
  • Memory optimization with buffer pooling and sync.Pool
  • Graceful shutdown patterns in Go (signal handling, connection draining)
  • Panic recovery and error isolation in concurrent systems
  • TLS/SSL encryption and certificate management
  • Security considerations (DoS protection, path traversal, HTTPS)
  • Go networking primitives and goroutine patterns
  • The critical importance of connection reuse for performance
  • How low-level implementation details impact throughput

Limitations

This is a learning project, not production software:

  • Not production-tested (use for learning/small projects only)
  • Single process (no clustering support)
  • No middleware system (implement yourself if needed)
  • No observability (no built-in metrics/tracing)
  • ~5k connection ceiling (performance degrades at high concurrency)

For production applications, use Go's net/http package.

Why This Matters

Most web development happens at the framework level. Building from TCP sockets up reveals what frameworks abstract away - HTTP parsing, connection management, buffer pooling, graceful shutdown, TLS encryption, and the networking fundamentals that determine performance.

The journey from 250 RPS to 11,000+ RPS demonstrates how understanding these low-level details enables meaningful optimization.

View Source Code on GitHub →

Built by Uthman | @codetesla51