[TOC]
web 开发
HTTP(超文本传输协议) 规定了客户端和服务器端的请求和响应,所有的服务器端接收的数据和响应都是 HTTP 协议。和语言没有关系
HTTP 协议是访问互联网使用的核心通信协议,也是所有 web 应用程序使用的通信协议
对于 HTTP 协议的消息模型:客户端发送请求消息,服务器端返回响应消息。传输层使用的是 TCP 协议,HTTP 协议本身不具有状态
go 语言中不使用框架也能够开发出 web 框架,因为内置在了 http 包中了
1 HTTP 请求
HTTP 请求消息分为消息头和消息主体,消息头和消息主体用空白行分隔。
我们可以通过 Fiddler 工具抓取浏览器的请求信息
HTTP 请求说明
1. 消息头第一行由三个以空格分隔的元素组成,分别为HTTP方法、请求的URL和使用的HTTP版本
➢HTTP方法;
1. GET:用于获取资源,参数通过URL 下字符串方式提交给服务器,无消息主体
2. POST:用于执行提交操作,参数可以通过URL 下字符串方式和消息主体提交给服务
3. HEAD:用于检测资源是否存在,与GET类似,区别在于在响应消息中返回的消息主体为空.
4. TRACE:用于诊断可判断客户端和服务器之间是否存在代理服务器,原理:服务器在响应主体中返回收到的请求消息的具体内容
5. OPTIONS:用于要求服务器报告对某一资源有效的HTTP方法, 服务器常返回Allow消息头的响应,并列出所有有效的方法
6. PUT:使用请求主体中的内容向服务器上传指定的资源,用于修改数据
7. DELETE:用于删除资源
8. CONNECT:
一般在使用的时候用GET,POST,HEAD,PUT,DELETE
➢请求URL:于捉请求的资源名称以及查下参数
➢使用的 HTTP 版本:常用2.0 和1.1版本,在 1.1 版本中请求消息中必须包含Host请求头
2. 其他
➢Host: 指定请求访问的主机名,当多个web站点部署在同-台主机上时需要使用Host消息头
➢User-Agent: 指定客户端软件的信息,不如浏览器类型和版本、操作系统类型和版本等
➢Referer:表示发出请求的原始URL
➢Cookie:提交服务器想客户端发布的其他参数
2 HTTP 响应
HTTP 响应消息分为消息头和消息体,消息头和消息体用空白行分割。
我们可以通过 Fiddler 工具抓取浏览器的请求信息
HTTP 响应说明:
1.消息头第一行由三个空格分开的元素组成,分别表示HTTP版本、请求状态码(数字)、请求状态描述
2.其他:
➢Server:旗标,指明使用的Web服务器软件
➢Set-Cookie: 设置cookie信息,在随后向服务器发送的请求中由Cookie消息头返回
➢Content-Type:指定消息主体类型
➢Content-Length:指定消息主体的字节长度
3 WEB 服务器端开发
http 包提供了 HTTP 服务器和客户端的开发接口,内置 web 服务器
1. 针对 web 服务端开发流程为
-
定义处理器/处理器函数
➢ 接收用户数据
➢ 返回信息
-
启动 web 服务器
HTTP
HTTP 服务端和客户端开发
url => 处理逻辑
处理器/处理器函数
2. 常用函数
-
服务端
- ListenAndServer:使用 http 协议启动 web 服务
- ListenAndServeTLS:使用 https 协议启动 web 服务器
- Handle:定义 url 对应处理器
- HandleFunc:定义 url 对应处理器函数
- Redirect:重定向
- SetCookie:设置 Cookie
- FileServer: 创建静态文件处理器
-
客户端
- Get:发送 get 请求
- Head:发送 head 请求
- Post:发送 post 请求
- PostForm:发送提交表单请求(application/x-www-form-urlencoded)
3. 常用结构体
-
Request
常用属性
➢ Method: 请求方式
➢ URL:请求的 URL
➢ Proto:请求协议
➢ Header:请求头
➢ ContentLength:请求体字节数量
➢ Body:请求体流对象
➢ Form:获取提交的所有数据,需要手动调用 ParseForm
➢ PostForm:获取所有 body 中提交的数据(application/x-www-form-urlencoded) ,需要手动调用 ParseForm
➢ MultipartForm:获取所有 body 中提交的数据(multipart/form-dat) ,需要手动调用 ParseMutipartForm
-
Reponse
常用属性
➢ Status:响应状态
➢ StatusCode:响应状态码
➢ Proto:协议信息
➢ Header:响应头
➢ ContentLength:响应体字节数量
➢ Body:响应体流对象
常用方法
➢ Cookies:cookie 信息
➢ Location:location 信息
➢ Write:将响应体写入输出流
-
ResponseWriter(接口)
常用方法:
➢ Write:写入响应体
➢ Header:写入响应头信息
➢ WriteHeader:写入请求行状态码(在调用后不能 Write 和设置 Header 信息)
-
Header
常用方法
➢ Set:设置头信息
➢ Add:添加头信息
➢ Del:删除头信息
➢ Get:获取头信息
-
Cookie
常用属性
➢ Name:名称
➢ Value:值
➢ Path:路径
➢ Domain:域名
➢ Expires:过期时间
➢ MaxAge:过期时间长度
➢ Secure:是否只有 https 携带
➢ HttpOnly:是否可使用 js 读取常用方法
➢ String:将 cookie 对象转化为字符串
3.1 处理器函数
- 处理器和处理器函数的区别在于,一个是面向对象(处理器),一个是面向过程(处理器函数)
- 处理器函数会通过 URL 去找处理器函数,找到之后在调用对应的处理器函数(http 包调用)
- 绑定 URL 和处理器关系
- 处理器函数签名由 http 包定义
// 定义处理器函数格式必须如下
func http.HandleFunc(pattern string, handler func(http.ResponseWriter, *http.Request))
范例:
package main
import "net/http"
// 处理器函数
// 定义处理器函数必须使用 http.ResponseWriter, *http.Request 这两个参数
func Home(w http.ResponseWriter, r *http.Request) {
}
func main() {
// addr 服务器监听的地址,:8888 监听我当前所有网卡的 8888
addr := ":8888"
// http.HandleFunc() 函数第一个参数是访问的 url , 第二个参数是绑定我们的处理器函数 Home
// 这里的意思是如果发现请求的是 /home 的 url 就交给 Home 函数来处理
http.HandleFunc("/home", Home)
// 启动 http 服务
http.ListenAndServe(addr, nil)
}
执行程序,8888 端口 已经监听
浏览器访问我们的服务器的 8888 端口,由于我们请求的这个 URL 现在并没有 / 文件所以状态码 404
当我们在浏览器输入 home
会发现并没有 404 状态码,而是 200
但是我们现在会发现我们请求了这个 /home
路径并没有做任何处理,所以这个时候我们需要在 Home 处理函数里面随便打印一点数据
package main
import (
"fmt"
"net/http"
)
// 处理器函数
// 定义处理器函数必须使用 http.ResponseWriter, *http.Request 这两个参数
func Home(w http.ResponseWriter, r *http.Request) {
fmt.Println("hello wordl")
}
func main() {
// addr 服务器监听的地址,:8888 监听我当前所有网卡的 8888
addr := ":8888"
// http.HandleFunc() 函数第一个参数是访问的 url , 第二个参数是绑定我们的处理器函数 Home
// 这里的意思是如果发现请求的是 /home 的 url 就交给 Home 函数来处理
http.HandleFunc("/home", Home)
// 启动 http 服务
http.ListenAndServe(addr, nil)
}
执行程序,在使用 浏览器访问该 rul
这个时候我们会发现该程序会在终端上输出我们的
hello world
我们为什么没有把数据输入到浏览器上呢,因为我们需要将数据写到 socket 才能实现,由此引出下面案例
3.1.1 实现将数据传输至浏览器(响应)
在 web 应用中,我们可以把 web 理解为一个调用函数的过程,我们在访问对应的 URL 其实就是在调用该 URL 对应的函数,如
http://10.0.0.10:8888/home
这个 home rul 对应的就是程序的 func Home
函数处理器。
对于 浏览器 来说其实也是有关注函数参数和返回值的,参数就是用户给服务器端提交的数据,返回值就是服务器端要告知用户的数据
把浏览器访问的过程对应的函数上就是,url = 函数名称,提交数据 = 函数参数(输入),服务器告知用户数据 = 函数返回值(输出)
对于我们的处理器函数,也要去处理用户提交的数据和告知用户的数据,当用户访问的时候其实就已经到我们的程序了,在这个过程中需要由代码转为协议的过程
package main
import (
"net/http"
)
// 处理器函数
// 定义处理器函数必须使用 http.ResponseWriter, *http.Request 这两个参数
func Home(w http.ResponseWriter, r *http.Request) {
// 用户提交的数据 http 内容 到 go 代码转换的关系
// 提交的所有数据都会转换到 *http.Request 中去(请求行,请求头,请求体)
// 获取用户提交的数据我们就需要通过 *http.Request 对象
// 输出就是 http.ResponseWriter 对象,这里把 hello zzz 输出到浏览器
w.Write([]byte("hello zzz"))
}
func main() {
// addr 服务器监听的地址,:8888 监听我当前所有网卡的 8888
addr := ":8888"
// http.HandleFunc() 函数第一个参数是访问的 url , 第二个参数是绑定我们的处理器函数 Home
// 这里的意思是如果发现请求的是 /home 的 url 就交给 Home 函数来处理
http.HandleFunc("/home", Home)
// 启动 http 服务
http.ListenAndServe(addr, nil)
}
执行
浏览器访问
http://10.0.0.10:8888/home
查看响应,就是我们在程序中的
hello zzz
3.2 go 实现静态web服务器
我们都知道 nginx 可以作为一个静态的 web 服务器,那么我们使用 go 该怎么写呢
// 第一步需要绑定处理器,使用 http.Handle() 函数
func http.Handle(pattern string, handler http.Handler)
// fileserver 返回一个 Handler 结构体
func FileServer(root FileSystem) Handler
范例:
package main
import "net/http"
func main() {
// 定义服务器开启端口
addr := ":8888"
// 步骤如下:
// 1.绑定处理器
// 2.将该 /static url 的请求和响应都交给 Handle 处理器来处理
// 3.通过 http.FileServer() 来定义处理器的类型
// 4.http.FileServer() 传递静态资源路径用来实现响应静态资源
// 5.http.FileServer() 需要使用 http.FileSystem 的结构体里面的方法来对静态资源进行处理
// 6.http.Dir 是一个 string 类型转换并不是函数,将 /static 的目录中的静态文件进行输出
http.Handle("/static/", http.FileServer(http.Dir("."))) //. 表示当前路径下的 static 目录
// 7.最后启动 web 服务器
http.ListenAndServe(addr, nil)
}
- 启动静态 web 服务器程序
- 浏览器访问 404 因为我们还没有创建 static 目录
- 在当前目录下创建
static
目录
[13:06:18 root@go staticserver]#mkdir static
# 追加两个文件进来
[13:09:18 root@go staticserver]#echo "11111" > static/1.txt
[13:09:30 root@go staticserver]#echo "22222" > static/2.txt
- 再次启动静态 web 服务器程序
- 浏览器再次访问
http://10.0.0.10:8888/static/
就会有1.txt
和2.txt
两个文件,内容也有
3.2.1 使用虚拟 URL 提供服务
当我们有时候不想把真是的服务器提供的 web 页面路径暴露出去,想通过一个虚拟的路径来提供访问,我们可以使用http.StripPrefix()
func http.StripPrefix(prefix string, h http.Handler) http.Handler
package main
import "net/http"
func main() {
// 定义服务器开启端口
addr := ":8888"
// http.Handle() 处理器处理来自 www 的 url 请求
// http.StripPrefix() 用来处理虚拟路径
http.Handle("/www/", http.StripPrefix("/www", http.FileServer(http.Dir("./static"))))
http.ListenAndServe(addr, nil)
}
启动程序浏览器访问http://10.0.0.10:8888/www/
,可以看到这里明明是访问的 www RUL 但是依旧能够实现访问到 /static 目录下的文件内容
3.3 web 处理器
处理器和处理器函数的区别在于,一个是面向对象(处理器),一个是面向过程(处理器函数)
我们在定义处理器的时候一般定义自定义类型或者定义结构体类型
# 查看 go doc
[06:38:02 root@go staticserver]#go doc http.handler
package http // import "net/http"
# 必须实现这个方法
type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}
范例
package main
import (
"fmt"
"net/http"
)
type Help struct{}
// 给 Help 绑定方法,实现 http.handler 接口,满足 ServHTTP 现这个方法
func (h *Help) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// 通过 fmt.Fprint 写入数据至浏览器响应
fmt.Fprint(w, "help")
}
func main() {
addr := ":8888"
// 绑定 url 传递 Help 结构体地址
http.Handle("/help", new(Help))
http.ListenAndServe(addr, nil)
}
执行
# 启动程序
[06:50:14 root@go web]#go run web1.go
# 访问本机的 8888 端口后 help 路径
[06:51:06 root@go ~]#curl 127.0.0.1:8888/help
help
# 输出 help
处理器函数和处理器的区别,一个是结构体或者说是自定义类型一个是函数
绑定的时候区别:
处理器: http.Handle()
是实现一个 ServeHTTP()
的类型,所以这里就传了一个结构体,然后这个结构体就满足 ServeHTTP()
的方法,这是一个面向对象的写法
处理器函数:http.HandleFunc()
是面向过程
4 WEB 客户端开发
当我们使用爬虫的时候,或者调用别人 API 的时候就会使用到 web 客户端开发。
对于客户端来说一般都是发起请求,客户端的话其实就是模拟浏览器的这么一个过程,需要将我们的代码组装成一个对象,在发起的时候其实就是创建 tcp 链接,给服务器发起一个 http 请求的文本
4.1 web Get 请求范例
在工作中不懂就看官方文档
# 通过 go doc 可以看到 http.get 函数中返回值有一个 response
[06:54:40 root@go ~]#go doc http.get
package http // import "net/http"
func Get(url string) (resp *Response, err error)
# 所以在查看 response
[07:05:01 root@go ~]#go doc http.response
type Response struct {
Status string // e.g. "200 OK"
StatusCode int // e.g. 200
Proto string // e.g. "HTTP/1.0"
ProtoMajor int // e.g. 1
ProtoMinor int // e.g. 0
.....省略.....
Body io.ReadCloser
# 由于要使用 response.Body 所以我们查看 response.Body 的信息发现他是一个接口
[07:06:33 root@go ~]#go doc io.ReadCloser
package io // import "io"
type ReadCloser interface {
Reader
Closer
}
范例代码:
package main
import (
"fmt"
"io"
"net/http"
"os"
)
func main() {
// 模拟浏览器,对服务器端发起 get 请求,这里是对本机的 127.0.0.1:8888/help 进行请求
response, err := http.Get("http://127.0.0.1:8888/help")
if err != nil {
fmt.Println(err)
return
}
// 由于 response.Body 是一个 io.ReadCloser 接口
// 这里通过 os.Stdout 将 response.Body 的信息输出到终端
io.Copy(os.Stdout, response.Body)
}
执行
[07:17:52 root@go webclient]#go run main.go
help
# 通过执行就拿到了 help 浏览器当前的数据
当然也可以拿取其他信息:
package main
import (
"fmt"
"io"
"net/http"
"os"
)
func main() {
response, err := http.Get("http://127.0.0.1:8888/help")
if err != nil {
fmt.Println(err)
return
}
// 协议信息
fmt.Println(response.Proto)
// 获取状态码
fmt.Println(response.StatusCode)
// 获取 Body
io.Copy(os.Stdout, response.Body)
}
执行
[07:17:55 root@go webclient]#go run main.go
HTTP/1.1
200
help
也能对 https 的 url 进行获取
package main
import (
"fmt"
"io"
"net/http"
"os"
)
func main() {
// 访问 百度
response, err := http.Get("https://www.baidu.com")
if err != nil {
fmt.Println(err)
return
}
// io.Copy 进行输出
io.Copy(os.Stdout, response.Body)
}
执行
[07:42:08 root@go webclient]#go run main.go
<html>
<head>
<script>
location.replace(location.href.replace("https://","http://"));
</script>
</head>
<body>
<noscript><meta http-equiv="refresh" content="0;url=http://www.baidu.com/"></noscript>
</body>
</html>
4.2 NewRequest 实现模拟请求
NewRequest 是最原生的实现方法
Golang 通过 http.NewRequest 实现模拟请求,添加请求头和请求参数:
[07:21:03 root@go webclient]#go doc http.newrequest
package http // import "net/http"
func NewRequest(method, url string, body io.Reader) (*Request, error)
# 通过查看 NewRequest() 函数我们可以看到它返回的就是一个 *Request 指针
范例
package main
import (
"fmt"
"io"
"net/http"
"os"
)
func main() {
// 实现一个 http 请求
req, err := http.NewRequest("DELETE", "http://127.0.0.1:8888/help", nil)
// 模拟客户端
client := http.Client{}
// client.DO() 需要接收一个 *http.Request 请求,然后返回 *http.Response, error
response, err = client.Do(req)
// 拿到 127.0.0.1:8888/help 的 body 信息
io.Copy(os.Stdout, response.Body)
}
执行
[07:37:29 root@go webclient]#go run main.go
help
4.3 对不受信任的网站进行访问
当我们有时候需要对不受信任的证书网站进行访问的时候就可以使用下面的这种方法。
这是默认的访问方式不做处理时
package main
import (
"fmt"
"io"
"net/http"
"os"
)
func main() {
// 对 https://www.conardli.top/ 该网站进行访问,这个网站是一个不受信任的 https
response, err := http.Get("https://www.conardli.top/")
if err != nil {
fmt.Println(err)
} else {
io.Copy(os.Stdout, response.Body)
}
}
执行
[07:52:40 root@go webclient]#go run main.go
Get "https://www.conardli.top/": x509: certificate has expired or is not yet valid: current time 2021-07-12T07:53:20+08:00 is after 2019-08-13T12:00:00Z
# 证书已过期或尚未生效:当前时间2021-07-12T07:53:20+08:00在2019-08-13T12:00:00Z之后
那我们如何对这种不受信任的网站进行访问呢?这个时候我们就需要使用最原生的方法来对他进行访问
package main
import (
"crypto/tls"
"fmt"
"io"
"net/http"
"os"
)
func main() {
// 定义最原生的访问方式并写入 url
res, err := http.NewRequest("Get", "https://www.conardli.top/", nil)
if err != nil {
fmt.Println(err)
return
}
// 定义对非信任的证书进行访问
client := http.Client{
Transport: &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
},
},
}
// 将响应结果进行输出
response, err := client.Do(res)
if err != nil {
fmt.Println(err)
} else {
io.Copy(os.Stdout, response.Body)
}
}
执行,这就是跳过一个非受信任的 https 服务器端的验证
07:53:20 root@go webclient]#go run main.go
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="tex
.....省略.....
5 如何获取请求数据
我们先写一个 server 端的的 http 服务器,输出当前时间
package main
import (
"fmt"
"net/http"
"time"
)
func main() {
addr := ":8888"
// 绑定处理器函数
http.HandleFunc("/", func(rw http.ResponseWriter, r *http.Request) {
fmt.Fprint(rw, time.Now().Format("2006-01-02 15:04:05"))
})
http.ListenAndServe(addr, nil)
}
执行
请求:
通过 Fiddler 工具我们可以看到我们现在请求体里面没有任何数据
响应
我们现在看一下响应,当前响应只有时间
现在我们来看一下请求行,请求头里面的数据因该如何来获取
5.1 获取请求行中数据
请求行中有请求的方式,请求的 Url
, 请求的协议,我们都知道请求的对象会封装到 http.Request
对象中,所以我们自然的看 http.Request
对象,我们所有请求的信息都在 http.Request
里面
通过 go doc http.Request
对象,发现他是一个结构体
package main
import (
"fmt"
"net/http"
"time"
)
func main() {
addr := ":8888"
http.HandleFunc("/", func(rw http.ResponseWriter, r *http.Request) {
// 获取请求方法
fmt.Println("method:", r.Method)
// 获取 url
fmt.Println("url:", r.URL)
// 获取 http 协议版本
fmt.Println("http protocol:", r.Proto)
fmt.Fprint(rw, time.Now().Format("2006-01-02 15:04:05"))
})
http.ListenAndServe(addr, nil)
}
只要我们一访问该服务,我们就会在终端看到我们的请求行的数据
终端输出
5.2 获取请求头中数据
我们通过go doc http.request.header
查看得知 header 是一个 header 自定义类型,由此我们知道请求的数据是一个 key/value 的结构体,所以 header 应该是一个 map 结构
[05:53:19 root@go web]#go doc http.header
package http // import "net/http"
type Header map[string][]string
A Header represents the key-value pairs in an HTTP header.
The keys should be in canonical form, as returned by CanonicalHeaderKey.
func (h Header) Add(key, value string)
func (h Header) Clone() Header
func (h Header) Del(key string)
func (h Header) Get(key string) string
func (h Header) Set(key, value string)
func (h Header) Values(key string) []string
func (h Header) Write(w io.Writer) error
func (h Header) WriteSubset(w io.Writer, exclude map[string]bool) error
package main
import (
"fmt"
"net/http"
"time"
)
func main() {
addr := ":8888"
http.HandleFunc("/", func(rw http.ResponseWriter, r *http.Request) {
// 因为我们知道了 r.Header 是一个 map 类型,所以我们 for-range 遍历获取请求头数据
for k, v := range r.Header {
fmt.Println(k, v)
}
fmt.Fprint(rw, time.Now().Format("2006-01-02 15:04:05"))
})
http.ListenAndServe(addr, nil)
}
只要我们一访问该服务,我们就会在终端看到我们的请求行的数据
通过输出的信息,我们发现 header 是一个 map 类型
[05:56:14 root@go web]#go run main.go
Upgrade-Insecure-Requests [1]
User-Agent [Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36 Edg/91.0.864.70]
Accept [text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9]
Accept-Encoding [gzip, deflate]
Accept-Language [zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6]
Connection [keep-alive]
Cache-Control [max-age=0]
Pragma [no-cache]
User-Agent [Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36 Edg/91.0.864.70]
Referer [http://10.0.0.10:8888/]
Accept-Encoding [gzip, deflate]
Connection [keep-alive]
Cache-Control [no-cache]
Accept [image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8]
Accept-Language [zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6]
5.2.1 获取单个请求头部数据
现在我们想获取单个请求头部信息,我们就可以通过 http.header
的方法进行获取
package main
import (
"fmt"
"net/http"
"time"
)
func main() {
addr := ":8888"
http.HandleFunc("/", func(rw http.ResponseWriter, r *http.Request) {
// 通过 GET 方法获取请求头的 User-Agent 信息
header := r.Header
// header.Get("User-Agent") 这里传入的 User-Agent 是 key 字段,然后输出对应的 value
// 获取其他头部信息也是通过 Get 方法
fmt.Println(header.Get("User-Agent"))
fmt.Fprint(rw, time.Now().Format("2006-01-02 15:04:05"))
})
http.ListenAndServe(addr, nil)
}
只要我们一访问该服务,我们就会在终端看到我们的请求行的数据
终端输出
[06:00:57 root@go web]#go run main.go
Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36 Edg/91.0.864.70
而且我们想要什么头部信息,他都能拿到
5.3 获取请求体中数据
我们都知道请求体如果发起 POST 方法的话可以提交数据。我们通过 http.request.body
可以获取请求体信息
http.request.body
是一个接口类型,实现了 reader 和 closer 方法,所以 body 也能读,就可以通过 io.Copy
方法将他输出到终端上
[06:25:55 root@go web]#go doc http.request.body
package http // import "net/http"
type Request struct {
// Body is the request's body.
//
// For client requests, a nil body means the request has no body, such as a GET
// request. The HTTP Client's Transport is responsible for calling the Close
// method.
//
// For server requests, the Request Body is always non-nil but will return EOF
// immediately when no body is present. The Server will close the request body.
// The ServeHTTP Handler does not need to.
//
// Body must allow Read to be called concurrently with Close. In particular,
// calling Close should unblock a Read waiting for input.
Body io.ReadCloser
// ... other fields elided ...
}
package main
import (
"fmt"
"io"
"net/http"
"os"
"time"
)
func main() {
addr := ":8888"
http.HandleFunc("/", func(rw http.ResponseWriter, r *http.Request) {
// 通过 io.Copy 将请求体数据输出
// os.Stdout 输出到终端上
// r.Body 获取请求体信息
io.Copy(os.Stdout, r.Body)
fmt.Fprint(rw, time.Now().Format("2006-01-02 15:04:05"))
})
http.ListenAndServe(addr, nil)
}
启动程序,之后我们通过 curl 浏览器对该程序提交一次 post 请求
[06:30:30 root@go ~]#curl -XPOST "127.0.0.1:8888/post" -d"a=curl&b=post请求"
2021-07-21 06:32:08
# -XPOST:请求方法是 post
# 127.0.0.1:8888/post:访问本机的 post 路径
# -d"a=curl&b=post请求":提交的请求数据
# 2021-07-21 06:32:08 服务器端 web 数据
curl 请求之后程序终端就会输出我们的请求数据
[06:30:54 root@go web]#go run main.go
a=curl&b=post请求
5.3.1 获取自定义提交参数
在请求的时候任意的东西都能作为参数给服务器端进行提交
package main
import (
"fmt"
"io"
"net/http"
"os"
"time"
)
func main() {
addr := ":8888"
http.HandleFunc("/", func(rw http.ResponseWriter, r *http.Request) {
// 获取请求的 Token 字段
header := r.Header
// 输出 token 字段的值
fmt.Println(header.Get("Token"))
fmt.Fprint(rw, time.Now().Format("2006-01-02 15:04:05"))
})
http.ListenAndServe(addr, nil)
}
启动程序,客户端 curl 访问
[06:32:08 root@go ~]#curl -XPOST "127.0.0.1:8888/post" -H "Token:zz"
2021-07-21 06:53:06
# -H "Token:zz" 请求头部信息 token 字段值为 zz
请求之后服务器响应
[06:52:20 root@go web]#go run main.go
zz
# 输出 token 字段的值
5.4 获取提交数据
http 请求里面任意一个文本,任意一块的内容都可以作为数据进行提交,但是我们常用的数据提交呢还有一些固定的使用方式,我们来看一下
提交方式:
- 第一种提交方式,在 URl 中传递数据,在访问的时候 url?加参数名字=他的值
url?argname1=argvalue1&argname2=argvalue2
- 第二种提交方式,通过 body 提交数据
- body 中数据格式,常用的有 3 种
application/x-www-form-urlencoded
application/json
multipart/form-data
- 自定义格式
5.4.1 第一种方式获取数据
package main
import (
"fmt"
"net/http"
"time"
)
func main() {
addr := ":8888"
http.HandleFunc("/", func(rw http.ResponseWriter, r *http.Request) {
/*
提交方式:
第一种提交方式
*/
fmt.Println(r.URL)
// 服务器端响应时间给客户端
fmt.Fprint(rw, time.Now().Format("2006-01-02 15:04:05"))
})
http.ListenAndServe(addr, nil)
}
启动程序浏览器访问,并且我们在访问的时候添加参数信息
http://10.0.0.10:8888/?z=1&g=2
此时服务器端会在终端输出提交信息
[07:09:37 root@go webrequest]#go run main.go
/?z=1&g=2
但是我们如何将这些获取到的数据提取出来呢?在 go 中的话已经有函数帮我们实现了该功能
[07:12:54 root@go webrequest]#go doc http.request.form
package http // import "net/http"
type Request struct {
// Form contains the parsed form data, including both the URL field's query
// parameters and the PATCH, POST, or PUT form data. This field is only
// available after ParseForm is called. The HTTP client ignores Form and uses
// Body instead.
Form url.Values # 类型为 url.Values
// ... other fields elided ...
}
# 查看 url.Values 类型
[07:13:04 root@go webrequest]#go doc url.values
package url // import "net/url"
type Values map[string][]string # key 为 string value 为 [] string
Values maps a string key to a list of values. It is typically used for query
parameters and form values. Unlike in the http.Header map, the keys in a
Values map are case-sensitive.
func ParseQuery(query string) (Values, error)
func (v Values) Add(key, value string)
func (v Values) Del(key string)
func (v Values) Encode() string
func (v Values) Get(key string) string
func (v Values) Set(key, value string)
package main
import (
"fmt"
"net/http"
"time"
)
func main() {
addr := ":8888"
http.HandleFunc("/", func(rw http.ResponseWriter, r *http.Request) {
/*
提交方式:
第一种提交方式
*/
// 解析 url 参数
r.ParseForm()
// 获取提交数据信息,接收的参数类型都是 string
fmt.Println(r.Form)
fmt.Fprint(rw, time.Now().Format("2006-01-02 15:04:05"))
})
http.ListenAndServe(addr, nil)
}
运行程序,浏览器访问时添加参数
http://10.0.0.10:8888/?z=1&g=2
此时启动程序终端就会输出一个 map 类型信息
[07:15:56 root@go webrequest]#go run main.go
map[g:[2] z:[1]]
# 也就是我们的 url 中添加的参数
5.4.1.1 获取提交数据的单个参数信息
http://10.0.0.10:8888/?z=1&g=2 我们在这个 url 中可以看到有 z 和 g 两个 key 的参数,那我们如何获取一个单独参数的数据呢,通过http.Request.Form.Get
方法获取
package main
import (
"fmt"
"net/http"
"time"
)
func main() {
addr := ":8888"
http.HandleFunc("/", func(rw http.ResponseWriter, r *http.Request) {
/*
提交方式:
第一种提交方式
*/
// 解析 url 参数
r.ParseForm()
// 获取提交数据信息,接收的参数类型都是 string
fmt.Println(r.Form)
// 获取单独参数的数据信息的第一个参数值
fmt.Println(r.Form.Get("z"))
fmt.Fprint(rw, time.Now().Format("2006-01-02 15:04:05"))
})
http.ListenAndServe(addr, nil)
}
浏览器访问
程序终端输出
[07:27:17 root@go webrequest]#go run main.go
map[g:[2] z:[1]]
1
# 把 z=1 的 value 进行输出
但是当我们在 url 中一个参数传递了多个值我们该如何获取呢?如这样的一个 url 请求 http://10.0.0.10:8888/?z=1&g=2&z=3,通过 http.Request.Form["参数key"]
package main
import (
"fmt"
"net/http"
"time"
)
func main() {
addr := ":8888"
http.HandleFunc("/", func(rw http.ResponseWriter, r *http.Request) {
/*
提交方式:
第一种提交方式
*/
// 解析 url 参数
r.ParseForm()
// 获取提交数据信息,接收的参数类型都是 string
fmt.Println(r.Form)
// 获取参数的多个数据值
fmt.Println(r.Form["z"])
fmt.Fprint(rw, time.Now().Format("2006-01-02 15:04:05"))
})
http.ListenAndServe(addr, nil)
}
启动程序浏览器访问
程序终端输出 z 的多个参数
[07:34:47 root@go webrequest]#go run main.go
map[g:[2] z:[1 3]]
[1 3]
5.4.2 对 body application/x-www-form-urlencoded 类型数据获取
代码这块其实没有过多变化,还是使用的和第一种方式一样的代码,只不过我们的请求方式变为了 POST
package main
import (
"fmt"
"net/http"
"time"
)
func main() {
addr := ":8888"
http.HandleFunc("/", func(rw http.ResponseWriter, r *http.Request) {
// 获取请求方法
fmt.Println("method:", r.Method)
// 解析 url 参数
r.ParseForm()
// 获取提交数据信息,接收的参数类型都是 string
fmt.Println(r.Form)
fmt.Fprint(rw, time.Now().Format("2006-01-02 15:04:05"))
})
http.ListenAndServe(addr, nil)
}
通过 curl 浏览器发起一个 POST 请求
[06:53:06 root@go ~]#curl -XPOST "127.0.0.1:8888/post" -d"a=curl&b=post请求"
程序输出
[07:47:47 root@go webrequest]#go run main.go
method: POST # 请求方式 post
map[a:[curl] b:[post请求]] # 获取 body 数据
所以我们可以通过 http.Request.Form
不仅可以获取 RUL 参数也能获取 body 中的参数
5.4.3 当 url 和 body 传入数据重复,如何只获取 body 数据
因为 http.Request.Form
会同时获取 url 和 body 中的数据,但是我们就想获取 body 数据该如何获取呢?
通过http.Request.PostForm
,而且和 Form 一样在使用之前都需要解析
package main
import (
"fmt"
"net/http"
"time"
)
func main() {
addr := ":8888"
http.HandleFunc("/", func(rw http.ResponseWriter, r *http.Request) {
// 获取请求方法
fmt.Println("method:", r.Method)
// 解析 url 参数
r.ParseForm()
// 获取
fmt.Println("form 获取", r.Form)
fmt.Println("post form 获取", r.PostForm)
fmt.Fprint(rw, time.Now().Format("2006-01-02 15:04:05"))
})
http.ListenAndServe(addr, nil)
}
启动程序,浏览器访问
[08:01:47 root@go ~]#curl -XPOST "127.0.0.1:8888/post?a=111" -d"a=curl&b=post请求"
2021-07-21 08:02:24
程序输出结果,就可以通过 post form
这种方式来指定获取 body 中参数
[08:02:19 root@go webrequest]#go run main.go
method: POST
form 获取 map[a:[curl 111] b:[post请求]]
post form 获取 map[a:[curl] b:[post请求]]
5.4.4 对 body application/json 类型进行解析
对自定义类型需要获取到 body 原始数据,使用特定解码器就 OK 了,如果我们提交的是 json 格式,使用 json 的解码器就成功了,我们在通过对自定义类型进行解析的时候不适用 http.request.parseForm
进行解析,而是通过我们自己定义的解析器进行解析
package main
import (
"fmt"
"io"
"net/http"
"os"
"time"
)
func main() {
addr := ":8888"
http.HandleFunc("/", func(rw http.ResponseWriter, r *http.Request) {
// 获取请求方法
fmt.Println("method:", r.Method)
// 自定义解析器
// 对 body 请求体进行解析
io.Copy(os.Stdout, r.Body)
fmt.Fprint(rw, time.Now().Format("2006-01-02 15:04:05"))
})
http.ListenAndServe(addr, nil)
}
启动服务,并通过 curl 浏览器进行访问,传递 json 格式
[08:37:42 root@go ~]#curl -XPOST "127.0.0.1:8888/post?a=111" -d"{\"x\":1}"
2021-07-21 08:37:46
# -d"{\"x\":1}" 传递 json 格式的 body
服务器拿到 json 格式
[08:36:20 root@go webrequest]#go run main.go
method: POST
{"x":1}
这个时候服务器端已经有了 body 信息了,就可以通过 json.Unmarshal()
进行解析并且拿到对应的数据
package main
import (
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"time"
)
func main() {
addr := ":8888"
http.HandleFunc("/", func(rw http.ResponseWriter, r *http.Request) {
// 获取请求方法
fmt.Println("method:", r.Method)
// 自定义解析器,并且将读取到的数据是一个 []byte 赋值给 ctx
ctx, err := ioutil.ReadAll(r.Body)
if err != nil {
fmt.Println(err)
return
}
// 定义 map 来接收解析后的数据
v := make(map[string]interface{})
// 解析,传递 v 的指针进行赋值
json.Unmarshal(ctx, &v)
// 此时的 v 就是我们传递过来的 json 请求数据
fmt.Println(v)
fmt.Fprint(rw, time.Now().Format("2006-01-02 15:04:05"))
})
http.ListenAndServe(addr, nil)
}
启动服务,并通过 curl 浏览器进行访问,传递 json 格式
[08:37:42 root@go ~]#curl -XPOST "127.0.0.1:8888/post?a=111" -d"{\"x\":1}"
2021-07-21 08:37:46
# -d"{\"x\":1}" 传递 json 格式的 body
服务器拿到 json 格式
[08:50:10 root@go webrequest]#go run main.go
method: POST
map[x:1] # 拿到请求的 json 数据
如果是 xml 类型的提交数据我们把解析类型改为 xml 进行解析,如果是自定义类型我们就通过自定义类型解析
5.4.5 实现多种提交类型数据解析
现在当我们请求一个 url 的时候想实现对以下的请求数据类型进行解析该怎么做?
application/x-www-form-urlencoded
application/json
application/xml
我们可以通过提交的数据编码格式来进行获取,也就是说在 Request 中的 Header: Content-Type 进行获取,Content-Type 就是指的我们提交数据的格式。
所以当我们一个函数要支持多种提交方式,可以通过 header 中获取 Content-Type , Content-Type 如果是 json 格式就通过 json 解析,如果是 x-www-form-urlencoded
就通过 ParseForm
解析。如果其他自定义类型就通过其他自定义类型来进行解析
package main
import (
"fmt"
"net/http"
"time"
)
func main() {
addr := ":8888"
http.HandleFunc("/", func(rw http.ResponseWriter, r *http.Request) {
// 获取请求方法
fmt.Println("method:", r.Method)
// 获取提交数据格式
fmt.Println(r.Header)
fmt.Fprint(rw, time.Now().Format("2006-01-02 15:04:05"))
})
http.ListenAndServe(addr, nil)
}
通过 curl 工具提交数据
[05:55:33 root@go ~]#curl -XPOST "127.0.0.1:8888/post?a=111" -d"{\"x\":1}" -H "Content-Type: application/json"
2021-07-23 05:57:16
# -H "Content-Type: application/json" 提交 json 格式的数据类型
# 服务器端获取到数据 Content-Type:[application/json] 为 json
[05:57:49 root@go webrequest]#go run main.go
method: POST
map[Accept:[*/*] Content-Length:[7] Content-Type:[application/json] User-Agent:[curl/7.29.0]]
5.4.6 对 multipart/form-data 类型进行获取
这种类型就是通过浏览器上传文件的数据类型,他的提交方式也是通过 body 的方法进行提交,只不过提交类型为 multipart/form-data
package main
import (
"fmt"
"net/http"
)
func main() {
addr := ":8888"
http.HandleFunc("/file/", func(rw http.ResponseWriter, r *http.Request) {
// ParseMultipartForm() 解析,因为文件在上传的时候有数据大小,所以在解析的时候需要定义解析的数据大小
// 因为他是通过批量接收的,所以每次内存中接收多少数据都是需要限制
// 接收文件过程中最大使用的内存
r.ParseMultipartForm(1024 * 1024)
// 解析完了之后拿到文件
fmt.Println(r.MultipartForm.File)
// 解析完了之后拿到数据
fmt.Println(r.MultipartForm.Value)
})
http.ListenAndServe(addr, nil)
}
执行
# 客户端访问 file url
[07:01:24 root@go ~]#curl -XPOST -F "x=@test.log" "127.0.0.1:8888/file/"
# -F "x=@test.log" 上传 test.log 文件
# 服务器端解析
[06:19:59 root@go webrequest]#go run main.go
map[]
map[a:[1] x:[test.log] z:[2]]
5.4.7 上传并读取文件
现在我们上传文件如何拿到文件内容,或者说将文件拿到另外的地方去
# 通过 go doc 获取 http.request.MultipartForm 的详细信息
[06:28:16 root@go webrequest]#go doc http.request.MultipartForm
package http // import "net/http"
type Request struct {
// MultipartForm is the parsed multipart form, including file uploads. This
// field is only available after ParseMultipartForm is called. The HTTP client
// ignores MultipartForm and uses Body instead.
MultipartForm *multipart.Form # 返回值为 *multipart.Form
// ... other fields elided ...
}
# 继续查看 *multipart.Form
[06:28:51 root@go webrequest]#go doc multipart.Form
package multipart // import "mime/multipart"
type Form struct {
Value map[string][]string
File map[string][]*FileHeader # File 的 value 字段是一个 *FileHeader 类型
}
Form is a parsed multipart form. Its File parts are stored either in memory
or on disk, and are accessible via the *FileHeader's Open method. Its Value
parts are stored as strings. Both are keyed by field name.
func (f *Form) RemoveAll() error
# 继续查看 *FileHeader
[06:32:39 root@go webrequest]#go doc multipart.fileheader
package multipart // import "mime/multipart"
type FileHeader struct {
Filename string
Header textproto.MIMEHeader
Size int64
// Has unexported fields.
}
A FileHeader describes a file part of a multipart request.
func (fh *FileHeader) Open() (File, error) # 该 Open() 方法返回了 File 类型
# 查看 file 接口,该方法里面有多个读取的方法,所以我们可以 open 以后再读取
[06:33:21 root@go webrequest]#go doc multipart.file
package multipart // import "mime/multipart"
type File interface {
io.Reader
io.ReaderAt
io.Seeker
io.Closer
}
File is an interface to access the file part of a multipart message. Its
contents may be either stored in memory or on disk. If stored on disk, the
File's underlying concrete type will be an *os.File.
代码
package main
import (
"fmt"
"io"
"net/http"
"os"
)
func main() {
addr := ":8888"
http.HandleFunc("/file/", func(rw http.ResponseWriter, r *http.Request) {
// 接收文件过程中最大使用的内存
r.ParseMultipartForm(1024 * 1024)
// 解析完了之后拿到文件
fmt.Println(r.MultipartForm.File)
// 将 MultipartForm.File 的 value = *multipart.FileHeader 结构体赋值给 fileHeaders 变量
if fileHeaders, ok := r.MultipartForm.File["x"]; ok {
// 遍历 *multipart.FileHeader 结构体中的属性
for _, fileHeader := range fileHeaders {
fmt.Println(fileHeader.Filename, fileHeader.Size)
// 打开上传文件
file, err := fileHeader.Open()
if err != nil {
fmt.Println(err)
return
}
// 通过 io.Copy 将 file 中的数据读取到终端输出
io.Copy(os.Stdout, file)
file.Close()
}
}
})
http.ListenAndServe(addr, nil)
}
启动程序并通过 curl 工具上传文件
# 上传 test.log 文件
[07:01:24 root@go ~]#curl -XPOST -F "x=@test.log" "127.0.0.1:8888/file/"
# 服务器读取到文件
[07:01:22 root@go webrequest]#go run main.go
map[]
map[x:[0xc000100190]]
test.log 832 # 该文件 832 字节
date
Fri Jul 2 11:10:37 CST 2021
Fri Jul 2 11:10:38 CST 2021
Fri Jul 2 11:10:38 CST 2021
Fri Jul 2 11:10:39 CST 2021
Fri Jul 2 11:10:39 CST 2021
Fri Jul 2 11:10:39 CST 2021
Fri Jul 2 11:15:42 CST 2021
.... 省略 ....
我们现在能将上传的文件内容读取出来,那我们可以将他的内容保存下来吗?
5.4.8 上传并保存文件
我们现在将上传的文件保存到服务器本地,我们先读取文件内容,然后再写入即可
package main
import (
"fmt"
"io"
"net/http"
"os"
)
func main() {
addr := ":8888"
http.HandleFunc("/file/", func(rw http.ResponseWriter, r *http.Request) {
// 接收文件过程中最大使用的内存
r.ParseMultipartForm(1024 * 1024)
// 解析完了之后拿到文件
fmt.Println(r.MultipartForm.File)
// 将 MultipartForm.File 的 value = *multipart.FileHeader 结构体赋值给 fileHeaders 变量,x 为拿到 post 请求的 x 操作
if fileHeaders, ok := r.MultipartForm.File["x"]; ok {
// 遍历 *multipart.FileHeader 结构体中的属性
for _, fileHeader := range fileHeaders {
// 创建一个新的文件,文件名就是我们上传的 fileHeader.Filename
// 默认是在 server 程序当前目录下创建,这里我将新文件上传值 /root/test/
NewFile, err := os.Create("/root/test/" + file.Filename)
// 打开上传文件
file, err := fileHeader.Open()
if err != nil {
fmt.Println(err)
return
}
// 将 file 读取的内容 copy 到 newfile 中
io.Copy(newfile, file)
file.Close()
newfile.Close()
}
}
})
http.ListenAndServe(addr, nil)
}
启动程序,并且通过 curl 进行上传
[07:12:57 root@go ~]#curl -XPOST -F "x=@test.log" "127.0.0.1:8888/file/"
# "x=@test.log" 上传 test.log 文件
当前目录下 test.log 文件已经上传
[18:49:32 root@go ~]#ll /root/test/
total 14384
-rw-r--r-- 1 root root 14725773 Sep 8 18:48 test.log # 已上传
6 提交数据常用方式
提交数据常用规则
-
一个名字对应一个值
-
提交数据方式
-
浏览器
-
GET(常用)
- 参数常在 URL
- 某些时候参数可放在 Body (取决于 client 工具/服务器端是否支持)
-
POST(常用)
- 参数常在 body
- 编码方式:
x-www-form-urlencodel
multipart/form-data
application/json
-
第三方工具:curl ,client
-
DELETE
- 参数常在 URL
- 某些时候参数可放在 Body(取决于 client 工具/服务器端是否支持)
-
PUT
- 参数常在 body
-
HEAD
- 参数常在 URL
- 某些时候参数可放在 Body(取决于 client 工具/服务器端是否支持)
-
6.1 对 URL 进行解析
package main
import (
"fmt"
"net/http"
)
func main() {
addr := ":8888"
http.HandleFunc("/", func(rw http.ResponseWriter, r *http.Request) {
// FormValue 自己会调用 path
fmt.Println(r.FormValue("x"))
})
http.ListenAndServe(addr, nil)
}
启动,浏览器访问,并且传入 x = 1
同时 服务器解析到 x = 1,但是我们要传入多个参数他就不管用了,因为他只解析第一个
![image-20210723102029409](web 开发.assets\image-20210723102029409.png)
6.2 对 post 进行解析
如果是 post 请求的话我们也可以通过postform
进行解析,而且 post 只会回去 body 中的数据
package main
import (
"fmt"
"net/http"
)
func main() {
addr := ":8888"
http.HandleFunc("/", func(rw http.ResponseWriter, r *http.Request) {
// postform 解析
fmt.Println(r.PostFormValue("y"))
})
http.ListenAndServe(addr, nil)
}
执行,并且通过 curl 进行提交数据
[10:18:57 root@go ~]#curl -XPOST "127.0.0.1:8888/post?a=111" -d "y=2"
# -d "y=2" 传入 body 参数 y = 2
服务器解析到 body 中 y 的数据,当然也可以使用 FormValue
原则都是一样的,FormValue
可以用在 URL
和 body
中
6.3 对文件进行解析
上传文件的时候我们可以使用 FormFile
,传入的是文件名字,返回值有三个
func (*http.Request).FormFile(key string) (multipart.File, *multipart.FileHeader, error)
package main
import (
"fmt"
"io"
"net/http"
"os"
)
func main() {
addr := ":8888"
http.HandleFunc("/", func(rw http.ResponseWriter, r *http.Request) {
// FormFile 函数是直接打开 file 的操作
// 如果文件打开没有出错,我们就可以直接 io.Copy 了
if file, fileHeader, err := r.FormFile("z"); err == nil {
fmt.Println(fileHeader.Filename, fileHeader.Size)
io.Copy(os.Stdout, file)
}
})
http.ListenAndServe(addr, nil)
}
执行,并且在客户端提交 post 请求
[10:57:16 root@go ~]#curl -XOPST -F "z=@init.sh" "127.0.0.1:8888/"
服务器输出把 init.sh 文件内容获取并且输出
[11:01:56 root@go webnormal]#go run main.go
init.sh 2655
#!/bin/bash
# 修改网卡名
PI(){
grep lv=centos /etc/default/grub
if [ $? = 0 ];then
.... 省略....
当然也可以通过这种方式进行文件的上传至 web 端
package main
import (
"fmt"
"io"
"net/http"
"os"
)
func main() {
addr := ":8888"
http.HandleFunc("/", func(rw http.ResponseWriter, r *http.Request) {
// r.FormFile("z") 解析到 post 请求中的 z 键
file, fileHandle, err := r.FormFile("z")
if err != nil {
fmt.Println(err)
return
}
// 这里的 NewFile 通过 fileHandle.Filename 获取到主机上传文件名
NewFile, err := os.Create(fileHandle.Filename)
// 通过 io.Copy 讲 file 的数据量读取并写入到 NewFile 中
io.Copy(NewFile, file)
})
http.ListenAndServe(addr, nil)
}
7 http-client开发
所谓的 http-client 开发就是需要对服务方发起请求
http.Client
,如何在我们的 client 上提交数据。
提交数据的几种方式
- 通过 RUL 提交参数
- 通过 HTTP 协议进行提交
- GET/POST
- HEADER
- BODY
7.1 客户端 Get 请求方式
1.编写一个 server 端
package main
import (
"fmt"
"io"
"net/http"
"os"
"time"
)
func main() {
addr := ":8888"
http.HandleFunc("/", func(rw http.ResponseWriter, r *http.Request) {
fmt.Println("method:", r.Method)
fmt.Println("url:", r.URL)
fmt.Println("protocol:", r.Proto)
fmt.Println("Body:")
io.Copy(os.Stdout, r.Body)
fmt.Fprintln(rw, time.Now().Format("2006-01-02 15-04-05"))
})
http.ListenAndServe(addr, nil)
}
执行
[11:33:14 root@go webrequest]#go run main.go
2.编写客户端
package main
import (
"fmt"
"io"
"net/http"
"os"
)
func main() {
// 请求本机的 http://127.0.0.1:8888/
res, err := http.Get("http://127.0.0.1:8888/")
if err != nil {
fmt.Println(err)
return
}
// 将请求到的服务器端 body 信息输出
io.Copy(os.Stdout, res.Body)
}
执行客户端
[11:38:35 root@go webclient]#go run main.go
2021-07-23 11-38-41
此时服务器端输出 客户端的请求信息
[11:33:14 root@go webrequest]#go run main.go
method: GET # 请求方法
url: / # 请求 url
protocol: HTTP/1.1 # 请求 http 协议
Body: {} # 客户端 body 信息为空
但是这个时候如果我客户端需要提交参数我该怎么提交,我们都知道 GET 方法的提交参数信息是在 URL 的位置进行提交,所以我们只需要在客户端的请求 URL 中提交即可
客户端提交参数如下图
然后再次执行客户端,此时服务器端就会获取到客户端的请求信息
7.2 客户端 Head 请求方式
Head(url string) (resp *http.Response, err error)
客户端代码
package main
import (
"io"
"log"
"net/http"
"os"
"os/exec"
)
func main() {
// 通过 http.Head 传入要访问的 url 请求
resq, err := http.Head("http://127.0.0.1:8888/?x=1&y=2")
if err != nil {
log.Println(err)
return
}
// 终端输出
io.Copy(os.Stdout, resq.Body)
}
服务器端还是使用上面的代码
然后执行结果,服务器端输出请求方式为 head
7.3 客户端 POST 请求方式
在 post 请求的时候有两种方式:
- 比较常见的
x-www-form-urlencode
- 自定义类型如 json 格式
7.3.1 PostForm 请求方式
PostForm(url string, data url.Values) (resp *http.Response, err error)
客户端代码:
package main
import (
"fmt"
"io"
"net/http"
"net/url"
"os"
)
func main() {
// 赋值 values 为 url.Values 结构体
values := url.Values{}
// 添加请求数据
values.Add("x", "1")
values.Add("x", "2")
// 更新请求数据
values.Set("y", "1")
values.Set("y", "2")
// 请求 URL http://127.0.0.1:8888/ ,请求数据 values
resq, err := http.PostForm("http://127.0.0.1:8888/", values)
if err != nil {
fmt.Println(err)
return
}
io.Copy(os.Stdout, resq.Body)
}
服务器端代码依旧不变,执行结果,body 拿到数据
7.3.2 JSON 格式请求
自定义类型 json 格式请求,需要使用到 Post()
func http.Post(url string, contentType string, body io.Reader) (resp *http.Response, err error)
编写 test.json
文件
{
"a":1,
"b":2
}
客户端代码
package main
import (
"fmt"
"io"
"net/http"
"os"
)
func main() {
// 打开 test.json 文件
f, _ := os.Open("test.json")
// 将 json 文件通过 http.Post 传递给 http://127.0.0.1:8888/ contentType(内容类型为 json)
resq, err := http.Post("http://127.0.0.1:8888/", "application/json", f)
if err != nil {
fmt.Println(err)
return
}
io.Copy(os.Stdout, resq.Body)
}
服务器端代码不变,最终执行并解析
但是我们现在有一个问题就是,假如我们想提交一个 json 文件还需要创建一个文件并编写内容,这样是不是特别麻烦,那么我们有没有比较简单的方法来实现,比如实现了一个 read 接口,然后又在内存里面呢?其实这个问题更是一个能不能在内存中对字符串进行操作的问题,我们都知道在 strings,bytes
两个包中有大量对字符串的操作,由此引出下面操作
package main
import (
"fmt"
"io"
"net/http"
"os"
"strings"
)
func main() {
// 通过 strings.NewReader 的同时直接进行写入我们的 json 对象
// 把 reader 变量变成了类似一个的流对象
reader := strings.NewReader(`
{"a":"x"}
`)
// 将 json 文件通过 http.Post 传递给 http://127.0.0.1:8888/ contentType(内容类型为 json),传入 reader 变量
resq, err := http.Post("http://127.0.0.1:8888/", "application/json", reader)
if err != nil {
fmt.Println(err)
return
}
io.Copy(os.Stdout, resq.Body)
}
执行
7.4 客户端文件上传
我们需要把我文件读取到内存中
package main
import (
"bytes"
"fmt"
"io"
"mime/multipart"
"net/http"
"os"
)
func main() {
// 初始化一个新的 buffer 缓冲区
buffer := bytes.NewBuffer(nil)
// 创建一个 multipart.NewWriter 对象,因为再 io.Copy 中第一个参数接收的是 writer 对象
writer := multipart.NewWriter(buffer)
// 然后上传到服务器上的新文件名 xx.json
filewriter, err := writer.CreateFormFile("z", "xx.json")
if err != nil {
fmt.Println(err)
return
}
// 打开文件 test.json
f, _ := os.Open("test.json")
// 然后把 test.json 文件中的内容读取并写入到 filewriter 变量中,filewriter 是等会再服务器上传后的文件
io.Copy(filewriter, f)
// 关闭写入
writer.Close()
resq, err := http.Post("http://127.0.0.1:8888/", "multipart/form-data", buffer)
if err != nil {
fmt.Println(err)
return
}
// 在终端输出请求到的 服务器端 body
io.Copy(os.Stdout, resq.Body)
}
服务器端代码不变
通过执行从 body 信息能够观察到文件已经上传
当然我们现在只是能够拿到数据,如果我想报错到服务器端的话需要编写服务器端代码
package main
import (
"fmt"
"io"
"net/http"
"os"
"time"
)
func main() {
addr := ":8888"
http.HandleFunc("/", func(rw http.ResponseWriter, r *http.Request) {
// 这里通过输出时间信息来新建一个文件
NewFile, err := os.Create(time.Now().Format("2006-01-02 15-04-05"))
if err != nil {
fmt.Println(err)
return
}
// 将我们读取到的 body 信息传递给 NewFile
io.Copy(NewFile, r.Body)
fmt.Fprintln(rw, time.Now().Format("2006-01-02 15-04-05"))
})
http.ListenAndServe(addr, nil)
}
我们看到有一个以时间上传的文件,就说明客户端的文件已经上传至服务器端