专栏首页网管叨bi叨Go Web编程--深入学习解析HTTP请求

Go Web编程--深入学习解析HTTP请求

之前这个系列的文章一直在讲用 Go语言怎么编写HTTP服务器来提供服务,如何给服务器配置路由来匹配请求到对应的处理程序,如何添加中间件把一些通用的处理任务从具体的Handler中解耦出来,以及如何更规范地在项目中应用数据库。不过一直漏掉了一个环节是服务器接收到请求后如何解析请求拿到想要的数据, Go语言使用 net/http包中的 Request结构体对象来表示 HTTP请求,通过 Request结构对象上定义的方法和数据字段,应用程序能够便捷地访问和设置 HTTP请求中的数据。

一般服务端解析请求的需求有如下几种

  • HTTP请求头中的字段值
  • URL 查询字符串中的字段值
  • 请求体中的 Form表单数据
  • 请求体中的 JSON格式数据
  • 读取客户端的上传的文件

今天这篇文章我们就按照这几种常见的服务端对 HTTP请求的操作来说一下服务器应用程序如何通过 Request对象解析请求头和请求体。

Request 结构定义

在说具体操作的使用方法之前我们先来看看 net/http包中 Request结构体的定义,了解一下 Request拥有什么样的数据结构。 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
}

我们快速地了解一下每个字段大致的含义,了解了每个字段的含义在不同的应用场景下需要读取访问 HTTP请求的不同部分时就能够有的放矢了。

Method

指定HTTP方法(GET,POST,PUT等)。

URL

URL指定要请求的URI(对于服务器请求)或要访问的URL(用于客户请求)。它是一个表示 URL的类型 url.URL的指针, url.URL的结构定义如下:

type URL struct {
    Scheme     string
    Opaque     string
    User       *Useri
    Host       string
    Path       string
    RawPath    string
    ForceQuery bool  
    RawQuery   string
    Fragment   string
}

Proto

ProtoProtoMajorProtoMinor三个字段表示传入服务器请求的协议版本。对于客户请求,这些字段将被忽略。 HTTP客户端代码始终使用 HTTP/1.1HTTP/2

Header包含服务端收到或者由客户端发送的 HTTP请求头,该字段是一个 http.Header类型的指针, http.Header类型的声明如下:

type Header map[string][]string

map[string][]string类型的别名, http.Header类型实现了 GETSETAdd等方法用于存取请求头。如果服务端收到带有如下请求头的请求:

Host: example.com
accept-encoding: gzip, deflate
Accept-Language: en-us
fOO: Bar
foo: two

那么 Header的值为:

Header = map[string][]string{
    "Accept-Encoding": {"gzip, deflate"},
    "Accept-Language": {"en-us"},
    "Foo": {"Bar", "two"},
}

对于传入的请求, Host标头被提升为 Request.Host字段,并将其从 Header对象中删除。 HTTP 定义头部的名称是不区分大小写的。 Go使用 CanonicalHeaderKey实现的请求解析器使得请求头名称第一个字母以及跟随在短横线后的第一个字母大写其他都为小写形式,比如: Content-Length。对于客户端请求,某些标头,例如 Content-LengthConnection会在需要时自动写入,并且标头中的值可能会被忽略。

Body

这个字段的类型是 io.ReadCloserBody是请求的主体。对于客户端发出的请求, nil主体表示该请求没有 Body,例如 GET请求。 HTTP客户端的传输会负责调用 Close方法。对于服务器接收的请求,请求主体始终为非 nil,但如果请求没有主体,则将立即返回 EOF。服务器将自动关闭请求主体。服务器端的处理程序不需要关心此操作。

GetBody

客户端使用的方法的类型,其声明为:

GetBody func() (io.ReadCloser, error)

ContentLength

ContentLength记录请求关联内容的长度。值-1表示长度未知。值>=0表示从 Body 中读取到的字节数。对于客户请求,值为0且非 nilBody也会被视为长度未知。

TransferEncoding

TransferEncoding为字符串切片,其中会列出从最外层到最内层的传输编码, TransferEncoding通常可以忽略;在发送和接收请求时,分块编码会在需要时自动被添加或者删除。

Close

Close表示在服务端回复请求或者客户端读取到响应后是否要关闭连接。对于服务器请求,HTTP服务器会自动处理 并且处理程序不需要此字段。对于客户请求,设置此字段为 true可防止重复使用到相同主机的请求之间的TCP连接,就像已设置 Transport.DisableKeepAlives一样。

Host

对于服务器请求, Host指定URL所在的主机,为防止DNS重新绑定攻击,服务器处理程序应验证 Host标头具有的值。 http库中的 ServeMux(复用器)支持注册到特定 Host的模式,从而保护其注册的处理程序。对于客户端请求, Host可以用来选择性地覆盖请求头中的 Host,如果不设置, Request.Write使用 URL.Host来设置请求头中的 Host

