命令记不住

登陆

Web 编程基础 http 请求

Web编程基础http请求

1.Web 版本的 "Hello World"

  • Hello World
package main

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

func main() {
	http.HandleFunc("/", hello)

	if err := http.ListenAndServe(":8080", nil); err != nil {
		log.Fatal(err)
	}
}

func hello(w http.ResponseWriter, r *http.Request) {
	fmt.Fprintf(w, "hello world")

}
  • 解析该程序
http.HandleFunc 将 hello 函数注册到根路径 / 上, hello函数我们也叫做处理器。它接收两个参数:
第一个参数为一个类型为 http.ResponseWriter 的接口,响应就是通过它发送给客户端的。
第二个参数是一个类型为 http.Request 的结构指针,客户端发送的信息都可以通过这个结构获取。
http.ListenAndServe 将在 8080 端口上监听请求,最后交由 hello 处理。

2.多路复用器

一个典型的 Go Web 程序结构如下

goweb1

  • 客户端发送请求;
  • 服务器中的多路复用器收到请求;
  • 多路复用器根据请求的 URL 找到注册的处理器,将请求交由处理器处理;
  • 处理器执行程序逻辑,必要时与数据库进行交互,得到处理结果;
  • 处理器调用模板引擎将指定的模板和上一步得到的结果渲染成客户端可识别的数据格式(通常是HTML);
  • 最后将数据通过响应返回给客户端;
  • 客户端拿到数据,执行对应的操作,例如渲染出来呈现给用户;

net/http 包内置了一个默认的多路复用器 DefaultServeMux 。定义如下

// src/net/http/server.go
// DefaultServeMux is the default ServeMux used by Serve.
var DefaultServeMux = &defaultServeMux
var defaultServeMux ServeMux

// src/net/http/server.go
func HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
	DefaultServeMux.HandleFunc(pattern, handler)
}

实际上, http.HandleFunc 的方法是将处理器注册到 DefaultServeMux 中的。

另外,我们使用 ":8080" 和 nil 作为参数调用 http.ListenAndServe 时,会创建一个默认的服务器:

// src/net/http/server.go
func ListenAndServe(addr string, handler Handler) {
	server := &Server{Addr: addr, Handler: handler}
	return server.ListenAndServe()
}

这个服务器默认使用 DefaultServeMux 来处理器请求:

type serverHandler struct {
	srv *Server
}
func (sh serverHandler) ServeHTTP(rw ResponseWriter, req *Request) {
	handler := sh.srv.Handler
	if handler == nil {
		handler = DefaultServeMux
	}
	handler.ServeHTTP(rw, req)
}

服务器收到的每个请求会调用对应多路复用器(即 ServeMux )的 ServeHTTP 方法。在ServeMux 的ServeHTTP 方法中,根据 URL 查找我们注册的处理器,然后将请求交由它处理。

不建议使用默认多路复用器原因:

虽然默认的多路复用器使用起来很方便,但是在生产环境中不建议使用。由于DefaultServeMux 是一个全局变量,所有代码, 包括第三方代码都可以修改它。 有些第三方代码会在 DefaultServeMux 注册一些处理器,这可能与我们注册的处理器冲突.

2.1创建多路复用器

创建多路复用器也比较简单,直接调用 http.NewServeMux 方法即可。然后,在新创建的多路复用器上注册处理器:

package main

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

func hello (w http.ResponseWriter,r *http.Request){
    fmt.Fprintf(w,"Hello world")

}

func main(){
    //创建Mux
    mux :=http.NewServeMux()
    mux.HandleFunc("/",hello)
  
    server := &http.Server{
        Addr :  ":8080",
        Handler: mux,
    }
    if err := server.ListenAndServe();err !=nil{
        log.Fatal(err)
    }
}

这里我们还自己创建了服务器对象。通过指定服务器的参数,我们可以创建定制化的服务器。

    server := &http.Server{
        Addr :  ":8080",
        Handler: mux,
        ReadTimeout:  1 * time.Second,
        WriteTimeout: 1 * time.Second,
    }

在上面代码,创建了一个读超时和写超时均为 1s 的服务器。

