GoWeb(上)之服务端处理请求请求响应

Posted AC_Jobim

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了GoWeb(上)之服务端处理请求请求响应相关的知识,希望对你有一定的参考价值。

GoWeb(上)之服务端、处理请求、请求响应

好的博客:Go HTTP编程

一、服务端

Go语言标准库内建提供了net/http包,涵盖了HTTP客户端和服务端的具体实现。

1.1 构建服务器

首先,我们编写一个最简单的Web服务器。编写这个Web服务只需要两步:

  • 注册一个处理器函数(注册到DefaultServeMux);
  • 设置监听的TCP地址并启动服务;

代码示例:

package main

import (
	"fmt"
	"net/http"
)

func handler(w http.ResponseWriter, r *http.Request) 
	fmt.Fprintln(w, "正在通过处理器函数处理请求!", r.URL.Path)


func main() 
	//1.注册一个给定模式的处理器函数到DefaultServeMux
	http.HandleFunc("/", handler)

	//2.设置监听的TCP地址并启动服务
	//参数1:TCP地址(IP+Port)
	//参数2:当设置为nil时表示使用DefaultServeMux
	err := http.ListenAndServe("127.0.0.1:8080", nil)
	fmt.Println(err)



运行该程序,通过浏览器访问localhost:8080

  • ListenAndServe使用指定的监听地址和处理器启动一个HTTP服务端。

    • 如果网络地址参数为空字符串,那么服务器默认使用 80 端口进行网络连接当
    • 处理器参数是nil时,这表示采用包变量DefaultServeMux作为处理器。
  • Handle和HandleFunc函数可以向DefaultServeMux添加任意多个处理器函数。

还可以通过 Server 结构对服务器进行更详细的配置:

  • 结构体Server的结构:

    type Server struct 
        Addr           string        // 监听的TCP地址,如果为空字符串会使用":http"
        Handler        Handler       // 调用的处理器,如为nil会调用http.DefaultServeMux
        ReadTimeout    time.Duration // 请求的读取操作在超时前的最大持续时间
        WriteTimeout   time.Duration // 回复的写入操作在超时前的最大持续时间
        MaxHeaderBytes int           // 请求的头域最大长度,如为0则用DefaultMaxHeaderBytes
        TLSConfig      *tls.Config   // 可选的TLS配置,用于ListenAndServeTLS方法
        // TLSNextProto(可选地)指定一个函数来在一个NPN型协议升级出现时接管TLS连接的所有权。
        // 映射的键为商谈的协议名;映射的值为函数,该函数的Handler参数应处理HTTP请求,
        // 并且初始化Handler.ServeHTTP的*Request参数的TLS和RemoteAddr字段(如果未设置)。
        // 连接在函数返回时会自动关闭。
        TLSNextProto map[string]func(*Server, *tls.Conn, Handler)
        // ConnState字段指定一个可选的回调函数,该函数会在一个与客户端的连接改变状态时被调用。
        // 参见ConnState类型和相关常数获取细节。
        ConnState func(net.Conn, ConnState)
        // ErrorLog指定一个可选的日志记录器,用于记录接收连接时的错误和处理器不正常的行为。
        // 如果本字段为nil,日志会通过log包的标准日志记录器写入os.Stderr。
        ErrorLog *log.Logger
        // 内含隐藏或非导出字段
    
    

自定义Server:

//定义多个处理器
type h1 struct
func (h1 *h1) ServeHTTP(w http.ResponseWriter, r *http.Request) 
    fmt.Fprintf(w, "h1")


type h2 struct
func (h2 *h2) ServeHTTP(w http.ResponseWriter, r *http.Request) 
    fmt.Fprintf(w, "h2")


func main() 
    h1 := h1
    h2 := h2
    //nil表明服务器使用默认的多路复用器DefaultServeMux
    server := http.Server
        Addr : "127.0.0.1:8080",
        Handler : nil,
		ReadTimeout : 2 * time.Second,
    
    //handle函数实际调用的是多路复用器DefaultServeMux.Handle方法
    http.Handle("/h1", &h1)
    http.Handle("/h2", &h2)

    server.ListenAndServe()