Form

Form包含已解析的表单数据,包括 URL字段的查询参数以及 PATCHPOSTPUT表单数据。此字段仅在调用 Request.ParseForm之后可用。 HTTP客户端会忽略 Form并改用 BodyForm字段的类型是 url.Values类型的指针。 url.Values类型的声明如下:

type Values map[string][]string

也是 map[string][]string类型的别名。 url.Values类型实现了 GETSETAddDel等方法用于存取表单数据。

PostForm

PostForm类型与 Form字段一样,包含来自 PATCHPOST的已解析表单数据或PUT主体参数。此字段仅在调用 ParseForm之后可用。 HTTP客户端会忽略 PostForm并改用 Body

MultipartForm

MultipartForm是已解析的多部分表单数据,包括文件上传。仅在调用 Request.ParseMultipartForm之后,此字段才可用。 HTTP客户端会忽略 MultipartForm并改用 Body。该字段的类型是 *multipart.Form

RemoteAddr

RemoteAddr允许 HTTP服务器和其他软件记录发送请求的网络地址,通常用于记录。 net/http包中的HTTP服务器在调用处理程序之前将 RemoteAddr设置为“ IP:端口”, HTTP客户端会忽略此字段。

RequestURI

RequestURI是未修改的 request-target客户端发送的请求行(RFC 7230,第3.1.1节)。在服务器端,通常应改用URL字段。在HTTP客户端请求中设置此字段是错误的。

Response

Response字段类型为 *Response,它指定了导致此请求被创建的重定向响应,此字段仅在客户端发生重定向时被填充。

ctx

ctx 是客户端上下文或服务器上下文。它应该只通过使用 WithContext复制整个 Request进行修改。这个字段未导出以防止人们错误使用 Context并更改同一请求的调用方所拥有的上下文。

读取请求头

上面分析了 GoHTTP请求头存储在 Request结构体对象的 Header字段里, Header字段实质上是一个 Map,请求头的名称为Map keyMapValue的类型为字符串切片,有的请求头像 Accept会有多个值,在切片中就对应多个元素。

Header类型的 Get方法可以获取请求头的第一个值,

func exampleHandler(w http.ResponseWriter, r *http.Request) {
    ua := r.Header.Get("User-Agent")
    ...
}

或者是获取值时直接通过 key获取对应的切片值就好,比如将上面的改为:

ua := r.Header["User-Agent"]

下面我们写个遍历请求头信息的示例程序,同时也会通上面介绍的 Request结构中定义的 MethodURLHostRemoteAddr等字段把请求的通用信息打印出来。在我们一直使用的 http_demo项目中增加一个 DisplayHeadersHandler,其源码如下:

package handler

import (
    "fmt"
    "net/http"
)

func DisplayHeadersHandler(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "Method: %s URL: %s Protocol: %s \n", r.Method, r.URL, r.Proto)
    // 遍历所有请求头
    for k, v := range r.Header {
        fmt.Fprintf(w, "Header field %q, Value %q\n", k, v)
    }

    fmt.Fprintf(w, "Host = %q\n", r.Host)
    fmt.Fprintf(w, "RemoteAddr= %q\n", r.RemoteAddr)
    // 通过 Key 获取指定请求头的值
    fmt.Fprintf(w, "\n\nFinding value of \"Accept\" %q", r.Header["Accept"])
}

将其处理程序绑定到 /index/display_headers路由上:

indexRouter.HandleFunc("/display_headers", handler.DisplayHeadersHandler)

然后启动项目,打开浏览器访问:

http://localhost:8000/index/display_headers

可以看到如下输出:

http_demo项目中已经添加了本文中所有示例的源码,公众号内回复 gohttp06可以获取源码的下载链接。

获取URL参数值

GET请求中的 URL查询字符串中的参数可以通过 url.Query(),我们来看一下啊 url.Query()函数的源码:

func (u *URL) Query() Values {
    v, _ := ParseQuery(u.RawQuery)
    return v
}

它通过 ParseQuery函数解析 URL参数然后返回一个 url.Values类型的值。 url.Values类型上面我们已经介绍过了是 map[string][]string类型的别名,实现了 GETSETAddDel等方法用于存取数据。

所以我们可以使用 r.URL.Query().Get("ParamName")获取参数值,也可以使用 r.URL.Query()["ParamName"]。两者的区别是 Get只返回切片中的第一个值,如果参数对应多个值时(比如复选框表单那种请求就是一个 name对应多个值),记住要使用第二种方式。

