请求的结构

HTTP 的交互以请求和响应的应答模式。Go 的请求我们早就见过了,handler 函数的第二个参数 http.Requests。其结构为:

type Request struct {
Method string URL *url.URL
Proto string // "HTTP/1.0"
ProtoMajor int // 1
ProtoMinor int // 0
Header Header
Body io.ReadCloser
ContentLength int64
TransferEncoding []string
Close bool
Host string
Form url.Values
PostForm url.Values
MultipartForm *multipart.Form
....
ctx context.Context
}

从 request 结构可以看到,http 请求的基本信息都囊括了。对于请求而言,主要关注一下请求的 URL,Method,Header,Body 这些结构。

URL

HTTP 的 url 请求格式为 scheme://[userinfo@]host/path[?query][#fragment], Go 的提供了一个 URL 结构,用来映射 HTTP 的请求 URL。

type URL struct {
Scheme string
Opaque string
User *Userinfo
Host string
Path string
RawQuery string
Fragment string
}

URL 的格式比较明确,其实更好的名词应该是 URI,统一资源定位。url 中比较重要的是查询字符串 query。通常作为 get 请求的参数。query 是一些使用 & 符号分割的 key1=value1&key2=value2 键值对,由于 url 编码是 ASSIC 码,因此 query 需要进行 urlencode。Go 可以通过 request.URI.RawQuery 读取 query

func indexHandler(w http.ResponseWriter, r *http.Request) {
info := fmt.Sprintln("URL", r.URL, "HOST", r.Host, "Method", r.Method, "RequestURL", r.RequestURI, "RawQuery", r.URL.RawQuery)
fmt.Fprintln(w, info)
}
$  curl -X POST -H "Content-Type: application/x-www-form-urlencoded" -d 'name=vanyar&age=27' "http://127.0.0.1:8000?lang=zh&version=1.1.0"
URL /?lang=zh&version=1.1.0 HOST 127.0.0.1:8000 Method POST RequestURL /?lang=zh&version=1.1.0 RawQuery lang=zh&version=1.1.0

header

header 也是 HTTP 中重要的组成部分。Request 结构中就有 Header 结构,Header 本质上是一个 map(map[string][]string)。将 http 协议的 header的 key-value 进行映射成一个图:

    Host: example.com
accept-encoding: gzip, deflate
Accept-Language: en-us
fOO: Bar
foo: two Header = map[string][]string{
"Accept-Encoding": {"gzip, deflate"},
"Accept-Language": {"en-us"},
"Foo": {"Bar", "two"},
}

header 中的字段包含了很多通信的设置,很多时候请求都需要指定 Content-Type。

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

    info := fmt.Sprintln(r.Header.Get("Content-Type"))
fmt.Fprintln(w, info)
}
$  curl -X POST -H "Content-Type: application/x-www-form-urlencoded" -d 'name=vanyar&age=27' "http://127.0.0.1:8000?lang=zh&version=1.1.0"
application/x-www-form-urlencoded

Golng 提供了不少打印函数,基本上分为三类三种。即 Print Println 和 Printf。
Print 比较简单,打印输出到标准输出流,Println 则也一样不同在于多打印一个换行符。至于 Printf 则是打印格式化字符串,三个方法都返回打印的 bytes 数。Sprint,Sprinln 和 Sprintf 则返回打印的字符串,不会输出到标准流中。Fprint,Fprintf 和 Fprinln 则把输出的结果打印输出到 io.Writer 接口中,http 中则是 http.ReponseWriter 这个对象中,返回打印的 bytes 数。

Body

http 中数据通信,主要通过 body 传输。Go 把 body 封装成 Request 的 Body,它是一个 ReadCloser 接口。接口方法 Reader 也是一个接口,后者有一个Read(p []byte) (n int, err error)方法,因此 body 可以通过读取 byte 数组获取请求的数据。

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

    info := fmt.Sprintln(r.Header.Get("Content-Type"))