1.2 处理器和处理器函数

1.2.1 处理器

  • 一个处理器就是实现了Handler这个接口:实现了Handler接口的对象可以注册到HTTP服务端,为特定的路径及其子树提供服务。

  • 那么也就是说,任何接口只要有一个ServeHTTP方法,并且该方法带有以下的签名,那么他就是一个处理器

    type Handler interface 
        ServeHTTP(ResponseWriter, *Request)
    
    
  • 多路复用器DefaultServeMux是一个特殊的处理器,它的任务是根据请求的URL将请求重定向到不同的处理器

实现一个处理器:

package main

import (
	"log"
	"net/http"
)
type MyHandler struct 

func (m *MyHandler)ServeHTTP(w http.ResponseWriter, r *http.Request) 
	_, _ = w.Write([]byte("Hello World!"))

func main() 

	//1.将处理器和URL进行绑定
	http.Handle("/", &MyHandler)

	//2.设置监听的TCP地址并启动服务
	//参数1:TCP地址(IP+Port)
	//参数2:当设置为nil时表示使用DefaultServeMux
	err := http.ListenAndServe("127.0.0.1:8080", nil)
	log.Fatal(err)

1.2.2 处理器函数

  • 处理器函数就是与ServeHTTP方法拥有相同签名的函数。

  • HandlerFunc函数,它可以把一个带有正确签名的函数f转换成一个带有方法f的Handler

    func HandleFunc(pattern string, handler func(ResponseWriter, *Request))
    

使用处理器函数处理请求:

package main

import (
    "fmt"
    "net/http"
)

func index(w http.ResponseWriter, r *http.Request) 
    hello := "Hello Wrold!"
    fmt.Fprintf(w, "%s\\n", hello)


func login(w http.ResponseWriter, r *http.Request) 
    fmt.Fprintf(w, "You are in the login\\n")


func home(w http.ResponseWriter, r *http.Request) 
    fmt.Fprintf(w, "You are in the home\\n")


func news(w http.ResponseWriter, r *http.Request) 
    fmt.Fprintf(w, "You are in the news\\n")


func main() 

    server := &http.Server
        Addr: "0.0.0.0:9090",
    

    http.HandleFunc("/", index)
    http.HandleFunc("/login", login)
    http.HandleFunc("/home", home)
    http.HandleFunc("/news", news)

    err := server.ListenAndServe()

    if err != nil 
        fmt.Println(err)
    

处理器函数的实现原理:

通过源码可知,这个函数实际上是调用了默认的DefaultServeMux的HandleFunc方法,即:默认注册到DefaultServeMux中

1.3 多路复用器

ServeMux和DefaultServeMux:

  • ServeMux是一个HTTP请求多路复用器,它负责接受HTTP请求并根据请求中的URL将请求重定向到正确的处理器。

  • ServeMux结构也实现了ServeHTTP方法,所以它也是一个处理器

  • 结构体 ServeMux 的相关方法:

  • DefaultServeMux是ServeMux的一个实例,当用户没有为Server结构指定处理器时,处理器就会使用DefaultServeMux作为ServeMux的默认实例

使用NewServeMux创建的多路复用器代码:

//创建处理器函数
func handler(w http.ResponseWriter, r *http.Request) 
	fmt.Fprintln(w, "通过自己创建的多路复用器处理请求!", r.URL.Path)


func main() 
	//创建多路复用器
	mux := http.NewServeMux()

	mux.HandleFunc("/", handler)

	//创建路由
	http.ListenAndServe(":8080", mux)

二、处理请求

好的博客:Go Web:处理请求

Go 语言的 net/http 包提供了一系列用于表示 HTTP 报文的结构,我们可以使用它处理请求和发送相应,其中 Request 结构代表了客户端发送的请求报文,下面让我们看一下 Request 结构体

