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"][
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值