一、什么是websocket
Websocket是一个应用层协议,它必须依赖HTTP协议进行一次握手,握手成功后,数据直接从TCP通道传输,此时就与HTTP无关了。所以websocket分为握手和数据传输两个阶段。
1. 握手阶段
客户端发送消息:
GET ws://192.168.2.123:2021/ws HTTP/1.1
Host: 192.168.2.123:2021
Connection: Upgrade
Upgrade: websocket
Origin: http://echo.localhost.com
Sec-WebSocket-Version: 13
Sec-WebSocket-Key: z3HD6sns4+TSzfTr8NG56A==
-
Connection
告诉服务端对协议进行升级,具体升级内容取决于 Upgrade部分 -
Sec-WebSocket-Key
为了保证握手一致性,由客户端生成随机字符串并base64编码,发送给服务端 -
Sec-WebSocket-Version
协议版本,常用13 -
Upgrade
升级至websocket协议
服务端返回消息:
HTTP/1.1 101 Switching Protocols
Connection: Upgrade
Upgrade: websocket
Sec-Websocket-Accept: dV84ft1FH/yq3Obi5LnPAUBLaas=
-
状态码101
代表协议升级成功 -
Connection
和Upgrade
内容代表协议成功升级为websocket -
Sec-WebSocket-Version
代表协议版本号,常用13 - Sec-WebSocket-Accept计算方法伪代码如下:
base64(sha1(Sec-WebSocket-Key + 258EAFA5-E914-47DA-95CA-C5AB0DC85B11))
其中Sec-WebSocket-Key为客户端传入,258EAFA5-E914-47DA-95CA-C5AB0DC85B11为固定值。
2. 传输阶段
Websocket的数据传输是frame形式传输的,比如会将一条消息分为几个frame,按照先后顺序传输出去。
websocket传输使用的协议如下图:
参数说明如下:
FIN:1位,用来表明这是一个消息的最后的消息片断,当然第一个消息片断也可能是最后的一个消息片断;
RSV1, RSV2, RSV3: 分别都是1位,如果双方之间没有约定自定义协议,那么这几位的值都必须为0,否则必须断掉websocket连接;
-
Opcode: 4位操作码,定义有效负载数据,如果收到了一个未知的操作码,连接也必须断掉,以下是定义的操作码:
%x0 表示连续消息片断 %x1 表示文本消息片断 %x2 表未二进制消息片断 %x3-7 为将来的非控制消息片断保留的操作码 %x8 表示连接关闭 %x9 表示心跳检查的ping %xA 表示心跳检查的pong %xB-F 为将来的控制消息片断的保留操作码
Mask: 1位,定义传输的数据是否有加掩码,如果设置为1,掩码键必须放在masking-key区域,客户端发送给服务端的所有消息,此位的值都是1;
Payload length: 传输数据的长度,以字节的形式表示:7位、7+16位、或者7+64位。如果这个值以字节表示是0-125这个范围,那这个值就表示传输数据的长度;如果这个值是126,则随后的两个字节表示的是一个16进制无符号数,用来表示传输数据的长度;如果这个值是127,则随后的是8个字节表示的一个64位无符合数,这个数用来表示传输数据的长度。多字节长度的数量是以网络字节的顺序表示。负载数据的长度为扩展数据及应用数据之和,扩展数据的长度可能为0,因而此时负载数据的长度就为应用数据的长度;
Masking-key: 0或4个字节,客户端发送给服务端的数据,都是通过内嵌的一个32位值作为掩码的;掩码键只有在掩码位设置为1的时候存在;
Payload data: (x+y)位,负载数据为扩展数据及应用数据长度之和;
Extension data: x位,如果客户端与服务端之间没有特殊约定,那么扩展数据的长度始终为0,任何的扩展都必须指定扩展数据的长度,或者长度的计算方式,以及在握手时如何确定正确的握手方式。如果存在扩展数据,则扩展数据就会包括在负载数据的长度之内;
Application data: y位,任意的应用数据,放在扩展数据之后,应用数据的长度=负载数据的长度-扩展数据的长度。
二、golang实现websocket简单案例
package wss
import (
"crypto/sha1"
"encoding/base64"
"encoding/binary"
"errors"
"fmt"
"log"
"math"
"net"
"net/http"
"net/textproto"
"strings"
)
type WsSocket struct {
MaskingKey []byte
Conn net.Conn
}
func NewWsSocket(conn net.Conn) *WsSocket {
return &WsSocket{Conn: conn}
}
// 读取数据帧
func (ws *WsSocket) ReadIframe() (data []byte, opcode byte, err error) {
err = nil
// 第一个字节:FIN + RSV1-3 + OPCODE
opcodeByte := make([]byte, 1)
ws.Conn.Read(opcodeByte)
fin := opcodeByte[0] >> 7
rsv1 := opcodeByte[0] >> 6 & 1
rsv2 := opcodeByte[0] >> 5 & 1
rsv3 := opcodeByte[0] >> 4 & 1
opcode = opcodeByte[0] & 15 // 取出后四bit位
log.Println(fin, rsv1, rsv2, rsv3, opcode)
log.Println("opcode:", opcode)
payloadLenByte := make([]byte, 1)
ws.Conn.Read(payloadLenByte)
// 取出mask位标识: 掩码, 定义payload数据是否进行了掩码处理,如果是1表示进行了掩码处理
mask := payloadLenByte[0] >> 7
payloadLen := int(payloadLenByte[0] & 0x7F) // 0111 1111
if payloadLen == 126 {
// 读取两个字节
extendedByte := make([]byte, 2)
ws.Conn.Read(extendedByte)
// 重置payloadLen,采用大端字节序
payloadLen = int(binary.BigEndian.Uint16(extendedByte))
}
if payloadLen == 127 {
// 读取8个字节
extendedByte := make([]byte, 8)
ws.Conn.Read(extendedByte)
// 重置payloadLen,采用大端字节序
payloadLen = int(binary.BigEndian.Uint64(extendedByte))
}
// 掩码键
maskingByte := make([]byte, 4)
if mask == 1 {
ws.Conn.Read(maskingByte)
ws.MaskingKey = maskingByte
}
payloadDataByte := make([]byte, payloadLen)
ws.Conn.Read(payloadDataByte)
log.Println("data:", payloadDataByte)
dataByte := make([]byte, payloadLen)
for i := 0; i < payloadLen; i++ {
if mask == 1 {
dataByte[i] = payloadDataByte[i] ^ maskingByte[i%4]
} else {
dataByte[i] = payloadDataByte[i]
}
}
if fin == 1 {
data = dataByte
return
}
// 递归读取数据
nextData, opcode, err := ws.ReadIframe()
if err != nil {
return
}
data = append(data, nextData...)
return
}
// websocket:发送数据帧(简单版)
func (ws *WsSocket) SendIframe(data []byte) error {
length := len(data)
if length <= 0 {
return errors.New("data cannot be empty")
}
//注意: 服务端发送数据,不使用掩码操作
ws.Conn.Write([]byte{0x81}) // 1000 0001 : 前段部分表示FIN、RSV1-3,后半部分表示opcode:0X1(文本数据帧)
switch {
case length <= 125:
var payLenByte byte
payLenByte = byte(0) | byte(length) //mask + payloadLength: mask位设置为0
ws.Conn.Write([]byte{payLenByte})
case length <= math.MaxUint16:
// 处理126的情况
ws.Conn.Write([]byte{0x7e}) // 01111110: mask + payloadLength,mask设置为0,payloadLength为126
// 2个字节
buf := make([]byte, 2)
binary.BigEndian.PutUint16(buf, uint16(length)) // 采用大端字节序
ws.Conn.Write(buf)
default:
// 处理127的情况
ws.Conn.Write([]byte{0x7f}) // 01111111: mask + payloadLength,mask设置为0,payloadLength为127
// 8个字节
buf := make([]byte, 8)
binary.BigEndian.PutUint64(buf, uint64(length)) // 采用大端字节序
ws.Conn.Write(buf)
}
// 发送数据
ws.Conn.Write(data)
return nil
}
// 升级协议
func Upgrade(w http.ResponseWriter, r *http.Request) *WsSocket {
errCode, err := verifyClientRequest(w, r)
if err != nil {
http.Error(w, err.Error(), errCode)
return nil
}
// 劫持http,获取底层TCP连接
hj, ok := w.(http.Hijacker)
if !ok {
err = errors.New("http.ResponseWriter does not implement http.Hijacker")
http.Error(w, http.StatusText(http.StatusNotImplemented), http.StatusNotImplemented)
return nil
}
w.Header().Set("Upgrade", "websocket")
w.Header().Set("Connection", "Upgrade")
key := r.Header.Get("Sec-WebSocket-Key")
w.Header().Set("Sec-WebSocket-Accept", secWebSocketAccept(key))
w.WriteHeader(http.StatusSwitchingProtocols)
netConn, _, err := hj.Hijack()
if err != nil {
err = fmt.Errorf("failed to hijack connection: %w", err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return nil
}
ws := NewWsSocket(netConn)
return ws
}
// 验证请求header
func verifyClientRequest(w http.ResponseWriter, r *http.Request) (errCode int, _ error) {
if !r.ProtoAtLeast(1, 1) {
return http.StatusUpgradeRequired, fmt.Errorf("WebSocket protocol violation: handshake request must be at least HTTP/1.1: %q", r.Proto)
}
if !headerContainsToken(r.Header, "Connection", "Upgrade") {
w.Header().Set("Connection", "Upgrade")
w.Header().Set("Upgrade", "websocket")
return http.StatusUpgradeRequired, fmt.Errorf("WebSocket protocol violation: Connection header %q does not contain Upgrade", r.Header.Get("Connection"))
}
if !headerContainsToken(r.Header, "Upgrade", "websocket") {
w.Header().Set("Connection", "Upgrade")
w.Header().Set("Upgrade", "websocket")
return http.StatusUpgradeRequired, fmt.Errorf("WebSocket protocol violation: Upgrade header %q does not contain websocket", r.Header.Get("Upgrade"))
}
if r.Method != "GET" {
return http.StatusMethodNotAllowed, fmt.Errorf("WebSocket protocol violation: handshake request method is not GET but %q", r.Method)
}
if r.Header.Get("Sec-WebSocket-Version") != "13" {
w.Header().Set("Sec-WebSocket-Version", "13")
return http.StatusBadRequest, fmt.Errorf("unsupported WebSocket protocol version (only 13 is supported): %q", r.Header.Get("Sec-WebSocket-Version"))
}
if r.Header.Get("Sec-WebSocket-Key") == "" {
return http.StatusBadRequest, errors.New("WebSocket protocol violation: missing Sec-WebSocket-Key")
}
return 0, nil
}
func headerContainsToken(h http.Header, key, token string) bool {
token = strings.ToLower(token)
for _, t := range headerTokens(h, key) {
if t == token {
return true
}
}
return false
}
func headerTokens(h http.Header, key string) []string {
key = textproto.CanonicalMIMEHeaderKey(key)
var tokens []string
for _, v := range h[key] {
v = strings.TrimSpace(v)
for _, t := range strings.Split(v, ",") {
t = strings.ToLower(t)
t = strings.TrimSpace(t)
tokens = append(tokens, t)
}
}
return tokens
}
var keyGUID = []byte("258EAFA5-E914-47DA-95CA-C5AB0DC85B11")
// 加密SecWebSocketKey
func secWebSocketAccept(secWebSocketKey string) string {
h := sha1.New()
h.Write([]byte(secWebSocketKey))
h.Write(keyGUID)
return base64.StdEncoding.EncodeToString(h.Sum(nil))
}
package main
import (
"library/wss"
"fmt"
"log"
"net/http"
)
func main() {
http.HandleFunc("/ws", func(w http.ResponseWriter, r *http.Request) {
ws := wss.Upgrade(w, r)
for {
data, opcode, _ := ws.ReadIframe()
fmt.Println("read data:", string(data))
fmt.Println("opcode:", opcode)
if opcode == 8 || len(data) == 0 {
ws.Conn.Write([]byte{0x8})
ws.Conn.Close()
break
}
err := ws.SendIframe(data)
if err != nil {
log.Println("sendIframe err:", err)
}
log.Println("send data")
}
})
log.Fatal(http.ListenAndServe(":2021", nil))
}
前端使用案例:(浏览器访问: http://echo.localhost.com/wss.html,域名根据实际情况自定义配置)
<!DOCTYPE html>
<title>WebSocket Echo Client</title>
<h2>Websocket Echo Client</h2>
<div id="output"></div>
<script>
function setup() {
output = document.getElementById("output");
// 建立websocket连接
ws = new WebSocket("ws://192.168.2.123:2021/ws");
// 监听打开连接
ws.onopen = function(e) {
log("Connected");
var msgObj = {content:"this is message"}
sendMessage(JSON.stringify(msgObj))
}
// 监听关闭连接
ws.onclose = function(e) {
log("Disconnected: " + e.code);
}
// 监听错误
ws.onerror = function(e) {
log("Error ");
}
// 监听消息
ws.onmessage = function(e) {
log("Message received: " + e.data);
// ws.close();
}
}
// 发送消息
function sendMessage(msg){
ws.send(msg);
log("Message sent");
}
function log(s) {
var p = document.createElement("p");
p.style.wordWrap = "break-word";
p.textContent = s;
output.appendChild(p);
console.log(s);
}
setup();
</script>
</html>