GO语言基础三

33.net/http包

在Go中,搭建一个http server简单到令人难以置信。只需要引入net/http包,写几行代码,一个http服务器就可以正常运行并接受访问请求。下面就是Go最简单的http服务器:

package main

import (
	"fmt"
	"net/http"
)

func myfunc(w http.ResponseWriter, r *http.Request) {
	fmt.Fprintf(w, "hi")
}

func main() {
	http.HandleFunc("/", myfunc)
	http.ListenAndServe(":8080", nil)
}

Request

http Request请求是由客户端发出的消息, 用来使服务器执行动作.发出的消息包括起始行, Headers, Body。

在net/http包中,request.go文件定义了结构:type Request struct

// 利用指定的method, url以及可选的body返回一个新的请求.如果body参数实现了io.Closer接口,
//Request返回值的Body字段会被设置为body,
//并会被Client类型的Do、Post和PostForm方法以及Transport.RoundTrip方法关闭。 
func NewRequest(method, urlStr string, body io.Reader) (*Request, error) 

// 从b中读取和解析一个请求. 
func ReadRequest(b *bufio.Reader) (req *Request, err error)

// 给request添加cookie, AddCookie向请求中添加一个cookie.按照RFC 6265 
// section 5.4的规则, AddCookie不会添加超过一个Cookie头字段.
// 这表示所有的cookie都写在同一行, 用分号分隔(cookie内部用逗号分隔属性) 
func (r *Request) AddCookie(c *Cookie)

// 返回request中指定名name的cookie,如果没有发现,返回ErrNoCookie 
func (r *Request) Cookie(name string) (*Cookie, error)

// 返回该请求的所有cookies 
func (r *Request) Cookies() []*Cookie

// 利用提供的用户名和密码给http基本权限提供具有一定权限的header。
// 当使用http基本授权时,用户名和密码是不加密的 
func (r *Request) SetBasicAuth(username, password string)

// 如果在request中发送,该函数返回客户端的user-Agent
func (r *Request) UserAgent() string

// 对于指定格式的key,FormFile返回符合条件的第一个文件,如果有必要的话,该函数会调用ParseMultipartForm和ParseForm。 
func (r *Request) FormFile(key string) (multipart.File, *multipart.FileHeader, error)

// 返回key获取的队列中第一个值。在查询过程中post和put中的主题参数优先级
// 高于url中的value。为了访问相同key的多个值,调用ParseForm然后直接
// 检查RequestForm。 
func (r *Request) FormValue(key string) string

// 如果这是一个有多部分组成的post请求,该函数将会返回一个MIME 多部分reader,
// 否则的话将会返回一个nil和error。使用本函数代替ParseMultipartForm可以将请求body当做流stream来处理。 
func (r *Request) MultipartReader() (*multipart.Reader, error)

// 解析URL中的查询字符串,并将解析结果更新到r.Form字段。对于POST或PUT请求,ParseForm还会将body当作表单解析,
//并将结果既更新到r.PostForm也更新到r.Form。
//解析结果中,POST或PUT请求主体要优先于URL查询字符串(同名变量,主体的值在查询字符串的值前面)。
//如果请求的主体的大小没有被MaxBytesReader函数设定限制,其大小默认限制为开头10MB。
// ParseMultipartForm会自动调用ParseForm。重复调用本方法是无意义的。
func (r *Request) ParseForm() error 

// ParseMultipartForm将请求的主体作为multipart/form-data解析。
//请求的整个主体都会被解析,得到的文件记录最多maxMemery字节保存在内存, 其余部分保存在硬盘的temp文件里。
//如果必要,ParseMultipartForm会自行调用 ParseForm。重复调用本方法是无意义的。
func (r *Request) ParseMultipartForm(maxMemory int64) error 

// 返回post或者put请求body指定元素的第一个值,其中url中的参数被忽略。
func (r *Request) PostFormValue(key string) string 

// 检测在request中使用的http协议是否至少是major.minor 
func (r *Request) ProtoAtLeast(major,minor int) bool

// 如果request中有refer,那么refer返回相应的url。Referer在request中是拼错的,这个错误从http初期就已经存在了。
//该值也可以从Headermap中利用Header["Referer"]获取;
//在使用过程中利用Referer这个方法而不是map的形式的好处是在编译过程中可以检查方法的错误,而无法检查map中key的错误。
func (r *Request) Referer() string 

// Write方法以有线格式将HTTP/1.1请求写入w(用于将请求写入下层TCPConn等)。
//本方法会考虑请求的如下字段:Host URL Method (defaults to "GET")Header ContentLength TransferEncoding Body
//如果存在Body,ContentLength字段<= 0且TransferEncoding字段未显式设置为["identity"],
//Write方法会显式添加”Transfer-Encoding: chunked”到请求的头域。Body字段会在发送完请求后关闭。
func (r *Request) Write(w io.Writer) error 

//该函数与Write方法类似,但是该方法写的request是按照http代理的格式去写。
//尤其是,按照RFC 2616 Section 5.1.2,WriteProxy会使用绝对URI包括协议和主机名)来初始化请求的第1行(Request-URI行)。
//无论何种情况,WriteProxy都会使用r.Host或r.URL.Host设置Host头。
func (r *Request) WriteProxy(w io.Writer) error

Response

