概述

上一篇文章中,我们介绍了请求的结构与处理。本文将详细介绍如何响应客户端的请求。其实在前面几篇文章中,我们已经使用过响应的功能——通过http.ResponseWriter发送字符串给客户端。

但是这种方式仅限于发送字符串。本文我们将介绍如何定制响应的参数。

ResponseWriter接口

如果你看了我前面几篇文章,应该对处理器和处理器函数都非常熟悉了。处理器函数即拥有以下签名的函数:

  1. func (w http.ResponseWriter, r *http.Request)

这里的ResponseWriter其实是定义在net/http包中的一个接口:

  1. // src/net/http/
  2. type ReponseWriter interface {
  3. Header() Header
  4. Write([]byte) (int, error)
  5. WriteHeader(statusCode int)
  6. }

我们响应客户端请求都是通过该接口的 3 个方法进行的。例如之前fmt.Fprintln(w, "Hello World")其实底层调用了Write方法。

收到请求后,多路复用器会自动创建一个http.response对象,它实现了http.ResponseWriter接口,然后将该对象和请求对象作为参数传给处理器。那为什么请求对象使用的时结构指针*http.Request,而响应要使用接口呢?

实际上,请求对象使用指针是为了能在处理逻辑中方便地获取请求信息。而响应使用接口来操作,一方面底层也是对象指针,可以保存修改。另一方面,我认为是为了扩展性。可以很方便地用新的实现替换而不用修改应用层代码,即处理器接口不用修改。例如,Go 标准库提供了一个测试 HTTP 请求的工具包net/http/httptest。它定义了一个ResponseRecorder结构,该结构实现了接口http.ResponseWriter。这个结构不将写入的数据发送给客户端,而是将数据记录下来,方便测试断言

接口ResponseWriter有 3 个方法,下面依次来介绍如何使用:

  • Write
  • WriteHeader
  • Header

Write方法

由于接口ResponseWriter拥有方法Write([]byte) (int, error),所以实现了ResponseWriter接口的结构也实现了io.Writer接口:

  1. // src/io/io.go
  2. type Writer interface {
  3. Write(p []byte) (n int, err error)
  4. }

这也是为什么http.ResponseWriter类型的变量w能在下面代码中使用的原因(fmt.Fprintln的第一个参数接收一个io.Writer接口):

  1. fmt.Fprintln(w, "Hello World")

我们也可以直接调用Write方法来向响应中写入数据:


  1. func writeHandler(w http.ResponseWriter, r *http.Request) {
  2. str := `<html>
  3. <head><title>Go Web 编程之 响应</title></head>
  4. <body><h1>直接使用 Write 方法<h1></body>
  5. </html>`
  6. w.Write([]byte(str))
  7. }
  8. mux.HandleFunc("/write", writeHandler)

下面,我们介绍一个工具curl来测试我们的 Web 应用。由于浏览器只会展示响应中主体的内容,其它元信息需要进行一些操作才能查看,不够直观。curl是一个 Linux 命令行程序,可用来发起 HTTP 请求,功能非常强大,如设置首部/请求体,展示响应首部等。

通常 Linux 系统会自带curl命令。简单介绍几种 Windows 上安装curl的方式。

  • 直接在curl官网下载可执行程序,下载完成后放在PATH目录中即可在CmdPowershell界面中使用;

  • Windows 提供了一个软件包管理工具chocolatey,可以安装/更新/删除 Windows 软件。安装chocolatey后,直接在CmdPowershell界面执行以下命令即可安装curl,也比较方便:

  1. choco install curl
  • 我想作为程序员,每个人都应该熟悉git。安装git for windows后,就可以直接在Git Bash中使用curl命令。实际上,git for windows使用了mingw来在 Windows 上模拟 Linux 环境。它提供了很多 Linux 命令的 Windows 版本,非常推荐使用。

启动服务器,使用下面命令测试Write方法:

  1. curl -i localhost:8080/write

选项-i的作用是显示响应首部。该命令返回:

  1. HTTP/1.1 200 OK
  2. Date: Thu, 19 Dec 2019 13:36:32 GMT
  3. Content-Length: 113
  4. Content-Type: text/html; charset=utf-8
  5. <html>
  6. <head><title>Go Web 编程之 响应</title></head>
  7. <body><h1>直接使用 Write 方法<h1></body>
  8. </html>

可以看出很清晰地看出响应的各个部分。也可以继续使用浏览器来测试:

