概述

前面我们学习了处理器和处理器函数,如何编写和注册处理器。本文我们将学习如何从请求中获取信息。

请求的结构

通过前面的学习,我们知道处理器函数需要符合下面的签名:

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

其中,http.Request就是请求的类型。客户端传递的数据都可以通过这个结构来获取。结构Request定义在包 net/http 中:

// src/net/http/request.go

type Request struct {
Method string
URL *url.URL
Proto string
ProtoMajor int
ProtoMinor int
Header Header
Body io.ReadCloser
ContentLength int
// 省略一些字段...
}

我们来看一下几个重要的字段。

Method

请求中的Method字段表示客户端想要调用服务器的哪个方法。在第一篇文章中,我们提到过 HTTP 协议方法。其取值有GET/POST/PUT/DELETE等。服务器根据请求方法的不同会进行不同的处理,例如GET方法只是获取信息(用户基本信息,商品信息等),POST方法创建新的资源(注册新用户,上架新商品等)。

URL

Tim Berners-Lee 在创建万维网的同时,也引入了使用字符串来表示互联网资源的概念。他称该字符串为统一资源标识符(URI,Uniform Resource Identifier)。URI 由两部分组成。一部分表示资源的名称,即统一资源名称(URN,Uniform Resource Name)。另一部分表示资源的位置,即统一资源定位符(URL,Uniform Resource Location)。

在 HTTP 请求中,使用 URL 来对要操作的资源位置进行描述。URL 的一般格式为:

