preface
This series is Golang combat, from 0 to 1 to achieve a barrage system. In the process of development, some off-the-shelf wheels will be implemented, just for learning to use, in practical use, or use off-the-shelf wheels.
At present, the live broadcasting industry is on fire. There are large live broadcasting platforms such as Douyu and Huya, as well as live broadcasting with goods and e-commerce, etc. All of these live broadcasting have bullet screen system. Live broadcasting without bullet screen is soulless. Not just live streaming, but when you watch videos on major video websites, don’t you open a pop-up screen? !!!!! Sometimes bullets are better than video content.
Bullet screen system has a wide range of application scenarios, and its main characteristics are high real-time performance and large concurrency, especially concurrency. In the live broadcast scene, often a big anchor, a live broadcast down millions of bullets easily. Due to golang’s superior performance in concurrent scenarios, we chose to use Golang to implement the barrage system.
Websocket protocol implementation
For sure, webSocket protocol cannot be wrapped around the bullet-screen system. The system using bullet-screen basically has H5 application, and the bullet-screen system on H5 application must be realized by WebSocket.
There are many ready-made WebSocket class libraries in various languages, such as nodeJS socke. IO, PHP swoole, etc. At the beginning of the project, we did not use these ready-made libraries, we implemented a simple WebSocket protocol by ourselves, and learned the WebSocket protocol in the implementation process.
The websocket protocol is described in RFC6455 and is available in Chinese.
Without further ado, let’s begin.
Websocket establishes a handshake based on the HTTP service. We start an HTTP service:
func main(a) {
http.HandleFunc("/echo".func(writer http.ResponseWriter, request *http.Request) {
serveWs(writer, request)
})
err := http.ListenAndServe("localhost:9527".nil)
iferr ! =nil {
log.Fatal(err)
}
}
Copy the code
We first use an Echo service to test the implementation of webSocket protocol, port 9527, serveWs function is very simple, establish socket connection, read information and write back, that is, an Echo service.
Handshake connection
Section 4 of the RFC6455 document details the process of establishing a handshake. We follow the documentation step by step.
The method of the request MUST be GET, and the HTTP version MUST be at least 1.1.
HTTP methods must be GET methods:
ifrequest.Method ! ="GET" {
return nil, fmt.Errorf("HTTP must be a GET method")}Copy the code
The request MUST contain an
Upgrade
header field whose value MUST include the “websocket” keyword.
There must be a Upgradeheader and its value must be websocket:
if! httpHeaderContainsValue(request.Header,"Upgrade"."websocket") {
return nil, fmt.Errorf("Must contain an Upgrade Header field, which must be websocket")}Copy the code
And so on a series of checks, I won’t go over them here.
After the HTTP header is verified, we can deal with TCP connections. As we all know, HTTP is an application layer protocol on top of TCP. Normally, TCP will be disconnected after an HTTP request is completed. A Websocket connection is actually a TCP connection, so we can’t break the TCP connection in HTTP. We manage the TCP connection ourselves. How do I get a TCP connection in HTTP? Golang provides us with a Hijack method.
hijacker, ok := writer.(http.Hijacker)
if! ok {return nil, fmt.Errorf("HTTP.Hijacker not implemented")
}
netConn, buf, err := hijacker.Hijack()
iferr ! =nil {
return nil, err
}
Copy the code
After receiving the handshake request from the client, we must respond to the request from the front end to complete the whole handshake process.
If the status code received from the server is not 101, the
client handles the response per HTTP [RFC2616] procedures. In particular, the client might perform authentication if it receives a 401 status code; the server might redirect the client using a 3xx status code (but clients are not required to follow them), etc. Otherwise, proceed as follows.
We simply implement a WebSocket protocol and set the status code to 101:
var response []byte
response = append(response, "HTTP / 1.1 101 Switching separate Protocols \ r \ n"...).Copy the code
If the response lacks an |Upgrade| header field or the |Upgrade|
header field contains a value that is not an ASCII case- insensitive match for the value “websocket”, the client MUST Fail the WebSocket Connection.
The response must contain Upgradeheader information and the value must be websocket (case insensitive) :
response = append(response, "Upgrade: websocket\r\n"...).Copy the code
If the response lacks a |Connection| header field or the
|Connection| header field doesn’t contain a token that is an ASCII case-insensitive match for the value “Upgrade”, the client MUST Fail the WebSocket Connection.
Must have Connection and the value must be Connection:
response = append(response, "Connection: Upgrade\r\n"...).Copy the code
If the response lacks a |Sec-WebSocket-Accept| header field or
the |Sec-WebSocket-Accept| contains a value other than the base64-encoded SHA-1 of the concatenation of the |Sec-WebSocket- Key| (as a string, not base64-decoded) with the string “258EAFA5- E914-47DA-95CA-C5AB0DC85B11” but ignoring any leading and trailing whitespace, the client MUST Fail the WebSocket Connection.
Sec-websocket-acceptheader is required. The value must be concatenated according to the value of sec-websocket-key and “258eAFa5-E914-47DA-95CA-C5AB0DC85B11”. Ignore all Spaces before and after the base64 SHA-1 encoding to obtain:
var acceptKeyGUID = []byte("258EAFA5-E914-47DA-95CA-C5AB0DC85B11")
generateAcceptKey(request.Header.Get("Sec-WebSocket-Key"))
func generateAcceptKey(key string) string {
h := sha1.New()
h.Write([]byte(key))
h.Write(acceptKeyGUID)
return base64.StdEncoding.EncodeToString(h.Sum(nil))
}
response = append(response, "Sec-WebSocket-Accept: "...). response =append(response, generateAcceptKey(request.Header.Get("Sec-WebSocket-Key"))...)
response = append(response, "\r\n\r\n"...).Copy the code
Other information that is not required will not be added. After the header is constructed, it responds to the client:
if_, err = netConn.Write(response); err ! =nil {
netConn.Close()
}
Copy the code
Since the TCP connection is managed by ourselves, we need to close the connection when the handshake fails.
Now, we’ve done the whole handshake. Test it out:
The handshake is successful, and then we begin the detailed sending and receiving processing.
Read the message
In the WebSocket protocol, data is transmitted through a series of data frames, which are described in detail in section 5 of RFC6455.
Basic data frame format:
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +-+-+-+-+-------+-+-------------+-------------------------------+ |F|R|R|R| opcode|M| Payload len | Extended payload length | |I|S|S|S| (4) |A| (7) | (16/64) | |N|V|V|V| |S| | (if payload len==126/127) | | |1|2|3| |K| | | +-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - + | Extended payload length continued, if payload len == 127 | + - - - - - - - - - - - - - - - +-------------------------------+ | |Masking-key, if MASK set to 1 | +-------------------------------+-------------------------------+ | Masking-key (continued) | Payload Data | +-------------------------------- - - - - - - - - - - - - - - - + : Payload Data continued ... : + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + | Payload Data continued ... | +---------------------------------------------------------------+Copy the code
Extract some important information:
FIN: 1 bit
Indicates that this is the final fragment in a message. The first fragment MAY also be the final fragment. Copy the code
RSV1, RSV2, RSV3: 1 bit each
MUST be 0 unless an extension is negotiated that defines meanings for non-zero values. If a nonzero value is received and none of the negotiated extensions defines the meaning of such a nonzero value, the receiving endpoint MUST _Fail the WebSocket Connection_. Copy the code
Opcode: 4 bits
Defines the interpretation of the "Payload data". If an unknown opcode is received, the receiving endpoint MUST _Fail the WebSocket Connection_. The following values are defined. * %x0 denotes a continuation frame * %x1 denotes a text frame * %x2 denotes a binary frame * %x3-7 are reserved for further non-control frames * %x8 denotes a connection close * %x9 denotes a ping * %xA denotes a pong * %xB-F are reserved for further control frames Copy the code
Mask: 1 bit
Defines whether the "Payload data" is masked. If set to 1, a masking key is present in masking-key, and this is used to unmask the "Payload data" as per Section 5.3. All frames sent from client to server have this bit set to 1. Copy the code
Payload length: 7 bits, 7+16 bits, or 7+64 bits
The length of the "Payload data", in bytes: if 0-125, that is the payload length. If 126, the following 2 bytes interpreted as a 16-bit unsigned integer are the payload length. If 127, the following 8 bytes interpreted as a 64-bit unsigned integer (the most significant bit MUST be 0) are the payload length. Multibyte length quantities are expressed in network byte order. Note that in all cases, the minimal number of bytes MUST be used to encode the length, for example, the length of a 124-byte-long string can't be encoded as the sequence 126, 0, 124. The payload length is the length of the "Extension data" + the length of the "Application data". The length of the "Extension data" may be zero, in which case the payload length is the length of the "Application data". Copy the code
Masking-key: 0 or 4 bytes
All frames sent from the client to the server are masked by a 32-bit value that is contained within the frame. This field is present if the mask bit is set to 1 and is absent if the mask bit is set to 0. See Section 5.3 for further information on client- to-server masking. Copy the code
Payload data: (x+y) bytes
The "Payload data" is defined as "Extension data" concatenated with "Application data". Copy the code
Before we start parsing data frames, let’s define some data structures and methods:
type Conn struct {
conn net.Conn
br *bufio.Reader
writeBuf []byte / / write cache
readLength int64 // Data length
maskKey [4]byte // mask key
}
// Data frame bit
/ / RFC6455 section 5.2
const (
finalBit = 1 << 7
rsv1Bit = 1 << 6
rsv2Bit = 1 << 5
rsv3Bit = 1 << 4
opCode = 0xf
maskBit = 1 << 7
pladloadLen = 0x7f
)
// Message type
RFC6455 section 5.2 or 11.8
const (
ContinuationMessage = 0
TextMessage = 1
BinaryMessage = 2
CloseMessage = 8
PingMessage = 9
PongMessage = 10
)
func (c *Conn)read(n int) ([]byte, error) {
// Read n bytes of data
p, err := c.br.Peek(n)
// Discard n bytes of data
c.br.Discard(len(p))
return p, err
}
func newConn(conn net.Conn, br *bufio.Reader) *Conn {
c := &Conn{
conn:conn,
br:br,
writeBuf:make([]byte.128), // Write to death, only 128 bytes of data are accepted
}
return c
}
Copy the code
Read data frame header information
In a network, data is transferred in bytes. In websocket, all important header information is in the first 2 bytes, read the first 2 bytes:
p, err := c.read(2)
iferr ! =nil {
return err
}
// Parse data frame RFC6455 section 5.2
// Force to press 0, regardless of whether there is extended information
if rsv := p[0] & (rsv1Bit | rsv2Bit | rsv3Bit); rsv ! =0 {
return fmt.Errorf("RSV must be 0")}// indicates that this is the last fragment of the message. The first clip could be the last.
// The FIN bit information is not considered for the moment
// final := p[0]&finalBit ! = 0
frameType := int(p[0]&opCode)
// Check whether the FIN and opcode match
/ / RFC6455 section 5.4
// todo
switch frameType {
case ContinuationMessage:
case TextMessage, BinaryMessage:
case CloseMessage, PingMessage, PongMessage:
default:
return fmt.Errorf("Unknown Opcode")}Copy the code
All frames sent from client to server have this bit set to 1.
The mask bit in the data frame of the client must be 1:
mask := p[1]&maskBit ! =0
if! mask {return fmt.Errorf("Mask bit must be marked as 1")}Copy the code
Then get the length of the application data
Payload length: 7 bits, 7+16 bits, or 7+64 bits
c.readLength = int64(p[1]&pladloadLen)
Copy the code
If the data length is less than or equal to 125, the actual value is the application data length. If equal to 126, then the next two bytes are interpreted as a 16-bit unsigned integer as the length of the application data. If equal to 127, then the next 8 bytes are interpreted as a 64-bit unsigned integer as the length of the application data.
// Get the length of the data
/ / https://tools.ietf.org/html/rfc6455#section-5.2
// The length of the "Payload data", in bytes: if 0-125, that is the
// payload length. If 126, the following 2 bytes interpreted as a
// 16-bit unsigned integer are the payload length. If 127, the
// following 8 bytes interpreted as a 64-bit unsigned integer (the
// most significant bit MUST be 0) are the payload length. Multibyte
// length quantities are expressed in network byte order.
c.readLength = int64(p[1]&pladloadLen)
switch c.readLength {
case 126:
p, err := c.read(2)
iferr ! =nil {
return err
}
c.readLength = int64(binary.BigEndian.Uint16(p))
case 127:
p, err := c.read(8)
iferr ! =nil {
return err
}
c.readLength = int64(binary.BigEndian.Uint64(p))
}
Copy the code
Obtain the mask-key mask:
Masking-key: 0 or 4 bytes
p, err := c.read(4)
iferr ! =nil {
return err
}
Copy the code
We only send the simplest data, other data frame information is not present, we will not be compatible with processing.
Reading Application Data
Reading application data is simple:
// The read length is increased by 1 to make it easier to read EOF directly
var p = make([]byte, c.readLength+1)
n, err := c.br.Read(p)
iferr ! =nil {
return nil, err
}
Copy the code
Since the data read is after the mask, we need to decode it. The mask and decoding algorithms are described in detail in Section 5.3.
Octet i of the transformed data (“transformed-octet-i”) is the XOR of octet i of the original data (“original-octet-i”) with octet at index i modulo 4 of the masking key (“masking-key-octet-j”):
j = i MOD 4 transformed-octet-i = original-octet-i XOR masking-key-octet-j Copy the code
// Only English and digits are supported
func maskBytes(key [4]byte, pos int, b []byte) int {
for i := range b {
b[i] ^= key[pos%4]
pos++
}
return pos%4
}
maskBytes(c.maskKey, 0, p[:n])
Copy the code
Test it out:
$ go run *.go
rsv msg WebSocket rocks
Copy the code
The data is successfully read. Let’s start writing the message.
Writing Application Data
To keep things simple, only 125 bytes of data can be written. Because data exceeding 125 bytes, Payload len bits have to be treated specially. In this case, the main header information is in the first two bytes.
Enforces text information, and sets RSV to 0 and FIN to 1:
// First byte (first 8 bits)
// The default message is text
b0 := byte(TextMessage)
// FIN 1
b0 |= finalBit
Copy the code
Then set the MASK bit and the application data length:
b1 := byte(0)
b1 |= byte(len(msg))
Copy the code
You can set the Payload len bit, because you can only write up to 125 bytes of data. Write cache:
c.writeBuf[0] = b0
c.writeBuf[1] = b1
Copy the code
With the headers set, we can write application data:
func (c *Conn) WriteMessage(msg []byte) error{...copy(c.writeBuf[2:], msg)
_, err := c.conn.Write(c.writeBuf)
return err
}
Copy the code
Let’s test it again:
At this point, we have a super simple, can only learn to use webSocket protocol implementation.
We did not deal with websocket extension information and binary type information, ping, pong, etc., and we did not deal with data fragmentation, sticky packet processing, etc. So this is just learning.
The code address of the project is gitee.com/ask/danmaku
There is a good blog about websocket.