http Response响应是由http Server服务端发出的消息,用来响应http Client端发出的http Request请求。发出的消息包括起始行, Headers, Body。

// 注意是在response.go中定义的,而在server.go有一个type response struct,注意大小写。
//这个结构是体现在server端的功能。
type Response struct 

// ReadResponse从r读取并返回一个HTTP 回复。req参数是可选的,指定该回复对应的请求(即是对该请求的回复)。
//如果是nil,将假设请 求是GET请求。客户端必须在结束resp.Body的读取后关闭它。
//读取完毕并关闭后,客户端可以检查resp.Trailer字段获取回复的 trailer的键值对。
func ReadResponse(r *bufio.Reader, req *Request) (*Response, error)

// 解析cookie并返回在header中利用set-Cookie设定的cookie值。
func (r *Response) Cookies() []*Cookie 

// 返回response中Location的header值的url。
//如果该值存在的话,则对于请求问题可以解决相对重定向的问题,如果该值为nil,则返回ErrNOLocation。
func (r *Response) Location() (*url.URL,error) 

// 判定在response中使用的http协议是否至少是major.minor的形式。
func (r *Response) ProtoAtLeast(major, minor int) bool 

// 将response中信息按照线性格式写入w中。
func (r *Response) Write(w io.Writer) error

client

http Client客户端主要用来发送http Request请求给http Server服务端,比如以Do方法,Get方法以及Post或PostForm方法发送http Request请求。

// Client具有Do,Get,Head,Post以及PostForm等方法。 其中Do方法可以对Request进行一系列的设定,而其他的对request设定较少。
//如果Client使用默认的Client,则其中的Get,Head,
//Post以及PostForm方法相当于默认的http.Get,http.Post, http.Head以及http.PostForm函数。
type Client struct

// 利用GET方法对一个指定的URL进行请求,如果response是如下重定向中的一个代码,则Get之后将会调用重定向内容,最多10次重定向。 
// 301 (永久重定向,告诉客户端以后应该从新地址访问) 
// 302 (暂时性重定向,作为HTTP1.0的标准,PHP的默认Location重定向用到也是302),注:303和307其实是对302的细化。 
// 303 (对于Post请求,它表示请求已经被处理,客户端可以接着使用GET方法去请求Location里的URl) 
// 307 (临时重定向,对于Post请求,表示请求还没有被处理,客户端应该向Location里的URL重新发起Post请求)
func Get(url string) (resp *Response, err error) 

// 该函数功能见net中Head方法功能。该方法与默认的defaultClient中
// Head方法一致。
func Head(url string) (resp *Response, err error) 

// 该方法与默认的defaultClient中Post方法一致。
func Post(url string, bodyType string, body io.Reader) (resp *Response, err error)

// 该方法与默认的defaultClient中PostForm方法一致。 
func PostForm(url string, data url.Values) (resp *Response, err error)

// Do发送http请求并且返回一个http响应, 遵守client的策略, 如重定向, 
// cookies以及auth等.错误经常是由于策略引起的, 当err是nil时, resp
// 总会包含一个非nil的resp.body.当调用者读完resp.body之后应该关闭它, 
// 如果resp.body没有关闭, 则Client底层RoundTripper将无法重用存在的
// TCP连接去服务接下来的请求, 如果resp.body非nil, 则必须对其进行关闭.
// 通常来说, 经常使用Get, Post, 或者PostForm来替代Do. 
func (c *Client) Do(req *Request) (resp *Response, err error)

// 利用get方法请求指定的url.Get请求指定的页面信息,并返回实体主体。
func (c *Client) Get(url string) (resp *Response, err error) 

// 利用head方法请求指定的url,Head只返回页面的首部。
func (c *Client) Head(url string) (resp *Response, err error) 

// post方法请求指定的URl, 如果body也是一个io.Closer, 则在请求之后关闭它 
func (c *Client) Post(url string, bodyType string, body io.Reader) (resp *Response, err error)

// 利用post方法请求指定的url, 利用data的key和value作为请求体. 
func (c *Client) PostForm(url string, data url.Values) (resp *Response, err error)

http.NewRequest可以灵活的对http Request进行配置,然后再使用http.Client的Do方法发送这个http Request请求。注意:如果使用Post或者PostForm方法,是不能使用http.NewRequest配置请求的,只有Do方法可以定制http.NewRequest。

利用http.Client以及http.NewRequest就可以完整模拟一个http Request请求,包括自定义的http Request请求的头部信息。有了前面介绍的 http Request 请求、http Response 响应、http Client 客户端 三个部分,我们已经可以模拟各种http Request 请求的发送,接收http Response 响应了。

下面我们来模拟http Request请求,请求中附带有cookie信息,通过http.Client的Do方法发送这个请求。

先配置http.NewRequest,然后我们通过http.Client的Do方法来发送任何http Request请求。示例如下:

模拟任何http Request请求:

package main

import (
    "compress/gzip"
    "fmt"
    "io/ioutil"
    "net/http"
    "strconv"
)

