目录:
- 服务端代码展示
- 客户端代码展示
- HTTP 服务器源码解读
- 函数是一等公民
- 注册原理
- 服务启动与请求处理流程
- HTTP 客户端源码解读
- 请求流程
- Transport RoundTrip 流程
服务端代码如下:
package main
import (
"log"
"net/http"
"time"
)
var (
Addr = ":1210"
)
func main() {
// 创建路由器
mux := http.NewServeMux()
// 设置路由规则
mux.HandleFunc("/bye", sayBye)
// 创建服务器
server := &http.Server{
Addr: Addr,
WriteTimeout: time.Second * 3,
Handler: mux,
}
// 监听端口并提供服务
log.Println("Starting httpserver at " + Addr)
log.Fatal(server.ListenAndServe())
}
func sayBye(w http.ResponseWriter, r *http.Request) {
time.Sleep(1 * time.Second)
w.Write([]byte("bye bye, this is httpServer"))
}
客户端代码如下:
package main
import (
"fmt"
"io/ioutil"
"net"
"net/http"
"time"
)
func main() {
// 创建连接池
transport := &http.Transport{
DialContext: (&net.Dialer{
Timeout: 30 * time.Second, // 连接超时
KeepAlive: 30 * time.Second, // 长连接超时时间
}).DialContext,
MaxIdleConns: 100, // 最大空闲连接
IdleConnTimeout: 90 * time.Second, // 空闲超时时间
TLSHandshakeTimeout: 10 * time.Second, // tls握手超时时间
ExpectContinueTimeout: 1 * time.Second, // 100-continue状态码超时时间
}
// 创建客户端
client := &http.Client{
Timeout: 30 * time.Second, // 请求超时时间
Transport: transport,
}
// 请求数据
resp, err := client.Get("http://127.0.0.1:1210/bye")
if err != nil {
panic(err)
}
defer resp.Body.Close()
// 读取内容
bds, err := ioutil.ReadAll(resp.Body)
if err != nil {
panic(err)
}
fmt.Println(string(bds))
}
HTTP 服务器源码解读
HTTP是工作中最常接触的协议
函数是一等公民
把函数作为一种数据类型去传递,去实现函数的回调(匿名函数、闭包)
这种定义可以将普通函数包装成实现 http.Handler 接口的对象
注册原理
本质上是有这样一个结构体:
type ServeMux struct {
mu sync.RWMutex
tree routingNode
index routingIndex
mux121 serveMux121 // used only when GODEBUG=httpmuxgo121=1
}
// A routingNode is a node in the decision tree.
// The same struct is used for leaf and interior nodes.
type routingNode struct {
// A leaf node holds a single pattern and the Handler it was registered
// with.
pattern *pattern
handler Handler
// An interior node maps parts of the incoming request to child nodes.
// special children keys:
// "/" trailing slash (resulting from {$})
// "" single wildcard
children mapping[string, *routingNode]
multiChild *routingNode // child with multi wildcard
emptyChild *routingNode // optimization: child with key ""
}
那注册路由的过程实际上传入的就是一个 pattern 和一个 handler 方法。
内部的本质实现就是向这个树形结构里插入节点:
- 先按 host(如果有)添加子节点。
- 再按 method(如
GET、POST或空字符串表示任意方法)添加子节点。 - 最后按路径的 segments 逐层添加,并在叶子节点存储
pattern和handler。
func (root *routingNode) addPattern(p *pattern, h Handler) {
// First level of tree is host.
n := root.addChild(p.host)
// Second level of tree is method.
n = n.addChild(p.method)
// Remaining levels are path.
n.addSegments(p.segments, p, h)
}
服务启动与请求处理流程
启动监听:调用 s.ListenAndServe()
- 确定监听地址(默认
:http即 80 端口) - 创建 TCP 监听器
ln - 将监听器交给
s.Serve(ln)进一步处理
接受连接:s.Serve(ln) 循环调用 ln.Accept()
for {
rw, err := l.Accept()
// 每个新连接创建一个 goroutine 处理
go (c *conn).serve(ctx)
}
每个连接独立并发处理。
处理连接:c.serve() 内部循环读取请求并调用 handler
- 读取 HTTP 请求,构造
ResponseWriter和Request - 调用
serverHandler{c.server}.ServeHTTP(w, req)找到并执行实际处理函数
路由到最终 handler:serverHandler.ServeHTTP
func (sh serverHandler) ServeHTTP(rw ResponseWriter, req *Request) {
handler := sh.srv.Handler
if handler == nil {
handler = DefaultServeMux // 默认使用标准库的路由器
}
// 特殊处理 OPTIONS * 请求
handler.ServeHTTP(rw, req)
}
如果用户设置了自定义 Server.Handler,则使用之;否则使用 DefaultServeMux(它实现了路由规则匹配)。
最终响应:handler 处理完请求后,通过 ResponseWriter 返回数据,连接关闭或复用。
HTTP 客户端源码解读
请求流程
Timeout:从连接建立到读完响应体的总超时,零值表示不设超时(可能永久阻塞)。Transport:真正执行网络通信的接口(http.RoundTripper),默认实现支持连接池、TLS 配置等。
client := &http.Client{
Timeout: 30 * time.Second, // 请求超时时间
Transport: transport,
}
NewRequest构造一个http.Request,包含方法、URL、可选的 body。- 实际处理交给
Do方法,以便统一处理所有 HTTP 方法。
func (c *Client) Get(url string) (resp *Response, err error) {
req, err := NewRequest("GET", url, nil)
if err != nil {
return nil, err
}
return c.Do(req)
}
Do 本身只是加了一些文档说明和校验,核心逻辑在 do 方法中。
func (c *Client) Do(req *Request) (*Response, error) {
return c.do(req)
}
无限循环,实现发送操作:
if resp, didTimeout, err = c.send(req, deadline); err != nil {
// c.send() always closes req.Body
reqBodyClosed = true
if !deadline.IsZero() && didTimeout() {
err = &timeoutError{err.Error() + " (Client.Timeout exceeded while awaiting headers)"}
}
return nil, uerr(err)
}
具体的 send 代码如下:
func (c *Client) send(req *Request, deadline time.Time) (resp *Response, didTimeout func() bool, err error) {
if c.Jar != nil {
for _, cookie := range c.Jar.Cookies(req.URL) {
req.AddCookie(cookie)
}
}
resp, didTimeout, err = send(req, c.transport(), deadline)
if err != nil {
return nil, didTimeout, err
}
if c.Jar != nil {
if rc := resp.Cookies(); len(rc) > 0 {
c.Jar.SetCookies(req.URL, rc)
}
}
return resp, nil, nil
}
在更内部的 send 函数中(p.s 怎么这么多层)去调用 RoundTrip 方法以得到 resp ,并最终逐层返回。
resp, err = rt.RoundTrip(req)
Transport RoundTrip 流程
RoundTrip 是 transport.go 中的 func (t *Transport) roundTrip(req *Request) (_ *Response, err error) 方法
在其中先执行 getConn 方法:
- 尝试拿去空闲链接
// Queue for idle connection.if delivered := t.queueForIdleConn(w); !delivered {
t.queueForDial(w)
} - 如果拿不到进入 select 并等待完成或者取消
// Wait for completion or cancellation.
select {
case r := <-w.result:
// Trace success but only for HTTP/1.
// HTTP/2 calls trace.GotConn itself.
if r.pc != nil && r.pc.alt == nil && trace != nil && trace.GotConn != nil {
info := httptrace.GotConnInfo{
Conn: r.pc.conn,
Reused: r.pc.isReused(),
}
if !r.idleAt.IsZero() {
info.WasIdle = true
info.IdleTime = time.Since(r.idleAt)
}
trace.GotConn(info)
}
if r.err != nil {
// If the request has been canceled, that's probably
// what caused r.err; if so, prefer to return the
// cancellation error (see golang.org/issue/16049).
select {
case <-treq.ctx.Done():
err := context.Cause(treq.ctx)
if err == errRequestCanceled {
err = errRequestCanceledConn
}
return nil, err
default:
// return below
}
}
return r.pc, r.err
case <-treq.ctx.Done():
err := context.Cause(treq.ctx)
if err == errRequestCanceled {
err = errRequestCanceledConn
}
return nil, err
}
这段代码的核心是 “优先返回最准确的错误”。它确保了在请求被取消时,调用方总能收到 context.Canceled 或 context.DeadlineExceeded 这类明确的错误,而不是在网络层被包装过的其他错误。
而下面这段代码就实现了最终得到连接的过程:
// Get the cached or newly-created connection to either the
// host (for http or https), the http proxy, or the http proxy
// pre-CONNECTed to https server. In any case, we'll be ready
// to send it requests.
pconn, err := t.getConn(treq, cm)
if err != nil {
req.closeBody()
return nil, err
}
var resp *Response
if pconn.alt != nil {
// HTTP/2 path.
resp, err = pconn.alt.RoundTrip(req)
} else {
resp, err = pconn.roundTrip(treq)
}
if err == nil {
if pconn.alt != nil {
// HTTP/2 requests are not cancelable with CancelRequest,
// so we have no further need for the request context.
//
// On the HTTP/1 path, roundTrip takes responsibility for
// canceling the context after the response body is read.
cancel(errRequestDone)
}
resp.Request = origReq
return resp, nil
}
// Failed. Clean up and determine whether to retry.
if http2isNoCachedConnError(err) {
if t.removeIdleConn(pconn) {
t.decConnsPerHost(pconn.cacheKey)
}
} else if !pconn.shouldRetryRequest(req, err) {
// Issue 16465: return underlying net.Conn.Read error from peek,
// as we've historically done.
if e, ok := err.(nothingWrittenError); ok {
err = e.error
}
if e, ok := err.(transportReadFromServerError); ok {
err = e.err
}
if b, ok := req.Body.(*readTrackingBody); ok && !b.didClose {
// Issue 49621: Close the request body if pconn.roundTrip
// didn't do so already. This can happen if the pconn
// write loop exits without reading the write request.
req.closeBody()
}
return nil, err
}
testHookRoundTripRetried()
// Rewind the body if we're able to.
req, err = rewindBody(req)
if err != nil {
return nil, err
}
上面是 HTTP/2 的实现流程,在 HTTP/1.X 中稍有区别。完成流程图参考如下:

