net/http包
在go中开发后端,最基础的就是使用net/http包,本文我将使用一个hello,world程序来进行debug,来探究在代码内部究竟发生了什么。
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
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服务器,可以这样写:
// 创建并配置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()
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)
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)
}
}
源码过长,因此下面的分析中省略不重要的代码
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)将监听器注销。
baseCtx := context.Background()
if s.BaseContext != nil {
baseCtx = s.BaseContext(origListener)
if baseCtx == nil {
panic("BaseContext returned a nil context")
}
}
http服务器中的BaseContext是一个回调函数,用于为整个http服务器设置服务器级别的基础上下文。
用法示例如下:
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)
最后是持续的监听环节
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()的源码
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()
因为这部分源码实在过多,因此只展示部分代码
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()
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()
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对象
因此调用
func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
f(w, r)
}
看起来这段代码似乎很多余,为什么不直接在原本的代码转成HnadlerFunc直接调用?
因为这样我们可以不使用HandlerFunc来处理逻辑,比如以下的代码
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 处理TCP请求和TCP连接的无关性
从上方的源码可以看出,如果监听的端口有新的tcp连接,会启动一个新的协程去处理,而如果原本的业务还在进行,用户刷新页面断开了链接,业务也依然会继续进行,下面将举一个实例来说明:
func main() {
router := gin.Default()
router.GET("/users/:id", func(c *gin.Context) {
param := c.Param("id")
c.String(200, "user detail")
for i := 0; i < 1000; i++ {
time.Sleep(1 * time.Second)
fmt.Println(time.Now(), ":业务逻辑处理中,用户:"+param)
}
})
router.Run()
}
在cmd中输入,然后ctrl+c,输入localhost:8080/users/2,再中断,一直到用户4,将会出现以下的结果:
可以看出,
TCP连接的中断,并不会打断业务的执行,业务执行完毕后
//业务逻辑执行
serverHandler{c.server}.ServeHTTP(w, w.req)
inFlightResponse = nil
w.cancelCtx()
if c.hijacked() {
c.r.releaseConn()
return
}
w.finishRequest()
c.rwc.SetWriteDeadline(time.Time{})
if !w.shouldReuseConnection() {
if w.requestBodyLimitHit || w.closedRequestBodyEarly() {
c.closeWriteAndWait()
}
return
}
在w.finishRequest()中,将数据发送回请求方,源码如下:
func (w *response) finishRequest() {
w.handlerDone.Store(true)
if !w.wroteHeader {
w.WriteHeader(StatusOK)
}
w.w.Flush()
putBufioWriter(w.w)
w.cw.close()
w.conn.bufw.Flush()
w.conn.r.abortPendingRead()
// Close the body (regardless of w.closeAfterReply) so we can
// re-use its bufio.Reader later safely.
w.reqBody.Close()
if w.req.MultipartForm != nil {
w.req.MultipartForm.RemoveAll()
}
}
在HTTP服务器通常有两级缓冲:
处理器写入 → 响应级缓冲(w.w) → 连接级缓冲(conn.bufw) → 网络
- w.w.Flush():将响应体数据从响应级缓冲推到连接级缓冲
- w.conn.bufw.Flush():将连接级缓冲的所有数据真正发送到网络
因为目前使用Ctrl+c中断了请求进程,因此在将连接级缓冲刷新到网络,将会产生一个错误:
这个错误是 “连接被对端重置” (WSAECONNRESET)
当服务器尝试向一个 已经被客户端关闭的 TCP 连接 写入数据时,操作系统会返回此错误。
接下来在w.shouldReuseConnection()中,因为之前记录了werr,所以将返回false,最终直接返回跳出整个协程。
func (w *response) shouldReuseConnection() bool {
if w.closeAfterReply {
// The request or something set while executing the
// handler indicated we shouldn't reuse this
// connection.
return false
}
if w.req.Method != "HEAD" && w.contentLength != -1 && w.bodyAllowed() && w.contentLength != w.written {
// Did not write enough. Avoid getting out of sync.
return false
}
// There was some error writing to the underlying connection
// during the request, so don't re-use this conn.
if w.conn.werr != nil {
return false
}
if w.closedRequestBodyEarly() {
return false
}
return true
}
案例2 浏览器中断和curl的区别
上个案例中,我们使用curl发送请求,并在业务处理的过程中使用Ctrl+c强行中断,最后发现在写入的时候,报错WSAECONNRESET。而这次使用浏览器发送请求,并在业务处理过程中直接将网页叉掉,行为是否和案例1中一样?
结论:不会报错,原因如下:
当关闭浏览器的标签页时,浏览器会进行正常的TCP连接关闭流程,因此它会向服务器发送一个FIN包,表示"已经没有数据要发送了,但是依然可以接收数据"
服务器收到
FIN后,会进入CLOSE_WAIT状态,并知道客户端已关闭其发送通道。此时,服务器仍然可以将缓冲区中剩余的响应数据发送出去。随后,服务器才会发送自己的
FIN包,最终完成四次挥手,连接彻底关闭。整个过程是“有序的协商”,因此服务器端通常不会遇到“连接被重置”的写入错误。
而使用命令行打断curl不同,操作系统会立即终止进程强行关闭套接字,这种方式是直接发送一个RST给服务器,而不是FIN,RST意味着立即废除这个连接,所有未处理的都丢弃。
总结
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进行处理