func main() {
    // 简式声明一个http.Client空结构体指针对象
    client := &http.Client{}

    // 使用http.NewRequest构建http Request请求
    request, err := http.NewRequest("GET", "http://www.baidu.com", nil)
    if err != nil {
        fmt.Println(err)
    }

    // 使用http.Cookie结构体初始化一个cookie键值对
    cookie := &http.Cookie{Name: "userId", Value: strconv.Itoa(12345)}

    // 使用前面构建的request方法AddCookie往请求中添加cookie
    request.AddCookie(cookie)

    // 设置request的Header,具体可参考http协议
    request.Header.Set("Accept", "text/html, application/xhtml+xml, application/xml;q=0.9, */*;q=0.8")
    request.Header.Set("Accept-Charset", "GBK, utf-8;q=0.7, *;q=0.3")
    request.Header.Set("Accept-Encoding", "gzip, deflate, sdch")
    request.Header.Set("Accept-Language", "zh-CN, zh;q=0.8")
    request.Header.Set("Cache-Control", "max-age=0")
    request.Header.Set("Connection", "keep-alive")

    // 使用http.Client 来发送request,这里使用了Do方法。
    response, err := client.Do(request)
    if err != nil {
        fmt.Println(err)
        return
    }

    // 程序结束时关闭response.Body响应流
    defer response.Body.Close()

    // 接收到的http Response 状态值
    fmt.Println(response.StatusCode)
    // 200意味成功得到http Server返回的http Response信息
    if response.StatusCode == 200 { 
        // gzip.NewReader对压缩的返回信息解压(考虑网络传输量,http Server一般都会对响应压缩后再返回)
        body, err := gzip.NewReader(response.Body)
        if err != nil {
            fmt.Println(err)
        }
        defer body.Close()

        r, err := ioutil.ReadAll(body)
        if err != nil {
            fmt.Println(err)
        }
        // 打印出http Server返回的http Response信息
        fmt.Println(string(r))
    }
}

使用http.Get 发送http Get请求非常简单,在一般简单不需要对http.Request配置的场景下我们可以使用,只需要提供URL即可。

//发送一个http Get请求:
package main

import (
    "fmt"
    "io/ioutil"
    "net/http"
)

func main() {
    // var DefaultClient = &Client{}
    // func Get(url string) (resp *Response, err error) {
    // return DefaultClient.Get(url)
    // }
    /*
        func (c *Client) Get(url string) (resp *Response, err error) {
            req, err := NewRequest("GET", url, nil)
            if err != nil {
                return nil, err
            }
            return c.Do(req)
        }
    */

    // http.Get实际上是DefaultClient.Get(url),Get函数是高度封装的,只有一个参数url。
    // 对于一般的http Request是可以使用,但是不能定制Request
    response, err := http.Get("http://www.baidu.com")
    if err != nil {
        fmt.Println(err)
    }

    //程序在使用完回复后必须关闭回复的主体。
    defer response.Body.Close()

    body, _ := ioutil.ReadAll(response.Body)
    fmt.Println(string(body))
}

使用http.Post 发送http Post请求也非常简单,在一般简单不需要对http.Request配置的场景下我们可以使用。

package main

import (
    "fmt"
    "io/ioutil"
    "net/http"
    "strings"
)

func main() {
    // application/x-www-form-urlencoded:为POST的contentType
    // strings.NewReader("mobile=xxxxxxxxxx&isRemberPwd=1") 理解为传递的参数
    resp, err := http.Post("http://localhost:8080/login.do",
        "application/x-www-form-urlencoded", strings.NewReader("mobile=xxxxxxxxxx&isRemberPwd=1"))
    if err != nil {
        fmt.Println(err)
        return
    }
    defer resp.Body.Close()
    body, err := ioutil.ReadAll(resp.Body)
    if err != nil {
        fmt.Println(err)
        return
    }
    fmt.Println(string(body))
}
//发送一个http.Post请求:
package main

import (
    "fmt"
    "io/ioutil"
    "net/http"
    "strings"
)

func main() {
    // application/x-www-form-urlencoded:为POST的contentType
    // strings.NewReader("mobile=xxxxxxxxxx&isRemberPwd=1") 理解为传递的参数
    resp, err := http.Post("http://localhost:8080/login.do",
        "application/x-www-form-urlencoded", strings.NewReader("mobile=xxxxxxxxxx&isRemberPwd=1"))
    if err != nil {
        fmt.Println(err)
        return
    }
    defer resp.Body.Close()
    body, err := ioutil.ReadAll(resp.Body)
    if err != nil {
        fmt.Println(err)
        return
    }
    fmt.Println(string(body))
}

使用http.PostForm 发送http Request请求也非常简单,而且可以附带参数的键值对作为请求的body传递到服务端。

//发送一个http.PostForm请求:
package main

import (
    "fmt"
    "io/ioutil"
    "net/http"
    "net/url"
)

func main() {
    postParam := url.Values{
        "mobile":      {"xxxxxx"},
        "isRemberPwd": {"1"},
    }
    // 数据的键值会经过URL编码后作为请求的body传递
    resp, err := http.PostForm("http://localhost:8080/login.do", postParam)
    if err != nil {
        fmt.Println(err)
        return
    }
    defer resp.Body.Close()
    body, err := ioutil.ReadAll(resp.Body)
    if err != nil {
        fmt.Println(err)
        return
    }
    fmt.Println(string(body))
}

上面列举了四种http Client客户端发送http Request请求的方式,其中只有Do方法最灵活。