len := r.ContentLength
body := make([]byte, len)
r.Body.Read(body)
fmt.Fprintln(w, info, string(body))
}
$  curl -X POST -H "Content-Type: application/x-www-form-urlencoded" -d 'name=vanyar&age=27' "http://127.0.0.1:8000?lang=zh&version=1.1.0"
application/x-www-form-urlencoded
name=vanyar&age=27

可见,当请求的 content-type 为 application/x-www-form-urlencoded, body 也是和 query 一样的格式,key-value 的键值对。换成 json 的请求方式则如下:

$  curl -X POST -H "Content-Type: application/json" -d '{name: "vanyar", age: 27}' "http://127.0.0.1:8000?lang=zh&version=1.1.0"
application/json
{name: "vanyar", age: 27}

multipart/form-data 的格式用来上传图片,请求的 body 如下:

#  curl -X POST -H "Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW" -F "name=vanyar" -F "age=27" "http://127.0.0.1:8000?lang=zh&version=1.1.0"
multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW; boundary=------------------------d07972c7800e4c23
--------------------------d07972c7800e4c23
Content-Disposition: form-data; name="name" vanyar
--------------------------d07972c7800e4c23
Content-Disposition: form-data; name="age" 27
--------------------------d07972c7800e4c23--

表单

解析 body 可以读取客户端请求的数据。而这个数据是无论是键值对还是 form-data 数据,都比较原始。直接读取解析还是挺麻烦的。这些 body 数据通常也是表单提供。因此 Go 提供处理这些表单数据的方法。

Form

Go 提供了 ParseForm 方法用来解析表单提供的数据,即 content-type 为 x-www-form-urlencode 的数据。

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

    contentType := fmt.Sprintln(r.Header.Get("Content-Type"))

    r.ParseForm()
fromData := fmt.Sprintf("%#v", r.Form)
fmt.Fprintf(w, contentType, fromData) }
$  curl -X POST -H "Content-Type: application/x-www-form-urlencoded" -d 'name=vanyar&age=27' "http://127.0.0.1:8000?lang=zh&version=1.1.0"
application/x-www-form-urlencoded
%!(EXTRA string=url.Values{"name":[]string{"vanyar"}, "age":[]string{"27"}, "lang":[]string{"zh"}, "version":[]string{"1.1.0"}})%

用来读取数据的结构和方法大致有下面几个:

    fmt.Println(r.Form["lang"])
fmt.Println(r.PostForm["lang"])
fmt.Println(r.FormValue("lang"))
fmt.Println(r.PostFormValue("lang"))

其中 r.Form 和 r.PostForm 必须在调用 ParseForm 之后,才会有数据,否则则是空数组。而 r.FormValue 和 r.PostFormValue("lang") 无需 ParseForm 的调用就能读取数据。

此外 r.Form 和 r.PostForm 都是数组结构,对于 body 和 url 都存在的同名参数,r.Form 会有两个值,即 ["en", "zh"],而带 POST 前缀的数组和方法,都只能读取 body 的数据。

$  curl -X POST -H "Content-Type: application/x-www-form-urlencoded" -d 'name=vanyar&age=27&lang=en' "http://127.0.0.1:8000?lang=zh&version=1.1.0"
application/x-www-form-urlencoded
%!(EXTRA string=url.Values{"version":[]string{"1.1.0"}, "name":[]string{"vanyar"}, "age":[]string{"27"}, "lang":[]string{"en", "zh"}})%

此时可以看到,lang 参数不仅 url 的 query 提供了,post 的 body 也提供了,Go 默认以 body 的数据优先,两者的数据都有,并不会覆盖。

如果不想读取 url 的参数,调用 PostForm 或 PostFormValue 读取字段的值即可。

r.PostForm["lang"][0]
r.PostFormValue["lang"]

对于 form-data 的格式的数据,ParseForm 的方法只会解析 url 中的参数,并不会解析 body 中的参数。

$  curl -X POST -H "Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW" -F "name=vanyar" -F "age=27" -F "lang=en" "http://127.0.0.1:8000?lang=zh&version=1.1.0"
multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW; boundary=------------------------5f87d5bfa764488d
%!(EXTRA string=url.Values{"lang":[]string{"zh"}, "version":[]string{"1.1.0"}}
)%

