go-webSocket——gorilla

Posted 尚墨1111

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了go-webSocket——gorilla相关的知识,希望对你有一定的参考价值。

文章目录

gorilla-websocket


参考文章:

0 HTTP

http 是典型的 C/S 架构,客户端向服务端发送请求(request),服务端做出应答(response)。

golang 的标准库 net/http 提供了 http 编程有关的接口,封装了内部TCP连接和报文解析的复杂琐碎的细节,使用者只需要和 http.requesthttp.ResponseWriter 两个对象交互。

0.1 http.HandleFunc

源码,相当于一个适配器

type HandlerFunc func(ResponseWriter, *Request)

// ServeHTTP calls f(w, r).
func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) 
    f(w, r)

0.1.1 简单实现

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(":8080", nil)

0.1.2 net/http 提供的handler

大部分的服务器逻辑都需要使用者编写对应的 Handler,不过有些 Handler 使用频繁,因此 net/http 提供了它们的实现。

  • 比如负责文件 hosting 的 FileServer
  • 负责 404 的NotFoundHandler
  • 负责重定向的RedirectHandler

0.1.3 ListenAndServer()

// addr:监听的地址
// handler:回调函数
func ListenAndServe(addr string, handler Handler) error 
    server := &ServerAddr: addr, Handler: handler
    return server.ListenAndServe()


    http.ListenAndServe("127.0.0.1:8000", nil)

0.1.4 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.0"
    ProtoMajor int    // 1
    ProtoMinor int    // 0

    // 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
    ...

0.1.5 ResponseWriter

ResponseWriter 是一个接口,定义了三个方法:

  • Header():返回一个 Header 对象,可以通过它的 Set() 方法设置头部,注意最终返回的头部信息可能和你写进去的不完全相同,因为后续处理还可能修改头部的值(比如设置 Content-LengthContent-type 等操作)
  • Write(): 写 response 的主体部分,比如 html 或者 json 的内容就是放到这里的
  • WriteHeader():设置 status code,如果没有调用这个函数,默认设置为 http.StatusOK, 就是 200 状态码
// 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)

0.2 test

0.2.1 HTTP服务端

package main

import (
    "fmt"
    "net/http"
)

func main() 
    http.HandleFunc("/go", myHandler)
    http.ListenAndServe("127.0.0.1:8000", 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成功"))

0.2.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.0.0.1:8000/go")
    defer resp.Body.Close()
    // 200 OK
    fmt.Println(resp.Status)
    fmt.Println(resp.Header)

    buf := make([]byte, 1024)
    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
        
    

1 webSocket

1.1 是什么

  • WebSocket是一种在单个TCP连接上进行全双工通信的协议,长连接,双向传输
  • 需要安装第三方包:go get -u -v github.com/gorilla/websocket
  • WebSocket 协议实现起来相对简单。 HTTP 协议初始握手建立连接,WebSocket 实质上使用原始 TCP 读取 / 写入数据
  • http有良好的兼容性,ws和http的默认端口都是80,wss和https的默认端口都是443

1.2 webSocket握手协议

1.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区分开

1.2.2 服务器响应 Response Header

HTTP/1.1 101 Switching Protocols
    Upgrade: websocket
    Connection: Upgrade
    Sec-WebSocket-Accept: HSmrc0sMlYUkAGmm5OPpG2HaGWk=
    Sec-WebSocket-Protocol: chat

1.2.3 websocket发送的消息类型

5种TextMessag、BinaryMessage、CloseMessag、PingMessage、PongMessage

  • TextMessagBinaryMessage分别表示发送文本消息和二进制消息

  • CloseMessage关闭帧,接收方收到这个消息就关闭连接

  • PingMessagePongMessage是保持心跳的帧,服务器发ping给浏览器,浏览器返回pong消息

2 gorilla/websocket

websocket由http升级而来,首先发送附带Upgrade请求头的Http请求,所以我们需要在处理Http请求时拦截请求并判断其是否为websocket升级请求,如果是则调用gorilla/websocket库相应函数处理升级请求

2.1 Upgrader

Upgrader发送附带Upgrade请求头的Http请求,把 http 请求升级为长连接的 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

2.1.1 创建Upgrader实例

该实例用于升级请求

var upgrader = websocket.Upgrader
    ReadBufferSize:  1024, //指定读缓存大小
    WriteBufferSize: 1024, //指定写缓存大小
    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

其中CheckOringin是一个函数,该函数用于拦截或放行跨域请求。函数返回值为bool类型,即true放行,false拦截。如果请求不是跨域请求可以不赋值

2.1.2 升级协议

func (*Upgrader) Upgrade 函数将 http 升级到 WebSocket 协议。

// 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)
    

2.1.3 设置关闭连接监听

函数为SetCloseHandler(h func(code int, text string) error)函数接收一个函数为参数,参数为nil时有一个默认实现,其源码为:

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

可以看到作为参数的函数的参数为int和string类型正好和前端的close(long string)对应即前端调用close(long string)关闭连接后两个参数会被发送给后端并最终被func(code int, text string) error所使用。

// 设置关闭连接监听
conn.SetCloseHandler(func(code int, text string) error 
    fmt.Println(code, text) // 断开连接时将打印code和text
    return nil
)

2.1.4 总览

type WsServer struct 
    ......
    // 定义一个 upgrade 类型用于升级 http 为 websocket
    upgrade  *websocket.Upgrader


func NewWsServer() *WsServer 
    ws.upgrade = &websocket.Upgrader
        ReadBufferSize:  4096,//指定读缓存区大小
        WriteBufferSize: 1024,// 指定写缓存区大小
        // 检测请求来源
        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)


3 Demo

3.1

3.1.1 服务端

package main

import (
	"fmt"
	"github.com/gorilla/websocket"
	"log"
	"net/http"
)

var upgrader = websocket.Upgrader
	ReadBufferSize:  4096,
	WriteBufferSize: 1024,
	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:8080", nil))


3.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:8080" + "/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 * 1000):
			//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 

以上是关于go-webSocket——gorilla的主要内容,如果未能解决你的问题,请参考以下文章

Go-websocket

GolangWeb 入门 08 集成 Gorilla Mux

GolangWeb 入门 08 集成 Gorilla Mux

使用 gorilla 会话时未保存 golang 中的会话变量

Gorilla

对使用 gorilla/mux URL 参数的函数进行单元测试