IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 网络协议 -> Go语言编程笔记14:处理请求 -> 正文阅读

[网络协议]Go语言编程笔记14:处理请求

Go语言编程笔记14:处理请求

image-20211108153040805

图源:wallpapercave.com

上一篇Go语言编程笔记13:处理器中我们讨论了如何创建一个Web应用并接收请求,本篇文章探讨如何来处理请求。

Request

Go语言编程笔记12:web基础中我们说过了,一个HTTP请求实际上就是一个HTTP请求报文,内容主要由首行、报文头、空行、报文体四个部分组成。

http库中,请求报文被抽象为http.Request这个结构体:

type Request struct {
	Method string
	URL *url.URL
	Proto      string // "HTTP/1.0"
	ProtoMajor int    // 1
	ProtoMinor int    // 0
	Header Header
	Body io.ReadCloser
	GetBody func() (io.ReadCloser, error)
	ContentLength int64
	TransferEncoding []string
	Close bool
	Host string
	Form url.Values
	PostForm url.Values
	MultipartForm *multipart.Form
	Trailer Header
	RemoteAddr string
	RequestURI string
	TLS *tls.ConnectionState
	Cancel <-chan struct{}
	Response *Response
	ctx context.Context
}

这个结构体并没有完全按照请求报文的结构来定义,但其中比较重要的依然是请求报文中包含的这几个概念:

URL

Request.URL代表请求报文的首行,即包含URL和HTTP协议的那部分。其具体定义为一个url.URL类型的结构体:

type URL struct {
	Scheme      string
	Opaque      string    // encoded opaque data
	User        *Userinfo // username and password information
	Host        string    // host or host:port
	Path        string    // path (relative paths may omit leading slash)
	RawPath     string    // encoded path hint (see EscapedPath method)
	ForceQuery  bool      // append a query ('?') even if RawQuery is empty
	RawQuery    string    // encoded query values, without '?'
	Fragment    string    // fragment for references, without '#'
	RawFragment string    // encoded fragment hint (see EscapedFragment method)
}

这个结构体中的字段其实对应下面这样的URL:

scheme://[userinfo@]host/[path][?query][#fragment]
  • [xxx]表示可选结构。
  • userinfo的部分为HTTP协议早期规定的可以使用URL进行身份验证的部分,即可以通过类似http://name:passwd@sample.com/index.php这样的URL进行身份验证和登录,显然这种做法已经过时,虽然依然遗留在HTTP协议中,但出于安全考虑并不会有网站使用这样的方式进行身份验证。

除了上边这种常见的URL结构,还可以对应一种不常见的:

