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
}

代碼連接,僅供學習html

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

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

// 經過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,也能夠做爲驗證遠程端點仍可響應golang

 

三 小經驗

1.瀏覽器目前沒有提供js接口發送ping幀,瀏覽器可能單向的發送pong幀(能夠利用文本幀看成ping幀來使用) 
2.服務端給瀏覽器發送ping幀,瀏覽器會盡快響應一樣負載數據的pong幀 
3.瀏覽器發送的websocket負載數據太大的時候會分片 
4.無論是瀏覽器,仍是服務器,收到close幀都回復一樣內容的close幀,而後作後續的操做web

 

四 鏈接斷開狀況分析

    • 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/數組

相關文章
相關標籤/搜索