go进行http请求偶发EOF问题分析

 更新时间:2025年01月09日 08:49:44   作者:刀山羊  
go使用连接池进行http请求,一般都能请求成功,但偶然会出现请求失败返回EOF错误的情况,本文主要来带大家分析一下为什么会出现这样的问题并提供解决方法,需要的可以参考下

简介

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秒后发送第二个请求...

报文

到此这篇关于go进行http请求偶发EOF问题分析的文章就介绍到这了,更多相关go http请求偶发EOF内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • 基于go-cqhttp与Flask搭建定制机器人项目实战示例

    基于go-cqhttp与Flask搭建定制机器人项目实战示例

    这篇文章主要为大家介绍了基于go-cqhttp与Flask搭建定制机器人项目实战示例,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2022-11-11
  • golang时间及时间戳的获取转换

    golang时间及时间戳的获取转换

    本文主要介绍了golang时间及时间戳的获取转换,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2022-06-06
  • 详解Golang编程中的常量与变量

    详解Golang编程中的常量与变量

    这篇文章主要介绍了详解Golang编程中的常量与变量,是Go语言入门学习中的基础知识,需要的朋友可以参考下
    2015-10-10
  • Go语言命令行操作命令详细介绍

    Go语言命令行操作命令详细介绍

    这篇文章主要介绍了Go语言命令行操作命令详细介绍,本文重点介绍了go build、go clean、go fmt、go get等命令,需要的朋友可以参考下
    2014-10-10
  • Go语言中处理错误的技巧分享

    Go语言中处理错误的技巧分享

    编写 Go 语言程序时,有效地处理错误是至关重要的,Go 语言提供了一些强大的工具和模式来处理错误,本文将介绍这些方法,以便编写健壮的 Go 代码,更好地处理错误,需要的朋友可以参考下
    2023-09-09
  • Go语言并发范式之future模式详解

    Go语言并发范式之future模式详解

    编程中经常遇到在一个流程中需要调用多个子调用的情况,此时就可以使用Go并发编程中的future模式,下面小编就来和大家聊聊future模式的具体使用,需要的可以参考一下
    2023-06-06
  • Go语言中Once懒加载的艺术详析

    Go语言中Once懒加载的艺术详析

    Go语言以其简洁高效的并发模型赢得了无数开发者的青睐,下面这篇文章主要介绍了Go语言中Once懒加载的相关资料,文中通过代码介绍的非常详细,需要的朋友可以参考下
    2026-04-04
  • 关于Go 是传值还是传引用?

    关于Go 是传值还是传引用?

    这篇文章主要讨论Go语言 是传值还是传引用?文章先从Go 官方的定义展开,随后是传值和传引用得介绍到map 和 slice得区别,需要的小伙伴可以参考一下文章得具体内容
    2021-10-10
  • Go设计模式之备忘录模式图文详解

    Go设计模式之备忘录模式图文详解

    备忘录模式是一种行为设计模式, 允许在不暴露对象实现细节的情况下保存和恢复对象之前的状态,本文主要通过一些图片来给大家介绍一下Go的备忘录模式,需要的朋友可以参考下
    2023-08-08
  • GoLang channel使用介绍

    GoLang channel使用介绍

    Channel 和 goroutine 的结合是 Go 并发编程的大杀器。而 Channel 的实际应用也经常让人眼前一亮,通过与 select,cancel,timer 等结合,它能实现各种各样的功能。接下来,我们就要梳理一下 channel 的应用
    2022-10-10

最新评论