[scheme:][//[userinfo@]host][/]path[?query][#fragment]
  • scheme:协议名,常见的有httphttpsftp
  • userInfo:若有,则表示用户信息,如用户名和密码可写作dj:password
  • host:表示主机域名或地址,和一个可选的端口信息。若端口未指定,则默认为 80。例如www.example.comwww.example.com:8080127.0.0.1:8080
  • path:资源在主机上的路径,以/分隔,如/posts
  • query:可选的查询字符串,客户端传输过来的键值对参数,键值直接用=,多个键值对之间用&连接,如page=1&count=10
  • fragment:片段,又叫锚点。表示一个页面中的位置信息。由浏览器发起的请求 URL 中,通常没有这部分信息。但是可以通过ajax等代码的方式发送这个数据;

我们来看一个完整的 URL:

http://dj:password@www.example.com/posts?page=1&count=10#fmt

Go 中的 URL 结构定义在net/url包中:

// net/url/url.go
type URL struct {
Scheme string
Opaque string
User *Userinfo
Host string
Path string
RawPath string
RawQuery string
Fragment string
}

可以通过请求对象中的URL字段获取这些信息。接下来,我们编写一个程序来具体看看(使用上一篇文章讲的 Web 程序基本结构,只需要增加处理器函数和注册即可):

func urlHandler(w http.ResponseWriter, r *http.Request) {
URL := r.URL fmt.Fprintf(w, "Scheme: %s\n", URL.Scheme)
fmt.Fprintf(w, "Host: %s\n", URL.Host)
fmt.Fprintf(w, "Path: %s\n", URL.Path)
fmt.Fprintf(w, "RawPath: %s\n", URL.RawPath)
fmt.Fprintf(w, "RawQuery: %s\n", URL.RawQuery)
fmt.Fprintf(w, "Fragment: %s\n", URL.Fragment)
} // 注册
mux.HandleFunc("/url", urlHandler)

运行服务器,通过浏览器访问localhost:8080/url/posts?page=1&count=10#main

Scheme:
Host:
Path: /url/posts
RawPath:
RawQuery: page=1&count=10
Fragment:

为什么会出现空字段?注意到源码Request结构中URL字段上有一段注释:

// URL specifies either the URI being requested (for server
// requests) or the URL to access (for client requests).
//
// For server requests, the URL is parsed from the URI
// supplied on the Request-Line as stored in RequestURI. For
// most requests, fields other than Path and RawQuery will be
// empty. (See RFC 7230, Section 5.3)
//
// For client requests, the URL's Host specifies the server to
// connect to, while the Request's Host field optionally
// specifies the Host header value to send in the HTTP
// request.

大意是作为服务器收到的请求时,URL中除了PathRawQuery,其它字段大多为空。对于这个问题,Go 的 Github 仓库上Issue 28940有过讨论。

我们还可以通过URL结构得到一个 URL 字符串:

URL := &net.URL {
Scheme: "http",
Host: "example.com",
Path: "/posts",
RawQuery: "page=1&count=10",
Fragment: "main",
}
fmt.Println(URL.String())

上面程序运行输出字符串:

http://example.com/posts?page=1&count=10#main

Proto/ProtoMajor/ProtoMinor

Proto表示 HTTP 协议版本,如HTTP/1.1ProtoMajor表示大版本,ProtoMinor表示小版本。

func protoFunc(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Proto: %s\n", r.Proto)
fmt.Fprintf(w, "ProtoMajor: %d\n", r.ProtoMajor)
fmt.Fprintf(w, "ProtoMinor: %d\n", r.ProtoMinor)
} mux.HandleFunc("/proto", protoFunc)

启动服务器,浏览器请求localhost:8080返回:

Proto: HTTP/1.1
ProtoMajor: 1
ProtoMinor: 1

当前 HTTP/1.1 是主流的版本。

Header

Header中存放的客户端发送过来的首部信息,键-值对的形式。Header类型底层其实是map[string][]string

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

每个首部的键和值都是字符串,可以设置多个相同的键。注意到Header值为[]string类型,存放相同的键的多个值。浏览器发起 HTTP 请求的时候,会自动添加一些首部。我们编写一个程序来看看:

func headerHandler(w http.ResponseWriter, r *http.Request) {
for key, value := range r.Header {
fmt.Fprintf(w, "%s: %v\n", key, value)
}
} mux.HandleFunc("/header", headerHandler)

启动服务器,浏览器请求localhost:8080/header返回:

Accept-Encoding: [gzip, deflate, br]
Sec-Fetch-Site: [none]
Sec-Fetch-Mode: [navigate]
Connection: [keep-alive]
Upgrade-Insecure-Requests: [1]
User-Agent: [Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.108 Safari/537.36]
Sec-Fetch-User: [?1]
Accept: [text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3]
Accept-Language: [zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7]

我使用的是 Chrome 浏览器,不同的浏览器添加的首部不完全相同。

常见的首部有:

  • Accept:客户端想要服务器发送的内容类型;
  • Accept-Charset:表示客户端能接受的字符编码;
  • Content-Length:请求主体的字节长度,一般在 POST/PUT 请求中较多;
  • Content-Type:当包含请求主体的时候,这个首部用于记录主体内容的类型。在发送 POST 或 PUT 请求时,内容的类型默认为x-www-form-urlecoded。但是在上传文件时,应该设置类型为multipart/form-data
  • User-Agent:用于描述发起请求的客户端信息,如什么浏览器。

Content-Length/Body

Content-Length表示请求体的字节长度,请求体的内容可以从Body字段中读取。细心的朋友可能发现了Body字段是一个io.ReadCloser接口。在读取之后要关闭它,否则会有资源泄露。可以使用defer简化代码编写

func bodyHandler(w http.ResponseWriter, r *http.Request) {
data := make([]byte, r.ContentLength)
r.Body.Read(data) // 忽略错误处理
defer r.Body.Close() fmt.Fprintln(w, string(data))
} mux.HandleFunc("/body", bodyHandler)

上面代码将客户端传来的请求体内容回传给客户端。还可以使用io/ioutil包简化读取操作:

data, _ := ioutil.ReadAll(r.Body)

直接在浏览器中输入 URL 发起的是GET请求,无法携带请求体。有很多种方式可以发起带请求体的请求,下面介绍两种:

使用表单

通过 HTML 的表单我们可以向服务器发送 POST 请求,将表单中的内容作为请求体发送。

func indexHandler(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, `
<html>
<head>
<title>Go Web 编程之 request</title>
</head>
<body>
<form method="post" action="/body">
<label for="username">用户名:</label>
<input type="text" id="username" name="username">
<label for="email">邮箱:</label>
<input type="text" id="email" name="email">
<button type="submit">提交</button>
</form>
</body>
</html>
`)
} mux.HandleFunc("/", indexHandler)

