2
头图

大家好,针对Go语言 net/http 标准库,将梳理的相关知识点分享给大家~~

围绕 net/http 标准库相关知识点还有许多章节,请大家多多关注~~

文章中代码案例只有关键片段,完整代码请查看github仓库:https://github.com/hltfaith/go-example/tree/main/net-http

本节内容

  • http基础知识
  • net/http库常用请求方法

http基础知识

HTTP 超文本传输协议(HyperText Transfer Protocol)。
Web应用的应用层协议,定义浏览器如何向Web服务器发送请求,以及Web服务器如何进行响应。

http报文格式

用于 HTTP 协议交互的信息被称为 HTTP 报文。请求端(客户端)的 HTTP 报文叫做请求报文,响应端(服务器端)的叫做响应报文。
HTTP 报文大致可分为 报文首部和报文主体 两块。两者由最初出现的空行(CR+LF)来划分。通常,并不一定要有报文主体。

请求报文结构

请求报文实例

响应报文结构

响应报文实例

注:上图来自《图解HTTP》书籍,更详细的http报文首部信息请查阅该书籍。

http1、http2协议

http1.0

  • 每次请求都必须新建一次连接,必须通过TCP的三次握手才能开始传输数据,连接结束之后还要经历四次挥手。
  • 不跟踪每个浏览器的历史请求。
  • 连接无法复用,每次请求都需要建立一个TCP连接,费时费力。
  • 队头阻塞,下一个请求必须在前一个请求响应到达后发送。如果某请求一直不到达,那么下一个请求就一直不发送。

http1.1

  • 默认支持长连接,即在一个TCP连接上可以传送多个HTTP请求和响应,减少了建立和关闭连接的消耗和延迟。
  • 管道化,通过长连接实现在一个连接中传输多个文件。
  • 支持断点续传。
  • 添加了其他请求方法:put、delete、options...

http2.0

  • 二进制传输:将请求和响应数据分为更小的帧,并且采用二进制编码。
  • Header压缩:采用HPACK算法压缩头部,同时同一个域名下的两个请求,只会发送差异数据,减少冗余的数据传输,降低开销。
  • 多路复用:同一个域名下所有通信都是单个连接上完成,单个连接可以承载任意数量的双向数据流,数据流以消息形式发送,而消息由一个或多个帧组成,可以乱序发送。
  • 服务端推送:服务端可以新建“流”主动向客户端发送消息,提前推送客户端需要的静态资源,减少等待延迟。
  • 提高安全性:HTTP2也是明文的,只不过格式是二进制的,但HTTP2都是https协议的,跑在TSL上面。

http协议9种请求类型

OPTIONS:允许客户端查看服务器的性能。
GET:请求指定的页面信息,并返回实体主体。
HEAD:类似于GET请求,响应中没有具体的内容,用于获取报头。
POST:向指定资源提交数据并进行处理请求。数据被包含在请求体中,POST请求可能会导致新的资源的建立或已有资源的修改。
PUT:从客户端向服务器传送新的数据到指定的位置中。
DELETE:请求服务器删除指定的页面。
TRACE:回显服务器收到的请求,主要用于测试或诊断。
CONNECT:可以开启一个客户端与所请求资源之间的双向沟通的通道。它可以用来创建隧道(tunnel)。
PATCH:是对请求方式中的PUT补充,用来对已知资源进行局部更新。

http常见状态码

更详细http状态码请查阅: https://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml

http头格式

HTTP 是一种无状态 (stateless) 协议, HTTP协议本身不会对发送过的请求和相应的通信状态进行持久化处理。这样做的目的是为了保持HTTP协议的简单性,从而能够快速处理大量的事务, 提高效率。