但是如果要查看首部,状态码等信息就必须使用浏览器的开发者工具了。Chrome 的开发者工具可以通过 F12 唤出,然后切换到Network标签,点击刚刚发送的请求:

我们看到上面红色的两个部分为响应的元信息,下面的绿色部分为请求的基本信息。

注意到,如果我们没有设置响应码,则响应码默认为200

而且我们也没有设置内容类型,但是返回的首部中有Content-Type: text/html; charset=utf-8,说明net/http会自动推断。net/http包是通过读取响应体中前面的若干个字节来推断的,并不是百分百准确的。

如何设置状态码和响应内容的类型呢?这就是WriteHeaderHeader()两个方法的作用。

WriteHeader方法

WriteHeader方法的名字带有一点误导性,它并不能用于设置响应首部。WriteHeader接收一个整数,并将这个整数作为 HTTP 响应的状态码返回。调用这个返回之后,可以继续对ResponseWriter进行写入,但是不能对响应的首部进行任何修改操作。如果用户在调用Write方法之前没有执行过WriteHeader方法,那么程序默认会使用 200 作为响应的状态码。

如果,我们定义了一个 API,还未定义其实现。那么请求这个 API 时,可以返回一个 501 Not Implemented 作为状态码。

  1. func writeHeaderHandler(w http.ResponseWriter, r *http.Request) {
  2. w.WriteHeader(501)
  3. fmt.Fprintln(w, "This API not implemented!!!")
  4. }
  5. mux.HandleFunc("/writeheader", writeHeaderHandler)

使用curl来测试刚刚编写的处理器:

  1. curl -i localhost:8080/writeheader

返回:

  1. HTTP/1.1 501 Not Implemented
  2. Date: Thu, 19 Dec 2019 14:15:16 GMT
  3. Content-Length: 28
  4. Content-Type: text/plain; charset=utf-8
  5. This API not implemented!!!

Header方法

Header方法其实返回的是一个http.Header类型,该类型的底层类型为map[string][]string

  1. // src/net/http/header.go
  2. type Header map[string][]string

类型Header定义了 CRUD 方法,可以通过这些方法操作首部。

  1. func headerHandler(w http.ResponseWriter, r *http.Request) {
  2. w.Header().Set("Location", "http://baidu.com")
  3. w.WriteHeader(302)
  4. }

通过第一篇文章我们知道 302 表示重定向,浏览器收到该状态码时会再发起一个请求到首部中Location指向的地址。使用curl测试:

  1. curl -i localhost:8080/header

返回:

  1. HTTP/1.1 302 Found
  2. Location: http://baidu.com
  3. Date: Thu, 19 Dec 2019 14:17:49 GMT
  4. Content-Length: 0

如何在浏览器中打开localhost:8080/header,网页会重定向到百度首页

接下来,我们看看如何设置自定义的内容类型。通过Header.Set方法设置响应的首部Contet-Type即可。我们编写一个返回 JSON 数据的处理器:

  1. type User struct {
  2. FirstName string `json:"first_name"`
  3. LastName string `json:"last_name"`
  4. Age int `json:"age"`
  5. Hobbies []string `json:"hobbies"`
  6. }
  7. func jsonHandler(w http.ResponseWriter, r *http.Request) {
  8. w.Header().Set("Content-Type", "application/json")
  9. u := &User {
  10. FirstName: "lee",
  11. LastName: "darjun",
  12. Age: 18,
  13. Hobbies: []string{"coding", "math"},
  14. }
  15. data, _ := json.Marshal(u)
  16. w.Write(data)
  17. }
  18. mux.HandleFunc("/json", jsonHandler)

通过curl发送请求:

  1. curl -i localhost:8080/json

返回:

  1. HTTP/1.1 200 OK
  2. Content-Type: application/json
  3. Date: Thu, 19 Dec 2019 14:31:03 GMT
  4. Content-Length: 78
  5. {"first_name":"lee","last_name":"darjun","age":18,"hobbies":["coding","math"]}

可以看到响应首部中类型Content-Type被设置成了application/json。类似的格式还有 xml(application/xml)/pdf(application/pdf)/png(image/png)等等。

cookie

概念

什么是 cookie?

cookie 的出现是为了解决 HTTP 协议的无状态性的。客户端通过 HTTP 协议与服务器通信,多次请求之间无法记录状态。服务器可以在响应中设置 cookie,客户端保存这些 cookie。然后每次请求时都带上这些 cookie,服务器就可以通过这些 cookie 记录状态,辨别用户身份等。

