No history yet

Bidirectional Streaming

Bidirectional Streaming

Bidirectional streaming RPCs establish a fully duplex communication channel where both the client and server can send a sequence of messages to each other independently. This communication happens over a single HTTP/2 connection, initiated by the client. The order of messages within each stream is preserved, but the client and server can read and write in any order they choose.

Both the client and server send a stream of messages to each other simultaneously.

This model is highly efficient for applications requiring real-time, low-latency, back-and-forth communication. Unlike unary, client-streaming, or server-streaming RPCs, it supports continuous, asynchronous data exchange without the overhead of establishing new connections for each message.

Defining the RPC

Defining a bidirectional streaming RPC in a .proto file is straightforward. You use the stream keyword for both the request and the response types in your service definition. This signals to the gRPC code generator that both the client and server will be sending multiple messages.

syntax = "proto3";

package chat;

// The chat service definition.
service Chat {
  // A bidirectional streaming RPC.
  rpc Join(stream ChatMessage) returns (stream ChatMessage) {};
}

// A message containing a user's name and content.
message ChatMessage {
  string user = 1;
  string text = 2;
}

In this example, the Join RPC method accepts a stream of ChatMessage objects from the client and returns a stream of ChatMessage objects to the client. This allows multiple clients to join a chat room, send messages, and receive messages from others in real time.

Implementation and Concurrency

Implementing bidirectional streaming requires handling concurrent reads and writes. Since the client and server can send messages at any time, your application logic must be prepared to process incoming messages while simultaneously sending outgoing ones. This is typically managed using threads or coroutines.

On the server side, the handler function receives a stream object. It can read messages from the client using the stream's Recv() method and write messages back using the Send() method. These operations are often performed in separate goroutines (in Go) or threads to avoid blocking.

// Go server-side handler example
func (s *server) Join(stream pb.Chat_JoinServer) error {
    // Goroutine to receive messages from the client
    go func() {
        for {
            in, err := stream.Recv()
            if err == io.EOF {
                return
            }
            if err != nil {
                log.Printf("Error receiving: %v", err)
                return
            }
            log.Printf("Got message from %s: %s", in.User, in.Text)
            // Process and broadcast message to other clients...
        }
    }()

    // Send messages to the client in the main goroutine (or another one)
    for msg := range s.messageChannel {
        if err := stream.Send(msg); err != nil {
            return err
        }
    }

    return nil
}

The client-side implementation mirrors this pattern. The client initiates the stream and gets a stream object in return. It can then use one thread or coroutine to send messages to the server and another to listen for incoming messages.

Flow Control and Error Handling

gRPC leverages the built-in flow control of HTTP/2 to manage message transmission. Both the client and server maintain a flow-control window, which is the amount of data they are willing to receive. As data is received and processed, the receiver sends WINDOW_UPDATE frames to the sender, allowing it to send more data. This prevents a fast sender from overwhelming a slow receiver and exhausting its memory.

Error handling in a streaming context is critical. An error can terminate the stream on both sides. When the server returns an error status, no further messages can be sent or received by either party. The client receives the error when it calls Recv() or, in some implementations, on a dedicated error channel. Similarly, if the client sends an error (e.g., by closing the stream prematurely), the server's Recv() call will return an error.

Proper error handling involves checking for errors on every Send() and Recv() call and implementing logic to gracefully tear down the connection and notify the user when the stream is terminated unexpectedly.

Quiz Questions 1/5

What is the primary characteristic of a bidirectional streaming RPC in gRPC?

Quiz Questions 2/5

How do you define a bidirectional streaming RPC named RouteChat that sends and receives RouteNote messages in a .proto file?

With its ability to handle real-time, two-way data flow, bidirectional streaming is a powerful tool for building complex, interactive distributed systems.