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

