首页 > 其他分享 >客户端禁用Keep-alive, 服务端开启Keep-alive,谁是主动断开方?

客户端禁用Keep-alive, 服务端开启Keep-alive,谁是主动断开方?

时间:2022-12-06 12:00:24浏览次数:52  
标签:http alive Keep Connection client close 连接 服务端 客户端

转自:https://www.cnblogs.com/JulianHuang/p/15870549.html

 

最近部署的web程序,在服务器上出现不少time_wait的连接状态,会占用tcp端口,费了几天时间排查。

之前我有结论:HTTP keep-alive 是在应用层对TCP连接的滑动续约复用,如果客户端、服务器稳定续约,就成了名副其实的长连接。

目前所有的HTTP网络库(不论是客户端、服务端)都默认开启了HTTP Keep-Alive,通过Request/Response的Connection标头来协商复用连接。

特定于连接的标头字段(例如 Connection)不得与 HTTP/2 一起使用

非常规做法导致的短连接

我手上有个项目,由于历史原因,客户端禁用了Keep-Alive,服务端默认开启了Keep-Alive,如此一来协商复用连接失败, 客户端每次请求会使用新的TCP连接, 也就是回退为短连接。

客户端强制禁用Keep-Alive

package main

import (
	"fmt"
	"io/ioutil"
	"log"
	"net/http"
	"time"
)

func main() {
	tr := http.Transport{
		DisableKeepAlives: true,
	}
	client := &http.Client{
		Timeout:   10 * time.Second,
		Transport: &tr,
	}
	for {
		requestWithClose(client)
		time.Sleep(time.Second * 1)
	}
}
> DisableKeepAlives: true, 会强制禁用客户端keep-alive
func requestWithClose(client *http.Client) {
	resp, err := client.Get("http://10.100.219.9:8081")
	if err != nil {
		fmt.Printf("error occurred while fetching page, error: %s", err.Error())
		return
	}
	defer resp.Body.Close()
	c, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		log.Fatalf("Couldn't parse response body. %+v", err)
	}

	fmt.Println(string(c))
}

web服务端默认开启Keep-Alive

package main

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

// 根据RemoteAddr 知道客户端使用的持久连接
func IndexHandler(w http.ResponseWriter, r *http.Request) {
	fmt.Println("receive a request from:", r.RemoteAddr, r.Header)
	w.Write([]byte("ok"))
}

func main() {
	fmt.Printf("Starting server at port 8081\n")
	// net/http 默认开启持久连接
	if err := http.ListenAndServe(":8081", http.HandlerFunc(IndexHandler)); err != nil {
		log.Fatal(err)
	}
}

从服务端的日志看,确实是短连接: remoteaddr的port不一样,客户端携带了 Connection:close header

receive a request from: 10.22.38.48:54722 map[Accept-Encoding:[gzip] Connection:[close] User-Agent:[Go-http-client/1.1]]
receive a request from: 10.22.38.48:54724 map[Accept-Encoding:[gzip] Connection:[close] User-Agent:[Go-http-client/1.1]]
receive a request from: 10.22.38.48:54726 map[Accept-Encoding:[gzip] Connection:[close] User-Agent:[Go-http-client/1.1]]
receive a request from: 10.22.38.48:54728 map[Accept-Encoding:[gzip] Connection:[close] User-Agent:[Go-http-client/1.1]]
receive a request from: 10.22.38.48:54731 map[Accept-Encoding:[gzip] Connection:[close] User-Agent:[Go-http-client/1.1]]
receive a request from: 10.22.38.48:54733 map[Accept-Encoding:[gzip] Connection:[close] User-Agent:[Go-http-client/1.1]]
receive a request from: 10.22.38.48:54734 map[Accept-Encoding:[gzip] Connection:[close] User-Agent:[Go-http-client/1.1]]
receive a request from: 10.22.38.48:54738 map[Accept-Encoding:[gzip] Connection:[close] User-Agent:[Go-http-client/1.1]]
receive a request from: 10.22.38.48:54740 map[Accept-Encoding:[gzip] Connection:[close] User-Agent:[Go-http-client/1.1]]
receive a request from: 10.22.38.48:54741 map[Accept-Encoding:[gzip] Connection:[close] User-Agent:[Go-http-client/1.1]]
receive a request from: 10.22.38.48:54743 map[Accept-Encoding:[gzip] Connection:[close] User-Agent:[Go-http-client/1.1]]
receive a request from: 10.22.38.48:54744 map[Accept-Encoding:[gzip] Connection:[close] User-Agent:[Go-http-client/1.1]]
receive a request from: 10.22.38.48:54746 map[Accept-Encoding:[gzip] Connection:[close] User-Agent:[Go-http-client/1.1]]

