# Http 长连接 & 短连接详解

# 网络连接基础

## TCP/IP

TCP/IP，是几乎所有互联网通信的基石。HTTP、WebSocket 和 常见的RPC框架尽管功能各异，但都运行在应用层，并从根本上依赖传输层的 TCP（传输控制协议）来实现可靠的、面向连接的数据传输 。

TCP 的核心职责是确保数据包从发送端到接收端可靠、按序且无损地传输 。这包括序列号、确认、流量控制和拥塞控制等机制。在网络层，IP（互联网协议）负责网络路由和寻址，使数据能够跨越不同网络到达其目的地 。现代操作系统普遍内置并管理 TCP/IP 协议栈，从而为应用开发者抽象了大部分底层复杂性。

这种分层架构意味着高层协议继承了 TCP 的基本属性（可靠性、顺序性、面向连接），而无需重新实现这些功能，从而简化了应用开发。然而，这种抽象也意味着底层 TCP 层的性能特征和限制会直接影响所有应用层协议。例如，TCP 的连接建立开销（即“三次握手”）是每个新连接固有的固定成本，无论应用协议如何，都无法避免。TCP 的慢启动机制也会影响任何新连接的初始性能。

因此，对于不同场景，在应用层上会做出一些不同的trade off，由此产生了不同的解决方案。

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1749787026320/17dfedb9-eac6-43b5-94a0-e7f7cdec470f.png align="center")

