Go 语言编程 — net/http — HTTP 服务端
目录
文章目录
- 目录
- HTTP 服务端
- 实现原理
- 注册处理程序和请求路由
- 监听并处理请求
- HTTP 服务器的可扩展性
- 自定义 Handler,同时使用默认的 DefaultServeMux 和默认的 Server 的结构
- 自定义 Handler 和 ServeMux,使用默认的 Server 的结构
- 自定义 Handler、ServeMux 和 Server
- 将函数作为处理器
HTTP 服务端
同样的,我们可以设想作为 HTTP 服务端处理一次请求应该具备哪些行为:
- 实现处理函数。
- 预设 URL、Request Method、处理函数,三者之间的路由映射。
- 监听请求。
- 分发请求并完成处理。
net/http 将上述行为整合为了两大步骤:
- 注册处理程序和请求路由。
- 监听并处理请求。
通过一个最简单的示例来感受:
package mainimport ("fmt""net/http"
)func indexHandler(w http.ResponseWriter, request *http.Request) {fmt.Fprintln(w, "Hello World.")
}func main() {http.HandleFunc("/", indexHandler)err := http.ListenAndServe("127.0.0.1:80", nil)if err != nil {fmt.Println("ListenAndServe: ", err)}
}
- http.HandleFunc 函数:用于注册处理程序 indexHandler 和请求路由 ‘/’。
- http.ListenAndServe 函数:用于监听 Socket(127.0.0.1:80)和处理外部请求。
实现原理
注册处理程序和请求路由
对于 net/http 而言,HTTP 服务器的本质就是一组实现了 http.Handler 接口的 Handlers(处理器集合),处理 HTTP 请求时,根据请求的 URL 进行路由选择到合适的 Handler 进行处理:
主要体现为两个关键的结构体 http.ServeMux 和 http.Handler:
http.Handler:处理 Request 并返回 Response。任何满足了 http.Handler 接口的对象都可作为一个 Handler。http.Handler 是 net/http 服务端具有可扩展性的核心体现。
http.ServrMux:本质上是一个 HTTP 请求路由器,或者叫多路复用器(Multiplexor)。它把收到的请求的 URL 与一组预先定义的 HASH 表进行匹配,HASH 的 keys 就是 URL paths + Request Methods 的组合、values 就是关联的 Handlers。
当我们调用 http.HandleFunc 注册一个 Handler(处理器)和请求路由时,默认使用的是 http.DefaultServeMux(路由器):
var defaultServeMux ServeMux
var DefaultServeMux = &defaultServeMuxfunc HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {DefaultServeMux.HandleFunc(pattern, handler) //调用ServeMux的HandleFunc函数
}
上述可见,DefaultServeMux 是 ServeMux 类型的变量,所以实际上调用了 http.ServeMux.HandleFunc 方法:
type ServeMux struct {mu sync.RWMutexm map[string]muxEntry // key 是 URLhosts bool
}type muxEntry struct {h Handler // 通过 HandleFunc 函数传入 Handlerpattern string // URL
}func (mux *ServeMux) HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {mux.Handle(pattern, HandlerFunc(handler)) //调用Handle
}func (mux *ServeMux) Handle(pattern string, handler Handler) {mux.mu.Lock()defer mux.mu.Unlock()//....mux.m[pattern] = muxEntry{h: handler, pattern: pattern}//....
}
可见,http.ServeMux.Handle 方法完成了 Handler 和请求路径的映射工作。http.ServeMux 的成员 http.muxEntry,存储了从 URL 到 Handler 的映射关系,HTTP 服务器在处理请求时就会使用该哈希查找到对应的 Handler。
与一个细节值得注意,HandleFunc 函数中会把 handler 变量强制类型转换为 HandlerFunc 类型:HandlerFunc(handler)
,HandleFunc 类型的定义如下:
type HandlerFunc func(ResponseWriter, *Request)func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {f(w, r)
}
可见,handler 变量和 HandleFunc 类型的函数签名是一致的:func(ResponseWriter, *Request)
,并且 HandleFunc 结构体具有一个 ServeHTTP 方法。同时 ServeHTTP 方法会调用传入的 handler,如:f(w, r)
。
综上,可以看出 HandlerFunc 只是对我们传入的 handler 变量进行了包装并提供了 ServeHTTP 方法来调用 handler 自身。
这里总结一下注册一个 Handler 的流程:
- http.HandlerFunc 调用 ServeMux.HandleFunc 方法。
- ServeMux.HandleFunc 首先对我们传进来的 handler 变量强制转换为 HandlerFunc 类型(目的是提供 ServerHTTP 接口来执行传入的 Handler)。然后调用 ServeMux.Handle 方法。
- ServeMux.Handle 方法会把 URL 和 handler 存在 ServeMux 结构的 m 字段中。
net/http 自建了几个常用的 Handler:FileServer、RedirectHandler 和 NotFoundHandler,例子:
package mainimport ("log""net/http"
)func main() {mux := http.NewServeMux()rh := http.RedirectHandler("http://www.baidu.com", 307)mux.Handle("/foo", rh)log.Println("Listening...")http.ListenAndServe(":3000", mux)
}
- 调用了 http.NewServeMux 函数来创建一个空的 ServeMux。
- 调用了 http.RedirectHandler 函数创建了一个新的 Handler,这个 Handler 会对收到的所有请求都执行 307 重定向到指定的 URL,如 http://www.baidu.com。
- 接下来使用 ServeMux.Handle 函数将 Handler 注册到新建的 ServeMux,所以它在 URL path /foo 上收到所有的请求都交给这个 Handler。
- 最后创建了一个新的 ServeHTTP,并通过 http.ListenAndServe 函数监听所有进入的请求,通过传递刚才创建的 ServeMux 来为请求去匹配对应 Handler。
监听并处理请求
http.ListenAndServe 函数用于监听 TCP 连接并处理请求,该函数会使用传入的监听地址和 Handler 初始化一个 HTTP 服务器 http.Server,然后调用该服务器的 http.Server.ListenAndServe 方法:
func ListenAndServe(addr string, handler Handler) error {server := &Server{Addr: addr, Handler: handler}return server.ListenAndServe()
}func (srv *Server) ListenAndServe() error {addr := srv.Addrif addr == "" {addr = ":http"}ln, err := net.Listen("tcp", addr) // 监听端口if err != nil {return err}return srv.Serve(tcpKeepAliveListener{ln.(*net.TCPListener)})
}
http.Server.ListenAndServe 方法使用了 net 库提供的 net.Listen 函数监听对应地址上的 TCP 连接并通过 http.Server.Serve 处理客户端的请求。http.Server.Serve 会在循环中监听外部的 TCP 连接并为每个连接调用 http.Server.newConn 创建新的结构体 http.conn,它是 HTTP 连接的服务端表示:
func (srv *Server) Serve(l net.Listener) error {// ...for {rw, e := l.Accept() // 等待请求//...c := srv.newConn(rw) // 这里创建了一个 conn 结构体,它代表一个连接c.setState(c.rwc, StateNew) // before Serve can returngo c.serve(ctx)}
}
创建了服务端的连接之后,net/http 中的实现会为每个 HTTP 请求创建单独的 Goroutine 并在其中调用 http.Conn.serve 方法,如果当前 HTTP 服务接收到了海量的请求,会在内部创建大量的 Goroutine,这可能会使整个服务质量明显降低无法处理请求。
func (c *conn) serve(ctx context.Context) {c.remoteAddr = c.rwc.RemoteAddr().String()ctx = context.WithValue(ctx, LocalAddrContextKey, c.rwc.LocalAddr())ctx, cancelCtx := context.WithCancel(ctx)c.cancelCtx = cancelCtxdefer cancelCtx()c.r = &connReader{conn: c}c.bufr = newBufioReader(c.r)c.bufw = newBufioWriterSize(checkConnErrorWriter{c}, 4<<10)for {w, _ := c.readRequest(ctx)serverHandler{c.server}.ServeHTTP(w, w.req)w.finishRequest()...}
}
上述代码片段是简化后的连接处理过程,其中包含读取 HTTP 请求、调用 Handler 处理 HTTP 请求以及调用完成该请求。读取 HTTP 请求会调用 http.Conn.readRequest 方法,该方法会从连接中获取 HTTP 请求并构建一个实现了 http.ResponseWriter 接口的变量 http.response,向该结构体写入的数据都会被转发到它持有的缓冲区中:
func (w *response) write(lenData int, dataB []byte, dataS string) (n int, err error) {...w.written += int64(lenData)if w.contentLength != -1 && w.written > w.contentLength {return 0, ErrContentLength}if dataB != nil {return w.w.Write(dataB)} else {return w.w.WriteString(dataS)}
}
解析了 HTTP 请求并初始化 http.ResponseWriter 之后,我们就可以调用 http.serverHandler.ServeHTTP 方法查找处理器来处理 HTTP 请求了:
type serverHandler struct {srv *Server
}func (sh serverHandler) ServeHTTP(rw ResponseWriter, req *Request) {handler := sh.srv.Handlerif handler == nil {handler = DefaultServeMux}if req.RequestURI == "*" && req.Method == "OPTIONS" {handler = globalOptionsHandler{}}handler.ServeHTTP(rw, req)
}
conn.serve 调用了 serverHandler 的 ServeHTTP 方法,如果我们没有手动设置新的 ServerMux 的话,那么这个方法会调用DefaultServeMux 的 ServeHTTP 方法处理外部的 HTTP 请求。
到目前为止,我们已经知道实现 ServeHTTP 的类型有两个:ServerMux 和 HandlerFunc。
- HandlerFunc 结构体:实现了 Handler 接口,开发者编写的 Handler 都会被转换为 HandlerFunc 类型,它的 ServeHTTP 方法会执行我们注册的 Handler,即:“处理器”。
- ServeMux 结构体:实现了 Handler 接口,它的 m 字段用于存储我们注册的 Handler,它的 ServeHTTP 方法提供了路由功能,即: “多路复用器”。
func (mux *ServeMux) ServeHTTP(w ResponseWriter, r *Request) {h, _ := mux.Handler(r) // 获取对应的 Handlerh.ServeHTTP(w, r) // 执行 Handler
}
经过一系列的函数调用,上述过程最终会调用 HTTP 服务器的 http.ServerMux.match 方法,该方法会遍历前面注册过的路由表并根据特定规则进行匹配:
func (mux *ServeMux) match(path string) (h Handler, pattern string) {v, ok := mux.m[path]if ok {return v.h, v.pattern}for _, e := range mux.es {if strings.HasPrefix(path, e.pattern) {return e.h, e.pattern}}return nil, ""
}
如果请求的路径和路由中的表项匹配成功,就会调用表项中对应的 Handler,Handler 中包含的业务逻辑会通过 http.ResponseWriter 构建 HTTP 请求对应的响应并通过 TCP 连接发送回客户端。
HTTP 服务器的可扩展性
net/http 服务器的可扩展性体现在 3 个方面。
自定义 Handler,同时使用默认的 DefaultServeMux 和默认的 Server 的结构
package mainimport ("io""log""net/http"
)func main() {// 设置路由规则http.HandleFunc("/", Tmp)// 使用默认的 DefaultServeMuxerr := http.ListenAndServe(":8080", nil)if err != nil {log.Fatal(err)}
}func Tmp(w http.ResponseWriter, r *http.Request) {io.WriteString(w, "version 1")
}
自定义 Handler 和 ServeMux,使用默认的 Server 的结构
package mainimport ("io""log""net/http"
)type myHandler struct{}func (*myHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {io.WriteString(w, "URL:"+r.URL.String())
}func Tmp(w http.ResponseWriter, r *http.Request) {io.WriteString(w, "version 2")
}func main(){mux := http.NewServeMux()mux.Handle("/", &myHandler{}) mux.HandleFunc("/tmp", Tmp)err = http.ListenAndServe(":8080", mux)if err != nil {log.Fatal(err)}
}
自定义 Handler、ServeMux 和 Server
package mainimport ("io""log""net/http""time"
)var mux map[string]func(http.ResponseWriter, *http.Request) func main(){server := http.Server{Addr: ":8080",Handler: &myHandler{},ReadTimeout: 5*time.Second,}mux = make(map[string]func(http.ResponseWriter, *http.Request))mux["/tmp"] = Tmperr := server.ListenAndServe()if err != nil {log.Fatal(err)}
}type myHandler struct{}func (*myHandler) ServeHTTP(w http.ResponseWriter, r *http.Request){// 实现路由的转发if h, ok := mux[r.URL.String()];ok{// 用这个 Handler 来实现路由转发,相应的路由调用相应 funch(w, r)return}io.WriteString(w, "URL:"+r.URL.String())
}func Tmp(w http.ResponseWriter, r *http.Request) {io.WriteString(w, "version 3")
}
将函数作为处理器
对于简单的情况(比如上面的例子),定义个新的有 ServerHTTP 方法的自定义类型有些累赘。
实际上,任何具有 func(http.ResponseWriter, *http.Request)
签名的函数都能转化为一个 HandlerFunc 类型。这很有用,因为 HandlerFunc 对象内置了 ServeHTTP 方法,后者可以聪明又方便的调用我们最初提供的函数内容。
package mainimport ("log""net/http""time"
)func timeHandler(w http.ResponseWriter, r *http.Request) {tm := time.Now().Format(time.RFC1123)w.Write([]byte("The time is: " + tm))
}func main() {mux := http.NewServeMux()// Convert the timeHandler function to a HandlerFunc typeth := http.HandlerFunc(timeHandler)// And add it to the ServeMuxmux.Handle("/time", th)log.Println("Listening...")http.ListenAndServe(":3000", mux)
}
实际上,将一个函数转换成 HandlerFunc 后注册到 ServeMux 是很普遍的用法,所以 Golang 为此提供了个便捷的 ServerMux.HandlerFunc 方法:
package mainimport ("log""net/http""time"
)func timeHandler(w http.ResponseWriter, r *http.Request) {tm := time.Now().Format(time.RFC1123)w.Write([]byte("The time is: " + tm))
}func main() {mux := http.NewServeMux()mux.HandleFunc("/time", timeHandler)log.Println("Listening...")http.ListenAndServe(":3000", mux)
}
再进一步的优化,如果我们想从 main() 函数中传递一些信息或者变量给 Hander 函数,一个优雅的方式是将我们 Hander 放到一个闭包中,将我们要使用的变量带进去:
package mainimport ("log""net/http""time"
)func timeHandler(format string) http.Handler {fn := func(w http.ResponseWriter, r *http.Request) {tm := time.Now().Format(format)w.Write([]byte("The time is: " + tm))}return http.HandlerFunc(fn)
}func main() {mux := http.NewServeMux()th := timeHandler(time.RFC1123)mux.Handle("/time", th)log.Println("Listening...")http.ListenAndServe(":3000", mux)
}
Go 语言编程 — net/http — HTTP 服务端相关推荐
- Netty的Socket编程详解-搭建服务端与客户端并进行数据传输
场景 Netty在IDEA中搭建HelloWorld服务端并对Netty执行流程与重要组件进行介绍: https://blog.csdn.net/BADAO_LIUMANG_QIZHI/article ...
- Socket网络编程(2)--服务端实现
中秋了,首先祝大家中秋快乐,闲着无事在家整一个socket的聊天程序,有点仿QQ界面,就是瞎折腾,不知道最后是不是能将所有功能实现. 如果你对socket不了解,请看这篇文章:http://www.c ...
- c# WINFORM SOCKET编程-简单聊天程序(服务端)
初学C#的SOCKET编程,照着网上的代码写下来,程序总是有问题,经过自己长时间的调试,完成程序,以下是原码,有需要可以参考一下,还不完善,欢迎大家批评指正.(这里的代码没更新,附件重新上传更新,在另 ...
- C语言实现UDP网络通信(附服务端和客服端完整源码)
C语言实现UDP网络通信 服务端源码 客户端源码 服务端源码 #ifdef _WIN32 #define _WINSOCK_DEPRECATED_NO_WARNINGS #define close c ...
- C语言实现TCP网络通信(附服务端和客服端完整源码)
C语言实现TCP网络通信 服务端源码 客户端源码 服务端源码 #include <stdio.h> #include <stdlib.h> #include <strin ...
- 前端3DOM编程3——Ajax和服务端通信
1.通信基础 2.HTTP协议 3.Ajax 4.cookie 六.Ajax和服务端通信 1.通信基础 (1)Web服务器搭建 服务器是一台24h不断电.不关机,提供某种服务(文件. ...
- python实现socket编程,客户端和服务端之间互相对话(二)
首先运行服务端,处于监听状态: 最后运行客户端,就可以实现服务端和客户端之间互相发送消息. 客户端: import os import cv2 import socketremote_IP='127. ...
- [C语言]一个很实用的服务端和客户端进行UDP通信的实例
前段时间发了个TCP通信的例子,现在再来一个UDP通信的例子.这些可以作为样本程序,用到开发中."裸写"socket老是记不住步骤,经常被鄙视-- 下面的例子很简单,写一个UDP的 ...
- java基础—网络编程——TCP客户端与服务端交互
import java.net.InetAddress; import java.net.Socket; import java.net.UnknownHostException; import ja ...
最新文章
- 计算机应届生必读的 AI 入门经典书单
- mfc窗口右下角如何显示一个三角形图案_大型建筑,如何做到室内外设计元素统一?...
- 微服务架构下分布式事务解决方案——阿里GTS
- 《Java和Android开发实战详解》——2.2节构建Java应用程序
- electronjs设置宽度_Js操作DOM元素及获取浏览器高宽的简单方法
- vs2010 打开项目卡死问题解决办法
- 完美且精准的 IE10- 版本检测。
- 超级搜索术4-学业有成/职场晋升
- Python办公自动化——发票开具明细汇总
- python方法怎么调用_python函数怎么调用自身?
- [日常] Go语言圣经前言
- Wps格式怎么转换成word,只要三分钟轻松搞定
- 找最大ASCII字符
- yolov5的anchor详解
- cncrypt安卓版_CnCrypt Protect
- 闪存,ROM,Nor Flash,NAND Flash
- Nokia NBU备份文件查看工具(包含联系人和短信)
- dede织梦如何去除网站底部的版权信息
- 【TCP 协议2】确认应答、超时重传机制
- python遗传算法排课表_遗传算法与Python图解
热门文章
- Xamarin.Forms中为WebView指定数据来源Source
- Wireshark网络分析实例集锦(大学霸内部资料)
- __weak与__block的区别
- java三大集合_java中三大集合框架
- 不同分类算法的优缺点是什么?(值得推荐)
- 计算机房电磁辐射防护,计算机房电磁屏蔽
- 这个赛车AI不再只图一时爽,学会了考虑长远策略
- 前NASA工程师让钢琴开口说英文,还能自弹世界上最难曲目,快到冒烟
- 真的有能开光追的手游了!自带实机演示的那种,OPPO这次玩“大”了
- 这年头,机器翻译都会通过文字脑补画面了 | NAACL 2021