Go实现websocket协议

WebSocket 是一种网络通信协议,很多高级功能都需要它。

本文介绍 WebSocket 协议的实现及通信协议。

简介

WebSocket 协议在2008年诞生,2011年成为国际标准。所有浏览器都已经支持了。

它的最大特点就是,服务器可以主动向客户端推送信息,客户端也可以主动向服务器发送信息,是真正的双向平等对话,属于服务器推送技术的一种。

img

Websocket既然是建立在TCP/IP协议之上的应用协议,那么就会存在数据传输的协议,而Websocket是借了Http协议的通道升级成一个长链接的Socket链接。

既然是和Socket连接一样,我们就去动手实现一下吧。提高提高我们对通信协议的理解及字节流的操作能力。

协议定义

协议地址rfc6455,协议直观图。从这张图中可以直观的看到每个字节中每个bit(位)所代表的含义。

image-20210518190427253

FIN

标识是否为此消息的最后一个数据包,占 1 bit

RSV

RSV1, RSV2, RSV3: 用于扩展协议,一般为0,各占 1 bit

opcode

数据包类型(frame type),占4bits
0x0:标识一个中间数据包

0x1:标识一个text类型数据包

0x2:标识一个binary类型数据包

0x3-7:保留

0x8:标识一个断开连接类型数据

0x9:标识一个ping类型数据包

0xA:表示一个pong类型数据包

0xB-F:保留

MASK

用于标识PayloadData是否经过掩码处理。如果是1,Masking-key域的数据即是掩码密钥,用于解码PayloadData。客户端发出的数据帧需要进行掩码处理,所以此位是1。

Payload length

Payload data的长度,占7bits,7+16bits,7+64bits:

  • 如果其值在0-125,则是payload的真实长度。
  • 如果值是126,则后面2个字节形成的16bits无符号整型数的值是payload的真实长度。注意,网络字节序,需要转换。
  • 如果值是127,则后面8个字节形成的64bits无符号整型数的值是payload的真实长度。注意,网络字节序,需要转换。

这里的长度表示遵循一个原则,用最少的字节表示长度(尽量减少不必要的传输)。举例说,payload真实长度是124,在0-125之间,必须用前7位表示;不允许长度1是126或127,然后长度2是124,这样违反原则。

Payload data

应用层数据

GO语言实现

当我们知道协议的定义之后,我们就可以一个字节一个字节的把数据读出来。然后按照协议定义中每个bit代表的含义解析出来就好了。

这其中解析字节中bit对应的值需要用到位移(>>,<<)和与(&)运算。原理也很简单,就是拿1左移到需要知道的bit的位置,然后进行&运算,就可以判断其bit值是0还是1。

1、先定义bit的位置

const (
    // Frame header byte 0 bits from Section 5.2 of RFC 6455
    finalBit = 1 << 7
    rsv1Bit  = 1 << 6
    rsv2Bit  = 1 << 5
    rsv3Bit  = 1 << 4

    maskBit = 1 << 7
)
复制代码

2、然后读取前2个字节

var p = make([]byte, 2)
_, err := conn.Read(p)
if err != nil {
  log.Fatalln("读取错误", err)
}
final := p[0]&finalBit != 0
rsv1 := p[0]&rsv1Bit != 0
rsv2 := p[0]&rsv2Bit != 0
rsv3 := p[0]&rsv3Bit != 0
opcode := p[0] & 0xf  
复制代码

第一个字节的前4个bit对应的是fin、rsv1、rsv2、rsv3,后4个bit是opcode对应的是操作代码。后4个bit采用的是0xf 也就是15,二进制是1111与第一个bit进行与运算可以得到后4bit的真实值。至此我们第一个字节的内容就都解析出来了,得到了5个不同的数值定义。

第二个字节的第一个bit为masked,是否使用了掩码处理。如果为1,则需要进行解码处理。后7个bit为长度可以通过0x7f进行 & 运算得到这7位的真实值,二进制为1111111,数值为127

masked := p[1]&maskBit != 0
length := uint64(p[1] & 0x7f)
if length == 126 {
  var lenBytes = make([]byte, 2)
  reader.Read(lenBytes)
  length = binary.BigEndian.Uint64(lenBytes)
}
if length == 127 {
  var lenBytes = make([]byte, 8)
  reader.Read(lenBytes)
  length = binary.BigEndian.Uint64(lenBytes)
}
复制代码

通过以上代码,我们就解析了是否为掩码数据及数据长度了。


3、处理掩码key

在Payload域中,前4个数据是数据本身还是掩码域取决于切面解析的Masked是否为1。如果是掩码,还得将数据的4位读取出来作为解码key

