websocket協議學習


一 實驗代碼

 

  1. client.html 
    
        
    
    
    
        
        
        
    
    

 

  1. websocket_server.go 
package main

import (
	"crypto/sha1"
	"encoding/base64"
	"encoding/binary"
	"io"
	"log"
	"net"
	"strings"
	"time"
)

type WsSocket struct {
	Conn net.Conn
}

// 幀類型(OPCODE). RFC 6455, section 11.8.
const (
	FRAME_CONTINUE = 0  //繼續幀
	FRAME_TEXT     = 1  //文本幀
	FRAME_BINARY   = 2  //二進制幀
	FRAME_CLOSE    = 8  //關閉幀
	FRAME_PING     = 9  //ping幀
	FRAME_PONG     = 10 //pong幀
)

func init() {
	//初始化日志打印格式
	log.SetFlags(log.Lshortfile | log.LstdFlags)
}

func main() {
	ln, err := net.Listen("tcp", ":8000")
	defer ln.Close()
	if err != nil {
		log.Panic(err)
	}

	for {
		conn, err := ln.Accept()
		if err != nil {
			log.Println("accept err:", err)
		}
		go handleConnection(conn)
	}

}

func handleConnection(conn net.Conn) {
	// http request open websocket
	content := make([]byte, 1024)
	conn.Read(content)
	log.Printf("http request:\n%s\n", string(content))
	headers := parseHttpHeader(string(content))
	secWebsocketKey := headers["Sec-WebSocket-Key"]

	// http response open websocket
	response := "HTTP/1.1 101 Switching Protocols\r\n"
	response += "Sec-WebSocket-Accept: " + computeAcceptKey(secWebsocketKey) + "\r\n"
	response += "Connection: Upgrade\r\n"
	response += "Upgrade: websocket\r\n\r\n"
	log.Printf("http response:\n%s\n", response)
	if lenth, err := conn.Write([]byte(response)); err != nil {
		log.Println(err)
	} else {
		log.Println("send http response len:", lenth)
	}

	// websocket established
	wssocket := &WsSocket{Conn: conn}

	//begin test case
	for {
		time.Sleep(5 * time.Second)
		// frame ping
		wssocket.SendIframe(FRAME_PING, []byte("hello"))
		// frame read //瀏覽器響應同樣負載數據的pong幀
		log.Printf("server read data from client:\n%s\n", string(wssocket.ReadIframe()))
	}
	//end test case
}

//發送幀給客戶端(不考慮分片)(只做服務端,無掩碼)
func (this *WsSocket) SendIframe(OPCODE int, frameData []byte) {
	dataLen := len(frameData)
	var n int
	var err error

	//第一個字節b1
	b1 := 0x80 | byte(OPCODE)
	n, err = this.Conn.Write([]byte{b1})
	if err != nil {
		log.Printf("Conn.Write() error,length:%d;error:%s\n", n, err)
		if err == io.EOF {
			log.Println("客戶端已經斷開WsSocket!")
		} else if err.(*net.OpError).Err.Error() == "use of closed network connection" {
			log.Println("服務端已經斷開WsSocket!")
		}
	}

	//第二個字節
	var b2 byte
	var payloadLen int
	switch {
	case dataLen <= 125:
		b2 = byte(dataLen)
		payloadLen = dataLen
	case 126 <= dataLen && dataLen <= 65535:
		b2 = byte(126)
		payloadLen = 126
	case dataLen > 65535:
		b2 = byte(127)
		payloadLen = 127
	}
	this.Conn.Write([]byte{b2})

	//如果payloadLen不夠用,寫入exPayLenByte,用exPayLenByte表示負載數據的長度
	switch payloadLen {
	case 126:
		exPayloadLenByte := make([]byte, 2)
		exPayloadLenByte[0] = byte(dataLen >> 8) //高8位
		exPayloadLenByte[1] = byte(dataLen)      //低8位
		this.Conn.Write(exPayloadLenByte)        //擴展2個字節表示負載數據長度, 最高位也可以用
	case 127:
		exPayloadLenByte := make([]byte, 8)
		exPayloadLenByte[0] = byte(dataLen >> 56) //第1個字節
		exPayloadLenByte[1] = byte(dataLen >> 48) //第2個字節
		exPayloadLenByte[2] = byte(dataLen >> 40) //第3個字節
		exPayloadLenByte[3] = byte(dataLen >> 32) //第4個字節
		exPayloadLenByte[4] = byte(dataLen >> 24) //第5個字節
		exPayloadLenByte[5] = byte(dataLen >> 16) //第6個字節
		exPayloadLenByte[6] = byte(dataLen >> 8)  //第7個字節
		exPayloadLenByte[7] = byte(dataLen)       //第8個字節
		this.Conn.Write(exPayloadLenByte)         //擴展8個字節表示負載數據長度, 最高位不可以用,必須為0
	}
	this.Conn.Write(frameData) //無掩碼,直接在表示長度的區域后面寫入數據
	log.Printf("real payloadLen=%d:該數據幀的真實負載數據長度(bytes).\n", dataLen)
	log.Println("MASK=0:沒有掩碼.")
	log.Printf("server send data to client:\n%s\n", string(frameData))

}