然而,在许多应用场景中,我们需要让浏览器记住当前的状态,常用的手段是在 http Header 设置 Token 等方式。
HTTP 头字段非常灵活,不仅可以使用标准里的 HostConnection 等已有头,也可以任意添加自定义头。
使用头字段需要注意下面几点:

  • 字段名不区分大小写,例如Host也可以写成host,但首字母大写的可读性更好;
  • 字段名里不允许出现空格,可以使用连字符-,但不能使用下划线_。例如,test-name是合法的字段名,而test nametest_name是不正确的字段名;
  • 字段名后面必须紧接着:,不能有空格,而:后的字段值前可以有多个空格;
  • 字段的顺序是没有意义的,可以任意排列不影响语义;
  • 字段原则上不能重复,除非这个字段本身的语义允许,例如 Set-Cookie

常见的一些 HTTP 标头:

更详细HTTP标头请查阅:https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers

http Payload概念

Payload指的是在HTTP通信中传输的实际数据部分。在HTTP请求中,Payload通常是请求体(Request Body),包含着客户端发送给服务器的数据。而在HTTP响应中,Payload则是响应体(Response Body),装载着服务器返回给客户端的数据。

常用的请求方法

Get()方法

通常我们需要的向指定的URL发起GET请求。
函数原型

func Get(url string) (resp *Response, err error)

接口中 *Response 类型,可以参考后续章节Response类型。
如下案例中,向 http://httpbin.org/get URL发起 GET 请求,对端服务接口返回Response body内容。

func main() {
    res, err := http.Get("http://httpbin.org/get")
    if err != nil {
        log.Fatal(err)
    }
    body, err := io.ReadAll(res.Body)
    defer res.Body.Close()
    if res.StatusCode != http.StatusOK {
        log.Fatalf("Response failed with status code: %d\n", res.StatusCode)
    }
    if err != nil {
        log.Fatal(err)
    }
    fmt.Printf("%s", body)
}

响应内容,包含请求客户端的IP、请求URL等信息。

{
  "args": {}, 
  "headers": {
    "Accept-Encoding": "gzip", 
    "Host": "httpbin.org", 
    "User-Agent": "Go-http-client/1.1", 
    "X-Amzn-Trace-Id": "Root=1-64a2c319-2b1c4b996944f20364070806"
  }, 
  "origin": "219.143.128.115", 
  "url": "http://httpbin.org/get"
}
注:这个GET方法本身是基于 NewRequest() 方法封装,其主要作用精简 GET 请求。

Head()方法

Head 请求表示只请求目标 URL 的响应头信息,不返回响应实体。
函数原型

func Head(url string) (resp *Response, err error)

使用和 http.Get() 方法一样,只需要传入目标的 URL参数即可

func main() {
    res, err := http.Head("http://httpbin.org/get")
    if err != nil {
        log.Fatal(err)
    }
    defer res.Body.Close()
    for k, v := range res.Header { // 打印头信息
        fmt.Println(k, ":", v)
    }
}

响应内容

Connection : [keep-alive]
Server : [gunicorn/19.9.0]
Access-Control-Allow-Origin : [*]
Access-Control-Allow-Credentials : [true]
Date : [Mon, 03 Jul 2023 12:55:56 GMT]
Content-Type : [application/json]
Content-Length : [241]

Post()方法

向指定URL发送POST请求。
函数原型

func Post(url, contentType string, body io.Reader) (resp *Response, err error)

调用 http.Post() 方法需要依次传递3个参数

  • 请求的目标URL
  • 数据资源类型
  • 数据的比特流([]byte 形式)

该 Post()方法本身也是基于 NewRequest() 方法封装。
本案例中,使用 application/json 载荷体类型,模拟数据向 http://httpbin.org/post api接口发送POST请求。

func main() {
    data := make(map[string]string, 0)
    data["key"] = "001"
    buf, err := json.Marshal(data)
    if err != nil {
        log.Fatal(err)
    }
    reqBody := strings.NewReader(string(buf))
    res, err := http.Post("http://httpbin.org/post", "application/json", reqBody)
    if err != nil {
        log.Fatal(err)
    }
    body, err := io.ReadAll(res.Body)
    defer res.Body.Close()
    if res.StatusCode != http.StatusOK {
        log.Fatalf("Response failed with status code: %d\n", res.StatusCode)
    }
    fmt.Printf("%s", body)
}

