掌握Go语言中的HTTP编程

发表时间: 2021-02-21 10:30

作者:JunChow520

出处:
https://www.jianshu.com/p/844ba023eac7

由于Web服务是HTTP协议的一个服务,Golang提供完善的 net/http 包,通过 net/http 包可以很方便地搭建一个可以运行的Web服务,同时 net/http 包能够简单地对Web的路由、静态资源、模板、Cookie等数据进行设置和操作。

net/http

Golang标准库内置 net/http 包涵盖了HTTP客户端和服务端的具体实现,使用 net/http 包可方便地编写HTTP客户端和服务端的程序。

Golang为了实现高并发和高性能,使用 goroutine 来处理连接的读写事件,以保证每个请求独立且互不阻塞以高效地响应网络事件。

c,err := srv.newConn(rw)if err!=nil {  continue}go c.serve()

Golang在等待客户端请求对连接处理时,客户端每次请求都会创建一个 Conn 连接对象,这个 Conn 连接对象中保存了本次请求的信息,然后再传递到对应的处理器,处理器可以方便地读取到相应地HTTP头信息,如此这般保证了每次请求的独立性。

服务端

基于HTTP构建的服务标准模型包括客户端和服务端,HTTP请求从客户端发出,服务端接收到请求后进行处理,然后将响应返回给客户端。HTTP服务器核心工作是如何接收来自客户端的请求,并向客户端返回响应。

HTTP服务器处理流程

当HTTP服务器接收到客户端请求时,首先会进入路由模块,路由又称为服务复用器(Multiplexer),路由的工作在于请求找到对应的处理器(Handler),处理器对接收到的请求进行对应处理后,构建响应并返回给客户端。

client -> Request -> Multiplexer(router)->handler ->Response -> client

运行流程

运行流程

  1. 创建Listen Socket监听指定端口,等待客户端请求到来。
func ListenAndServe(addr string, handler Handler) error {    server := &Server{Addr: addr, Handler: handler}    return server.ListenAndServe()}

首先初始化 Server 对象,然后调用其 ListenAndServe() 方法。

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)}

调用 Server 实例的 ListenAndServe() 方法会调用底层的 net.Listen("tcp", addr) 方法,即基于TCP协议创建Listen Socket,通过传入的主机地址和端口号,在指定端口上监听客户端请求。

  1. Listen Socket接受客户端请求并建立连接以获取Client Socket,通过Client Socket与客户端通信。

创建Listen Socket成功后会调用 Server 实例的 Serve(net.Listener) 方法,该方法用于接受并处理客户端请求。

Serve(net.Listener) 方法内部会开启一个 for 死循环,在循环体内通过 net.Listener (即Listen Socket)实例的 Accept 方法来接受客户端请求,接收到请求后根据请求会创建 net.Conn 连接实例(即Client Socket)。为了处理并发请求,会单独为每个连接实例开启一个 goroutine 去服务,请求的具体逻辑处理都会在 serve() 方法内完成。

  1. 处理客户端请求并返回响应

客户端请求的处理集中在 conn 连接实例的 serve() 方法内, serve() 方法主要实现将HTTP请求分配给指定的处理器函数来进行处理。

首先从Client Socket中读取HTTP请求的协议头,判断请求方法若是POST则需读取客户端提交的数据,然后交给对应的Handler来处理请求,Handler处理完毕后准备后客户端所需数据,再通过Client Socket写给客户端。

连接实例通过 readRequest() 方法解析请求,然后再通过 serverHandler{c.server}.ServeHTTP(w, w.req) 中的 ServeHTTP() 方法获取请求对应的处理器。

创建服务

创建HTTP服务需经过两个阶段,首先需注册路由即提供URL模式和Handler处理函数的映射,然后是实例化 Server 对象并开启对客户端的监听。

例如:使用 net/http 包搭建Web服务

mux:= http.NewServeMux()mux.Handle("/", http.RedirectHandler("http://www.baidu.com", 307))server := &http.Server{Addr: ":3000", Handler: mux}server.ListenAndServe()
  1. 注册路由,即注册一个到 ServeMux 的处理器函数。

注册路由即提供URL模式和Handler处理函数的映射

http.HandleFunc("/", func(rw http.ResponseWriter, rq *http.Request) {    rw.Write([]byte(time.Now().Format(time.RFC3339)))})http.ListenAndServe(":3000", nil)

net/http 包提供了注册路由的API, http.HandleFunc 方法默认会采用 DefaultServeMux 作为服务复用器, DefaultServeMux 实际是 ServeMux 的一个实例。

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

net/http 包也提供了 NewServeMux() 方法来创建一个自定义的 ServeMux 实例,默认则创建一个 DefaultServeMux