//讀取客戶端發送的幀(考慮分片)
func (this *WsSocket) ReadIframe() (frameData []byte) {
	var n int
	var err error

	//第一個字節
	b1 := make([]byte, 1)
	n, err = this.Conn.Read(b1)
	if err != nil {
		log.Printf("Conn.Read() error,length:%d;error:%s\n", n, err)
		if err == io.EOF {
			log.Println("客戶端已經斷開WsSocket!")
		} else if err.(*net.OpError).Err.Error() == "use of closed network connection" {
			log.Println("服務端已經斷開WsSocket!")
		}
	}
	FIN := b1[0] >> 7
	OPCODE := b1[0] & 0x0F
	if OPCODE == 8 {
		log.Println("OPCODE=8:連接關閉幀.")
		this.SendIframe(FRAME_CLOSE, formatCloseMessage(1000, "因為收到客戶端的主動關閉請求,所以響應."))
		this.Conn.Close()
		return
	}

	//第二個字節
	b2 := make([]byte, 1)
	this.Conn.Read(b2)
	payloadLen := int64(b2[0] & 0x7F) //payloadLen:表示數據報文長度(可能不夠用),0x7F(16) > 01111111(2)
	MASK := b2[0] >> 7                //MASK=1:表示客戶端發來的數據,且表示采用了掩碼(客戶端傳來的數據必須采用掩碼)
	log.Printf("second byte:MASK=%d, raw payloadLen=%d\n", MASK, payloadLen)

	//擴展長度
	dataLen := payloadLen
	switch {
	case payloadLen == 126:
		// 如果payloadLen=126,啟用2個字節作為拓展,表示更長的報文
		// 負載數據的長度范圍(bytes):126~65535(2) 0xffff
		log.Println("raw payloadLen=126,啟用2個字節作為拓展(最高有效位可以是1,使用所有位),表示更長的報文")
		exPayloadLenByte := make([]byte, 2)
		n, err := this.Conn.Read(exPayloadLenByte)
		if err != nil {
			log.Printf("Conn.Read() error,length:%d;error:%s\n", n, err)
		}
		dataLen = int64(exPayloadLenByte[0])<<8 + int64(exPayloadLenByte[1])

	case payloadLen == 127:
		// 如果payloadLen=127,啟用8個字節作為拓展,表示更長的報文
		// 負載數據的長度范圍(bytes):65536~0x7fff ffff ffff ffff
		log.Println("payloadLen=127,啟用8個字節作為拓展(最高有效位必須是0,舍棄最高位),表示更長的報文")
		exPayloadLenByte := make([]byte, 8)
		this.Conn.Read(exPayloadLenByte)
		dataLen = int64(exPayloadLenByte[0])<<56 + int64(exPayloadLenByte[1])<<48 + int64(exPayloadLenByte[2])<<40 + int64(exPayloadLenByte[3])<<32 + int64(exPayloadLenByte[4])<<24 + int64(exPayloadLenByte[5])<<16 + int64(exPayloadLenByte[6])<<8 + int64(exPayloadLenByte[7])
	}
	log.Printf("real payloadLen=%d:該數據幀的真實負載數據長度(bytes).\n", dataLen)

	//掩碼
	maskingByte := make([]byte, 4)
	if MASK == 1 {
		this.Conn.Read(maskingByte)
		log.Println("MASK=1:負載數據采用了掩碼.")
	} else if MASK == 0 {
		log.Println("MASK=0:沒有掩碼.")
	}

	//數據
	payloadDataByte := make([]byte, dataLen)
	this.Conn.Read(payloadDataByte)
	dataByte := make([]byte, dataLen)
	for i := int64(0); i < dataLen; i++ {
		if MASK == 1 { //解析掩碼數據
			dataByte[i] = payloadDataByte[i] ^ maskingByte[i%4]
		} else {
			dataByte[i] = payloadDataByte[i]
		}
	}

	//如果沒有數據,強制停止遞歸
	if dataLen <= 0 {
		return
	}
	//最后一幀,正常停止遞歸
	if FIN == 1 {
		return dataByte
	}
	//中間幀
	nextData := this.ReadIframe()
	//匯總
	return append(frameData, nextData...)
}