重要性

整个计算机行业的收入都建立在 cookie 机制之上,广告领域更是如此。

上面的说法虽然有些夸张,但是可见 cookie 的重要性。

我们知道广告是互联网最常见的盈利方式。其中有一个很厉害的广告模式,叫做联盟广告。你有没有这样一种经历,刚刚在百度上搜索了某个关键字,然后打开淘宝或京东后发现相关的商品已经被推荐到首页或边栏了。这是由于这些网站组成了广告联盟,只要加入它们,就可以共享用户浏览器的 cookie 数据。

使用

Go 中 cookie 使用http.Cookie结构表示,在net/http包中定义:

  1. // src/net/http/cookie.go
  2. type Cookie struct {
  3. Name string
  4. Value string
  5. Path string
  6. Domain string
  7. Expires time.Time
  8. RawExpires string
  9. MaxAge int
  10. Secure bool
  11. HttpOnly bool
  12. SameSite SameSite
  13. Raw string
  14. Unparsed []string
  15. }
  • Name/Value:cookie 的键值对,都是字符串类型;
  • 没有设置Expires字段的 cookie 被称为会话 cookie临时 cookie,这种 cookie 在浏览器关闭时就会自动删除。设置了Expires字段的 cookie 称为持久 cookie,这种 cookie 会一直存在,直到指定的时间来临或手动删除;
  • HttpOnly字段设置为true时,该 cookie 只能通过 HTTP 访问,不能使用其它方式操作,如 JavaScript。提高安全性;

注意:

ExpiresMaxAge都可以用于设置 cookie 的过期时间。Expires字段设置的是 cookie 在什么时间点过期,而MaxAge字段表示 cookie 自创建之后能够存活多少秒。虽然 HTTP 1.1 中废弃了Expires,推荐使用MaxAge代替。但是几乎所有的浏览器都仍然支持Expires;而且,微软的 IE6/IE7/IE8 都不支持 MaxAge。所以为了更好的可移植性,可以只使用Expires或同时使用这两个字段。

cookie 需要通过响应的首部发送给客户端。浏览器收到Set-Cookie首部时,会将其中的值解析成 cookie 格式保存在浏览器中。下面我们来具体看看如何设置 cookie:

  1. func setCookie(w http.ResponseWriter, r *http.Request) {
  2. c1 := &http.Cookie {
  3. Name: "name",
  4. Value: "darjun",
  5. HttpOnly: true,
  6. }
  7. c2 := &http.Cookie {
  8. Name: "age",
  9. Value: 18,
  10. HttpOnly: true,
  11. }
  12. w.Header().Set("Set-Cookie", c1.String())
  13. w.Header().Add("Set-Cookie", c2.String())
  14. }
  15. mux.HandleFunc("/set_cookie", setCookie)

运行程序,打开浏览器输入localhost:8080/set_cookie,浏览器中什么都没有显示,我们需要通过开发者工具查看 cookie。在 chrome 浏览器(其它浏览器类似)按下 F12,切换到 Application(应用)标签,在左侧 Cookies 下点击测试的 URL,右侧即可显示我们刚刚设置的 cookie:

当然,我们也可以使用curl测试。但是curl返回的结果就只是响应中的Set-Cookie首部:

  1. curl -i localhost:8080/set_cookie
  1. HTTP/1.1 200 OK
  2. Set-Cookie: name=darjun; HttpOnly
  3. Set-Cookie: age=18; HttpOnly
  4. Date: Fri, 20 Dec 2019 14:08:01 GMT
  5. Content-Length: 0

上面构造 cookie 的代码中,有几点需要注意:

  • 首部名称为Set-Cookie
  • 首部的值需要是字符串,所以调用了Cookie类型的String方法将其转为字符串再设置;
  • 设置第一个 cookie 调用Header类型的Set方法,添加第二个 cookie 时调用Add方法。Set会将同名的键覆盖掉。如果第二个也调用Set方法,那么第一个 cookie 将会被覆盖。

为了使用的便捷,net/http包还提供了SetCookie方法。用法如下:

  1. func setCookie2(w http.ResponseWriter, r *http.Request) {
  2. c1 := &http.Cookie {
  3. Name: "name",
  4. Value: "darjun",
  5. HttpOnly: true,
  6. }
  7. c2 := &http.Cookie {
  8. Name: "age",
  9. Value: "18",
  10. HttpOnly: true,
  11. }
  12. http.SetCookie(w, c1)
  13. http.SetCookie(w, c2)
  14. }
  15. mux.HandleFunc("/set_cookie2", setCookie2)