(图片来源：[https://www.geeksforgeeks.org/tcp-3-way-handshake-process/](https://www.geeksforgeeks.org/tcp-3-way-handshake-process/))

# 长连接

## 介绍

HTTP 协议的长连接和短连接，由于构建在7层，所以实质上是 TCP 协议的长连接和短连接。

HTTP长连接的请求数量限定是最多连续发送100个请求，超过限定将关闭这条连接。

与短连接相反，长连接（也称为持久连接或 Keep-Alive 连接）在客户端和服务器之间建立一个 TCP 连接后，该连接会保持一段时间，用于发送一系列连续的请求，而不会在每次请求-响应完成后立即关闭 。

长连接也带来了一定的挑战。服务器需要维持更多的客户端连接，即使这些连接处于空闲状态，也会持续消耗服务器资源 。这可能导致服务器维护过多连接，尤其是在客户端不主动关闭连接的情况下 。在重负载下，长连接也可能面临拒绝服务（DoS）攻击的风险 。为应对这些问题，服务器通常会配置`Keep-Alive` 超时时间，例如 Apache 2.0 默认设置为 15 秒，Apache 2.2 默认设置为 5 秒 。

当连接在设定的时间内没有任何活动时，服务器会发送探测报文段以检测客户端状态，从而识别和关闭半开放或已崩溃客户端的连接，以防止恶意连接影响后端服务 。

长连接的实现需要在性能提升和服务器资源消耗之间进行权衡。虽然长连接可以显著提高性能，但也增加了服务器端连接管理的复杂性。例如，需要实现保活机制来检测半开放连接，并设置合理的超时策略来释放空闲资源。如果客户端不主动关闭连接，服务器可能会保持过多连接，这需要服务器采取额外的控制机制，如限制每个客户端的最大长连接数，以防止恶意客户端耗尽整个后端服务资源。

因此，选择长连接需要仔细考虑其带来的运维复杂性，并实施相应的连接管理策略。

# 短连接

## 介绍

在短连接模型中，客户端和服务器之间为每个独立的请求-响应周期建立一个新的 TCP 连接，并在完成通信后立即终止 。这个过程本质上涉及每次事务的 TCP“三次握手”用于连接建立和“四次挥手”用于连接拆除 。HTTP/1.0 默认采用这种行为。

短连接的优点在于其管理相对简单。从服务器的角度来看，所有现有连接都是活跃的，这可能消除了对复杂空闲连接管理机制的需求。此外，服务器在请求处理完成后立即释放资源，不会长时间占用空闲连接。

然而，短连接存在显著的缺点。最主要的问题是高昂的开销。对于频繁的客户端请求，重复建立和终止 TCP 连接会浪费大量时间（因为每次连接建立都需要消耗时间和资源）和带宽 。此外，TCP 连接的性能只有在被使用一段时间后（即成为“热连接”）才能得到改善，而短连接模式下，每次都创建新的“冷连接”会破坏 TCP 的这种性能提升能力，导致整体效率低下 。

# 案例

## server

```go
package main

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

func main() {
	// 创建一个自定义的 Transport，配置连接池参数
	// MaxIdleConnsPerHost: 每个主机的最大空闲连接数，用于长连接复用
	// IdleConnTimeout: 空闲连接的最大存活时间
	tr := &http.Transport{
		MaxIdleConns:        100,             // 所有主机的最大空闲连接数
		MaxIdleConnsPerHost: 10,              // 每个主机的最大空闲连接数
		IdleConnTimeout:     90 * time.Second, // 空闲连接的超时时间
		DisableKeepAlives:   false,           // 默认启用 Keep-Alive，这里显式设置为 false 确保启用
	}

	// 创建一个使用自定义 Transport 的 HTTP 客户端
	client := &http.Client{Transport: tr}

	// 模拟多次请求以复用连接
	for i := 0; i < 5; i++ {
		resp, err := client.Get("http://localhost:8080/hello")
		if err!= nil {
			log.Printf("Request %d failed: %v\n", i+1, err)
			continue
		}

		// 确保读取并关闭响应体，以便连接可以返回到连接池中复用
		_, err = io.Copy(ioutil.Discard, resp.Body)
		if err!= nil {
			log.Printf("Failed to discard response body for request %d: %v\n", i+1, err)
		}
		resp.Body.Close()

		fmt.Printf("Request %d successful, status: %s\n", i+1, resp.Status)
		time.Sleep(500 * time.Millisecond) // 稍作等待
	}

	// 客户端也可以配置为短连接，例如通过设置 DisableKeepAlives 为 true
	// shortConnClient := &http.Client{
	// 	Transport: &http.Transport{
	// 		DisableKeepAlives: true, // 禁用 Keep-Alive，强制短连接
	// 	},
	// }
	// resp, err := shortConnClient.Get("http://localhost:8080/hello")
	//...
}
```

## client

```go
package main

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

func main() {
	// 创建一个自定义的 Transport，配置连接池参数
	// MaxIdleConnsPerHost: 每个主机的最大空闲连接数，用于长连接复用
	// IdleConnTimeout: 空闲连接的最大存活时间
	tr := &http.Transport{
		MaxIdleConns:        100,             // 所有主机的最大空闲连接数
		MaxIdleConnsPerHost: 10,              // 每个主机的最大空闲连接数
		IdleConnTimeout:     90 * time.Second, // 空闲连接的超时时间
		DisableKeepAlives:   false,           // 默认启用 Keep-Alive，这里显式设置为 false 确保启用
	}

	// 创建一个使用自定义 Transport 的 HTTP 客户端
	client := &http.Client{Transport: tr}

	// 模拟多次请求以复用连接
	for i := 0; i < 5; i++ {
		resp, err := client.Get("http://localhost:8080/hello")
		if err!= nil {
			log.Printf("Request %d failed: %v\n", i+1, err)
			continue
		}

		// 确保读取并关闭响应体，以便连接可以返回到连接池中复用
		_, err = io.Copy(ioutil.Discard, resp.Body)
		if err!= nil {
			log.Printf("Failed to discard response body for request %d: %v\n", i+1, err)
		}
		resp.Body.Close()

		fmt.Printf("Request %d successful, status: %s\n", i+1, resp.Status)
		time.Sleep(500 * time.Millisecond) // 稍作等待
	}

	// 客户端也可以配置为短连接，例如通过设置 DisableKeepAlives 为 true
	// shortConnClient := &http.Client{
	// 	Transport: &http.Transport{
	// 		DisableKeepAlives: true, // 禁用 Keep-Alive，强制短连接
	// 	},
	// }
	// resp, err := shortConnClient.Get("http://localhost:8080/hello")
	//...
}
```

# 长连接降级

## **客户端侧导致长连接变为短连接：**

* **明确要求关闭连接：** 客户端在发送 HTTP 请求时，如果其请求头中明确包含 `Connection: close`，那么即使服务器支持长连接，客户端也会在收到响应后主动关闭 TCP 连接。这在某些特定场景下可能会发生，例如客户端判断不再需要与该服务器进行更多交互，或者需要强制刷新 DNS 等。
    
* **客户端异常断开：** 如果客户端由于网络问题、应用崩溃或用户强制关闭等原因导致 TCP 连接异常中断，那么正在使用的长连接自然也就断开了，未来的新请求就需要重新建立短连接（或者新的长连接）。
    
* **客户端设置了较短的空闲超时：** 客户端在管理其连接池时，可能会为长连接设置一个空闲超时时间。如果在这个时间内没有新的请求发送，客户端会主动关闭该连接，从而变为短连接行为。
    

## **服务器侧导致长连接变为短连接**

* **服务器设置了空闲超时 (Keep-Alive Timeout)：** 这是最常见的情况。服务器为了避免持有过多的空闲连接（占用资源），会为长连接设置一个超时时间。如果在指定时间内，客户端没有发送新的请求，服务器会主动关闭该 TCP 连接。当客户端再次发送请求时，就必须重新建立连接，表现为短连接行为。
    
* **服务器达到最大连接数限制：** 如果服务器承载的连接数达到其配置的最大值，为了保证服务可用性，可能会主动关闭一些空闲或不活跃的长连接，为新的连接腾出空间。
    
* **服务器资源耗尽或异常：** 服务器在处理请求过程中出现内存不足、CPU 过高、服务崩溃等异常情况，可能会强制关闭所有活跃连接或拒绝新的长连接请求，迫使客户端回退到短连接模式（重新建立连接）。
    
* **服务器明确要求关闭连接：** 服务器在响应头中包含 `Connection: close`，这会告诉客户端在收到响应后关闭连接。例如，服务器可能在处理完一个特定的、不希望被复用的请求后（如某些管理操作），或在服务器即将关闭/重启前，发出这样的指令。
