【微服务网关——Websocket代理】

1.Websocket协议与原理

1.1 连接建立协议

1.1.1 客户端发起连接请求

客户端通过 HTTP 请求发起 WebSocket 连接。以下是一个 WebSocket 握手请求的例子:

json 复制代码
GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==
Sec-WebSocket-Version: 13
  • GET /chat HTTP/1.1:请求使用 GET 方法。
  • Host: server.example.com:请求目标主机。
  • Upgrade: websocket:表明希望升级到 WebSocket 协议。
  • Connection: Upgrade:指示当前的连接请求升级。
  • Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==:一个 Base64 编码的随机密钥,用于服务器响应中进行确认。
  • Sec-WebSocket-Version: 13:表明 WebSocket 的版本(13 是最新的版本)。

1.1.2 服务器响应

服务器接收到请求后,如果同意升级协议,将发送一个 HTTP 响应来确认连接升级:

json 复制代码
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: HSmrc0sMlYUkAGmm5OPpG2HaGWk=
  • HTTP/1.1 101 Switching Protocols:表示协议切换成功。
  • Upgrade: websocket:确认升级到 WebSocket。
  • Connection: Upgrade:确认连接升级。
  • Sec-WebSocket-Accept: HSmrc0sMlYUkAGmm5OPpG2HaGWk=:服务器根据客户端的 Sec-WebSocket-Key 生成的一个响应密钥。

1.1.3 连接建立

一旦服务器发回 101 状态码,WebSocket 连接便建立成功。之后,客户端和服务器可以在此连接上相互发送和接收数据帧,而不需要再使用 HTTP 协议的头部。

  • 握手过程:WebSocket 握手基于 HTTP,并包含 Upgrade 和 Connection 头部来请求和确认协议的升级。
  • Sec-WebSocket-Key:客户端生成的随机密钥,服务器用于验证连接的合法性。
  • Sec-WebSocket-Accept:服务器生成的响应密钥,基于客户端提供的 Sec-WebSocket-Key 和固定 GUID 的 SHA-1 和 Base64 编码。

1.2 数据传输协议

  • FIN (1 bit): 表示是否为消息的最后一个帧。1 表示是最后一个帧。
  • RSV1, RSV2, RSV3 (1 bit each): 保留位,通常设置为 0,除非在扩展中有定义。
  • Opcode (4 bits): 表示帧的类型:
    • 0x0: 延续帧(continuation frame)
    • 0x1: 文本帧(text frame)
    • 0x2: 二进制帧(binary frame)
    • 0x8: 连接关闭(connection close)
    • 0x9: Ping
    • 0xA: Pong
  • Mask (1 bit): 表示是否应用掩码。客户端发送的帧必须设置为 1,服务器发送的帧必须设置为 0。
  • Payload Length (7 bits or 7+16/64 bits): 数据载荷的长度:
    • 如果值在 0 到 125 之间,则为数据载荷的实际长度。
    • 126: 后接 16 位整数表示长度。
    • 127: 后接 64 位整数表示长度。
  • Masking-Key (0 or 4 bytes): 掩码键,存在于客户端发送的帧中,用于解码数据载荷。
  • Payload Data (x bytes): 实际的应用数据。

1.3 Websocket原理

掩码: 确保客户端发送的数据经过掩码处理,防止恶意数据影响。

控制帧: 必须尽快处理控制帧,避免它们与数据帧混合,导致错误。

Websocket补充-Connection Header头意义

  • 标记请求发起方与第一代理的状态
  • 决定当前事务完成后,是否会关闭代理
    • Connection:keep-alive 不关闭网络
    • Connection:close 关闭网络
    • Connection:Upgrade 协议升级

2.Websocket代理实战

2.1 基于go构建Websocket测试服务器和客户端

这是一个前后端一体代码:

go 复制代码
package main

import (
	"flag"
	"html/template"
	"log"
	"net/http"

	"github.com/gorilla/websocket"
)

var addr = flag.String("addr", "localhost:2003", "http service address")

var upgrader = websocket.Upgrader{} // use default options

func echo(w http.ResponseWriter, r *http.Request) {
	c, err := upgrader.Upgrade(w, r, nil)
	if err != nil {
		log.Print("upgrade:", err)
		return
	}
	defer c.Close()
	for {
		mt, message, err := c.ReadMessage()
		if err != nil {
			log.Println("read:", err)
			break
		}
		log.Printf("recv: %s", message)
		err = c.WriteMessage(mt, message)
		if err != nil {
			log.Println("write:", err)
			break
		}
	}
}

func home(w http.ResponseWriter, r *http.Request) {
	homeTemplate.Execute(w, "ws://"+r.Host+"/echo")
}

func main() {
	flag.Parse()
	log.SetFlags(0)
	http.HandleFunc("/echo", echo)
	http.HandleFunc("/", home)
	log.Println("Starting websocket server at " + *addr)
	log.Fatal(http.ListenAndServe(*addr, nil))
}

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

运行后访问:http://localhost:2003/

Open 尝试与服务器建立连接

Send 基于Websocket向服务器发送了链接

Close关闭连接

2.2 深入理解upgrader.Upgrade

  • 获取Sec-Websocket-Key
  • sha1生成Sec-WebSocket-Accept
  • 向客户端发送101status
