golang 长连接web socket原理

1.原理

WebSocket协议用ws表示。此外,还有wss协议,表示加密的WebSocket协议,对应HTTPs协议。

完成握手以后,WebSocket协议就在TCP协议之上,开始传送数据

websocket原理及运行机制

WebSocket是HTML5下一种新的协议。它实现了浏览器与服务器全双工通信,能更好的节省服务器资源和带宽并达到实时通讯的目的。它与HTTP一样通过已建立的TCP连接来传输数据,但是它和HTTP最大不同是:WebSocket是一种双向通信协议。在建立连接后,WebSocket服务器端和客户端都能主动向对方发送或接收数据,就像Socket一样;WebSocket需要像TCP一样,先建立连接,连接成功后才能相互通信。传统HTTP客户端与服务器请求响应模式如下图所示:

golang 长连接web socket原理_第1张图片

WebSocket模式客户端与服务器请求响应模式如下图:

golang 长连接web socket原理_第2张图片

上图对比可以看出,==相对于传统HTTP每次请求-应答都需要客户端与服务端建立连接的模式,WebSocket是类似Socket的TCP长连接通讯模式。一旦WebSocket连接建立后,后续数据都以帧序列的形式传输。在客户端断开WebSocket连接或Server端中断连接前,不需要客户端和服务端重新发起连接请求==。在海量并发及客户端与服务器交互负载流量大的情况下,极大的节省了网络带宽资源的消耗,有明显的性能优势,且客户端发送和接受消息是在同一个持久连接上发起,实时性优势明显。

 

2.特点

相比HTTP长连接,WebSocket有以下特点:

  • 是真正的全双工方式,建立连接后客户端与服务器端是完全平等的,可以互相主动请求。而HTTP长连接基于HTTP,是传统的客户端对服务器发起请求的模式。HTTP长连接中,每次数据交换除了真正的数据部分外,服务器和客户端还要大量交换HTTP header,信息交换效率很低。

  • Websocket协议通过第一个request建立了TCP连接之后,之后交换的数据都不需要发送 HTTP header就能交换数据,这显然和原有的HTTP协议有区别所以它需要对服务器和客户端都进行升级才能实现(主流浏览器都已支持HTML5)。

  • 此外还有 multiplexing、不同的URL可以复用同一个WebSocket连接等功能。这些都是HTTP长连接不能做到的。

  • 连接建立后定期的心跳检测

 

3.数据格式

在客户端,new WebSocket实例化一个新的WebSocket客户端对象,请求类似 ws://yourdomain:port/path的服务端WebSocket URL,客户端WebSocket对象会自动解析并识别为WebSocket请求,并连接服务端端口,执行双方握手过程,客户端发送数据格式类似:

  1. GET /webfin/websocket/ HTTP/1.1

  2. Host: localhost

  3. 可以看到,客户端发起的WebSocket连接报文类似传统HTTP报文

  4. Connection: Upgrade

  5. Sec-WebSocket-Key: xqBt3ImNzJbYqRINxEFlkg==

  6. Origin: http://localhost:8080

  7. Sec-WebSocket-Version: 13

可以看到,客户端发起的WebSocket连接报文类似传统HTTP报文

 

  • Upgrade:websocket参数值表明这是WebSocket类型请求,
  • Sec-WebSocket-Key是WebSocket客户端发送的一个 base64编码的密文,要求服务端必须返回一个对应加密的Sec-WebSocket-Accept应答,否则客户端会抛出Error during WebSocket handshake错误,并关闭连接。

Upgrade: websocket

Connection: Upgrade

这个就是Websocket的核心了,告诉Apache、Nginx等服务器进行协议转换

 

  • Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==
  • Sec-WebSocket-Protocol: chat, superchat
  • Sec-WebSocket-Version: 13

 

首先,Sec-WebSocket-Key 是一个Base64 encode的值,这个是浏览器随机生成的,告诉服务器验证websocket协议。

然后,Sec_WebSocket-Protocol 是一个用户定义的字符串,用来区分同URL下,不同的服务所需要的协议。

服务端收到报文后返回的数据格式类似:

  • HTTP/1.1 101 Switching Protocols
  • Upgrade: websocket
  • Connection: Upgrade
  • Sec-WebSocket-Accept: K7DJLdLooIwIG/MOpvWFB3y3FE8=

Sec-WebSocket-Accept 的值是服务端采用与客户端一致的密钥计算出来后返回客户端的。

HTTP/1.1 101 Switching Protocols表示服务端接受WebSocket协议的客户端连接,经过这样的请求-响应处理后,两端的WebSocket连接握手成功, 后续就可以进行TCP通讯了。用户可以查阅WebSocket协议栈了解WebSocket客户端和服务端更详细的交互数据格式。

在开发方面,WebSocket API 也十分简单:只需要实例化 WebSocket,创建连接,然后服务端和客户端就可以相互发送和响应消息。在WebSocket 实现及案例分析部分可以看到详细的 WebSocket API 及代码实现。

golang 长连接web socket原理_第3张图片

 

4.源码

golang中的websokectgithub.com/gorilla/websocket

项目中主要使用 github.com/gorilla/websocket这个包。

通过上面对websocket原理的描述可以知道,http到websocket有一个协议转换的过程,重点关注 Upgrade服务端协议转换函数。

// Upgrade upgrades the HTTP server connection to the WebSocket protocol.

//

// The responseHeader is included in the response to the client's upgrade

// request. Use the responseHeader to specify cookies (Set-Cookie) and the

// application negotiated subprotocol (Sec-Websocket-Protocol).

//

// If the upgrade fails, then Upgrade replies to the client with an HTTP error

// response.

func (u *Upgrader) Upgrade(w http.ResponseWriter, r *http.Request, responseHeader http.Header) (*Conn, error) {

	if r.Method != "GET" {

		return u.returnError(w, r, http.StatusMethodNotAllowed, "websocket: not a websocket handshake: request method is not GET")

	}

	if _, ok := responseHeader["Sec-Websocket-Extensions"]; ok {

		return u.returnError(w, r, http.StatusInternalServerError, "websocket: application specific 'Sec-Websocket-Extensions' headers are unsupported")

	}

	if !tokenListContainsValue(r.Header, "Connection", "upgrade") {

		return u.returnError(w, r, http.StatusBadRequest, "websocket: not a websocket handshake: 'upgrade' token not found in 'Connection' header")

	}

	if !tokenListContainsValue(r.Header, "Upgrade", "websocket") {

		return u.returnError(w, r, http.StatusBadRequest, "websocket: not a websocket handshake: 'websocket' token not found in 'Upgrade' header")

	}

	if !tokenListContainsValue(r.Header, "Sec-Websocket-Version", "13") {

		return u.returnError(w, r, http.StatusBadRequest, "websocket: unsupported version: 13 not found in 'Sec-Websocket-Version' header")

	}

	checkOrigin := u.CheckOrigin

	if checkOrigin == nil {

		checkOrigin = checkSameOrigin

	}

	if !checkOrigin(r) {

		return u.returnError(w, r, http.StatusForbidden, "websocket: 'Origin' header value not allowed")

	}

	challengeKey := r.Header.Get("Sec-Websocket-Key")

	if challengeKey == "" {

		return u.returnError(w, r, http.StatusBadRequest, "websocket: not a websocket handshake: `Sec-Websocket-Key' header is missing or blank")

	}

	subprotocol := u.selectSubprotocol(r, responseHeader)

	// Negotiate PMCE

	var compress bool

	if u.EnableCompression {

		for _, ext := range parseExtensions(r.Header) {

			if ext[""] != "permessage-deflate" {

				continue

			}

			compress = true

			break

		}

	}

	var (

		netConn net.Conn

		err error

	)

	h, ok := w.(http.Hijacker)

	if !ok {

		return u.returnError(w, r, http.StatusInternalServerError, "websocket: response does not implement http.Hijacker")

	}

	var brw *bufio.ReadWriter

	netConn, brw, err = h.Hijack()

	if err != nil {

		return u.returnError(w, r, http.StatusInternalServerError, err.Error())

	}

	if brw.Reader.Buffered() > 0 {

		netConn.Close()

		return nil, errors.New("websocket: client sent data before handshake is complete")

	}

	c := newConnBRW(netConn, true, u.ReadBufferSize, u.WriteBufferSize, brw)

	c.subprotocol = subprotocol

	if compress {

		c.newCompressionWriter = compressNoContextTakeover

		c.newDecompressionReader = decompressNoContextTakeover

	}

	p := c.writeBuf[:0]

	p = append(p, "HTTP/1.1 101 Switching ProtocolsrnUpgrade: websocketrnConnection: UpgradernSec-WebSocket-Accept: "...)

	p = append(p, computeAcceptKey(challengeKey)...)

	p = append(p, "rn"...)

	if c.subprotocol != "" {

		p = append(p, "Sec-Websocket-Protocol: "...)

		p = append(p, c.subprotocol...)

		p = append(p, "rn"...)

	}

	if compress {

		p = append(p, "Sec-Websocket-Extensions: permessage-deflate; server_no_context_takeover; client_no_context_takeoverrn"...)

	}

	for k, vs := range responseHeader {

		if k == "Sec-Websocket-Protocol" {

			continue

		}

		for _, v := range vs {

			p = append(p, k...)

			p = append(p, ": "...)

			for i := 0; i < len(v); i++ {

				b := v[i]

				if b <= 31 {

					// prevent response splitting.

					b = ' '

				}

				p = append(p, b)

			}

			p = append(p, "rn"...)

		}

	}

	p = append(p, "rn"...)

	// Clear deadlines set by HTTP server.

	netConn.SetDeadline(time.Time{})

	if u.HandshakeTimeout > 0 {

		netConn.SetWriteDeadline(time.Now().Add(u.HandshakeTimeout))

	}

	if _, err = netConn.Write(p); err != nil {

		netConn.Close()

		return nil, err

	}

	if u.HandshakeTimeout > 0 {

		netConn.SetWriteDeadline(time.Time{})

	}

	return c, nil

}

通过该函数可以看到大致流程:

  • 判断请求方法是否为GET,不是GET则为非法握手方法

  • 根据client的请求头信息,确认升级协议

  • 校验跨域

  • 填充响应头,响应返回客户端,链接建立

具体实现Server端

主要采用Upgrade函数进行协议转换。指定了ReadBufferSize、WriteBufferSize、HandshakeTimeout参数,同时跨域叫为采用默认校验函数,自定义的校验函数总是返回true跳过了跨域校验

//controller
type MyWebSocketController struct {
	beego.Controller
}

var upgrader = websocket.Upgrader{
	ReadBufferSize: 1024,
	WriteBufferSize: 1024,
	HandshakeTimeout: 5 * time.Second,
	CheckOrigin: func(r *http.Request) bool {
		return true
	},
}

func (c *MyWebSocketController) Get() {
	ws, err := upgrader.Upgrade(c.Ctx.ResponseWriter, c.Ctx.Request, nil)
	if err != nil {
		log.Fatal(err)
	}
	socket.Clients.Set(ws, true)
	_, body, _ := ws.ReadMessage()
	msg := socket.Message{Message: string(body)}
	socket.Broadcast <- msg
}

消息处理及转发

var (
	Clients = make(map[*websocket.Conn]bool, 1024)
	Broadcast = make(chan Message, 1024)
)

type Message struct {
	Message string `json:"message"`
}

func init() {
	go handleMessages()
}

//广播发送至页面
func handleMessages() {
	for {
		msg := <-Broadcast
		for client := range Clients {
			err := client.WriteJSON(msg)
			if err != nil {
				client.Close()
				delete(Clients, client)
			}
		}
	}
}

路由注册(采用beego的注解式路由无法完成协议转换,具体原因还未找到

beego.Router("/ws", ¬iceMq.MyWebSocketController{})

go client

采用golang自带的golang.org/x/net/websocket包发送消息

package websocket

import (
"net/url"
"github.com/astaxie/beego"
"golang.org/x/net/websocket"
)

type Client struct {
	Host string
	Path string

}

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

func (this *Client) SendMessage(body []byte) error {
	u := url.URL{Scheme: "ws", Host: this.Host, Path: this.Path}
	ws, err := websocket.Dial(u.String(), "", "http://"+this.Host+"/")
	defer ws.Close() //关闭连接
	if err != nil {
		beego.Error(err)
		return err
	}
	_, err = ws.Write(body)
	if err != nil {
		beego.Error(err)
		return err
	}
	return nil
}

js client

目前主流浏览器都支持WebSocket协议(包括IE 10+)




    
    Sample of websocket with golang
    
<>
    $(function() {
        var ws = new WebSocket('ws://api.mdevo.com/ws');
        ws.onopen = function(e) {
        $('
  • ').text("connected").appendTo($ul); } ws.onmessage = function(e) { $('
  • ').text(event.data).appendTo($ul); }; var $ul = $('#msg-list'); });
    • @转载,侵删。

      你可能感兴趣的:(golang 长连接web socket原理)