如果收到的响应中有 cookie 信息,浏览器会将这些 cookie 保存下来。只有没有过期,在向同一个主机发送请求时都会带上这些 cookie。在服务端,我们可以从请求的Header字段读取Cookie属性来获得 cookie:

  1. func getCookie(w http.ResponseWriter, r *http.Request) {
  2. fmt.Fprintln(w, "Host:", r.Host)
  3. fmt.Fprintln(w, "Cookies:", r.Header["Cookie"])
  4. }
  5. mux.HandleFunc("/get_cookie", getCookie)

第一次启动服务器,请求localhost:8080/get_cookie时,结果如下,没有 cookie 信息:

先请求一次localhost:8080/set_cookie,然后再次请求localhost:8080/get_cookie,结果如下,浏览器将 cookie 传过来了:

r.Header["Cookie"]返回一个切片,这个切片又包含了一个字符串,而这个字符串又包含了客户端发送的任意多个 cookie。如果想要取得单个键值对格式的 cookie,就需要解析这个字符串。

为此,net/http包在http.Request上提供了一些方法使我们更容易地获取 cookie:

  1. func getCookie2(w http.ResponseWriter, r *http.Request) {
  2. name, err := r.Cookie("name")
  3. if err != nil {
  4. fmt.Fprintln(w, "cannot get cookie of name")
  5. }
  6. cookies := r.Cookies()
  7. fmt.Fprintln(w, c1)
  8. fmt.Fprintln(w, cookies)
  9. }
  10. mux.HandleFunc("/get_cookies", getCookies2)
  • Cookie方法返回以传入参数为键的 cookie,如果该 cookie 不存在,则返回一个错误;
  • Cookies方法返回客户端传过来的所有 cookie。

测试新的 URL get_cookie2

有一点需要注意,cookie 是与主机名绑定的,不考虑端口。我们上面查看 cookie 的图中有一列Domain表示的就是主机名。可以这样来验证一下,创建两个服务器,一个绑定在 8080 端口,一个绑定在 8081 端口,先请求localhost:8080/set_cookie设置 cookie,然后请求localhost:8081/get_cookie

  1. func main() {
  2. mux1 := http.NewServeMux()
  3. mux1.HandleFunc("/set_cookie", setCookie)
  4. mux1.HandleFunc("/get_cookie", getCookie)
  5. server1 := &http.Server{
  6. Addr: ":8080",
  7. Handler: mux1,
  8. }
  9. mux2 := http.NewServeMux()
  10. mux2.HandleFunc("/get_cookie", getCookie)
  11. server2 := &http.Server {
  12. Addr: ":8081",
  13. Handler: mux2,
  14. }
  15. wg := sync.WaitGroup{}
  16. wg.Add(2)
  17. go func () {
  18. defer wg.Done()
  19. if err := server1.ListenAndServe(); err != nil {
  20. log.Fatal(err)
  21. }
  22. }()
  23. go func() {
  24. defer wg.Done()
  25. if err := server2.ListenAndServe(); err != nil {
  26. log.Fatal(err)
  27. }
  28. }()
  29. wg.Wait()
  30. }

发送给端口 8081 的请求同样可以获取 cookie:

建议自己尝试一下,(_)

上面代码中,不能直接在主 goroutine 中依次ListenAndServe两个服务器。因为ListenAndServe只有在出错或关闭时才会返回。在此之前,第二个服务器永远得不到机会运行。所以,我创建两个 goroutine 各自运行一个服务器,并且使用sync.WaitGroup来同步。否则,主 goroutine 运行结束之后,整个程序就退出了。

总结

本文介绍了如何响应客户端的请求和 cookie 的相关知识。相关代码在Github上,非常建议大家自己编写运行一遍以便加深印象。

参考

  1. Go Web 编程
  2. net/http标准库文档

我的博客

欢迎关注我的微信公众号【GoUpUp】,共同学习,一起进步~

本文由博客一文多发平台 OpenWrite 发布!

