介绍
当一个程序需要与另一个程序通信时,许多开发人员会使用HTTP。Go的优势之一是其标准库的广度,HTTP也不例外。Go-net/http包不仅支持创建http服务器,还可以作为客户端发出http请求。
在本教程中,您将创建一个向HTTP服务器发出多种类型HTTP请求的程序。首先,您将使用默认的GoHTTP客户端发出GET请求。然后,您将增强您的程序,以使用body发出POST请求。最后,您将自定义POST请求以包含HTTP头,并添加一个超时,如果您的请求耗时过长,则会触发该超时。
先决条件
要遵循本教程,您需要:
- 安装1.16或更高版本。要设置此设置,请遵循操作系统的“如何安装Go”教程。
- 在Go中创建HTTP服务器的经验,可以在教程“如何在Go上创建HTTP服务器”中找到。
- 熟悉goroutines和阅读频道。有关更多信息,请参阅教程“如何在Go中并发运行多个函数”。
- 建议了解HTTP请求的组成和发送方式。
提出GET请求
Go-net/http包有几种不同的方式将其用作客户端。您可以使用具有HTTP等功能的通用全局HTTP客户端。Get只需一个URL和一个正文就可以快速发出HTTP Get请求,或者您可以创建一个HTTP。请求开始定制个别请求的某些方面。在本节中,您将使用http创建一个初始程序。获取一个HTTP请求,然后将其更新为使用HTTP。使用默认HTTP客户端请求。
使用 http.Get
提出请求
在程序的第一次迭代中,您将使用http。Get函数向程序中运行的HTTP服务器发出请求。http:。Get函数很有用,因为您不需要在程序中进行任何其他设置来发出请求。如果您需要快速请求,请使用http。获取可能是最好的选择。
要开始创建程序,您需要一个目录来保存程序的目录。在本教程中,您将使用名为projects的目录。
首先,创建项目目录并导航到它:
- mkdir projects
- cd projects
接下来,为项目创建目录并导航到它。在这种情况下,请使用目录httpclient:
- mkdir httpclient
- cd httpclient
在httpclient目录中,使用nano或您最喜欢的编辑器打开main.go文件
在main.go文件,首先添加以下行:
package main
import (
"errors"
"fmt"
"net/http"
"os"
"time"
)
const serverPort = 3333
您可以添加包名称main,以便将程序编译为可以运行的程序,然后在该程序中将使用的各种包中包含一个import语句。之后,创建一个名为serverPort的常量,其值为3333,将用作HTTP服务器正在侦听的端口和HTTP客户端将连接的端口。
接下来,在main中创建一个main函数。go文件并设置goroutine以启动HTTP服务器:
func main() {
go func() {
mux := http.NewServeMux()
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Printf("server: %s /\n", r.Method)
})
server := http.Server{
Addr: fmt.Sprintf(":%d", serverPort),
Handler: mux,
}
if err := server.ListenAndServe(); err != nil {
if !errors.Is(err, http.ErrServerClosed) {
fmt.Printf("error running http server: %s\n", err)
}
}
}()
time.Sleep(100 * time.Millisecond)
HTTP服务器已设置为使用fmt。Printf可在请求根/路径时打印有关传入请求的信息。它还设置为侦听serverPort。最后,一旦启动服务器goroutine,您的程序就会占用时间。短时间睡眠。这个睡眠时间允许HTTP服务器有足够的时间启动并开始响应下一个请求。
现在,同样在主函数中,使用fmt设置请求URL。Sprintf将http://localhost主机名和服务器正在侦听的serverPort值。然后,使用http。获取对该URL的请求,如下所示:
...
requestURL := fmt.Sprintf("http://localhost:%d", serverPort)
res, err := http.Get(requestURL)
if err != nil {
fmt.Printf("error making http request: %s\n", err)
os.Exit(1)
}
fmt.Printf("client: got response!\n")
fmt.Printf("client: status code: %d\n", res.StatusCode)
}
HTTP服务器已设置为使用fmt。Printf可在请求根/路径时打印有关传入请求的信息。它还设置为侦听serverPort。最后,一旦启动服务器goroutine,您的程序就会占用时间。短时间睡眠。这个睡眠时间允许HTTP服务器有足够的时间启动并开始响应下一个请求。
现在,同样在主函数中,使用fmt设置请求URL。Sprintf将http://localhost主机名和服务器正在侦听的serverPort值。然后,使用http。获取对该URL的请求,如下所示:
- go run main.go
您将看到以下输出:
Output
server: GET /
client: got response!
client: status code: 200
在输出的第一行,服务器打印它从客户机收到了一个GET请求,请求/路径。然后,下面两行表示客户机从服务器返回了一个响应,并且该响应的状态代码是200。
http:。Get函数对于快速HTTP请求很有用,如您在本节中所做的请求。然而,http。请求提供了更广泛的选项来定制您的请求。
使用http.Request提出请求
与http相反。获取,http。请求函数为您提供了对请求的更大控制,而不仅仅是HTTP方法和被请求的URL。您还不会使用其他功能,而是使用http。现在请求,您将能够在本教程稍后添加这些自定义设置。
在您的代码中,第一个更新是更改HTTP服务器处理程序,以使用fmt.Fprintf返回假JSON数据响应。如果这是一个完整的HTTP服务器,那么该数据将使用Go的encoding/json包生成。如果您想了解有关在围棋中使用JSON的更多信息,我们的“如何在围棋里使用JSON”教程提供了。此外,您还需要将io/ioutil作为导入包含进来,以便在本次更新中稍后使用。
现在,打开主管道。再次转到文件并更新您的程序以开始使用http。请求如下:
package main
import (
...
"io/ioutil"
...
)
...
func main() {
...
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Printf("server: %s /\n", r.Method)
fmt.Fprintf(w, `{"message": "hello!"}`)
})
...
现在,更新您的HTTP请求代码,以代替使用HTTP。要向服务器发出请求,请使用http。NewRequest和http。DefaultClient的Do方法:
...
requestURL := fmt.Sprintf("http://localhost:%d", serverPort)
req, err := http.NewRequest(http.MethodGet, requestURL, nil)
if err != nil {
fmt.Printf("client: could not create request: %s\n", err)
os.Exit(1)
}
res, err := http.DefaultClient.Do(req)
if err != nil {
fmt.Printf("client: error making http request: %s\n", err)
os.Exit(1)
}
fmt.Printf("client: got response!\n")
fmt.Printf("client: status code: %d\n", res.StatusCode)
resBody, err := ioutil.ReadAll(res.Body)
if err != nil {
fmt.Printf("client: could not read response body: %s\n", err)
os.Exit(1)
}
fmt.Printf("client: response body: %s\n", resBody)
}
在此更新中,您使用http。NewRequest函数生成http。请求值,或在无法创建值时处理错误。与http不同。不过,Get函数是http。NewRequest函数不会立即向服务器发送HTTP请求。由于它不会立即发送请求,因此您可以在发送请求之前对其进行任何更改。
一旦http。创建和配置请求时,使用http的Do方法。DefaultClient将请求发送到服务器。http:。DefaultClient值是Go的默认HTTP客户端,与您在HTTP.Get中使用的相同。不过,这一次,您直接使用它来告诉它发送您的http.Request。HTTP客户端的Do方法返回从HTTP接收到的相同值。Get函数,以便您可以以相同的方式处理响应。
打印请求结果后,使用ioutil。ReadAll函数读取HTTP响应的正文。身体是一个io。ReadCloser值,io的组合。阅读器和io。更近一些,这意味着你可以使用任何可以从io读取的东西来读取身体的数据。读取器值。ioutil。ReadAll函数很有用,因为它将从io读取。读取器,直到它到达数据末尾或遇到错误。然后,它将返回数据作为[]字节值,您可以使用fmt打印。Printf或它遇到的错误值。
要运行更新的程序,请保存更改并使用go-run命令:
- go run main.go
这一次,您的输出应该与之前非常相似,但有一点:
Output
server: GET /
client: got response!
client: status code: 200
client: response body: {"message": "hello!"}
在第一行中,您可以看到服务器仍在接收对/path的GET请求。客户端还从服务器接收200响应,但它也在读取和打印服务器响应的正文。在更复杂的程序中,您可以接受{“message”:“hello!”}值,并使用encoding/JSON包将其处理为JSON。
在本节中,您使用HTTP服务器创建了一个程序,并以各种方式向其发出HTTP请求。首先,您使用了http。Get函数仅使用服务器的URL向服务器发出Get请求。然后,您将程序更新为使用http。NewRequest创建http。请求值。创建后,使用Go的默认HTTP客户端HTTP的Do方法。DefaultClient,以发出请求并打印http。输出的响应主体。
不过,HTTP协议不仅仅使用GET请求在程序之间进行通信。当您想从其他程序接收信息时,GET请求很有用,但当您想将信息从程序发送到服务器时,可以使用另一种HTTP方法POST方法。
发送POST请求
在REST API中,GET请求仅用于从服务器检索信息,因此为了让您的程序完全参与REST API,您的程序还需要支持发送POST请求。POST请求几乎与GET请求相反,客户端将数据发送到请求主体中的服务器。
在本节中,您将更新程序以将请求作为POST请求而不是GET请求发送。您的POST请求将包括一个请求主体,您将更新服务器以打印出有关您从客户端发出的请求的更多信息。
要开始进行这些更新,请打开主菜单。转到文件并添加一些您将要使用的新包到导入语句中:
... import ( "bytes" "errors" "fmt" "io/ioutil" "net/http" "os" "strings" "time" ) ...
然后,更新您的服务器处理程序函数,以打印有关传入请求的各种信息,例如查询字符串值、头值和请求正文:
... mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { fmt.Printf("server: %s /\n", r.Method) fmt.Printf("server: query id: %s\n", r.URL.Query().Get("id")) fmt.Printf("server: content-type: %s\n", r.Header.Get("content-type")) fmt.Printf("server: headers:\n") for headerName, headerValue := range r.Header { fmt.Printf("\t%s = %s\n", headerName, strings.Join(headerValue, ", ")) } reqBody, err := ioutil.ReadAll(r.Body) if err != nil { fmt.Printf("server: could not read request body: %s\n", err) } fmt.Printf("server: request body: %s\n", reqBody) fmt.Fprintf(w, `{"message": "hello!"}`) }) ...
在服务器HTTP请求处理程序的更新中,您添加了一些更有用的fmt.Printf语句,以查看有关传入请求的信息。您使用r.URL.Query().Get获取名为id的查询字符串值,使用r.Header.Get获得名为content-type的头的值。您还可以使用带有r.Header的for循环来打印服务器接收到的每个HTTP头的名称和值。如果您的客户机或服务器没有按照预期的方式运行,则此信息对于解决问题非常有用。最后,您还使用ioutil.ReadAll函数在r.body中读取HTTP请求的正文。
更新服务器处理程序函数后,更新主函数的请求代码,以便它发送带有请求主体的POST请求:
... time.Sleep(100 * time.Millisecond) jsonBody := []byte(`{"client_message": "hello, server!"}`) bodyReader := bytes.NewReader(jsonBody) requestURL := fmt.Sprintf("http://localhost:%d?id=1234", serverPort) req, err := http.NewRequest(http.MethodPost, requestURL, bodyReader) ...
在对主函数请求的更新中,您正在定义的一个新值是jsonBody值。在本例中,该值表示为[]字节而不是标准字符串,因为如果您使用encoding/json包对json数据进行编码,它将返回[]字节,而不是字符串。
下一个值bodyReader是字节。包装jsonBody数据的读取器。一个http。请求正文要求值为io。Reader和jsonBody的[]字节值不实现io。阅读器,所以您不能将其单独用作请求主体。字节。存在读取器值以提供该io。读取器接口,因此可以使用jsonBody值作为请求主体。
requestURL值也被更新为包括id=1234查询字符串值,主要是为了显示查询字符串值如何也可以与其他标准URL组件一起包含在请求URL中。
最后,http。NewRequest函数调用被更新为使用带有http的POST方法。通过将最后一个参数从nil主体更新为bodyReader(JSON数据io.Reader)来包含请求主体。
保存更改后,可以使用go run运行程序:
go run main.go
由于您更新服务器以显示其他信息,输出将比以前更长:
Output
server: POST /
server: query id: 1234
server: content-type:
server: headers: Accept-Encoding = gzip User-Agent = Go-http-client/1.1 Content-Length = 36
server: request body: {"client_message": "hello, server!"}
client: got response!
client: status code: 200
client: response body: {"message": "hello!"}
来自服务器的第一行显示您的请求现在作为POST请求传递到/path。第二行显示添加到请求URL的id查询字符串值的1234值。第三行显示了客户端发送的Content-Type头的值,该头在该请求中恰好为空。
第四行可能与上面看到的输出略有不同。在Go中,当您使用range迭代映射值时,无法保证映射值的顺序,因此您的r.headers头可能会以不同的顺序打印出来。根据您使用的Go版本,您可能还会看到与上面不同的User Agent版本。
最后,输出中的最后一个变化是服务器显示了从客户端接收到的请求主体。然后,服务器可以使用encoding/json包解析客户端发送的json数据并制定响应。
在本节中,您更新了程序以发送HTTP POST请求而不是GET请求。您还更新了程序,以发送一个请求正文,其中[]字节数据由bytes.Reader读取。最后,您更新了服务器处理程序函数,以打印出有关HTTP客户端正在发出的请求的更多信息。
通常,在HTTP请求中,客户端或服务器会告诉对方它在正文中发送的内容类型。正如您在上一个输出中看到的那样,您的HTTP请求没有包含Content-Type头来告诉服务器如何解释正文的数据。在下一节中,您将进行一些更新来定制HTTP请求,包括设置Content-Type头,让服务器知道您发送的数据类型。
自定义HTTP请求
随着时间的推移,HTTP请求和响应被用于在客户端和服务器之间发送更多种类的数据。在某一点上,HTTP客户端可能会认为他们从HTTP服务器接收的数据是HTML,并且很有可能是正确的。不过现在,它可以是HTML、JSON、音乐、视频或任何其他数据类型。为了提供有关通过HTTP发送的数据的更多信息,协议包括HTTP头,其中一个重要的头是Content-Type头。这个头告诉服务器(或客户端,取决于数据的方向)如何解释它正在接收的数据。
在本节中,您将更新程序以在HTTP请求上设置Content-Type头,以便服务器知道它正在接收JSON数据。您还将更新您的程序以使用除Go的默认HTTP之外的HTTP客户端。DefaultClient,以便您可以自定义发送请求的方式。
要进行这些更新,请打开主菜单。再次打开文件并更新主函数,如下所示:
... req, err := http.NewRequest(http.MethodPost, requestURL, bodyReader) if err != nil { fmt.Printf("client: could not create request: %s\n", err) os.Exit(1) } req.Header.Set("Content-Type", "application/json") client := http.Client{ Timeout: 30 * time.Second, } res, err := client.Do(req) if err != nil { fmt.Printf("client: error making http request: %s\n", err) os.Exit(1) } ...
在此更新中,您可以访问http。使用req请求标头。Header,然后将请求的Content-Type头的值设置为application/json。application/json媒体类型在媒体类型列表中定义为json的媒体类型。这样,当服务器接收到您的请求时,它知道将主体解释为JSON,而不是XML。
下一个更新是创建自己的http。客户端变量中的客户端实例。在此客户端中,您将超时值设置为30秒。这一点很重要,因为它表示,向客户端发出的任何请求都将在30秒后放弃并停止尝试接收响应。Go的默认http。DefaultClient没有指定超时,因此如果您使用该客户端发出请求,它将等待直到收到响应、服务器断开连接或程序结束。如果您有许多请求像这样等待响应,那么您可能正在使用计算机上的大量资源。设置超时值可限制请求在您定义的时间之前等待的时间。
最后,您更新了请求,以使用客户端变量的Do方法。您不需要在此进行任何其他更改,因为您一直在http上调用Do。客户重视整个时间。Go的默认HTTP客户端HTTP。DefaultClient只是一个http。默认情况下创建的客户端。所以,当你调用http时。Get,函数正在为您调用Do方法,当您更新请求以使用http时。DefaultClient,您使用的是http。客户直接。现在唯一的区别是您创建了http。你这次使用的客户价值。
现在,保存文件并使用go-run运行程序:
go run main.go
您的输出应该与之前的输出非常相似,但包含有关内容类型的更多信息:
Output server: POST / server: query id: 1234 server: content-type: application/json server: headers: Accept-Encoding = gzip User-Agent = Go-http-client/1.1 Content-Length = 36 Content-Type = application/json server: request body: {"client_message": "hello, server!"} client: got response! client: status code: 200 client: response body: {"message": "hello!"}
您将看到来自服务器的内容类型值,以及客户端正在发送的内容类型标头。这就是为什么可以同时为JSON和XML API提供相同的HTTP请求路径。通过指定请求的内容类型,服务器和客户端可以对数据进行不同的解释。
不过,这个示例不会触发您配置的客户端超时。要查看当请求花费太长时间并且触发超时时会发生什么,请打开主窗口。去文件并添加时间。对HTTP服务器处理程序函数的睡眠函数调用。然后,腾出时间。睡眠持续时间超过指定的超时时间。在这种情况下,将其设置为35秒:
... func main() { go func() { mux := http.NewServeMux() mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { ... fmt.Fprintf(w, `{"message": "hello!"}`) time.Sleep(35 * time.Second) }) ... }() ... }
现在,保存更改并使用go run运行程序:
go run main.go
当您这次运行它时,退出的时间将比以前长,因为它在HTTP请求完成后才会退出。由于添加了time.Sleep(35*time.Second),HTTP请求在达到30秒超时之前不会完成:
Output server: POST / server: query id: 1234 server: content-type: application/json server: headers: Content-Type = application/json Accept-Encoding = gzip User-Agent = Go-http-client/1.1 Content-Length = 36 server: request body: {"client_message": "hello, server!"} client: error making http request: Post "http://localhost:3333?id=1234": context deadline exceeded (Client.Timeout exceeded while awaiting headers) exit status 1
在这个程序输出中,您可以看到服务器收到了请求并进行了处理,但当它到达HTTP处理程序函数的末尾时,您的时间就到了。睡眠功能调用是,它开始睡眠35秒。同时,HTTP请求的超时正在倒计时,并在HTTP请求完成之前达到30秒的限制。这将导致客户端。Do方法调用失败,上下文截止日期超过错误,因为请求的30秒截止日期已过。然后,您的程序使用os.Exit(1)退出,失败状态代码为1。
在本节中,您更新了程序,通过向HTTP请求添加Content-Type头来定制HTTP请求。您还更新了程序以创建新的http。客户端超时30秒,然后使用该客户端发出HTTP请求。您还通过添加时间测试了30秒超时。睡眠到HTTP请求处理程序。最后,您还了解了为什么使用自己的http很重要。如果您想避免许多请求可能永远处于空闲状态,请设置超时的客户端值。
结论
在本教程中,您使用HTTP服务器创建了一个新程序,并使用Go的net/HTTP包向该服务器发出HTTP请求。首先,您使用了http。Get函数使用Go的默认HTTP客户端向服务器发出Get请求。然后,您使用了http。带有http的NewRequest。DefaultClient的Do方法发出GET请求。接下来,您使用bytes.NewReader更新了请求,使其成为带有正文的POST请求。最后,您在http上使用了Set方法。Request's Header字段用于设置请求的Content-Type头,并通过创建自己的HTTP客户端而不是使用Go的默认客户端来设置请求持续时间的30秒超时。
net/http包不仅仅包括您在本教程中使用的功能。它还包括一个http。Post函数,可用于发出Post请求,类似于http。获取函数。该软件包还支持保存和检索cookie等功能。
- 登录 发表评论