go http server读书笔记

package main

import (
"fmt"
"net/http"
"time"
)

type customHandler struct {
}

func (cb *customHandler) chenxun(w http.ResponseWriter, r *http.Request) {
fmt.Println("customHandler!!")
w.Write([]byte("customHandler!!"))
}

func main() {
var server *http.Server = &http.Server{
Addr: ":8080",
Handler: &customHandler{},
ReadTimeout: 10 * time.Second,
WriteTimeout: 10 * time.Second,
MaxHeaderBytes: 1 << 20,
}
server.ListenAndServe()
select {}
}


HTTP服务器

接下来就是重点了,我们的HTTP服务器,这个大家都不陌生,HTTP是最常用的方式之一,通用性很强,跨团队协作上也比较受到推荐,排查问题也相对来说简单。

我们接下来以3种方式来展现Golang的HTTP服务器的简洁和强大。

  1. 写一个简单的HTTP服务器
  2. 写一个稍微复杂带路由的HTTP服务器
  3. 分析源码,然后实现一个自定义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这个路由了。

问题:

  1. 什么原因导致的路由失效
  2. 如何解决这种问题,做一个可以用Controller来控制的路由

问题1:

我们在源码阅读分析的时候会解决。

问题2:

我们可以设定一个控制器Handle,它有2个action,我们的执行handle/res对应的结果是调用Handle的控制器下的res方法。这样是不是很酷。

来我们先上代码:

静态目录:

  1. css
  2. js
  3. 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这个绑定路由的方法,上面一直没解释有啥区别。现在我们来看一下。

// HandleFunc registers the handler function for the given pattern
// in the DefaultServeMux.
// The documentation for ServeMux explains how patterns are matched.
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上呢?

// DefaultServeMux is the default ServeMux used by Serve.
varDefaultServeMux = NewServeMux()

因为DefaultServeMux就是ServeMux的实例对象。导致我们就把路由和执行方法绑注册好了。不过大家请想下handle/res的问题?

从上面的分析我们要知道几个重要的概念。

HandlerFunc

// The HandlerFunc type is an adapter to allow the use of
// ordinary functions as HTTP handlers. If f is a function
// with the appropriate signature, HandlerFunc(f) is a
// Handler object that calls f.
type HandlerFuncfunc(ResponseWriter, *Request)

// ServeHTTP calls f(w, r).
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)//这行输出的结果是serve1
a.Serve()//这行输出的结果是serve2
}

上面结果是serve1,serve2

Golang的源码里用了很多HandleFunc这个适配器。

接下来我们看第二个,ServeMux结构,最终我们是绑定它,也是通过它来解析。

type ServeMuxstruct{
mu sync.RWMutex//读写锁
m map[string]muxEntry//路由map,pattern->HandleFunc
hosts bool//是否包含hosts
}

type muxEntrystruct{
explicit bool//是否精确匹配,这个在Golang实现里是ture
h Handler //这个路由表达式对应哪个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 // 所有请求需要调用的Handler(
ReadTimeouttime.Duration // 读的最大Timeout时间
WriteTimeouttime.Duration // 写的最大Timeout时间
MaxHeaderBytesint // 请求头的最大长度
TLSConfig *tls.Config // 配置TLS
... //结构太长我省略些,感兴趣大家自己看下
}

Server提供的方法有:

func(srv *Server) Serve(l net.Listener) error   //对某个端口进行监听,里面就是调用for进行accept的处理了
func(srv *Server) ListenAndServe() error //开启http server服务
func(srv *Server) ListenAndServeTLS(certFile, keyFile string) error //开启https server服务

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

// ResponseWriter的作用是被Handler调用来组装返回的Response的
type ResponseWriter interface {
// 这个方法返回Response返回的Header供读写
Header() Header

// 这个方法写Response的Body
Write([]byte) (int, error)

// 这个方法根据HTTP State Code来写Response的Header

WriteHeader(int)
}

// Flusher的作用是被Handler调用来将写缓存中的数据推给客户端
type Flusher interface {
// 刷新缓冲区
Flush()
}

// Hijacker的作用是被Handler调用来关闭连接的
type Hijacker interface {
Hijack() (net.Conn, *bufio.ReadWriter, error)

}

而我们这里的w也就是ResponseWriter了。而调用了下面这句方法,就可以利用它的Write方法输出内容给客户端了。

serverHandler{c.server}.ServeHTTP(w, w.req)

这句就是触发路由绑定的方法了。要看这个触发器我们还要知道几个接口。

具体我们先看下如何实现这三个接口的,因为后面我们要看触发路由执行逻辑片段。实现这三个接口的结构是response

response
// response包含了所有server端的HTTP返回信息
type response struct {
conn *conn // 保存此次HTTP连接的信息
req *Request // 对应请求信息
chunking bool // 是否使用chunk
wroteHeaderbool // header是否已经执行过写操作
wroteContinuebool // 100 Continue response was written
header Header // 返回的HTTP的Header
written int64 // Body的字节数
contentLength int64 // Content长度
status int // HTTP状态
needSniffbool
//是否需要使用sniff。(当没有设置Content-Type的时候,开启sniff能根据HTTP body来确定Content-Type)
closeAfterReplybool
//是否保持长链接。如果客户端发送的请求中connection有keep-alive,这个字段就设置为false。
requestBodyLimitHitbool
//是否requestBody太大了(当requestBody太大的时候,response是会返回411状态的,并把连接关闭)
}

在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)
// either dataB or dataS is non-zero.
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执行的简单流程.

  1. h, _ := mux.Handler(r)
  2. h.ServeHTTP(w, r)   //执行ServeHttp函数

查找路由,mux.handler函数里又调用了另外一个函数mux.handler(r.Host, r.URL.Path)。

还记得我们的ServeMux里的hosts标记吗?这个函数里会进行判断。

// Host-specific pattern takes precedence over generic ones
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

按顺序做了几件事:

  1. 调用了DefaultServerMux的HandleFunc
  2. 调用了DefaultServerMux的Handle
  3. 往DefaultServeMux的map[string]muxEntry中增加对应的handler和路由规则

别忘记DefaultServerMux是ServeMux的实例。其实都是围绕ServeMux,muxEntry2个结构进行操作绑定。

其次调用http.ListenAndServe(":12345", nil)

按顺序做了几件事情:

  1. 实例化Server
  2. 调用Server的ListenAndServe()
  3. 调用net.Listen("tcp", addr)监听端口,启动for循环,等待accept请求
  4. 对每个请求实例化一个Conn,并且开启一个goroutine处理请求。
  5. 如:go c.serve()
  6. 读取请求的内容w, err := c.readRequest(),也就是response的取值过程。
  7. 调用serverHandler的ServeHTTP,ServeHTTP里会判断Server的属性里的header是否为空,如果没有设置handler,handler就设置为DefaultServeMux,反之用自己的(我们后面会做一个利用自己的Handler写服务器)
  8. 调用DefaultServeMux的ServeHttp( 因为我们没有自己的Handler,所以走默认的)
  9. A request匹配handler的方式。Hosts+pattern或pattern或notFound
    B 如果有路由满足,返回这个handler
    C 如果没有路由满足,返回NotFoundHandler
  10. 根据返回的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 {
}
}