谁是主动断开方?

我想当然的以为客户端是主动断开方,被现实啪啪打脸。

某一天服务器上超过300的time_wait报警告诉我这tmd是服务器主动终断连接。

常规的TCP4次挥手, 主动断开方会进入time_wait状态,等待2MSL后释放占用的SOCKET

以下是从服务器上tcpdump抓取的tcp连接信息。

红框2,3部分明确提示是从 Server端发起TCP的FIN消息, 之后Client回应ACK确认收到Server的关闭通知; 之后Client再发FIN消息,告知现在可以关闭了, Server端最终发ACK确认收到,并进入Time_WAIT状态,等待2MSL的时间关闭Socket。

特意指出,红框1表示TCP双端同时关闭,此时会在Client,Server同时留下time_wait痕迹,发生概率较小。

没有源码说个串串

此种情况是服务端主动关闭,我们往回翻一翻golang httpServer的源码

  • http.ListenAndServe(":8081")
  • server.ListenAndServe()
  • srv.Serve(ln)
  • go c.serve(connCtx) 使用go协程来处理每个请求

服务器连接处理请求的简略源码如下:

func (c *conn) serve(ctx context.Context) {
	c.remoteAddr = c.rwc.RemoteAddr().String()
	ctx = context.WithValue(ctx, LocalAddrContextKey, c.rwc.LocalAddr())
	defer func() {
    if !c.hijacked() {
			c.close()
			c.setState(c.rwc, StateClosed, runHooks)
		}
	}()

  ......
	// HTTP/1.x from here on.

	ctx, cancelCtx := context.WithCancel(ctx)
	c.cancelCtx = cancelCtx
	defer cancelCtx()

	c.r = &connReader{conn: c}
	c.bufr = newBufioReader(c.r)
	c.bufw = newBufioWriterSize(checkConnErrorWriter{c}, 4<<10)

	for {
		w, err := c.readRequest(ctx)
		......
		serverHandler{c.server}.ServeHTTP(w, w.req)
		w.cancelCtx()
		if c.hijacked() {
			return
		}
		w.finishRequest()
		if !w.shouldReuseConnection() {
			if w.requestBodyLimitHit || w.closedRequestBodyEarly() {
				c.closeWriteAndWait()
			}
			return
		}
		c.setState(c.rwc, StateIdle, runHooks)
		c.curReq.Store((*response)(nil))

		if !w.conn.server.doKeepAlives() {
			// We're in shutdown mode. We might've replied
			// to the user without "Connection: close" and
			// they might think they can send another
			// request, but such is life with HTTP/1.1.
			return
		}

		if d := c.server.idleTimeout(); d != 0 {
			c.rwc.SetReadDeadline(time.Now().Add(d))
			if _, err := c.bufr.Peek(4); err != nil {
				return
			}
		}
		c.rwc.SetReadDeadline(time.Time{})
	}
}

我们需要关注

① for循环,表示尝试复用该conn,用于处理迎面而来的请求

② w.shouldReuseConnection() = false, 表明读取到ClientConnection:Close标头,设置closeAfterReply=true,跳出for循环,协程即将结束,结束之前执行defer函数,defer函数内close该连接

c.close()
......
// Close the connection.
func (c *conn) close() {
	c.finalFlush()
	c.rwc.Close()
}

③ 如果 w.shouldReuseConnection() = true,则将该连接状态置为idle, 并继续走for循环,处理后续请求。

自圆其说

最后我们来回顾一下: 为什么客户端禁用长连接, 会是服务端 主动关闭连接。

Q: 上面只是现象印证了这个结论, 怎么自圆其说明,这个现象的设计初衷呢?

A: http是请求-响应模型,发起方一直是客户端,connection:keep-alive的初衷是为客户端后续的请求重用连接
如果我们在某次请求--响应模型中,请求定义了connection:close, 那不再重用这个连接的时机就只有在服务端了,不能等到下次请求再关闭连接,因为可能根本就没下次请求,所以我们在请求-响应这个周期的末端关闭连接是合理的。

