Go 从0到1实现 HTTP 服务器

从 TCP 底层到 HTTP 应用层,完整解析 Go 网络编程的每一层原理

一、整体架构:网络分层

应用层(Application Layer) HTTP / HTTP/2 / WebSocket — 我们的业务代码在这里 Go 标准库层(net/http + net/textproto) http.Request / http.Response / http.Server — 协议解析 & 路由 传输层(Transport Layer)— net 包 net.Listener / net.Conn — TCP 连接管理、读写缓冲 操作系统层(OS Layer)— Socket API syscall socket()/bind()/listen()/accept() — Go 通过 syscall 调用 OS 物理层 — 网卡(NIC)→ 二进制比特流

二、核心问题:TCP 需要自己写吗?

✅ 不需要(99% 场景)

  • net/http 标准库,3行启动服务器
  • TCP 由 net 包封装好了
  • 你只写 HTTP 处理逻辑
  • Go 官方推荐方式

✅ 可以自己写(学习/底层场景)

  • net.Listen("tcp", ...) 手写 TCP 服务器
  • 自己解析 HTTP 文本协议(读字节流、按 \r\n 分割)
  • 理解 TCP 粘包、半包处理
  • 造轮子 / 面试 / 学习用

下面我们从最底层开始,一步步自己实现一个 HTTP 服务器,先手写 TCP,再手写 HTTP 解析,最后用标准库对比。

三、Step 1:手写 TCP 服务器(从0开始)

1TCP 三层核心:socket → bind → listen → accept

操作系统提供 Socket API,Go 的 net 包封装了这些系统调用。我们先不用 http,只用 net 写一个 TCP 回显服务器。

// step1_tcp_server.go — 最朴素的 TCP 服务器
// 编译器:go run step1_tcp_server.go
package main

import (
    "bufio"
    "fmt"
    "net"
    "strings"
)

func main() {
    // 1. Listen:在内核中创建监听 socket,绑定端口 8080
    //    等价于 syscall: socket(AF_INET, SOCK_STREAM, 0) + bind(8080) + listen(backlog=128)
    listener, err := net.Listen("tcp", ":8080")
    if err != nil {
        panic(err)
    }
    defer listener.Close()
    fmt.Println("TCP 服务器启动,监听 :8080")

    for {
        // 2. Accept:阻塞等待客户端连接(内核维护已完成三次握手的队列)
        //    等价于 syscall: accept() → 返回新的 fd(文件描述符)
        conn, err := listener.Accept()
        if err != nil {
            fmt.Println("Accept error:", err)
            continue
        }
        fmt.Printf("客户端连接: %s\n", conn.RemoteAddr())

        // 3. 每个连接开一个 goroutine 处理(Go 的并发优势!)
        go handleConn(conn)
    }
}

func handleConn(conn net.Conn) {
    defer conn.Close()
    reader := bufio.NewReader(conn)

    for {
        // 4. Read:从内核接收缓冲区读数据(TCP 字节流)
        //   注意:TCP 是字节流,一次 Read 可能只读到部分数据(半包)
        line, err := reader.ReadString('\n')
        if err != nil {
            return
        }
        line = strings.TrimSpace(line)
        fmt.Printf("收到: %s\n", line)

        // 5. Write:写回响应(写入内核发送缓冲区,由 TCP 协议栈负责发送)
        conn.Write([]byte("回声: " + line + "\n"))

        if line == "quit" {
            return
        }
    }
}
关键知识点:
TCP 是字节流,不是消息流! ReadString('\n') 按换行分割是我们自己定义的协议。真实 HTTP 需要按 \r\n\r\n 分割 header,再按 Content-Length 读 body。这就是"粘包/半包"问题的来源。

四、Step 2:在 TCP 之上手写 HTTP 协议解析

2HTTP 协议本质:结构化文本 + TCP 字节流传输

HTTP/1.1 是文本协议,格式固定。我们只用 TCP 的 net.Conn 读取字节,然后按协议格式解析。

// step2_http_parser.go — 在 TCP 之上手写 HTTP 解析
package main

import (
    "bufio"
    "fmt"
    "io"
    "net"
    "strings"
)

// 我们自己定义的 HTTP 请求结构体(标准库叫 http.Request)
type MyRequest struct {
    Method  string
    Path    string
    Version string
    Headers map[string]string
    Body    string
}

type MyResponse struct {
    StatusCode int
    Headers    map[string]string
    Body       string
}

func main() {
    listener, _ := net.Listen("tcp", ":8080")
    defer listener.Close()
    fmt.Println("HTTP 服务器启动 :8080")

    for {
        conn, _ := listener.Accept()
        go handleHTTPConn(conn)
    }
}