mux := http.NewServeMux()mux.Handle("/", http.RedirectHandler("http://www.baidu.com", 307))http.ListenAndServe(":3000", mux)
  1. 监听启动,设置监听的TCP地址并启动服务

监听启动实际上是实例化一个Server对象,并开启对客户端的监听。

func http.ListenAndServe(addr string, handler Handler) error

net/http 提供的 http.ListenAndServe(addr string, handler Handler) 用于在指定的TCP网络地址进行监听,然后调用服务端处理程序来处理传入的请求。

http.HandleFunc("/", func(rw http.ResponseWriter, rq *http.Request) {    n, err := rw.Write([]byte(rq.RemoteAddr))    if err != nil || n <= 0 {        panic(err)    }})err := http.ListenAndServe(":3000", nil)if err != nil {    panic(err)}

处理器默认为 nil 表示服务端会调用包变量 http.DefaultServeMux 作为默认处理器,服务端编写的业务逻辑处理程序 http.Handler()http.HandleFunc() 会默认注入 http.DefaultServeMux 中。

若不想采用默认的的 http.DefaultServeMux 可使用 net/http 包中提供的 NewServeMux() 创建自定义的 ServeMux

func NewServeMux() *ServeMux

http.ListenAndSerTLS() 方法用于处理HTTPS请求

func http.ListenAndServeTLS(addr string, certFile string, keyFile string, handler Handler) error

注册路由

理解Golang中的HTTP服务最重要的是理解Multiplexer多路转接器和Handler处理器,Multiplexer基于 ServeMux 结构同时实现了 Handler 接口。

  • ServeMux 本质上是一个HTTP请求路由器,又称为多路转接器(Multiplexor),它会将接收到的请求与一组预先定义的URL路径列表做对比,然后匹配路径时调用关联的处理器(Handler)。
  • Handler 处理器负责输出HTTP响应的头和正文,任何满足 http.Handler 接口的对象都可以作为一个处理器。

多路转接器

HTTP请求的多路转接器(即路由)会负责将每个请求的URL与注册模式列表进行匹配,并调用和URL最佳匹配模式的处理器。多路转换器内部使用一个 map 映射来保存所有处理器。

