参考文章:
-
GitHub:https://github.com/gorilla/websocket
-
Doc:https://godoc.org/github.com/gorilla/websocket
1 HTTP协议
1.1 是什么?
HTTP是一个基于TCP/IP通信协议来传递数据(HTML 文件, 图片文件, 查询结果等)
- HTTP是基于客户端/服务端(C/S)的架构模型,通过一个可靠的链接来交换信息,是一个无状态的请求/响应协议。
- 一个HTTP"客户端"是一个应用程序(Web浏览器或其他任何客户端),连接到服务器达到向服务器发送一个或多个HTTP的请求。
- 一个HTTP"服务器"同样也是一个应用程序(通常是一个Web服务,如Apache Web服务器或IIS服务器等),通过接收客户端的请求并向客户端发送HTTP响应数据。
- HTTP使用统一资源标识符(Uniform Resource Identifiers, URI)来传输数据和建立连接。
1.1.1 客户端请求消息
客户端发送一个HTTP请求到服务器的请求消息包括以下格式:请求行(request line)、请求头部(header)、空行和请求数据四个部分组成,下图给出了请求报文的一般格式。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1X0mAnbT-1661397084862)(https://atts.w3cschool.cn/attachments/image/21161225/1456372149731272.png)]
GET /hello.txt HTTP/1.1
User-Agent: curl/7.16.3 libcurl/7.16.3 OpenSSL/1.9.7l zlib/1.2.3
Host: www.example.com
Accept-Language: en, mi
1.1.2 服务器响应消息
HTTP响应也由四个部分组成,分别是:状态行、消息报头、空行和响应正文。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-462SsJTz-1661397084863)(https://atts.w3cschool.cn/attachments/image/21161225/1456372149775233.jpg)]
# 状态行
HTTP/1.1 211 OK
# 响应头
Date: Mon, 27 Jul 2119 12:28:53 GMT
Server: Apache
Last-Modified: Wed, 22 Jul 2119 19:15:56 GMT
ETag: "34aa387-d-1568eb11"
Accept-Ranges: bytes
Content-Length: 51
Vary: Accept-Encoding
Content-Type: text/plain
1.2 HTTP 请求方法
HTTP1.1 定义了三种请求方法: GET, POST 和 HEAD 方法。
HTTP1.1 新增了六种请求方法:OPTIONS、PUT、PATCH、DELETE、TRACE 和 CONNECT 方法。
序号 | 方法 | 描述 |
---|---|---|
1 | GET | 请求指定的页面信息,并返回实体主体。 |
2 | HEAD | 类似于 GET 请求,只不过返回的响应中没有具体的内容,用于获取报头 |
3 | POST | 向指定资源提交数据进行处理请求(例如提交表单或者上传文件)。数据被包含在请求体中。POST 请求可能会导致新的资源的建立和/或已有资源的修改。 |
4 | PUT | 从客户端向服务器传送的数据取代指定的文档的内容。 |
5 | DELETE | 请求服务器删除指定的页面。 |
6 | CONNECT | HTTP/1.1 协议中预留给能够将连接改为管道方式的代理服务器。 |
7 | OPTIONS | 允许客户端查看服务器的性能。 |
8 | TRACE | 回显服务器收到的请求,主要用于测试或诊断。 |
9 | PATCH | 是对 PUT 方法的补充,用来对已知资源进行局部更新 。 |
1.3 HTTP 状态码
HTTP 状态码的英文为 HTTP Status Code
分类 | 分类描述 |
---|---|
1** | 信息,服务器收到请求,需要请求者继续执行操作 |
2** | 成功,操作被成功接收并处理 |
3** | 重定向,需要进一步的操作以完成请求 |
4** | 客户端错误,请求包含语法错误或无法完成请求 |
5** | 服务器错误,服务器在处理请求的过程中发生了错误 |
常用HTTP状态码列表:
状态码 | 状态码英文名称 | 中文描述 |
---|---|---|
211 | OK | 请求成功。一般用于GET与POST请求 |
311 | Moved Permanently | 永久移动。请求的资源已被永久的移动到新URI,返回信息会包括新的URI,浏览器会自动定向到新URI。 |
315 | Use Proxy | 使用代理。所请求的资源必须通过代理访问 |
411 | Bad Request | 客户端请求的语法错误,服务器无法理解 |
411 | Unauthorized | 请求要求用户的身份认证 |
413 | Forbidden | 服务器理解请求客户端的请求,但是拒绝执行此请求 |
414 | Not Found | 服务器无法根据客户端的请求找到资源(网页)。 |
511 | Internal Server Error | 服务器内部错误,无法完成请求 |
512 | Bad Gateway | 作为网关或者代理工作的服务器尝试执行请求时,从远程服务器接收到了一个无效的响应 |
514 | Gateway Time-out | 充当网关或代理的服务器,未及时从远端服务器获取请求 |
1.4 content-type 内容类型
参考:https://www.runoob.com/http/http-content-type.html
Content-Type 标头告诉客户端实际返回的内容的内容类型。用于定义网络文件的类型和网页的编码,决定浏览器将以什么形式、什么编码读取这个文件。
语法格式:
Content-Type: text/html; charset=utf-8
Content-Type: multipart/form-data; boundary=something
实例:
常见的媒体格式类型如下:
- text/html : HTML格式
- text/plain :纯文本格式
- text/xml : XML格式
- image/gif :gif图片格式
- image/jpeg :jpg图片格式
- image/png:png图片格式
以application开头的媒体格式类型:
- application/xhtml+xml :XHTML格式
- application/xml: XML数据格式
- application/atom+xml :Atom XML聚合格式
- application/json: JSON数据格式
- application/pdf:pdf格式
- application/msword : Word文档格式
- application/octet-stream : 二进制流数据(如常见的文件下载)
- application/x-www-form-urlencoded : 中默认的encType,form表单数据被编码为key/value格式发送到服务器(表单默认的提交数据的格式)
另外一种常见的媒体格式是上传文件之时使用的:
- multipart/form-data : 需要在表单中进行文件上传时,就需要使用该格式
2 http.HandleFunc
参考:https://www.cnblogs.com/f-ck-need-u/p/11121951.html
2.1 是什么
Multiplexer根据URL将请求路由给指定的Handler。Handler用于处理请求并给予响应。更严格地说,用来读取请求体、并将请求对应的响应字段(respones header)写入ResponseWriter中,然后返回
什么是Handler。它是一个接口,定义在net/http/server.go中:
type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}
也就是说,实现了ServerHTTP方法的都是Handler。注意ServerHTTP方法的参数:http.ResponesWriter接口和Request指针。
在Handler的注释中,给出了几点主要的说明:
- Handler用于响应一个HTTP request
- 接口方法ServerHTTP应该用来将response header和需要响应的数据写入到ResponseWriter中,然后返回。
2.1.1 ResponseWriter接口
ResponseWriter接口的作用是用于构造HTTP response,并将响应header和响应数据通过网络链接发送给客户端。
// A ResponseWriter interface is used by an HTTP handler to
// construct an HTTP response.
//
// A ResponseWriter may not be used after the Handler.ServeHTTP method
// has returned.
type ResponseWriter interface {
Header() Header
Write([]byte) (int, error)
WriteHeader(statusCode int)
}
这个接口有3个方法:
type Header map[string][]string
Go有一个函数HandleFunc(),它表示使用第二个参数的函数作为handler,处理匹配到的url路径请求。HandleFunc 的第一个参数指的是请求路径,第二个参数是一个函数类型,表示这个请求需要处理的事情。没有处理复杂的逻辑,而是直接给DefaultServeMux处理,如源码:
参考:https://blog.51cto.com/u_8184160/2052853
func HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
DefaultServeMux.HandleFunc(pattern, handler)
}
net/httphttp.requesthttp.ResponseWriter
源码,相当于一个适配器
type HandlerFunc func(ResponseWriter, *Request)
// ServeHTTP calls f(w, r).
func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
f(w, r)
}
2.2 简单实现
package main
import (
"io"
"net/http"
)
func helloHandler(w http.ResponseWriter, r *http.Request) {
io.WriteString(w, "hello world!\n")
}
func main() {
// 回调函数
http.HandleFunc("/", helloHandler)
http.ListenAndServe(":8181", nil)
}
2.2.1 net/http 提供的handler
net/http
FileServerNotFoundHandlerRedirectHandler
2.2.2 ListenAndServer()
在启动go http自带的web服务时,调用了函数ListenAndServe()。这个函数的定义如下:
func ListenAndServe(addr string, handler Handler) error
该函数有两个参数,第一个参数是自带的web监听地址和端口,第二个参数是Handler,用来处理每个接进来的http request,但一般第二个参数设置为nil,表示调用默认的Multiplexer:DefaultServeMux。这个默认的ServeMux唯一的作用,是将请求根据URL路由给对应的handler进行处理。
var DefaultServeMux = &defaultServeMux
// addr:监听的地址
// handler:回调函数
func ListenAndServe(addr string, handler Handler) error {
server := &Server{Addr: addr, Handler: handler}
return server.ListenAndServe()
}
http.ListenAndServe("127.1.1.1:8111", nil)
2.2.3 Request
Request 就是封装好的客户端请求,包括 URL,method,header 等等所有信息,以及一些方便使用的方法:
Handler 需要知道关于请求的任何信息,都要从这个对象中获取,一般不会直接修改这个对象
type Request struct {
// Method specifies the HTTP method (GET, POST, PUT, etc.).For client requests an empty string means GET.
Method string
// URL specifies either the URI being requested (for server requests) or the URL to access (for client requests).
URL *url.URL
// The protocol version for incoming requests.
// Client requests always use HTTP/1.1.
Proto string // "HTTP/1.1"
ProtoMajor int // 1
ProtoMinor int // 1
// A header maps request lines to their values.
// If the header says
//
// accept-encoding: gzip, deflate
// Accept-Language: en-us
// Connection: keep-alive
//
// then
//
// Header = map[string][]string{
// "Accept-Encoding": {"gzip, deflate"},
// "Accept-Language": {"en-us"},
// "Connection": {"keep-alive"},
// }
Header Header
// Body is the request's body.
Body io.ReadCloser
ContentLength int64
TransferEncoding []string
Close bool
Host string
Form url.Values
PostForm url.Values
MultipartForm *multipart.Form
...
RemoteAddr string
...
}
2.2.4 ResponseWriter
ResponseWriter 是一个接口,定义了三个方法:
Header()Set()Content-LengthContent-typeWrite()htmljsonWriteHeader()http.StatusOK211
// A ResponseWriter interface is used by an HTTP handler to
// construct an HTTP response.
type ResponseWriter interface {
Header() Header
Write([]byte) (int, error)
WriteHeader(int)
}
2.3 test
2.3.1 HTTP服务端
package main
import (
"fmt"
"net/http"
)
func main() {
http.HandleFunc("/go", myHandler)
http.ListenAndServe("127.1.1.1:8111", nil)
}
func myHandler(w http.ResponseWriter, r *http.Request) {
fmt.Println(r.RemoteAddr, "连接成功")
// 请求方式:GET POST DELETE PUT UPDATE
fmt.Println("method:", r.Method)
fmt.Println("url:", r.URL.Path)
fmt.Println("header:", r.Header)
fmt.Println("body:", r.Body)
// 回复
w.Write([]byte("test成功"))
}
2.3.2 HTTP客户端
package main
import (
"fmt"
"io"
"net/http"
)
func main() {
//resp, _ := http.Get("http://www.baidu.com")
//fmt.Println(resp)
resp, _ := http.Get("http://127.1.1.1:8111/go")
defer resp.Body.Close()
// 211 OK
fmt.Println(resp.Status)
fmt.Println(resp.Header)
buf := make([]byte, 1124)
for {
// 接收服务端信息
n, err := resp.Body.Read(buf)
if err != nil && err != io.EOF {
fmt.Println(err)
return
} else {
fmt.Println("读取完毕")
res := string(buf[:n])
fmt.Println(res)
break
}
}
}
3 webSocket
3.1 是什么
HTTPWebSocketTCP
3.2 webSocket握手协议
3.2.1 客户端请求 Request Header
GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket // 指明使用WebSocket协议
Connection: Upgrade // 指明使用WebSocket协议
Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw== // Bse64 encode的值,是浏览器随机生成的
Sec-WebSocket-Protocol: chat, superchat
Sec-WebSocket-Version: 13 //指定Websocket协议版本
Origin: http://example.com
服务端收到Sec-WebSocket-Key后拼接上一个固定的GUID,进行一次SHA-1摘要,再转成Base64编码,得到Sec-WebSocket-Accept返回给客户端。客户端对本地的Sec-WebSocket-Key执行同样的操作跟服务端返回的结果进行对比,如果不一致会返回错误关闭连接。如此操作是为了把websocket header 跟http header区分开
3.2.2 服务器响应 Response Header
HTTP/1.1 111 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: HSmrc1sMlYUkAGmm5OPpG2HaGWk=
Sec-WebSocket-Protocol: chat
3.2.3 websocket发送的消息类型
TextMessag、BinaryMessage、CloseMessag、PingMessage、PongMessage
TextMessagBinaryMessageCloseMessagePingMessagePongMessagepingpong
3.2.4 控制类消息
ConnWriteControlWriteMessageNextWriter
ConnSetCloseHandlerNextReaderReadMessageRead*CloseError
ConnSetPingHandler
ConnSetPongHandler
NextReaderReadMessageRead
Conn
4 gorilla/websocket
gorilla/websocket
4.1 Upgrader
Upgrader WebSocket
type Upgrader struct {
// 升级 websocket 握手完成的超时时间
HandshakeTimeout time.Duration
// io 操作的缓存大小,如果不指定就会自动分配。
ReadBufferSize, WriteBufferSize int
// 写数据操作的缓存池,如果没有设置值,write buffers 将会分配到链接生命周期里。
WriteBufferPool BufferPool
//按顺序指定服务支持的协议,如值存在,则服务会从第一个开始匹配客户端的协议。
Subprotocols []string
// http 的错误响应函数,如果没有设置 Error 则,会生成 http.Error 的错误响应。
Error func(w http.ResponseWriter, r *http.Request, status int, reason error)
// 如果请求Origin标头可以接受,CheckOrigin将返回true。 如果CheckOrigin为nil,则使用安全默认值:如果Origin请求头存在且原始主机不等于请求主机头,则返回false。
// 请求检查函数,用于统一的链接检查,以防止跨站点请求伪造。如果不检查,就设置一个返回值为true的函数
CheckOrigin func(r *http.Request) bool
// EnableCompression 指定服务器是否应尝试协商每个邮件压缩(RFC 7692)。 将此值设置为true并不能保证将支持压缩。 目前仅支持“无上下文接管”模式
EnableCompression bool
}
4.1.1 创建Upgrader实例
该实例用于升级请求
var upgrader = websocket.Upgrader{
ReadBufferSize: 1124, //指定读缓存大小
WriteBufferSize: 1124, //指定写缓存大小
CheckOrigin: checkOrigin,
}
// 检测请求来源
func checkOrigin(r *http.Request) bool {
if r.Method != "GET" {
fmt.Println("method is not GET")
return false
}
if r.URL.Path != "/ws" {
fmt.Println("path error")
return false
}
return true
}
CheckOringinbooltruefalse
4.1.2 升级协议
func (*Upgrader) Upgrade
// responseHeader包含在对客户端升级请求的响应中。
// 使用responseHeader指定cookie(Set-Cookie)和应用程序协商的子协议(Sec-WebSocket-Protocol)。
// 如果升级失败,则升级将使用HTTP错误响应回复客户端
// 返回一个 Conn 指针,使用 Conn 读写数据与客户端通信。
func (u *Upgrader) Upgrade(w http.ResponseWriter, r *http.Request, responseHeader http.Header) (*Conn, error)
升级为websocket连接并获得一个conn实例,之后的发送接收操作皆有conn,其类型为websocket.Conn。
//Http入口
func (e *Engine) ServeHTTP(w http.ResponseWriter, r *http.Request) {
//判断请求是否为websocket升级请求。
if websocket.IsWebSocketUpgrade(r) {
// 收到 http 请求后升级协议
conn, err := upgrader.Upgrade(w, r, w.Header())
// 向客户端发送消息使用 WriteMessage(messageType int, data []byte),参数1为消息类型,参数2消息内容
conn.WriteMessage(websocket.TextMessage, []byte("升级成功"))
// 接受客户端消息使用 ReadMessage(),该操作阻塞线程所以建议运行在其他协程上。
//返回值(接收消息类型、接收消息内容、发生的错误)当然正常执行时错误为 nil。一旦连接关闭返回值类型为-1可用来终止读操作。
go func() {
for {
t, c, _ := conn.ReadMessage()
fmt.Println(t, string(c))
if t == -1 {
return
}
}
}()
} else {
//处理普通请求
c := newContext(w, r)
e.router.handle(c)
}
}
4.1.3 设置关闭连接监听
SetCloseHandler(h func(code int, text string) error)
func (c *Conn) SetCloseHandler(h func(code int, text string) error) {
if h == nil {
h = func(code int, text string) error {
message := FormatCloseMessage(code, "")
c.WriteControl(CloseMessage, message, time.Now().Add(writeWait))
return nil
}
}
c.handleClose = h
}
func(code int, text string) error
// 设置关闭连接监听
conn.SetCloseHandler(func(code int, text string) error {
fmt.Println(code, text) // 断开连接时将打印code和text
return nil
})
4.1.4 总览
type WsServer struct {
......
// 定义一个 upgrade 类型用于升级 http 为 websocket
upgrade *websocket.Upgrader
}
func NewWsServer() *WsServer {
ws.upgrade = &websocket.Upgrader{
ReadBufferSize: 4196,//指定读缓存区大小
WriteBufferSize: 1124,// 指定写缓存区大小
// 检测请求来源
CheckOrigin: func(r *http.Request) bool {
if r.Method != "GET" {
fmt.Println("method is not GET")
return false
}
if r.URL.Path != "/ws" {
fmt.Println("path error")
return false
}
return true
},upgrade
}
return ws
}
func (self *WsServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
......
// 收到 http 请求后 升级 协议
conn, err := self.upgrade.Upgrade(w, r, nil)
if err != nil {
fmt.Println("websocket error:", err)
return
}
fmt.Println("client connect :", conn.RemoteAddr())
go self.connHandle(conn)
}
5 Demo
5.1
5.1.1 服务端
package main
import (
"fmt"
"github.com/gorilla/websocket"
"log"
"net/http"
)
var upgrader = websocket.Upgrader{
ReadBufferSize: 4196,
WriteBufferSize: 1124,
CheckOrigin: func(r *http.Request) bool {
//if r.Method != "GET" {
// fmt.Println("method is not GET")
// return false
//}
//if r.URL.Path != "/ws" {
// fmt.Println("path error")
// return false
//}
return true
},
}
// ServerHTTP 用于升级协议
func ServerHTTP(w http.ResponseWriter, r *http.Request) {
// 收到http请求之后升级协议
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
log.Println("Error during connection upgrade:", err)
return
}
defer conn.Close()
for {
// 服务端读取客户端请求
messageType, message, err := conn.ReadMessage()
if err != nil {
log.Println("Error during message reading:", err)
break
}
log.Printf("Received:%s", message)
// 开启关闭连接监听
conn.SetCloseHandler(func(code int, text string) error {
fmt.Println(code, text) // 断开连接时将打印code和text
return nil
})
//服务端给客户端返回请求
err = conn.WriteMessage(messageType, message)
if err != nil {
log.Println("Error during message writing:", err)
return
}
}
}
func home(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Index Page")
}
func main() {
http.HandleFunc("/socket", ServerHTTP)
http.HandleFunc("/", home)
log.Fatal(http.ListenAndServe("localhost:8181", nil))
}
5.1.2 客户端
// client.go
package main
import (
"github.com/gorilla/websocket"
"log"
"os"
"os/signal"
"time"
)
var done chan interface{}
var interrupt chan os.Signal
func receiveHandler(connection *websocket.Conn) {
defer close(done)
for {
_, msg, err := connection.ReadMessage()
if err != nil {
log.Println("Error in receive:", err)
return
}
log.Printf("Received: %s\n", msg)
}
}
func main() {
done = make(chan interface{}) // Channel to indicate that the receiverHandler is done
interrupt = make(chan os.Signal) // Channel to listen for interrupt signal to terminate gracefully
signal.Notify(interrupt, os.Interrupt) // Notify the interrupt channel for SIGINT
socketUrl := "ws://localhost:8181" + "/socket"
conn, _, err := websocket.DefaultDialer.Dial(socketUrl, nil)
if err != nil {
log.Fatal("Error connecting to Websocket Server:", err)
}
defer conn.Close()
go receiveHandler(conn)
// 无限循环使用select来通过通道监听事件
for {
select {
case <-time.After(time.Duration(1) * time.Millisecond * 1111):
//conn.WriteMessage()每秒钟写一条消息
err := conn.WriteMessage(websocket.TextMessage, []byte("Hello from GolangDocs!"))
if err != nil {
log.Println("Error during writing to websocket:", err)
return
}
//如果激活了中断信号,则所有未决的连接都将关闭
case <-interrupt:
// We received a SIGINT (Ctrl + C). Terminate gracefully...
log.Println("Received SIGINT interrupt signal. Closing all pending connections")
// Close our websocket connection
err := conn.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, ""))
if err != nil {
log.Println("Error during closing websocket:", err)
return
}
select {
// 如果receiveHandler通道退出,则通道'done'将关闭
case <-done:
log.Println("Receiver Channel Closed! Exiting....")
//如果'done'通道未关闭,则在1秒钟后会有超时,因此程序将在1秒钟超时后退出
case <-time.After(time.Duration(1) * time.Second):
log.Println("Timeout in closing receiving channel. Exiting....")
}
return
}
}
}
5.1.3 简单版
服务端
package main
import (
"github.com/gorilla/websocket"
"log"
"net/http"
)
var upgrade = websocket.Upgrader{
ReadBufferSize: 1124,
WriteBufferSize: 1124,
CheckOrigin: func(r *http.Request) bool {
return true
},
}
func HelloHTTP(w http.ResponseWriter, r *http.Request) {
//1.升级协议,并返回升级后的长连接
conn, err := upgrade.Upgrade(w, r, nil)
if err != nil {
log.Println("Error during connection upgrade:", err)
return
}
defer conn.Close()
for {
// 2.读取客户端的请求信息
messageType, message, err := conn.ReadMessage()
if err != nil {
log.Println("Error during message writing:", err)
return
}
log.Printf("Recive message:%s", message)
// 3.返回给客户端信息
err = conn.WriteMessage(messageType, message)
if err != nil {
log.Println("Error during message writing:", err)
return
}
}
}
func main() {
http.HandleFunc("/socket", HelloHTTP)
http.ListenAndServe(":8181", nil)
}
客户端
package main
import (
"github.com/gorilla/websocket"
"log"
"time"
)
func ReceiveHandler(con *websocket.Conn) {
for {
_, message, err := con.ReadMessage()
if err != nil {
log.Println("Error during Receive:", err)
return
}
log.Printf("Receive:%s\n", message)
}
}
func main() {
socketUrl := "ws://localhost:8181" + "/socket"
// 使用 net.Dialer Dialer.Dial 函数建立 TCP 连接,建立成功后,取得了 net.Conn 对象,
conn, _, err := websocket.DefaultDialer.Dial(socketUrl, nil)
if err != nil {
log.Fatal("Error connecting to websocket Server:", err)
}
defer conn.Close()
ticker := time.Tick(time.Second)
for range ticker {
err = conn.WriteMessage(websocket.TextMessage, []byte("Hello World!"))
if err != nil {
log.Println("Error during writing to websocket:", err)
return
}
// 接受客户端消息使用ReadMessage()该操作会阻塞线程所以建议运行在其他协程上
go ReceiveHandler(conn)
}
}