func handleHTTPConn(conn net.Conn) {
    defer conn.Close()
    reader := bufio.NewReader(conn)

    // ===== 1. 解析请求行 =====
    requestLine, _ := reader.ReadString('\n')
    requestLine = strings.TrimRight(requestLine, "\r\n")
    parts := strings.Split(requestLine, " ")
    if len(parts) < 3 {
        return
    }

    req := &MyRequest{
        Method:  parts[0],
        Path:    parts[1],
        Version: parts[2],
        Headers: make(map[string]string),
    }
    fmt.Printf("→ %s %s %s\n", req.Method, req.Path, req.Version)

    // ===== 2. 解析请求头(遇到空行 \r\n 为止)=====
    var contentLength int = 0
    for {
        line, _ := reader.ReadString('\n')
        line = strings.TrimRight(line, "\r\n")
        if line == "" {
            break  // 空行 = header 结束
        }
        kv := strings.SplitN(line, ": ", 2)
        if len(kv) == 2 {
            req.Headers[strings.ToLower(kv[0])] = kv[1]
            if strings.ToLower(kv[0]) == "content-length" {
                fmt.Scanf(kv[1], "%d", &contentLength)
            }
        }
    }

    // ===== 3. 读取 Body =====
    if contentLength > 0 {
        bodyBuf := make([]byte, contentLength)
        io.ReadFull(reader, bodyBuf)
        req.Body = string(bodyBuf)
        fmt.Println("Body:", req.Body)
    }

    // ===== 4. 路由处理 =====
    var respBody string
    var statusCode int = 200
    switch req.Path {
    case "/":
        respBody = "<h1>Hello from 手写 HTTP 服务器!</h1>"
    case "/api":
        respBody = `{"msg":"hello","method":"` + req.Method + `"}`
    default:
        statusCode = 404
        respBody = "404 Not Found"
    }

    // ===== 5. 构造 HTTP 响应,写回 TCP 连接 =====
    writeHTTPResponse(conn, statusCode, respBody)
}

func writeHTTPResponse(conn net.Conn, statusCode int, body string) {
    statusText := "OK"
    if statusCode == 404 {
        statusText = "Not Found"
    }

    // 按 HTTP 协议格式拼接响应文本
    response := fmt.Sprintf(
        "HTTP/1.1 %d %s\r\nContent-Length: %d\r\nContent-Type: text/html\r\nConnection: close\r\n\r\n%s",
        statusCode, statusText, len(body), body,
    )
    conn.Write([]byte(response))
}
HTTP 请求报文结构(文本协议) GET /api/users?name=foo HTTP/1.1 ← 请求行(Method Path Version) Host: localhost:8080 ← 请求头(Key: Value) Content-Length: 15 ← 空行之前都是 header ␍␊ (\r\n) ← 空行,分隔 header 和 body

五、Step 3:用 Go 标准库 net/http(正式写法)

3生产环境:3 行启动,标准库帮你搞定一切

Go 的 net/http 标准库已经完整实现了 HTTP/1.1 和 HTTP/2 协议,包括:连接复用、keep-alive、chunked 传输、TLS、路由、中间件等。

// step3_standard_http.go — 生产级写法(推荐)
package main

import (
    "encoding/json"
    "fmt"
    "net/http"
)

func main() {
    // 1. 注册路由(内部维护了一张路由表 = map[pattern]HandlerFunc)
    http.HandleFunc("/", homeHandler)
    http.HandleFunc("/api", apiHandler)
    http.HandleFunc("/health", healthHandler)

    // 2. 启动服务器(内部会自动完成:Listen + Accept + goroutine per conn + HTTP 解析)
    fmt.Println("服务器启动 :8080")
    // ListenAndServe 内部:net.Listen("tcp", addr) → for { Accept() → go serve(conn) }
    // 每个连接由 go serve(conn) 处理,conn 中循环读取 HTTP 请求并调用对应 Handler
    err := http.ListenAndServe(":8080", nil)
    if err != nil {
        panic(err)
    }
}

func homeHandler(w http.ResponseWriter, r *http.Request) {
    // http.ResponseWriter 是对 conn 的封装,Write() 最终写入 TCP 发送缓冲区
    w.Header().Set("Content-Type", "text/html; charset=utf-8")
    w.Write([]byte("<h1>Hello from net/http!</h1>"))
}

func apiHandler(w http.ResponseWriter, r *http.Request) {
    // http.Request 是标准库帮我们解析完的 HTTP 请求结构体
    fmt.Printf("收到请求: %s %s\n", r.Method, r.URL.Path)

    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(map[string]interface{}{
        "method":  r.Method,
        "path":    r.URL.Path,
        "headers": r.Header,
    })
}

func healthHandler(w http.ResponseWriter, r *http.Request) {
    w.Write([]byte("OK"))
}

六、深层原理:net/http 内部到底做了什么?

net/http.ListenAndServe 内部调用链 main() 调用 http.ListenAndServe(":8080", nil) 入口函数 net.Listen("tcp", ":8080") → 创建 net.Listener 内部调用 syscall socket()→bind()→listen() server.Serve(listener) → for { listener.Accept() } 主协程:死循环接受新连接 go serve(conn) — 每个连接一个 goroutine 并发处理,goroutine 轻量级线程 listener.Accept() 继续阻塞等待 主协程不阻塞,继续接受新连接 conn.readRequest() — 解析 HTTP 请求行+header 按 \r\n 分割,填入 http.Request 结构体 调用注册的处理函数 handler.ServeHTTP()

