一、什么是websocket
Websocket是一個(gè)應(yīng)用層協(xié)議,它必須依賴HTTP協(xié)議進(jìn)行一次握手,握手成功后,數(shù)據(jù)直接從TCP通道傳輸,此時(shí)就與HTTP無關(guān)了。所以websocket分為握手和數(shù)據(jù)傳輸兩個(gè)階段。
1. 握手階段
客戶端發(fā)送消息:
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告訴服務(wù)端對協(xié)議進(jìn)行升級,具體升級內(nèi)容取決于 Upgrade部分 -
Sec-WebSocket-Key為了保證握手一致性,由客戶端生成隨機(jī)字符串并base64編碼,發(fā)送給服務(wù)端 -
Sec-WebSocket-Version協(xié)議版本,常用13 -
Upgrade升級至websocket協(xié)議
服務(wù)端返回消息:
HTTP/1.1 101 Switching Protocols
Connection: Upgrade
Upgrade: websocket
Sec-Websocket-Accept: dV84ft1FH/yq3Obi5LnPAUBLaas=
-
狀態(tài)碼101代表協(xié)議升級成功 -
Connection和Upgrade內(nèi)容代表協(xié)議成功升級為websocket -
Sec-WebSocket-Version代表協(xié)議版本號(hào),常用13 - Sec-WebSocket-Accept計(jì)算方法偽代碼如下:
base64(sha1(Sec-WebSocket-Key + 258EAFA5-E914-47DA-95CA-C5AB0DC85B11))
其中Sec-WebSocket-Key為客戶端傳入,258EAFA5-E914-47DA-95CA-C5AB0DC85B11為固定值。
2. 傳輸階段
Websocket的數(shù)據(jù)傳輸是frame形式傳輸?shù)模热鐣?huì)將一條消息分為幾個(gè)frame,按照先后順序傳輸出去。
websocket傳輸使用的協(xié)議如下圖:

