go 进阶 webSocket

本文介绍了WebSocket协议的特点和与HTTP、Socket的关系,展示了Golang中使用gorilla/websocket库创建WebSocket服务的基础示例,包括连接升级过程。同时,文章还讲解了如何构建一个GolangWebSocket代理,用于转发请求到目标WebSocket服务。
摘要由CSDN通过智能技术生成

一. 复习 webSocket

为什么要用 webSocket

  1. 先看一下http通信时存在的问题
  1. http是一种无状态协议,服务端接收到请求后无法确认当前请求属于哪个客户端(基于用户token解决)
  2. http是一次请求,一次响应,并且每次请求和响应就携带有大量的header头,对于实时通讯来说,解析请求头也是需要一定的时间,因此效率也更低下
  3. 最重要的是http是客户端主动发送请求,服务端被接收请求,一次请求,一次响应,服务端无法实现主动发送
  1. 落到业务上的问题: 假设当前需要获取后端某个数据的变化,实时性数据解决方案
  1. Polling轮询: 通过定时任务重复的向服务端发送新请求, 缺点:通过定时任务去跑数据可能会出现延迟,任务多次调用接口造成资源浪费
  2. Long-polling长轮询: 在长轮询中,客户端发送一个请求到服务端,如果服务端没有新的数据更动,本次连接将会被保持,直到等待到更新后的数据,返回给客户端并关闭这个连接, 在Servlet3中提供了异步任务,Spring提供了DeferedResult
  3. SSE(Server-Sent Events)服务器推模式: 基于HTTP长连接,类似于长轮询但是它在每一次的连接中,不只等待一次数据的获取,客户端发送一个请求到服务端,服务端保持这个请求直到一个新的消息准备好,将消息返回至客户端,一直通过这个链接返回后续的更新数据.SSE的一大特色就是重复利用一个连接来处理每一个消息(又称event)
  4. WebSocket方式: WebSocket不同于以上的这些技术,因为它提供了一个真正意义上的双向连接

webSocket 基础介绍

  1. WebSocket是HTML5下基于tcp实现的一种全双工通信持久化协议,并不代表一定要用在 HTML 中才能使用,不同语言针对webSocket都提供了支持
  2. (此处说的持久化是相对于HTTP这种非持久的协议来说)
  1. HTTP的生命周期通过Request来界定,一个请求中存在一个Request,一个Response
  2. 在HTTP1.1中进行了改进,加入了keep-alive,在一个HTTP连接中,可以发送多个Request,接收多个Response
  3. 但是一个Request 还是对应一个Response,而且这个response也是被动的,不能主动发起

1. webSocket 特点

  1. 真正的全双工方式,建立连接后客户端与服务器端是完全平等的,可以互相主动请求
  2. HTTP长连接中,每次数据交换除了真正的数据部分外,还会携带 header,信息交换效率很低,Websocket协议通过第一个request建立了TCP连接之后,之后交换的数据都不需要发送 header就能交换数据,
  3. 此外还有 multiplexing、不同的URL可以复用同一个WebSocket连接等功能

2. webSocket的连接-握手过程

  1. 浏览器和服务端利用HTTP协议通过三次握手来建立TCP连接
  1. 浏览器发起一个http建立连接请求,请求地址以ws://开头,请求头中包含Upgrade: websocket和Connection: Upgrade表示这个连接将要被转换为WebSocket 连接(WebSocket-Version 版本号)
  2. 服务器收到客户端的握手请求后,同样采用HTTP协议回馈数据,响应头HTTP/1.1 101 Switching Protocols和Upgrade: websocket表示本次连接的 HTTP 协议即将被更改(代码 101)改为指定的 webSocket 协议,
  3. 服务端响应 HTTP 握手,返回 code 101表示连接升级为webSocket
  4. 客户端收到了连接成功的消息后,通过TCP通道进行传输通信,连接会持续存在,server 和 client 都可单方面断开连接
    在这里插入图片描述
  1. webSocket 数据传输,数据包协议
    在这里插入图片描述
  2. webSocket中Connection Header头意义: 用来标记请求发起方与第一代理的连接状态是否关闭网络
  1. Connection: keep-alive 不关闭
  2. Connection: close 关闭
  3. Connection: Upgrade 协议升级