http.Client与http.NewRequest结合可以模拟任何http Request请求,方法是Do。像Get方法,Post方法和PostForm方法,http.NewRequest都是定制好的,所以使用方便但灵活性不够。不过好在有Do方法,我们可以更灵活来配置http.NewRequest。

func NewRequest(method, url string, body io.Reader) (*Request, error)

func (c *Client) Get(url string) (resp *Response, err error) {
    req, err := NewRequest("GET", url, nil)
......

func (c *Client) Post(url string, contentType string, body io.Reader) (resp *Response, err error) {
    req, err := NewRequest("POST", url, body)
......

http Server 服务端

http Server服务端用来接收并响应http Client端发出的http Request请求,是net/http包中非常重要和关键的一个功能。我们在Go语言中简单就能搭建HTTP服务器,就是因为它的存在。

在server.go文件中还定义了一个非常重要的接口:Handler,另外还有一个结构体response,这和http.Response结构体只有首字母大小写不一致,不过这个response 也是响应,只不过是专门用在服务端,和http.Response结构体是完全两回事。

type Handler interface {
    ServeHTTP(ResponseWriter, *Request)
}

type Server struct

// 监听TCP网络地址srv.Addr然后调用Serve来处理接下来连接的请求。如果srv.Addr是空的话,则使用“:http”。
func (srv *Server) ListenAndServe() error 

// ListenAndServeTLS监听srv.Addr确定的TCP地址,并且会调用Serve方法处理接收到的连接。必须提供证书文件和对应的私钥文件。
// 如果证书是由权威机构签发的,certFile参数必须是顺序串联的服务端证书和CA证书。
// 如果srv.Addr为空字符串,会使 用”:https”。
func (srv *Server) ListenAndServeTLS(certFile, keyFile string) error 

// 接受Listener l的连接,创建一个新的服务协程。该服务协程读取请求然后调用srv.Handler来应答。
//实际上就是实现了对某个端口进行监听,然后创建相应的连接。 
func (srv *Server) Serve(l net.Listener) error

// 该函数控制是否http的keep-alives能够使用,默认情况下,keep-alives总是可用的。
// 只有资源非常紧张的环境或者服务端在关闭进程中时,才应该关闭该功能。 
func (s *Server) SetKeepAlivesEnabled(v bool)

// 是一个http请求多路复用器,它将每一个请求的URL和一个注册模式的列表进行匹配,然后调用和URL最匹配的模式的处理器进行后续操作。
type ServeMux

// 初始化一个新的ServeMux 
func NewServeMux() *ServeMux

// 将handler注册为指定的模式,如果该模式已经有了handler,则会出错panic。
func (mux *ServeMux) Handle(pattern string, handler Handler) 

// 将handler注册为指定的模式 
func (mux *ServeMux) HandleFunc(pattern string, handler func(ResponseWriter, *Request))

// 根据指定的r.Method, r.Host以及r.RUL.Path返回一个用来处理给定请求的handler。
// 该函数总是返回一个非nil的 handler,如果path不是一个规范格式,则handler会重定向到其规范path。
// Handler总是返回匹配该请求的的已注册模式;在内建重定向处理器的情况下,pattern会在重定向后进行匹配。
// 如果没有已注册模式可以应用于该请求,本方法将返回一个内建的”404 page not found”处理器和一个空字符串模式。
func (mux *ServeMux) Handler(r *Request) (h Handler, pattern string) 

// 该函数用于将最接近请求url模式的handler分配给指定的请求。 
func (mux *ServeMux) ServeHTTP(w ResponseWriter, r *Request)

Handler接口应该算是server.go中最关键的接口了,如果我们仔细看这个文件的源代码,将会发现很多结构体实现了这个接口的ServeHTTP方法。

注意这个接口的注释:Handler响应HTTP请求。没错,最终我们的HTTP服务是通过实现ServeHTTP(ResponseWriter, *Request)来达到服务端接收客户端请求并响应。

理解 HTTP 构建的网络应用只要关注两个端—客户端(Client)和服务端(Server),两个端的交互来自 Client的 Request,以及Server端的Response。HTTP服务器,主要在于如何接受 Client端的 Request,Server端向Client端返回Response。

那这个过程是什么样的呢?要讲清楚这个过程,还需要回到开始的HTTP服务器程序。这里以前面我们了解到的http Request、http Response、http Client作为基础,并重点分析server.go源代码才能讲清楚:

func main() {
    http.HandleFunc("/", myfunc)
    http.ListenAndServe(":8080", nil)
}

以上两行代码,就成功启动了一个HTTP服务器。我们通过net/http 包源代码分析发现,调用Http.HandleFunc,按顺序做了几件事:

1.Http.HandleFunc调用了DefaultServeMux的HandleFunc

func HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
    DefaultServeMux.HandleFunc(pattern, handler)
}

2.DefaultServeMux.HandleFunc调用了DefaultServeMux的Handle,DefaultServeMux是一个ServeMux指针变量。而ServeMux 是Go语言中的Multiplexer(多路复用器),通过Handle匹配pattern 和我们定义的handler(其实就是http.HandlerFunc函数类型变量)。

var DefaultServeMux = &defaultServeMux
var defaultServeMux ServeMux

func (mux *ServeMux) HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
    mux.Handle(pattern, HandlerFunc(handler))
}