我们通过运行一个示例程序 display_url_params.go来看一下两种获取 URL参数的区别

package handler

import (
"fmt"
"net/http"
)

func DisplayUrlParamsHandler(w http.ResponseWriter, r *http.Request) {
    for k, v := range r.URL.Query() {
        fmt.Fprintf(w, "ParamName %q, Value %q\n", k, v)
        fmt.Fprintf(w, "ParamName %q, Get Value %q\n", k, r.URL.Query().Get(k))
    }
}

将其处理程序绑定到 /index/display_url_params路由上:

indexRouter.HandleFunc("/display_url_params", handler.DisplayUrlParamsHandler)

打开浏览器访问

http://localhost:8000/index/display_url_params?a=b&c=d&a=c

浏览器会输出:

ParamName "a", Value ["b" "c"]
ParamName "a", Get Value "b"
ParamName "c", Value ["d"]
ParamName "c", Get Value "d"

我们为参数 a传递了两个参数值,可以看到通过 url.Query.Get()只能读取到第一个参数值。

获取表单中的参数值

Request结构的 Form字段包含已解析的表单数据,包括 URL字段的查询参数以及 PATCHPOSTPUT表单数据。此字段仅在调用 Request.ParseForm之后可用。不过 Request对象提供一个 FormValue方法来获取指定名称的表单数据, FormValue方法会根据 Form字段是否有设置来自动执行 ParseForm方法。

func (r *Request) FormValue(key string) string {
   if r.Form == nil {
      r.ParseMultipartForm(defaultMaxMemory)
   }
   if vs := r.Form[key]; len(vs) > 0 {
      return vs[0]
   }
   return ""
}

可以看到 FormValue方法也是只返回切片中的第一个值。如果需要获取字段对应的所有值,那么需要通过字段名访问 Form字段。如下:

获取表单字段的单个值

r.FormValue(key)

获取表单字段的多个值

r.ParseForm()

r.Form["key"]

下面是我们的示例程序,以及对应的路由:

//handler/display_form_data.go
package handler

import (
    "fmt"
    "net/http"
)

func DisplayFormDataHandler(w http.ResponseWriter, r *http.Request) {
    if err := r.ParseForm(); err != nil {
        panic(err)
    }

    for key, values := range r.Form {
        fmt.Fprintf(w, "Form field %q, Values %q\n", key, values)

        fmt.Fprintf(w, "Form field %q, Value %q\n", key, r.FormValue(key))
    }
}

//router.go
indexRouter.HandleFunc("/display_form_data", handler.DisplayFormDataHandler)

我们在命令行中使用 cURL命令发送表单数据到处理程序,看看效果。

curl -X POST -d 'username=James&password=123' \
     http://localhost:8000/index/display_form_data

返回的响应如下:

Form field "username", Values ["James"]
Form field "username", Value "James"
Form field "password", Values ["123"]
Form field "password", Value "123"

获取Cookie

Request对象专门提供了一个 Cookie方法用来访问请求中携带的 Cookie数据,方法会返回一个 *Cookie类型的值以及 errorCookie类型的定义如下:

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 
}

所以要读取请求中指定名称的 Cookie值,只需要

cookie, err := r.Cookie(name)
// 错误检查
...
value := cookie.Value

Request.Cookies()方法会返回 []*Cookie切片,其中会包含请求中所有的 Cookie

下面的示例程序,会打印请求中所有的 Cookie

// handler/read_cookie.go
package handler

import (
    "fmt"
    "net/http"
)

func ReadCookieHandler(w http.ResponseWriter, r *http.Request) {
    for _, cookie := range r.Cookies() {
        fmt.Fprintf(w, "Cookie field %q, Value %q\n", cookie.Name, cookie.Value)
    }
}
//router/router.go
indexRouter.HandleFunc("/read_cookie", handler.ReadCookieHandler)

我们通过 cURL在命令行请求 http://localhost:8000/index/read_cookie

curl --cookie "USER_TOKEN=Yes" http://localhost:8000/index/read_cookie

执行命令后会返回:

Cookie field "USER_TOKEN", Value "Yes"

解析请求体中的JSON数据

现在前端都倾向于把请求数据以 JSON格式放到请求主体中传给服务器,针对这个使用场景,我们需要把请求体作为 json.NewDecoder()的输入流,然后将请求体中携带的 JSON格式的数据解析到声明的结构体变量中

//handler/parse_json_request
package handler

import (
    "encoding/json"
    "fmt"
    "net/http"
)

type Person struct {
    Name string
    Age  int
}