//計算Sec-WebSocket-Accept
func computeAcceptKey(secWebsocketKey string) string {
	var keyGUID = []byte("258EAFA5-E914-47DA-95CA-C5AB0DC85B11")
	h := sha1.New()
	h.Write([]byte(secWebsocketKey))
	h.Write(keyGUID)
	return base64.StdEncoding.EncodeToString(h.Sum(nil))
}

//HTTP報文頭部map
func parseHttpHeader(content string) map[string]string {
	headers := make(map[string]string, 10)
	lines := strings.Split(content, "\r\n")
	for _, line := range lines {
		if len(line) >= 0 {
			words := strings.Split(line, ":")
			if len(words) == 2 {
				headers[strings.Trim(words[0], " ")] = strings.Trim(words[1], " ")
			}
		}
	}
	return headers
}

//二進制逐位打印字節數組
func printBinary(data []byte) {
	for i := 0; i < len(data); i++ {
		byteData := data[i]
		var j uint
		for j = 7; j > 0; j-- {
			log.Printf("%d", ((byteData >> j) & 0x01))
		}
		log.Printf("%d\n", ((byteData >> j) & 0x01))
	}
}

//關閉碼 + 關閉原因 = 關閉幀的負載數據
func formatCloseMessage(closeCode int, text string) []byte {
	buf := make([]byte, 2+len(text))
	binary.BigEndian.PutUint16(buf, uint16(closeCode))
	copy(buf[2:], text)
	return buf
}

代碼鏈接,僅供學習

websocket_server.go: websocket 基於 tcp socket 的粗糙實現, 只提供 websocket 服務

websocket_http_server.go: 把該實現移植到了 http socket 環境(也可以是某個 golang web 框架), 實現了 websocket http 利用同一個端口,同時對> 外服務。原理:

// 通過Hijacker拿到http連接下的tcp連接,Hijack()之后該連接完全由自己接管
conn, _, err := w.(http.Hijacker).Hijack()

 

二 websocket協議閱讀要點記錄