响应内容

{
  "args": {}, 
  "data": "{\"key\":\"001\"}", 
  "files": {}, 
  "form": {}, 
  "headers": {
    "Accept-Encoding": "gzip", 
    "Content-Length": "13", 
    "Content-Type": "application/json", 
    "Host": "httpbin.org", 
    "User-Agent": "Go-http-client/1.1", 
    "X-Amzn-Trace-Id": "Root=1-64a2c7d6-17f1aadd5816ef5a3ae14ae8"
  }, 
  "json": {
    "key": "001"
  }, 
  "origin": "219.143.128.115", 
  "url": "http://httpbin.org/post"
}

可以看到,在响应内容中 json 结构字段中已有传入的请求数据。

PostForm()方法

发送表单POST请求。
函数原型

func PostForm(url string, data url.Values) (resp *Response, err error)

PostForm() 方法中,Content-Type 头信息类型是 application/x-www-form-urlencoded

注:POST 请求参数需要通过 url.Values 方法进行编码和封装。

url.Values 类型,归属于 net/url 模块下。

type Values map[string][]string

Values将字符串键映射到值列表,它通常用于查询参数和表单值、头映射、值映射中的键是区分大小写的。
案例中使用 url.Values 模拟数据

func main() {
    res, err := http.PostForm("http://httpbin.org/post", url.Values{"key": {"001"}})
    if err != nil {
        log.Fatalln(err)
    }
    body, err := io.ReadAll(res.Body)
    defer res.Body.Close()
    if res.StatusCode != http.StatusOK {
        log.Fatalf("Response failed with status code: %d\n", res.StatusCode)
    }
    fmt.Printf("%s", body)
}

响应内容

{
  "args": {}, 
  "data": "", 
  "files": {}, 
  "form": {
    "key": "001"
  }, 
  "headers": {
    "Accept-Encoding": "gzip", 
    "Content-Length": "7", 
    "Content-Type": "application/x-www-form-urlencoded", 
    "Host": "httpbin.org", 
    "User-Agent": "Go-http-client/1.1", 
    "X-Amzn-Trace-Id": "Root=1-64a2d1fa-2ccbe28174e1e75e4742f109"
  }, 
  "json": null, 
  "origin": "219.143.128.115", 
  "url": "http://httpbin.org/post"
}

可以看到在响应内容中,form 表单字段已经有传入的数据。

NewRequest()方法

NewRequest方法原型是 context.Background 封装的NewRequestWithContext()方法。
net/http 包没有封装直接使用请求带header的 GET 或者 POST 方法,所以,要想请求中携带header,只能使用NewRequest()方法。
函数原型

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

函数参数依次是:请求类型、请求目标地址、请求体(byte类型)
案例中,请求头设置载荷体application/json类型,发送POST请求。

func main() {
    data := make(map[string]string, 0)
    data["key"] = "001"
    buf, err := json.Marshal(data)
    if err != nil {
        log.Fatal(err)
    }
    reqBody := strings.NewReader(string(buf))
    request, err := http.NewRequest("POST", "http://httpbin.org/post", reqBody)
    request.Header.Set("Content-Type", "application/json")
    client := &http.Client{}
    res, err := client.Do(request) // 发起客户端请求
    if err != nil {
        log.Fatal(err)
    }
    body, _ := io.ReadAll(res.Body)
    defer res.Body.Close()
    if res.StatusCode != http.StatusOK {
        log.Fatalf("Response failed with status code: %d\n", res.StatusCode)
    }
    fmt.Printf("%s", body)
}

响应内容

