Какие проблемы возникают при конкурентной записи в WebSocket соединение и как их решать?

Ответ

Основная проблема — состояние гонки (data race). Большинство реализаций WebSocket (например, популярная gorilla/websocket) не являются потокобезопасными для записи. Спецификация WebSocket требует, чтобы сообщения отправлялись последовательно, и одновременная запись из нескольких горутин может привести к:

  1. Повреждению данных: Фрагменты разных сообщений могут перемешаться, делая их нечитаемыми на стороне клиента.
  2. Панике (Panic): Некоторые библиотеки могут запаниковать при обнаружении конкурентной записи.
  3. Непредсказуемому поведению: Соединение может быть разорвано или перейти в некорректное состояние.

Решения

Для синхронизации доступа к соединению на запись используется два основных подхода:

1. Мьютекс (Mutex)

Обернуть вызовы записи в мьютекс, чтобы гарантировать, что в каждый момент времени пишет только одна горутина. Это самый простой подход.

import (
    "sync"
    "github.com/gorilla/websocket"
)

type SafeConn struct {
    conn *websocket.Conn
    mu   sync.Mutex
}

func (sc *SafeConn) WriteJSON(v interface{}) error {
    sc.mu.Lock()
    defer sc.mu.Unlock()
    return sc.conn.WriteJSON(v)
}

func (sc *SafeConn) WriteMessage(messageType int, data []byte) error {
    sc.mu.Lock()
    defer sc.mu.Unlock()
    return sc.conn.WriteMessage(messageType, data)
}

2. Канал-диспетчер (Централизованная запись)

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

  • Плюсы: Не блокирует горутины-отправители. Они быстро отправляют данные в буферизованный канал и продолжают работу.
  • Минусы: Немного сложнее в реализации.
// Горутина, отвечающая за запись в соединение
func writer(conn *websocket.Conn, ch <-chan []byte) {
    for msg := range ch {
        if err := conn.WriteMessage(websocket.TextMessage, msg); err != nil {
            // Обработка ошибки, например, логирование и выход
            return
        }
    }
}

// В основной части программы:
// 1. Создаем канал
writeChan := make(chan []byte, 256)
// 2. Запускаем горутину-писателя
go writer(conn, writeChan)

// 3. В других горутинах отправляем сообщения в канал
go func() {
    // ... какая-то логика
    writeChan <- []byte("Hello from goroutine 1")
}()
go func() {
    // ... какая-то логика
    writeChan <- []byte("Hello from goroutine 2")
}()