注意: 上面的方法命名Handle,HandleFunc和HandlerFunc,Handler(接口),他们很相似,容易混淆。记住Handle和HandleFunc和pattern 匹配有关,也即往DefaultServeMux的map[string]muxEntry中增加对应的handler和路由规则。

接着我们看看myfunc的声明和定义:

func myfunc(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "hi")
}

而type HandlerFunc func(ResponseWriter, *Request) 是一个函数类型,而我们定义的myfunc的函数签名刚好符合这个函数类型。

所以http.HandleFunc(“/”, myfunc),实际上是mux.Handle(“/”, HandlerFunc(myfunc))。

HandlerFunc(myfunc) 让myfunc成为了HandlerFunc类型,我们称myfunc为handler。而HandlerFunc类型是具有ServeHTTP方法的,而有了ServeHTTP方法也就是实现了Handler接口。

func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
    f(w, r) // 这相当于自身的调用
}

现在ServeMux和Handler都和我们的myfunc联系上了,myfunc是一个Handler接口变量也是HandlerFunc类型变量,接下来和结构体server有关了。

从http.ListenAndServe的源码可以看出,它创建了一个server对象,并调用server对象的ListenAndServe方法:

func ListenAndServe(addr string, handler Handler) error {
    server := &Server{Addr: addr, Handler: handler}
    return server.ListenAndServe()
}

而我们HTTP服务器中第二行代码:http.ListenAndServe(":8080", nil) 创建了一个server对象,并调用server对象的ListenAndServe方法,这里没有直接传递Handler,而是默认使用DefautServeMux作为multiplexer,myfunc是存在于handler和路由规则中的。

Server的ListenAndServe方法中,会初始化监听地址Addr,同时调用Listen方法设置监听。

for {
    rw, e := l.Accept()
    ...
    c := srv.newConn(rw)
	c.setState(c.rwc, StateNew) 
	go c.serve(ctx)
}

监听开启之后,一旦客户端请求过来,Go就开启一个协程go c.serve(ctx)处理请求,主要逻辑都在serve方法之中。

func (c *conn) serve(ctx context.Context),这个方法很长,里面主要的一句:serverHandler{c.server}.ServeHTTP(w, w.req)。其中w由w, err := c.readRequest(ctx)得到,因为有传递context。

还是来看源代码:

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{}
	}
	handler.ServeHTTP(rw, req)
}

从http.ListenAndServe(“:8080”, nil)开始,handler是nil,所以最后实际ServeHTTP方法是DefaultServeMux.ServeHTTP(rw, req)。

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
    }
    h, _ := mux.Handler(r)
    h.ServeHTTP(w, r)
}

通过func (mux ServeMux) Handler(r Request) (h Handler, pattern string),我们得到Handler h,然后执行h.ServeHTTP(w, r)方法,也就是执行我们的myfunc函数(别忘了myfunc是HandlerFunc类型,而他的ServeHTTP(w, r)方法这里其实就是自己调用自己),把response写到http.ResponseWriter对象返回给客户端,fmt.Fprintf(w, “hi”),我们在客户端会接收到hi 。至此整个HTTP服务执行完成。
总结下,HTTP服务整个过程大概是这样:

Request -> ServeMux(Multiplexer) -> handler-> Response

我们再看下面代码:

http.ListenAndServe(":8080", nil)
func ListenAndServe(addr string, handler Handler) error {
    server := &Server{Addr: addr, Handler: handler}
    return server.ListenAndServe()
}

上面代码实际上就是server.ListenAndServe()执行的实际效果,只不过简单声明了一个结构体Server{Addr: addr, Handler: handler}实例。如果我们声明一个Server实例,完全可以达到深度自定义 http.Server的目的:

package main

import (
	"fmt"
	"net/http"
)

func myfunc(w http.ResponseWriter, r *http.Request) {
	fmt.Fprintf(w, "hi")
}

func main() {
	server := http.Server{
		Addr:         ":8080",
		ReadTimeout:  0,
		WriteTimeout: 0,
	}
	mux := http.NewServeMux()
	server.Handler = mux

	mux.HandleFunc("/", myfunc)
	server.ListenAndServe()
}

在前面pprof 包的内容中我们也用了本章开头这段代码,当我们访问http://localhost:8080/debug/pprof/ 时可以看到对应的性能分析报告。 因为我们这样导入 _“net/http/pprof” 包时,在文件 pprof.go 文件中init 函数已经定义好了handler:

func init() {
    http.HandleFunc("/debug/pprof/", Index)
    http.HandleFunc("/debug/pprof/cmdline", Cmdline)
    http.HandleFunc("/debug/pprof/profile", Profile)
    http.HandleFunc("/debug/pprof/symbol", Symbol)
    http.HandleFunc("/debug/pprof/trace", Trace)
}

所以,我们就可以通过浏览器访问上面地址来看到报告。现在再来看这些代码,我们就明白怎么回事了!

自定义处理器(Custom Handlers)

标准库http提供了Handler接口,用于开发者实现自己的handler。只要实现接口的ServeHTTP方法即可。

package main

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

type timeHandler struct {
    format string
}

func (th *timeHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    tm := time.Now().Format(th.format)
    w.Write([]byte("The time is: " + tm))
}

func main() {
    mux := http.NewServeMux()
    th := &timeHandler{format: time.RFC1123}
    mux.Handle("/time", th)
    log.Println("Listening...")
    http.ListenAndServe(":3000", mux)
}