{
  "args": {}, 
  "data": "{\"key\":\"001\"}", 
  "files": {}, 
  "form": {}, 
  "headers": {
    "Accept-Encoding": "gzip", 
    "Content-Length": "13", 
    "Content-Type": "application/json", 
    "Host": "httpbin.org", 
    "User-Agent": "Go-http-client/1.1", 
    "X-Amzn-Trace-Id": "Root=1-64a2d9ed-3adcecba48c87a5743068ea8"
  }, 
  "json": {
    "key": "001"
  }, 
  "origin": "219.143.128.115", 
  "url": "http://httpbin.org/post"
}

NewRequestWithContext()方法

NewRequestWithContext返回一个给定方法、URL和可选主体的新Request。
NewRequestWithContext返回一个适合客户端使用的请求,可以使用 Client.Do方法或 Transport.RoundTrip
body 类型可以是 *bytes.Buffer*bytes.Reader*strings.Reader
返回的请求的ContentLength被设置为它的确切值(而不是-1),如果ContentLength为0,body被设置为NoBody。
函数原型

func NewRequestWithContext(ctx context.Context, method, url string, body io.Reader) (*Request, error)

函数使用

func main() {
    data := make(map[string]string, 0)
    data["key"] = "001"
    buf, err := json.Marshal(data)
    if err != nil {
        log.Fatal(err)
    }
    reqBody := strings.NewReader(string(buf))
    request, err := http.NewRequestWithContext(context.Background(), "POST", "http://httpbin.org/post", reqBody)
    request.Header.Set("Content-Type", "application/json")
    res, err := http.DefaultClient.Do(request)
    if err != nil {
        log.Fatal(err)
    }
    body, _ := io.ReadAll(res.Body)
    defer res.Body.Close()
    if res.StatusCode != http.StatusOK {
        log.Fatalf("Response failed with status code: %d\n", res.StatusCode)
    }
    fmt.Printf("%s", body)
}

响应内容

{
  "args": {}, 
  "data": "{\"key\":\"001\"}", 
  "files": {}, 
  "form": {}, 
  "headers": {
    "Accept-Encoding": "gzip", 
    "Content-Length": "13", 
    "Content-Type": "application/json", 
    "Host": "httpbin.org", 
    "User-Agent": "Go-http-client/1.1", 
    "X-Amzn-Trace-Id": "Root=1-64a2df9c-43bf00871026b6211598c1bf"
  }, 
  "json": {
    "key": "001"
  }, 
  "origin": "219.143.128.115", 
  "url": "http://httpbin.org/post"
}

ReadRequest()方法

ReadRequest 用于读取解析HTTP请求报文,读取请求体为 *bufio.Reader类型。
ReadRequest 是一个低级的函数,只能用于专门的应用程序。

注:ReadRequest()函数只能处理 HTTP/1.x请求, HTTP/2请求请使用 golang.org/x/net/http2

函数原型

func ReadRequest(b *bufio.Reader) (*Request, error)

函数使用
首先模拟 HTTP GET请求体报文,然后通过 ReadRequest 函数进行将请求报文内容进行解析。
readrequest.go

下面将请求报文通过 ReadRequest 函数做解析。

ReadResponse()方法

ReadResponse 用于读取解析HTTP响应报文,读取响应体为 *bufio.Reader类型,以及请求封装 http.Request 类型。
函数原型

func ReadResponse(r *bufio.Reader, req *Request) (*Response, error)

函数使用
readresponse.go

上述 ReadRequest() 、ReadResponse() 函数使用案例中,全部是我们自己构造模拟的HTTP报文。 下面我们通过实际的 Get()请求函数,将真实报文响应做解析。
readresponse2.go

代码输出

200
{
  "args": {}, 
  "headers": {
    "Accept-Encoding": "gzip", 
    "Host": "httpbin.org", 
    "User-Agent": "Go-http-client/1.1", 
    "X-Amzn-Trace-Id": "Root=1-663b237c-07a4a929184bb68b65d2d032"
  }, 
  "origin": "222.128.49.98", 
  "url": "http://httpbin.org/get"
}

技术文章持续更新,请大家多多关注呀~~

搜索微信公众号,关注我【 帽儿山的枪手 】

参考材料


帽儿山的枪手
71 声望18 粉丝