Go Web: 搭建web服务器的底层实现流程
搭建一个简单的web服务#
package main
import (
"fmt"
"net/http"
"strings"
"log"
)
func sayhelloName(w http.ResponseWriter, r *http.Request) {
r.ParseForm() // 解析参数,默认是不会解析的
fmt.Println(r.Form) // 这些信息是输出到服务器端的打印信息
fmt.Println("path", r.URL.Path)
fmt.Println("scheme", r.URL.Scheme)
fmt.Println(r.Form["num"])
for k, v := range r.Form {
fmt.Println("key:", k)
fmt.Println("val:", strings.Join(v, ""))
}
fmt.Fprintf(w, "Hello World") // 这个写入到w的是输出到客户端的
}
func main() {
http.HandleFunc("/", sayhelloName) // 注册了请求/的路由规则,当请求uri为"/",路由就会转到函数sayhelloName
err := http.ListenAndServe(":9090", nil) // 设置监听的端口
if err != nil {
log.Fatal("ListenAndServe: ", err)
}
}
在上述代码中,主函数调用了net/http包下的两个函数HandleFunc() 和ListenAndServe()。
HandleFunc()#
// http.HandleFunc("/", sayhelloName)
func HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
DefaultServeMux.HandleFunc(pattern, handler)
}
在本例中pattern = "/", handler = sayhelloName(),代表HandleFunc() 注册了请求"/"的路由规则:当请求uri为"/",路由就会转到函数sayhelloName()。
函数HandleFunc() 的底层实现步骤为:
-
调用DefaultServeMux.HandleFunc() 方法
// DefaultServeMux.HandleFunc(pattern, handler) func (mux *ServeMux) HandleFunc(pattern string, handler func(ResponseWriter, *Request)) { if handler == nil { panic("http: nil handler") } mux.Handle(pattern, HandlerFunc(handler)) }
其中,DefaultServeMux的类型是*ServeMux:
// DefaultServeMux is the default ServeMux used by Serve. var DefaultServeMux = &defaultServeMux var defaultServeMux ServeMux
ServeMux类型是HTTP请求的多路转接器。它会将每一个接收的请求的URL与一个注册模式的列表进行匹配,并调用和URL最匹配的模式的处理器。
-
调用mux.Handle()方法,向DefaultServeMux的map[string]muxEntry中增加对应的handler和路由规则。
// mux.Handle(pattern, HandlerFunc(handler)) func (mux *ServeMux) Handle(pattern string, handler Handler) { mux.mu.Lock() defer mux.mu.Unlock() ... if mux.m == nil { mux.m = make(map[string]muxEntry) } e := muxEntry{h: handler, pattern: pattern} mux.m[pattern] = e // 增加对应的handler和路由规则 ... }
其中,ServeMux类型的属性如下:
type ServeMux struct { mu sync.RWMutex //锁,由于请求涉及到并发处理,因此这里需要一个锁机制 m map[string]muxEntry // 路由规则,一个string对应一个mux实体,这里的string就是注册的路由表达式 hosts bool // 是否在任意的规则中带有host信息 } type muxEntry struct { explicit bool // 是否精确匹配 h Handler // 这个路由表达式对应哪个handler pattern string //匹配字符串 }
-
我们可以看到muxEntry类型的key[h]对应Handler类型:
type Handler interface { ServeHTTP(ResponseWriter, *Request) }
而我们定义的函数sayhelloName并未实现该接口,因此理论上是不能作为参数放入e := muxEntry{h: handler, pattern: pattern} 表达式中的。这时我们注意到在第二步调用mux.Handle(pattern, HandlerFunc(handler)) 时,其实已经通过 HandlerFunc(handler) 将函数 sayhelloName强制转换为HandlerFunc类型。而类型HandlerFunc默认拥有方法:ServeHTTP ,因此sayhelloName 就实现了Handler接口,从而可以为特定的路径处理HTTP请求。
// HandlerFunc(handler) type HandlerFunc func(ResponseWriter, *Request) func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request)
ListenAndServe()#
// err := http.ListenAndServe(":9090", nil)
func ListenAndServe(addr string, handler Handler) error {
server := &Server{Addr: addr, Handler: handler}
return server.ListenAndServe()
}
其底层实现的步骤为:
-
server := &Server{Addr: addr, Handler: handler}实例化了一个Server,它定义了运行HTTP服务端的参数:
type Server struct { Addr string // 监听的TCP地址,如果为空字符串会使用":http" Handler Handler // 调用的处理器,如为nil会调用http.DefaultServeMux ... }
-
调用Server的ListenAndServe() 方法,该方法分为两个部分:监听端口(ln, err := net.Listen("tcp", addr))和 提供服务(srv.Serve(ln))。
// return server.ListenAndServe() func (srv *Server) ListenAndServe() error { ... addr := srv.Addr if addr == "" { addr = ":http" } ln, err := net.Listen("tcp", addr) if err != nil { return err } return srv.Serve(ln) }
-
调用net.Listen() 方法监听端口,返回一个Listener接口。
// ln, err := net.Listen("tcp", addr) func Listen(network, address string) (Listener, error) { var lc ListenConfig return lc.Listen(context.Background(), network, address) } type Listener interface { // Addr返回该接口的网络地址 Addr() Addr // Accept等待并返回下一个连接到该接口的连接 Accept() (c Conn, err error) // Close关闭该接口,并使任何阻塞的Accept操作都会不再阻塞并返回错误。 Close() error }
Listener是一个用于面向流的网络协议的公用的网络监听器接口。多个线程可能会同时调用一个Listener的方法。
-
将上一步返回的Listener接口ln作为参数,调用Sever.Serve() 方法:
启动一个for循环,在循环体中调用Listener接口的Accept() 方法,等待接收客户端发出的请求。
// return srv.Serve(ln) func (srv *Server) Serve(l net.Listener) error { defer l.Close() var tempDelay time.Duration // how long to sleep on accept failure for { // 等待接收客户端发出的请求,返回一个接口类型Conn的实例rw rw, err := l.Accept() ... c := srv.newConn(rw) ... go c.serve(connCtx) }
-
c := srv.newConn(rw)对客户端的每个请求实例化一个conn(此处的c为结构体类型conn,上文中的rw为接口类型Conn,注意大小写), 即创建了一个连接实体。这个连接实体里面保存了该次请求的信息。
func (srv *Server) newConn(rwc net.Conn) *conn { c := &conn{ server: srv, rwc: rwc, } if debugServerConnections { c.rwc = newLoggingConn("server", c.rwc) } return c }
-
go c.serve(connCtx)调用conn.Serve()方法开启一个goroutine为这个连接进行服务。这体现了go的支持高并发,用户的每一次请求都是在一个新的goroutine去服务,相互不影响。
func (c *conn) serve(ctx context.Context) { ... for { // 读取每个请求的内容 w, err := c.readRequest(ctx) if c.r.remain != c.server.initialReadLimitSize() { // If we read any bytes off the wire, we're active. c.setState(c.rwc, StateActive) } ... // 处理请求 serverHandler{c.server}.ServeHTTP(w, w.req) .... }
-
Serve()方法分为两个部分,分别是:w, err := c.readRequest(ctx)和serverHandler{c.server}.ServeHTTP(w, w.req)。首先调用conn.readRequest() 方法对请求进行解析,返回了一个*response类型的响应w:
// w, err := c.readRequest(ctx) func (c *conn) readRequest(ctx context.Context) (w *response, err error) { ... w = &response{ conn: c, cancelCtx: cancelCtx, req: req, reqBody: req.Body, ... } ... }
结构体类型response代表对一个HTTP请求的响应。由于该结构体首字母小写,因此无法在包外调用。而*response实现了ResponseWriter的全部方法。因此我们可以将*response当作一个ResponseWriter传入相应的函数,从而通过引用传递来操作response。
type ResponseWriter interface { Header() Header WriteHeader(int) Write([]byte) (int, error) } type response struct { conn *conn req *Request // request for this response ... } func (w *response) Header() Header { if w.cw.header == nil && w.wroteHeader && !w.cw.wroteHeader { w.cw.header = w.handlerHeader.Clone() } w.calledHeader = true return w.handlerHeader } func (w *response) WriteHeader(code int) { if w.conn.hijacked() { caller := relevantCaller() w.conn.server.logf("http: response.WriteHeader on hijacked connection from %s (%s:%d)", caller.Function, path.Base(caller.File), caller.Line) return } ... } func (w *response) Write(data []byte) (n int, err error) { return w.write(len(data), data, "") }
这也解释了为什么ServeHTTP方法中的两个参数:ResponseWriter和*Request,只有Request是按引用传递的,因为传入ResponseWriter的变量本质是*response。
-
随后在Serve()方法中调用serverHandler结构体类型的ServeHTTP方法,并将w和w.req分别作为响应和请求传入:
serverHandler{c.server}.ServeHTTP(w, w.req)
serverHandler结构体只有一个字段srv,它是一个指向Server结构体类型的指针。调用ServeHTTP方法时,会首先判断之前实例化得到的Server中的Handler字段是否为空。若为空,则设置handler = DefaultServeMux。
type serverHandler struct { srv *Server } func (sh serverHandler) ServeHTTP(rw ResponseWriter, req *Request) { handler := sh.srv.Handler if handler == nil { handler = DefaultServeMux } ... handler.ServeHTTP(rw, req) }
由于本例中调用http.ListenAndServe(":9090", nil)时,传入Handler的参数值为nil,因此设置handler = DefaultServeMux。上文提到,DefaultServeMux是一个指向ServeMux的指针,而*ServeMux的ServeHTTP方法为:
\\ handler.ServeHTTP(rw, req) func (mux *ServeMux) ServeHTTP(w ResponseWriter, r *Request) { ... h, _ := mux.Handler(r) h.ServeHTTP(w, r) }
其中,h, _ := mux.Handler(r)内部还调用了ServeMux.handler方法,其作用是调用mux.match()根据path在DefaultServeMux中寻找对应的Handler函数。由于我们已经通过在主函数中的第一行代码中调用HandleFunc()向DefaultServeMux中注册了请求"/"的路由规则(映射关系),因此当请求uri为"/"时,返回的变量h便是Handler函数sayhelloName。
func (mux *ServeMux) Handler(r *Request) (h Handler, pattern string) { ... host := stripHostPort(r.Host) ... return mux.handler(host, r.URL.Path) } // handler is the main implementation of Handler. func (mux *ServeMux) handler(host, path string) (h Handler, pattern string) { ... if mux.hosts { h, pattern = mux.match(host + path) } if h == nil { h, pattern = mux.match(path) } if h == nil { h, pattern = NotFoundHandler(), "" } return }
-
因此上述代码中的h.ServeHTTP(w, r)就是调用了Handler函数sayhelloName的ServeHttp方法:
func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) { f(w, r) }
ServeHTTP方法有2个参数,第二个参数是 Request ,该对象包含了该HTTP请求的所有的信息,比如请求地址、Header和Body等信息;第一个参数是 ResponseWriter ,利用 ResponseWriter 可以构造针对该请求的响应。
这个方法内部其实就是调用sayhelloName本身。
实现流程图#
测试结果#
一、无参数发起请求#
客户端#
服务端#
二、带参数发起请求#
客户端#
服务端#
作者:koktlzz
出处:https://www.cnblogs.com/koktlzz/p/14176452.html
版权:本作品采用「署名-非商业性使用-相同方式共享 4.0 国际」许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· AI与.NET技术实操系列:基于图像分类模型对图像进行分类
· go语言实现终端里的倒计时
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· 25岁的心里话
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现