scheme:opaque[?query][#fragment]

因为的确不常见到,所以这里不做讨论。

我们可以使用以下代码来观察Request.URL的内容:

package main

import (
	"fmt"
	"net/http"
)

func helloHandleFunc(rw http.ResponseWriter, r *http.Request) {
	fmt.Fprintf(rw, "%#v", r.URL)
	fmt.Fprintf(rw, "%#v", r.Proto)
}

func main() {
	serverMux := http.NewServeMux()
	server := http.Server{
		Addr:    "127.0.0.1:8080",
		Handler: serverMux,
	}
	serverMux.HandleFunc("/hello/", helloHandleFunc)
	server.ListenAndServe()
}

如果使用浏览器请求http://127.0.0.1:8080/hello/lalala?name=icexmoon&id=123#here,就会显示:

&url.URL{Scheme:"", Opaque:"", User:(*url.Userinfo)(nil), Host:"", Path:"/hello/lalala", RawPath:"", ForceQuery:false, RawQuery:"name=icexmoon&id=123", Fragment:"", RawFragment:""}"HTTP/1.1"

比较奇怪的是这里的Request.URL.Scheme是一个空字符串,无论是使用HTTP连接还是HTTPS连接都是如此,但是Request.Proto属性中的值的确是HTTP/1.1

虽然我们在URL中指定了页面锚点#here,但服务端并没有获取到Request.URL.Fragment,这是因为在传输过程中浏览器自动“抛弃”了这个信息,这点从请求报文中可以得到验证:

GET /hello/lalala?name=icexmoon&id=123 HTTP/1.1
Host: 127.0.0.1:8080
...

但是如果请求的客户端传递了这个信息(比如使用自己用http库编写的Web客户端),服务器是可以正常接收的。

虽然我们可以通过解析字符串的方式从Request.URL.RawQuery中获取查询字符串中包含的查询参数,但这显然很不方便,对此http库提供了其它更方便的方式,比如formValue函数等,这会在之后介绍。

header

请求中除了URL相关信息,报文头也很重要,HTTP传输控制以及内容编码等信息都是通过报文头的方式进行传递的,此外Cookie也以报文头的形式发送给服务端。

http库中表示请求报文头的内容是Request.Header,这是一个http.Header类型的数据,其实是一个映射:

type Header map[string][]string

这个映射的key是字符串,value是一个字符串切片。之所以这样设计是因为报文头并非是简单的key-value结构,每一个key对应的内容可以是单个信息(比如Cache-Control: no-cache),也可以是多条信息(比如Accept-Encoding: gzip, deflate, br),对于多条信息的报文头条目,请求报文会以,分隔的方式进行发送。相应的,服务端也会将其转换为字符串切片进行保存。

同样的,我们可以在页面打印报文头信息:

...
func helloHandleFunc(rw http.ResponseWriter, r *http.Request) {
	fmt.Fprintf(rw, "%#v", r.Header)
}
...

如果要观察某个报文头条目,可以:

...
func helloHandleFunc(rw http.ResponseWriter, r *http.Request) {
	fmt.Fprintf(rw, "%#v", r.Header["Accept-Encoding"])
}
...

但和我想的不一样的是,Request.Header并不会将Accept-Encoding: gzip, deflate, br这样的报文头存储为[]string{"gzip","deflate","br"},也就是说按,切割后保存,而是依然存储为一整个字符串[]string{"gzip, deflate, br"}。这似乎和将报文头的结构设定为map[string][]string的方式是不相符的,目前这种实现完全可以用map[string]string这样的映射来实现。

为了验证我这种猜测,我尝试用一段代码对报文头信息筛选:

...
func helloHandleFunc(rw http.ResponseWriter, r *http.Request) {
	for key, value := range r.Header {
		if len(value) > 1 {
			fmt.Fprintf(rw, "%s\n", key)
		}
	}
}
...

结果证实了我的猜测,的确没有切片元素超过1的报文信息。

通过后面的Cookie相关学习我发现我这里的想法是错误的,事实上HTTP请求和响应报文中,同一个报文头可能存在多条,就像后文中设置两个以上的Set-Cookie报文头:

HTTP/1.1 200 OK
Set-Cookie: first_cookie=cookie1
Set-Cookie: second_cookie=cookie2
Date: Sat, 25 Dec 2021 05:51:05 GMT
Content-Length: 0

此时显然就需要使用一个切片来对应同一个名称的报文头了。

除了直接使用下标来获取报文内容,比如r.Header["Accept-Encoding"][0],还可以使用Request.HeaderGet方法:

...
func helloHandleFunc(rw http.ResponseWriter, r *http.Request) {
	ae := r.Header.Get("Accept-Encoding")
	fmt.Fprintf(rw, "%s", ae)
}
...

body

除了可以利用URL中的路径和查询字符串传递少量信息以外,大部分用户提交的信息都是以请求报文体的方式传递,这不仅包含文本信息,还包含可能上传的二进制文件。

http中表示请求报文体的变量是Request.Body,这是一个io.ReadCloser接口:

	Body io.ReadCloser

Go语言编程笔记6:接口中我们展示过Go语言中接口如何套用,并仿照io.Reader等接口作为示例,实际上这里的io.ReadCloser接口与彼时介绍的用法类似,同样支持readclose方法,我们可以利用read方法读取请求报文体的内容。

...
func helloHandleFunc(rw http.ResponseWriter, r *http.Request) {
	len := r.ContentLength
	body := make([]byte, len)
	if _, err := r.Body.Read(body); err != nil && err != io.EOF {
		log.Fatal(err)
	}
	fmt.Fprintln(rw, string(body))
}
...

Read方法返回的是一个读取的字节数和错误类型,需要注意的是处理错误时不能只使用err != nil,还需要考虑err != io.EOF,因为当Read方法读取完所有数据后,它会返回io.EOF这个错误。

一般来说我们要往服务端传送带有报文体的HTTP请求,需要使用一个带有form表单的HTML页面。但也可以使用其它的HTTP客户端工具,比如curl

? curl -id "name=icexmoon&id=123" 127.0.0.1:8080/hello/index
HTTP/1.1 200 OK
Date: Thu, 23 Dec 2021 07:22:39 GMT
Content-Length: 21
Content-Type: text/plain; charset=utf-8

name=icexmoon&id=123

curl是一个支持多个平台的命令行网络调试工具,像上面展示的那样,可以利用它来发送带报文体的HTTP请求。其中-i参数表示返回信息包含报文头,-d表示后边跟的内容是POST方法发送的数据(即表单数据)。

更多详细的参数和使用方式可以通过curl --help进行查看。

获取数据

事实上通过Request.URLRequest.BodyRequest.Header我们就可以获取到HTTP请求的全部内容。但显然这并不会很方便,因为这些内容都没有经过解析分组,我们不能便捷得获取到单个表单元素的值或者单个查询参数的值。

所以http包提供了一些更为方便的方式。

查询字符串

通过查询字符串来传递和获取信息是最为简单的Web编程方式:

...
func helloHandleFunc(rw http.ResponseWriter, r *http.Request) {
	r.ParseForm()
	fmt.Fprintf(rw, "%#v", r.Form)
}
...

对于http://127.0.0.1:8080/hello/lalala?name=icexmoon&id=123#here这样的请求,会输出:

url.Values{"id":[]string{"123"}, "name":[]string{"icexmoon"}}

可以看到Request.Form是一个url.Values类型的映射,其实际类型是map[string][]string,和Request.Header是一样的。可能有些对Web开发理解不多的人会疑惑,为什么这里会使用[]string作为查询参数的值,而不是string。但实际上查询字符串是可以包含多个同样命名的查询参数的,比如:

http://127.0.0.1:8080/hello/lalala?name=icexmoon&name=apple&id=123#here

对这样的请求,输出结果是:

url.Values{"id":[]string{"123"}, "name":[]string{"icexmoon", "apple"}}

所以Request.Form的类型是map[string][]string,而非map[string]string

虽然一般情况下不太会在网站链接中生成重名查询参数的情况,但表单元素实际上也可以通过查询字符串进行传递,而表单元素中的某些多选组件(比如checkbox)就是重名的。

所以我们可以使用下标方式获取具体的查询参数的值(准确的说是第一个值):

...
func helloHandleFunc(rw http.ResponseWriter, r *http.Request) {
	r.ParseForm()
	fmt.Fprintf(rw, "%s\n", r.Form["name"][0])
	fmt.Fprintf(rw, "%s\n", r.Form["id"][0])
}
...

当然这样同样不方便,所以:

...
func helloHandleFunc(rw http.ResponseWriter, r *http.Request) {
	r.ParseForm()
	fmt.Fprintf(rw, "%s\n", r.Form.Get("name"))
	fmt.Fprintf(rw, "%s\n", r.Form.Get("id"))
}
...

之所以这里使用的是Request.Form而不是Request.QueryParams之类的命名,是因为实际上默认情况下的表单元素都是以查询字符串的方式进行编码后通过报文体传递的,所以一般情况下两者都可以通过Request.Form来获取。

最后提醒一下,在使用Request.Form之前,要确保手动调用过Request.ParseForm()方法,这个方法的作用是将查询字符串和报文体中的表单元素进行解析,然后保存到Request.Form中。

表单数据

Form

除了查询字符串以外,重要的数据一般会以表单的方式提交给服务端:

<html>
    <head></head>
    <body>
        <form action="/user_submit" method="post">
            name:<input type="text" name="name" value="icexmoon"/><br/>
            age:<input type="text" name="age" value="12"/></br>
            <input type='submit'/>
        </form>
    </body>
</html>

对应的服务端代码:

package main

import (
	"fmt"
	"log"
	"net/http"
	"text/template"
)

func userSubmit(rw http.ResponseWriter, r *http.Request) {
	r.ParseForm()
	fmt.Fprintf(rw, "%s\n", r.Form.Get("name"))
	fmt.Fprintf(rw, "%s\n", r.Form.Get("age"))
}

func userInfo(rw http.ResponseWriter, r *http.Request) {
	t, err := template.ParseFiles("user.html")
	if err != nil {
		log.Fatal(err)
	}
	t.Execute(rw, nil)
}

func main() {
	serverMux := http.NewServeMux()
	server := http.Server{
		Addr:    "127.0.0.1:8080",
		Handler: serverMux,
	}
	serverMux.HandleFunc("/user_submit", userSubmit)
	serverMux.HandleFunc("/user_info", userInfo)
	server.ListenAndServe()
}

这里userInfo处理器函数中加载HTML页面的相关代码可以先忽略,下篇笔记会进行说明。

HTML中的actiontype属性表明,表单在提交后会使用POST的方式传递到/user_info这个URL。在服务端,由userSubmit这个处理器函数处理该请求,与之前提取查询字符串值类似,同样可以使用Request.Form提取表单元素中的数据。

PostForm

虽然不常见,但是其实是可以同时使用查询字符串和表单提交数据的,甚至字段名称可能重复,比如:

<!DOCTYPE html>
<html>
    <head></head>
    <body>
        <form action="/user_submit?name=guest&age=10" method="post">
            name:<input type="text" name="name" value="icexmoon"/><br/>
            age:<input type="text" name="age" value="12"/></br>
            <input type="submit" value="submit"/>
        </form>
    </body>
</html>

此时如果在服务端打印Request.Form

...
func userSubmit(rw http.ResponseWriter, r *http.Request) {
	r.ParseForm()
	fmt.Fprintf(rw, "%#v", r.Form)
}
...

会发现它同时包含查询字符串和表单的内容:

url.Values{"age":[]string{"12", "10"}, "name":[]string{"icexmoon", "guest"}}

并且对于同名字段,表单内容在前,查询字符串内容在后。

如果我们仅需要提取表单传递的内容,而非查询字符串,可以使用Request.PostForm

...
func userSubmit(rw http.ResponseWriter, r *http.Request) {
	r.ParseForm()
	fmt.Fprintf(rw, "%#v\n", r.PostForm)
	fmt.Fprintf(rw, "%s\n", r.PostForm.Get("name"))
	fmt.Fprintf(rw, "%s\n", r.PostForm.Get("age"))
}
...

输出:

url.Values{"age":[]string{"12"}, "name":[]string{"icexmoon"}}
icexmoon
12

MultipartForm

你可能注意到了,上边这种情况下提交表单时,实际上表单内容是是以URL Encoding方式编码后在报文体内传输。具体表单提交时数据以何种方式编码其实由表单的enctype属性决定:

  • application/x-www-form-urlencoded,默认值,以url encode方式编码。
  • multipart/form-data,以MIME格式编码。
  • text/plain,不进行编码,仅会将空格替换为+

下面演示以multipart/form-data方式提交表单内容和服务端捕获:

<!DOCTYPE html>
<html>
    <head></head>
    <body>
        <form action="/user_submit" method="post" enctype="multipart/form-data">
            name:<input type="text" name="name" value="icexmoon"/><br/>
            age:<input type="text" name="age" value="12"/></br>
            <input type="submit" value="submit"/>
        </form>
    </body>
</html>

用浏览器开发者工具观察就能发现,实际上请求报文体中的内容为:

------WebKitFormBoundaryDuyBzmaT9gbsqyLg
Content-Disposition: form-data; name="name"

icexmoon
------WebKitFormBoundaryDuyBzmaT9gbsqyLg
Content-Disposition: form-data; name="age"

12
------WebKitFormBoundaryDuyBzmaT9gbsqyLg--

这就是用MIME格式编码后的表单数据。

此时是无法使用Requeset.ParseFormRequest.Form的方式获取表单信息的,需要使用以下方式:

...
func userSubmit(rw http.ResponseWriter, r *http.Request) {
	if err := r.ParseMultipartForm(1024); err != nil {
		log.Fatal(err)
	}
	fmt.Fprintf(rw, "%#v\n", r.MultipartForm)
	fmt.Fprintf(rw, "%s\n", r.MultipartForm.Value["name"][0])
	fmt.Fprintf(rw, "%s\n", r.MultipartForm.Value["age"][0])
}
...

此时需要调用Request.ParseMultipartForm函数以MIME格式解析表单内容,然后从Request.MultipartForm属性获取相应的表单元素值。

  • ParseMultipartForm函数接收一个参数,表示解析报文体内容的字节长度,通常可以指定一个比较长的数字,比如1024。
  • 如果需要,ParseMultipartForm函数会主动调用ParseForm函数。

提交表单后的输出是:

&multipart.Form{Value:map[string][]string{"age":[]string{"12"}, "name":[]string{"icexmoon"}}, File:map[string][]*multipart.FileHeader{}}
icexmoon
12

可以看到Request.MultipartForm的类型是*multipart.Form,其包含两个属性:ValueFileValue的结构和Request.Form是一样的,File属性的结构是map[string][]*multipart.FileHeader,通过File属性可以获取通过请求报文发送的文件。

下面展示一个发送文件的示例:

...
<body>
    <form action="/user_submit?name=guest&age=10" method="post" enctype="multipart/form-data">
        name:<input type="text" name="name" value="icexmoon" /><br />
        age:<input type="text" name="age" value="12" /><br />
        file:<input type="file" name="upload"/><br />
        <input type="submit" value="submit" />
    </form>
</body>
...

使用表单发送文件时,表单编码必须是enctype="multipart/form-data"

服务端使用Request.MultipartForm.File读取文件:

...
func userSubmit(rw http.ResponseWriter, r *http.Request) {
	if err := r.ParseMultipartForm(1024); err != nil {
		log.Fatal(err)
	}
	fh := r.MultipartForm.File["upload"][0]
	file, err := fh.Open()
	if err != nil {
		log.Fatal(err)
	}
	contents, err := ioutil.ReadAll(file)
	if err != nil && err != io.EOF {
		log.Fatal(err)
	}
	fmt.Fprintln(rw, string(contents))
}
...

这里读取到内容后将其原样输出给页面。

FormValue

除了上面说的方式获取表单和查询字符串内容以外,还可以用更好用的FormValue方法:

...
func userSubmit(rw http.ResponseWriter, r *http.Request) {
	fmt.Fprintf(rw, "%s\n", r.FormValue("id"))
	fmt.Fprintf(rw, "%s\n", r.FormValue("name"))
	fmt.Fprintf(rw, "%s\n", r.FormValue("age"))
}
...

Request.FormValue方法可以自动调用Request.ParseForm方法和Request.ParseMultipartForm方法,并从Request.Form属性中获取相应的Key的第一个值。

类似的,如果只需要提取表单元素,可以使用Request.PostFormValue方法:

...
func userSubmit(rw http.ResponseWriter, r *http.Request) {
	fmt.Fprintf(rw, "%s\n", r.PostFormValue("id"))
	fmt.Fprintf(rw, "%s\n", r.PostFormValue("name"))
	fmt.Fprintf(rw, "%s\n", r.PostFormValue("age"))
}
...

在Go的早期版本中,存在一个bug:如果表单内容以MIME方式编码,则服务端通过Request.PostFormValue获取信息就会失败,原因是此时数据只会保存在Request.MultipartForm中,Request.PostForm中没有表单数据,而PostFormValue只会尝试从后者获取数据。该问题已经在较新版本中解决。

此外,对于从表单提取上传文件,也存在一个类似的便捷方法FormFile

...
func userSubmit(rw http.ResponseWriter, r *http.Request) {
	file, _, err := r.FormFile("upload")
	if err != nil {
		log.Fatal(err)
	}
    ...
}
...

Request.FormFile方法返回三个信息:打开的文件、文件头、错误。实际上其等价于:

...
func userSubmit(rw http.ResponseWriter, r *http.Request) {
	if err := r.ParseMultipartForm(1024); err != nil {
		log.Fatal(err)
	}
	fh := r.MultipartForm.File["upload"][0]
	file, err := fh.Open()
	if err != nil {
		log.Fatal(err)
	}
	...
}

对于一个表单元素只上传一个文件的情况,无疑使用FormFile方法获取文件更为方便。

总结

可能你会和我一样,觉得http包提供的获取HTTP请求数据的方式显得很乱,这里我绘制了一张表作为总结:

image-20211224133432637

Ajax和Json

除了上面介绍的一般方式以外,Web开发中还经常会使用Ajax来传递数据:

<!DOCTYPE html>
<html>

<head>
    <script src="https://cdn.staticfile.org/jquery/1.10.2/jquery.min.js"></script>
    <script>
        function send_json() {
            $.ajax({
                url: "/user_submit",
                type: "PUT",
                data: {
                    name: $("input[name=name]").val(),
                    age: $("input[name=age]").val()
                },
                dataType: "json",
                contentType: "application/x-www-form-urlencoded",
                async: true,
                success: function (result, status, xhr) {
                    console.log(result);
                },
                error: function (xhr, status, error) {
                    console.log(error);
                }
            });
        }
    </script>
</head>

<body>
    name:<input type="text" name="name" value="icexmoon" /><br />
    age:<input type="text" name="age" value="19" /><br />
    <button onclick="send_json()">send json.</button>
</body>

</html>

使用Jquery发送Ajax请求时,其中的contentType表示发送的数据将以何种方式进行编码,默认是application/x-www-form-urlencoded,此时Ajax请求中的data数据将会被编码为查询字符串的方式附加在请求报文体中进行发送,所以此时我们是可以按照之前介绍的方式在服务端提取信息的:

...
type result struct {
	Result string `json:"result"`
	Name   string `json:"name"`
	Age    string `json:"age"`
}

func userSubmit(rw http.ResponseWriter, r *http.Request) {
	name := r.FormValue("name")
	age := r.FormValue("age")
	resp, err := json.Marshal(result{Result: "ok", Name: name, Age: age})
	if err != nil {
		log.Fatal(err)
	}
	fmt.Fprintf(rw, "%s", string(resp))
}
...

除此之外,更常见的Ajax请求会将数据编码为Json格式后发送:

 ...
 <script>
        function send_json() {
            $.ajax({
                url: "/user_submit",
                type: "PUT",
                data: JSON.stringify({
                    name: $("input[name=name]").val(),
                    age: $("input[name=age]").val()
                }),
                dataType: "json",
                contentType: "application/json",
                ...
            });
        }
    </script>
 ...

注意此时的contentTypeapplication/json,其实写成json也是可以的,因为这两种值的情况下服务端都不会进行“预处理”,都需要开发者手动解析。

此时就需要在服务端“手动”从报文体中读取数据后进行Json解析以获取信息:

...
type person struct {
	Name string `json:"name"`
	Age  string `json:"age"`
}

func userSubmit(rw http.ResponseWriter, r *http.Request) {
	body := make([]byte, r.ContentLength)
	if _, err := r.Body.Read(body); err != nil && err != io.EOF {
		log.Fatal(err)
	}
	p := new(person)
	if err := json.Unmarshal(body, p); err != nil {
		log.Fatal(err)
	}
	name := p.Name
	age := p.Age
	resp, err := json.Marshal(result{Result: "ok", Name: name, Age: age})
	if err != nil {
		log.Fatal(err)
	}
	fmt.Fprintf(rw, "%s", string(resp))
}
...

返回数据

就像之前展示的那样,如果要向Web客户端回写信息,需要使用处理器收到的http.ResponseWriter类型的参数,实际上该类型是一个接口:

type ResponseWriter interface {
	Header() Header
	Write([]byte) (int, error)
	WriteHeader(statusCode int)
}

这个接口包含三个方法:

  • Header
  • Write
  • WriteHeader

分别用于设置响应报文的报文头、报文体、首行。

设置首行

通常情况下我们无需手动设置返回报文的首行,因为默认情况下会自动设置为200 ok,但某些情况下这样做是有意义的,比如对于一个未完成开发的接口,我们可能需要返回一个特定的HTTP状态码以告诉客户端,这个接口还没有实现:

...
func userSubmit(rw http.ResponseWriter, r *http.Request) {
	rw.WriteHeader(501)
}
...

使用curl请求就能看到这样的返回报文:

? curl -i 127.0.0.1:8080/user_submit
HTTP/1.1 501 Not Implemented
Date: Sat, 25 Dec 2021 03:46:18 GMT
Content-Length: 0

需要说明的是,调用Request.WriteHeader设置了响应报文的首行后,就不能再尝试设置报文头了,所以设置响应报文首行的操作应当放在设置完响应报文头之后。

设置报文头

设置报文头的操作在Web开发中非常常见,比如设置客户端Cookie、设置响应报文的ContentType以让客户端正确处理响应结果、让客户端进行页面跳转(重定向)等。

这里举一个很常见的重定向例子:

...
func redirect(rw http.ResponseWriter, r *http.Request) {
	rw.Header().Set("location", "/user_info")
	rw.WriteHeader(301)
}

func main() {
	...
	serverMux.HandleFunc("/redirect", redirect)
	server.ListenAndServe()
}
...

如果使用浏览器请求http://127.0.0.1:8080/redirect,就会发现页面会跳转到http://127.0.0.1:8080/user_info

这里ResponseWriter.Header方法返回的是一个http.Header类型,其具体定义为:

type Header map[string][]string

实际上是一个代表了响应报文头的映射,我们可以直接使用映射修改相应报文头:

...
func redirect(rw http.ResponseWriter, r *http.Request) {
	header := rw.Header()
	header["location"] = append(header["location"], "/user_info")
	rw.WriteHeader(301)
}
...

但显然更方便的做法是使用该类型的Set方法。

如果需要给一个报文头追加两个及以上的内容,可以在使用完Set方法后使用Add方法。

设置报文体

实际上之前示例中fmt.Fprintf(rw,...)的做法就是直接将信息写入报文体,这里是利用了fmt包的Fprint系列方法接受一个io.Writer接口的参数,并调用相应的Write方法写入信息:

func Fprintf(w io.Writer, format string, a ...interface{}) (n int, err error) {
	p := newPrinter()
	p.doPrintf(format, a)
	n, err = w.Write(p.buf)
	p.free()
	return
}

这么做的好处在于一来Go的开发者对fmt包相当熟悉,可以用类似输出到控制台的语法来将信息回写到Web客户端。二来fmt的相关打印方法支持格式化字符串,可以利用相关格式化参数满足一些特别的需要。

当然我们也可以使用更“原始”的方式直接调用ResponseWriterWrite方法来回写信息:

...
func userSubmit(rw http.ResponseWriter, r *http.Request) {
	name := r.PostFormValue("name")
	age := r.PostFormValue("age")
	respStr := fmt.Sprintf("name:%s\nage:%s\n", name, age)
	rw.Write([]byte(respStr))
}
...

当我们在响应报文体中写入数据,并返回响应报文后,http客户端如何显示和处理响应报文取决于响应报文的Content-Type报文头。默认情况下,http包会根据响应报文体内的数据内容来设置相应的报文头:

...
func hello(rw http.ResponseWriter, r *http.Request) {
	respStr := `
	<html>
		<body>
			<h1>hello world!</h1>
		</body>
	</html>
	`
	rw.Write([]byte(respStr))
}

func main() {
    ...
	serverMux.HandleFunc("/hello", hello)
	server.ListenAndServe()
}

在上边的示例中,通过Write方法写入响应报文体内的信息是html页面,所以相应的报文头Content-Type会被设置为text/html

? curl -i 127.0.0.1:8080/hello
HTTP/1.1 200 OK
Date: Sat, 25 Dec 2021 05:26:51 GMT
Content-Length: 63
Content-Type: text/html; charset=utf-8

当然我们也可以根据需要手动设置Content-Type报文头。

cookie

之前接触过Web开发的童鞋肯定对cookie不陌生,简单地说就是因为HTTP协议天生是一个无状态协议,所以没法保存会话状态,为了弥补这个问题,诞生了客户端存储技术cookie和服务端存储技术session。这两个技术都可以看作是给HTTP协议打的“补丁”。

结合使用cookie和session就可以实现完整的会话状态控制,通常网站的登录状态等都是以这样的方式实现的。

下面来看如何通过http包使用cookie:

写入cookie

http包对Cookie的抽象是http.Cookie这个结构体:

type Cookie struct {
	Name  string
	Value string
	Path       string    // optional
	Domain     string    // optional
	Expires    time.Time // optional
	RawExpires string    // for reading cookies only
	MaxAge   int
	Secure   bool
	HttpOnly bool
	SameSite SameSite
	Raw      string
	Unparsed []string // Raw text of unparsed attribute-value pairs
}

其中比较重要的属性有:

  • Name,Cookie名称。
  • Value,Cookie的值。
  • Expires,过期日期,如果设定,Cookie将会在指定日期后失效。
  • MaxAge,Cookie在HTTP客户端创建后的有效时间(秒数),超过该时间后Cookie失效。

如果Cookie没有设置有效时间,则只在“会话”内有效,即浏览器关闭后Cookie就自动失效,这样的Cookie被称为会话Cookie。如果设置了有效时间,则在有效期内Cookie都是有效的,这样的Cookie被称为长效Cookie

Cookie的Expires和MaxAge属性都可以用来指定Cookie的有效时间,并且HTTP1.1协议中已经将Expires属性作废,推荐使用MaxAge属性。但因为历史原因,目前主流浏览器依然支持Expires属性,所以考虑兼容性问题,推荐单独使用Expires属性或者同时使用Expires和MaxAge属性。

利用http.Cookie我们可以创建并设置Cookie:

package main

import (
	"net/http"
)

func writeCookie(rw http.ResponseWriter, r *http.Request) {
	c1 := http.Cookie{
		Name:  "first_cookie",
		Value: "cookie1",
	}
	c2 := http.Cookie{
		Name:  "second_cookie",
		Value: "cookie2",
	}
	rw.Header().Set("Set-Cookie", c1.String())
	rw.Header().Add("Set-Cookie", c2.String())
}

func main() {
	serverMux := http.NewServeMux()
	server := http.Server{
		Addr:    "127.0.0.1:8080",
		Handler: serverMux,
	}
	serverMux.HandleFunc("/write_cookie", writeCookie)
	server.ListenAndServe()
}

Cookie.string()方法可以将Cookie实例转化为字符串形式,然后通过响应报文头Set-Cookie返回给HTTP客户端写入Cookie。

使用curl工具查看返回报文信息:

? curl -i 127.0.0.1:8080/write_cookie
HTTP/1.1 200 OK
Set-Cookie: first_cookie=cookie1
Set-Cookie: second_cookie=cookie2
Date: Sat, 25 Dec 2021 05:51:05 GMT
Content-Length: 0

如果使用浏览器请求,就会查看到浏览器端生成相应的Cookie:

image-20211225135503051

除了上边这种通过响应报文头来设置Cookie的“传统方式”以外,http包还提供一个工具函数,可以“稍微”让设置Cookie变得简便一些:

...
func writeCookie(rw http.ResponseWriter, r *http.Request) {
	c1 := http.Cookie{
		Name:  "first_cookie",
		Value: "cookie1",
	}
	c2 := http.Cookie{
		Name:  "second_cookie",
		Value: "cookie2",
	}
	http.SetCookie(rw, &c1)
	http.SetCookie(rw, &c2)
}
...

需要注意的是,SetCookie函数第二个参数是Cookie类型的指针,所以需要传递&c1而非c1

接收cookie

当浏览器端存在当前网站的有效Cookie时,就会在发起到服务端的请求时在请求报文头中附带上Cookie信息:

GET /get_cookie HTTP/1.1
Host: 127.0.0.1:8080
...
Accept-Language: zh-CN,zh-TW;q=0.9,zh-HK;q=0.8,zh;q=0.7,en;q=0.6,und;q=0.5
Cookie: first_cookie=cookie1; second_cookie=cookie2

所以我们可以通过http.Request实例中的Header获取Cookie:

...
func getCookie(rw http.ResponseWriter, r *http.Request) {
	fmt.Fprintf(rw, "%s\n", r.Header["Cookie"][0])
}

func main() {
    ...
	serverMux.HandleFunc("/get_cookie", getCookie)
	server.ListenAndServe()
}

但这样获取到的是以一定格式编码后的Cookie:

first_cookie=cookie1; second_cookie=cookie2

并不能方便地获取某个Cookie的值,所以更常见的做法是使用Request.Cookie方法:

...
func getCookie(rw http.ResponseWriter, r *http.Request) {
	c1, _ := r.Cookie("first_cookie")
	c2, _ := r.Cookie("second_cookie")
	fmt.Fprintf(rw, "first_cookie:%s\n", c1.Value)
	fmt.Fprintf(rw, "second_cookie:%s\n", c2.Value)
}
...

如果想获取到所有的Cookie实例,则可以使用Request.Cookies方法:

...
func getCookie(rw http.ResponseWriter, r *http.Request) {
	cookies := r.Cookies()
	for _, c := range cookies {
		fmt.Fprintf(rw, "%s:%s\n", c.Name, c.Value)
	}
}
...

闪回消息

Cookie有很多用途,其中之一是实现“闪回消息”。

假设我们现在要做一个登陆页,用户如果正常登陆,则跳转到网站的首页,并显示一条欢迎语句。如果登陆失败,则不跳转,依然停留在登录页,并显示登录失败等信息。

当然具体实现这个功能可以有很多方式,但这里主要是学习Cookie,所以用Cookie实现。

package main

import (
	"encoding/base64"
	"log"
	"net/http"
	"text/template"
	"time"
)

const FLASH_MSG_COOKIE_NAME = "flash"

func writeFlashMsg(rw http.ResponseWriter, msg string) {
	flashCookie := http.Cookie{
		Name:  FLASH_MSG_COOKIE_NAME,
		Value: base64.StdEncoding.EncodeToString([]byte(msg)),
	}
	http.SetCookie(rw, &flashCookie)
}

func readFlashMsg(rw http.ResponseWriter, r *http.Request) (msg string, err error) {
	flashCookie, err := r.Cookie(FLASH_MSG_COOKIE_NAME)
	if err != nil {
		if err == http.ErrNoCookie {
			return "", nil
		}
		return
	}
	bytes, err := base64.StdEncoding.DecodeString(flashCookie.Value)
	if err != nil {
		return
	}
	msg = string(bytes)
	//清除http客户端cookie
	flashCookie = &http.Cookie{
		Name:    FLASH_MSG_COOKIE_NAME,
		Expires: time.Unix(1, 0),
		MaxAge:  -1,
	}
	http.SetCookie(rw, flashCookie)
	return
}

func checkLogin(rw http.ResponseWriter, r *http.Request) {
	name := r.PostFormValue("name")
	pasword := r.PostFormValue("password")
	if name == "icexmoon" && pasword == "12345" {
		//写入闪回消息
		writeFlashMsg(rw, "hello! welcome to our web site!")
		rw.Header().Set("location", "/index")
		rw.WriteHeader(301)
	} else {
		writeFlashMsg(rw, "username or password error!")
		rw.Header().Set("location", "/login")
		rw.WriteHeader(301)
	}
}

func login(rw http.ResponseWriter, r *http.Request) {
	t, err := template.ParseFiles("login.html")
	if err != nil {
		log.Fatal(err)
	}
	msg, err := readFlashMsg(rw, r)
	if err != nil {
		log.Fatal(err)
	}
	t.Execute(rw, msg)
}

func index(rw http.ResponseWriter, r *http.Request) {
	t, err := template.ParseFiles("index.html")
	if err != nil {
		log.Fatal(err)
	}
	msg, err := readFlashMsg(rw, r)
	if err != nil {
		log.Fatal(err)
	}
	t.Execute(rw, msg)
}

func main() {
	serverMux := http.NewServeMux()
	server := http.Server{
		Addr:    "127.0.0.1:8080",
		Handler: serverMux,
	}
	serverMux.HandleFunc("/index", index)
	serverMux.HandleFunc("/login", login)
	serverMux.HandleFunc("/check_login", checkLogin)
	server.ListenAndServe()
}

这里的主要逻辑是,在表单提交到/check_loginURL进行验证的时候,如果用户名密码正确,服务端就向客户端Cookie中写入一个闪回消息“hello! welcome…”,然后让客户端跳转到/index页面。否则服务端向客户端Cookie写入一个“login error”消息,然后让客户端跳转回/login页面。

要让/index/login页面能显示可能存在的闪回消息,就需要在加载页面的时候服务端从客户端发送过来的Cookie信息中读取,如果有,就进行显示,同时重新将该Cookie设置为过期,以从客户端进行清除。

这样做的好处在于这种“闪回消息”只会出现一次,比如登录成功后,在首页看到相关欢迎信息,但如果此时按F5刷新一下首页,就能发现该消息没了。可以将这种“闪回消息”看做是某种“一次性消息”,就像我在前面说的,这实际上是利用读写Cookie和设置Cookie的过期时间来实现的。

好了,关于处理HTTP请求的内容就到这里了,我好久没写过这么长的文章了。

谢谢阅读。

参考资料

  网络协议 最新文章
使用Easyswoole 搭建简单的Websoket服务
常见的数据通信方式有哪些?
Openssl 1024bit RSA算法---公私钥获取和处
HTTPS协议的密钥交换流程
《小白WEB安全入门》03. 漏洞篇
HttpRunner4.x 安装与使用
2021-07-04
手写RPC学习笔记
K8S高可用版本部署
mySQL计算IP地址范围
上一篇文章      下一篇文章      查看所有文章
加:2021-12-26 22:36:04  更:2021-12-26 22:37:02 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2025年1日历 -2025/1/9 1:26:23-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码