Client 、Transport 配置
transport := &http.Transport{
DialContext: (&net.Dialer{
Timeout: 30 * time.Second, // 连接超时
KeepAlive: 30 * time.Second, // 长连接超时时间
}).DialContext,
MaxIdleConns: 100, // 最大空闲连接
IdleConnTimeout: 90 * time.Second, // 空闲超时时间
TLSHandshakeTimeout: 10 * time.Second, // tls握手超时时间
ExpectContinueTimeout: 1 * time.Second, // 100-continue状态码超时时间
}

从 Client.Do 开始,到请求结束(连接进入空闲状态),整个过程可以拆解为以下关键阶段:
Client.Do:整个请求的起始点,对应总超时。Dial:建立 TCP 连接。TLS handshake:如果使用 HTTPS,进行 TLS 握手。Request:发送 HTTP 请求头部和正文(如果有)。Resp. headers:读取服务器返回的响应头部。Response body:读取响应正文。Idle:请求完成后,连接进入空闲状态,等待复用。
| 阶段 | 对应超时字段 | 说明 |
|---|---|---|
| 整个请求 | http.Client.Timeout | 从 Client.Do 开始,到读完响应正文的总时间。如果超时,请求会被取消。 |
| TCP 连接建立 | net.Dialer.Timeout | 建立 TCP 连接的最大等待时间(包括域名解析)。 |
| TLS 握手 | http.Transport.TLSHandshakeTimeout | HTTPS 请求中,TLS 握手允许的最大时间。 |
| 发送请求 | (无独立超时) | Go 没有单独的“请求发送”超时,发送过程通常很快,但如果网络阻塞,可能受底层 TCP 写超时影响,但一般不单独设置。 |
| 读取响应头 | http.Transport.ResponseHeaderTimeout | 从写完请求到读完响应头部允许的最大时间。如果服务器响应慢,此超时生效。 |
| 读取响应体 | (无独立超时) | 读取正文没有独立超时,但可以通过 Client.Timeout 整体控制,或通过 context.WithTimeout 在请求级别设置更细粒度的超时。 |
| 连接空闲 | http.Transport.IdleConnTimeout | 连接在连接池中保持空闲的最大时间,超时后会被关闭。 |