diff --git a/chap-35-build-an-http-client/35-build-an-http-client.md b/chap-35-build-an-http-client/35-build-an-http-client.md new file mode 100644 index 0000000000000000000000000000000000000000..d5269ba58a38efd6e80c73eaaa5d5c69051fa590 --- /dev/null +++ b/chap-35-build-an-http-client/35-build-an-http-client.md @@ -0,0 +1,601 @@ +# 构建一个 HTTP 客户端 + +## 1 本章中能学到的内容 +* 什么是客户端/服务器模式 +* 如何构建一个 HTTP 客户端 +* 如何发送 HTTP 请求 +* 如何在请求中加入特定头部信息 + +## 2 本章中提及的技术概念 +* HTTP 头部信息 +* 客户端/服务器 +* HTTP 协议 + +## 3 客户端/服务器 +### 3.0.0.1 示例 1 +想象你已经编写了一个 Go 程序管理你的照片。这个程序在电脑上管理你的照片集。你想要将照片分享给你的家人和朋友,你可以通过发送电子邮件的方式。当你有一万张照片时,这个解决方案将不再可行。 +你可以将照片上传到你最喜欢的社交网站上。如果一张一张的上传,这个操作很消耗时间。另外的解决方案就是让你的程序自动上传到社交网络上。 +我们可以通过社交网络公开的 API 完成这一操作。可以用一个 for 循环调用他们的 API 发送图片。 +在这个案例中,你的程序将调用 API。你的程序就是客户端,社交网络作为服务器。 + + +### 3.0.0.2 示例 2 +接受和处理支付流程是一件困难的工作。很多主要的电商平台都会使用支付服务供应商。电商网站会通过调用支付服务供应商的 API 来实现信用卡支付流程。在这一过程中,电商网站是客户端,支付服务供应商是服务器。 + +### 3.0.0.3 示例 3 +如果你想在你的网站里集成一个小工具来展示旧金山的温度,你可以创建一个 API 客户端发送一请求个到气象 API (服务器)。然后解析 API 的响应,最后在网站上展示。 + +### 3.0.0.4 示例 4 +调用一个 API 意味着发送一个精确定义的 HTTP 请求到 web 服务器。 +客户端和服务器这两个定义很重要,千万切记 +* 客户端使用(或调用)一个 API +* 服务器是一个用来接受并响应客户端 API 请求的程序。 +![客户端-服务器](./images/client_server.png) + +## 4 什么是 REFT API +REST 代表着 Representational State Transfer (表述性状态转移)。Roy Fielding 在他2000年的博士论文里首创了这个术语。Fielding 想要开发一个 ”现代的网络构架模型"。他提出的 REST 是一种 "构架模型”, 一个构架规范来创建网络应用。 + +注意,Fielding 定义了 REST 的概念,但是我们已经在他的论文之前使用了这样的构架规范。REST 现在被广泛的应用在 web 社区里,我们甚至发明了一个形容词来形容使用这样构架模型的 API - RESTful。 + +API 的消费者和生产者(创建生产者的程序员们)都在这种规范中获益。API 构建者们可以遵循规范,也可以选择忽略限制。在另一边,因为在技术层面遵循统一的标准,API 的消费者可以更快的构建程序。有大量的模块和包可以用来构建服务器端和客户端。 + +这些规范包括: +* 交流是无状态的:这意味着服务器不需要保存会话信息,每一个请求被独立响应与处理。服务器不会记得之前的请求。所以每个请求需要包含所有关键信息方便服务器处理。 +* 可以缓存的响应:服务器的响应能够被缓存用于以后的请求。 +* 统一的接口:REST APIs 总是给客户端提供相同的接口。这些接口有以下三个子规范: + * 资源定位:服务中的每一个资源(订单,产品,照片)都被一个统一资源定位符(URI)所定位。 + * 操作资源的表现形式来:当调用 API 时,可以控制资源的表现形式。资源可以表现为 JSON, XML, YAML... + * 自描述的消息:在客户端和服务器之间的消息必须含有足够的信息来让其被正确的处理,这些信息应该被转换成客户端可以轻松理解的信息。为了实现这一目标,API 应该包括 HTTP 方法(HTTP 1.1 定义了 8 种方法),HTTP 头部信息,请求体,以及查询字符串。 + * 超链接作为程序状态的引擎:web 服务器能够发送指向资源自己或者其他资源的链接。例如,一个客户发送一个请求 `https://maximilien-andlie.com/product/2` 来获取关于 2 号产品的信息,web 服务器应该可以给客户端发送关于产品其他特性(颜色,大小,...) 的链接。 +* 有层次的系统:当构建一个 API 时,你可以有一个指定的系统来处理请求的权限问题,另外一个部分负责从数据库中提取数据并构建响应。这些层级对于客户端来说是透明的。 +* 根据需求返回代码:最后的这个规范是可选择的。在现实中,也没有被业界广泛使用。它允许服务器可以返回一个脚本。客户端可以在系统中执行这个脚本。广告业有大量使用这个功能,他们使用 API 来发送 Javascript 或者 HTML 横幅到客户端 + +## 5 一个基本的 HTTP 客户端 +客户端是发送请求到服务的程序部分。 +我们使用 `net/http` 中标准的 Go 客户端。想要创建一个 HTTP 客户端,可以创建一个 http.Client 类型的变量。 + +``` +// consuming-api/simple/main.go +package main + +import ( + "fmt" + "io/ioutil" + "net/http" + "time" +) + +func main() { + c := http.Client{Timeout: time.Duration(1) * time.Second} + resp, err := c.Get("https://www.google.com") + if err != nil { + fmt.Printf("Error %s", err) + return + } + defer resp.Body.Close() + body, err := ioutil.ReadAll(resp.Body) + fmt.Printf("Body : %s", body) +} +``` + +当创建 HTTP 客户端时,可以设置以下选项: +`Transport` (类型:`http.RoundTripper`)你可以设置 HTTP 请求的这个选项到一个实现了 http.RoundTripper 这个接口的类型。这是一个高级选项,大多时候并不需要。当创建服务器时,可以忽略这个设置。默认情况下,`DefaultTransport` 会被使用。如果你对此感兴趣,可以查看 Go 中源码:`https://golang.org/src/net/http/transport.go` + +`CheckRedirect` (类型 `func(req *Request, vis []*Request)`):可是使用这个选项来定义一个函数,每当请求被重定向之后,这个函数就会被调用。当服务器发送一个特殊响应的时候,重定向会被触发。重定向响应有一个特殊的状态码( HTTP 状态码)(查看基础 HTTP 协议部分来了解关于重定向的更多知识)。 + * 这个的用处是在跟随重定向跳转前,进行相应的检查。 + +`Jar`(类型 `CookieJar`):这个选项是用来设置请求 Cookies 的。Cookie 被传输到服务器。服务器自身也会添加 Cookies 到请求中。这些“进入 Cookies”会被添加到`Jar`中。 + * Cookie 是一小段数据,被客户端的浏览器存储在本地 + * 可以通过 javascript 代码来设置 cookies + * 服务器也可以使用特定的头部选项来设置 cookies + +`Timeout` (类型 `time.Duration`):客户端会通过 HTTP 打开一条通往服务器的链接,服务器也许会需要一些时间来响应客户端的请求。这个选项定义了最长的响应等待时间。如果这个选项没有被设定,默认没有链接超时。这对于客户端侧的用户体验是不友好的。在我看来,显示链接超时的错误好过让用户一直等待下去。请注意,这个超时的时间包括: + * 链接时间(连接到远端服务器所需要的时间) + * 被重定向占用的时间(如果有) + * 解析响应体所占用的时间 + +这里,我们将创建一个 DefaultTransport, 没有 CheckRedirect 函数,没有 cookies,超时时长为 1 秒的 HTTP 客户端。 +使用这个客户端,我们可以发送一个 GET 请求到 URL http://wwww.google.com + +``` +resp, err := c.Get("https://www.google.com") +if err != nil { + fmt.Errorf("Error %s", err) + return +} +``` + +也可以用它发送一个 HTTP POST 请求 + +``` +myJson := bytes.NewBuffer([]byte(`{"name":"Maximilien"}`)) +resp, err := c.Post("https://www.google.com", "application/json", myJson) +if err != nil { + fmt.Errorf("Error %s", err) + return +} +``` + +注意,在发送 POST 请求时,除了URL,需要定义请求体的信息以及请求体数据的格式。请求体就是我们需要传输的数据。为什么不使用 `JSON` 字符串?为什么不使用 `bytes.Buffer`?因为这个变量必须实现 `io.Reader` 接口。 + +使用 HEAD 来发送请求的语法 +``` +resp, err = c.Head("https://www.google.com") +if err != nil { + fmt.Errorf("Error %s", err) + return +} +fmt.Println(resp.Header) +``` + +注意, HEAD 请求不会返回响应体,而是会返回响应头部。(这个方法用来测试 URL,检查是否 URL 可用,正确或者是否有改变) + +让我们回到之前最后一部分代码 +``` +defer resp.Body.Close() +body, err := ioutil.ReadAll(resp.Body) +fmt.Printf("Body : %s", body) +``` + +我在函数结束之前,我们使用 `defer resp.Body.Close()` 来关闭返回体解析。`resp.Body` 是 http 客户端解析的数据流。当函数结束时,千万别忘记关闭解析返回体。否则,客户端可能无法重用这个和服务器的链接。(参考:https://golang.org/pkg/net/http/#Client.Do) + +## 6 请求头部 +### 6.1 什么是请求头部 +请求头部,简称头部,是一些附加在请求里的信息。客户端(你的程序)可以添加这些关于请求的附加信息提交给服务器。 +HTTP 1/1 定义了一些可使用的头部信息,这里选取一些常用的 + +`Content-Type`:用来指定给服务器请求的多媒体类型 - 例子:`application/json; chrset=utf-8` => 使用基于 UTF-8 字符集 JSON 数据 + +`Content-Length`:指定信息的大小(使用字节为单位)- 例子:`42` + +`User-Agent`:发送请求的程序的名称和版本号 - 例子:`curl/7.16.3 libcurl/7.16.3 OpenSSL/0.9.7l zlib/1.2.3` 使用 curl 发送请求 `Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.182 Safari/537.36` 使用 chrome 在 MacBook + +`Accept`: 指定可接受的响应的多媒体类型 - 例子:`*/*` 接受所有的媒体类型, `application/json` 只接受 JSON 类型 + +`Accept-Encoding`:指定可接受的响应的字符集 - 例子:`gzip` 可接受 gzip 压缩的内容 + +`Authorization`:这个头部设定中,发送者能够指定他的身份验证信息( API key,用户名/密码, JWT ...)- 例子:`Bearer cGFydDJibGEcGFydDJibGEcGFydDJibGEcGFydDJibGE=.cGFydDJcGFydDJibGEcGFydDJibGEcGFydDJibGEibGE=.eW9sbcGFydDJibGEcGFydDJibGEcGFydDJibGEw==` + +### 6.2 如何添加请求头部 +添加头部信息需要创建请求。这个过程是冗长和复杂的,但是能够给你更多的控制 +``` +// consuming-api/request-building/main.go +package main + +import ( + "fmt" + "io/ioutil" + "net/http" + "time" +) + +func main() { + + c := http.Client{Timeout: time.Duration(1) * time.Second} + req, err := http.NewRequest("GET", "http://www.google.fr", nil) + if err != nil { + fmt.Printf("error %s", err) + return + } + req.Header.Add("Accept", `application/json`) + resp, err := c.Do(req) + if err != nil { + fmt.Printf("error %s", err) + return + } + defer resp.Body.Close() + body, err := ioutil.ReadAll(resp.Body) + fmt.Printf("Body : %s", body) +} +``` + +需要创建一个客户端,然后可以使用有如下签名的 `http.NewRequest` 方法 +``` +(method, url string, body io.Reader) (*Request, error) +``` + +方他的第一个参数可以使用以下的选项: +* OPTIONS +* GET +* HEAD +* POST +* PUT +* DELETE +* TRACE +* CONNECT + +第二个参数是 URL。 +第三个参数是请求体。在我们这个例子中,我们设置为 nil 因为我们没有请求体。 + +这个方法不会发送请求,而是会返回一个 `http.Request`。 + +使用 `req.Header.Add`(req 是一个 http.Request 类型的结构体)可以给请求添加头部信息: +``` +req.Header.Add("Accept", `application/json`) +``` + +注意,Go 是通过 map[string] []string 这个 map 来实现请求头部的。这个 map 的键是字符串,值是切片字符串。 + +使用 `c.Do(req)` 发送请求。 + +### 6.3 默认添加的头部信息 +Go 在请求中会自动添加一些头部信息 +在标准的 GET 请求中: +``` +c.Get("http://localhost:8091/status") +``` +* Accpet-Encoding: gzip +* User-Agent: Go-http-client/1.1 + +在标准的 POST 请求中: +``` +c.Post("http://localhost:8091/status", "application/json", bytes.NewBuffer([]byte("42"))) +``` +* Accept-Encoding: gzip +* User-Agent: Go-http-client/1.1 +* Content-Length: 2 +* Content-Type: application/json + +## 7 真实案例: GitHub API +### 7.0.1 第一个请求 +在这个部分,我们会建立一个 HTTP 客户端,发送请求到 GitHub API. GitHub 允许分享代码和同其他程序员一同工作。 +GitHub 提供了 API 来查询项目,获取和更新项目中的问题。GitHub 开放的 API 提供了巨大的潜力。 +GitHub API 的官方文档在这里 `https://developer.github.com/v3` +在这个例子里,我们使用 v3 API。 +GitHub API 基本 URL 是 `https://api.github.com/`;让我们来发送一个 HTTP GET 请求。 +``` +// consuming-api/github/main.go + +c := http.Client{Timeout: time.Duration(1) * time.Second} +req, err := http.NewRequest("GET", "https://api.github.com/", nil) +if err != nil { + fmt.Printf("error %s", err) + return +} +req.Header.Add("Accept", `application/json`) +resp, err := c.Do(req) +if err != nil { + fmt.Printf("error %s", err) + return +} +defer resp.Body.Close() +body, err := ioutil.ReadAll(resp.Body) +if err != nil { + fmt.Printf("error %s", err) + return +} +fmt.Printf("Body : %s \n ", body) +fmt.Printf("Response status : %s \n", resp.Status) +``` +当我们运行这个脚本,会得到下面的输出: +``` +Body : {"current_user_url":"https://api.github.com/user","current_user_authorizations_html_url":"https://github.com/settings/connections/applications{/clie +nt_id}", +... +} + + Response status: 200 OK +``` + +响应体是 JSON 格式,注意,三个点(...)是这里没有打印出全部的响应体。这个响应的状态码是“200 OK”,意味着请求是成功的。 +响应体提示我们可以使用 GitHub API 其他的路径。获取现在的用户,可以发送请求到 `https://api.github.com/user`。 让我们来试一下: +``` +req, err := http.NewRequest("GET", "https://api.github.com/user", nil) +``` +响应并不是很迷人: +``` +Body : {"message":"Requires authentication","documentation_url":"https://developer.github.com/v3/users/#get-the-authenticated-user"} + +Response status: 401 Unauthorized +``` +状态码是 `401 Unauthorized`,这意味着这个请求需要确认用户身份。这个请求必须要包括一个 WWW-Authenticate 头信息,这个信息需要需要有足够的权限来访问请求资源。 +我们所请求的资源需(查看当下用户部分获得更多信息)需要我们提供我们的身份证明。为了完成请求,我们需要在 GitHub 上注册。如果你已经有了 GitHub 账户,你可以访问 `https://github.com/settings/tokens`。如果你没有账户,可以创建一个。注意,你可以在没有 GitHub 账户的情况下跟随这篇教程。这里的关键是如何使用一个 API。 +大多是时候,API 的提供者会需要你注册之后才能使用他们的 API,这样方便他们对于掌控数据的接触。 + +### 7.0.2 给请求授权 +导航到 `https://github.com/settings/tokens`。在这里,点击“Generate new token”按钮。 +![生成令牌](./images/github_new_token.png) +生成 GitHub 个人访问令牌 + +这里要求给这个个人访问令牌一个名字,并且选择这个令牌可以访问的 GitHub 资源。这样可以严格控制每一个令牌的权限。在我们的例子里,我们需要选择 “Gist” 权限。你可以通过 Gist 来同其他的程序员分享代码。我们将使用 GitHub API 来创建 Gist。 + +![生成令牌](./images/github_personnal_token.png) +生成 GitHub 个人访问令牌 +![选择 Gist 权限](./images/create_gist_scope.png) +选择 Gist 权限 + +一旦你拿到了个人访问令牌,你就可以把它嵌入你的请求当中。你可能会问,要放在哪里?我们遵循 HTTP 1/1 RFC,所以我们把他嵌入在 `Authenticate` 头选项中。因为没有一个统一的嵌入身份信息令牌的方式,你需要阅读 API 的文档。 + +![GitHub API 的验证头部信息](./images/authentification_github_api_doc.png) +GitHub API 的验证头信息 + +让我们来尝试发之前的请求,但是这次我们根据 GitHub 文档来设置身份验证信息头部。 +``` +// current user +req, err = http.NewRequest("GET", "https://api.github.com/user", nil) +// TODO : handle error +req.Header.Add("Accept", `application/json`) +// add header for authentication +req.Header.Add("Authorization", fmt.Sprintf("token %s", os.Getenv("TOKEN")) +// ... +``` +这其中最重要的是这个部分 +``` +req.Header.Add("Authorization", fmt.Sprintf("token %s", os.Getenv("GITHUB_TOKEN")) +``` +注意我们没有把访问令牌直接写入我们的代码。如果你分享你的代码给其他人,这是一个非常糟糕的行为。因为你不想其他的程序员通过这个令牌来接入你的 GitHub 账户。 +取而代之,我们使用 `os.Getenv("GITHUB_TOKEN")`。os 标准包里的 `Getenv` 方法可以获取名字为 `GITHUB_TOKEN` 的变量,并且解析。注意这是程序的配置,也可以使用一个配置文件。网络上有很多可以管理配置文件的包。 +用如下命令来设置环境变量。 +``` +$ GITHUB_TOKEN=aabbcc go run main.go +``` + +### 7.0.3 创建一个 gist +第一件事就是阅读文档。因为我们想要创建数据,我们需要使用 POST 方法。URL 也许包含 gist,这就是一切了。下面是一个文档网页的截图 +![POST /gists 文档](./images/create_gist_documentation.png) +POST /gists 文档 + +这个端点有如下的指示: +* HTTP 的方法是 POST +* 输入的参数是 + `files`(是一个对象) + `description`:一个字符串 + `public`:布尔值 +实现这个调用 +``` +// consuming-api/github-post-gist/main.go + +req, err := http.NewRequest("POST", "https://api.github.com/gists", bytes.NewBuffer(gistRequestJson)) +if err != nil { + fmt.Printf("%s", err) + return +} +req.Header.Add("Accept", `application/json`) +// add header for authentication +req.Header.Add("Authorization", fmt.Sprintf("token %s", os.Getenv("TOKEN"))) + +resp, err := c.Do(req) +if err != nil { + fmt.Printf("Error %s", err) + return +} +``` + +没有嵌入要求的参数时候得到的返回错误信息如下 +``` +Body : {"message":"Invalid request.\n\nFor 'links/0/schema', nil is not an object.","documentation_url":"https://developer.github.com/v3/gists/#create-a-gi +st"} + Response status: 422 Unprocessable Entity +``` + +我们得到了一个错误提示,可见 GitHub 服务器有一个输入验证模块来控制用户输入。 + +这些参数都会被转换到请求体。参数会被编码在 JSON 里。我们需要创建一个结构体来接受参数。 +``` +type GistRequest struct { + Files map[string]File `json:"files"` + Description string `json:"description"` + Public bool `json:"public"` +} +type File struct { + Content string `json:"content"` +} +``` +这个结构体接受参数 +``` +files := map[string]File{ + "main.go": File{"test"}} +gistRequest := GistRequest{ + Files: files, + Description: "this is a test", + Public: false} +``` + +变量 `gistRequest` 需要被转换成可用的 JSON 字符串: +``` +gistRequestJson, err := json.Marshal(gistRequest) +if err != nil { + fmt.Printf("%s", err) + return +} +``` + +接着我们需要把请求体放进我们的新的 JSON 里。这里有一些小困难:`json.Marshal` 返回一个切片字节类型。 `NerRequest` 函数的签名是 +``` +func NewRequest(method, url string, body io.Reader) (*Request, err) +``` +请求体需要实现 `io.Reader` 类型的接口。 + +一个简单的方式来实现这个,就是使用标准包 `bytes`: `bytes.NewBuffer(gistRequestJson)` +`bytes.NewBuffer` 的返回值实现了 `io.Reader` 接口。 +现在我们可以发送请求了 +``` +req, err := http.NewRequest("POST", "https://api.github.com/gists", bytes.NewBuffer(gistRequestJson)) +req.Header.Add("Accept", `application/json`) +req.Header.Add("Authorization", fmt.Sprintf("token %s", os.Getenv("TOKEN"))) +resp, err := c.Do(req) +//... +``` + +服务器响应了成功 +``` +Body : {"url":"https://api.github.com/gists/c960d211532f7c35aeb0c854892bf108",...} + Response status : 201 Created +``` + +Gist 已经被成功创建。可以通过图片 1 来查看。新资源的 URI 也在包含在响应体之中: https://api.github.com/gists/c960d211532f7c35aeb0c854892bf108 +这个并不是一个永远使用的案例,因为不是所有的 API 都会严格遵循 RFC 的推荐。当你没有在响应体里得到 URI 的时候,请不要惊讶。响应的状态码也不一定总是“201 Created”,也可能是“200 OK”。 + +大多数时候,你需要根据集成的 API 来调整你的客户端。响应返回状态码并不一定会一样,验证方法也许会不同 +![通过 API 成功创建 gist](./images/gist_created.png) +通过 API 成功创建 gist + +### 7.0.4 更新一个 gist +我们已经创建了一个资源。现在我们可以尝试更新它。大多数时候,你使用 HTTP PUT 或者 HTTP PATCH 请求来进行更新操作。 +让我们来看一下 GitHub API 文档。 + +![更性 gist API 的文档](./images/edit_a_gist.png) +更新一个 Gist API 的文档 + +这个请求使用 PATCH 方法。URL 是 `/gist/:gist_id`,意味着我们需要使用我们想修改的 gist 的 id 来替换 `gist_id`。在 REST API 的世界里,每一个资源都有一个身份号码,在这里,gist 的身份码被重命名为 `gist_id`。我们这个例子里,gist 的身份码是 `c960d211532f7c35aeb0c854892bf108`。 所以请求的 URL 是: +``` +"https://api.github.com/gists/c960d211532f7c35aeb0c854892bf108" +``` + +这个参数和 POST 请求是一样的(创建 gist)。下面是这个请求体 +``` +// consuming-api/github-patch-gist/main.go + +files := map[string]File{ + "main.go": File{"test updated"}} +gistRequest := GistRequest{ + Files: files, + Description: "this is a test", + Public: false} +gistRequestJson, err := json.Marshal(gistRequest) +if err != nil { + fmt.Printf("%s", err) + return +} +``` + +这里我们将 main.go 的内容更新为 `test updated`。下一件事就是创建一个 HTTP 客户端,创建一个请求,执行发送,和展示结果。 +``` +// consuming-api/github-patch-gist/main.go + +c := http.Client{Timeout: time.Duration(4) * time.Second} + +req, err := http.NewRequest("PATCH", "https://api.github.com/gists/79c9cec21a116f6ee166fd73ba750565", bytes.NewBuffer(gistRequestJson)) +if err != nil { + fmt.Printf("%s", err) + return +} +req.Header.Add("Accept", `application/json`) +// add header for authentication +req.Header.Add("Authorization", fmt.Sprintf("token %s", os.Getenv("TOKEN"))) + +resp, err := c.Do(req) +if err != nil { + fmt.Printf("Error %s", err) + return +} +defer resp.Body.Close() +body, err := ioutil.ReadAll(resp.Body) +fmt.Printf("Body : %s \n ", body) +fmt.Printf("Response status : %s \n", resp.Status) +``` + +执行结果是 +``` +Body : {"url":"https://api.github.com/gists/c960d211532f7c35aeb0c854892bf108",...} + Response status : 200 OK +``` + +响应状态码 200 意味着服务器正确的处理了请求。 + +在 GitHub 网站上,我们的 gist 已经被更新了(看图 3) +![通过 API 调用更新 Gist](./images/gist_updated.png) +通过 API 调用更新 Gist + +### 7.0.5 删除一个 gist +删除一个资源在网络上是一个普遍的操作。通常通过 HTTP DELETE 来实现这个操作。GitHub API 的文档很直接。 +![删除一个 gist 文档](./images/delete_gist_documentation.png) +删除一个 gist 的文档。 + +代码相对于之前的更为简洁,因为不需要包含请求体。 +``` +// consuming-api/github-gist-delete/main.go + +c := http.Client{Timeout: time.Duration(4) * time.Second} +req, err := http.NewRequest("DELETE", "https://api.github.com/gists/79c9cec21a116f6ee166fd73ba750565", nil) +if err != nil { + fmt.Printf("Error %s", err) + return +} +req.Header.Add("Accept", `application/json`) +// add header for authentication +req.Header.Add("Authorization", fmt.Sprintf("token %s", os.Getenv("TOKEN"))) + +resp, err := c.Do(req) +if err != nil { + fmt.Printf("Error %s", err) + return +} +defer resp.Body.Close() +body, err := ioutil.ReadAll(resp.Body) +if err != nil { + fmt.Printf("Error %s", err) + return +} +fmt.Printf("Body : %s \n ", body) +fmt.Printf("Response status : %s \n", resp.Status) +``` + +执行之后,收到一个空响应体的回复,状态码为 204,没有内容 +``` +Body : + +Response status : 204 No Content +``` +响应状态码是 2 开头,意味着我们的请求已经成功。Gist 已经被删除了。 + +## 8 自我测试 +### 8.1 测试问题 +1. 判断题:服务器负责发送请求到客户端,对还是错? +2. 写出两个 REST API 构架的限制(在 Roy Fielding论文中定义的) +3. 如何创建一个有 3 秒钟超时的 HTTP 客户端? +4. 应该在哪一个头部信息中放置用户的密码? +5. 使用默认的 Go HTTP 客户端发送一个 POST 请求,那些头部信息被默认设置? + +### 8.2 测试答案 +1. 判断题:服务器负责发送请求到客户端,对还是错? + 1. 错误 + 2. 服务器接受和处理请求,客户端发送请求。 +2. 写出两个 REST API 构架的限制(在 Roy Fielding论文中定义的) + 1. 无状态 + 2. 应答可以缓存 + 3. 统一接口 + 4. 有层次的系统 + 5. 根据需求编写代码 +3. 如何创建一个有 3 秒钟超时的 HTTP 客户端? +``` + c := http.Client{ Timeout:3 * time.Second } +``` +4. 应该在哪一个头部信息中放置用户的密码? + 1. 在 `Authorization` 头部中 +5. 使用默认的 Go HTTP 客户端发送一个 POST 请求,那些头部信息被默认设置? + 1. User-Agent + 2. Content-Length + 3. Content-Type + 4. Accept-Encoding + +## 9 要点 +* 在客户酸/服务器模式中,客户端请求资源(或服务)。服务器提供资源。 +* 客户酸发送请求,服务器接收和处理请求。 +* 客户端也可叫做消费者,服务器也可被称作生产者。 +* Roy Fielding 在他的博士论文里提出了 REST API 的概念 +* Roy Fielding 给出了一些关于创建 REST API 的构架规范 +* 创建一个 HTTP 客户端,需要初始化一个 http.Client 类型的变量 +``` +c := http.Client{ Timeout:1 * time.Second } +``` +可以使用这个客户端变量来发送请求 +``` +// 发送简单的 GET 和 POST 请求 +res, err := c.Get("http://localhost:8091/status") +// ... +res, err := c.Post("http://localhost:8091/status", "application/json", bytes.NewBuffer([]byte("42"))) + +// 构建更复杂的请求 +req, err := http.NewRequest("HEAD", "http://localhost:8091/status", nil) +// ... +req.Header.Add("Accept", `application/json`) +// 发送请求 +c.Do(req) +``` + +## 引用 +* [fielding2000architectural] Fielding, Roy T, and Richard N Taylor. 2000. Architectural Styles and the Design of Network-Based Software Architectures. Vol. 7. University of California, Irvine Doctoral dissertation. +* [fielding1999rfc] Fielding, R, J Gettys, J Mogul, H Frystyk, L Masinter, P Leach, and T Berners-Lee. 1999. “RFC 2616.” Hypertext Transfer Protocol–HTTP/1.1 2 (1): 2–2. +* [fielding1999hypertext] Fielding, Roy, Jim Gettys, Jeffrey Mogul, Henrik Frystyk, Larry Masinter, Paul Leach, and Tim Berners-Lee. 1999. “Hypertext Transfer Protocol–HTTP/1.1.” +* [fielding1999hypertext] Fielding, Roy, Jim Gettys, Jeffrey Mogul, Henrik Frystyk, Larry Masinter, Paul Leach, and Tim Berners-Lee. 1999. “Hypertext Transfer Protocol–HTTP/1.1.” diff --git a/chap-35-build-an-http-client/images/authentification_github_api_doc.png b/chap-35-build-an-http-client/images/authentification_github_api_doc.png new file mode 100644 index 0000000000000000000000000000000000000000..1f9cc718db99563ae260b4ee49192693b4a1694b Binary files /dev/null and b/chap-35-build-an-http-client/images/authentification_github_api_doc.png differ diff --git a/chap-35-build-an-http-client/images/client_server.png b/chap-35-build-an-http-client/images/client_server.png new file mode 100644 index 0000000000000000000000000000000000000000..238a404f058f94dcf5da426626230ae8ad18deea Binary files /dev/null and b/chap-35-build-an-http-client/images/client_server.png differ diff --git a/chap-35-build-an-http-client/images/create_gist_documentation.png b/chap-35-build-an-http-client/images/create_gist_documentation.png new file mode 100644 index 0000000000000000000000000000000000000000..aa0841fabcc77052fb0eae608d0e8816bc502cab Binary files /dev/null and b/chap-35-build-an-http-client/images/create_gist_documentation.png differ diff --git a/chap-35-build-an-http-client/images/create_gist_scope.png b/chap-35-build-an-http-client/images/create_gist_scope.png new file mode 100644 index 0000000000000000000000000000000000000000..cd3d0b2d2e1e6feafb7f52ccc2f6907b8f4f71e4 Binary files /dev/null and b/chap-35-build-an-http-client/images/create_gist_scope.png differ diff --git a/chap-35-build-an-http-client/images/delete_gist_documentation.png b/chap-35-build-an-http-client/images/delete_gist_documentation.png new file mode 100644 index 0000000000000000000000000000000000000000..96611d3ec3f85bc8f560afc9e450c4b046936e23 Binary files /dev/null and b/chap-35-build-an-http-client/images/delete_gist_documentation.png differ diff --git a/chap-35-build-an-http-client/images/edit_a_gist.png b/chap-35-build-an-http-client/images/edit_a_gist.png new file mode 100644 index 0000000000000000000000000000000000000000..e1c1f5e5135fc071e9c9b97fa27e674c06de93ee Binary files /dev/null and b/chap-35-build-an-http-client/images/edit_a_gist.png differ diff --git a/chap-35-build-an-http-client/images/gist_created.png b/chap-35-build-an-http-client/images/gist_created.png new file mode 100644 index 0000000000000000000000000000000000000000..c44ddbc666d9b4b0cf303cb64c2b0b6f018ac387 Binary files /dev/null and b/chap-35-build-an-http-client/images/gist_created.png differ diff --git a/chap-35-build-an-http-client/images/gist_updated.png b/chap-35-build-an-http-client/images/gist_updated.png new file mode 100644 index 0000000000000000000000000000000000000000..8143d8bbeada96069049f0dd8e6901504968e1e0 Binary files /dev/null and b/chap-35-build-an-http-client/images/gist_updated.png differ diff --git a/chap-35-build-an-http-client/images/github_new_token.png b/chap-35-build-an-http-client/images/github_new_token.png new file mode 100644 index 0000000000000000000000000000000000000000..4db4fa80eec36077976cf5b58dfa42fd01f78faf Binary files /dev/null and b/chap-35-build-an-http-client/images/github_new_token.png differ diff --git a/chap-35-build-an-http-client/images/github_personnal_token.png b/chap-35-build-an-http-client/images/github_personnal_token.png new file mode 100644 index 0000000000000000000000000000000000000000..2da6f7a483ebad62ead883e5f4f5ce1b84b3a936 Binary files /dev/null and b/chap-35-build-an-http-client/images/github_personnal_token.png differ