gRPC Fundamentals
💡 Vad är gRPC?
gRPC (Google Remote Procedure Call) är som att ringa en funktion på en annan dator!
Tänk dig att du har en kompis som är superb på matte. Istället för att skicka SMS:ar fram och tillbaka (REST), ringer du direkt och säger “beräkna 15 * 23” och får svaret “345” tillbaka direkt. Det är gRPC - snabbt, effektivt och känns som att anropa lokala funktioner.
🎯 TL;DR
gRPC = snabb, binär kommunikation mellan tjänster. Använder Protocol Buffers istället för JSON. Perfekt för microservices som behöver hög prestanda och typ-säkerhet.
📋 Efter att ha läst detta kommer du att kunna
- Förstå skillnaden mellan gRPC och REST
- Definiera gRPC-tjänster med Protocol Buffers
- Implementera gRPC server och klient i C-Sharp
- Hantera olika gRPC-kommunikationsmönster
- Veta när gRPC är bättre än REST
🔧 Grundprinciper
Princip 1: Binär protokoll
Data skickas som binär istället för text (JSON) - mycket snabbare.
Princip 2: Contract-first
Du definierar tjänsten i en .proto-fil först, sedan genereras koden.
Princip 3: Typ-säkerhet
Kompilern fångar fel tidigt - inga runtime-överraskningar.
💭 gRPC vs REST
💬 Skillnaden mellan gRPC och REST:
REST:
- HTTP/1.1 eller HTTP/2
- JSON (text-baserat)
- Request/Response
- Människor kan läsa URL:er
- Webbrowser-kompatibel
gRPC:
- HTTP/2 (binärt)
- Protocol Buffers (binärt)
- 4 kommunikationsmönster
- Mer effektivt
- Behöver specialklienter
🚀 gRPC i praktiken
🟢 Definiera tjänsten med Protocol Buffers
// user.proto
syntax = "proto3";
option csharp_namespace = "GrpcDemo";
package user;
// Tjänst-definition
service UserService {
// Unary call - en request, ett svar
rpc GetUser (GetUserRequest) returns (UserResponse);
// Server streaming - en request, många svar
rpc GetAllUsers (Empty) returns (stream UserResponse);
// Client streaming - många requests, ett svar
rpc CreateUsers (stream CreateUserRequest) returns (CreateUsersResponse);
// Bidirectional streaming - många requests, många svar
rpc ChatWithUsers (stream ChatMessage) returns (stream ChatMessage);
}
// Meddelande-typer
message GetUserRequest {
int32 id = 1;
}
message CreateUserRequest {
string name = 1;
string email = 2;
}
message UserResponse {
int32 id = 1;
string name = 2;
string email = 3;
string created_at = 4;
}
message CreateUsersResponse {
int32 users_created = 1;
}
message ChatMessage {
string user_name = 1;
string message = 2;
string timestamp = 3;
}
message Empty {}
🟡 gRPC Server i ASP.NET Core
// Install: dotnet add package Grpc.AspNetCore
// dotnet add package Google.Protobuf
// dotnet add package Grpc.Tools
// Program.cs
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddGrpc();
builder.Services.AddScoped<IUserRepository, UserRepository>();
var app = builder.Build();
app.MapGrpcService<UserGrpcService>();
// För utveckling - aktivera gRPC reflection
if (app.Environment.IsDevelopment())
{
app.MapGrpcReflectionService();
}
app.Run();
// UserGrpcService.cs - implementera tjänsten
public class UserGrpcService : UserService.UserServiceBase
{
private readonly IUserRepository _userRepository;
private readonly ILogger<UserGrpcService> _logger;
public UserGrpcService(IUserRepository userRepository, ILogger<UserGrpcService> logger)
{
_userRepository = userRepository;
_logger = logger;
}
// Unary call - en request, ett svar
public override async Task<UserResponse> GetUser(GetUserRequest request, ServerCallContext context)
{
_logger.LogInformation("Getting user with ID: {UserId}", request.Id);
var user = await _userRepository.GetUserByIdAsync(request.Id);
if (user == null)
{
throw new RpcException(new Status(StatusCode.NotFound, "User not found"));
}
return new UserResponse
{
Id = user.Id,
Name = user.Name,
Email = user.Email,
CreatedAt = user.CreatedAt.ToString("yyyy-MM-dd HH:mm:ss")
};
}
// Server streaming - en request, många svar
public override async Task GetAllUsers(Empty request, IServerStreamWriter<UserResponse> responseStream, ServerCallContext context)
{
var users = await _userRepository.GetAllUsersAsync();
foreach (var user in users)
{
// Kontrollera om klienten har avbrutit
if (context.CancellationToken.IsCancellationRequested)
break;
await responseStream.WriteAsync(new UserResponse
{
Id = user.Id,
Name = user.Name,
Email = user.Email,
CreatedAt = user.CreatedAt.ToString("yyyy-MM-dd HH:mm:ss")
});
// Simulera lite fördröjning
await Task.Delay(100, context.CancellationToken);
}
}
// Client streaming - många requests, ett svar
public override async Task<CreateUsersResponse> CreateUsers(IAsyncStreamReader<CreateUserRequest> requestStream, ServerCallContext context)
{
int usersCreated = 0;
await foreach (var request in requestStream.ReadAllAsync())
{
var user = new User
{
Name = request.Name,
Email = request.Email,
CreatedAt = DateTime.UtcNow
};
await _userRepository.CreateUserAsync(user);
usersCreated++;
_logger.LogInformation("Created user: {UserName}", request.Name);
}
return new CreateUsersResponse { UsersCreated = usersCreated };
}
// Bidirectional streaming - många requests, många svar
public override async Task ChatWithUsers(IAsyncStreamReader<ChatMessage> requestStream, IServerStreamWriter<ChatMessage> responseStream, ServerCallContext context)
{
// Läs inkommande meddelanden i bakgrunden
var readTask = Task.Run(async () =>
{
await foreach (var message in requestStream.ReadAllAsync())
{
_logger.LogInformation("Received chat message from {User}: {Message}", message.UserName, message.Message);
// Echo tillbaka meddelandet med tidsstämpel
var response = new ChatMessage
{
UserName = "Server",
Message = $"Echo: {message.Message}",
Timestamp = DateTime.UtcNow.ToString("HH:mm:ss")
};
await responseStream.WriteAsync(response);
}
});
await readTask;
}
}
🔴 gRPC Klient
// Program.cs för klient-applikation
using Grpc.Net.Client;
using GrpcDemo;
// Skapa gRPC-kanal
using var channel = GrpcChannel.ForAddress("https://localhost:5001");
var client = new UserService.UserServiceClient(channel);
// 1. Unary call
var userResponse = await client.GetUserAsync(new GetUserRequest { Id = 1 });
Console.WriteLine($"User: {userResponse.Name} ({userResponse.Email})");
// 2. Server streaming
using var streamingCall = client.GetAllUsers(new Empty());
await foreach (var user in streamingCall.ResponseStream.ReadAllAsync())
{
Console.WriteLine($"Streamed user: {user.Name}");
}
// 3. Client streaming
using var clientStreamingCall = client.CreateUsers();
// Skicka flera användare
await clientStreamingCall.RequestStream.WriteAsync(new CreateUserRequest { Name = "Anna", Email = "anna@example.com" });
await clientStreamingCall.RequestStream.WriteAsync(new CreateUserRequest { Name = "Bert", Email = "bert@example.com" });
await clientStreamingCall.RequestStream.CompleteAsync();
var createResponse = await clientStreamingCall;
Console.WriteLine($"Created {createResponse.UsersCreated} users");
// 4. Bidirectional streaming
using var chatCall = client.ChatWithUsers();
// Skicka meddelanden i bakgrunden
var sendTask = Task.Run(async () =>
{
await chatCall.RequestStream.WriteAsync(new ChatMessage { UserName = "Marcus", Message = "Hej från klient!" });
await chatCall.RequestStream.WriteAsync(new ChatMessage { UserName = "Marcus", Message = "Hur mår du?" });
await chatCall.RequestStream.CompleteAsync();
});
// Läs svar
await foreach (var response in chatCall.ResponseStream.ReadAllAsync())
{
Console.WriteLine($"{response.UserName} [{response.Timestamp}]: {response.Message}");
}
await sendTask;
📊 gRPC kommunikationsmönster
| Mönster | Beskrivning | Användningsfall |
|---|---|---|
| Unary | En request → ett svar | Vanligaste, som REST |
| Server Streaming | En request → många svar | Live data, notifikationer |
| Client Streaming | Många requests → ett svar | File upload, batch processing |
| Bidirectional | Många requests ↔ många svar | Chat, real-time collaboration |
🔧 Avancerade gRPC-funktioner
Authentication och Authorization
// JWT Token authentication
public class JwtAuthenticationHandler : AuthenticationHandler<AuthenticationSchemeOptions>
{
// Implementation för JWT validation
}
// I Program.cs
builder.Services.AddAuthentication("Bearer")
.AddScheme<AuthenticationSchemeOptions, JwtAuthenticationHandler>("Bearer", null);
builder.Services.AddAuthorization();
// I gRPC service
[Authorize]
public override async Task<UserResponse> GetUser(GetUserRequest request, ServerCallContext context)
{
var user = context.GetHttpContext().User;
var userId = user.FindFirst("sub")?.Value;
// Använd userId för authorization logic
}
Error Handling
public override async Task<UserResponse> GetUser(GetUserRequest request, ServerCallContext context)
{
try
{
if (request.Id <= 0)
{
throw new RpcException(new Status(StatusCode.InvalidArgument, "User ID must be positive"));
}
var user = await _userRepository.GetUserByIdAsync(request.Id);
if (user == null)
{
throw new RpcException(new Status(StatusCode.NotFound, $"User with ID {request.Id} not found"));
}
return MapToUserResponse(user);
}
catch (Exception ex) when (!(ex is RpcException))
{
_logger.LogError(ex, "Error getting user {UserId}", request.Id);
throw new RpcException(new Status(StatusCode.Internal, "Internal server error"));
}
}
Interceptors (Middleware för gRPC)
public class LoggingInterceptor : Interceptor
{
private readonly ILogger<LoggingInterceptor> _logger;
public LoggingInterceptor(ILogger<LoggingInterceptor> logger)
{
_logger = logger;
}
public override async Task<TResponse> UnaryServerHandler<TRequest, TResponse>(
TRequest request,
ServerCallContext context,
UnaryServerMethod<TRequest, TResponse> continuation)
{
var method = context.Method;
_logger.LogInformation("Starting gRPC call: {Method}", method);
var stopwatch = Stopwatch.StartNew();
try
{
var response = await continuation(request, context);
_logger.LogInformation("Completed gRPC call: {Method} in {ElapsedMs}ms", method, stopwatch.ElapsedMilliseconds);
return response;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error in gRPC call: {Method}", method);
throw;
}
}
}
// Registrera interceptor
builder.Services.AddGrpc(options =>
{
options.Interceptors.Add<LoggingInterceptor>();
});
⚠️ gRPC begränsningar och överväganden
✅ Använd gRPC när:
- Microservices som kommunicerar internt
- Hög prestanda är kritiskt
- Stark typ-säkerhet behövs
- Streaming av data krävs
- Polyglot-miljöer (flera språk)
❌ Undvik gRPC när:
- Webbrowsers-kompatibilitet krävs
- RESTful web API för externa kunder
- Simple CRUD-operationer
- Teamet saknar gRPC-kunskap
- Debugging-vänlighet är viktigare än prestanda
Vanliga misstag:
// MISSTAG: Glömma hantera exceptions
public override async Task<UserResponse> GetUser(GetUserRequest request, ServerCallContext context)
{
var user = await _userRepository.GetUserByIdAsync(request.Id); // Kan kasta exception
return MapToUserResponse(user); // Kan vara null!
}
// RÄTT: Proper error handling
public override async Task<UserResponse> GetUser(GetUserRequest request, ServerCallContext context)
{
try
{
var user = await _userRepository.GetUserByIdAsync(request.Id);
if (user == null)
{
throw new RpcException(new Status(StatusCode.NotFound, "User not found"));
}
return MapToUserResponse(user);
}
catch (Exception ex) when (!(ex is RpcException))
{
throw new RpcException(new Status(StatusCode.Internal, "Internal error"));
}
}
🛠️ Testing gRPC
// gRPC unit test
[Test]
public async Task GetUser_WithValidId_ReturnsUser()
{
// Arrange
var mockRepository = new Mock<IUserRepository>();
mockRepository.Setup(r => r.GetUserByIdAsync(1))
.ReturnsAsync(new User { Id = 1, Name = "Marcus", Email = "marcus@example.com" });
var service = new UserGrpcService(mockRepository.Object, Mock.Of<ILogger<UserGrpcService>>());
var context = TestServerCallContext.Create();
// Act
var response = await service.GetUser(new GetUserRequest { Id = 1 }, context);
// Assert
Assert.AreEqual("Marcus", response.Name);
Assert.AreEqual("marcus@example.com", response.Email);
}
📚 Sammanfattning
gRPC är kraftfullt för service-till-service kommunikation:
- Binär protokoll för hög prestanda
- Stark typ-säkerhet med Protocol Buffers
- 4 kommunikationsmönster för olika behov
- HTTP/2 med multiplexing och streaming
- Polyglot - fungerar i många språk
- Contract-first utveckling
- Inte browser-kompatibel utan proxy
🎯 Övningar
- 🟢 Grundläggande: Skapa en enkel Calculator gRPC-tjänst med Add/Subtract
- 🟡 Utmanande: Implementera server streaming för att skicka live stock prices
- 🔴 Expert: Bygg en chat-applikation med bidirectional streaming
😄 Obligatorisk dad joke
Varför gillar utvecklare gRPC så mycket? För att det är det enda protokollet som är snabbare än ljuset - åtminstone snabbare än deras tidigare JSON-API:er! ⚡