因此当请求的 content-type 为 form-data 的时候,ParseFrom 则需要改成 MutilpartFrom,否则 r.From 是读取不到 body 的内容,只能读取到 query string 中的内容。

MutilpartFrom

ParseMutilpartFrom 方法需要提供一个读取数据长度的参数,然后使用同样的方法读取表单数据,MutilpartFrom 只会读取 body 的数据,不会读取 url 的 query 数据。

func indexHandler(w http.ResponseWriter, r *http.Request) {
r.ParseMultipartForm(1024) fmt.Println(r.Form["lang"])
fmt.Println(r.PostForm["lang"])
fmt.Println(r.FormValue("lang"))
fmt.Println(r.PostFormValue("lang"))
fmt.Println(r.MultipartForm.Value["lang"]) fmt.Fprintln(w, r.MultipartForm.Value)
}

可以看到请求之后返回 map[name:[vanyar] age:[27] lang:[en]]。即 r.MultipartForm.Value 并没有 url 中的参数。

总结一下,读取 urlencode 的编码方式,只需要 ParseForm 即可,读取 form-data 编码需要使用 ParseMultipartForm 方法。如果参数中既有 url,又有 body,From 和 FromValue 方法都能读取。而带Post 前缀的方法,只能读取 body 的数据内容。其中 MultipartForm 的数据通过 r.MultipartForm.Value 访问得到。

文件上传

form-data 格式用得最多方式就是在图片上传的时候。r.MultipartForm.Value 是 post 的 body 字段数据,r.MultipartForm.File 则包含了图片数据:

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

    r.ParseMultipartForm(1024)
fileHeader := r.MultipartForm.File["file"][0]
fmt.Println(fileHeader)
file, err := fileHeader.Open()
if err == nil{
data, err := ioutil.ReadAll(file)
if err == nil{
fmt.Println(len(data))
fmt.Fprintln(w, string(data))
}
}
fmt.Println(err)
}

发出请求之后,可以看见返回了图片。当然,Go 提供了更好的工具函数 r.FormFile,直接读取上传文件数据。而不需要再使用 ParseMultipartForm 方法。

    file, _, err := r.FormFile("file")

    if err == nil{
data, err := ioutil.ReadAll(file)
if err == nil{
fmt.Println(len(data))
fmt.Fprintln(w, string(data))
}
}
fmt.Println(err)

这种情况只适用于出了文件字段没有其他字段的时候,如果仍然需要读取 lang 参数,还是需要加上 ParseMultipartForm 调用的。读取到了上传文件,接下来就是很普通的写文件的 io 操作了。

JSON

现在流行前后端分离,客户端兴起了一些框架,angular,vue,react 等提交的数据,通常习惯为 json 的格式。对于 json 格式,body 就是原生的 json 字串。也就是 Go 解密 json 为 Go 的数据结构。

type Person struct {
Name string
Age int
} func indexHandler(w http.ResponseWriter, r *http.Request) {
decode := json.NewDecoder(r.Body)
var p Person
err := decode.Decode(&p)
if err != nil{
log.Fatalln(err)
}
info := fmt.Sprintf("%T\n%#v\n", p, p)
fmt.Fprintln(w, info)
}
$  curl -X POST -H "Content-Type: application/json"  -d '{"name": "vanyar", "age": 27 }' "http://127.0.0.1:8000?lang=zh&version=1.1.0"
main.Person
main.Person{Name:"vanyar", Age:27}

更多关于 json 的细节,以后再做讨论。访问官网文档获取更多的信息。

Response

请求和响应是 http 的孪生兄弟,不仅它们的报文格式类似,相关的处理和构造也类似。Go 构造响应的结构是 ResponseWriter 接口。

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

里面的方法也很简单,Header 方法返回一个 header 的 map 结构。WriteHeader 则会返回响应的状态码。Write 返回给客户端的数据。

我们已经使用了 fmt.Fprintln 方法,直接向 w 写入响应的数据。也可以调用 Write 方法返回的字符。

