Go实现Websocket

WebSocket

  • WebSocket 是独立的、创建在 TCP 上的协议。
  • WebSocket在 HTML5 游戏和网页消息推送都使用比较多。WebSocket 是 HTML5 的重要特性,它实现了基于浏览器的远程socket,它使浏览器和服务器可以进行全双工通信,能更好的节省服务器资源和带宽并达到实时通讯的目的。
  • 它与HTTP一样通过已建立的TCP连接来传输数据,但是它和HTTP最大不同是:WebSocket是一种双向通信协议。在建立连接后,WebSocket服务器端和客户端都能主动向对方发送或接收数据,就像Socket一样;WebSocket需要像TCP一样,先建立连接,连接成功后才能相互通信。

与HTTP长连接的区别

类似Socket的TCP长连接通讯:首先WebSocket是类似Socket的TCP长连接通讯模式。一旦WebSocket连接建立后,后续数据都以帧序列的形式传输。在客户端断开WebSocket连接或Server端中断连接前,不需要客户端和服务端重新发起连接请求

相比HTTP长连接WebSocket的特点

  • 是真正的全双工方式,建立连接后客户端与服务器端都可以互相主动请求。而HTTP长连接基于HTTP,是传统的客户端对服务器发起请求的模式。HTTP长连接中,每次数据交换除了真正的数据部分外,服务器和客户端还要大量交换HTTP header,信息交换效率很低。
  • Websocket协议通过第一个request建立了TCP连接之后,之后交换的数据都不需要发送 HTTP header就能交换数据,这显然和原有的HTTP协议有区别所以它需要对服务器和客户端都进行升级才能实现(主流浏览器都已支持HTML5)。
  • 不同的URL可以复用同一个WebSocket连接等功能。这是HTTP长连接不能做到的。
  • 连接建立后定期的心跳检测
  • 浏览器请求头:
    General:
    Request URL: ws://127.0.0.1:8080/ws
    Request Method: GET
    Status Code: 101 Switching Protocols
    
    Response Headers:
    //服务端返回的header
    Connection: Upgrade
    Sec-WebSocket-Accept: 37En2yJBKtzkzTbOExAi+tc6DRs=
    Upgrade: websocket
    //客户端请求的header
    Accept-Encoding: gzip, deflate, br
    Accept-Language: zh-CN,zh;q=0.9,zh-TW;q=0.8,en-US;q=0.7,en;q=0.6
    Cache-Control: no-cache
    Connection: Upgrade
    Host: 127.0.0.1:8080
    Origin: file://
    Pragma: no-cache
    Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits
    Sec-WebSocket-Key: cRYRFb0jzm06V3NPbnRjmw==
    Sec-WebSocket-Version: 13
    Upgrade: websocket
    User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.105 Safari/537.36
    

    1.Upgrade: websocket :表明这是WebSocket类型请求;
    2. Sec-WebSocket-Key: cRYRFb0jzm06V3NPbnRjmw== :是WebSocket客户端发送的一个 base64编码的密文(浏览器随机生成的),要求服务端必须返回一个对应加密的Sec-WebSocket-Accept应答,否则客户端会抛出Error during WebSocket handshake错误,并关闭连接。
    3.经过这样的请求-响应处理后,两端的WebSocket连接握手成功, 后续就可以进行TCP通讯了

Go中websocket的服务端代码实现

  • 使用github.com/gorilla/websocket包
  • websocket重点在于协议转换的过程:通过Upgrade函数实现
    func (u *Upgrader) Upgrade(w http.ResponseWriter, r *http.Request, responseHeader http.Header) (*Conn, error) {}
    

    该函数的功能主要是:
    判断请求方法是否为GET,不是GET则为非法握手方法
    根据client的请求头信息,确认升级协议
    校验跨域
    填充响应头,并返回客户端,链接建立

type WsServer struct {
	listener net.Listener
	addr     string
	upgrade  *websocket.Upgrader
}

func NewWsServer() *WsServer {
	ws := new(WsServer)
	ws.addr = "0.0.0.0:8080"
	ws.upgrade = &websocket.Upgrader{
		// 指定升级 websocket 握手完成的超时时间
		HandshakeTimeout :time.Second*5,

		// 写数据操作的缓存池,如果没有设置值,write buffers 将会分配到链接生命周期里。
		//WriteBufferPool BufferPool

		//按顺序指定服务支持的协议,如值存在,则服务会从第一个开始匹配客户端的协议。
		//Subprotocols []string

		// 指定 http 的错误响应函数,如果没有设置 Error 则,会生成 http.Error 的错误响应。
		//Error func(w http.ResponseWriter, r *http.Request, status int, reason error)

		// EnableCompression 指定服务器是否应尝试协商每个邮件压缩(RFC 7692)。
		// 将此值设置为true并不能保证将支持压缩。
		// 目前仅支持“无上下文接管”模式
		//EnableCompression bool

		// 指定 io 操作的缓存大小,如果不指定就会自动分配。
		ReadBufferSize:1024,
		WriteBufferSize:1024,

		// 请求检查函数,用于统一的链接检查,以防止跨站点请求伪造。如果不检查,就设置一个返回值为true的函数。
		// 如果请求Origin标头可以接受,CheckOrigin将返回true。 如果CheckOrigin为nil,则使用安全默认值:
		// 如果Origin请求头存在且原始主机不等于请求主机头,则返回false
		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
		},
	}
	return ws
}