在 HTML 中使用form来显示一个表单。点击提交按钮后,浏览器会发送一个 POST 请求到路径/body上,将用户名和邮箱作为请求包体。

启动服务器,进入主页localhost:8080/,显示表单。填写信息,点击提交:

浏览器向服务器发送 POST 请求,URL 为/bodybodyHandler处理完成后将包体回传给客户端。最后客户端显示:

上面的数据使用了x-www-form-urlencoded编码,这是表单的默认编码。后文还有详述。

使用 Postman

Postman 是一款功能非常强大的 API 测试工具。

  • 支持 HTTP 协议的所有方法请求(GET/POST/PUT/DELETE)。
  • 可以在请求中携带首部信息,请求体的内容;
  • 支持json/xml/http等各种格式的内容;
  • 界面友好。

接下来我们看看如何使用 PostMan 测试我们的bodyHandler

  • 黑色部分:选择 HTTP 协议方法,这里选择 POST 以便可以携带请求体;
  • 绿色部分:请求的 URL;
  • 蓝色部分:可以设置请求的首部,请求体;
  • 淡红色部分:请求体支持多种格式,这里选择原始格式;
  • 灰色部分:请求体的具体内容;
  • 红色部分:发送之后显示的响应信息,可以查看响应首部,Cookie,响应体等。可以看到是原样返回。

获取请求参数

上面我们分析了 Go 中 HTTP 请求的常见字段。在实际开发中,客户端通常需要在请求中传递一些参数。参数传递的方式一般有两种方式:

  • URL 中的键值对,又叫查询字符串,即 query string;
  • 表单。

下面依次来介绍。

URL 键值对

前文中介绍 URL 的一般格式时提到过,URL 的后面可以跟一个可选的查询字符串,以?与路径分隔,形如key1=value1&key2=value2

URL 结构中有一个RawQuery字段。这个字段就是查询字符串。

func queryHandler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, r.URL.RawQuery)
} mux.HandleFunc("/query", queryHandler)

如果我们以localhost:8080/query?name=dj&age=20请求,查询字符串name=dj&age=20会原样传回客户端。但是RawQuery是字符串类型的,使用字符串方法解析也能用,但是太麻烦了!!!

表单

表单狭义上说是通过表单发送请求,广义上说可以将数据放在请求体中发送到服务器。接下来我们简单编写一个 HTML 页面,通过页面表单发送 HTTP 请求:

<html>
<head>
<title>Go Web 编程之 request</title>
</head> <body>
<form action="/form?lang=cpp&name=dj" method="post" enctype="application/x-www-form-urlencoded">
<label>Form:</label>
<input type="text" name="lang" />
<input type="text" name="age" />
<button type="submit">提交</button>
</form>
</body>
</html>
  • action表示提交表单时请求的 URL,method表示请求的方法。如果使用GET请求,由于GET方法没有请求体,参数将会拼接到 URL 尾部
  • enctype指定请求体的编码方式,默认为application/x-www-form-urlencoded。如果需要发送文件,必须指定为multipart/form-data

我们介绍一下什么是urlencoded编码。RFC 3986 中定义了 URL 中的保留字以及非保留字,所有保留字符都需要进行 URL 编码。URL 编码会把字符转换成它在 ASCII 编码中对应的字节值,接着把这个字节值表示为一个两位长的十六进制数字,最后在这个数字前面加上一个百分号(%)。例如空格的 ASCII 编码为 32,十六进制为 20,故 URL 编码为%20