type Request struct 
    // Method指定HTTP方法(GET、POST、PUT等)。对客户端,""代表GET。
    Method string
    // URL在服务端表示被请求的URI,在客户端表示要访问的URL。
    //
    // 在服务端,URL字段是解析请求行的URI(保存在RequestURI字段)得到的,
    // 对大多数请求来说,除了Path和RawQuery之外的字段都是空字符串。
    // (参见RFC 2616, Section 5.1.2)
    //
    // 在客户端,URL的Host字段指定了要连接的服务器,
    // 而Request的Host字段(可选地)指定要发送的HTTP请求的Host头的值。
    URL *url.URL
    // 接收到的请求的协议版本。本包生产的Request总是使用HTTP/1.1
    Proto      string // "HTTP/1.0"
    ProtoMajor int    // 1
    ProtoMinor int    // 0
    // Header字段用来表示HTTP请求的头域。如果头域(多行键值对格式)为:
    //	accept-encoding: gzip, deflate
    //	Accept-Language: en-us
    //	Connection: keep-alive
    // 则:
    //	Header = map[string][]string
    //		"Accept-Encoding": "gzip, deflate",
    //		"Accept-Language": "en-us",
    //		"Connection": "keep-alive",
    //	
    // HTTP规定头域的键名(头名)是大小写敏感的,请求的解析器通过规范化头域的键名来实现这点。
    // 在客户端的请求,可能会被自动添加或重写Header中的特定的头,参见Request.Write方法。
    Header Header
    // Body是请求的主体。
    //
    // 在客户端,如果Body是nil表示该请求没有主体买入GET请求。
    // Client的Transport字段会负责调用Body的Close方法。
    //
    // 在服务端,Body字段总是非nil的;但在没有主体时,读取Body会立刻返回EOF。
    // Server会关闭请求的主体,ServeHTTP处理器不需要关闭Body字段。
    Body io.ReadCloser
    // ContentLength记录相关内容的长度。
    // 如果为-1,表示长度未知,如果>=0,表示可以从Body字段读取ContentLength字节数据。
    // 在客户端,如果Body非nil而该字段为0,表示不知道Body的长度。
    ContentLength int64
    // TransferEncoding按从最外到最里的顺序列出传输编码,空切片表示"identity"编码。
    // 本字段一般会被忽略。当发送或接受请求时,会自动添加或移除"chunked"传输编码。
    TransferEncoding []string
    // Close在服务端指定是否在回复请求后关闭连接,在客户端指定是否在发送请求后关闭连接。
    Close bool
    // 在服务端,Host指定URL会在其上寻找资源的主机。
    // 根据RFC 2616,该值可以是Host头的值,或者URL自身提供的主机名。
    // Host的格式可以是"host:port"。
    //
    // 在客户端,请求的Host字段(可选地)用来重写请求的Host头。
    // 如过该字段为"",Request.Write方法会使用URL字段的Host。
    Host string
    // Form是解析好的表单数据,包括URL字段的query参数和POST或PUT的表单数据。
    // 本字段只有在调用ParseForm后才有效。在客户端,会忽略请求中的本字段而使用Body替代。
    Form url.Values
    // PostForm是解析好的POST或PUT的表单数据。
    // 本字段只有在调用ParseForm后才有效。在客户端,会忽略请求中的本字段而使用Body替代。
    PostForm url.Values
    // MultipartForm是解析好的多部件表单,包括上传的文件。
    // 本字段只有在调用ParseMultipartForm后才有效。
    // 在客户端,会忽略请求中的本字段而使用Body替代。
    MultipartForm *multipart.Form
    // Trailer指定了会在请求主体之后发送的额外的头域。
    //
    // 在服务端,Trailer字段必须初始化为只有trailer键,所有键都对应nil值。
    // (客户端会声明哪些trailer会发送)
    // 在处理器从Body读取时,不能使用本字段。
    // 在从Body的读取返回EOF后,Trailer字段会被更新完毕并包含非nil的值。
    // (如果客户端发送了这些键值对),此时才可以访问本字段。
    //
    // 在客户端,Trail必须初始化为一个包含将要发送的键值对的映射。(值可以是nil或其终值)
    // ContentLength字段必须是0或-1,以启用"chunked"传输编码发送请求。
    // 在开始发送请求后,Trailer可以在读取请求主体期间被修改,
    // 一旦请求主体返回EOF,调用者就不可再修改Trailer。
    //
    // 很少有HTTP客户端、服务端或代理支持HTTP trailer。
    Trailer Header
    // RemoteAddr允许HTTP服务器和其他软件记录该请求的来源地址,一般用于日志。
    // 本字段不是ReadRequest函数填写的,也没有定义格式。
    // 本包的HTTP服务器会在调用处理器之前设置RemoteAddr为"IP:port"格式的地址。
    // 客户端会忽略请求中的RemoteAddr字段。
    RemoteAddr string
    // RequestURI是被客户端发送到服务端的请求的请求行中未修改的请求URI
    // (参见RFC 2616, Section 5.1)
    // 一般应使用URI字段,在客户端设置请求的本字段会导致错误。
    RequestURI string
    // TLS字段允许HTTP服务器和其他软件记录接收到该请求的TLS连接的信息
    // 本字段不是ReadRequest函数填写的。
    // 对启用了TLS的连接,本包的HTTP服务器会在调用处理器之前设置TLS字段,否则将设TLS为nil。
    // 客户端会忽略请求中的TLS字段。
    TLS *tls.ConnectionState

