上篇关于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 时:
- 操作系统会分配一个文件描述符
- 这个文件描述符可以用于后续的网络操作(读、写、关闭等)
- 应用程序通过这个文件描述符与 Socket 交互
二. TCP连接建立过程
- Socket 创建
// net 包内部实现
fd, err := socket(family, syscall.SOCK_STREAM, syscall.IPPROTO_TCP)
这一步通过系统调用创建了一个 socket 文件描述符。
- 服务端绑定(Bind)和监听(Listen)
// 简化的服务端代码流程
bind(fd, addr)
listen(fd, backlog)
服务器将 socket 绑定到特定地址和端口,然后开始监听连接请求。
- 接受连接(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
}
}
- 客户端连接(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
}
-
数据传输:
// 读取数据 n, err := syscall.Read(fd, buf) // 写入数据 n, err := syscall.Write(fd, data)
-
关闭连接:
err := syscall.Close(fd)
三. 关键实现细节
- 多路复用
- HTTP/1.1 使用 Keep-Alive 机制复用 TCP 连接
- HTTP/2 使用流(Stream)实现多路复用,多个 HTTP 请求共享同一个 TCP 连接
- 连接池管理
// net/http/transport.go
type Transport struct {
// 空闲连接池
idleConn map[connectMethodKey][]*persistConn
// 最大空闲连接数
maxIdleConns int
// ... 其他字段
}
- 超时控制
// 设置连接超时
conn.SetDeadline(time.Now().Add(timeout))
- 错误处理和重试机制
// 简化的重试逻辑
for retry := 0; retry < maxRetries; retry++ {
conn, err := dial()
if err == nil {
return conn
}
// 等待后重试
time.Sleep(backoff)
}
四. 工作流程
-
当客户端发起 HTTP 请求时:
- 首先检查连接池是否有可用连接
- 如果没有,创建新的 TCP 连接
- 发送 HTTP 请求数据
- 等待并读取响应
-
服务端处理请求时:
- Accept 循环接受新连接
- 为每个连接创建 goroutine
- 解析 HTTP 请求
- 处理请求并返回响应
这就是 net/http
包如何在 TCP 协议之上实现 HTTP 连接的核心机制。它通过抽象和封装,使开发者不需要直接处理底层的 TCP 连接细节,同时提供了高效的连接管理和复用机制。