Система Bullet screen от 0 до 1 — реализация сверхпростого протокола веб-сокетов

Go

предисловие

Эта серия представляет собой реальный бой Голанга, в котором реализована система заграждения от 0 до 1. В процессе разработки будут реализованы некоторые готовые колеса, просто в целях обучения.В реальной эксплуатации до сих пор используются готовые колеса.

Сейчас индустрия прямых трансляций находится в огне.Существуют крупные платформы прямых трансляций, такие как Douyu и Huya, а также прямые трансляции с товарами, прямые трансляции электронной коммерции и т. д. Эти прямые трансляции будут иметь систему заграждений.Прямые трансляции без заграждений бездушны. Не только в прямом эфире, но и при просмотре роликов на крупных видеосайтах шквал не открываете? ! ! Иногда даммаку выглядит лучше, чем видеоконтент.

Сценарии использования системы Bullet Screen очень широки, и ее основными особенностями являются высокая производительность в реальном времени и большое количество параллелизма, особенно количество параллелизма. В сцене прямого эфира часто присутствует большой якорь, и в прямом эфире можно легко транслировать миллион заграждений. Из-за превосходной производительности golang в параллельных сценариях мы решили использовать golang для реализации системы заграждения.

реализация протокола веб-сокета

Система пулевого экрана не должна иметь возможности обойти протокол веб-сокета.Система, использующая пулевой экран, в основном будет иметь приложения H5.Система пулевого экрана в приложении H5 должна быть реализована с помощью веб-сокета.

Есть много готовых библиотек Websocket классов на основных языках, таких как Nodejs'ssocket.io, PHPswooleПодождите, в начале проекта мы не использовали эти готовые библиотеки, мы реализовали простой протокол вебсокета сами, а протокол вебсокета изучили в процессе внедрения.

Протокол веб-сокета находится вRFC6455Подробно описано в , переведено Великим БогомКитайская версия.

Без дальнейших церемоний, давайте начнем.

Когда веб-сокет устанавливает рукопожатие, оно основано на службе HTTP.Сначала мы запускаем службу HTTP:

func main() {
	http.HandleFunc("/echo", func(writer http.ResponseWriter, request *http.Request) {
		serveWs(writer, request)
	})
	err := http.ListenAndServe("localhost:9527", nil)
	if err != nil {
		log.Fatal(err)
	}
}

мы сначала используемechoсервис для проверки реализации протокола websocket, порт9527, функцияserveWsФункция очень проста, установить сокетное соединение, прочитать информацию и записать обратно, то естьechoСлужить.

рукопожатие

Документ RFC6455Раздел 4Подробно описан процесс установления рукопожатия. Мы следуем документации шаг за шагом.

The method of the request MUST be GET, and the HTTP version MUST be at least 1.1.

Метод HTTP должен бытьGETметод:

if request.Method != "GET" {
	return nil, fmt.Errorf("HTTP必须是GET方法")
}

The request MUST contain an Upgrade header field whose value MUST include the "websocket" keyword.

должен иметьUpgradeзаголовок, его значение должно бытьwebsocket:

if !httpHeaderContainsValue(request.Header, "Upgrade", "websocket") {
	return nil, fmt.Errorf("必须包含一个Upgrade header字段,它的值必须为websocket")
}

Дождитесь серии проверок, которые здесь повторяться не будут.

После проверки HTTP-заголовка мы займемся TCP-соединением. Как мы все знаем, HTTP — это протокол прикладного уровня поверх TCP. В нормальных условиях после завершения HTTP-запроса TCP также будет отключен. Соединение через веб-сокет на самом деле является TCP-соединением, поэтому мы не можем позволить TCP-соединению в HTTP-соединении отключиться, мы сами управляем TCP-соединением. Как получить TCP-соединение в HTTP? Golang предоставляет намHijackметод.

hijacker, ok := writer.(http.Hijacker)
if !ok {
    return nil, fmt.Errorf("未实现http.Hijacker")
}
netConn, buf, err := hijacker.Hijack()
if err != nil {
    return nil, err
}

После получения запроса на рукопожатие от клиента мы должны ответить на внешний запрос, чтобы завершить весь процесс рукопожатия.

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.

Мы просто реализуем протокол веб-сокета, и здесь мы напрямую устанавливаем код состояния 101:

var response []byte
response = append(response, "HTTP/1.1 101 Switching Protocols\r\n"...)

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.

Ответ должен иметьUpgradeИнформация заголовка и значение должны быть WebSocket (без учета регистра):

response = append(response, "Upgrade: websocket\r\n"...)

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.

должен иметьConnectionИ значение должно быть Connection:

response = append(response, "Connection: Upgrade\r\n"...)

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-Acceptзаголовок, значение должно основываться наSec-WebSocket-KeyЗначение и "258EAFA5-E914-47DA-95CA-C5AB0DC85B11" объединяются, игнорируя все начальные и конечные пробелы для кодировки base64 SHA-1, чтобы получить:

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"...)

Мы не будем добавлять другую дополнительную информацию. После построения заголовка ответ отправляется клиенту:

if _, err = netConn.Write(response); err != nil {
    netConn.Close()
}

Поскольку это TCP-соединение теперь управляется нами, в случае сбоя во время процесса рукопожатия нам необходимо закрыть соединение самостоятельно.

Итак, мы завершили весь процесс рукопожатия. есть тест:

Рукопожатие прошло успешно, после чего мы начинаем детальную обработку отправки и получения.

прочитать сообщение

В протоколе WebSocket данные передаются через серию кадров данных в RFC6455.Раздел 5Подробности о кадре данных.

Основной формат фрейма данных:

      0                   1                   2                   3
      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 ...                |
     +---------------------------------------------------------------+