一些比较重要的字段:

  • URL:请求 URL
  • Method:请求方法
  • Proto:HTTP 协议版本
  • Header:请求头(字典类型的键值对集合)
  • Body:请求实体(实现了 io.ReadCloser 接口的只读类型)
  • Form、PostForm、MultipartForm:请求表单相关字段,可用于存储表单请求信息

2.1 获取请求URL

URL也是一个结构体:

type URL struct 
	Scheme 	string
	Opaque 	string		//编码后的不透明数据User
	User	*Userinfo 	//用户名和密码信息
	Host	string		//host或host:port
	Path	string		//表示 HTTP 请求路径
	RawQuery string 	//编码后的查询字符串,没有"?’
	Fragment string 	//引用的片段(文档位置),没有'#'

  • Path 字段和RawQuery 字段:

    对于URL:http://127.0.0.1:8080/hello?username=admin&password=123456

    • 通过 r.URL.Path 只能得到 /hello
    • 通过 r.URL.RawQuery 得到的是 username=admin&password=123456

URL解析示例:

func handler(w http.ResponseWriter, r *http.Request) 
    
	fmt.Printf("URL: %#v\\n", r.URL)
	fmt.Printf("URL.Path: %#v\\n", r.URL.Path)
	fmt.Printf("URL.RawQuery: %#v\\n", r.URL.RawQuery)
    


func main() 
	http.HandleFunc("/hello", handler)
	
	http.ListenAndServe(":8080", nil)

访问http://127.0.0.1:8080/hello?username=admin&password=123456,执行结果:

2.2 获取请求头中的信息

通过 Request 结果中的Header 字段用来获取请求头中的所有信息,Header 字段的类型是 Header 类型,而 Header 类型是一个 map[string][]string,string 类型的 key,string 切片类型的值。

下面是 Header 类型及它的方法:

获取请求头中的某个具体属性的值,如获取 Accept-Encoding 的值:

  • 方式一:r.Header["Accept-Encoding"],得到的是一个字符串切片

  • 方式二:r.Header.Get("Accept-Encoding"),得到的是字符串形式的值,多个值使用逗号分隔

  • 代码示例:

    func handler(w http.ResponseWriter, r *http.Request) 
    
    	//请求头
    	fmt.Printf("Header: %#v\\n", r.Header)
    	fmt.Println("请求头中Accept-Encoding属性的值:",r.Header["Accept-Encoding"])
    	fmt.Println("请求头中Accept-Encoding属性的值:",r.Header.Get("Accept-Encoding"))
    	
    
    
    func main() 
    	http.HandleFunc("/hello", handler)
        
    	http.ListenAndServe(":8080", nil)
    
    

    访问http://127.0.0.1:8080/hello?username=admin&password=123456,执行结果:

2.3 获取请求体中的信息

