525 lines
12 KiB
Markdown
525 lines
12 KiB
Markdown
# gRPC Streaming Clients
|
|
|
|
Build gRPC clients for event streaming in multiple languages.
|
|
|
|
## Overview
|
|
|
|
gRPC event streaming clients support multiple programming languages:
|
|
- **C# / .NET** - Native gRPC support via Grpc.Net.Client
|
|
- **TypeScript / Node.js** - @grpc/grpc-js package
|
|
- **Go** - google.golang.org/grpc
|
|
- **Python** - grpcio package
|
|
|
|
## C# / .NET Client
|
|
|
|
### Installation
|
|
|
|
```bash
|
|
dotnet add package Grpc.Net.Client
|
|
dotnet add package Google.Protobuf
|
|
dotnet add package Grpc.Tools
|
|
```
|
|
|
|
### Basic Client
|
|
|
|
```csharp
|
|
using Grpc.Net.Client;
|
|
using Svrnty.CQRS.Events.Grpc;
|
|
|
|
// Create channel
|
|
using var channel = GrpcChannel.ForAddress("https://localhost:5001");
|
|
var client = new EventStreamService.EventStreamServiceClient(channel);
|
|
|
|
// Subscribe to persistent stream
|
|
using var call = client.SubscribeToPersistent();
|
|
|
|
await call.RequestStream.WriteAsync(new PersistentSubscriptionRequest
|
|
{
|
|
StreamName = "orders",
|
|
StartOffset = 0,
|
|
SubscriptionId = Guid.NewGuid().ToString()
|
|
});
|
|
|
|
await foreach (var @event in call.ResponseStream.ReadAllAsync())
|
|
{
|
|
Console.WriteLine($"{@event.EventType}: {@event.EventId}");
|
|
}
|
|
```
|
|
|
|
### Production Client
|
|
|
|
```csharp
|
|
public class EventStreamGrpcClient : IDisposable
|
|
{
|
|
private readonly GrpcChannel _channel;
|
|
private readonly EventStreamService.EventStreamServiceClient _client;
|
|
|
|
public EventStreamGrpcClient(string address)
|
|
{
|
|
_channel = GrpcChannel.ForAddress(address, new GrpcChannelOptions
|
|
{
|
|
MaxReceiveMessageSize = 10 * 1024 * 1024, // 10 MB
|
|
MaxSendMessageSize = 10 * 1024 * 1024,
|
|
Credentials = ChannelCredentials.SecureSsl
|
|
});
|
|
|
|
_client = new EventStreamService.EventStreamServiceClient(_channel);
|
|
}
|
|
|
|
public async Task SubscribeAsync(
|
|
string streamName,
|
|
long startOffset,
|
|
Func<StreamEventProto, Task> handler,
|
|
CancellationToken ct)
|
|
{
|
|
using var call = _client.SubscribeToPersistent(cancellationToken: ct);
|
|
|
|
await call.RequestStream.WriteAsync(new PersistentSubscriptionRequest
|
|
{
|
|
StreamName = streamName,
|
|
StartOffset = startOffset,
|
|
SubscriptionId = Guid.NewGuid().ToString()
|
|
});
|
|
|
|
await foreach (var @event in call.ResponseStream.ReadAllAsync(ct))
|
|
{
|
|
await handler(@event);
|
|
}
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
_channel?.Dispose();
|
|
}
|
|
}
|
|
|
|
// Usage
|
|
using var client = new EventStreamGrpcClient("https://event-store.example.com");
|
|
|
|
await client.SubscribeAsync(
|
|
"orders",
|
|
startOffset: 0,
|
|
handler: async @event =>
|
|
{
|
|
Console.WriteLine($"Received: {@event.EventType}");
|
|
await ProcessEventAsync(@event);
|
|
},
|
|
ct);
|
|
```
|
|
|
|
## TypeScript / Node.js Client
|
|
|
|
### Installation
|
|
|
|
```bash
|
|
npm install @grpc/grpc-js @grpc/proto-loader
|
|
npm install --save-dev @types/node
|
|
```
|
|
|
|
### Proto Loading
|
|
|
|
```typescript
|
|
import * as grpc from '@grpc/grpc-js';
|
|
import * as protoLoader from '@grpc/proto-loader';
|
|
import path from 'path';
|
|
|
|
// Load proto file
|
|
const PROTO_PATH = path.join(__dirname, '../protos/event_stream.proto');
|
|
const packageDefinition = protoLoader.loadSync(PROTO_PATH, {
|
|
keepCase: true,
|
|
longs: String,
|
|
enums: String,
|
|
defaults: true,
|
|
oneofs: true
|
|
});
|
|
|
|
const eventStreamProto = grpc.loadPackageDefinition(packageDefinition).svrnty.cqrs.events as any;
|
|
|
|
// Create client
|
|
const client = new eventStreamProto.EventStreamService(
|
|
'localhost:5001',
|
|
grpc.credentials.createInsecure()
|
|
);
|
|
```
|
|
|
|
### Subscription Client
|
|
|
|
```typescript
|
|
interface StreamEvent {
|
|
event_id: string;
|
|
event_type: string;
|
|
stream_name: string;
|
|
offset: number;
|
|
timestamp: { seconds: number; nanos: number };
|
|
data: string;
|
|
metadata: Record<string, string>;
|
|
}
|
|
|
|
async function subscribe(
|
|
streamName: string,
|
|
startOffset: number,
|
|
handler: (event: StreamEvent) => Promise<void>
|
|
): Promise<void> {
|
|
const call = client.subscribeToPersistent();
|
|
|
|
// Send subscription request
|
|
call.write({
|
|
stream_name: streamName,
|
|
start_offset: startOffset,
|
|
subscription_id: crypto.randomUUID()
|
|
});
|
|
|
|
// Receive events
|
|
call.on('data', async (event: StreamEvent) => {
|
|
try {
|
|
await handler(event);
|
|
} catch (error) {
|
|
console.error('Error processing event:', error);
|
|
}
|
|
});
|
|
|
|
call.on('error', (error: Error) => {
|
|
console.error('Stream error:', error);
|
|
});
|
|
|
|
call.on('end', () => {
|
|
console.log('Stream ended');
|
|
});
|
|
|
|
// Keep call alive
|
|
return new Promise((resolve, reject) => {
|
|
call.on('error', reject);
|
|
call.on('end', resolve);
|
|
});
|
|
}
|
|
|
|
// Usage
|
|
await subscribe('orders', 0, async (event) => {
|
|
console.log(`${event.event_type}: ${event.event_id}`);
|
|
|
|
const data = JSON.parse(event.data);
|
|
await processEvent(data);
|
|
});
|
|
```
|
|
|
|
## Go Client
|
|
|
|
### Installation
|
|
|
|
```bash
|
|
go get google.golang.org/grpc
|
|
go get google.golang.org/protobuf/proto
|
|
```
|
|
|
|
### Generate Code
|
|
|
|
```bash
|
|
protoc --go_out=. --go_opt=paths=source_relative \
|
|
--go-grpc_out=. --go-grpc_opt=paths=source_relative \
|
|
event_stream.proto
|
|
```
|
|
|
|
### Subscription Client
|
|
|
|
```go
|
|
package main
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"log"
|
|
|
|
"google.golang.org/grpc"
|
|
"google.golang.org/grpc/credentials/insecure"
|
|
pb "github.com/your-org/event-stream/proto"
|
|
)
|
|
|
|
type EventHandler func(*pb.StreamEventProto) error
|
|
|
|
func Subscribe(
|
|
address string,
|
|
streamName string,
|
|
startOffset int64,
|
|
handler EventHandler,
|
|
) error {
|
|
// Connect
|
|
conn, err := grpc.Dial(address, grpc.WithTransportCredentials(insecure.NewCredentials()))
|
|
if err != nil {
|
|
return fmt.Errorf("failed to connect: %w", err)
|
|
}
|
|
defer conn.Close()
|
|
|
|
client := pb.NewEventStreamServiceClient(conn)
|
|
ctx := context.Background()
|
|
|
|
// Create stream
|
|
stream, err := client.SubscribeToPersistent(ctx)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to subscribe: %w", err)
|
|
}
|
|
|
|
// Send subscription request
|
|
err = stream.Send(&pb.PersistentSubscriptionRequest{
|
|
StreamName: streamName,
|
|
StartOffset: startOffset,
|
|
SubscriptionId: uuid.New().String(),
|
|
})
|
|
if err != nil {
|
|
return fmt.Errorf("failed to send request: %w", err)
|
|
}
|
|
|
|
// Receive events
|
|
for {
|
|
event, err := stream.Recv()
|
|
if err == io.EOF {
|
|
break
|
|
}
|
|
if err != nil {
|
|
return fmt.Errorf("failed to receive: %w", err)
|
|
}
|
|
|
|
if err := handler(event); err != nil {
|
|
log.Printf("Error processing event %s: %v", event.EventId, err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Usage
|
|
func main() {
|
|
err := Subscribe(
|
|
"localhost:5001",
|
|
"orders",
|
|
0,
|
|
func(event *pb.StreamEventProto) error {
|
|
fmt.Printf("%s: %s\n", event.EventType, event.EventId)
|
|
return processEvent(event)
|
|
},
|
|
)
|
|
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
}
|
|
```
|
|
|
|
## Python Client
|
|
|
|
### Installation
|
|
|
|
```bash
|
|
pip install grpcio grpcio-tools
|
|
```
|
|
|
|
### Generate Code
|
|
|
|
```bash
|
|
python -m grpc_tools.protoc \
|
|
-I. \
|
|
--python_out=. \
|
|
--grpc_python_out=. \
|
|
event_stream.proto
|
|
```
|
|
|
|
### Subscription Client
|
|
|
|
```python
|
|
import grpc
|
|
import uuid
|
|
from event_stream_pb2 import PersistentSubscriptionRequest
|
|
from event_stream_pb2_grpc import EventStreamServiceStub
|
|
|
|
class EventStreamClient:
|
|
def __init__(self, address: str):
|
|
self.channel = grpc.insecure_channel(address)
|
|
self.client = EventStreamServiceStub(self.channel)
|
|
|
|
def subscribe(
|
|
self,
|
|
stream_name: str,
|
|
start_offset: int,
|
|
handler
|
|
):
|
|
def request_iterator():
|
|
yield PersistentSubscriptionRequest(
|
|
stream_name=stream_name,
|
|
start_offset=start_offset,
|
|
subscription_id=str(uuid.uuid4())
|
|
)
|
|
|
|
responses = self.client.SubscribeToPersistent(request_iterator())
|
|
|
|
for event in responses:
|
|
try:
|
|
handler(event)
|
|
except Exception as e:
|
|
print(f"Error processing event {event.event_id}: {e}")
|
|
|
|
def close(self):
|
|
self.channel.close()
|
|
|
|
# Usage
|
|
client = EventStreamClient('localhost:5001')
|
|
|
|
try:
|
|
client.subscribe(
|
|
'orders',
|
|
0,
|
|
lambda event: print(f"{event.event_type}: {event.event_id}")
|
|
)
|
|
finally:
|
|
client.close()
|
|
```
|
|
|
|
### Async Client
|
|
|
|
```python
|
|
import asyncio
|
|
import grpc.aio
|
|
from event_stream_pb2 import PersistentSubscriptionRequest
|
|
from event_stream_pb2_grpc import EventStreamServiceStub
|
|
|
|
class AsyncEventStreamClient:
|
|
def __init__(self, address: str):
|
|
self.channel = grpc.aio.insecure_channel(address)
|
|
self.client = EventStreamServiceStub(self.channel)
|
|
|
|
async def subscribe(
|
|
self,
|
|
stream_name: str,
|
|
start_offset: int,
|
|
handler
|
|
):
|
|
async def request_iterator():
|
|
yield PersistentSubscriptionRequest(
|
|
stream_name=stream_name,
|
|
start_offset=start_offset,
|
|
subscription_id=str(uuid.uuid4())
|
|
)
|
|
|
|
call = self.client.SubscribeToPersistent(request_iterator())
|
|
|
|
async for event in call:
|
|
try:
|
|
await handler(event)
|
|
except Exception as e:
|
|
print(f"Error processing event {event.event_id}: {e}")
|
|
|
|
async def close(self):
|
|
await self.channel.close()
|
|
|
|
# Usage
|
|
async def main():
|
|
client = AsyncEventStreamClient('localhost:5001')
|
|
|
|
try:
|
|
await client.subscribe(
|
|
'orders',
|
|
0,
|
|
lambda event: print(f"{event.event_type}: {event.event_id}")
|
|
)
|
|
finally:
|
|
await client.close()
|
|
|
|
asyncio.run(main())
|
|
```
|
|
|
|
## Authentication
|
|
|
|
### C# with Bearer Token
|
|
|
|
```csharp
|
|
var credentials = CallCredentials.FromInterceptor((context, metadata) =>
|
|
{
|
|
metadata.Add("Authorization", $"Bearer {accessToken}");
|
|
return Task.CompletedTask;
|
|
});
|
|
|
|
var channel = GrpcChannel.ForAddress("https://localhost:5001", new GrpcChannelOptions
|
|
{
|
|
Credentials = ChannelCredentials.Create(
|
|
new SslCredentials(),
|
|
credentials)
|
|
});
|
|
```
|
|
|
|
### TypeScript with Metadata
|
|
|
|
```typescript
|
|
const metadata = new grpc.Metadata();
|
|
metadata.add('authorization', `Bearer ${accessToken}`);
|
|
|
|
const call = client.subscribeToPersistent(metadata);
|
|
```
|
|
|
|
### Go with Interceptor
|
|
|
|
```go
|
|
func authInterceptor(token string) grpc.UnaryClientInterceptor {
|
|
return func(
|
|
ctx context.Context,
|
|
method string,
|
|
req, reply interface{},
|
|
cc *grpc.ClientConn,
|
|
invoker grpc.UnaryInvoker,
|
|
opts ...grpc.CallOption,
|
|
) error {
|
|
ctx = metadata.AppendToOutgoingContext(ctx, "authorization", fmt.Sprintf("Bearer %s", token))
|
|
return invoker(ctx, method, req, reply, cc, opts...)
|
|
}
|
|
}
|
|
|
|
conn, err := grpc.Dial(
|
|
address,
|
|
grpc.WithTransportCredentials(credentials.NewTLS(tlsConfig)),
|
|
grpc.WithUnaryInterceptor(authInterceptor(token)),
|
|
)
|
|
```
|
|
|
|
### Python with Credentials
|
|
|
|
```python
|
|
call_credentials = grpc.access_token_call_credentials(access_token)
|
|
channel_credentials = grpc.ssl_channel_credentials()
|
|
composite_credentials = grpc.composite_channel_credentials(
|
|
channel_credentials,
|
|
call_credentials
|
|
)
|
|
|
|
channel = grpc.secure_channel('localhost:5001', composite_credentials)
|
|
```
|
|
|
|
## Best Practices
|
|
|
|
### ✅ DO
|
|
|
|
- Use secure channels in production (TLS)
|
|
- Implement reconnection logic
|
|
- Handle errors gracefully
|
|
- Use async/await patterns
|
|
- Close channels properly
|
|
- Use appropriate timeouts
|
|
- Implement authentication
|
|
- Log connection lifecycle
|
|
|
|
### ❌ DON'T
|
|
|
|
- Don't use insecure channels in production
|
|
- Don't ignore connection errors
|
|
- Don't block event processing
|
|
- Don't leak resources (unclosed channels)
|
|
- Don't use very short timeouts
|
|
- Don't skip authentication
|
|
- Don't ignore cancellation
|
|
- Don't forget error handling
|
|
|
|
## See Also
|
|
|
|
- [gRPC Streaming Overview](README.md)
|
|
- [Persistent Subscriptions](persistent-subscriptions.md)
|
|
- [Queue Subscriptions](queue-subscriptions.md)
|
|
- [gRPC Integration](../../grpc-integration/README.md)
|
|
- [Event Streaming Overview](../README.md)
|