//读取mask掩码
maskkey := make([]byte, 4)
if masked {
  reader.Read(maskkey)
}
复制代码

4、读取Payload

万事具备,只剩下读取Payload了,读取长度为length。如果masked为1,就解码就是

//读取内容
payload := make([]byte, length)
reader.Read(payload)
if masked {
  //解码
  maskBytes(maskkey, 0, payload)
}
复制代码

完整代码

这里提供实例代码,仅供参考

package wsframe

import (
	"encoding/binary"
	"io"
	"log"
	"net"
	"unsafe"
)

const (
	// Frame header byte 0 bits from Section 5.2 of RFC 6455
	finalBit = 1 << 7
	rsv1Bit  = 1 << 6
	rsv2Bit  = 1 << 5
	rsv3Bit  = 1 << 4

	maskBit = 1 << 7
)

// The message types are defined in RFC 6455, section 11.8.
const (
	// TextMessage denotes a text data message. The text message payload is
	// interpreted as UTF-8 encoded text data.
	TextMessage = 0x1

	// BinaryMessage denotes a binary data message.
	BinaryMessage = 0x2

	// CloseMessage denotes a close control message. The optional message
	// payload contains a numeric code and text. Use the FormatCloseMessage
	// function to format a close message payload.
	CloseMessage = 0x8

	// PingMessage denotes a ping control message. The optional message payload
	// is UTF-8 encoded text.
	PingMessage = 0x9

	// PongMessage denotes a pong control message. The optional message payload
	// is UTF-8 encoded text.
	PongMessage = 0x10
)

const (
	noFrame = -1
)

func ReadMessage(reader io.Reader) {
	var p = make([]byte, 2)
	_, err := reader.Read(p)
	if err != nil {
		log.Fatalln("读取错误", err)
	}
	log.Println("前2个byte值", p)
	//通过将1进行左移一定的位数后与第一个字节进行与运算,可以判断该值是否为1
	final := p[0]&finalBit != 0
	rsv1 := p[0]&rsv1Bit != 0
	rsv2 := p[0]&rsv2Bit != 0
	rsv3 := p[0]&rsv3Bit != 0
	//通过0xf=15来与后4位进行与运行,可以得到这4位原本的值
	opcode := p[0] & 0xf
	log.Println("第一个字节的值", final, rsv1, rsv2, rsv3, opcode)
	//获取第2个字节的数据
	masked := p[1]&maskBit != 0
	length := uint64(p[1] & 0x7f)
	if length == 126 {
		var lenBytes = make([]byte, 2)
		reader.Read(lenBytes)
		length = binary.BigEndian.Uint64(lenBytes)
	}
	if length == 127 {
		var lenBytes = make([]byte, 8)
		reader.Read(lenBytes)
		length = binary.BigEndian.Uint64(lenBytes)
	}
	log.Println("masked and length", masked, length)
	switch opcode {
	case CloseMessage, PingMessage, PongMessage:
		log.Println("控制消息", opcode)
		break
	case TextMessage:
		log.Println("文本消息", opcode)
		break
	case BinaryMessage:
		log.Println("二进制消息", opcode)
		break
	}
	//读取mask掩码
	maskkey := make([]byte, 4)
	if masked {
		reader.Read(maskkey)
	}
	//读取内容
	payload := make([]byte, length)
	reader.Read(payload)
	if masked {
		//解码
		maskBytes(maskkey, 0, payload)
	}
	log.Println("内容", string(payload))
}

const wordSize = int(unsafe.Sizeof(uintptr(0)))

func maskBytes(key []byte, pos int, b []byte) int {
	// Mask one byte at a time for small buffers.
	if len(b) < 2*wordSize {
		for i := range b {
			b[i] ^= key[pos&3]
			pos++
		}
		return pos & 3
	}

	// Mask one byte at a time to word boundary.
	if n := int(uintptr(unsafe.Pointer(&b[0]))) % wordSize; n != 0 {
		n = wordSize - n
		for i := range b[:n] {
			b[i] ^= key[pos&3]
			pos++
		}
		b = b[n:]
	}

	// Create aligned word size key.
	var k [wordSize]byte
	for i := range k {
		k[i] = key[(pos+i)&3]
	}
	kw := *(*uintptr)(unsafe.Pointer(&k))

	// Mask one word at a time.
	n := (len(b) / wordSize) * wordSize
	for i := 0; i < n; i += wordSize {
		*(*uintptr)(unsafe.Pointer(uintptr(unsafe.Pointer(&b[0])) + uintptr(i))) ^= kw
	}

	// Mask one byte at a time for remaining bytes.
	b = b[n:]
	for i := range b {
		b[i] ^= key[pos&3]
		pos++
	}

	return pos & 3
}

复制代码
© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享