请求和响应的主体都是有 Request 结构中的 Body 字段表示,该字段的类型是io.ReadCloser接口。该接口包含了 Reader 接口和 Closer 接口,Reader 接口拥有 Read方法,Closer 接口拥有 Close 方法

  • 代码示例:从请求中读取Body并输出

    func handler(w http.ResponseWriter, r *http.Request) 
    
    	//获取请求体中内容的长度
    	len := r.ContentLength
    	//创建byte切片
    	body := make([]byte, len)
    	//将请求体中的内容读取到body中
    	r.Body.Read(body)
    	//在浏览器中显示请求体中的内容
    	fmt.Fprintln(w, "请求体中的内容有:", string(body))
        
    
    
    func main() 
    	http.HandleFunc("/getBody", handler)
    	
    	http.ListenAndServe(":8080", nil)
    
    
    

    使用表单发送post请求

    <form action="http://localhost:8080/getBody" method="POST">
        用户名:<input type="text" name="username" value="zhangsan" /><br/>
        密码:<input type="password" name="password" value="666666" /><br/>
        <input type="submit" />
    </form>
    

    访问http://localhost:8080/getBody,浏览器显示结果:

2.4 获取请求参数

在Request结构中,有3个和form有关的字段:

// Form字段包含了解析后的form数据,包括URL的query、POST/PUT提交的form数据
// 该字段只有在调用了ParseForm()之后才有数据
Form url.Values

// PostForm字段不包含URL的query,只包括POST/PATCH/PUT提交的form数据
// 该字段只有在调用了ParseForm()之后才有数据
PostForm url.Values // Go 1.1

// MultipartForm字段包含multipart form的数据
// 该字段只有在调用了ParseMultipartForm()之后才有数据
MultipartForm *multipart.Form

一般获取请求参数的逻辑:

  • 方法一:

    • 先调用ParseForm()或ParseMultipartForm()解析请求中的数据
    • 按需访问Request结构中的Form、PostForm或MultipartForm字段
  • 方法二:直接使用Request的方法:

    • FormValue(key)
    • PostFormValue(key)

2.4.1 Form和PostForm字段

Form 和 PostForm字段都是 url.Values 类型,都能用来获取表单中的请求参数。

注意:Form 和 PostForm字段必须调用 Request 的 ParseForm() 方法后才有效

  • 不同的是:

    • Form 字段包括 URL 字段的请求参数POST 或 PUT 的表单数据
    • PostForm字段只包括POST 或 PUT 的表单数据
  • Form 和 PostForm 字段只支持 application/x-www-form-urlencoded 编码,如果 form 表单的 enctype 属性值为 multipart/form-data,那么使用 Form 和 PostForm 字段无法获取表单中的数据,此时需要使用 MultipartForm 字段。(此时Form 字段仍然有 URL 字段的请求参数)

代码示例:(获取请求参数)

  • form表单

    <form action="http://localhost:8080/getBody?username=admin&pwd=123456" method="POST">
        用户名:<input type="text" name="username" value="zhangsan" /><br/>
        密码:<input type="password" name="password" value="666666" /><br/>
        <input type="submit" />
    </form>
    
  • web handler代码:

    func handler(w http.ResponseWriter, r *http.Request) 
    
    	//解析表单,在调用r.Form之前必须执行该操作
    	r.ParseForm()
    	//获取请求参数
    	//如果form表单的action属性的URL地址中也有与form表单参数名相同的请求参数,
    	//那么参数值都可以得到,并且form表单中的参数值在URL的参数值的前面
    	fmt.Fprintln(w, "请求参数有:", r.Form)
    	fmt.Fprintln(w, "POST请求的form表单中的请求参数有:", r.PostForm)
    
    
    
    func main() 
    	http.HandleFunc("/getBody", handler)
    	
    	http.ListenAndServe(":8080", nil)
    
    
    
  • 点击提交按钮显示结果:

2.4.2 FormValue()和PostFormValue()

这两个方法在需要时会自动调用ParseForm()或ParseMultipartForm(),所以使用这两个方法取Form数据的时候,可以不用显式解析Form。

  • FormValue()返回form数据和url 请求参数后的第一个值。要取得完整的值,还是需要访问Request.Form或Request.PostForm字段。但因为FormValue()已经解析过Form了,所以无需再显式调用ParseForm()再访问request中Form相关字段。

  • PostFormValue()返回form数据的第一个值,因为它只能访问form数据,所以忽略URL的请求参数部分

代码示例: