Какие проблемы возникают при конкурентной записи в 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")
}()

Ответ 18+ 🔞

А, ну ты смотри, какие проблемы подкинули! WebSocket, говоришь? И горутины туда же, писать хотят все сразу, как голодные псы на миску. Так вот, слушай сюда, вся соль в этой состоянии гонки (data race), ёпта. Библиотеки эти, типа gorilla/websocket, они же не железобетонные, они как та мартышлюшка на тонкой ветке — если с двух сторон трясти, она в пизду полетит.

Если из нескольких горутин начать в одно соединение писать, то получится полный пиздец, в рот меня чих-пых:

  1. Данные перемешаются как салат оливье после пятой рюмки. Клиент получит абракадабру и офигеет.
  2. Паника (Panic) может случиться, библиотека взвоет: "На хуя вы меня одновременно дергаете?!" — и рухнет.
  3. Непредсказуемость — соединение может просто взять и откинуться, как будто его и не было.

Короче, так делать — это как ебаться без презерватива: вроде весело, но последствия ебать.

Ну и как с этим бороться?

Есть два классических способа, как навести порядок в этом борделе.

1. Мьютекс (Mutex) — Просто и в лоб

Берёшь свой WebSocket-коннект, оборачиваешь в структуру с замком и всё. Кто первый захватил мьютекс — тот и пишет, остальные ждут, как лохи в очереди за халявой.

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)
}

Проще пареной репы. Но есть нюанс: пока одна горутина пишет, остальные тупо стоят и ждут. Для многих сценариев — норм.

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

А это уже для эстетов. Заводим одну-единственную горутину-писателя. Она сидит, слушает канал и аккуратно, по одному, отправляет сообщения. Все остальные горутины не лезут в коннект, а просто швыряют свои сообщения в этот канал и дальше по своим делам.

  • Плюс: Горутины-отправители не блокируются. Отправил в канал — и свободен, как птица.
  • Минус: Нужно мозги включать, архитектуру продумывать.
// Эта горутина — наш единственный легальный писатель. Царь и бог записи.
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("Привет из первой горутины!")
}()
go func() {
    // ... делаем что-то ещё
    writeChan <- []byte("А это вторая горутина шлёт привет!")
}()

Вот и вся магия. Либо мьютексом по лбу всем, кто не в очередь, либо завести одного ответственного писаку. Выбирай, что для твоего случая больше подходит, а то так и до беды недалеко, ядрёна вошь.