我们知道,NewServeMux可以创建一个ServeMux实例,ServeMux同时也实现了ServeHTTP方法,因此代码中的mux也是一种handler。把它当成参数传给http.ListenAndServe方法,后者会把mux传给Server实例。因为指定了handler,因此整个http服务就不再是DefaultServeMux,而是mux,无论是在注册路由还是提供请求服务的时候。

任何有 func(http.ResponseWriter,*http.Request) 签名的函数都能转化为一个 HandlerFunc 类型。这很有用,因为 HandlerFunc 对象内置了 ServeHTTP 方法,后者可以聪明又方便的调用我们最初提供的函数内容。

将函数作为处理器

package main

import (
	"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 type
	th := http.HandlerFunc(timeHandler)
	// And add it to the ServeMux
	mux.Handle("/time", th)
	log.Println("Listening...")
	http.ListenAndServe(":3000", mux)
}

点击本地连接 ,输出的结果:

The time is: Thu, 18 Apr 2024 10:04:39 CST

创建新的server:

package main

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

func index(w http.ResponseWriter, r *http.Request) {
	w.Header().Set("Content-Type", "text/html")

	html := `<doctype html>
		<html>
			<head>
			  <title>Hello World</title>
			</head>
			<body>
				<p>Welcome</p>
			</body>
		</html>`
	fmt.Fprintln(w, html)
}

func main() {
	http.HandleFunc("/", index)

	server := &http.Server{
		Addr:         ":8000",
		ReadTimeout:  60 * time.Second,
		WriteTimeout: 60 * time.Second,
	}
	log.Println("127.0.0.1:8000 Listening....")
	server.ListenAndServe()
}

中间件Middleware

所谓中间件,就是连接上下级不同功能的函数或者软件,通常进行一些包裹函数的行为,为被包裹函数提供添加一些功能或行为。前文的HandleFunc就能把签名为 func(w http.ResponseWriter, r *http.Reqeust)的函数包裹成handler。这个函数也算是中间件。

Go的http中间件很简单,只要实现一个函数签名为func(http.Handler) http.Handler的函数即可。http.Handler是一个接口,接口方法我们熟悉的为serveHTTP。返回也是一个handler。因为Go中的函数也可以当成变量传递或者或者返回,因此也可以在中间件函数中传递定义好的函数,只要这个函数是一个handler即可,即实现或者被handlerFunc包裹成为handler处理器。

package main

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

func index(w http.ResponseWriter, r *http.Request) {
	w.Header().Set("Content-Type", "text/html")

	html := `<doctype html>
        <html>
        <head>
          <title>Hello World</title>
        </head>
        <body>
        <p>
          Welcome
        </p>
        </body>
</html>`
	fmt.Fprintln(w, html)
}

func middlewareHandler(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		// 执行handler之前的逻辑
		next.ServeHTTP(w, r)
		// 执行完毕handler后的逻辑
	})
}

func loggingHandler(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		start := time.Now()
		log.Printf("Started %s %s", r.Method, r.URL.Path)
		next.ServeHTTP(w, r)
		log.Printf("Completed %s in %v", r.URL.Path, time.Since(start))
	})
}

func main() {
	http.Handle("/", loggingHandler(http.HandlerFunc(index)))
	http.ListenAndServe(":8000", nil)
}

34. context包

在Go中,每个请求的request在单独的goroutine中进行,处理一个request也可能涉及多个goroutine之间的交互。一个请求衍生出的各个 goroutine 之间需要满足一定的约束关系,以实现一些诸如有效期,中止routine树,传递请求全局变量之类的功能。于是Go为我们提供一个解决方案,标准context包。使用context可以使开发者方便的在这些goroutine之间传递request相关的数据、取消goroutine的signal或截止时间等。

每个goroutine在执行之前,都要先知道程序当前的执行状态,通常将这些执行状态封装在一个Context变量中,传递给要执行的goroutine中。上下文则几乎已经成为传递与请求同生存周期变量的标准方法。在网络编程下,当接收到一个网络请求Request,处理Request时,我们可能需要开启不同的goroutine来获取数据与逻辑处理,即一个请求Request,会在多个goroutine中处理。而这些goroutine可能需要共享Request的一些信息;同时当Request被取消或者超时的时候,所有从这个Request创建的所有goroutine也应该被结束。

context包不仅实现了在程序单元之间共享状态变量的方法,同时能通过简单的方法,使我们在被调用程序单元的外部,通过设置ctx变量值,将过期或撤销这些信号传递给被调用的程序单元。若存在A调用B的API,B再调用C的API,若A调用B取消,那也要取消B调用C,通过在A, B, C的API调用之间传递Context,以及判断其状态。

// Context包含过期,取消信号,request值传递等,方法在多个goroutine中协程安全
type Context interface {
    // Done 方法在context被取消或者超时返回一个close的channel
    Done() <-chan struct{}

    Err() error

    // Deadline 返回context超时时间
    Deadline() (deadline time.Time, ok bool)

    // Value 返回context相关key对应的值
    Value(key interface{}) interface{}
}
  • Deadline会返回一个超时时间,goroutine获得了超时时间后,例如可以对某些io操作设定超时时间。
  • Done方法返回一个通道(channel),当Context被撤销或过期时,该通道关闭,即它是一个表示Context是否已关闭的信号。
  • 当Done通道关闭后,Err方法表明Context被撤的原因。
  • Value可以让goroutine共享一些数据,当然获得数据是协程安全的。但使用这些数据的时候要注意同步,比如返回了一个map,而这个map的读写则要加锁。

