• Welcome to Journal web site.

我是 PHP 程序员

- 开发无止境 -

Next
Prev

Go语言标准库之net/http(三) —— Client

Data: 2017-05-05 18:34:14Form: JournalClick: 5

前面两篇文章分别介绍了 RequestRespone,了解知道了HTTP请求与响应的大致结构以及所需的信息。那 RequestRespone之间是如何通过网络进行交互的呢,这时候就需要ClientServer来协助与处理了,此篇文章重点介绍Client部分。

Client这里顾名思义就是HTTP客户端,用于发送HTTP请求( Request) 并获得响应Respone

下面我们来详细介绍下在Go语言的net/http包中,Client是如何被定义以及使用的。

Client结构体

Go语言中的http.Client结构体是用于发送HTTP请求并返回响应的组件。它的定义如下:

type Client struct {
    Transport     RoundTripper
    CheckRedirect func(req *Request, via []*Request) error
    Jar           CookieJar
    Timeout       time.Duration
}

下面对各个字段进行分别说明:

  • Transport

一个http.RoundTripper接口类型的对象,只包含一个方法RoundTrip,它接受一个*http.Request类型的参数,表示HTTP请求,返回一个*http.Response类型的响应和一个错误对象,该方法的作用是发送HTTP请求并返回响应,同时处理可能出现的传输错误,如超时、连接错误、重定向等。

http.RoundTripper 的默认实现是http.Transport,该实现使用TCP连接池,支持HTTP/1.1HTTP/2协议,同时还支持HTTPS、代理、压缩和连接复用等特性。如果需要更灵活地控制HTTP请求的传输过程,可以自定义实现http.RoundTripper接口,并将其传递给http.ClientTransport字段。

  • CheckRedirect

一个函数类型,用于控制HTTP重定向。默认情况下,http.DefaultCheckRedirect允许自动跟随HTTP重定向。

  • Jar

一个http.CookieJar接口类型的对象,用于管理HTTP cookie。默认情况下,http.DefaultCookieJar使用net/http/cookiejar包中的默认cookie实现。

  • Timeout

一个time.Duration类型的对象,用于控制HTTP请求的超时时间。默认情况下,如果该字段没有设置超时时间,即无限期等待响应。

创建一个 Client也很简单,最简单的创建如下:

Client := &http.Client{}

一行代码搞定,当然也可以带上你自己所需要的参数来创建Client,比如使用http.ClientTimeout字段创建一个有超时时间的客户端:

Client := &http.Client{
    Timeout: 15 * time.Second,
}

有一些更细粒度的超时控制:

Client := &http.Client{  
    Transport: &Transport{
        Dial: (&net.Dialer{
                Timeout:   30 * time.Second,
                KeepAlive: 30 * time.Second,
        }).Dial,
        TLSHandshakeTimeout:   10 * time.Second,
        ResponseHeaderTimeout: 10 * time.Second,
        ExpectContinueTimeout: 1 * time.Second,
    }
}

代码当中一些参数,下面列出解释以便理解:

  • net.Dialer.Timeout 限制建立TCP连接的时间
  • http.Transport.TLSHandshakeTimeout 限制 TLS握手的时间
  • http.Transport.ResponseHeaderTimeout 限制读取response header的时间
  • http.Transport.ExpectContinueTimeout 限制client在发送包含 Expect: 100-continueheader到收到继续发送bodyresponse之间的时间等待

Client 使用

标准请求