func indexHandler(w http.ResponseWriter, r *http.Request) {
str := `<html>
<head><title>Go Web Programming</title></head>
<body><h1>Hello World</h1></body>
</html>`
w.Write([]byte(str))
}
$  curl -i http://127.0.0.1:8000/
HTTP/1.1 200 OK
Date: Wed, 07 Dec 2016 09:13:04 GMT
Content-Length: 95
Content-Type: text/html; charset=utf-8 <html>
<head><title>Go Web Programming</title></head>
<body><h1>Hello World</h1></body>
</html>%

Go 根据返回的字符,自动修改成了 text/html 的 Content-Type 格式。返回数据自定义通常需要修改 header 相关信息。

func indexHandler(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(501)
fmt.Fprintln(w, "No such service, try next door")
}
$ curl -i http://127.0.0.1:8000/
HTTP/1.1 501 Not Implemented
Date: Wed, 07 Dec 2016 09:14:58 GMT
Content-Length: 31
Content-Type: text/plain; charset=utf-8 No such service, try next door

重定向

重定向的功能可以更加设置 header 的 location 和 http 状态码实现。

func indexHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Location", "https://google.com")
w.WriteHeader(302)
}
$  curl -i http://127.0.0.1:8000/
HTTP/1.1 302 Found
Location: https://google.com
Date: Wed, 07 Dec 2016 09:20:19 GMT
Content-Length: 31
Content-Type: text/plain; charset=utf-8

重定向是常用的功能,因此 Go 也提供了工具方法,http.Redirect(w, r, "https://google.com", http.StatusFound)。

与请求的 Header 结构一样,w.Header 也有几个方法用来设置 headers

func (h Header) Add(key, value string) {
textproto.MIMEHeader(h).Add(key, value)
} func (h Header) Set(key, value string) {
textproto.MIMEHeader(h).Set(key, value)
} func (h MIMEHeader) Add(key, value string) {
key = CanonicalMIMEHeaderKey(key)
h[key] = append(h[key], value)
} func (h MIMEHeader) Set(key, value string) {
h[CanonicalMIMEHeaderKey(key)] = []string{value}
}

Set和Add方法都可以设置 headers,对于已经存在的 key,Add 会追加一个值 value 的数组中,,set 则是直接替换 value 的值。即 append 和赋值的差别。

Json

请求发送的数据可以是 JSON,同样响应的数据也可以是 json。restful 风格的 api 也是返回 json 格式的数据。对于请求是解码 json 字串,响应则是编码 json 字串,Go 提供了标准库 encoding/json

type Post struct {
User string
Threads []string
} func indexHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
post := &Post{
User: "vanyar",
Threads: []string{"first", "second", "third"},
}
json, _ := json.Marshal(post)
w.Write(json)
}
$  curl -i http://127.0.0.1:8000/
HTTP/1.1 200 OK
Content-Type: application/json
Date: Thu, 08 Dec 2016 06:45:17 GMT
Content-Length: 54 {"User":"vanyar","Threads":["first","second","third"]}

当然,更多的 json 处理细节稍后再做介绍。

总结

对于 web 应用程式,处理请求,返回响应是基本的内容。Golang 很好的封装了 Request 和 ReponseWriter 给开发者。无论是请求还是响应,都是针对 url,header 和 body 相关数据的处理。也是 http 协议的基本内容。

除了 body 的数据处理,有时候也需要处理 header 中的数据,一个常见的例子就是处理 cookie。这将会在 cookie 的话题中讨论。

