Http 长连接 & 短连接详解

EkrekeEkreke
2 min read

网络连接基础

TCP/IP

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

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

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

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

(图片来源: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

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

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,这会告诉客户端在收到响应后关闭连接。例如,服务器可能在处理完一个特定的、不希望被复用的请求后(如某些管理操作),或在服务器即将关闭/重启前,发出这样的指令。

0
Subscribe to my newsletter

Read articles from Ekreke directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Ekreke
Ekreke