1.客戶端必須掩碼(mask)它發送到服務器的所有幀(更多詳細信息請參見 5.3 節)。 
2.當收到一個沒有掩碼的幀時,服務器必須關閉連接。在這種情況下,服務器可能發送一個定義在 7.4.1 節的狀態碼 1002(協議錯誤)的 Close 幀。 
3.服務器必須不掩碼發送到客戶端的所有幀。如果客戶端檢測到掩碼的幀,它必須關閉連接。在這種情況下,它可能使用定義在 7.4.1節的狀態碼 1002(協議錯誤)。 
4.一個沒有分片的消息由單個帶有 FIN 位設置(5.2 節)和一個非 0 操作碼的幀組成。 
5.一個分片的消息由單個帶有 FIN 位清零(5.2 節)和一個非 0 操作碼的幀組成,跟隨零個或多個帶有 FIN 位清零和操作碼設置為 0 的幀,且終止於一個帶有 FIN 位設置且 0 操作碼的幀。一個分片的消息概念上是等價於單個大的消息,其負載是等價於按順序串聯片段的負載 
6.控制幀(參見 5.5 節)可能被注入到一個分片消息的中間。控制幀本身必須不被分割。 
7.消息分片必須按發送者發送順序交付給收件人。 
8.一個消息的所有分片是相同類型,以第一個片段的操作碼設置。 
9.關閉幀可以包含內容體(“幀的“應用數據”部分)指示一個關閉的原因,例如端點關閉了、端點收到的幀太大、或端點收到的幀不符合端點期望的格式。如果有內容體,內容體的頭兩個字節必須是 2 字節的無符號整數(按網絡字節順序)代表一個在 7.4 節的/code/值定義的狀態碼。跟着 2 字節的整數,內容體可以包含 UTF-8 編碼的/reason/值,本規范沒有定義它的解釋。數據不必是人類可讀的但可能對調試或傳遞打開連接的腳本相關的信息是有用的。由於數據不保證人類可讀,客戶端必須不把它顯示給最終用戶。 
10.在應用發送關閉幀之后,必須不發送任何更多的數據幀。 
11.發送並接收一個關閉消息后,一個端點認為 WebSocket 連接關閉了且必須關閉底層的 TCP 連接。服務器必須立即關閉底層 TCP 連接,客戶端應該等待服務器關閉連接但可能在發送和接收一個關閉消息之后的任何時候關閉連接,例如,如果它沒有在一個合理的時間周期內接收到服務器的 TCP 關閉。 
12.一個端點可以在連接建立之后並在連接關閉之前的任何時候發送一個 Ping 幀。注意:一個 Ping 即可以充當一個 keepalive,也可以作為驗證遠程端點仍可響應

 

三 小經驗

1.瀏覽器目前沒有提供js接口發送ping幀,瀏覽器可能單向的發送pong幀(可以利用文本幀當作ping幀來使用) 
2.服務端給瀏覽器發送ping幀,瀏覽器會盡快響應同樣負載數據的pong幀 
3.瀏覽器發送的websocket負載數據太大的時候會分片 
4.不管是瀏覽器,還是服務器,收到close幀都回復同樣內容的close幀,然后做后續的操作

 

四 連接斷開情況分析

    • server:s
    • browser:b 

      情況0 
      動作:b發s連接關閉幀,s無操作 
      現象: 
      0) b過很久之后觸發了onclose 
      1) s寫入: *net.OpError: write tcp 127.0.0.1:8000->127.0.0.1:34508: write: broken pipe 
      2) s讀取: *errors.errorString: EOF 

      情況1 
      動作:b發s連接關閉幀,s回應連接關閉幀 
      現象: 
      0) b馬上觸發了onclose 
      1) s寫入: *net.OpError: write tcp 127.0.0.1:8000->127.0.0.1:34482: write: broken pipe 
      2) s讀取: *errors.errorString: EOF 

      情況2 
      動作:b發s連接關閉幀,s回應連接關閉幀,s關閉tcp socket 
      現象: 
      0) b馬上觸發了onclose 
      1) s寫入: *net.OpError: write tcp 127.0.0.1:8000->127.0.0.1:34502: use of closed network connection 
      2) s讀取: *net.OpError: read tcp 127.0.0.1:8000->127.0.0.1:34502: use of closed network connection 

      情況3 
      動作:s發b連接關閉幀,b無操作 
      現象: 
      0) b馬上回應相同數據的關閉幀, 接着觸發onclose 
      1) s寫入: *net.OpError: write tcp 127.0.0.1:8000->127.0.0.1:34482: write: broken pipe 
      2) s讀取: *errors.errorString: EOF 

      情況4 
      動作:s發b連接關閉幀,s關閉tcp socket 
      現象: 
      0) b馬上觸發了onclose 
      1) s寫入: *net.OpError: write tcp 127.0.0.1:8000->127.0.0.1:34542: use of closed network connection 
      2) s讀取: *net.OpError: tcp 127.0.0.1:8000->127.0.0.1:34542: use of closed network connection

 

http://hopehook.com/2017/01/08/websocket/


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM