Что такое Bidirectional streaming в gRPC?

Ответ

Bidirectional streaming (двунаправленная потоковая передача) — это один из четырех типов вызовов в gRPC, при котором и клиент, и сервер могут отправлять последовательность сообщений друг другу асинхронно через одно установленное сетевое соединение. Это позволяет организовать полноценный двусторонний диалог.

Как это выглядит в .proto-файле:

service ChatService {
  // Клиент отправляет поток MessageRequest, сервер отвечает потоком MessageResponse
  rpc StartConversation (stream MessageRequest) returns (stream MessageResponse);
}

message MessageRequest {
  string text = 1;
}

message MessageResponse {
  string text = 1;
  int64 timestamp = 2;
}

Типичные сценарии использования:

  • Чат-приложения и онлайн-игры (обмен сообщениями в реальном времени).
  • Синхронизация данных (клиент отправляет изменения, сервер отправляет подтверждения и обновления).
  • Real-time мониторинг и дашборды (сервер пушит обновления метрик, клиент может отправлять запросы на изменение интервала).

Пример реализации сервера на C#:

public override async Task StartConversation(
    IAsyncStreamReader<MessageRequest> requestStream,
    IServerStreamWriter<MessageResponse> responseStream,
    ServerCallContext context)
{
    // Задача 1: Чтение потока запросов от клиента
    var readTask = Task.Run(async () =>
    {
        await foreach (var message in requestStream.ReadAllAsync())
        {
            Console.WriteLine($"Client said: {message.Text}");
            // Можно обработать сообщение и, например, отправить ответ в другой задаче
        }
    });

    // Задача 2: Отправка потока ответов клиенту (например, broadcast уведомлений)
    var writeTask = Task.Run(async () =>
    {
        int count = 0;
        while (!context.CancellationToken.IsCancellationRequested)
        {
            await responseStream.WriteAsync(new MessageResponse
            {
                Text = $"Server notification #{count++}",
                Timestamp = DateTime.UtcNow.Ticks
            });
            await Task.Delay(TimeSpan.FromSeconds(5), context.CancellationToken);
        }
    });

    await Task.WhenAll(readTask, writeTask); // Ожидаем завершения обеих задач
}

Ключевые преимущества:

  • Эффективность: Одно долгоживущее соединение вместо множества отдельных HTTP-запросов (как в REST).
  • Гибкость и асинхронность: Клиент и сервер могут отправлять данные независимо, без блокировки на ожидание ответа.
  • Реальное время: Идеально подходит для сценариев с низкой задержкой и постоянным обменом данными.

Ответ 18+ 🔞

Давай разберём эту вашу двунаправленную потоковую передачу, чтоб ей пусто было. Представь себе, что ты пробил дыру в стене к соседу и суёте друг другу записки. Только тут не просто разок сунул и ждёшь ответа, а вы оба можете пихать эти записки одновременно, когда захотите. Одна дыра, а бардак полный — это и есть bidirectional streaming в gRPC.

Как это в .proto-файле выглядит, спросишь? Да вот так, просто и понятно, как палка:

service ChatService {
  // Клиент строчит поток MessageRequest, сервер строчит поток MessageResponse. Оба строчат, общаются, жизнь кипит.
  rpc StartConversation (stream MessageRequest) returns (stream MessageResponse);
}

message MessageRequest {
  string text = 1;
}

message MessageResponse {
  string text = 1;
  int64 timestamp = 2;
}

Где эту дичь применять?

  • Чаты, онлайн-игры — ну тут очевидно, ебать. Два потока, один другому чешет, как на базаре.
  • Синхронизация данных — клиент шепчет: «я тут поменял», сервер в ответ: «окей, принял, держи ещё обновлённую хуйню».
  • Дашборды и мониторинг в реальном времени — сервер пихает тебе свежие циферки, а ты можешь в ответ орать: «давай чаще, сука!» или «хватит, задолбал!».

А вот как это на сервере C# выглядит, если не бояться:

public override async Task StartConversation(
    IAsyncStreamReader<MessageRequest> requestStream,
    IServerStreamWriter<MessageResponse> responseStream,
    ServerCallContext context)
{
    // Задача первая: слушаем, что клиент несёт. Читаем его поток, как книжку с продолжением.
    var readTask = Task.Run(async () =>
    {
        await foreach (var message in requestStream.ReadAllAsync())
        {
            Console.WriteLine($"Клиент ляпнул: {message.Text}");
            // Тут можно на каждую его реплику буркнуть что-то в ответ, но осторожно, а то запутаешься.
        }
    });

    // Задача вторая: сами строчим клиенту что-то своё, например, уведомления раз в пять секунд.
    var writeTask = Task.Run(async () =>
    {
        int count = 0;
        while (!context.CancellationToken.IsCancellationRequested)
        {
            await responseStream.WriteAsync(new MessageResponse
            {
                Text = $"Серверное уведомление №{count++}",
                Timestamp = DateTime.UtcNow.Ticks
            });
            await Task.Delay(TimeSpan.FromSeconds(5), context.CancellationToken);
        }
    });

    // И теперь сидим, ждём, когда хоть одна из задач накроется медным тазом. Или обе.
    await Task.WhenAll(readTask, writeTask);
}

И в чём, собственно, кайф?

  • Эффективность, блядь: Одна дыра в стене на всё общение. Не надо каждый раз новый запрос открывать, как в этом вашем REST, где на каждое «ку» — отдельный поклон.
  • Гибкость ёбаная: Клиент может орать своё, сервер — своё, и никто никого не ждёт. Асинхронность в чистом виде, красота.
  • Реальное время: Всё летит моментально, как только родилось в голове. Для чатов, игр и прочей движухи — то, что доктор прописал, ёпта.

Вот и вся магия. Два потока в одной трубе, и никто не путается. Почти.