3. webSocket与Socket的关系

  1. Socket是传输控制层协议,webSocket是应用层协议
  2. Socket是为了方便使用TCP或UDP而抽象出来位于应用层和传输控制层之间的一组接口一层,是一种抽象
  3. webSocket是一个典型的应用层协议,也可以看为时Socket的一种落地实现

4. websocket与http的关系

  1. 相同点:
  1. 都是基于tcp的,都是可靠性传输协议
  2. 都是应用层协议
  1. 不同点:
  1. webSocket是双向通信协议,模拟Socket协议,可以双向发送或接受信息, HTTP是单向的
  2. WebSocket是需要浏览器和服务器握手进行建立连接的
  3. 而http是浏览器发起向服务器的连接,服务器预先并不知道这个连接
  1. 两者的关系: webSocket在建立握手时,数据是通过HTTP传输的,但是建立之后,在真正传输时候是不需要HTTP协议的

二. golang webSocket 基础示例

  1. 示例流程描述
  1. 创建webSocket客户端,也就是提供一个支持webSocket访问的html页面对应下方的home()
  2. 提供一个基于webSocket通信的路由函数,也就是echo()
  3. 构建服务地址,在该地址上main方法开启监听启动服务
  4. 启动服务后,先基于http访问home(),也就是请求"http://localhost:2003/",跳入支持webSocket的html页面
  5. 点击页面中的open按钮开启webSocket, 点击页面中的send按钮,基于webSocket访问"ws://localhost:2003/echo"
package main

import (
	"flag"
	"html/template"
	"log"
	"net/http"
	"github.com/gorilla/websocket"
)

// 1.构建服务器地址
// 通过golang标准库flag构建的,string()函数中
// 参数1:表示key
// 参数2:表示默认的value值
// 参数3:表示描述
var addr = flag.String("addr", "localhost:2003", "service address")

// 2.main方法启动服务
func main() {
	//上方通过flag构建了服务地址,此处通过Parse()对命令行参数进行解析
	flag.Parse()
	log.SetFlags(0)
	//2.1注册一个http路由函数,
	//在home()中会返回一个支持了webSocket的页面
	//点击页面中的open按钮,打开webSocket连接
	//点击send按钮,通过webSocket访问下方的/echo
	http.HandleFunc("/", home)
	//2.2注册webSocket路由函数echo()
	http.HandleFunc("/echo", echo)

	log.Println("Starting websocket server at " + *addr)

	//2.2监听指定地址启动服务
	log.Fatal(http.ListenAndServe(*addr, nil))
}

// 3.http的路由函数,该函数中通过Template嵌套支持了webSocket的html页面,意思是创建一个webSocket客户端
// 当启动服务,访问该接口函数时"http://localhost:2003/",会返回一下支持了websocket的页面,
// 页面中提供了一个open按钮,打开webSocket连接,
// 提供了send按钮,点击按钮,会方法页面中的"ws://"+r.Host+"/echo"这个地址
// 也就是基于webSocket访问上方提供的echo(),实现webSocket通信
func home(w http.ResponseWriter, r *http.Request) {
	homeTemplate.Execute(w, "ws://"+r.Host+"/echo")
}

