概述

上一篇文章中,我们介绍了请求的结构与处理。本文将详细介绍如何响应客户端的请求。其实在前面几篇文章中,我们已经使用过响应的功能——通过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. html手机端全屏显示和溢出问题

    <meta name="viewport" content="width=1200, initial-scale=0.3"> initial-sca ...

  2. H3C ACL规则的匹配顺序

  3. Python工程编译成跨平台可执行文件(.pyc)

    原文:https://blog.csdn.net/zylove2010/article/details/79593655 在某些场景下,若不方便将python编写的源码工程直接给到其他人员,则可以将p ...

  4. 2018-8-10-C#-代码占用的空间

    title author date CreateTime categories C# 代码占用的空间 lindexi 2018-08-10 19:16:52 +0800 2018-2-13 17:23 ...

  5. Redux 初始化完整结构

    文件管理 目录文档 ★★★index.js★★★ ★★★app.js★★★ ★★★store->index.js★★★ ★★★actions->index.js★★★ ★★★store-& ...

  6. 在spring security3中使用自定义的MD5和salt进行加密

    首先看代码: <authentication-manager alias="authenticationManager"> <authentication-pro ...

  7. UVA 1025 "A Spy in the Metro " (DAG上的动态规划?? or 背包问题??)

    传送门 参考资料: [1]:算法竞赛入门经典:第九章 DAG上的动态规划 题意: Algorithm城市的地铁有 n 个站台,编号为 1~n,共有 M1+M2 辆列车驶过: 其中 M1 辆列车从 1 ...

  8. 2019-9-2-C#命令行解析工具

    title author date CreateTime categories C#命令行解析工具 lindexi 2019-09-02 12:57:37 +0800 2018-2-13 17:23: ...

  9. P1022 绵羊排序

    题目描述 聪聪想要给他家农场里的 \(n\) 只绵羊按照品质从高到低进行排序. 农场里的 \(n\) 只绵羊编号从 \(1\) 到 \(n\) ,第 \(i\) 只绵羊的体重为 \(w_i\) ,高度 ...

  10. git 安装及基本配置

    git 基本上来说是开发者必备工具了,在服务器里没有 git 实在不太能说得过去.何况,没有 git 的话,面向github编程 从何说起,如同一个程序员断了左膀右臂. 你对流程熟悉后,只需要一分钟便 ...