处理请求
Request结构体
type Request struct {
Method string //请求方法
URL *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
}
Request结构体主要用于返回HTTP请求的响应,是HTTP处理请求中非常重要的一部分。只有正确地解析请求数据,才能向客户端返回响应。接下来通过简单示例来测试一下。
/*
Request结构体解析返回示例
*/
func main() {
http.HandleFunc("/hello", request)
if err := http.ListenAndServe(":8080", nil); err != nil {
log.Fatal(err)
}
}
func request(w http.ResponseWriter, r *http.Request) {
fmt.Println("Request解析")
//HTTP方法
fmt.Println("method", r.Method)
//RequestURI是被客户端发送到服务器端的请求行中未修改的请求URI
fmt.Println("RequestURI:", r.RequestURI)
//URL类型
fmt.Println("URL_path", r.URL.Path)
fmt.Println("URL_RawQuery", r.URL.RawQuery)
fmt.Println("URL_Fragment", r.URL.Fragment)
//协议版本
fmt.Println("proto", r.Proto)
fmt.Println("protomajor", r.ProtoMajor)
fmt.Println("protominor", r.ProtoMinor)
//HTTP请求头
for k, v := range r.Header {
for _, vv := range v {
fmt.Println("header key:" + k + " value:" + vv)
}
}
//判断是否为multipart方式
isMultipart := false
for _, v := range r.Header["Content-Type"] {
if strings.Index(v, "multipart/form-data") != -1 {
isMultipart = true
}
}
fmt.Println("isMultipart:", isMultipart)
//解析Form表单
if isMultipart == true {
r.ParseMultipartForm(128)
fmt.Println("解析方式:ParseMultipartForm")
} else {
r.ParseForm()
fmt.Println("解析方式:ParseForm")
}
//HTTP Body内容长度
fmt.Println("ContentLength:", r.ContentLength)
//是否在回复请求后关闭连接
fmt.Println("Close", r.Close)
//HOST
fmt.Println("host", r.Host)
//请求的来源地址
fmt.Println("RemoteAddr", r.RemoteAddr)
fmt.Fprintf(w, "hello")
}
//GET http://127.0.0.1:8080/hello?age=18
Request解析
method GET
RequestURI: /hello
URL_path /hello
URL_RawQuery
URL_Fragment
proto HTTP/1.1
protomajor 1
protominor 1
header key:Upgrade-Insecure-Requests value:1
header key:Accept value:text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
header key:User-Agent value:Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_2) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.4 Safari/605.1.15
header key:Accept-Language value:zh-cn
header key:Accept-Encoding value:gzip, deflate
header key:Connection value:keep-alive
isMultipart: false
解析方式:ParseForm
ContentLength: 0
Close false
host 127.0.0.1:8080
RemoteAddr 127.0.0.1:50554
Request解析
method GET
RequestURI: /hello?age=18
URL_path /hello
URL_RawQuery age=18
URL_Fragment
proto HTTP/1.1
protomajor 1
protominor 1
header key:Accept-Encoding value:gzip, deflate
header key:Connection value:keep-alive
header key:Upgrade-Insecure-Requests value:1
header key:Accept value:text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
header key:User-Agent value:Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_2) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.4 Safari/605.1.15
header key:Accept-Language value:zh-cn
isMultipart: false
解析方式:ParseForm
ContentLength: 0
Close false
host 127.0.0.1:8080
RemoteAddr 127.0.0.1:50554
URL结构体
type URL struct {
Scheme string //方案
Opaque string //编码后的不透明数据
User *Userinfo //基本验证方式中的username和password信息
Host string //主机字段
Path string //路径
RawPath string
ForceQuery bool
RawQuery string //查询字段
Fragment string //分片字段
}
该结构体主要用来存储URL各部分的值。net/url包中的很多方法都是对URL结构体进行相关操作,其中Parse()函数的定义如下:
func Parse(rawrul string) (*URL, error)
/*
Parse()函数查看URL结构体
*/
func main() {
path := "http://localhost:8080/article?id=1"
p, _ := url.Parse(path)
println(p.Host) //localhost:8080
println(p.User) //0x0
println(p.RawQuery) //id=1
println(p.RequestURI())// /article?id=i
}
请求头
请求头和响应头使用Header类型表示。Header类型是一个映射(map)类型,表示HTTP请求头中的多个键值对。其定义如下:
type Header map[string][]string
通过请求对象的Header属性可以访问到请求头信息。Header属性是映射结构,提供了Get()方法以获取key对应的第一个值。Get()方法的定义如下:
func (h Header) Get(key string)
func (h Header) Set(key, value string)
func (h Header) Add(key, value string)
func (h Header) Del(key string)
func (h Header) Write(w io.Writer) error
例如,要返回一个JSON格式的数据:
func main() {
http.HandleFunc("/", Hello)
if err := http.ListenAndServe(":8080", nil); err != nil {
log.Fatal(err)
}
}
type Greeting struct {
Message string `json:"message"`
}
func Hello(w http.ResponseWriter, r *http.Request) {
//返回JSON格式数据
greeting := Greeting{
"hello world",
}
message, _ := json.Marshal(greeting)
//通过Set()方法设置Content-Type为application/json类型
w.Header().Set("Content-Type", "application/json")
w.Write(message)
}
请求体
请求体和响应体都由Request结构中的Body字段表示。Body字段是一个io.ReadCloser接口。ReadCloser接口的定义如下:
type ReadCloser interface {
Reader
Closer
}
Body字段是Reader接口和Closer接口的结合,Reader接口的定义如下:
type Reader interface {
Read(p []byte) (n int, err error)
}
通过Reader接口可以看到,Read()方法实现了ReadCloser接口。所以,可以通过Body.Read()方法来读取请求体信息。接下来通过示例来加深对Body.Read()方法的理解。
/*
Body.Read()方法
*/
func main() {
http.HandleFunc("/getBody", getBody)
http.ListenAndServe(":8080", nil)
}
func getBody(w http.ResponseWriter, r *http.Request) {
//获取请求报文内容的长度
len := r.ContentLength
//新建一个字节切片,长度与请求报文的内容长度相同
body := make([]byte, len)
//读取r的请求体,并将具体内容写入Body中
r.Body.Read(body)
//将获取的参数内容写入响应的报文中
fmt.Fprintf(w, string(body))
}
处理HTML表单
POST和GET请求都可以传递表单,但GET请求会暴露参数给用户,所以一般用POST请求传递表单。
在用GET请求传递表单时,表单数据以键值对的形式包含在请求的URL里。服务器在接收到浏览器发送的表单数据后,需要先对这些数据进行语法分析,才能提取数据中记录的键值对。
表单的enctype属性
HTML表单的内容类型(content type)决定了POST请求在发送键值对时将使用何种格式。HTML表单的内容类型是由表单的enctype属性指定的。enctype属性有以下3种:
- application/x-www-form-urlencoded
这是表单默认的编码类型。该类型会把表单中的数据编码为键值对,且所有字符会被编码(空格被转换为“+”号,特殊符号被转换为ASC||HEX值)。- 当Method属性为GET时,表单中的数据会被转换为“name1=value1&name2=value2&…”形式,并拼接到请求的URL后面,以“?”分隔。queryString的URL加密采用的编码字符集取决于浏览器。例如表单中有“age:28”,采用UTF-8编码,则请求的URL为“…?age=28”。
- 当method属性为POST时,在数据被添加到HTTP Body(请求体)中后,浏览器会根据在网页的ContentType(“text/html;charset-=UTF-8”)中指定的编码对表单中的数据进行编码,请求数据同上为“age=28”。
- multipart/form-data.
如果不对字符编码,则此时表单通常采用POST方式提交。该类型对表单以控件为单位进行分隔,为每个部分加上Content–Disposition(form-data|file)、Content–Type(默认text/plain)、name(控件name)等信息,并加上分隔符(边界boundary)。该类型一般用于将二进制文件上传到服务器。 - text/plain。
text/plain类型用于发送纯文本内容,常用于向服务器传递大量文本数据。该类型会将空格转换为加号(+),不对特殊字符进行编码,一般用于发送E-mail之类的数据信息。
Go语言的Form与PostForm字段
Form字段支持URL编码,键值的来源是URL和表单。
PostForm字段支持URL编码,键值的来源是表单。如果一个键同时拥有表单键值和URL键值,同时用户只想获取表单键值,则可使用PostForm字段,示例代码如下:
func process(w http.ResponseWriter, r *http.Request) {
if r.Method == "GET" {
t, _ := template.ParseFiles("form.html")
t.Execute(w, nil)
} else {
r.ParseForm() //语法分析
fmt.Fprintln(w, "表单键值对和URL键值对:", r.Form)
fmt.Fprintln(w, "表单键值对:", r.PostForm)
}
}
对应的HTML:
<!DOCTYPE html>
<html lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html" charset="UTF-8">
<title>Form提交</title>
</head>
<body>
<form action="http://127.0.0.1:8089?name=go&color=green" method="post" enctype="application/x-www-form-urlencoded">
<input type="text" name="name" value="shirdon"/>
<input type="text" name="color" value="green"/>
<input type="submit"/>
</form>
</body>
</html>
Go语言的MultipartForm字段
Go语言的MultipartForm字段支持mutipart/form-data编码,键值来源是表单,常用于文件的上传。
MultipartForm字段的使用示例如下:
func multiProcess(w http.ResponseWriter, r *http.Request) {
if r.Method == "GET" {
t, _ := template.ParseFiles("form.html")
t.Execute(w, nil)
} else {
r.ParseMultipartForm(1024) //从表单里提取多少字节的数据
fmt.Fprintln(w, "表单键值对:", r.MultipartForm) //multipartform是包含2个映射的结构
}
}
multpart/form-data编码通常用于实现文件上传,需要File类型的Input标签。其HTML示例如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html" charset="UTF-8">
<title>upload上传文件</title>
</head>
<body>
<form action="http://localhost:8089/file" method="post" enctype="multipart/form-data">
<input type="file" name="uploaded">
<input type="submit">
</form>
</body>
</html>
Form表单上传的Go语言示例代码如下:
//上传
func upload(w http.ResponseWriter, r *http.Request) {
if r.Method == "GET" {
t, _ := template.ParseFiles("./src/bookWebPro/chapter3/upload.html")
t.Execute(w, nil)
} else {
r.ParseMultipartForm(4096)
fileHeader := r.MultipartForm.File["uploaded"][0] //获取名为"uploaded"的第一个文件头
file, err := fileHeader.Open() //获取文件
if err != nil {
fmt.Println("error")
return
}
data, err := ioutil.ReadAll(file) //读取文件
if err != nil {
fmt.Println("error!")
return
}
fmt.Fprintln(w, string(data))
}
}
func main() {
http.HandleFunc("/", upload) //设置首页的路由
http.ListenAndServe(":8089", nil) //设置监听的端口
}
ResponseWriter原理
G0语言对接口的实现,不需要显示的声明,只要实现了接口定义的方法,那就实现了相应的接口。
io.Writer是一个接口类型。如果要使用io.Writer接口的Write()方法,则需要实现Write(p []byte)(n int,err error)方法。
在Go语言中,客户端请求信息都被封装在Request对象中。但是发送给客户端的响应并不是Response对象,而是ResponseWriter接口。ResponseWriter接口是处理器用来创建HTTP响应的接口的。ResponseWriter接口的定义如下:
type ResponseWriter interface {
//用于设置或者获取所有响应头信息
Header() Header
//用于写入数据到响应体中
Write([]byte) (int, error)
//用于设置响应状态码
WriteHeader(int)
}
实际上,在底层支撑ResponseWriter接口的是http.response结构体。在调用处理器处理HTTP请求时,会调用readRequest()方法。readRequest()方法会声明response结构体,并且其返回值是response指针。这也是在处理器方法声明时,Request是指针类型,而ResponseWriter不是指针类型的原因。实际上,响应对象也是指针类型。readRequest()方法的核心代码如下:
// Read next request from connection.
func (c *conn) readRequest(ctx context.Context) (w *response, err error) {
//....
w = &response{
conn: c,
cancelCtx: cancelCtx,
req: req,
reqBody: req.Body,
handlerHeader: make(Header),
contentLength: -1,
closeNotifyCh: make(chan bool, 1),
wants10KeepAlive: req.wantsHttp10KeepAlive(),
wantsClose: req.wantsClose(),
}
if isH2Upgrade {
w.closeAfterReply = true
}
w.cw.res = w
w.w = newBufioWriterSize(&w.cw, bufferBeforeChunkingSize)
return w, nil
}
response结构体的定义和ResponseWriter接口都位于server.go文件中。不过由于response结构体是私有的,对外不可见,所以只能通过ResponseWriter接口访问它。两者之间的关系是:ResponseWriter是一个接口,而response结构体实现了它。我们引用ResponseWriter接口,实际上引用的是response结构体的实例。
ResponseWriter接口包含WriteHeader()、Header()、Write()三个方法来设置响应状态码。
WriteHeader()方法
VriteHeader()方法支持传入一个整型数据来表示响应状态码。如果不调用该方法,则默认响应状态码是200。WriteHeader()方法的主要作用是在API接口中返回错误码。例如,可以自定义一个处理器方法noAuth(),并通过w.WriteHeader()方法返回一个401未认证状态码(注意,在运行时,w代表的是对应的response对象实例,而不是接口)。
/*
用WriteHeader()方法返回401未认证状态码
*/
func main() {
http.HandleFunc("/noAuth", noAuth)
if err := http.ListenAndServe(":8080", nil); err != nil {
log.Fatal(err)
}
}
func noAuth(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(401)
fmt.Fprintf(w, "未授权,认证后才能访问该接口!")
}
Header()方法
Header()方法用于设置响应头。可以通过w.Header().Set()方法设置响应头。w.Header()方法返回的是Header响应头对象,它和请求头共用一个结构体。因此在请求头中支持的方法这里都支持,比如可以通过w.Header().Add()方法新增响应头。
例如,如果要设置一个301重定向响应,则只需要通过w.WriteHeader()方法将响应状态码设置为301,再通过w.Header().Set()方法将“Location”设置为一个可访问域名即可。
新建一个处理器方法Redirect(),在其中编写重定向实现代码,如下:
/*
用w.Header().Set()方法设置301重定向示例
*/
func main() {
http.HandleFunc("/redirect", Redirect)
if err := http.ListenAndServe(":8080", nil); err != nil {
log.Fatal(err)
}
}
func Redirect(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Location", "https://www.baidu.com")
w.WriteHeader(301)
}
Write()方法
Write()方法用于将数据写入HTTP响应体中。如果在调用Write()方法时还不知道Content-Type类型,则可以通过数据的前512个byte进行判断。用Write()方法可以返回字符串
数据,也可以返回HTML文档和JSON等常见的文本格式。
== 返回文本字符串数据:==
func main() {
http.HandleFunc("/welcome", Welcome1)
if err := http.ListenAndServe(":8080", nil); err != nil {
log.Fatal(err)
}
}
func Welcome1(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("hello world"))
}
== 返回HTML文档:==
func Home(w http.ResponseWriter, r *http.Request) {
html := `<html>
<head>
<title>Write方法返回HTML文档</title>
</head>
<body>
<h1>你好 </h1>
</body>
</html>`
w.Write([]byte(html))
}
func main() {
http.HandleFunc("/", Home)
err := http.ListenAndServe(":8080", nil)
if err != nil {
fmt.Println(err)
}
}
此外,由于响应数据的内容类型变成了HTML。在响应头中可以看到,Content-Type也自动调整成了text/html,不再是纯文本格式。这里的Content-Type是根据传入的数据自行判断出来的。
== 返回JSON格式数据:==
type Greeting1 struct {
Message string `json:"message"`
}
func Hello1(w http.ResponseWriter, r *http.Request) {
// 返回 JSON 格式数据
greeting := Greeting1{
"欢迎",
}
message, _ := json.Marshal(greeting)
w.Header().Set("Content-Type", "application/json")
w.Write(message)
}
func main() {
http.HandleFunc("/", Hello1)
err := http.ListenAndServe(":8080", nil)
if err != nil {
fmt.Println(err)
}
}