// 支持webSocket的html页面
var homeTemplate = template.Must(template.New("").Parse(`
<!DOCTYPE html>
<head>
<meta charset="utf-8">
<script>  
window.addEventListener("load", function(evt) {

    var output = document.getElementById("output");
    var input = document.getElementById("input");
    var ws;

    var print = function(message) {
        var d = document.createElement("div");
        d.innerHTML = message;
        output.appendChild(d);
    };

    document.getElementById("open").onclick = function(evt) {
        if (ws) {
            return false;
        }
		var web_url=document.getElementById("web_url").value
        ws = new WebSocket(web_url);
        ws.onopen = function(evt) {
            print("OPEN");
        }
        ws.onclose = function(evt) {
            print("CLOSE");
            ws = null;
        }
        ws.onmessage = function(evt) {
            print("RESPONSE: " + evt.data);
        }
        ws.onerror = function(evt) {
            print("ERROR: " + evt.data);
        }
        return false;
    };

    document.getElementById("send").onclick = function(evt) {
        if (!ws) {
            return false;
        }
        print("SEND: " + input.value);
        ws.send(input.value);
        return false;
    };

    document.getElementById("close").onclick = function(evt) {
        if (!ws) {
            return false;
        }
        ws.close();
        return false;
    };

});
</script>
</head>
<body>
<table>
<tr><td valign="top" width="50%">
<p>Click "Open" to create a connection to the server, 
"Send" to send a message to the server and "Close" to close the connection. 
You can change the message and send multiple times.
<p>
<form>
<button id="open">Open</button>
<button id="close">Close</button>
<p><input id="web_url" type="text" value="{{.}}">
<p><input id="input" type="text" value="Hello world!">
<button id="send">Send</button>
</form>
</td><td valign="top" width="50%">
<div id="output"></div>
</td></tr></table>
</body>
</html>
`))

//======4.接收webSocket协议请求=========

// 4.1初始化用来获取连接的Upgrader
var upgrader = websocket.Upgrader{} // use default options
//或使用下方的upgrader 
/*var upgrader = websocket.Upgrader{
	ReadBufferSize:  1024,
	WriteBufferSize: 1024,
	//如果需要跨域的话添加该配置
	CheckOrigin: func(r *http.Request) bool {
		return true
	},
}*/

// 4.2根据 func(http.ResponseWriter,  *http.Request)定义接收请求函数
// 通过http.Request获取数据
// 通过http.ResponseWriter写出数据
func echo(w http.ResponseWriter, r *http.Request) {
	//1.获取连接
	c, err := upgrader.Upgrade(w, r, nil)
	if err != nil {
		log.Print("upgrade:", err)
		return
	}
	//3.关闭
	defer c.Close()

	//2.循环读取或写出数据
	for {
		//2.1读取数据
		mt, message, err := c.ReadMessage()
		if err != nil {
			log.Println("read:", err)
			break
		}
		log.Printf("recv: %s", message)
		//2.2写出数据
		err = c.WriteMessage(mt, message)
		if err != nil {
			log.Println("write:", err)
			break
		}
	}
}

深入理解upgrader.Upgrade

  1. 在上方基于webSocket通信时提供了一个echo(),在echo()函数中调用了"c, err := upgrader.Upgrade(w, r, nil)"获取连接
func (u *Upgrader) Upgrade(w http.ResponseWriter, r *http.Request, responseHeader http.Header)
  1. Upgrader使用的是"github.com/gorilla/websocket"开源类库的,需要get这个包下来
  2. Upgrade()方法内部
  1. 获取Sec-Websocket-Key
  2. 通过sha1生成Sec-WebSocket-Accept
  3. 向客户端发送101状态码,webSocket连接建立成功
  1. 源码