2.2处理器和处理器函数

服务器收到请求后,会根据其 URL 将请求交给相应的处理器处理。处理器实现了 Handler 接口的结构, Handler 接口定义在 net/http 包中:

// src/net/http/server.go
type Handler interface {
func ServeHTTP(w Response.Writer, r *Request)
}

可以定义一个实现该接口的结构,注册这个结构类型的对象到多路复用器中:

package main

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

type GreetingHandler struct {
    Language  string
}
func (h GreetingHandler) ServeHTTP(w http.ResponseWriter,r *http.Request){
    fmt.Fprintf(w,"%s",h.Language)
}
func main(){
    mux := http.NewServeMux()
    mux.Handle("/chinese",GreetingHandler{Language: "你好"})
    mux.Handle("/english",GreetingHandler{Language: "Hello"})
  
    server := &http.Server{
        Addr: ":8080",
        Handler: mux,
        }
        if err := server.ListenAndServe();err !=nil{
            log.Fatal(err)
        }
}


解析:

定义一个实现Handler 接口的结构 GreetingHandler 。然后,创建该结构的两个对象,分别将它注册到多路复用器的 /hello 和 /world 路径上。注意,这里注册使用的是 Handle 方法,注意与 HandleFunc 方法对应,虽然,自定义处理器这种方式比较灵活,强大,但是需要定义一个新的结构,实现 ServeHTTP 方法,还是比较繁琐的。

为了方便用 net/http 包提供了以函数的方式注册处理器,即使用 HandleFunc 注册。函数必须满足签名:func (w http.ResponseWriter, r *http.Request) 。 这个函数称为处理器函数。 HandleFunc 方法内部,会将传入的处理器函数转换为 HandlerFunc 类型

// src/net/http/server.go
func (mux *ServeMux) HandleFunc(pattern string, handler func(ResponseWriter,*Request)) {
if handler == nil {
panic("http: nil handler")
}
mux.Handle(pattern, HandlerFunc(handler))
}

HandlerFunc 是底层类型为 func (w ResponseWriter, r *Request) 的新类型,它可以自定义其方法。由于 HandlerFunc 类型实现了 Handler 接口,所以它也是一个处理器类型,最终使用 Handle 注册。

// src/net/http/server.go
type HandlerFunc func(w *ResponseWriter, r *Request)
func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
f(w, r)
}

注意,这几个接口和方法名很容易混淆:

  • Handler :处理器接口,定义在 net/http 包中。实现该接口的类型,其对象可以注册到多路复用器中
  • Handle :注册处理器的方法;
  • HandleFunc :注册处理器函数的方法;
  • HandlerFunc :底层类型为 func (w ResponseWriter, r *Request) 的新类型,实现了Handler 接口。它连接了处理器函数与处理器。
2.3URL 匹配

一般的 Web 服务器有非常多的 URL 绑定,不同的 URL 对应不同的处理器。但是服务器是怎么决定使用哪个处理器的呢?例如我们现在绑定了3个URL, / 和 /hello 和 /hello/world

  • 如果请求的 URL 为 / ,则调用 / 对应的处理器
  • 如果请求的 URL 为 /hello ,则调用 /hello 对应的处理器
  • 如果请求的 URL 为 /hello/world ,则调用 /hello/world 对应的处理器

但是,如果请求的是 /hello/others ,那么使用哪一个处理器呢? 匹配遵循以下规则:

  • 首先,精确匹配。即查找是否有 /hello/others 对应的处理器。如果有,则查找结束。如果没有,执行下一步;
  • 将路径中最后一个部分去掉,再次查找。即查找 /hello/ 对应的处理器。如果有,则查找结束。
    如果没有,继续执行这一步。即查找 / 对应的处理器

这里有一个注意点, 如果注册的 URL 不是以 / 结尾的,那么它只能精确匹配请求的 URL。反之,即使请求的 URL 只有前缀与被绑定的 URL 相同, ServeMux 也认为它们是匹配的。

这也是为什么上面步骤进行到 /hello/ 时,不能匹配 /hello 的原因。因为 /hello 不以 / 结尾,必须要精确匹配。 如果,我们绑定的 URL 为 /hello/ ,那么当服务器找不到与 /hello/others 完全匹配的处理器时,就会退而求其次,开始寻找能够与 /hello/ 匹配的处理器。

package main

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

func indexHandler(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "This is the index page")
}
func helloHandler(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "This is the hello page")
}
func worldHandler(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "This is the world page")
}
func main() {
    mux := http.NewServeMux()
    mux.HandleFunc("/", indexHandler)
    mux.HandleFunc("/hello", helloHandler)
    mux.HandleFunc("/hello/world", worldHandler)
    server := &http.Server{
        Addr: ":8080",
        Handler: mux,
    }
    if err := server.ListenAndServe(); err != nil {
        log.Fatal(err)
    }
}

3.http请求

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

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 字段获取这些信息

package main

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

func index(w http.ResponseWriter, r *http.Request) {
    fmt.Printf("Method:%v\n", r.Method)
    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)
    // Proto
    fmt.Fprintf(w, "Proto: %s\n", r.Proto)
    fmt.Fprintf(w, "ProtoMajor: %d\n", r.ProtoMajor)
    fmt.Fprintf(w, "ProtoMinor: %d\n", r.ProtoMinor)
    // Header
    for key, value := range r.Header {
        fmt.Fprintf(w, "%s: %v\n", key, value)
    }
    // Body
    //  r.ContentLength -> 请求数据的长度
    data := make([]byte, r.ContentLength)
    r.Body.Read(data) // 从请求体中读取数据,忽略错误处理
    defer r.Body.Close()  // 一定要记得关闭r.Body

    fmt.Fprintln(w, string(data))
    fmt.Fprintf(w, "Hello, index!")
}


func main() {
    http.HandleFunc("/index", index)

    if err := http.ListenAndServe(":8080", nil); err != nil {
        log.Fatal(err)
    }
}

  • Proto

Proto

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)

表示 HTTP 协议版本,如 HTTP/1.1 , ProtoMajor 表示大版本, ProtoMinor 表示小版本
启动服务器,浏览器请求 localhost:8080 返回

Proto: HTTP/1.1
ProtoMajor: 1
ProtoMinor: 1
// src/net/http/header.go
type Header map[string][]string
  • 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-Enreading: [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/79.0.1904.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]

常见的首部有:

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))
}

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

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

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

3.1使用表单请求

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

package main

import (
    "fmt"
    "io/ioutil"
    "log"
    "net/http"
)

func login(w http.ResponseWriter, r *http.Request){
    // 当用户以GET 请求 /login 时,表示用户想要一个登录的页面
    b, _ := ioutil.ReadFile("./login.html")
    w.Write(b)
}

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

    fmt.Fprintln(w, string(data))
}


func main() {
    http.HandleFunc("/login", login)
    http.HandleFunc("/body", body)

    if err := http.ListenAndServe(":8080", nil); err != nil {
        log.Fatal(err)
    }
}


<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>login demo</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>

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

启动服务器,进入主页 localhost:8080/ ,显示表单。填写完成后,点击提交。浏览器向服务器发送POST 请求, URL 为 /body , bodyHandler 处理完成后将包体回传给客户端,上面的数据使用了x-www-form-urlencoded 编码,这是表单的默认编码。

调试工具
  • Postman
  • Postwoman
  • Paw

获取请求参数

在实际开发中,客户端通常需要在请求中传递一些参数。参数传递的方式一般有两种方式 :

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

前文中介绍 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=ls&age=20 请求,查询字符串name=ls&age=20 会传回客户端。

表单

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

<html>
<head>
    <title>Go Web</title>
</head>
<body>
    <form action="/form?lang=cpp&name=ls" 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 底层实际上是 mapstringstring 。调用 ParseForm 方法之后,可以使用url.Values 的方法操作数据。
使用 ParseForm 还能解析查询字符串,将上面的表单改为:

<html>
<head>
<title>Go Web</title>
</head>
<body>
<form action="/form?lang=cpp&name=ls" 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 键值。

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 字段。