使用 http.Client进行发送HTTP请求以及返回响应,基本流程如下:

  1. 创建http.Client对象。首先,我们需要创建一个http.Client对象。可以通过http.DefaultClient使用默认的HTTP客户端,也可以通过手动创建一个新的http.Client对象,以便自定义其参数。
  2. 创建HTTP请求。有了http.Client对象后,我们需要创建一个HTTP请求。在Request章节中,我们讲述到http.NewRequest函数,我们可以通过该函数创建一个新的请求对象,并设置请求URL、方法、请求体等参数。
  3. 发送HTTP请求。有了请求对象后,将请求对象传递给http.ClientDo方法,以便发送HTTP请求。Do方法返回一个响应对象,其中包含服务器的响应信息,如状态码、响应头和响应体等。
  4. 处理HTTP响应。我们可以使用响应对象中的方法和属性,如resp.StatusCoderesp.Headerresp.Body等,处理服务器的响应。通常,我们需要读取响应体的内容,并将其解析为合适的数据类型,如JSONXML
  5. 关闭HTTP响应。获取响应后,我们需要确保关闭HTTP响应的主体。可以使用defer resp.Body.Close()语句在函数退出时自动关闭响应体,以避免内存泄漏。

以下是一个简单的示例代码,演示了使用http.Client发送HTTP GET请求的基本流程:

package main

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

func main() {
    // 创建http.Client对象
    client := &http.Client{}

    // 创建HTTP请求
    req, err := http.NewRequest("GET", "http://example.com", nil)
    if err != nil {
        panic(err)
    }

    // 发送HTTP请求
    resp, err := client.Do(req)
    if err != nil {
        panic(err)
    }

    // 处理HTTP响应
    defer resp.Body.Close()
    body, err := ioutil.ReadAll(resp.Body)
    if err != nil {
        panic(err)
    }
    fmt.Println(string(body))
}

上述代码仅仅是以HTTP GET请求为例子作说明重点是突出请求的标准步骤,更多灵活的组合操作马上进行讲解。

自定义Client

一般情况下,我们并不需要自定义http.Client来控制控制HTTP请求的行为和配置,使用net/http包中默认的 http.DefaultClient 即可,http.DefaultClient 在 client.go 文件中是这样定义的:

var DefaultClient = &Client{}

可以看出默认的http.DefaultClient并没有设置Client任何属性值,但是如果我们需要设置HTTP请求的超时时间、代理、连接池等选项,可能就需要我们自己去定义和创建http.Client了。

根据Client结构体内容,我们知道http.Client拥有 TransportCheckRedirectJarTimeout四个属性字段,详细介绍如下:

  • Timeout

它用于设置 HTTP客户端的超时时间,是一个 time.Duration 类型的值,表示客户端在发送请求后等待服务器响应的最大时间。如果在这个时间内服务器没有响应,客户端会放弃请求并返回一个错误。这个超时时间是一个全局的设置,对于所有的 HTTP请求都生效。

默认情况下,http.Client.Timeout 的值是零,表示没有超时限制。下面是创建一个10秒超时时间的客户端示例:

client := &http.Client{
       Timeout: 10 * time.Second,   
} 
  • Jar

它是一个 http.CookieJar 接口类型的值,表示 HTTP客户端使用的 cookie容器。这个容器会自动存储服务器发送给客户端的 cookie,并在后续的 HTTP 请求中自动发送这些 cookie给服务器。

默认情况下,http.Client.Jar 是空的,这意味着 HTTP客户端不会发送任何 cookie给服务器。

可以创建一个自定义的 cookie容器,并将其赋值给 http.Client.Jar 属性,例如:

jar, err := cookiejar.New(nil)   
if err != nil {       
// handle error   
}   
client := &http.Client{       
    Jar: jar,   
}
  • CheckRedirect

它是一个可选的回调函数,用于在 HTTP客户端进行重定向时决定是否要遵循该重定向。

默认情况下,如果不设置 CheckRedirect 函数,HTTP客户端会遵循所有的重定向。对于 http.Gethttp.Head 等高级别的 HTTP 请求函数,它们默认使用一个简单的 CheckRedirect 函数,该函数会在重定向次数超过 10次时返回一个 http.ErrUseLastResponse 错误。

可以通过自定义 http.ClientCheckRedirect 函数来控制 HTTP客户端的重定向行为。下面是一个简单的例子:

func CheckRedirect(eq http.Request, via []http.Request) error {
     if len(via) >= 2 {
         return fmt.Errorf("too many redirects")
     }     return nil   
}

