接下来就是重点了,我们的HTTP服务器,这个大家都不陌生,HTTP是最常用的方式之一,通用性很强,跨团队协作上也比较受到推荐,排查问题也相对来说简单。
我们接下来以3种方式来展现Golang的HTTP服务器的简洁和强大。
- 写一个简单的HTTP服务器
- 写一个稍微复杂带路由的HTTP服务器
- 分析源码,然后实现一个自定义Handler的服务器
然后我们对照net/http包来进行源码分析,加强对http包的理解。
1、写一个简单的HTTP服务器:
package main;
import (
"net/http"
)
funchello(w http.ResponseWriter, req *http.Request) {
w.Write([]byte("Hello"))
}
funcsay(w http.ResponseWriter, req *http.Request) {
w.Write([]byte("Hello"))
}
funcmain() {
http.HandleFunc("/hello", hello);
http.Handle("/handle",http.HandlerFunc(say));
http.ListenAndServe(":8001", nil);
select{};
}
是不是很简单,我用2种方式演示了这个例子,HandleFunc和Handle方式不同,却都能实现一个路由的监听,其实很简单,但是很多人看到这都会有疑惑,别着急,咱们源码分析的时候你会看到。
2、写一个稍微复杂带路由的HTTP服务器:
对着上面的例子想一个问题,我们在开发中会遇到很多问题,比如handle/res,handle/rsa…等等路由,这两个路由接受的参数都不一样,我们应该怎么写。我先来个图展示下运行结果。
是不是挺惊讶的,404了,路由没有匹配到。可是我们写handle这个路由了。
问题:
- 什么原因导致的路由失效
- 如何解决这种问题,做一个可以用Controller来控制的路由
问题1:
我们在源码阅读分析的时候会解决。
问题2:
我们可以设定一个控制器Handle,它有2个action,我们的执行handle/res对应的结果是调用Handle的控制器下的res方法。这样是不是很酷。
来我们先上代码:
静态目录:
- css
- js
- image
静态目录很好实现,只要一个函数http.FileServer(),这个函数从文字上看就是文件服务器,他需要传递一个目录,我们常以http.Dir("Path")来传递。
其他目录大家自己实现下,我们来实现问题2,一个简单的路由。
我们来看下代码
package main;
import (
"net/http"
"strings"
"reflect"
"fmt"
)
funchello(w http.ResponseWriter, req *http.Request) {
w.Write([]byte("Hello"));
}
type Handlers struct{
}
func(h *Handlers) ResAction(w http.ResponseWriter, req *http.Request) {
fmt.Println("res");
w.Write([]byte("res"));
}
funcsay(w http.ResponseWriter, req *http.Request) {
pathInfo := strings.Trim(req.URL.Path, "/");
parts := strings.Split(pathInfo, "/");
varaction = "";
fmt.Println(strings.Join(parts,"|"));
if len(parts) >1 {
action = strings.Title(parts[1]) + "Action";
}
fmt.Println(action);
handle := &Handlers{};
controller := reflect.ValueOf(handle);
method := controller.MethodByName(action);
r := reflect.ValueOf(req);
wr := reflect.ValueOf(w);
method.Call([]reflect.Value{wr, r});
}
funcmain() {
http.HandleFunc("/hello", hello);
http.Handle("/handle/",http.HandlerFunc(say));
http.ListenAndServe(":8081", nil);
select{};
}
上面代码就可以实现handle/res,handle/rsa等路由监听,把前缀相同的路由业务实现放在一个文件里,这样也可以解耦合,是不是清爽多了。其实我们可以在做的更加灵活些。在文章最后我们放出来一个流程图,按照流程图做你们就能写出一个简单的mvc路由框架。接下来看运行之后的结果。
如下图:
(点击放大图像)
3、分析源码,然后实现一个自定义Handler的服务器
现在我们利用这个例子来分析下http包的源码(只是服务器相关的,Request我们此期不讲,简单看看就行。)
其实使用Golang做web服务器的方式有很多,TCP也是一种,net包就可以实现,不过此期我们不讲,因为HTTP服务器如果不懂,TCP会让你更加不明白。
我们从入口开始,首先看main方法里的http.HandleFunc和http.Handle这个绑定路由的方法,上面一直没解释有啥区别。现在我们来看一下。
funcHandleFunc(pattern string, handler func(ResponseWriter, *Request))
funcHandle(pattern string, handler Handler)
Handle 和HandleFunc都是注册路由,从上面也能看出来这两个函数都是绑定注册路由函数的。如何绑定的呢?我们来看下。
上面2个函数通过DefaultServeMux.handle,DefaultServeMux.handleFunc把pattern和HandleFunc绑定到ServeMux的Handle上。
为什么DefaultServeMux会把路由绑定到ServeMux上呢?
varDefaultServeMux = NewServeMux()
因为DefaultServeMux就是ServeMux的实例对象。导致我们就把路由和执行方法绑注册好了。不过大家请想下handle/res的问题?
从上面的分析我们要知道几个重要的概念。
HandlerFunc
type HandlerFuncfunc(ResponseWriter, *Request)
func(f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
f(w, r)
}
上面的大概意思是,定义了一个函数适配器(可以理解成函数指针)HandleFunc,通过HandlerFunc(f)来进行适配。其实调用的实体是f本身。
package main
import "fmt"
type A func(int, int)
func(f A)Serve() {
fmt.Println("serve2")
}
funcserve(int,int) {
fmt.Println("serve1")
}
funcmain() {
a := A(serve)
a(1,2)
a.Serve()
}
上面结果是serve1,serve2
Golang的源码里用了很多HandleFunc这个适配器。
接下来我们看第二个,ServeMux结构,最终我们是绑定它,也是通过它来解析。
type ServeMuxstruct{
mu sync.RWMutex
m map[string]muxEntry
hosts bool
}
type muxEntrystruct{
explicit bool
h Handler
pattern string
}
看到explicit的时候是不是就明白为啥handle/res不能用handle来监听了?原来如此。大致绑定流程大家看明白了吗?如果不理解可以回去再看一遍。
接下来我们来看实现“启动/监听/触发”服务器的代码。
http.ListenAndServe(":8081", nil);
上面这句就是,”:8081”是监听的端口,也是socket监听的端口,第二个参数就是我们的Handler,这里我们写nil。
funcListenAndServe(addr string, handler Handler) error {
server := &Server{Addr: addr, Handler: handler}
return server.ListenAndServe()
}
从这个代码看出来,Server这个结构很重要。我们来看看他是什么。
type Server struct {
Addr string
Handler Handler
ReadTimeouttime.Duration
WriteTimeouttime.Duration
MaxHeaderBytesint
TLSConfig *tls.Config
...
}
Server提供的方法有:
func(srv *Server) Serve(l net.Listener) error
func(srv *Server) ListenAndServe() error
func(srv *Server) ListenAndServeTLS(certFile, keyFile string) error
Server的ListenAndServe方法通过TCP的方式监听端口,然后调用Serve里的实现等待client来accept,然后开启一个协程来处理逻辑(go c.serve)。
它的格式
func(srv *Server) ListenAndServe() error
看到这里我们要了解几个重要的概念。
ResponseWriter:生成Response的接口
Handler:处理请求和生成返回的接口
ServeMux:路由,后面会说到ServeMux也是一种Handler
Conn : 网络连接
这几个概念看完之后我们下面要用。
type conn struct
这个结构是一个网络间接。我们暂时忽略。
这个c.serve里稍微有点复杂,它有关闭这次请求,读取数据的,刷新缓冲区的等实现。这里我们主要关注一个c.readRequest(),通过redRequest可以得到Response,就是输出给客户端数据的一个回复者。
它里面包含request。如果要看懂这里的实现就要搞懂三个接口。
ResponseWriter, Flusher, Hijacker
type ResponseWriter interface {
Header() Header
Write([]byte) (int, error)
WriteHeader(int)
}
type Flusher interface {
Flush()
}
type Hijacker interface {
Hijack() (net.Conn, *bufio.ReadWriter, error)
}
而我们这里的w也就是ResponseWriter了。而调用了下面这句方法,就可以利用它的Write方法输出内容给客户端了。
serverHandler{c.server}.ServeHTTP(w, w.req)
这句就是触发路由绑定的方法了。要看这个触发器我们还要知道几个接口。
具体我们先看下如何实现这三个接口的,因为后面我们要看触发路由执行逻辑片段。实现这三个接口的结构是response
response
type response struct {
conn *conn
req *Request
chunking bool
wroteHeaderbool
wroteContinuebool
header Header
written int64
contentLength int64
status int
needSniffbool
closeAfterReplybool
requestBodyLimitHitbool
}
在response中是可以看到
func(w *response) Header() Header
func(w *response) WriteHeader(code int)
func(w *response) Write(data []byte) (n int, err error)
func(w *response) WriteString(data string) (n int, err error)
func(w *response) write(lenDataint, dataB []byte, dataS string) (n int, err error)
func(w *response) finishRequest()
func(w *response) Flush()
func(w *response) Hijack() (rwcnet.Conn, buf *bufio.ReadWriter, err error)
我简单罗列一些,从上面可以看出,response实现了这3个接口。
接下来我们请求真正的触发者也就是serverHandle要触发路由(hijacked finishRequest暂且不提)。先看一个接口。
Handler
type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}
实现了handler接口,就意味着往server端添加了处理请求的逻辑函数。
serverHandle调用ServeHttp来选择触发的HandleFunc。这里面会做一个判断,如果你传递了Handler,就调用你自己的,如果没传递就用DefaultServeMux默认的。到这整体流程就结束了。
过程是:
DefaultServeMux.ServeHttp执行的简单流程.
- h, _ := mux.Handler(r)
- h.ServeHTTP(w, r) //执行ServeHttp函数
查找路由,mux.handler函数里又调用了另外一个函数mux.handler(r.Host, r.URL.Path)。
还记得我们的ServeMux里的hosts标记吗?这个函数里会进行判断。
if mux.hosts {
h, pattern = mux.match(host + path)
}
if h == nil {
h, pattern = mux.match(path)
}
if h == nil {
h, pattern = NotFoundHandler(), ""
}
上面就是匹配查找pattern和handler的流程了
我们来总结一下。
首先调用Http.HandleFunc
按顺序做了几件事:
- 调用了DefaultServerMux的HandleFunc
- 调用了DefaultServerMux的Handle
- 往DefaultServeMux的map[string]muxEntry中增加对应的handler和路由规则
别忘记DefaultServerMux是ServeMux的实例。其实都是围绕ServeMux,muxEntry2个结构进行操作绑定。
其次调用http.ListenAndServe(":12345", nil)
按顺序做了几件事情:
- 实例化Server
- 调用Server的ListenAndServe()
- 调用net.Listen("tcp", addr)监听端口,启动for循环,等待accept请求
- 对每个请求实例化一个Conn,并且开启一个goroutine处理请求。
- 如:go c.serve()
- 读取请求的内容w, err := c.readRequest(),也就是response的取值过程。
- 调用serverHandler的ServeHTTP,ServeHTTP里会判断Server的属性里的header是否为空,如果没有设置handler,handler就设置为DefaultServeMux,反之用自己的(我们后面会做一个利用自己的Handler写服务器)
- 调用DefaultServeMux的ServeHttp( 因为我们没有自己的Handler,所以走默认的)
- 通过request选择匹配的handler:
A request匹配handler的方式。Hosts+pattern或pattern或notFound
B 如果有路由满足,返回这个handler
C 如果没有路由满足,返回NotFoundHandler
- 根据返回的handler进入到这个handler的ServeHTTP
大概流程就是这个样子,其实在net.Listen("tcp", addr)里也做了很多事,我们下期说道TCP服务器的时候回顾一下他做了哪些。
通过上面的解释大致明白了我们绑定触发的都是DefaultServeMux的Handler。现在我们来实现一个自己的Handler,这也是做框架的第一步。我们先来敲代码。
package main;
import (
"fmt"
"net/http"
"time"
)
type customHandlerstruct{
}
func(cb *customHandler) ServeHTTP( w http.ResponseWriter, r *http.Request ) {
fmt.Println("customHandler!!");
w.Write([]byte("customHandler!!"));
}
funcmain() {
varserver *http.Server = &http.Server{
Addr: ":8080",
Handler: &customHandler{},
ReadTimeout: 10 * time.Second,
WriteTimeout: 10 * time.Second,
MaxHeaderBytes: 1 <<20,
}
server.ListenAndServe();
select {
}
}
是不是很酷,我们可以利用自己的handler做一个智能的路由出来。
不过还是建议使用国内Golang语言框架beego,已开源。一款非常不错的框架,谢大维护的很用心,绝对良心框架,而且文档支持,社区也很不错。
最后附上一张最早设计框架时候的一个流程图(3年前)。大家可以简单看看,当然也可以尝试的动动手。起码收获很多。
(点击放大图像)
[1]: http://item.jd.com/11573034.html
[2]: https://github.com/astaxie/beego