Form字段

使用x-www-form-urlencoded编码的请求体,在处理时首先调用请求的ParseForm方法解析,然后从Form字段中取数据:

func formHandler(w http.ResponseWriter, r *http.Request) {
r.ParseForm()
fmt.Fprintln(w, r.Form)
} mux.HandleFunc("/form", formHandler)

运行程序,验证结果:

Form字段的类型url.Values底层实际上是map[string][]string。调用ParseForm方法之后,可以使用url.Values的方法操作数据。

使用ParseForm还能解析查询字符串,将上面的表单改为:

<html>
<head>
<title>Go Web 编程之 request</title>
</head> <body>
<form action="/form?lang=cpp&name=dj" method="post" enctype="application/x-www-form-urlencoded">
<label>Form:</label>
<input type="text" name="lang" />
<input type="text" name="age" />
<button type="submit">提交</button>
</form>
</body>
</html>

请求结果:

可以看出,查询字符串中的键值对和表单中解析处理的合并到一起了。同一个键下,表单值总是排在前面,如[golang cpp]

PostForm字段

如果一个请求,同时有 URL 键值对和表单数据,而用户只想获取表单数据,可以使用PostForm字段。

使用PostForm只会返回表单数据,不包括 URL 键值。如果把上面的程序中,r.Form改为r.PostForm,那么程序将显示以下结果:

MultipartForm字段

如果要处理上传的文件,那么就必须使用multipart/form-data编码。与之前的Form/PostForm类似,处理multipart/form-data编码的请求时,也需要先解析后使用。只不过使用的方法不同,解析使用ParseMultipartForm,之后从MultipartForm字段取值。

<form action="/multipartform?lang=cpp&name=dj" method="post" enctype="multipart/form-data">
<label>MultipartForm:</label>
<input type="text" name="lang" />
<input type="text" name="age" />
<input type="file" name="uploaded" />
<button type="submit">提交</button>
</form>
func multipartFormHandler(w http.ResponseWriter, r *http.Request) {
r.ParseMultipartForm(1024)
fmt.Fprintln(w, r.MultipartForm) fileHeader := r.MultipartForm.File["uploaded"][0]
file, err := fileHeader.Open()
if err != nil {
fmt.Println("Open failed: ", err)
return
} data, err := ioutil.ReadAll(file)
if err == nil {
fmt.Fprintln(w, string(data))
}
} mux.HandleFunc("/multipartform", multipartFormHandler)

运行程序:

MultipartForm包含两个map类型的字段,一个表示表单键值对,另一个为上传的文件信息。

使用表单中文件控件名获取MultipartForm.File得到通过该控件上传的文件,可以是多个。得到的是multipart.FileHeader类型,通过该类型可以获取文件的各个属性。

需要注意的是,这种方式用来处理文件。为了安全,ParseMultipartForm方法需要传一个参数,表示最大使用内存,避免上传的文件占用空间过大。

FormValue/PostFormValue

为了方便地获取值,net/http包提供了FormValue/PostFormValue方法。它们在需要时会自动调用ParseForm/ParseMultipartForm方法。

FormValue方法返回请求的Form字段中指定键的值。如果同一个键对应多个值,那么返回第一个。如果需要获取全部值,直接使用Form字段。下面代码将返回hello对应的第一个值:

fmt.Fprintln(w, r.FormValue("hello"))

PostFormValue方法返回请求的PostForm字段中指定键的值。如果同一个键对应多个值,那么返回第一个。如果需要获取全部值,直接使用PostForm字段

注意:

当编码被指定为multipart/form-data时,FormValue/PostFormValue将不会返回任何值,它们读取的是Form/PostForm字段,而ParseMultipartForm将数据写入MultipartForm字段。

其他格式

通过 AJAX 之类的技术可以发送其它格式的数据,例如application/json等。这种情况下:

  • 首先通过首部Content-Type来获知具体是什么格式;
  • 通过r.Body读取字节流;
  • 解码使用。

