предисловие
Эта серия представляет собой реальный бой Голанга, в котором реализована система заграждения от 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
Я видел очень хорошее введение в веб-сокет в Интернете.Сообщение блога.