[Go] Http包 使用简介的更多相关文章

  1. Charles抓包软件简介

    Charles简介: Charles是一款抓包神器,因为他是基于 java 开发的,所以跨平台,Mac.Linux.Window下都是可以使用的,确保安装之前已经安装了JDK.Charles官网地址: ...

  2. 包嗅探和包回放简介-tcpdump,tcpreplay

    一.  嗅探 1.1  嗅探技术简介 1.1.1  目标 嗅探的目标:获取在网络上传输的各种有价值信息:账号.密码.非公开协议 1.1.2  原理 嗅探的原理:大多数嗅探都是在以太网内,利用数据链路层 ...

  3. wireshark抓包工具简介以及tcp三次握手的一些含义

    wireshark是非常流行的网络封包分析软件,功能十分强大.可以截取各种网络封包,显示网络封包的详细信息.使用wireshark的人必须了解网络协议,否则就看不懂wireshark了.为了安全考虑, ...

  4. Fiddler (HTTP抓包)简介

    1.什么是Fiddler? Fiddler是位于客户端和服务器端的HTTP代理,也是目前最常用的http抓包工具之一.它能够记录客户端和服务器之间的所有HTTP请求,可以针对特定的HTTP请求,分析请 ...

  5. Spring各个jar包的简介

    spring.jar是包含有完整发布的单个jar 包,spring.jar中包含除了spring-mock.jar里所包含的内容外其它所有jar包的内容,因为只有在开发环境下才会用到 spring-m ...

  6. java常用的包的简介

    java常用的包: java.lang:包含java语言的核心类,如String.math.system和thread类等,使用这个包下的类无需import导入,系统会自动导入这个包下的所有类.   ...

  7. Android最新支持包Design简介

    Android 5.0 Lollipop是曾经最著名的Android发布之一,这样说很大一部分原因是材料设计的引入,而材料设计则是一种刷新了整个Android体验的设计语言.这个详细说明是开始适应材料 ...

  8. nodejs 搭建 RESTful API 服务器的常用包及其简介

    常用包 框架: yarn add express 数据库链接: yarn add sequelize yarn add mysql2 处理 favicon: yarn add serve-favico ...

  9. Concurrent包常用方法简介

    1 Executor接口 留给开发者自己实现的接口,一般情况下不需要再去实现.它只有一个方法 void execute(Runnable command) 2 ExecutorService接口 它继 ...

  10. Golang 包管理简介

    Golang 包管理 在一个项目里,如果想引用本地包,经常会把新手搞的莫名其妙.这里通俗记录一下. 首先先要知道几个默认的规则 必须定义环境变量GOPATH,GOPATH可以定义多个目录 所有项目代码 ...

随机推荐

  1. Jquery常用方法合集,超实用

    转自:十分钟玩转 jQuery.实例大全 一.简介 定义 jQuery创始人是美国John Resig,是优秀的Javascript框架: jQuery是一个轻量级.快速简洁的javaScript库. ...

  2. EM算法理解

    一.概述 概率模型有时既含有观测变量,又含有隐变量,如果概率模型的变量都是观测变量,那么给定数据,可以直接利用极大似然估计法或者贝叶斯估计法估计模型参数.但是,当模型同时又含有隐变量时,就不能简单地使 ...

  3. ubuntu16.04 caffe(GPU模式)安装

    历时5天终于完成了,配置中出现了各种各样的Error,这里记录一下,希望能为正在安装的人提供一点帮助. 配置中主要参考博客:http://blog.csdn.net/yhaolpz/article/d ...

  4. 转:VMWare服务器虚拟化--转自CSDN

    http://blog.csdn.net/kkfloat/article/category/1249845/3

  5. springMVC非注解常用的"处理器映射器"、"适配器"、"处理器"

    非注解处理器映射器1. org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping url 到bean name的映射2. or ...

  6. php的递归函数示例

    递归函数太难理解了,写了一个示例放在这里方便没事的时候看一下. <?php /** *php递归函数示例 *(从1到100的累加和计算) * */ function summation($num ...

  7. Flask源码解析:Flask应用执行流程及原理

    WSGI WSGI:全称是Web Server Gateway Interface,WSGI不是服务器,python模块,框架,API或者任何软件,只是一种规范,描述服务器端如何与web应用程序通信的 ...

  8. Django Rest Framework-APIView源码分析

    class APIView(View): # The following policies may be set at either globally, or per-view. renderer_c ...

  9. Spring加载XML配置文件

    原创链接:http://www.cnblogs.com/yanqin/p/5282929.html(允许转载,但请注明原创链接) BeanFactory加载单个文件 当使用beanfactory去获取 ...

  10. c++中的类(class)

    c++的class(类)使用方法 这几天一直在调splay之类的东西,突然想转指针...qwq 于是,我就在沙华大佬的帮助下,学了下一顿乱指( $ -> $ ),也就是class(类) 首先:c ...