1.创建一个Web Server
http.ListenAndServer()
//第一个参数是网络地址
//如果为“”,那么默认就是所有网络接口的80端口
//第二个参数就是handler
//如果为nil,那么默认就是DefaultServerMux
什么是Handler?什么是DefaultServeMux?
type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}
从中可以看出handler就是一个接口,它实现了了ServeHTTP()这个方法,因此只要实现了ServeHTTP这个方法的对象都可以看成是一个handler。
我们再从Go官方文档找点信息。
//A Handler responds to an HTTP request.
//ServeHTTP should write reply headers and data to the ResponseWriter and then return.
//Returning signals that the request is finished;
//it is not valid to use the ResponseWriter or read from the Request.
//Body after or concurrently with the completion of the ServeHTTP call.
大概意思就是handler就是一个基于HTTP协议的请求和响应的一个方法。在这个方法中,我们接收客户端发过来的HTTP请求,提取请求中夹带的数据,处理数据,最后回写响应头和响应数据发送(return)给客户端。
当然在这个过程中数据的处理是由顺序的(客户端/服务端本质就是数据传递和数据处理的过程)
Go官方文档中携带一句话
it may not be possible to read from the Request.Body after writing to the ResponseWriter.
Cautious handlers should read the Request.Body first, and then reply.
意思就是在回写给ResponseWriter之后不可在读取Request.Body里面的数据。严格按照读数据,写数据的顺序。
明白了Handler,那什么是DefaultServeMux呢?
var DefaultServeMux = &defaultServeMux
var defaultServeMux ServeMux
原来DefaultServeMux就是serve使用的默认ServeMux。我们自己写一个handler太过复杂(有现成的谁喜欢造轮子啊),Go语言提供一个默认的multiplexer(即多路复用器,defaultServeMux充当这个多路复用器)
func (mux *ServeMux) ServeHTTP(w ResponseWriter, r *Request)
可以看出ServeMux(即DefaultServeMux,多了一层封装而已)实现了ServeHTTP方法,因此它也是一个handler,实现数据处理的过程。
综上我们可以先做一个连接建立请求的抽象图
OK,Talk is free,Show me the code。
package main
import "net/http"
func main() {
http.ListenAndServe("localhost:8088", nil)
}
这一行代码呢,就在我们本地的电脑上启动一个进程,监听本地8088端口接受(Aceept)客户端发来的请求,其中handler为nil,使用默认的defaultServeMux。
但是我们使用浏览器访问,无法查找到页面,这是因为我们没有针对特定的地址设定路由,也没有根据特定的地址设置相应的处理逻辑。(上面那只是一行代码,只能接收请求,但是接收进来的请求啥都没干,啥数据都没处理,因此啥数据也没有回传,故客户端得到东西为nil)
现在我们给服务端添加数据处理逻辑
我们理所当然的想将defaultServeMux转换成我们自己写的一个handler(只要实现了ServeHTTP接口就可以了嘛,很简单的啦)
package main
import "net/http"
type myHandler struct{}
func (mh myHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("the first line code of Golang Web"))
}
func main() {
http.ListenAndServe("localhost:9088", new(myHandler))
}
走你~在命令行运行它
如果运行不起来的同学,看看是不是这个端口是不是被其他进程占用了,代码没错换个端口就好了
发现正常服务端正常回写数据,但是怎么回事?hello和world这两个路由竟然返回相同的结果
原来当前业务逻辑如上,请求进来,经过myHandler处理,就回传给客户端了 。
因此我们需要给我们的服务端添加不同的路由来进行不同的数据处理
逻辑如上图,我们根据不同的路由,创建不同的handler,然后将我们的handler注册到defaultServeMux(不然怎么叫它多路复用器(multiplexer)呢)
Talk is free,Show me the code
package main
import "net/http"
type hello struct{}
func (mh hello) ServeHTTP(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("hello hello"))
}
type world struct{}
func (mh world) ServeHTTP(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("world world"))
}
func main() {
//向defaultServeMux绑定不同的路由
http.Handle("/hello", &hello{})
http.Handle("/world", &world{})
http.ListenAndServe("localhost:9088", nil)
}
完美,第一部分结束
2.现在我们再介绍另一种添加路由的方法(原理都是一样的)
package main
import "net/http"
func main() {
//向defaultServeMux绑定不同的路由
http.HandleFunc("/hello", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("hello hello"))
})
http.HandleFunc("/world", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("world world"))
})
http.ListenAndServe("localhost:9088", nil)
}
是不是简化了很多,不用写handler。这个HandlerFunc可以将某个具有适当签名的函数f,适配成为一个Handler(这也就解释了为什么我们不用写handler),而这个适配的Handler具有方法f。
我们来研究下它的原码
func HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
DefaultServeMux.HandleFunc(pattern, handler)
}
可见 handleFunc调用了DefaultServeMux中的HandleFunc
func (mux *ServeMux) HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
if handler == nil {
panic("http: nil handler")
}
mux.Handle(pattern, HandlerFunc(handler)) //注意,这一行是关键
} //我们看看这一行代码干了什么
DefaultServeMux(它是ServeMux类型)又调用了它自身的方法Handle
type HandlerFunc func(ResponseWriter, *Request)
// ServeHTTP calls f(w, r).
func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
f(w, r)
}
以上可以看出, HandlerFunc(handler)将我们的handler函数[handler func(ResponseWriter, *Request)]转换成handle,因为它实现了ServeHTTP方法了嘛。这就是适配器模式转换的应用。
现在我们再回到这两行代码上
http.HandleFunc("/world", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("world world"))
})
http.ListenAndServe("localhost:9088", nil)
不知道你们有没有一种疑惑,handleFunc与listenAndServe之间对应关系是什么,从代码上看这两个函数互不相关,也看不出什么对象与对象之间的调用关系,那么handleFunc又是如何把路由与Serve联系起来呢?
现在我们从底层开始理解这两行代码
listenAndServe干了什么呢
//The handler is typically nil, in which case the DefaultServeMux is used.
func ListenAndServe(addr string, handler Handler) error {
server := &Server{Addr: addr, Handler: handler}
return server.ListenAndServe()
}
它注册了一个server对象,调用了这个serve对象的listenAndserve方法
type Server struct {
Addr string // TCP address to listen on, ":http" if empty
Handler Handler // handler to invoke http.DefaultServeMux if nil
TLSConfig *tls.Config // optional TLS config, used by ListenAndServeTLS
...
Server的结构如上,节选了一些关键字段
func (srv *Server) ListenAndServe() error {
if srv.shuttingDown() {
return ErrServerClosed
}
addr := srv.Addr
if addr == "" {
addr = ":http"
}
ln, err := net.Listen("tcp", addr)
if err != nil {
return err
}
return srv.Serve(ln)
}
Serve的listenAndServe方法如上。其中net.Listen() 方法监听本地的网络地址,network参数可以是 tcp、tcp4、tcp6、 unix 或者 unixpacket。address 参数可以用主机名(hostname),但是不建议,因为这样创建的listener(监听器)最多监听主机的一个IP地址。
server.ListenAndServe()函数的主要作用就是初始化一个 TCPListener 结构体的实例,然后调用Server.Serve()
方法为这个连接提供服务。
for {
rw, e := l.Accept()
...
c, err := srv.newConn(rw)
c.setState(c.rwc, StateNew)
go c.serve()
}
上图节选server对象的serve方法中的部分核心代码。其涉及的底层原理为socket套接字的创建过程。socket编程涉及4个子过程。
第一bind,将主机名和端口号绑定。
第二listen,再这个端口号上进行监听,得到一个监听器,监听是一个阻塞的过程。
第三是Aceept,一旦有连接请求进来,阻塞接触,创建连接。
第四是Send/Write,创建连接完成后,可以开启一个新的线程/协程来处理该连接(数据的write和send),这时listen线程又开始阻塞接收新的连接(这个过程本质也就是个for循环)
for {
conn, err := listen.AcceptTCP()
if err != nil {
fmt.Println("accept tcp err:", err)
continue
}
//将处理新连接的业务方法和conn进行绑定,得到我们的连接模块
deadlConn := NewConnection(s, conn, cid, s.MsgHandler)
cid++
//启动当前的连接业务处理
go deadlConn.Start()
}
上述代码是我之前写过的一个基于tcp协议的高并发服务端demo,可以看到监听连接的过程就是通过for循环实现,listen阻塞住【也就是listen.AcceptTCP()这个代码会阻塞】,一旦有新的连接进来解除阻塞执行后续代码创建连接开启新的协程处理连接,之后又开启新的一轮for循环。
这里简单提一下什么是socket编程
众所周知,TCP/IP只是一个协议栈,就像操作系统的运行机制一样,必须要具体实现,同时还要提供对外的操作接口和编程接口。因此TCP/IP也必须对外提供编程接口,这个接口就是Socket接口。我们具体实现这个接口,比如实现上面的bind,listen,accpet,send/write,当然很多细节go语言都帮我们封装好了,我们只要调用补充参数就行,,才能基于TCP/IP协议栈完成网络的连接。
OK,扯远了,现在我们看看c.serve这个代码干了什么
for{
w, err := c.readRequest(ctx)
...
serverHandler{c.server}.ServeHTTP(w, w.req)
...
w.finishRequest()
}
看到这个ServeHTTP(),函数是不是有点熟悉?没错,就是serverHandler{c.server}.ServeHTTP(w, w.req)。
这行代码会去匹配在http.HandleFunc()中注册的路由,找到对应的处理函数,执行我们写的业务逻辑。
type serverHandler struct {
srv *Server
}
func (sh serverHandler) ServeHTTP(rw ResponseWriter, req *Request) {
handler := sh.srv.Handler
if handler == nil {
handler = DefaultServeMux
}
if req.RequestURI == "*" && req.Method == "OPTIONS" {
handler = globalOptionsHandler{}
}
if req.URL != nil && strings.Contains(req.URL.RawQuery, ";") {
var allowQuerySemicolonsInUse int32
req = req.WithContext(context.WithValue(req.Context(), silenceSemWarnContextKey, func() {
atomic.StoreInt32(&allowQuerySemicolonsInUse, 1)
}))
defer func() {
if atomic.LoadInt32(&allowQuerySemicolonsInUse) == 0 {
sh.srv.logf("http: URL query contains semicolon,
which is no longer a supported separator;
parts of the query may be stripped when parsed; see golang.org/issue/25192")
}
}()
}
handler.ServeHTTP(rw, req)
}
调用 serverHandler的方法ServeHTTP,在这个方法中本质上使用handler为handler := sh.srv.Handler,即默认的defaultServeMux。
OK,搞定了第一部分listenAndServe中的代码,现在我们来看看handleFunc里面干了什么
func HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
DefaultServeMux.HandleFunc(pattern, handler)
}
func (mux *ServeMux) HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
mux.Handle(pattern, HandlerFunc(handler))
}
func (mux *ServeMux) HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
if handler == nil {
panic("http: nil handler")
}
mux.Handle(pattern, HandlerFunc(handler))//重要,适配器
}
以上三部分就是我们之前讲的适配器模式嘛,接下来开始讲重点了。
//mux.Handle
func (mux *ServeMux) Handle(pattern string, handler Handler) {
...
mux.m[pattern] = muxEntry{explicit: true, h: handler, pattern: pattern}
...
}
我们可以看到,再mux.Handle这个方法中存储了我们定义的pattern及handler信息到mux中。
由此可以看到我们自定义的handler转为HandlerFunc后,根据pattern的不同,然后存储在mux的m中,m格式是map[string]muxEntry,里面存储着pattern对应的muxEntry,而muxEntry中存储着pattern及Handler。
type ServeMux struct {
mu sync.RWMutex
m map[string]muxEntry
es []muxEntry // slice of entries sorted from longest to shortest.
hosts bool // whether any patterns contain hostnames
}
type muxEntry struct {
explicit bool
h Handler
pattern string
}
总结一下,handlefunc将函数参数转换成handler后,以key-value形式存储在defaultServeMux的m属性中,其中key就是path,value就是handler。然后在处理请求的时候,serve会去自身的m属性匹配最适合的路由,然后取出其中的handler进行业务处理。以上原理如此。最后再强调一遍,DefaultServeMux实际是类型是ServeMux(上面有涉及)。
综合,我们说清楚了handleFunc和listenAndServe这两行代码背后共同使用的defaultServeMux逻辑。
3.最后,我们来举个栗子加深理解。
栗子的业务逻辑图如上。
Talk is free, Show me the code
type AuthMiddleware struct {
Next http.Handler
}
func (am *AuthMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if am.Next == nil {
am.Next = http.DefaultServeMux
}
auth := r.Header.Get("Authorization")
if auth != "" {
am.Next.ServeHTTP(w, r)
} else {
w.WriteHeader(http.StatusUnauthorized)
}
}
在AuthMiddleware这个中间件中,我们检测HTTP请求头中是否带有“AuthMiddleware"这个字段,如果没有携带这个字段,直接往响应头中回写http.StatusUnauthorized这个状态码,然后返回。如果携带了上述字段,那么请求就会转而向TimeoutMiddleware这个中间件传递。
type TimeoutMiddleware struct {
Next http.Handler
}
func (tm TimeoutMiddleware) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
if tm.Next == nil {
tm.Next = http.DefaultServeMux
}
ctx := request.Context()
ctx, _ = context.WithTimeout(ctx, 3*time.Second)
request.WithContext(ctx)
ch := make(chan struct{})
go func() {
tm.Next.ServeHTTP(writer, request)
ch <- struct{}{}
}()
select {
case <-ch:
return
case <-ctx.Done():
writer.WriteHeader(http.StatusRequestTimeout)
}
ctx.Done()
}
TimeOutMiddleware在生产环境下可以携带时间上下文,以限定业务再规定时间内完成,若不能完成则返回超时状态码(如查询数据库等是一个耗时任务,如在规定时间内不能得到结果,应返回)。
最后到达了DefaultServeMux,按照其中注册的路由寻找对应的handler进行数据处理和回传。
type company struct {
ID int `json:"id"`
Name string `json:"name"`
Country string `json:"country"`
}
func main() {
http.HandleFunc("/companies", func(writer http.ResponseWriter, request *http.Request) {
c := company{
ID: 123,
Name: "google",
Country: "USA",
}
enc := json.NewEncoder(writer)
enc.Encode(c)
})
http.ListenAndServe("localhost:9088", &AuthMiddleware{
new(TimeoutMiddleware),
})
}