总结

本文介绍了net/http包中请求的各方面内容。从Request结构到如何传递参数,最后介绍各种编码的请求如何处理。

参考

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

我的博客

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

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

Go Web 编程之 请求的更多相关文章

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

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

  2. python web编程-概念预热篇

    互联网正在引发一场革命??不喜欢看概念的跳过,注意这里仅仅是一些从python核心编程一书的摘抄 这正是最激动人心的一部分了,web编程 Web 客户端和服务器端交互使用的“语言”,Web 交互的标准 ...

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

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

  4. MVC 5 Web编程2 -- URL映射

    ASP.NET MVC 5 Web编程2 -- URL映射(路由原理) 2015-02-12 08:50 by hangwei, 704 阅读, 5 评论, 收藏, 编辑 本章将讲述ASP.NET M ...

  5. Java Web服务收到请求时线程的情况

    Web请求线程的状态 在开发中,突然想到了这样的一个问题, Java对每一次Web的请求,是否都会创建一条线程去进行处理呢?也就是说,当一个Class的方法同时有1000个请求访问时,线程是如何运作的 ...

  6. Go web编程实例

    1. go web编程入门 记录个web编程例子方便以后使用. 主要有: chan的使用(带缓存,不带缓存) client发起get/post请求 server解析get/post请求参数 http. ...

  7. C++ Web 编程

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

  8. web编程的初步认识

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

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

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

随机推荐

  1. flowable笔记 - 简单的通用流程

    简介 通用流程可以用于一些基本的申请,例如请假.加班. 大致过程是: 1. 创建申请 2. 分配给审批人(需要审批人列表,当前审批人) -> 有下一个审批人 -> 3 -> 无 -& ...

  2. 在对文件进行随机读写,RandomAccessFile类,如何提高其效率

    花1K内存实现高效I/O的RandomAccessFile类 JAVA的文件随机存取类(RandomAccessFile)的I/O效率较低.通过分析其中原因,提出解决方案.逐步展示如何创建具备缓存读写 ...

  3. codeforces 609B

    #include<bits/stdc++.h> using namespace std; ]; int main() { memset(num,,sizeof(num)); int n,m ...

  4. MySQL高级配置

    参考文章:http://www.jb51.net/article/47419.htm https://blog.csdn.net/waneto2008/article/details/52502208 ...

  5. vue样式加scoped后不能覆盖组件的原有样式解决方法

    <style scoped> </style> 为了vue页面样式模块化,不对全局造成污染,建议每个页面的style标签加上scoped,表示他的样式只属于当前的页面,父组件的 ...

  6. 无限调用函数add(1)(2)(3)......

    无限调用函数,并且累计结果 其实这也算一道面试题吧,笔者曾经被提问过,可惜当时没能答上来...

  7. Codeforces 1100F(离线 or 在线)

    传送门 •参考资料 [1]:在线线性基 [2]:离线线性基 [3]:离线线性基 •题意 给你 n 个数,m 次询问: 每次询问给定一个区间 $l,r$,求 $a_{l \cdots r}$ 异或的最大 ...

  8. idea运行项目时报错:Error:java无效的源发行版:1.8

    解决办法:project structure中设置 JDK 和language 匹配即可.如图: 另外如果有maven,需要把maven中JDK版本设置成一样的.

  9. js实现instanceof

        instanceof 是通过原型链判断的,A instanceof B, 在A的原型链中层层查找,是否有原型等于B.prototype,如果一直找到A的原型链的顶端null,仍然不等于B.pr ...

  10. jekyll 在博客添加流程图

    本文告诉大家如何在博客使用流程图. 如果你使用的是我博客的模板,那么就可以直接使用我说的文件,如果是自己的主题,就需要在自己文件对应的地方加上代码. 在我的博客里,需要添加下面的js到博客,可以打开 ...