func DisplayPersonHandler(w http.ResponseWriter, r *http.Request) {
    var p Person

    // 将请求体中的 JSON 数据解析到结构体中
    // 发生错误,返回400 错误码
    err := json.NewDecoder(r.Body).Decode(&p)
    if err != nil {
        http.Error(w, err.Error(), http.StatusBadRequest)
        return
    }

    fmt.Fprintf(w, "Person: %+v", p)
}

// router/router.go
indexRouter.HandleFunc("/parse_json_request", handler.ParseJsonRequestHandler)

在命令行里用 cURL命令测试我们的程序:

curl -X POST -d '{"name": "James", "age": 18}' \
     -H "Content-Type: application/json" \
     http://localhost:8000/index/parse_json_request

返回响应如下:

Person: {Name:James Age:18}%

读取上传文件

服务器接收客户端上传的文件,使用 Request定义的 FormFile()方法。该方法会自动调用 r.ParseMultipartForm(32<<20)方法解析请求多部表单中的上传文件,并把文件可读入内存的大小设置为 32M(32向左位移20位),如果内存大小需要单独设置,就要在程序里单独调用 ParseMultipartForm()方法才行。

func ReceiveFile(w http.ResponseWriter, r *http.Request) {
    r.ParseMultipartForm(32 << 20) 
    var buf bytes.Buffer

    file, header, err := r.FormFile("file")
    if err != nil {
        panic(err)
    }
    defer file.Close()
    name := strings.Split(header.Filename, ".")
    fmt.Printf("File name %s\n", name[0])

    io.Copy(&buf, file)
    contents := buf.String()
    fmt.Println(contents)
    buf.Reset()

    return
}

Go语言解析 HTTP请求比较常用的方法我们都介绍的差不多了。因为想总结全一点,篇幅还是有点长,不过整体不难懂,而且也可以下载程序中的源码自己运行调试,动手实践一下更有助于理解吸收。 HTTP客户端发送请求要设置的内容也只今天讲的 Request结构体的字段, Request对象也提供了一些设置相关的方法供开发人员使用,今天就先说这么多了。

本文分享自微信公众号 - 网管叨bi叨(kevin_tech),作者:KevinYan11

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2020-02-25

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 五分钟用Docker快速搭建Go开发环境

    挺早以前在我写过一篇用 Docker搭建LNMP开发环境的文章:用Docker搭建Laravel开发环境,里面详细介绍了将 nginx、 mysql和 php三...

    KevinYan
  • 在程序设计中使用Interface

    在PHP和Java中都有Interface的概念,刚接触开发时大家都知道在面向对象中Interface负责定义一些抽象方法来抽象和界定类对象的行为,更有一个“鸭...

    KevinYan
  • Laravel源码解析之用户认证系统(一)

    使用过Laravel的开发者都知道,Laravel自带了一个认证系统来提供基本的用户注册、登录、认证、找回密码,如果Auth系统里提供的基础功能不满足需求还可以...

    KevinYan
  • 总是没时间读书?有了它们,利用碎片时间就能轻松看完一本书 | 亲儿子 #25

    现代社会生活节奏快,每个人都很忙:忙着工作,忙着聚会,忙着玩手机。悲哀的是,很少有人愿意把一点点用来「忙」的时间,分给「读书」这项活动。

    知晓君
  • 小程序带来的商机,你知道有多大么

    2017年,至少有7亿资金押注在小程序领域。在四月的互联网大会上有人曾预言:2018年,将会有上百亿的资本进入小程序。

    中微信通
  • 小程序 · 一周报

    8 月 17 日起,在苹果(Apple)iOS 系统下,微信小程序不可提供虚拟物品的购买支付,不应展示支付功能,也不得引导至外部网页或 app 来实现支付功能。

    极乐君
  • Python之requests入门

    最近在学习Python相关的框架花了点时间,早期在python+Selenium+Unittest+HTMLTestRunner(UI自动化这块花了些时间实践了...

    测试小兵
  • Nmap 7.50更新:自去年12月来的重大更新

    Network Mapper 近日发布了最新更新 Nmap 7.50 ,上一次的更新还需要追溯到2016年12月,而此次的版本上对于对于数百项功能进行了改进。 ...

    FB客服
  • 为什么小程序将成为风口?为什么要做小程序?

    为什么小程序将成为风口? 因为你的竞争对手都在做小程序! 为什么要做微信小程序? 因为你的目标客户都在使用微信! 在当今,国民级别的聊天应用非微信莫属。在现今,...

    企鹅号小编
  • 干货丨小程序和APP的区别

    小程序上线以来,一向被称为“便携版”的APP,关于两者之间的区别,无外乎小程序相对轻便、开发成本低,但是对于两者的详细对比较少,小程序从诞生到产品落地和推广,到...

    齿轮易创说互联网

扫码关注云+社区

领取腾讯云代金券