Некоторые важные информационные выдержки:

FIN: 1 bit

Indicates that this is the final fragment in a message.  The first
fragment MAY also be the final fragment.

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_.

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

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.

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".

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.

Payload data: (x+y) bytes

 The "Payload data" is defined as "Extension data" concatenated
 with "Application data".

Прежде чем мы начнем анализировать фрейм данных, давайте определим некоторые структуры данных и методы:

type Conn struct {
	conn net.Conn
	br *bufio.Reader

	writeBuf []byte // 写缓存

	readLength int64 //数据长度

	maskKey [4]byte // mask key
}

// 数据帧位
// RFC6455 5.2节
const (
	finalBit = 1 << 7
	rsv1Bit = 1 << 6
	rsv2Bit = 1 << 5
	rsv3Bit = 1 << 4

	opCode = 0xf

	maskBit = 1 << 7
	pladloadLen = 0x7f
)

// 消息类型
// RFC6455 5.2节或11.8节
const (
	ContinuationMessage = 0
	TextMessage = 1
	BinaryMessage = 2
	CloseMessage = 8
	PingMessage = 9
	PongMessage = 10
)

func (c *Conn)read(n int) ([]byte, error) {
    // 读取n个字节数据
	p, err := c.br.Peek(n)
    // 丢弃掉n个字节数据
	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), // 写死,只接受128字节数据
	}
	return c
}

Чтение информации заголовка фрейма данных

В сети данные передаются в байтах. В websocket важная информация заголовка находится в первых 2 байтах, прочитайте первые 2 байта:

p, err := c.read(2)
if err != nil {
    return err
}

// 解析数据帧 RFC6455 5.2节
// 强制按0判断,不考虑是否有扩展信息
if rsv := p[0] & (rsv1Bit | rsv2Bit | rsv3Bit); rsv != 0 {
    return fmt.Errorf("RSV必须为0")
}
// 表示这是消息的最后一个片段。第一个片段也有可能是最后一个片段。
// 暂时不考虑FIN位信息
// final := p[0]&finalBit != 0
frameType := int(p[0]&opCode)

// 判断FIN和opcode为是否匹配
// RFC6455 5.4节
// todo
switch frameType {
case ContinuationMessage:
case TextMessage, BinaryMessage:
case CloseMessage, PingMessage, PongMessage:
default:
    return fmt.Errorf("未知的opcode")
}

All frames sent from client to server have this bit set to 1.

Во фрейме данных клиентаmaskБит должен быть 1:

mask := p[1]&maskBit != 0
if !mask {
    return fmt.Errorf("mask位必须标记为1")
}

Затем получите длину данных приложения

Payload length: 7 bits, 7+16 bits, or 7+64 bits

c.readLength = int64(p[1]&pladloadLen)

Если длина данных меньше или равна 125, фактическим значением является длина данных приложения. Если оно равно 126, то следующие 2 байта интерпретируются как 16-битное целое без знака как длина данных приложения. Если оно равно 127, то следующие 8 байтов интерпретируются как 64-битное целое число без знака как длина данных приложения.

// 获取数据长度
// 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)
    if err != nil {
        return err
    }
    c.readLength = int64(binary.BigEndian.Uint16(p))
case 127:
    p, err := c.read(8)
    if err != nil {
        return err
    }
    c.readLength = int64(binary.BigEndian.Uint64(p))
}

Получите маску маски-ключа:

Masking-key: 0 or 4 bytes

p, err := c.read(4)
if err != nil {
    return err
}

Мы отправляем только самые простые данные, другой информации о фрейме данных не существует, мы не совместимы с обработкой.

Чтение данных приложения

Чтение данных приложения так же просто, как:

// 读取长度加1,是为了简单处理,直接将EOF也读取出来
var p = make([]byte, c.readLength+1)
n, err := c.br.Read(p)
if err != nil {
    return nil, err
}

Поскольку прочитанные данные — это данные после маски, поэтому нам нужно декодировать. Алгоритмы маскирования и декодированияРаздел 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
// 只支持英文和数字
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])

есть тест:

$ go run *.go
rsv msg WebSocket rocks

Данные успешно прочитаны. Начните писать информацию ниже.

записать данные приложения

Продолжить простую обработку, поддерживает только запись 125 байт данных. Поскольку более 125 байт данных,Payload lenБиты подлежат специальной обработке. В этом случае основная информация заголовка находится в первых 2 байтах.

Обязательная текстовая информация, и RSV все установлены в 0, FIN непосредственно 1:

// 第一个字节(前8位)
// 默认为文本信息
b0 := byte(TextMessage)

// FIN 1
b0 |= finalBit

затем установитеMASKБит и длина данных приложения:

b1 := byte(0)
b1 |= byte(len(msg))

Поскольку он уже ограничен записью данных не более 125 байт, его можно установить напрямую.Payload lenкусочек. Кэш записи:

c.writeBuf[0] = b0
c.writeBuf[1] = b1

После того, как информация заголовка установлена, данные приложения могут быть записаны:

func (c *Conn) WriteMessage(msg []byte) error {
	...
	copy(c.writeBuf[2:], msg)
	_, err := c.conn.Write(c.writeBuf)
	return err
}

Проверим еще раз:

На данный момент мы реализовали очень простой протокол веб-сокетов, который можно использовать только для обучения.

Мы не имели дело с информацией о расширении и информацией о двоичном типе websocket, ping, pong и т. д., а также с фрагментацией данных, обработкой залипших пакетов и т. д. Так что это только для обучения.

Кодовый адрес проектаgitee.com/ask/danmaku

Я видел очень хорошее введение в веб-сокет в Интернете.Сообщение блога.