Go Web 编程之 响应的更多相关文章

  1. 物联网网络编程、Web编程综述

    本文是基于嵌入式物联网研发工程师的视觉对网络编程和web编程进行阐述.对于专注J2EE后端服务开发的童鞋们来说,这篇文章可能稍显简单.但是网络编程和web编程对于绝大部分嵌入式物联网工程师来说是一块真 ...

  2. Java Web编程的主要组件技术——Servlet

    参考书籍:<J2EE开源编程精要15讲> Servlet是可以处理客户端传来的HTTP请求,并返回响应,由服务器端调用执行,有一定编写规范的Java类. 例如: package test; ...

  3. C++ Web 编程

    C++ Web 编程 什么是 CGI? 公共网关接口(CGI),是一套标准,定义了信息是如何在 Web 服务器和客户端脚本之间进行交换的. CGI 规范目前是由 NCSA 维护的,NCSA 定义 CG ...

  4. web编程的初步认识

    一直以后, 只知道打开浏览器, 输入网址便可以上网浏览网页, 但是当认真琢磨起这web编程的时候, 对于很多细节却是感觉很迷惑, 在慢慢的学习中, 才逐渐有了些了解. web有client/serve ...

  5. 物联网网络编程和web编程

    本文是基于嵌入式物联网研发project师的视觉对网络编程和web编程进行阐述. 对于专注J2EE后端服务开发的同学来说,这篇文章可能略微简单.可是网络编程和web编程对于绝大部分嵌入式物联网proj ...

  6. C++ Web 编程(菜鸟教程)

    C++ Web 编程(菜鸟教程) C++ Web 编程 什么是 CGI? 公共网关接口(CGI),是一套标准,定义了信息是如何在 Web 服务器和客户端脚本之间进行交换的. CGI 规范目前是由 NC ...

  7. 客户端请求服务器端通信, Web 编程发展基础|乐字节

    乐字节的小伙伴们,好久不见,甚是想念啊! 前面我发布的文章算是把Java初级基础阶段讲完了,接下来小乐将会给大家接着讲Java中级阶段——Javaweb. 首先,我们要看看Javaweb阶段主要重点掌 ...

  8. go web编程——路由与http服务

    本文主要讲解go语言web编程中的路由与http服务基本原理. 首先,使用go语言启动一个最简单的http服务: package main import ( "log" " ...

  9. Go Web 编程之 程序结构

    概述 一个典型的 Go Web 程序结构如下,摘自<Go Web 编程>: 客户端发送请求: 服务器中的多路复用器收到请求: 多路复用器根据请求的 URL 找到注册的处理器,将请求交由处理 ...

随机推荐

  1. ip2long之后有什么好处?

    ip2long需要bigint来存储,而且在32位和64位系统中存储方式还有区别: 而保存成字符串,只需要char20即可. 那么,ip2long好处在哪? 做投票项目的时候,将ip地址处理后用int ...

  2. LRJ

    //3-1 #define _CRT_SECURE_NO_WARNINGS #include <cstdio> int main() { int T; ]; scanf("%d& ...

  3. H3C 帧中继地址映射

  4. springboot 项目打包可运行jar文件

    eclipse 运行run as  maven bulid  ,填入package ,运行打包 java -jar xxx.jar

  5. CF1055F Tree and XOR

    CF1055F Tree and XOR 就是选择两个数找第k大对儿 第k大?二分+trie上验证 O(nlognlogn) 直接按位贪心 维护可能的决策点(a,b)表示可能答案的对儿在a和b的子树中 ...

  6. tensorflow在文本处理中的使用——CBOW词嵌入模型

    代码来源于:tensorflow机器学习实战指南(曾益强 译,2017年9月)——第七章:自然语言处理 代码地址:https://github.com/nfmcclure/tensorflow-coo ...

  7. es6笔记 day1---let和const的应用

    ES6 -> ECMA标准 ES7  ES8 最早是由ECMA-262版本实现的 ---------------------------------------- ES6 也称为ES2015,2 ...

  8. VXLAN IBGP RR 实验

    网络拓扑图: SPINE1配置 ====================================================== hostname SPINE-1nv overlay ev ...

  9. 21.time和random

    原文:https://www.cnblogs.com/yuanchenqi/article/5732581.html time模块 三种时间表示 在Python中,通常有这几种方式来表示时间: 时间戳 ...

  10. Visio高级应用部件

    标注与公式的应用: 插入标注 怎么让标注与图形建立关联:拖动标注的时候坐下角会出现黄色的点 把标准拖动到形状边的时候让黄点进入形状就是建立了关联 然后标注就会随着形状的移动而移动 而且复制和删除也都是 ...