func main() {
     client := &http.Client{
         CheckRedirect: CheckRedirect,     
    }   
}

上述代码重新定义了重定向函数,设定了只允许重定向2次以免出现重定向循环。

  • Transport

它表示了 http.Client 使用的网络传输层。默认情况下,http.Client 使用的是 http.DefaultTransport,它是一个基于 TCP的传输层。

http.Client.Transport 是一个接口类型,它定义了如下两个方法:

  • RoundTrip(req *Request) (*Response, error):执行一个 HTTP 请求并返回响应结果或者错误。
  • CancelRequest(req *Request):取消一个正在执行的请求

如果你需要创建自己的传输层,你需要实现 http.RoundTripper 接口。这个接口只有一个方法:

  • RoundTrip(req *Request) (*Response, error):执行一个 HTTP 请求并返回响应结果或者错误。

解释完 http.Client 得几个属性后,我们通过下面几个例子来了解和理解如何进行自定义 http.Client

(1) 通过 Transport 字段自定义传输层

transport := &http.Transport{
      Proxy: http.ProxyFromEnvironment,
      TLSClientConfig: &tls.Config{
          InsecureSkipVerify: true,
      },
  }
 client := &http.Client{
      Transport: transport,
  }
 resp, err := client.Get("https://example.com")

上述代码自定义一个使用了代理服务器和自签名证书的的传输层。

(2) 通过自定义函数来进行重新制定重定向规则

func userCheckRedirect(req *http.Request, via []*http.Request) error {
     //只能执行3次重定向
     if len(via) >= 3 {
         return errors.New("stopped after 3 redirects")
     }
     return nil
  }
  client := &http.Client{
      CheckRedirect: userCheckRedirect,
  }

通过自定义函数 userCheckRedirect来控制重定向次数最多为3次,防止无限循环。如果不进行自定义函数,则默认重定向次数为 10次。

例子就简单举这几个把,完成了 http.Client自定义,下面我们再看看 http.Request的自定义。

自定义Request

相当于 http.Client 来说,http.Request的自定义使用频率更高,更为普遍。

http.Request它包含了请求的方法、URLHeader以及 Body等信息。我们可以创建一个 http.Request 对象并设置它的http.Request.Headerhttp.Request.Bodyhttp.Request.URL等属性。

net/http(一)--Request章节中讲到过,创建一个新的 http.Request, 使用的是 http.NewRequest 函数,下面将以代码示例形式讲述各种场景的自定义Request创建:

(1)自定义请求 Header

req, err := http.NewRequest("GET", "https://www.example.com", nil)    
if err != nil {
        log.Fatal(err)    
}
req.Header.Set("Accept", "application/json")
req.Header.Set("Authorization", "Bearer my-token")

从上代码可以看出可以使用 Request.Header.Set方法来创建请求头。

(2) 自定义参数:

query := url.Values{}    
query.Add("q", "golang")    
query.Add("page", "1")
url := &url.URL{
        Scheme:   "https",
        Host:     "www.example.com",
        Path:     "/search",
        RawQuery: query.Encode(),
}
req, err := http.NewRequest("GET", url.String(), nil)    
if err != nil {
        log.Fatal(err)
}

通过 对url.Values类型数据的添加,并在最后进行 Encode()处理后赋值给url.URL.RawQuery字段,这样就可以完成URL参数的添加。

(3) 自定义 Basic认证:

eq, err := http.NewRequest("GET", "https://www.example.com", nil)    
if err != nil {
        log.Fatal(err)    
}
username := "my-username"    
password := "my-password"    
auth := username + ":" + password
base64Encoded := base64.StdEncoding.EncodeToString([]byte(auth))
req.Header.Set("Authorization", "Basic "+base64Encoded)

Basic 认证是比较常见的API调用认证,此处是通过设置请求头的 Authorization完成设置。

(4) 使用 Cookie :

