从 TCP 底层到 HTTP 应用层,完整解析 Go 网络编程的每一层原理
net/http 标准库,3行启动服务器net 包封装好了net.Listen("tcp", ...) 手写 TCP 服务器下面我们从最底层开始,一步步自己实现一个 HTTP 服务器,先手写 TCP,再手写 HTTP 解析,最后用标准库对比。
操作系统提供 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 } } }
ReadString('\n') 按换行分割是我们自己定义的协议。真实 HTTP 需要按 \r\n\r\n 分割 header,再按 Content-Length 读 body。这就是"粘包/半包"问题的来源。
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)) }
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")) }
| 问题 | 原因 | HTTP 解决方案 |
|---|---|---|
| 粘包(多个消息挤在一起) | TCP 发送缓冲区合并发送 | 用 \r\n 分隔 header,空行分隔 header/body |
| 半包(一次读不完) | TCP 分段/MSS 限制 | 循环 Read 直到读到完整 HTTP 消息 |
| body 长度未知 | 动态内容 | Content-Length 或 Transfer-Encoding: chunked |
| 连接复用(keep-alive) | 一个 TCP 连接多个 HTTP 请求 | 靠 Content-Length 确定每条消息边界 |
connReader 要做"缓冲读取"的原因。
// 完整版:手写 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
step1_tcp_server.go,用 telnet localhost 8080 体验 TCP 字节流step4_complete.go,用 curl 体验 HTTP 协议src/net/http/server.go 的 serve() 函数,对比自己的实现