Go的net/http包是如何建立tcp连接的

85 阅读3分钟

上篇关于echo是如何接受restful请求的文章中留下了几个新的问题,这篇我们就来看看, net/http 包是如何在 TCP 协议基础上建立连接的。

一. 前言

在开始之前,先简单说下两个概念, Socket 和文件描述符,方便后续的理解。

Socket 是什么

Socket(套接字)是网络通信的基础抽象,它提供了一种应用程序访问网络协议栈的标准接口。简单来说,Socket 是网络通信的端点,它允许不同计算机上的程序通过网络进行数据交换。

Socket 主要特点:

  • 是应用层与传输层之间的接口
  • 可以看作是一种特殊的文件,可以进行读写操作
  • 有不同类型:TCP Socket(面向连接)和 UDP Socket(无连接)等

文件描述符是什么

文件描述符(File Descriptor)是操作系统用来标识和管理打开文件的整数值。在 Unix/Linux 系统中,一切皆文件,包括普通文件、目录、设备甚至网络连接。

文件描述符的关键点:

  • 是一个非负整数,通常从 0 开始(0 是标准输入,1 是标准输出,2 是标准错误)
  • 在操作系统内核中,文件描述符是指向文件表项的索引
  • 每个进程都有自己的文件描述符表

Socket 与文件描述符的关系

在 Unix/Linux 系统中,Socket 也被视为一种特殊的文件,因此也有对应的文件描述符。当你创建一个 Socket 时:

  1. 操作系统会分配一个文件描述符
  2. 这个文件描述符可以用于后续的网络操作(读、写、关闭等)
  3. 应用程序通过这个文件描述符与 Socket 交互

二. TCP连接建立过程

  1. Socket 创建
// net 包内部实现
fd, err := socket(family, syscall.SOCK_STREAM, syscall.IPPROTO_TCP)

这一步通过系统调用创建了一个 socket 文件描述符。

  1. 服务端绑定(Bind)和监听(Listen)
// 简化的服务端代码流程
bind(fd, addr)
listen(fd, backlog)

服务器将 socket 绑定到特定地址和端口,然后开始监听连接请求。

  1. 接受连接(Accept)
// net/http/server.go 简化版
func (srv *Server) Serve(l net.Listener) error {
    for {
        rw, err := l.Accept()  // 接受新的连接
        if err != nil {
            // 处理错误
            continue
        }
        go srv.newConn(rw).serve(ctx)  // 为每个连接创建一个新的 goroutine
    }
}
  1. 客户端连接(Connect)
// net/http/transport.go 简化版
func (t *Transport) dialConn(ctx context.Context, addr string) (*conn, error) {
    // 创建 TCP 连接
    netConn, err := t.dial(ctx, "tcp", addr)
    if err != nil {
        return nil, err
    }
    // 封装成 HTTP 连接
    return &conn{
        conn: netConn,
        // ... 其他字段
    }, nil
}
  1. 数据传输

    // 读取数据
    n, err := syscall.Read(fd, buf)
    // 写入数据
    n, err := syscall.Write(fd, data)
    
  2. 关闭连接

    err := syscall.Close(fd)
    

三. 关键实现细节

  1. 多路复用
  • HTTP/1.1 使用 Keep-Alive 机制复用 TCP 连接
  • HTTP/2 使用流(Stream)实现多路复用,多个 HTTP 请求共享同一个 TCP 连接
  1. 连接池管理
// net/http/transport.go
type Transport struct {
    // 空闲连接池
    idleConn     map[connectMethodKey][]*persistConn
    // 最大空闲连接数
    maxIdleConns int
    // ... 其他字段
}
  1. 超时控制
// 设置连接超时
conn.SetDeadline(time.Now().Add(timeout))
  1. 错误处理和重试机制
// 简化的重试逻辑
for retry := 0; retry < maxRetries; retry++ {
    conn, err := dial()
    if err == nil {
        return conn
    }
    // 等待后重试
    time.Sleep(backoff)
}

四. 工作流程

  1. 当客户端发起 HTTP 请求时:

    • 首先检查连接池是否有可用连接
    • 如果没有,创建新的 TCP 连接
    • 发送 HTTP 请求数据
    • 等待并读取响应
  2. 服务端处理请求时:

    • Accept 循环接受新连接
    • 为每个连接创建 goroutine
    • 解析 HTTP 请求
    • 处理请求并返回响应

这就是 net/http 包如何在 TCP 协议之上实现 HTTP 连接的核心机制。它通过抽象和封装,使开发者不需要直接处理底层的 TCP 连接细节,同时提供了高效的连接管理和复用机制。