cookie := &http.Cookie{        
    Name:  "session_id",        
    Value: "my-session-id",    
}
// Create the request    
url := "https://www.example.com/api/v1"    
req, err := http.NewRequest("GET", url, nil)    
if err != nil {        
    log.Fatal(err)    
}    
req.AddCookie(cookie)

Cookie 在 HTTP 请求中属实常见,代码中通过 Request.AddCookie方法来设置cookie。

(5) 发送POST表单数据:

url := "https://www.example.com/api/v1/posts"    
params := []string{"xjx", "zzz"}    
req, err := http.NewRequest("POST", url, nil)    
if err != nil {
        log.Fatal(err)
}    
data := url.Values{}    
data.Set("title", title)    
data.Set("content", content)    
for _, tag := range params {
        data.Add("tags", tag)
}    
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")    
req.Body = ioutil.NopCloser(strings.NewReader(data.Encode()))

POST表单数据一般通过设置请求体来发送,而请求体(Request Body)的数据设置一般跟 请求头的 Content-Type值相关。

下面列举常见的 Content-Type构造请求体例子:

[1] application/x-www-form-urlencoded


该类型Request Body数据一般跟 自定义参数类型,通过将需要发送的表单数据通过url.Values格式添加并Encode进行加编码,不同的是最后需要转化为*Reader对象。示例:

url := "http://www.example.com"  
contentType := "application/x-www-form-urlencoded"  
data := "key1=value1&key2=value2"  
requestBody := strings.NewReader(data)  
req, _ := http.NewRequest("POST", url, requestBody)  
req.Header.Set("Content-Type", contentType)

也可以

url := "http://www.example.com"  
contentType := "application/x-www-form-urlencoded"  
data := "key1=value1&key2=value2"  
requestBody := strings.NewReader(data)  
req, _ := http.NewRequest("POST", url, nil)  
req.Header.Set("Content-Type", contentType)  
req.Body = io.NopCloser(requestBody)

[2]application/json


该类型Request Body数据是json格式,非url.Values类型,其他操作方式差不多,直接看示例:

url := "http://www.example.com"  
contentType := "application/json"  
data := `{"key1":"xjx","key2":18}`  
requestBody := strings.NewReader(data)  
req, _ := http.NewRequest("POST", url, requestBody)  
req.Header.Set("Content-Type", contentType)

[3] application/xml


该类型Request Body数据是xml格式或者[]byte格式,如果是字符串形式跟JSON处理一致,[]byte类型如下:

url := "http://www.example.com"  
contentType := "application/xml"  
data := []byte(`<?xml version="1.0" encoding="UTF-8"?>      <root>          <name>John Doe</name>          <email>john.doe@example.com</email>      </root>`)  
requestBody := bytes.NewBuffer(data)  
req, _ := http.NewRequest("POST", url, requestBody)  
req.Header.Set("Content-Type", contentType)

[4] multipart/form-data


该类型一般用于上传文件,该类型表单数据一般通过二进制传输,我们转为 []byte或者string类型即可,示例如下:

url := "http://www.example.com"  
contentType := "multipart/form-data"  
data, _ := ioutil.ReadFile("post.txt")  
requestBody := bytes.NewReader(data)  
req, _ := http.NewRequest("POST", url, requestBody)  
req.Header.Set("Content-Type", contentType)

这些例子涵盖了 http.Request 的一些实际场景自定义使用,包括添加请求头、发送 GET请求参数和发送 POST表单数据等。你可以根据自己的需求进行更多自定义组合。

下面我们介绍一些使用 http.Client自带的函数(比如http.Client.Get/http.Gethttp.Client.Post/http.Posthttp.Client.PostForm/http.PostForm等)发送HTTP请求操作,这些自带函数基本都是对标请求准流程进行了封装以适应不同场景简便使用。

自带函数请求

net/http包的client.go 文件提供了自带函数来简便的调用Client通过GETPOST等方式请求HTTP,下面来简单举例说明。