从这个思路看起来,我开篇想当然认为是 【客户端是主动断开方】很弱智啊。
按照这个请求-响应单向模型思路, 即使客户端开启了keep-alive, 如果与服务器协商失败(服务器强制关闭),服务器还是会主动关闭, 故主动关闭连接的一方只能是 服务端。

从上文源码看, 服务端有能力从客户端标头拿到Connection:Close, 也可以找到服务端自己的Keep-Alive策略,所以
如果客户端开启Keep-Alive, 服务端禁用Keep-Alive,在请求-响应单向模型,服务端依旧是主动断开方.

我的收获

    1. tcp 4次挥手的八股文
    2. 短连接在服务器上的效应,time_wait,占用可用的SOCKET, 根据实际业务看是否需要切换为长连接
    3. golang http keep-alive复用tcp连接的源码级分析
    4. tcpdump抓包的姿势
    5. 提出这个疑问的原因 还是自己对于请求-响应单向模型 认识不深刻,在这个单向模型下,连接不重用的时机只能是 服务端。

标签:http,alive,Keep,Connection,client,close,连接,服务端,客户端
From: https://www.cnblogs.com/fnlingnzb-learner/p/16954794.html

相关文章

  • 搭建ZooKeeper3.7.0集群(传统方式&Docker方式)
    简介:搭建ZooKeeper3.7.0集群(传统方式&Docker方式)正文一、传统方式安装1、下载安装包https://dlcdn.apache.org/zookeeper/zookeeper-3.7.0/apache-zookeeper-3.7.0-bi......
  • Java网络编程---基于TCP协议实现客户端服务端通信
    首先,对于TCP协议,我们要明确:TCP:传输控制协议TCP会尽自己所能,尽量将数据发送给对方;但并不能保证100%可以发送给对方TCP会在数据发送不到对方的情况下,会给应用......
  • #yyds干货盘点#前端keepalive缓存清理
    说到​​Vue​​​缓存,我们肯定首先选择官方提供的缓存方案​​keep-alive​​内置组件来实现。​​keep-alive​​组件提供给我们缓存组件的能力,可以完整的保存当前组......
  • 服务端架构演进史
    一、引子当我们入行成为一名后端程序员时,就很羡慕架构师这个岗位的人,视同神一样的存在。而要成为一名后端架构师,必会技能就是分布式架构。今天我们不讲各种组件怎么去实现......
  • 如何使用 Spring Cloud Zookeeper 进行服务发现和分布式配置
    该项目通过以下方式为SpringBoot应用程序提供Zookeeper集成自动配置并绑定到Spring环境和其他Spring编程模型习语。通过一些注释,您可以快速启用和配置常见模式......
  • 大数据--Hadoop环境部署(3)JDK和ZooKeeper环境配置
    Linux环境搭建:https://www.cnblogs.com/Studywith/p/16946297.html免密连接:https://www.cnblogs.com/Studywith/p/16946310.html在完成了Linux虚拟机的基础配置后,接下来......
  • Zookeeper学习-入门教程
    一、Zookeeper概念Zookeeper是ApacheHadoop项目下的一个子项目,是一个树形目录服务。Zookeeper翻译过来就是动物园管理员,他是用来管Hadoop(大象)、Hive(蜜蜂)、Pig(......
  • zookeeper集群搭建
    前言:本人通过macm1搭建,使用虚拟机,jdk等环境均为arm架构1,Zookeeper概述1.1简要  ZooKeeper是一个分布式的,开放源码的分布式应用程序协调服务,是Google的Chubby一个开......
  • [ABC248F] Keep Connect 题解
    [ABC248F]KeepConnectSolution目录[ABC248F]KeepConnectSolution更好的阅读体验戳此进入题面SolutionCodeUPD更好的阅读体验戳此进入题面给定$n,p$,存在如图......
  • 简述Zookeeper作注册中心
    Zookeeper的数据模型很简单,有一系列被称为ZNode的数据节点组成,与传统的磁盘文件系统不同的是,zk将全量数据存储在内存中,可谓是高性能,而且支持集群,可谓高可用,另外支持事件监听......