七、关键难点:TCP 字节流与 HTTP 消息边界

问题原因HTTP 解决方案
粘包(多个消息挤在一起) TCP 发送缓冲区合并发送 \r\n 分隔 header,空行分隔 header/body
半包(一次读不完) TCP 分段/MSS 限制 循环 Read 直到读到完整 HTTP 消息
body 长度未知 动态内容 Content-Length 或 Transfer-Encoding: chunked
连接复用(keep-alive) 一个 TCP 连接多个 HTTP 请求 靠 Content-Length 确定每条消息边界
面试高频:TCP 有没有"消息"概念?
答:没有! TCP 是字节流(byte stream),只有发送缓冲区和接收缓冲区。HTTP 消息边界完全由应用层协议(\r\n 分隔符 + Content-Length)来界定。这就是 net/http 的 connReader 要做"缓冲读取"的原因。

八、总结:从 TCP 到 HTTP 完整映射

// 完整版:手写 HTTP 服务器(整合所有步骤)
// 文件:step4_complete.go
package main

import (
    "bufio"
    "fmt"
    "io"
    "net"
    "strings"
)

// ─────────────────────────────────────────────
// 1. TCP 层:Listen + Accept + goroutine 并发
// ─────────────────────────────────────────────
func main() {
    listener, err := net.Listen("tcp", ":8080")
    if err != nil { panic(err) }
    fmt.Println("🚀 手写 HTTP 服务器启动 :8080")

    for {
        conn, err := listener.Accept()
        if err != nil { continue }
        go serve(conn)  // 每个连接一个 goroutine
    }
}

// ─────────────────────────────────────────────
// 2. HTTP 层:在一个 TCP 连接上解析 HTTP 协议
//   支持 keep-alive(一个 TCP 连接处理多个请求)
// ─────────────────────────────────────────────
func serve(conn net.Conn) {
    defer conn.Close()
    reader := bufio.NewReader(conn)

    // keep-alive:循环处理多条请求(HTTP/1.1 默认持久连接)
    for {
        // --- 解析请求行 ---
        requestLine, err := reader.ReadString('\n')
        if err != nil { return }  // 客户端断开
        requestLine = strings.TrimRight(requestLine, "\r\n")
        if requestLine == "" { continue }

        parts := strings.Split(requestLine, " ")
        if len(parts) < 3 { return }

        method := parts[0]
        path   := parts[1]

        // --- 解析请求头 ---
        headers := make(map[string]string)
        var contentLength int = 0
        for {
            line, _ := reader.ReadString('\n')
            line = strings.TrimRight(line, "\r\n")
            if line == "" { break }
            kv := strings.SplitN(line, ": ", 2)
            if len(kv) == 2 {
                headers[strings.ToLower(kv[0])] = kv[1]
                if kv[0] == "Content-Length" {
                    fmt.Scanf(kv[1], "%d", &contentLength)
                }
            }
        }

        // --- 读取 Body(处理半包:循环读直到够数)---
        var body string
        if contentLength > 0 {
            buf := make([]byte, contentLength)
            _, err := io.ReadFull(reader, buf)
            if err == nil { body = string(buf) }
        }

        fmt.Printf("→ %s %s  Body=%d bytes\n", method, path, contentLength)

        // --- 路由 + 构造响应 ---
        var respBody string
        statusCode := 200
        switch path {
        case "/":
            respBody = "<html><body><h1>从 TCP 到 HTTP,完整实现!</h1></body></html>"
        case "/api/hello":
            respBody = `{"hello":"world","method":"` + method + `"}`
        default:
            statusCode = 404
            respBody = "404 page not found"
        }

        // --- 写回 HTTP 响应(\r\n 是协议要求,不能用 \n)---
        statusText := "OK"
        if statusCode == 404 { statusText = "Not Found" }

        response := fmt.Sprintf(
            "HTTP/1.1 %d %s\r\n"+
            "Content-Length: %d\r\n"+
            "Content-Type: text/html; charset=utf-8\r\n"+
            "Connection: keep-alive\r\n"+
            "\r\n"+
            "%s",
            statusCode, statusText, len(respBody), respBody,
        )
        conn.Write([]byte(response))
        // 循环继续 → 处理下一条 HTTP 请求(keep-alive)
    }
}

九、运行方式

# 运行手写 TCP 服务器
go run step1_tcp_server.go

# 运行手写 HTTP 服务器
go run step2_http_parser.go
go run step4_complete.go

# 测试(另一个终端)
curl http://localhost:8080/
curl http://localhost:8080/api/hello
curl -X POST -d '{"name":"test"}' http://localhost:8080/api/hello
学习路径建议:
1. 先运行 step1_tcp_server.go,用 telnet localhost 8080 体验 TCP 字节流
2. 再运行 step4_complete.go,用 curl 体验 HTTP 协议
3. 最后读 Go 源码 src/net/http/server.goserve() 函数,对比自己的实现
Go 从0到1实现 HTTP 服务器 — TCP 原理 · HTTP 协议解析 · net/http 源码分析