1. 简单的http协议
1.1. server端
在项目实践中,如果是存在多个API接口,并且根据不同的HTTP方法进行功能区分时,通常我们使用 Gin 框架进行项目编写,其编程复杂度会低很多。但是如果服务只有个别的接口,如 GET /healthz
接口,或者 GET /metrics
接口,可以使用 http
包编写服务端,优势在于非常轻量级。
http server端工作原理如下图所示,使用 http.HandleFunc()
注册路由及处理器到 serverMux.es
列表(该列表会将以 /
结尾的pattern按照长度倒序排列)。 http.ListenAndServe()
开启TCP端口监听,并使用for循环从监听端口取出请求,内部处理(生成request和response两个结构体)后,交给handler处理器,由处理器进行匹配,并处理 request 和 response。 尤其需要注意的是,handler处理器注册时,要使用 /
结尾,否则不会进入 serverMux.es 列表中。
1.1.1. 简单的server
func main() {
http.HandleFunc("/bbs/", bbs)
http.HandleFunc("/", index)
http.HandleFunc("/bbs/user/", user)
err := http.ListenAndServe("127.0.0.1:9001", nil) // 该函数会阻塞(for循环)
if err != nil {
logger.Fatalf("listen failed, err:%s", err.Error())
return
}
}
func index(w http.ResponseWriter, r *http.Request) {
process(w,r,"index")
}
func bbs(w http.ResponseWriter, r *http.Request) {
process(w,r,"bbs")
}
func user(w http.ResponseWriter, r *http.Request) {
process(w,r,"user")
}
// 处理请求,返回请求的信息,以及被哪个handler函数处理了
func process(w http.ResponseWriter, r *http.Request, handleName string) {
method:= r.Method
host := r.Host
url := r.URL.String()
content := fmt.Sprintf("handle name:%s; host:%s;url:%s;method:%s\n",handleName, host, url, method)
logger.Infof("recv request,%s", content[:len(content)-1])
_, err := io.WriteString(w, content)
if err != nil {
logger.Errorf("send response failed, err:%s", err.Error())
return
}
}
# 按照最长的 pattern 进行匹配的,在发起请求时,如果仅仅匹配到pattern,则需要以/结尾
[root@duduniao go_learn]# curl 127.0.0.1:9001/
handle name:index; host:127.0.0.1:9001;url:/;method:GET
[root@duduniao go_learn]# curl 127.0.0.1:9001/bbs/
handle name:bbs; host:127.0.0.1:9001;url:/bbs/;method:GET
[root@duduniao go_learn]# curl 127.0.0.1:9001/bbs/user/
handle name:user; host:127.0.0.1:9001;url:/bbs/user/;method:GET
[root@duduniao go_learn]# curl -X DELETE 127.0.0.1:9001/bbs/user/index.html
handle name:user; host:127.0.0.1:9001;url:/bbs/user/index.html;method:DELETE
1.1.2. 设置响应头部
在响应客户端请求过程中,一般除了写入响应体之外,还可能需要设置响应头,响应头是 map[string]string
,常用方法问 Set()
和 Get()
func main() {
http.HandleFunc("/user/", user)
err := http.ListenAndServe("127.0.0.1:9001", nil) // 该函数会阻塞(for循环)
if err != nil {
logger.Fatalf("listen failed, err:%s", err.Error())
return
}
}
func user(w http.ResponseWriter, r *http.Request) {
// 获取请求基本信息
res := make(map[string]interface{}, 4)
res["method"] = r.Method
res["host"] = r.Host
res["url"] = r.URL.String()
res["content"] = fmt.Sprintf("welcome to /user/ location")
marshal, _ := json.Marshal(res) // 响应体
// 设置响应头信息
w.Header().Add("Content-Type", "application/json; charset=utf-8")
_, err := w.Write(marshal)
if err != nil {
logger.Errorf("send response failed, err:%s", err.Error())
return
}
}
[root@duduniao go_learn]# curl -v -X DELETE 127.0.0.1:9001/user/
* Trying 127.0.0.1:9001...
* TCP_NODELAY set
* Connected to 127.0.0.1 (127.0.0.1) port 9001 (#0)
> DELETE /user/ HTTP/1.1
> Host: 127.0.0.1:9001
> User-Agent: curl/7.68.0
> Accept: */*
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< Content-Type: application/json; charset=utf-8
< Date: Sat, 09 Jan 2021 01:22:37 GMT
< Content-Length: 97
<
* Connection #0 to host 127.0.0.1 left intact
{"content":"welcome to /user/ location","host":"127.0.0.1:9001","method":"DELETE","url":"/user/"}
1.1.3. 获取请求信息
一般获取请求的信息分为三种:
- 获取请求头参数
- 获取请求中URL参数
- 获取请求体
- json格式的请求体
- 表单格式的请求体 ```go func main() { http.HandleFunc(“/user/“, user) http.HandleFunc(“/comment/“, comment) http.HandleFunc(“/deploy/“, deploy) err := http.ListenAndServe(“127.0.0.1:9001”, nil) // 该函数会阻塞(for循环) if err != nil { logger.Fatalf(“listen failed, err:%s”, err.Error()) return } }
// 读取header和URL数据 func user(w http.ResponseWriter, r *http.Request) { // 获取请求基本信息, 常见获取认证信息,比如jwt认证的信息 token := r.Header.Get(“Authorization”) // 获取认证信息, 校验操作此处省略 if token == “” { logger.Errorf(“parse Authorization header failed,err:%s”, “Authorization is null”) w.WriteHeader(401) , = io.WriteString(w, “invalid request, header does not has Authorization”) return } // 获取请求URL参数 query := r.URL.Query() // 获取请求参数的url.Values{“id”:[]string{“1”}, “name”:[]string{“zhangsan”}} fmt.Printf(“%#v\n”, query) id := query.Get(“id”) // 如果对于的value长度为零,取切片的第一个元素 name := query.Get(“name”) res, := json.Marshal(map[string]string{“id”: id, “name”: name}) // 设置响应头信息 w.Header().Add(“Content-Type”, “application/json; charset=utf-8”) , err := w.Write(res) if err != nil { logger.Errorf(“send response failed, err:%s”, err.Error()) return } }
// 读取表单的数据 func comment(w http.ResponseWriter, r *http.Request) { token := r.Header.Get(“Authorization”) // 获取认证信息, 校验操作此处省略 if token == “” { logger.Errorf(“parse Authorization header failed,err:%s”, “Authorization is null”) w.WriteHeader(401) , = io.WriteString(w, “invalid request, header does not has Authorization”) return } // r.ParseForm() 会做两个操作: // 1. POST PUT PATCH 请求,则将表单参数和URL参数解析到 r.Form和r.PostForm // 2. 其它类型的请求,会跳过表单数据解析,仅解析URL参数到 r.Form // 请求的 Content-Type: application/x-www-form-urlencoded if err := r.ParseForm(); err != nil { logger.Errorf(“parse form failed,err:%s”, err.Error()) w.WriteHeader(401) , = io.WriteString(w, “invalid request, parse form failed”) return } fmt.Printf(“form: %#v\n”, r.Form) fmt.Printf(“formdata: %#v\n”, r.PostForm) , = io.WriteString(w, “ok\n”) }
// 读取请求体中的json数据 func deploy(w http.ResponseWriter, r *http.Request) { token := r.Header.Get(“Authorization”) // 获取认证信息, 校验操作此处省略 if token == “” { logger.Errorf(“parse Authorization header failed,err:%s”, “Authorization is null”) w.WriteHeader(401) , = io.WriteString(w, “invalid request, header does not has Authorization”) return } // 如果不是 POST 和 PUT 请求则返回 if r.Method != http.MethodPost && r.Method != http.MethodPut { logger.Errorf(“request method invalid current:%s, the handler need %s,%s”, r.Method, http.MethodPut, http.MethodPost) w.WriteHeader(401) , = io.WriteString(w, “invalid request, request method is not supported”) return } content, err := ioutil.ReadAll(r.Body) // request中的body不需要手动关闭,程序会自动关闭 // 读取body失败则返回 if err != nil { logger.Errorf(“read body failed, err:%s”, err.Error()) w.WriteHeader(401) , = io.WriteString(w, “invalid request, request body error”) return } var requestBody map[string]interface{} err = json.Unmarshal(content, &requestBody) if err != nil { logger.Errorf(“unmarshal body failed, err:%s”, err.Error()) w.WriteHeader(401) , = io.WriteString(w, “invalid request, request body error”) return } fmt.Printf(“%#v\n”, requestBody) , = w.Write(content) }
```
[root@duduniao ~]# curl http://127.0.0.1:9001/user/ # 必须要添加 Authorization 的header
invalid request, header does not has Authorization
[root@duduniao ~]# curl -H "Authorization: 123456" "http://127.0.0.1:9001/user/?name=zhangsan&id=001" # 能正常解析并返回URL参数
{"id":"001","name":"zhangsan"}
[root@duduniao ~]# curl -H "Authorization: 123456" -d "year=2020&day=09" "http://127.0.0.1:9001/comment/?name=zhangsan&id=001"
ok
[root@duduniao ~]# curl -H "Authorization: 123456" -d '{"id":"001","name":"zhangsan"}' "http://127.0.0.1:9001/deploy/" # 解析json
{"id":"001","name":"zhangsan"}
1.2. client端
http client 通常使用Http包进行编写,其中比较重要的是在处理完毕请求后,需要关闭 resopnse.Body,否则会导致内存泄露!server 端采用上 1.1.3 中的代码!
1.2.1. 简单的get请求
func main() {
simpleGet("user/")
}
func simpleGet(uri string) {
resp, err := http.Get(fmt.Sprintf("http://127.0.0.1:9001/%s", uri))
if err != nil {
logger.Errorf("send request failed, url:http://127.0.0.1:9001/%s, error:%s", uri, err.Error())
return
}
defer func() { _ = resp.Body.Close() }()
respBody, err := ioutil.ReadAll(resp.Body)
if err != nil {
logger.Errorf("read response body failed, url:http://127.0.0.1:9001/%s, error:%s", uri, err.Error())
return
}
fmt.Println(string(respBody))
}
[root@duduniao http]# go run client.go
invalid request, header does not has Authorization
1.2.2. 复杂的Post请求
其它类型的请求,都是类似的操作,区别在于 Method 不同!
func main() {
complexPost("deploy/")
}
// 如果不涉及头部信息的添加,可以直接使用 http.Post() 函数简化流程, 和 simpleGet() 类似
func complexPost(uri string) {
// 创建http客户端
client := http.Client{
Timeout: time.Second * 3,
}
data := `{"id":"001","name":"zhangsan"}`
// 定义http请求
req, err := http.NewRequest(http.MethodPost, fmt.Sprintf("http://127.0.0.1:9001/%s", uri), bytes.NewBufferString(data))
if err != nil {
logger.Errorf("new request failed, url:http://127.0.0.1:9001/%s, error:%s", uri, err.Error())
return
}
// 添加http头部
req.Header.Add("Authorization", "123456")
req.Header.Add("Content-Type", "application/json")
response, err := client.Do(req)
if err != nil {
logger.Errorf("send request failed, url:http://127.0.0.1:9001/%s, error:%s", uri, err.Error())
return
}
defer func() { _ = response.Body.Close() }()
// 读取响应,如果响应是Json,正常进行json解析即可
content, err := ioutil.ReadAll(response.Body)
if err != nil {
logger.Errorf("read response body failed, url:http://127.0.0.1:9001/%s, error:%s", uri, err.Error())
return
}
logger.Infof("response code:%d, body:%s", response.StatusCode, string(content))
}
[root@duduniao http]# go run client.go
2021-01-09 13:09:37.521|response code:200, body:{"id":"001","name":"zhangsan"}
2. websocket协议
普通的http协议中,必须是client向server发起一次 request ,然后server回应一个response。取法实现server主动向client推送实时数据,而websocket主要就是解决这个问题的。其将http协议升级到websocket协议,并维持长连接,此时 server 和 client 可以互发数据。
2.1. websocket发送[]byte
// server.go
var msg = make(chan string, 50)
func main() {
http.HandleFunc("/ws/", pushMessage)
go createMsg()
err := http.ListenAndServe("127.0.0.1:9001", nil)
if err != nil {
logger.Errorf("listen 127.0.0.1:9001 failed , err:%s",err.Error())
return
}
}
func createMsg() { // 通过管道不断发送消息
for {
now := time.Now().Format("2006-01-02 15:04:05")
str := random.String(8, random.Alphabetic)
msg <- fmt.Sprintf("time:%s,str:%s", now, str)
time.Sleep(time.Second)
}
}
func pushMessage(w http.ResponseWriter, r *http.Request) {
upgrade := websocket.Upgrader{} // 定义一个 upgrade
conn, err := upgrade.Upgrade(w, r, nil) // 升级http协议到websocket
if err != nil {
logger.Errorf("upgrade to websocket failed, err:%s", err.Error())
return
}
defer func() { _ = conn.Close() }()
for {
select {
case msgStr := <-msg:
if err := conn.WriteMessage(1, []byte(msgStr)); err != nil {
logger.Errorf("send message failed,err:%s",err.Error())
return
}
}
}
}
// client.go
func main() {
dialer := websocket.Dialer{}
connect, _, err := dialer.Dial("ws://127.0.0.1:9001/ws/", nil) // 拨号
if err != nil {
fmt.Printf("connect to server error:%s\n", err.Error())
return
}
defer func() { _ = connect.Close() }()
for {
_, msg, err := connect.ReadMessage() // 从 服务端接受消息
if err != nil {
logger.Errorf("recv message failed, err:%s", err.Error())
break // 不能用continue,当服务端关闭后,再次取值会panic
}
logger.Infof("recv msg:%s", string(msg))
}
}
# 当client断开,server不会受影响。因为会退出循环,关闭连接。
[root@duduniao websocket]# go run client.go
2021-01-09 23:06:26.846|recv msg:time:2021-01-09 23:06:23,str:cwlOglvQ
2021-01-09 23:06:26.846|recv msg:time:2021-01-09 23:06:24,str:FAPoLJBO
2021-01-09 23:06:26.846|recv msg:time:2021-01-09 23:06:25,str:DgEUsGLY
2021-01-09 23:06:26.846|recv msg:time:2021-01-09 23:06:26,str:hvXjPuqJ
2021-01-09 23:06:27.204|recv msg:time:2021-01-09 23:06:27,str:CQjvmsEY
2021-01-09 23:06:28.205|recv msg:time:2021-01-09 23:06:28,str:QAFnzGtg
2021-01-09 23:06:29.205|recv msg:time:2021-01-09 23:06:29,str:TzpGuSjG
^Csignal: interrupt
[root@duduniao websocket]# go run server.go
2021-01-09 23:06:31.205|send message failed,err:write tcp 127.0.0.1:9001->127.0.0.1:59630: write: broken pipe
# 当server端口,client可以正常退出
[root@duduniao websocket]# go run client.go
2021-01-09 23:10:32.275|recv msg:time:2021-01-09 23:10:32,str:lNrfEjre
2021-01-09 23:10:33.275|recv msg:time:2021-01-09 23:10:33,str:JicBHAPm
2021-01-09 23:10:34.275|recv msg:time:2021-01-09 23:10:34,str:iujSOelc
2021-01-09 23:10:35.275|recv msg:time:2021-01-09 23:10:35,str:xeSMvUsl
2021-01-09 23:10:36.276|recv msg:time:2021-01-09 23:10:36,str:OsXQAIwZ
2021-01-09 23:10:37.276|recv msg:time:2021-01-09 23:10:37,str:HNmHmwFx
2021-01-09 23:10:37.289|recv message failed, err:websocket: close 1006 (abnormal closure): unexpected EOF
2.2. websocket发送json数据
上述的websockt发送的是简单的 []byte 数据,实践中常常用到 json 数据转发,websocket 包自带了json读写功能。
// server.go
package main
import (
"github.com/gorilla/websocket"
"github.com/labstack/gommon/random"
"go_learn/logger"
"net/http"
"time"
)
//var msg = make(chan string, 50)
var msg = make(chan map[string]string, 50)
func main() {
http.HandleFunc("/ws/", pushMessage)
go createMsg()
err := http.ListenAndServe("127.0.0.1:9001", nil)
if err != nil {
logger.Errorf("listen 127.0.0.1:9001 failed , err:%s", err.Error())
return
}
}
func createMsg() {
for {
now := time.Now().Format("2006-01-02 15:04:05")
str := random.String(8, random.Alphabetic)
//msg <- fmt.Sprintf("time:%s,str:%s", now, str)
msg <- map[string]string{"time": now, "random string": str}
time.Sleep(time.Second)
}
}
func pushMessage(w http.ResponseWriter, r *http.Request) {
upgrade := websocket.Upgrader{} // 定义一个 upgrade
conn, err := upgrade.Upgrade(w, r, nil) // 升级http协议到websocket
if err != nil {
logger.Errorf("upgrade to websocket failed, err:%s", err.Error())
return
}
defer func() { _ = conn.Close() }()
for {
select {
case msgBody := <-msg:
if err := conn.WriteJSON(msgBody); err != nil {
logger.Errorf("send message failed,err:%s", err.Error())
return
}
}
}
}
// client.go
package main
import (
"fmt"
"github.com/gorilla/websocket"
"go_learn/logger"
)
func main() {
dialer := websocket.Dialer{}
connect, _, err := dialer.Dial("ws://127.0.0.1:9001/ws/", nil) // 拨号
if err != nil {
fmt.Printf("connect to server error:%s\n", err.Error())
return
}
defer func() { _ = connect.Close() }()
var respBody map[string]string
for {
err := connect.ReadJSON(&respBody) // 从 服务端接受消息,用ReadMessage接收到的是base64格式
if err != nil {
logger.Errorf("recv message failed, err:%s", err.Error())
break // 不能用continue,当服务端关闭后,再次取值会panic
}
logger.Infof("recv msg:%#v", respBody)
}
}
[root@duduniao websocket]# go run client.go
2021-01-09 23:15:52.339|recv msg:map[string]string{"random string":"khsWFkuq", "time":"2021-01-09 23:15:49"}
2021-01-09 23:15:52.339|recv msg:map[string]string{"random string":"ArRNXQgB", "time":"2021-01-09 23:15:50"}
2021-01-09 23:15:52.339|recv msg:map[string]string{"random string":"PsdjJtID", "time":"2021-01-09 23:15:51"}
2021-01-09 23:15:52.685|recv msg:map[string]string{"random string":"jKRUJawe", "time":"2021-01-09 23:15:52"}
2021-01-09 23:15:53.685|recv msg:map[string]string{"random string":"tIZPHrqz", "time":"2021-01-09 23:15:53"}
2021-01-09 23:15:54.685|recv msg:map[string]string{"random string":"bsaZryne", "time":"2021-01-09 23:15:54"}