简介
go使用连接池进行http请求,一般都能请求成功,但偶然会出现请求失败返回EOF错误的情况;类似java的org.apache.http.NoHttpResponseException
分析
客户端通过keep alive机制保障性能,简单理解就是复用tcp五元会话,用于进行多次http请求;但如果服务端的空闲保活时间是10s,在第一次请求完的10s进行了第二次请求,此时客户端认为连接仍然有效继续发起请求,但服务端发出了FIN报文不再对此连接进行响应,从而导致客户端请求失败并出现EOF错误。
偶发就是因为两个时间要恰好碰到一起才可能触发这个问题
- 服务器发送了FIN报文,但是客户端还没有收到,但是客户端已经发送了请求数据包
- 如果在服务器超时前发起了请求,那连接此时还可用,正常
- 如果在服务器超时后发起了请求,那连接已经完成FIN关闭流程,请求会触发新的会话,正常
解决方式:
- 在出现EOF的时候,进行重试,此时会触发新的五元组连接进行请求(推荐)
- 设置客户端的空闲保活时间小于服务端的空闲保活时间
- IdleConnTimeout 此时客户端会在超时时主动向服务端发送RST进行连接重置
代码
package main
import (
"bytes"
"crypto/tls"
"encoding/json"
"errors"
"fmt"
"io"
"io/ioutil"
"net/http"
"time"
)
func main() {
// 创建自定义的 Transport,设置连接池参数
tr := &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true, // 忽略 TLS 证书验证
},
MaxIdleConns: 1, // 限制最大空闲连接数为1
MaxIdleConnsPerHost: 1, // 限制每个host最大空闲连接数为1
IdleConnTimeout: 20 * time.Second, // 本地空闲连接超时设置为20s
DisableKeepAlives: false, // 启用 keep-alive
MaxConnsPerHost: 1, // 限制每个host的最大连接数为1,强制复用连接
ForceAttemptHTTP2: false, // 禁用 HTTP/2
}
// 创建 HTTP 客户端
client := &http.Client{
Transport: tr,
Timeout: 5 * time.Second, // 设置请求超时时间
}
// 准备请求参数
url := "https://192.168.24.70:2018/api/zguard/sysmng/syscfg/basecfg/sysname/651d5b9f-225b-4c8c-9f06-80bfad3fa977"
cookie := "session-id=f416b188c91bc72a06853b362d5cb7b3a6b68a43"
// 准备请求体数据
requestBody := map[string]string{
"sys_name": "N-GUARD",
}
// 将 map 转换为 JSON
jsonBody, err := json.Marshal(requestBody)
if err != nil {
fmt.Printf("JSON 编码失败: %v\n", err)
}
// 循环发送请求,模拟使用已关闭的连接
for i := 0; i < 5; i++ { // 只测试两次请求即可
// 每次请求都创建新的 bytes.Buffer,确保 Body 可以重复读取
bodyReader := bytes.NewBuffer(jsonBody)
req, err := http.NewRequest("PUT", url, bodyReader)
if err != nil {
fmt.Printf("创建请求失败: %v\n", err)
continue
}
// 设置 Content-Length
req.ContentLength = int64(len(jsonBody))
// 设置请求头
req.Header.Set("Cookie", cookie)
req.Header.Set("Content-Type", "application/json")
fmt.Printf("发送第 %d 个请求...\n", i+1)
// 发送请求
resp, err := client.Do(req)
if err != nil {
fmt.Printf("请求失败: %v\n", err)
if errors.Is(err, io.EOF) {
fmt.Printf("连接不再可用: 重试:新的五元重新发起连接\n")
bodyReader := bytes.NewBuffer(jsonBody)
reqretry, err := http.NewRequest("PUT", url, bodyReader)
if err != nil {
fmt.Printf("创建请求失败: %v\n", err)
continue
}
// 设置 Content-Length
reqretry.ContentLength = int64(len(jsonBody))
// 设置请求头
reqretry.Header.Set("Cookie", cookie)
reqretry.Header.Set("Content-Type", "application/json")
resp, err = client.Do(reqretry)
if err != nil {
fmt.Printf("err:\n", err)
continue
}
} else {
continue
}
}
// 读取响应
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
fmt.Printf("读取响应失败: %v\n", err)
}
resp.Body.Close()
fmt.Printf("请求 %d - 状态码: %d, 响应: %s\n", i+1, resp.StatusCode, string(body))
fmt.Println("等待10秒后发送第二个请求...")
time.Sleep(10 * time.Second) // 等待10秒,此时服务端已经关闭连接(10s)
time.Sleep(500 * time.Millisecond)
}
}
运行
[xiaofeng@localhost httpkeepalive]$ go run main.go
发送第 1 个请求...
请求 1 - 状态码: 200, 响应: {"code":0,"result":"0","message":"成功"}
等待10秒后发送第二个请求...
发送第 2 个请求...
请求 2 - 状态码: 200, 响应: {"code":0,"result":"0","message":"成功"}
等待10秒后发送第二个请求...
发送第 3 个请求...
请求失败: Put "https://192.168.24.70:2018/api/zguard/sysmng/syscfg/basecfg/sysname/651d5b9f-225b-4c8c-9f06-80bfad3fa977": EOF
连接不再可用: 重试:新的五元重新发起连接
请求 3 - 状态码: 200, 响应: {"code":0,"result":"0","message":"成功"}
等待10秒后发送第二个请求...
发送第 4 个请求...
请求失败: Put "https://192.168.24.70:2018/api/zguard/sysmng/syscfg/basecfg/sysname/651d5b9f-225b-4c8c-9f06-80bfad3fa977": EOF
连接不再可用: 重试:新的五元重新发起连接
请求 4 - 状态码: 200, 响应: {"code":0,"result":"0","message":"成功"}
等待10秒后发送第二个请求...
发送第 5 个请求...
请求 5 - 状态码: 200, 响应: {"code":0,"result":"0","message":"成功"}
等待10秒后发送第二个请求...