func (u *Upgrader) Upgrade(w http.ResponseWriter, r *http.Request, responseHeader http.Header) (*Conn, error) {
	const badHandshake = "websocket: the client is not using the websocket protocol: "
	//1.校验是否携带了表示升级为webSocket的请求头
	if !tokenListContainsValue(r.Header, "Connection", "upgrade") {
		return u.returnError(w, r, http.StatusBadRequest, badHandshake+"'upgrade' token not found in 'Connection' header")
	}

	if !tokenListContainsValue(r.Header, "Upgrade", "websocket") {
		return u.returnError(w, r, http.StatusBadRequest, badHandshake+"'websocket' token not found in 'Upgrade' header")
	}
	
	//2.校验是否是get请求
	if r.Method != http.MethodGet {
		return u.returnError(w, r, http.StatusMethodNotAllowed, badHandshake+"request method is not GET")
	}
	//3.校验是否携带了Websocket版本号等信息
	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")
	}

	if _, ok := responseHeader["Sec-Websocket-Extensions"]; ok {
		return u.returnError(w, r, http.StatusInternalServerError, "websocket: application specific 'Sec-WebSocket-Extensions' headers are unsupported")
	}

	checkOrigin := u.CheckOrigin
	if checkOrigin == nil {
		checkOrigin = checkSameOrigin
	}
	if !checkOrigin(r) {
		return u.returnError(w, r, http.StatusForbidden, "websocket: request origin not allowed by Upgrader.CheckOrigin")
	}
	//4.获取Sec-Websocket-Key
	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
		}
	}

	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
	//5.获取连接
	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")
	}

	var br *bufio.Reader
	if u.ReadBufferSize == 0 && bufioReaderSize(netConn, brw.Reader) > 256 {
		// Reuse hijacked buffered reader as connection reader.
		br = brw.Reader
	}

	buf := bufioWriterBuffer(netConn, brw.Writer)

	var writeBuf []byte
	if u.WriteBufferPool == nil && u.WriteBufferSize == 0 && len(buf) >= maxFrameHeaderSize+256 {
		// Reuse hijacked write buffer as connection buffer.
		writeBuf = buf
	}
	
	//6.对拿到的连接进行封装,封装为实现了读数据和写数据方法的Conn结构
	c := newConn(netConn, true, u.ReadBufferSize, u.WriteBufferSize, u.WriteBufferPool, br, writeBuf)
	c.subprotocol = subprotocol

	if compress {
		c.newCompressionWriter = compressNoContextTakeover
		c.newDecompressionReader = decompressNoContextTakeover
	}

	// Use larger of hijacked buffer and connection write buffer for header.
	p := buf
	if len(c.writeBuf) > len(p) {
		p = c.writeBuf
	}
	p = p[:0]
	//7.构建webSocket响应标识,例如101状态码等等
	p = append(p, "HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: "...)
	p = append(p, computeAcceptKey(challengeKey)...)
	p = append(p, "\r\n"...)
	if c.subprotocol != "" {
		p = append(p, "Sec-WebSocket-Protocol: "...)
		p = append(p, c.subprotocol...)
		p = append(p, "\r\n"...)
	}
	if compress {
		p = append(p, "Sec-WebSocket-Extensions: permessage-deflate; server_no_context_takeover; client_no_context_takeover\r\n"...)
	}
	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, "\r\n"...)
		}
	}
	p = append(p, "\r\n"...)

	// Clear deadlines set by HTTP server.
	netConn.SetDeadline(time.Time{})

	if u.HandshakeTimeout > 0 {
		netConn.SetWriteDeadline(time.Now().Add(u.HandshakeTimeout))
	}
	//8.将webSocket标识响应写出
	if _, err = netConn.Write(p); err != nil {
		netConn.Close()
		return nil, err
	}
	if u.HandshakeTimeout > 0 {
		netConn.SetWriteDeadline(time.Time{})
	}
	//9.返回封装的Conn
	return c, nil
}

三. golang webSocket 代理示例

  1. 请求,该服务接收到请求后,将请求代理转发到,流程源码
  1. 创建连接池
  2. 编写代理函数,返回httputil.ReverseProxy,通过httputil.ReverseProxy实现代理,在代理函数中获取目标服务地址,封装协调者director, 封装modifyFunc ,封装异常处理函数
  3. 目标服务地址"http://127.0.0.1:2003"是一个webSocket服务,也就是上面的webSocket示例("/“返回一个支持webSocket的html, 请求”/echo"基于webSoket通信)
  4. 返回 httputil.ReverseProxy
import (
	"bytes"
	"compress/gzip"
	"fmt"
	"io/ioutil"
	"log"
	"net"
	"net/http"
	"net/http/httputil"
	"net/url"
	"strconv"
	"strings"
	"time"
)

// 1.初始化连接池
var transport = &http.Transport{
	DialContext: (&net.Dialer{
		Timeout:   30 * time.Second, //连接超时
		KeepAlive: 30 * time.Second, //长连接超时时间
	}).DialContext,
	MaxIdleConns:          100,              //最大空闲连接
	IdleConnTimeout:       90 * time.Second, //空闲超时时间
	TLSHandshakeTimeout:   10 * time.Second, //tls握手超时时间
	ExpectContinueTimeout: 1 * time.Second,  //100-continue超时时间
}

