net/http包
在go中开发后端,最基础的就是使用net/http包,本文我将使用一个hello,world程序来进行debug,来探究在代码内部究竟发生了什么。
go
package main
import (
"fmt"
"log"
"net/http"
)
func HelloHandler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello World")
}
func main() {
http.HandleFunc("/hello", HelloHandler)
log.Fatal(http.ListenAndServe("localhost:8080", nil))
}
Debug
http.ListenAndServe()
整个程序的启动在于http.ListenAndServe("localhost:8080", nil)这行代码: 传入地址localhost:8080以及nil
go
func ListenAndServe(addr string, handler Handler) error {
server := &Server{Addr: addr, Handler: handler}
return server.ListenAndServe()
}
可以看出这个函数的作用就是封装Server对象的创建并调用server.ListenAndServe(), Server对象代表着一个http服务器,内部封装了很多http相关的参数。
从这里可以看出,如果是需要一个快速简单的的http服务器,直接使用http.ListenAndServe("localhost:8080", nil),如果需要更加精细化参数的http服务器,可以这样写:
go
// 创建并配置Server
server := &http.Server{
Addr: ":8080", // 监听所有接口的8080端口
Handler: mux, // 使用自定义多路复用器
// 重要的超时设置
ReadHeaderTimeout: 5 * time.Second, // 5秒内必须读完请求头
ReadTimeout: 10 * time.Second, // 10秒内必须读完整个请求
WriteTimeout: 10 * time.Second, // 10秒内必须写完响应
IdleTimeout: 120 * time.Second, // 空闲连接2分钟后关闭
MaxHeaderBytes: 1 << 20, // 请求头最大1MB
}
fmt.Println("服务器正在 8080 端口监听...")
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
fmt.Printf("服务器错误: %v\n", err)
}
server.ListenAndServe()
go
func (s *Server) ListenAndServe() error {
if s.shuttingDown() {
return ErrServerClosed
}
addr := s.Addr
if addr == "" {
addr = ":http"
}
ln, err := net.Listen("tcp", addr)
if err != nil {
return err
}
return s.Serve(ln)
}
既然方法叫ListenAndServe,自然便有Listen和Serve的部分,从源码可以看出ln, err := net.Listen("tcp", addr)在对应的主机端口上启动一个TCP监听器,最后调用s.Serve(ln)
TCP端口监听:Serve(ln)
go
func (s *Server) Serve(l net.Listener) error {
if fn := testHookServerServe; fn != nil {
fn(s, l) // call hook with unwrapped listener
}
origListener := l
l = &onceCloseListener{Listener: l}
defer l.Close()
if err := s.setupHTTP2_Serve(); err != nil {
return err
}
if !s.trackListener(&l, true) {
return ErrServerClosed
}
defer s.trackListener(&l, false)
baseCtx := context.Background()
if s.BaseContext != nil {
baseCtx = s.BaseContext(origListener)
if baseCtx == nil {
panic("BaseContext returned a nil context")
}
}
var tempDelay time.Duration // how long to sleep on accept failure
ctx := context.WithValue(baseCtx, ServerContextKey, s)
for {
rw, err := l.Accept()
if err != nil {
if s.shuttingDown() {
return ErrServerClosed
}
if ne, ok := err.(net.Error); ok && ne.Temporary() {
if tempDelay == 0 {
tempDelay = 5 * time.Millisecond
} else {
tempDelay *= 2
}
if max := 1 * time.Second; tempDelay > max {
tempDelay = max
}
s.logf("http: Accept error: %v; retrying in %v", err, tempDelay)
time.Sleep(tempDelay)
continue
}
return err
}
connCtx := ctx
if cc := s.ConnContext; cc != nil {
connCtx = cc(connCtx, rw)
if connCtx == nil {
panic("ConnContext returned nil")
}
}
tempDelay = 0
c := s.newConn(rw)
c.setState(c.rwc, StateNew, runHooks) // before Serve can return
go c.serve(connCtx)
}
}
源码过长,因此下面的分析中省略不重要的代码
go
origListener := l
l = &onceCloseListener{Listener: l}
defer l.Close()
...
if !s.trackListener(&l, true) {
return ErrServerClosed
}
defer s.trackListener(&l, false)
这部分代码先是备份了先前创建的TCP监听器,然后将其封装为oneCloseListener,随后使用s.trackListener(&l, true)将监听器注册到http服务器中,退出方法时调用s.trackListener(&l, false)将监听器注销。
go
baseCtx := context.Background()
if s.BaseContext != nil {
baseCtx = s.BaseContext(origListener)
if baseCtx == nil {
panic("BaseContext returned a nil context")
}
}
http服务器中的BaseContext是一个回调函数,用于为整个http服务器设置服务器级别的基础上下文。 用法示例如下:
go
ctx, cancel := context.WithCancel(context.Background())
server := &http.Server{
BaseContext: func(l net.Listener) context.Context {
// 可以在这里为每个连接添加上下文值
return context.WithValue(ctx, "listener_addr", l.Addr().String())
},
}
// 将cancel函数注册到实际server的关闭钩子
server.RegisterOnShutdown(cancel)
最后是持续的监听环节
go
for {
rw, err := l.Accept()
if err != nil {
if s.shuttingDown() {
return ErrServerClosed
}
if ne, ok := err.(net.Error); ok && ne.Temporary() {
if tempDelay == 0 {
tempDelay = 5 * time.Millisecond
} else {
tempDelay *= 2
}
if max := 1 * time.Second; tempDelay > max {
tempDelay = max
}
s.logf("http: Accept error: %v; retrying in %v", err, tempDelay)
time.Sleep(tempDelay)
continue
}
return err
}
connCtx := ctx
if cc := s.ConnContext; cc != nil {
connCtx = cc(connCtx, rw)
if connCtx == nil {
panic("ConnContext returned nil")
}
}
tempDelay = 0
c := s.newConn(rw)
c.setState(c.rwc, StateNew, runHooks) // before Serve can return
go c.serve(connCtx)
}
当l.Accept()接收到请求后,通过http服务器的ConnContext在原有的上下文的基础上再次设置上下文,之后封装TCPConn为http连接,并设置状态为StateNew,并允许http服务器中的ConnState钩子触发。最后启动一个协程开始该连接的服务。
trackListener()
以下是trackListener()的源码
go
func (s *Server) trackListener(ln *net.Listener, add bool) bool {
s.mu.Lock()
defer s.mu.Unlock()
if s.listeners == nil {
s.listeners = make(map[*net.Listener]struct{})
}
if add {
if s.shuttingDown() {
return false
}
s.listeners[ln] = struct{}{}
s.listenerGroup.Add(1)
} else {
delete(s.listeners, ln)
s.listenerGroup.Done()
}
return true
}
先判断http服务器中的监听器容器是否已经初始化了,如果没有则分配一个map对象,利用add来判断是增加监听器还是移除监听器。从这份源码可以看出,一个http服务器是支持监听多个端口的。只需要同时运行多个s.Serve(ln)
HTTP请求处理serve()
因为这部分源码实在过多,因此只展示部分代码
css
for{
w, err := c.readRequest(ctx)
...
serverHandler{c.server}.ServeHTTP(w, w.req)
}
方法中有一段循环,实现在同一个TCP连接上用于处理多个HTTP请求,实现HTTP/1.1的Keep-Alive特性。
w, err := c.readRequest(ctx)返回一个response,request被包含在这个response对象中,后续通过serverHandler{c.server}.ServeHTTP(w, w.req)来处理http请求
serverHandler的ServeHTTP()
ini
func (sh serverHandler) ServeHTTP(rw ResponseWriter, req *Request) {
handler := sh.srv.Handler
if handler == nil {
handler = DefaultServeMux
}
if !sh.srv.DisableGeneralOptionsHandler && req.RequestURI == "*" && req.Method == "OPTIONS" {
handler = globalOptionsHandler{}
}
handler.ServeHTTP(rw, req)
}
此处会取出最早的时候创建http服务器时传入的Handler,如果这个接口对象是nil,将使用默认的DefaultServeMux,最终使用这个来处理http请求
handler的ServeHTTP()
erlang
func (mux *ServeMux) ServeHTTP(w ResponseWriter, r *Request) {
if r.RequestURI == "*" {
if r.ProtoAtLeast(1, 1) {
w.Header().Set("Connection", "close")
}
w.WriteHeader(StatusBadRequest)
return
}
var h Handler
if use121 {
h, _ = mux.mux121.findHandler(r)
} else {
h, r.Pattern, r.pat, r.matches = mux.findHandler(r)
}
h.ServeHTTP(w, r)
}
核心逻辑是根据use121的值来使用新版的findHandler还是旧版的mux121.findHandler,最终会根据请求的URI来寻找到对应的Handler接口对象,本例中是HelloHandler,它是一个HandlerFunc对象 因此调用
scss
func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
f(w, r)
}
看起来这段代码似乎很多余,为什么不直接在原本的代码转成HnadlerFunc直接调用?
因为这样我们可以不使用HandlerFunc来处理逻辑,比如以下的代码
go
type LogMiddler struct{
next Handler
}
func (logMiddler *LogMiddler) ServeHTTP(w ResponseWriter, r *Request){
fmt.Println("处理的日期是....")
logMiddler.next(w,r)
}
LogMiddler实现了Handler接口,因此上述的h可以是这个LogMiddler对象,将原本的HelloHandler封装在LogMiddler中,就实现了一种链式调用,增强原本的HelloHandler。 到目前为止,自定义的路径处理逻辑已经执行,后续都是一些善后处理。
总结
1.http.HandleFunc("/hello", HelloHandler)将处理逻辑注册到DefaultServeMux
2.创建一个http服务器
3.创建一个TCP监听器,并使用http服务器的BaseContext创建初始的context并设置一些其他值
4.当TCP请求到达时,使用http服务器的ConnContext在context基础上创建当此次TCP连接的context,并设置当前的连接状态为StateNew,如果http服务器设置了ConnState钩子函数,在连接状态变动时会触发。最后开启一个新的协程来处理这个TCP连接
5.使用一个循环不断读取和处理http请求
6.处理http请求阶段,如果创建http服务器时传入nil,则使用DefaultServeMux,然后根据RequestURI来寻找对应的Handler接口对象,最终调用它的ServeHTTP进行处理