概述

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

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

ResponseWriter接口

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

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

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

// src/net/http/
type ReponseWriter interface {
Header() Header
Write([]byte) (int, error)
WriteHeader(statusCode int)
}

我们响应客户端请求都是通过该接口的 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接口:

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

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

fmt.Fprintln(w, "Hello World")

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


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

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

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

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

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

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

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

curl -i localhost:8080/write

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

HTTP/1.1 200 OK
Date: Thu, 19 Dec 2019 13:36:32 GMT
Content-Length: 113
Content-Type: text/html; charset=utf-8 <html>
<head><title>Go Web 编程之 响应</title></head>
<body><h1>直接使用 Write 方法<h1></body>
</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 作为状态码。

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

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

curl -i localhost:8080/writeheader

返回:

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

Header方法

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

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

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

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

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

curl -i localhost:8080/header

返回:

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

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

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

type User struct {
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
Age int `json:"age"`
Hobbies []string `json:"hobbies"`
} func jsonHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
u := &User {
FirstName: "lee",
LastName: "darjun",
Age: 18,
Hobbies: []string{"coding", "math"},
}
data, _ := json.Marshal(u)
w.Write(data)
} mux.HandleFunc("/json", jsonHandler)

通过curl发送请求:

curl -i localhost:8080/json

返回:

HTTP/1.1 200 OK
Content-Type: application/json
Date: Thu, 19 Dec 2019 14:31:03 GMT
Content-Length: 78 {"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包中定义:

// src/net/http/cookie.go
type Cookie struct {
Name string
Value string
Path string
Domain string
Expires time.Time
RawExpires string
MaxAge int
Secure bool
HttpOnly bool
SameSite SameSite
Raw string
Unparsed []string
}
  • 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:

func setCookie(w http.ResponseWriter, r *http.Request) {
c1 := &http.Cookie {
Name: "name",
Value: "darjun",
HttpOnly: true,
}
c2 := &http.Cookie {
Name: "age",
Value: 18,
HttpOnly: true,
}
w.Header().Set("Set-Cookie", c1.String())
w.Header().Add("Set-Cookie", c2.String())
} mux.HandleFunc("/set_cookie", setCookie)

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

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

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

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

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

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

func setCookie2(w http.ResponseWriter, r *http.Request) {
c1 := &http.Cookie {
Name: "name",
Value: "darjun",
HttpOnly: true,
}
c2 := &http.Cookie {
Name: "age",
Value: "18",
HttpOnly: true,
}
http.SetCookie(w, c1)
http.SetCookie(w, c2)
} mux.HandleFunc("/set_cookie2", setCookie2)

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

func getCookie(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "Host:", r.Host)
fmt.Fprintln(w, "Cookies:", r.Header["Cookie"])
} 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:

func getCookie2(w http.ResponseWriter, r *http.Request) {
name, err := r.Cookie("name")
if err != nil {
fmt.Fprintln(w, "cannot get cookie of name")
} cookies := r.Cookies()
fmt.Fprintln(w, c1)
fmt.Fprintln(w, cookies)
} 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

func main() {
mux1 := http.NewServeMux()
mux1.HandleFunc("/set_cookie", setCookie)
mux1.HandleFunc("/get_cookie", getCookie) server1 := &http.Server{
Addr: ":8080",
Handler: mux1,
} mux2 := http.NewServeMux()
mux2.HandleFunc("/get_cookie", getCookie) server2 := &http.Server {
Addr: ":8081",
Handler: mux2,
} wg := sync.WaitGroup{}
wg.Add(2) go func () {
defer wg.Done() if err := server1.ListenAndServe(); err != nil {
log.Fatal(err)
}
}() go func() {
defer wg.Done() if err := server2.ListenAndServe(); err != nil {
log.Fatal(err)
}
}() wg.Wait()
}

发送给端口 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. vbox ubuntu虚拟机中加载笔记本内置摄像头

    使用C:\Program Files\Oracle\VirtualBox\VBoxManage.exe工具加载摄像头 1,显示可用摄像头 C:\Program Files\Oracle\Virtual ...

  2. Java RandomAccessFile用法(转载)

    RandomAccessFile RandomAccessFile是用来访问那些保存数据记录的文件的,你就可以用seek( )方法来访问记录,并进行读写了.这些记录的大小不必相同:但是其大小和位置必须 ...

  3. Pycharm中Python PEP8 的警告

    https://blog.csdn.net/serizawa_tamao/article/details/88658694

  4. git的安装与命令行基本的使用

    1.https://git-scm.com/ 点击这个网址进入git的官方网站 2,.进去里面会有提示,64位于32位的,根据自己的电脑安装 3 下载完了过后就直接安装,一般会安装在c盘里面 ,进入安 ...

  5. C# AddRange 添加位置

    有没人想知道, AddRange 添加位置 是哪? 是添加到数组的开始,还是数组的末尾? 假如有一个 代码,看起来是下面的,很简单,把一个 list b 放进list a List<int> ...

  6. 【js】 vue 2.5.1 源码学习(十二)模板编译

    大体思路(十) 本节内容: 1. baseoptions 参数分析 2. options 参数分析 3. parse 编译器 4. parseHTNL 函数解析 // parse 解析 parser- ...

  7. tf.train.string_input_producer()

    处理从文件中读数据 官方说明 简单使用 示例中读取的是csv文件,如果要读tfrecord的文件,需要换成 tf.TFRecordReader import tensorflow as tf file ...

  8. dotnet core 获取 MacAddress 地址方法

    本文告诉大家如何在 dotnet core 获取 Mac 地址 因为在 dotnetcore 是没有直接和硬件相关的,所以无法通过 WMI 的方法获取当前设备的 Mac 地址 但是在 dotnet c ...

  9. Oracle单引号拼接和替换

    1.oracle拼接一个单引号: 正常写法:''''|| 由于单引号存在转义,第一个和最后一个是指定你要使用的字符,第二个’是单引号的转义字符,所以需要第三个‘才是真正你要拼接的那个. 也可以用 ch ...

  10. json_encode函数的JOSN_UNESCAPE_UNICODE

    echo  json_encode('测试');  //\u6d4b\u8bd5 echo  json_encode('测试',JSON_UNESCAPED_UNICODE); // 测试 加上JSO ...