http.Get/http.Client.Get

使用net/http包编写一个简单的发送HTTP请求的Client端,可以使用 http.Get 或者 http.Client.Get函数,这两函数本质是一样的,源码如下:

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最终本质还是调用了 http.Client.Get函数来发送HTTP请求,唯一的区别就是 http.Get使用了默认的DefaultClient来作为Client从而可以不用自主创建一个新的Client,而http.Client.Get则需要自行创建一个新的Client(类似 $Client = $&http.Client{})来调用。

下面看一个 http.Get 函数无参数请求HTTP示例代码如下:

package main

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

func main() {
    //Get方式获取URL的展示信息
    resp, err := http.Get("https://www.qq.com")
    if err != nil {
        fmt.Println("get url failed, err:", err)
    } else {
        defer resp.Body.Close()
        //读取body
        body, err := ioutil.ReadAll(resp.Body)
        if err != nil {
            fmt.Println("read from resp.Body failed,err:", err)
        } else {
            fmt.Println(string(body))
        }
    }
}

再来一个关于有参数的HTTP请求示例代码:

package main

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

func main() {
    apiUrl := "http://www.example.com"
    //解析URl字符串获取URL对象
    u, err := url.ParseRequestURI(apiUrl)
    if err != nil {
        fmt.Printf("parse url requestUrl failed,err:%v\n", err)
    }
    //添加参数
    params := url.Values{}
    params.Add("age", "10")
    params.Add("name", "xjx")
    //将参数URL化,生成结果类似:bar=baz&foo=quux格式并赋值给URL.RawQuery
    u.RawQuery = params.Encode()
    resp, err := http.Get(u.String())
    if err != nil {
        fmt.Println("get url failed, err:", err)
    } else {
        defer resp.Body.Close()
        body, err := ioutil.ReadAll(resp.Body)
        if err != nil {
            fmt.Println("read from resp.Body failed,err:", err)
        } else {
            fmt.Println(string(body))
        }
    }
}

简易GET请求大致就是这样了,如果需要更复杂的则需要自定义请求来构造相关的Request信息了。

http.Post/http.Client.Post

上面演示了使用net/http包发送GET请求的示例,而发送POST请求则可以使用 http.Post 或者 http.Client.Post函数,源码如下:

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

func (c *Client) Post(url, contentType string, body io.Reader) (resp *Response, err error) {
    req, err := NewRequest("POST", url, body)
    if err != nil {
        return nil, err
    }
    req.Header.Set("Content-Type", contentType)
    return c.Do(req)
}

从上面代码可以看出, http.Post最终本质还是调用了 http.Client.Post函数来发送HTTP请求,在此就不再具体分析了。

 

下面来看下net/http包发送Post请求的示例:

package main

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

func main() {
    apiUrl := "http://www.example.com"

    // 构造类型为 x-www-form-urlencoded 的请求体数据
    // 发送 POST 请求
    requestBody := bytes.NewBufferString("key1=value1&key2=value2")
    resp, err := http.Post(apiUrl, "application/x-www-form-urlencoded", requestBody)

    if err != nil {
        // 发生错误
        fmt.Println("Error occurred while sending request:", err)
        return
    }
    defer resp.Body.Close()

    // 读取响应结果
    response, err := io.ReadAll(resp.Body)
    if err != nil {
        fmt.Println("Error occurred while reading response:", err)
        return
    }

    // 输出响应结果
    fmt.Println(string(response))
}

http.Post 中第二个函数是可以自己控制的,它的值可以是 RequestContent-Type 字段的允许值范围内,不清楚的可以点击Content-Type取值范围查看。

net/http包发送application/x-www-form-urlencoded类型的Post请求还有一个更简便的函数 http.PostForm或者 http.Client.PostForm

func PostForm(url string, data url.Values) (resp *Response, err error) {
    return DefaultClient.PostForm(url, data)
}

func (c *Client) PostForm(url string, 
                
                
                
                
                
                
              
Name:
<提交>