You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Go中TCP客户端调用HTTP服务器无Connection:close无法读取响应体

问题说明

用Go编写的TCP客户端向本地HTTP服务器发起请求时,若请求头包含Connection: close,能读取到完整响应;但不包含该头时,仅能读取响应头部分,无法获取已返回的响应体。

代码示例

客户端代码

package main

import (
    "bufio"
    "bytes"
    "fmt"
    "log"
    "net"
)

func main() {

    con, err := net.Dial("tcp", "127.0.0.1:8080")
    if err != nil {
        log.Fatalln(err)
    }
    defer con.Close()

    serverReader := bufio.NewScanner(con)

    buf := bytes.Buffer{}
    buf.WriteString("GET / HTTP/1.1\n")
    buf.WriteString("Content-Type: text/hmtl\n")
    //buf.WriteString("Connection: close\n")
    buf.WriteString("User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.5060.134 Safari/537.36\n")
    buf.WriteString("Accept: */*\n")
    buf.WriteString("Host: 127.0.0.1\n")
    buf.WriteString("Accept-Encoding: gzip, deflate, br\n\n")

    if _, err = con.Write(buf.Bytes()); err != nil {
        log.Printf("failed to send the client request: %v\n", err)
    }

    //Waiting for the server response
    for serverReader.Scan() {
        fmt.Println(serverReader.Text())
    }

}

服务端代码

package main

import (
    "log"
    "net/http"
    "time"
)

func main() {
    var handler http.HandlerFunc = func(w http.ResponseWriter, r *http.Request) {
        log.Println("request received")

        if r.URL.String() == "/path" {
            w.WriteHeader(400)
            return
        }
        w.WriteHeader(200)
        w.Header().Set("Content-Type", "application/json")
        w.Write([]byte("hello"))
        w.Write([]byte("hi"))
        time.Sleep(time.Millisecond * 10)
        w.Write([]byte("how're you"))
        return
    }
    http.ListenAndServe(":8080", handler)
}

问题原因

HTTP/1.1默认使用持久连接(Connection: keep-alive),服务器发送完响应后不会主动关闭TCP连接,而是保持连接等待后续请求。

你的客户端用bufio.Scanner按行读取响应,但响应体是连续无换行的文本,Scanner会一直等待换行符或连接关闭才停止扫描。没有Connection: close时,连接保持打开状态,Scanner就会卡住,无法读取响应体;添加该头后,服务器响应完成后关闭连接,Scanner检测到连接关闭,就能返回所有读取内容(包括响应体)。

另外,服务端未设置Content-Length头,HTTP服务器会默认使用**分块传输编码(Transfer-Encoding: chunked)**发送响应体,这种情况下按行扫描的方式也无法正确解析分块内容。

解决方案

方案1:显式指定Connection: close

取消客户端代码中Connection: close的注释,让服务器响应后关闭连接,Scanner就能读取完整响应。但该方案会失去持久连接的优势。

方案2:正确解析HTTP响应(推荐)

不要用bufio.Scanner按行读取整个响应,改用bufio.Reader先读取响应头,再根据响应头信息读取响应体:

  • 若响应头包含Content-Length,读取对应长度的字节;
  • 若使用分块传输编码,按分块格式解析每一块内容。

修改后的客户端代码示例:

package main

import (
    "bufio"
    "bytes"
    "fmt"
    "log"
    "net"
    "strconv"
    "strings"
)

func main() {
    con, err := net.Dial("tcp", "127.0.0.1:8080")
    if err != nil {
        log.Fatalln(err)
    }
    defer con.Close()

    // 发送请求
    buf := bytes.Buffer{}
    buf.WriteString("GET / HTTP/1.1\n")
    buf.WriteString("Content-Type: text/html\n") // 修正原代码的hmtl拼写错误
    buf.WriteString("User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.5060.134 Safari/537.36\n")
    buf.WriteString("Accept: */*\n")
    buf.WriteString("Host: 127.0.0.1\n")
    buf.WriteString("Accept-Encoding: gzip, deflate, br\n\n")

    if _, err = con.Write(buf.Bytes()); err != nil {
        log.Printf("failed to send the client request: %v\n", err)
        return
    }

    reader := bufio.NewReader(con)

    // 读取响应头
    var contentLength int
    var isChunked bool
    for {
        line, err := reader.ReadString('\n')
        if err != nil {
            log.Printf("read header error: %v\n", err)
            return
        }
        line = strings.TrimSpace(line)
        fmt.Println(line) // 打印响应头

        // 空行表示响应头结束
        if line == "" {
            break
        }

        // 解析Content-Length
        if strings.HasPrefix(strings.ToLower(line), "content-length:") {
            parts := strings.SplitN(line, ":", 2)
            if len(parts) == 2 {
                cl, err := strconv.Atoi(strings.TrimSpace(parts[1]))
                if err == nil {
                    contentLength = cl
                }
            }
        }

        // 解析Transfer-Encoding
        if strings.HasPrefix(strings.ToLower(line), "transfer-encoding:") {
            parts := strings.SplitN(line, ":", 2)
            if len(parts) == 2 && strings.Contains(strings.ToLower(parts[1]), "chunked") {
                isChunked = true
            }
        }
    }

    // 读取响应体
    fmt.Println("\n---响应体---")
    if isChunked {
        // 处理分块传输
        for {
            // 读取块大小
            chunkSizeLine, err := reader.ReadString('\n')
            if err != nil {
                log.Printf("read chunk size error: %v\n", err)
                break
            }
            chunkSizeLine = strings.TrimSpace(chunkSizeLine)
            chunkSize, err := strconv.ParseInt(chunkSizeLine, 16, 64)
            if err != nil {
                log.Printf("parse chunk size error: %v\n", err)
                break
            }
            if chunkSize == 0 {
                // 读取最后的空行
                reader.ReadString('\n')
                break
            }

            // 读取块内容
            chunk := make([]byte, chunkSize)
            _, err = reader.Read(chunk)
            if err != nil {
                log.Printf("read chunk content error: %v\n", err)
                break
            }
            fmt.Print(string(chunk))

            // 读取块后的换行
            reader.ReadString('\n')
        }
    } else if contentLength > 0 {
        // 按Content-Length读取
        body := make([]byte, contentLength)
        _, err := reader.Read(body)
        if err != nil {
            log.Printf("read body error: %v\n", err)
            return
        }
        fmt.Print(string(body))
    }
    fmt.Println()
}

该代码会正确解析HTTP响应头,根据传输方式读取完整响应体,无论是否使用持久连接都能正常工作。同时修正了原客户端代码中Content-Type: text/hmtl的拼写错误。

内容的提问来源于stack exchange,提问作者Jeffy Mathew

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.23 09:24:42