type ServeMux struct {    mu    sync.RWMutex//读写互斥锁,并发请求需锁机制    m     map[string]muxEntry//路由规则,一个路由表达式对应一个复用器实体    es    []muxEntry // slice of entries sorted from longest to shortest.    hosts bool       // 是否在任意规则中携带主机信息}

虽然 ServeMux 也实现了 ServerHTTP 方法算得上是一个处理器,但 ServeMuxServeHTTP 方法并非用来处理请求和响应,而是用来查找注册路由对应的处理器。

DefaultServeMux 是默认的 ServeMux ,随着 net/http 包初始化而被自动初始化。

快捷函数

net/http 包提供了一组快捷函数 http.Handlehttp.HandleFunc 来配置 DefaultServeMux ,快捷函数会将处理器注册到 DefaultServeMux 。当 ListenAndServe 在没有提供其他处理器的情况下,即 handlernil 时内部会使用 DefaultServeMux

默认多路转接器

package mainimport (    "net/http")func defaultHandler(rw http.ResponseWriter, rq *http.Request) {    rw.Write([]byte(rq.RemoteAddr))}func main() {    http.Handle("/", http.HandlerFunc(defaultHandler))    http.ListenAndServe(":3000", nil)}

任何具有 func(http.ResponseWriter, *http.Request) 签名的函数都能转换成为一个 http.HandlerFunc 类型的对象,因为 HandlerFunc 对象内置了 ServeHTTP() 方法。

自定义快捷函数

实际上将函数转换为 HandlerFunc 后注册到 ServeMux 是很普遍的用法,当显式地使用 ServeMux 时,Golang提供了更为方便地 ServeMux.HandleFunc 函数。

package mainimport (    "net/http")func defaultHandler(rw http.ResponseWriter, rq *http.Request) {    rw.Write([]byte(rq.RemoteAddr))}func main() {    http.HandleFunc("/", defaultHandler)    http.ListenAndServe(":3000", nil)}

此时若需要从 main() 函数中传递参数到处理器,应该如何实现呢?一种优雅的方式是将处理器放入闭包中,将参数传入。

package mainimport (    "net/http"    "time")func defaultHandler(format string) http.Handler {    return http.HandlerFunc(func(rw http.ResponseWriter, rq *http.Request) {        rw.Write([]byte(time.Now().Format(format)))    })}func main() {    handler := defaultHandler(time.RFC3339)    http.Handle("/", handler)    http.ListenAndServe(":3000", nil)}

这里 defaultHandler() 函数除了将函数封装成为 Handler 外还会返回一个处理器。

也可在返回时使用一个到 http.HandlerFunc 类型的隐式转换

package mainimport (    "net/http"    "time")func defaultHandler(format string) http.HandlerFunc {    return func(rw http.ResponseWriter, rq *http.Request) {        rw.Write([]byte(time.Now().Format(format)))    }}func main() {    handler := defaultHandler(time.RFC3339)    http.Handle("/", handler)    http.ListenAndServe(":3000", nil)}

处理器

Golang中没有继承、多态,可通过接口来实现。而接口则是定义声明的函数签名,任何结构体只要实现与接口函数签名相同的方法,即等同于实现了对应的接口。

Golang的 net/http 包实现的HTTP服务都是基于 http.Handler 接口进行处理的

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

任何结构体只要实现了 ServeHTTP 方法即可称之为处理器对象, http.ServeMux 多路转接器会使用处理器对象并调用其 ServeHTTP 方法来处理请求并返回响应。

处理器函数的实现实际上调用默认 ServeMuxHandleFunc() 方法

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

若使用 http.Handle() 方法则第二个参数需实现 Handler 接口,实现 Handler 接口需实现其 ServeHTTP() 方法。换句话说,只要具有如下签名的 ServeHTTP 方法即可作为处理器。

ServeHTTP(http.ResponseWriter, *http.Request)

ServeHTTP

  • handler 函数表示具有 func(ResponseWriter, *Request) 签名的函数
  • handler 处理器函数表示经过 http.HandlerFunc 结构包装的 handler 函数, http.HandlerFunc 结构实现了 ServeHTTP 接口,因此调用 handler 处理器的 ServeHTTP() 方法时,也就是在调用 handler 函数本身。
  • handler 对象表示实现了 http.Handler 接口中 ServerHTTP() 方法的结构实例

自定义处理器

package mainimport (    "net/http"    "time")type TestHandler struct {    format string}func (t *TestHandler) ServeHTTP(rw http.ResponseWriter, rq *http.Request) {    rw.Write([]byte(time.Now().Format(t.format)))}func main() {    mux := http.NewServeMux()    th := &TestHandler{format: time.RFC3339}    mux.Handle("/", th)    http.ListenAndServe(":3000", mux)}

Golang中 net/http 包中自带处理程序包括 FileServerNotFoundHandlerRedirectHandler 等。

适配器

type HandlerFunc func(ResponseWriter, *Request)

HandlerFunc 适配器实现了 ServeHTTP 接口,因此它也是一个处理器。

func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {    f(w, r)}

HandlerFunc 适配器的作用是将自定义的函数转换为 Handler 处理器类型,当调用 HandlerFunc(f) 后会强制将 f 函数类型转换为 HandlerFunc 类型,这样 f 函数就具有了 ServeHTTP 方法,同时也就转换成为了一个处理器。

请求

Web服务最基本的工作是接受请求返回响应, net/http 包封装了 http.Request 结构体,用于获取一次HTTP请求的所有信息。

type Request struct {    Method string//请求方法    URL *url.URL//请求地址    Proto      string // "HTTP/1.0"    ProtoMajor int    // 1    ProtoMinor int    // 0    Header Header//请求头    Body io.ReadCloser//请求体    GetBody func() (io.ReadCloser, error)//获取请求体    ContentLength int64//内容长度    TransferEncoding []string//传输编码    Close bool//连接是否关闭    Host string//服务器主机地址    Form url.Values//GET表单    PostForm url.Values//POST表单    MultipartForm *multipart.Form//上传表单    Trailer Header    RemoteAddr string//远程客户端地址    RequestURI string//请求URI    TLS *tls.ConnectionState//HTTPS    Cancel <-chan struct{}    Response *Response//响应    ctx context.Context//上下文对象}
http.HandleFunc("/", func(rw http.ResponseWriter, rq *http.Request) {    //rw.Write([]byte(rq.RemoteAddr))    fmt.Printf("protocol: %v\n", rq.Proto)    fmt.Printf("method: %v\n", rq.Method)    fmt.Printf("content length: %v\n", rq.ContentLength)    fmt.Printf("url: %v\n", rq.URL)    fmt.Printf("uri: %v\n", rq.RequestURI)    fmt.Printf("remoteAddr: %v\n", rq.RemoteAddr)    fmt.Printf("host: %v\n", rq.Host)})http.ListenAndServe(":3000", nil)

响应

net/http 包中提供了访问Web服务的函数,比如 http.Get()http.Post()http.Head() 等,用于读取请求数据。服务端发送的响应报文会被保存在 http.Response 结构体中,响应包体会被存放在 ResponseBody 字段中。程序使用完响应必须关闭回复主体。

服务端

package mainimport (    "fmt"    "net/http")func server() {    http.HandleFunc("/", func(rw http.ResponseWriter, rq *http.Request) {        fmt.Printf("client %v %v %v\n", rq.RemoteAddr, rq.Method, rq.URL)        rw.Write([]byte(rq.RemoteAddr))    })    http.ListenAndServe(":3000", nil)}func main() {    server()}

GET

客户端发送不带参数的GET请求

package mainimport (    "fmt"    "io/ioutil"    "net/http")func client() {    url := "http://127.0.0.1:3000?id=1"    fmt.Printf("request: GET %v\n", url)    rp, err := http.Get(url)    if err != nil {        panic(err)    }    defer rp.Body.Close()    fmt.Printf("response: status=%v, code=%v\n", rp.Status, rp.StatusCode)    body, err := ioutil.ReadAll(rp.Body)    if err != nil {        panic(err)    }    fmt.Printf("response: body=%v\n", string(body))}func main() {    client()}

运行测试

客户端发送带参数的GET请求

//请求参数params := url.Values{}params.Set("id", "1")params.Set("pid", "0")//设置URLrawURL := "http://127.0.0.1:3000"reqURL, err := url.ParseRequestURI(rawURL)if err != nil {    panic(err)}//整合参数reqURL.RawQuery = params.Encode()fmt.Printf("request: GET %v\n", reqURL.String())//发送请求rp, err := http.Get(reqURL.String())if err != nil {    panic(err)}//延迟关闭响应包体defer rp.Body.Close()//解析响应fmt.Printf("response: status=%v, code=%v\n", rp.Status, rp.StatusCode)body, err := ioutil.ReadAll(rp.Body)//一次性读取响应包体内容if err != nil {    panic(err)}fmt.Printf("response: body=%v\n", string(body))
request: GET http://127.0.0.1:3000?id=1&pid=0response: status=200 OK, code=200response: body=127.0.0.1:3000: id=1, pid=0

服务端接收GET请求并解析参数

http.HandleFunc("/", func(rw http.ResponseWriter, rq *http.Request) {    //延迟关闭请求包体    defer rq.Body.Close()    fmt.Printf("client %v %v %v\n", rq.RemoteAddr, rq.Method, rq.URL)    //获取GET请求参数    val := rq.URL.Query()    id := val.Get("id")    pid := val.Get("pid")    //返回响应    msg := fmt.Sprintf("%v: id=%v, pid=%v", rq.Host, id, pid)    rw.Write([]byte(msg))})http.ListenAndServe(":3000", nil)

POST

application/x-www-form-urlencoded

客户端发送POST请求

url := "http://127.0.0.1:3000"//内容类型contentType := "application/x-www-form-urlencoded"//对应内容类型的数据样式id := 10pid := 1data := fmt.Sprintf("id=%v&pid=%v", id, pid)body := strings.NewReader(data)//发送请求rp, err := http.Post(url, contentType, body)if err != nil {    panic(err)}defer rp.Body.Close() //延迟关闭响应包体//解析响应包体arr, err := ioutil.ReadAll(rp.Body) //一次性读取完毕if err != nil {    panic(err)}msg := string(arr)fmt.Println(msg)

服务端解析POST参数

http.HandleFunc("/", func(rw http.ResponseWriter, rq *http.Request) {    defer rq.Body.Close()    //解析表单    rq.ParseForm()    //获取表单字段    id := rq.PostForm.Get("id")    pid := rq.PostForm.Get("pid")    fmt.Printf("%v %v %v id=%v pid=%v\n", rq.RemoteAddr, rq.Method, rq.Proto, id, pid)    //返回响应    msg := fmt.Sprintf("%v %v %v", rq.Host, rq.Method, rq.Proto)    rw.Write([]byte(msg))})

application/json

客户端发送POST JSON数据

url := "http://127.0.0.1:3000"//内容类型contentType := "application/json"//对应内容类型的数据样式id := 10pid := 1data := fmt.Sprintf(`{"id":%v, "pid":%v}`, id, pid)body := strings.NewReader(data)//发送请求rp, err := http.Post(url, contentType, body)if err != nil {    panic(err)}defer rp.Body.Close() //延迟关闭响应包体//解析响应包体arr, err := ioutil.ReadAll(rp.Body) //一次性读取完毕if err != nil {    panic(err)}msg := string(arr)fmt.Println(msg)

服务端解析POST JSON

http.HandleFunc("/", func(rw http.ResponseWriter, rq *http.Request) {    defer rq.Body.Close()    //读取数据    arr, err := ioutil.ReadAll(rq.Body)    if err != nil {        panic(err)    }    str := string(arr)    //获取表单字段    fmt.Printf("%v %v %v %v\n", rq.RemoteAddr, rq.Method, rq.Proto, str)    //返回响应    msg := fmt.Sprintf(`{"code":%v, "message":%v}`, 1, "success")    rw.Write([]byte(msg))})

POST JSON

作者:JunChow520

出处:
https://www.jianshu.com/p/844ba023eac7