// 2.编写代理函数,返回httputil.ReverseProxy,通过httputil.ReverseProxy实现代理转发
func NewLoadBalanceReverseProxy2() *httputil.ReverseProxy {
	//1.请求协调者
	director := func(req *http.Request) {
		//获取目标地址
		nextAddr := "http://127.0.0.1:2003"
		target, err := url.Parse(nextAddr)
		if err != nil {
			log.Fatal(err)
		}
		targetQuery := target.RawQuery
		req.URL.Scheme = target.Scheme
		req.URL.Host = target.Host
		req.URL.Path = singleJoiningSlash(target.Path, req.URL.Path)
		req.Host = target.Host
		if targetQuery == "" || req.URL.RawQuery == "" {
			req.URL.RawQuery = targetQuery + req.URL.RawQuery
		} else {
			req.URL.RawQuery = targetQuery + "&" + req.URL.RawQuery
		}
		if _, ok := req.Header["User-Agent"]; !ok {
			req.Header.Set("User-Agent", "user-agent")
		}
	}

	//更改内容
	modifyFunc := func(resp *http.Response) error {
		//todo 兼容websocket
		if strings.Contains(resp.Header.Get("Connection"), "Upgrade") {
			return nil
		}
		var payload []byte
		var readErr error

		//todo 兼容gzip压缩
		if strings.Contains(resp.Header.Get("Content-Encoding"), "gzip") {
			gr, err := gzip.NewReader(resp.Body)
			if err != nil {
				return err
			}
			payload, readErr = ioutil.ReadAll(gr)
			resp.Header.Del("Content-Encoding")
		} else {
			payload, readErr = ioutil.ReadAll(resp.Body)
		}
		if readErr != nil {
			return readErr
		}

		//异常请求时设置StatusCode
		if resp.StatusCode != 200 {
			payload = []byte("StatusCode error:" + string(payload))
		}

		//todo 因为预读了数据所以内容重新回写
		resp.Body = ioutil.NopCloser(bytes.NewBuffer(payload))
		resp.ContentLength = int64(len(payload))
		resp.Header.Set("Content-Length", strconv.FormatInt(int64(len(payload)), 10))
		return nil
	}

	//错误回调 :关闭real_server时测试,错误回调
	//范围:transport.RoundTrip发生的错误、以及ModifyResponse发生的错误
	errFunc := func(w http.ResponseWriter, r *http.Request, err error) {
		//todo record error log
		fmt.Println(err)
	}

	return &httputil.ReverseProxy{Director: director, Transport: transport, ModifyResponse: modifyFunc, ErrorHandler: errFunc}
}

func singleJoiningSlash(a, b string) string {
	aslash := strings.HasSuffix(a, "/")
	bslash := strings.HasPrefix(b, "/")
	switch {
	case aslash && bslash:
		return a + b[1:]
	case !aslash && !bslash:
		return a + "/" + b
	}
	return a + b
}
  1. main方法监听指定地址启动服务,该服务的所有接口都会通过httputil.ReverseProxy代理执行
func main() {
	//1.获取*httputil.ReverseProxy
	proxy := NewLoadBalanceReverseProxy2()
	//将*httputil.ReverseProxy代理函数绑定到域名上,请求该域名时都会被*httputil.ReverseProxy代理
	log.Fatal(http.ListenAndServe("127.0.0.1:2002", proxy))
}
  1. httputil.ReverseProxy代理工具对http,webSoket都进行了兼容处理,运行测试时,先访问"http://localhost:2002/",被代理到"http://localhost:2003/“返回支持webSocket通信的html页面, 点击页面中的open开启webSocket通信,点击send按钮请求"ws://127.0.0.1:2002/echo”,被代理到"ws://127.0.0.1:2003/echo"开始基于webSocket进行通信
    在这里插入图片描述
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值