goroutine的创建和调用关系总是像层层调用进行的,就像人的辈分一样,而更靠顶部的goroutine应有办法主动关闭其下属的goroutine的执行(不然程序可能就失控了)。为了实现这种关系,Context结构也应该像一棵树,叶子节点须总是由根节点衍生出来的。

要创建Context树,第一步就是要得到根节点,context.Background函数的返回值就是根节点:func Background() Context

该函数返回空的Context,该Context一般由接收请求的第一个goroutine创建,是与进入请求对应的Context根节点,它不能被取消、没有值、也没有过期时间。它常常作为处理Request的顶层context存在。

有了根节点,又该怎么创建其它的子节点,孙节点呢?context包为我们提供了多个函数来创建他们:

func WithCancel(parent Context) (ctx Context, cancel CancelFunc)
func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc)
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
func WithValue(parent Context, key interface{}, val interface{}) Context

函数都接收一个Context类型的参数parent,并返回一个Context类型的值,这样就层层创建出不同的节点。子节点是从复制父节点得到的,并且根据接收参数设定子节点的一些状态值,接着就可以将子节点传递给下层的goroutine了。

再回到之前的问题:该怎么通过Context传递改变后的状态呢?使用Context的goroutine无法取消某个操作,其实这也是符合常理的,因为这些goroutine是被某个父goroutine创建的,而理应只有父goroutine可以取消操作。在父goroutine中可以通过WithCancel方法获得一个cancel方法,从而获得cancel的权利。

第一个WithCancel函数,它是将父节点复制到子节点,并且还返回一个额外的CancelFunc函数类型变量,该函数类型的定义为:

type CancelFunc func()

调用CancelFunc对象将撤销对应的Context对象,这就是主动撤销Context的方法。在父节点的Context所对应的环境中,通过WithCancel函数不仅可创建子节点的Context,同时也获得了该节点Context的控制权,一旦执行该函数,则该节点Context就结束了,则子节点需要类似如下代码来判断是否已结束,并退出该goroutine:

select {
    case <-cxt.Done():
        // do some clean...
}

WithDeadline函数的作用也差不多,它返回的Context类型值同样是parent的副本,但其过期时间由deadline和parent的过期时间共同决定。这是因为父节点过期时,其所有的子孙节点必须同时关闭;反之,返回的父节点的过期时间则为deadline。

WithTimeout函数与WithDeadline类似,不过它传入的是从现在开始Context剩余的生命时长。他们都同样也都返回了所创建的子Context的控制权,一个CancelFunc类型的函数变量。

当顶层的Request请求函数结束后,我们就可以cancel掉某个context,从而再在对应协程中根据cxt.Done()来决定是否结束。

WithValue函数,它返回parent的一个副本,调用该副本的Value(key)方法将得到对应key的值。这样我们不光将根节点原有的值保留了,还可以在子孙节点中加入了新的值,注意若存在Key相同,则会被覆盖。

Context对象的生存周期一般仅为一个请求的处理周期。即针对一个请求创建一个Context变量(它为Context树结构的根);在请求处理结束后,撤销此ctx变量,释放资源。

每次创建一个协程时,可以将原有的Context传递给这个子协程,或者新创建一个子Context传递给这个协程。

Context能灵活地存储不同类型、不同数目的值,并且使多个协程安全地读写其中的值。

当通过父Context对象创建子Context对象时,即可获得子Context的一个撤销函数,这样父Context对象的创建环境就获得了对子Context的撤销权。

注意:使用时遵循context规则

1.不要把Context存在一个结构体当中,显式地传入函数。Context变量需要作为第一个参数使用,一般命名为ctx。
2.即使方法允许,也不要传入一个nil的Context,如果你不确定你要用什么Context的时候传一个context.TODO。
3.使用context的Value相关方法只应该用于在程序和接口中传递的和请求相关的元数据,不要用它来传递一些可选的参数。
4.样的Context可以用来传递到不同的goroutine中,Context在多个goroutine中是安全的。

在子Context被传递到的goroutine中,应该对该子Context的Done通道(channel)进行监控,一旦该通道被关闭(即上层运行环境撤销了本goroutine的执行),应主动终止对当前请求信息的处理,释放资源并返回。

context应用

前面介绍协程(goroutine)时,对协程的管理和控制我们并没有进行讨论。到目前我们已经清楚认识了channel、context以及sync包,通过这三者,我们完全可以达到完美控制协程运行的目的。

通过go关键字让我们很容易启动一个协程,但难的是很好的管理和控制他们的运行。有几种方法我们可以根据场景使用:

(1)使用sync.WaitGroup,它用于线程总同步,会等待一组线程集合完成,才会继续向下执行,这对监控所有子协程全部完成情况特别有用,但要控制某个协程就无能为力了;
(2)使用channel来传递消息,一个协程来发送channel信号,另一个协程通过select来得到channel信息,这种方式可以满足协程之间的通信,来控制协程运行。但如果协程数量达到一定程度,就很难把控了;或者这两个协程还和其他协程也有类似通信,比如A与B,B与C,如果A发信号B退出了,C有可能等不到B的channel信号而被遗忘;
(3)使用Context来传递消息,Context是层层传递机制,根节点完全控制了子节点,根节点(父节点)可以根据需要选择自动还是手动结束子节点。而每层节点所在的协程就可以根据信息来决定下一步的操作。