//往conn中发送信息
func (w *WsServer) send(conn *websocket.Conn, stopCh chan int) {
	//w.send10(conn)
	for {
		select {
		case <-stopCh:
			fmt.Println("connect closed")
			return
		case <-time.After(time.Second * 1):
			data := fmt.Sprintf("hello websocket(timeStamp: %v)", time.Now().UnixNano())
			err := conn.WriteMessage(1, []byte(data))
			fmt.Println("sending....")
			if err != nil {
				fmt.Println("send msg faild ", err)
				return
			}
		}
	}
}

//从conn中读取信息
func (w *WsServer) connHandle(conn *websocket.Conn) {
	defer func() {
		conn.Close()
	}()
	stopCh := make(chan int)
	go w.send(conn, stopCh)
	for {
		conn.SetReadDeadline(time.Now().Add(time.Second * time.Duration(5000)))
		_, msg, err := conn.ReadMessage()
		if err != nil {
			close(stopCh)
			// 判断是不是超时
			if netErr, ok := err.(net.Error); ok {
				if netErr.Timeout() {
					fmt.Printf("ReadMessage timeout remote: %v\n", conn.RemoteAddr())
					return
				}
			}
			// 其他错误,如果是 1001 和 1000 就不打印日志
			if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseNormalClosure) {
				fmt.Printf("ReadMessage other remote:%v error: %v \n", conn.RemoteAddr(), err)
			}
			return
		}
		fmt.Println("收到消息:", string(msg))
	}
}

//WsServer需要实现了Handler这个接口类
func (w *WsServer) ServeHTTP(rsp http.ResponseWriter, req *http.Request) {
	if req.URL.Path != "/ws" {
		httpCode := http.StatusInternalServerError
		statusCodeMsg := http.StatusText(httpCode)
		fmt.Println("500:", statusCodeMsg)
		http.Error(rsp, statusCodeMsg, httpCode)
		return
	}
	conn, err := w.upgrade.Upgrade(rsp, req, nil)
	if err != nil {
		fmt.Println("websocket error:", err)
		return
	}
	fmt.Println("client connect :", conn.RemoteAddr())
	go w.connHandle(conn)

}

服务开启:

func main() {
	w:= NewWsServer()
	w.listener, err = net.Listen("tcp", w.addr)
	if err != nil {
		fmt.Println("net listen error:", err)
		return
	}
	//WsServer实现了Handler这个接口类所以第二个参数可以直接传w
	err = http.Serve(w.listener, w)
	if err != nil {
		fmt.Println("http serve error:", err)
		return
	}
}

客户端

通过golang.org/x/net/websocket包来dial发送消息


type Client struct {
	Host string
	Path string
}

func NewWebsocketClient(host, path string) *Client {
	return &Client{
		Host: host,
		Path: path,
	}
}

func (c *Client) SendMessage(body []byte) error {
	u := url.URL{Scheme: "ws", Host: c.Host, Path: c.Path}
	//使用golang.org/x/net/websocket来dial发送消息
	ws, err := websocket.Dial(u.String(), "", "http://"+c.Host+"/")
	defer ws.Close() //关闭连接
	if err != nil {
		fmt.Println("websocket dial err:",err)
		return err
	}

	_, err = ws.Write(body)
	if err != nil {
		fmt.Println("websocket Write err:",err)
		return err
	}
	return nil
}

func main(){
	wc:=NewWebsocketClient("127.0.0.1:8080","ws")
	wc.SendMessage([]byte("hello"))
}

前端Js请求:

<!DOCTYPE html>
<html>

<head>
    <meta charset="UTF-8" />
    <title>Sample of websocket with golang</title>
    <script src="http://apps.bdimg.com/libs/jquery/2.1.4/jquery.min.js"></script>

    <script>
        $(function() {
        	var $ul = $('#msg');
            var ws = new WebSocket('ws://127.0.0.1:8080/ws');

            ws.onopen = function(e) {
                $('<li>').text("connected").appendTo($ul);
            }

            ws.onmessage = function(e) {
                $('<li>').text(e.data).appendTo($ul);
            };
        
        });
    </script>
</head>

<body>
<ul id="msg"></ul>
</body>

</html>


浏览器访问结果:

connected
hello websocket(timeStamp: 1596425101756145600)
hello websocket(timeStamp: 1596425102756530000)
hello websocket(timeStamp: 1596425103771658100)
hello websocket(timeStamp: 1596425104771919200)
hello websocket(timeStamp: 1596425105771982700)
hello websocket(timeStamp: 1596425106771997900)
hello websocket(timeStamp: 1596425107782578100)
hello websocket(timeStamp: 1596425108794537100)
hello websocket(timeStamp: 1596425109799885400)
hello websocket(timeStamp: 1596425110800988500)
...

猜你喜欢

转载自blog.csdn.net/wzb_wzt/article/details/107760373