go 复制代码
// Upgrade 升级 HTTP 服务器连接到 WebSocket 协议。
// responseHeader 包含在响应中,以回应客户端的升级请求。
// 使用 responseHeader 指定 cookies (Set-Cookie) 和应用程序协商的子协议 (Sec-WebSocket-Protocol)。
// 如果升级失败,Upgrade 会用 HTTP 错误响应来回复客户端。
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: "

    // 检查 "Connection" 头部是否包含 "upgrade" 令牌
	if !tokenListContainsValue(r.Header, "Connection", "upgrade") {
		return u.returnError(w, r, http.StatusBadRequest, badHandshake+"'upgrade' token not found in 'Connection' header")
	}

    // 检查 "Upgrade" 头部是否包含 "websocket" 令牌
	if !tokenListContainsValue(r.Header, "Upgrade", "websocket") {
		return u.returnError(w, r, http.StatusBadRequest, badHandshake+"'websocket' token not found in 'Upgrade' header")
	}

    // 检查请求方法是否为 GET
	if r.Method != "GET" {
		return u.returnError(w, r, http.StatusMethodNotAllowed, badHandshake+"request method is not GET")
	}

    // 检查 "Sec-Websocket-Version" 头部是否包含版本号 "13"
	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")
	}

    // 检查 responseHeader 中是否包含 "Sec-Websocket-Extensions",不支持应用程序特定的扩展头
	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")
	}

    // 获取 "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)

    // PMCE (Per-Message Compression Extensions) 协商
	var compress bool
	if u.EnableCompression {
		for _, ext := range parseExtensions(r.Header) {
			if ext[""] != "permessage-deflate" {
				continue
			}
			compress = true
			break
		}
	}

    // 检查响应是否实现了 http.Hijacker 接口
	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
	// 通过劫持获得net/IO流
	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 {
		br = brw.Reader
	}

    // 获取缓冲写入器
	buf := bufioWriterBuffer(netConn, brw.Writer)

    // 根据情况重用缓冲写入器作为连接缓冲
	var writeBuf []byte
	if u.WriteBufferPool == nil && u.WriteBufferSize == 0 && len(buf) >= maxFrameHeaderSize+256 {
		writeBuf = buf
	}

    // 创建新的 WebSocket 连接
	c := newConn(netConn, true, u.ReadBufferSize, u.WriteBufferSize, u.WriteBufferPool, br, writeBuf)
	c.subprotocol = subprotocol

    // 设置压缩/解压缩功能
	if compress {
		c.newCompressionWriter = compressNoContextTakeover
		c.newDecompressionReader = decompressNoContextTakeover
	}

    // 使用较大的缓冲区写入响应头
	p := buf
	if len(c.writeBuf) > len(p) {
		p = c.writeBuf
	}
	p = p[:0]

    // 构建 WebSocket 握手响应
	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 {
					// 防止响应分割。
					b = ' '
				}
				p = append(p, b)
			}
			p = append(p, "\r\n"...)
		}
	}
	p = append(p, "\r\n"...)

    // 清除 HTTP 服务器设置的超时时间。
	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
}
  • 协议验证: 检查 HTTP 请求头部中的必要字段,确保请求是一个有效的 WebSocket 握手请求。
  • 请求合法性检查: 验证请求的方法、版本、来源和密钥等,以确保请求的合法性和安全性。
  • 协议协商: 根据客户端请求和服务器配置,选择合适的子协议和是否启用数据压缩。
  • 连接劫持: 使用 http.Hijacker 接口劫持 HTTP 连接,以便直接读取和写入网络数据。
  • 发送握手响应: 构建并发送 WebSocket 握手响应,确认协议升级成功。
  • 返回连接对象: 创建 WebSocket 连接对象,并返回给调用者,以便后续的数据传输。
相关推荐
qq_1715388513 小时前
利用Spring Cloud Gateway Predicate优化微服务路由策略
android·javascript·微服务
龙哥·三年风水15 小时前
workman服务端开发模式-应用开发-vue-element-admin封装websocket
分布式·websocket·vue
科技互联人生19 小时前
微服务常用的中间件及其用途
微服务·中间件·系统架构
小蜗牛慢慢爬行20 小时前
如何在 Spring Boot 微服务中设置和管理多个数据库
java·数据库·spring boot·后端·微服务·架构·hibernate
小扳1 天前
微服务篇-深入了解 MinIO 文件服务器(你还在使用阿里云 0SS 对象存储图片服务?教你使用 MinIO 文件服务器:实现从部署到具体使用)
java·服务器·分布式·微服务·云原生·架构
ZoeLandia1 天前
WebSocket | 背景 概念 原理 使用 优缺点及适用场景
网络·websocket·网络协议
zquwei1 天前
SpringCloudGateway+Nacos注册与转发Netty+WebSocket
java·网络·分布式·后端·websocket·网络协议·spring
DT辰白1 天前
如何解决基于 Redis 的网关鉴权导致的 RESTful API 拦截问题?
后端·微服务·架构
老猿讲编程2 天前
技术发展历程:从 CORBA 到微服务
微服务·云原生·架构
carterslam2 天前
解决:websocket 1002 connection rejected 426upgrade required
网络·websocket·网络协议