下面我们来看看具体使用Context怎么来控制协程的运行:

这里用Context同时控制2个协程,这2个协程都可以收到cancel()发出的信号,甚至doNothing这样不结束协程可反复接收cancel信息。

package main

import (
	"context"
	"log"
	"os"
	"time"
)

var logs *log.Logger

func doClearn(ctx context.Context) {
	// for 循环来每1秒work一下,判断ctx是否被取消了,如果是就退出
	for {
		time.Sleep(1 * time.Second)
		select {
		case <-ctx.Done():
			logs.Println("doClearn:收到Cancel,做好收尾工作后马上退出。")
			return
		default:
			logs.Println("doClearn:每隔1秒观察信号,继续观察...")
		}
	}
}

func doNothing(ctx context.Context) {
	for {
		time.Sleep(3 * time.Second)
		select {
		case <-ctx.Done():
			logs.Println("doNothing:收到Cancel,但不退出......")

			// 注释return可以观察到,ctx.Done()信号是可以一直接收到的,return不注释意味退出协程
			//return
		default:
			logs.Println("doNothing:每隔3秒观察信号,一直运行")
		}
	}
}

func main() {
	logs = log.New(os.Stdout, "", log.Ltime)

	// 新建一个ctx
	ctx, cancel := context.WithCancel(context.Background())

	// 传递ctx
	go doClearn(ctx)
	go doNothing(ctx)

	// 主程序阻塞20秒,留给协程来演示
	time.Sleep(20 * time.Second)
	logs.Println("cancel")

	// 调用cancel:context.WithCancel 返回的CancelFunc
	cancel()

	// 发出cancel 命令后,主程序阻塞10秒,再看协程的运行情况
	time.Sleep(10 * time.Second)
}

程序输出:

......
cancel
doClearn:收到Cancel,做好收尾工作后马上退出。
doNothing:收到Cancel,但不退出......
doNothing:收到Cancel,但不退出......
doNothing:收到Cancel,但不退出......

这里用Context嵌套控制3个协程,A,B,C。在主程序发出cancel信号后,每个协程都能接收根Context的Done()信号而退出。

package main

import (
	"context"
	"fmt"
	"time"
)

func A(ctx context.Context) int {
	ctx = context.WithValue(ctx, "AFunction", "Great")

	go B(ctx)

	select {
	// 监测自己上层的ctx ...
	case <-ctx.Done():
		fmt.Println("A Done")
		return -1
	}
	return 1
}

func B(ctx context.Context) int {
	fmt.Println("A value in B:", ctx.Value("AFunction"))
	ctx = context.WithValue(ctx, "BFunction", 999)

	go C(ctx)

	select {
	// 监测自己上层的ctx ...
	case <-ctx.Done():
		fmt.Println("B Done")
		return -2
	}
	return 2
}

func C(ctx context.Context) int {
	fmt.Println("B value in C:", ctx.Value("AFunction"))
	fmt.Println("B value in C:", ctx.Value("BFunction"))
	select {
	// 结束时候做点什么 ...
	case <-ctx.Done():
		fmt.Println("C Done")
		return -3
	}
	return 3
}

func main() {
	// 自动取消(定时取消)
	ctx, _ := context.WithTimeout(context.Background(), 10 * time.Second)

	fmt.Println("A 执行完成,返回:", A(ctx))
	select {
	case <-ctx.Done():
		fmt.Println("context Done")
		break
	}

	time.Sleep(20 * time.Second)
}

最后我们看看Context在http 是怎么传递的:

package main

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

// ContextMiddle是http服务中间件,统一读取通行cookie并使用ctx传递
func ContextMiddle(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        cookie, _ := r.Cookie("Check")
        if cookie != nil {
            ctx := context.WithValue(r.Context(), "Check", cookie.Value)
            next.ServeHTTP(w, r.WithContext(ctx))
        } else {
            next.ServeHTTP(w, r)
        }
    })
}

// 强制设置通行cookie
func CheckHandler(w http.ResponseWriter, r *http.Request) {
    expitation := time.Now().Add(24 * time.Hour)
    cookie := http.Cookie{Name: "Check", Value: "42", Expires: expitation}
    http.SetCookie(w, &cookie)
}

func indexHandler(w http.ResponseWriter, r *http.Request) {
    // 通过取中间件传过来的context值来判断是否放行通过
    if chk := r.Context().Value("Check"); chk == "42" {
        w.WriteHeader(http.StatusOK)
        w.Write([]byte("Let's go! \n"))
    } else {
        w.WriteHeader(http.StatusNotFound)
        w.Write([]byte("No Pass!"))
    }
}

func main() {
    mux := http.NewServeMux()

    mux.HandleFunc("/", indexHandler)

    // 人为设置通行cookie
    mux.HandleFunc("/chk", CheckHandler)

    ctxMux := ContextMiddle(mux)
    http.ListenAndServe(":8080", ctxMux)
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值