參數(shù)說明如下:
FIN:1位,用來表明這是一個(gè)消息的最后的消息片斷,當(dāng)然第一個(gè)消息片斷也可能是最后的一個(gè)消息片斷;
RSV1, RSV2, RSV3: 分別都是1位,如果雙方之間沒有約定自定義協(xié)議,那么這幾位的值都必須為0,否則必須斷掉websocket連接;
-
Opcode: 4位操作碼,定義有效負(fù)載數(shù)據(jù),如果收到了一個(gè)未知的操作碼,連接也必須斷掉,以下是定義的操作碼:
%x0 表示連續(xù)消息片斷 %x1 表示文本消息片斷 %x2 表未二進(jìn)制消息片斷 %x3-7 為將來的非控制消息片斷保留的操作碼 %x8 表示連接關(guān)閉 %x9 表示心跳檢查的ping %xA 表示心跳檢查的pong %xB-F 為將來的控制消息片斷的保留操作碼 Mask: 1位,定義傳輸?shù)臄?shù)據(jù)是否有加掩碼,如果設(shè)置為1,掩碼鍵必須放在masking-key區(qū)域,客戶端發(fā)送給服務(wù)端的所有消息,此位的值都是1;
Payload length: 傳輸數(shù)據(jù)的長度,以字節(jié)的形式表示:7位、7+16位、或者7+64位。如果這個(gè)值以字節(jié)表示是0-125這個(gè)范圍,那這個(gè)值就表示傳輸數(shù)據(jù)的長度;如果這個(gè)值是126,則隨后的兩個(gè)字節(jié)表示的是一個(gè)16進(jìn)制無符號(hào)數(shù),用來表示傳輸數(shù)據(jù)的長度;如果這個(gè)值是127,則隨后的是8個(gè)字節(jié)表示的一個(gè)64位無符合數(shù),這個(gè)數(shù)用來表示傳輸數(shù)據(jù)的長度。多字節(jié)長度的數(shù)量是以網(wǎng)絡(luò)字節(jié)的順序表示。負(fù)載數(shù)據(jù)的長度為擴(kuò)展數(shù)據(jù)及應(yīng)用數(shù)據(jù)之和,擴(kuò)展數(shù)據(jù)的長度可能為0,因而此時(shí)負(fù)載數(shù)據(jù)的長度就為應(yīng)用數(shù)據(jù)的長度;
Masking-key: 0或4個(gè)字節(jié),客戶端發(fā)送給服務(wù)端的數(shù)據(jù),都是通過內(nèi)嵌的一個(gè)32位值作為掩碼的;掩碼鍵只有在掩碼位設(shè)置為1的時(shí)候存在;
Payload data: (x+y)位,負(fù)載數(shù)據(jù)為擴(kuò)展數(shù)據(jù)及應(yīng)用數(shù)據(jù)長度之和;
Extension data: x位,如果客戶端與服務(wù)端之間沒有特殊約定,那么擴(kuò)展數(shù)據(jù)的長度始終為0,任何的擴(kuò)展都必須指定擴(kuò)展數(shù)據(jù)的長度,或者長度的計(jì)算方式,以及在握手時(shí)如何確定正確的握手方式。如果存在擴(kuò)展數(shù)據(jù),則擴(kuò)展數(shù)據(jù)就會(huì)包括在負(fù)載數(shù)據(jù)的長度之內(nèi);
Application data: y位,任意的應(yīng)用數(shù)據(jù),放在擴(kuò)展數(shù)據(jù)之后,應(yīng)用數(shù)據(jù)的長度=負(fù)載數(shù)據(jù)的長度-擴(kuò)展數(shù)據(jù)的長度。
二、golang實(shí)現(xiàn)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}
}
// 讀取數(shù)據(jù)幀
func (ws *WsSocket) ReadIframe() (data []byte, opcode byte, err error) {
err = nil
// 第一個(gè)字節(jié):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位標(biāo)識(shí): 掩碼, 定義payload數(shù)據(jù)是否進(jìn)行了掩碼處理,如果是1表示進(jìn)行了掩碼處理
mask := payloadLenByte[0] >> 7
payloadLen := int(payloadLenByte[0] & 0x7F) // 0111 1111
if payloadLen == 126 {
// 讀取兩個(gè)字節(jié)
extendedByte := make([]byte, 2)
ws.Conn.Read(extendedByte)
// 重置payloadLen,采用大端字節(jié)序
payloadLen = int(binary.BigEndian.Uint16(extendedByte))
}
if payloadLen == 127 {
// 讀取8個(gè)字節(jié)
extendedByte := make([]byte, 8)
ws.Conn.Read(extendedByte)
// 重置payloadLen,采用大端字節(jié)序
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
}
// 遞歸讀取數(shù)據(jù)
nextData, opcode, err := ws.ReadIframe()
if err != nil {
return
}
data = append(data, nextData...)
return
}
// websocket:發(fā)送數(shù)據(jù)幀(簡單版)
func (ws *WsSocket) SendIframe(data []byte) error {
length := len(data)
if length <= 0 {
return errors.New("data cannot be empty")
}
//注意: 服務(wù)端發(fā)送數(shù)據(jù),不使用掩碼操作
ws.Conn.Write([]byte{0x81}) // 1000 0001 : 前段部分表示FIN、RSV1-3,后半部分表示opcode:0X1(文本數(shù)據(jù)幀)
switch {
case length <= 125:
var payLenByte byte
payLenByte = byte(0) | byte(length) //mask + payloadLength: mask位設(shè)置為0
ws.Conn.Write([]byte{payLenByte})
case length <= math.MaxUint16:
// 處理126的情況
ws.Conn.Write([]byte{0x7e}) // 01111110: mask + payloadLength,mask設(shè)置為0,payloadLength為126
// 2個(gè)字節(jié)
buf := make([]byte, 2)
binary.BigEndian.PutUint16(buf, uint16(length)) // 采用大端字節(jié)序
ws.Conn.Write(buf)
default:
// 處理127的情況
ws.Conn.Write([]byte{0x7f}) // 01111111: mask + payloadLength,mask設(shè)置為0,payloadLength為127
// 8個(gè)字節(jié)
buf := make([]byte, 8)
binary.BigEndian.PutUint64(buf, uint64(length)) // 采用大端字節(jié)序
ws.Conn.Write(buf)
}
// 發(fā)送數(shù)據(jù)
ws.Conn.Write(data)
return nil
}
// 升級協(xié)議
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
}
// 驗(yàn)證請求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,域名根據(jù)實(shí)際情況自定義配置)
<!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");
// 監(jiān)聽打開連接
ws.onopen = function(e) {
log("Connected");
var msgObj = {content:"this is message"}
sendMessage(JSON.stringify(msgObj))
}
// 監(jiān)聽關(guān)閉連接
ws.onclose = function(e) {
log("Disconnected: " + e.code);
}
// 監(jiān)聽錯(cuò)誤
ws.onerror = function(e) {
log("Error ");
}
// 監(jiān)聽消息
ws.onmessage = function(e) {
log("Message received: " + e.data);
// ws.close();
}
}
// 發(fā)送消息
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>