It is a drop in protocol replacement for gRPC; the .NET gRPC API has no hard bindings to either the marshaller or the underlying transport; protobuf-net.Grpc offered ways to change the marshaller (for example, allowing you to use protobuf-net) - and now protobuf-net.GrpcLite allows you to change the transport - from HTTP/2 to a custom transport inspired by HTTP/2, but simpler and with lower overheads. It is also fully managed, unlike HTTP/2 which often required unmanaged library or OS support.
The transport is not compatible with regular HTTP/2 gRPC, but: all of your existing gRPC code should continue to function, as long as you have a client and server that can talk the same dialect.
At the client, instead of using var channel = new Channel(...); (unmanaged HTTP/2) or var channel = GrpcChannel.ForAddress(...); (managed HTTP/2), you would use something like:
using var channel = await ConnectionFactory.ConnectSocket(endPoint).AsFrames().CreateChannelAsync();The rest of your client code shouldn't change at all. This is just one example; other terminators are possible - for example, anything that can provide a Stream should work, including support
for things like TLS, compression, named pipes, etc.
At the server, the code is currently a bit closer to the unmanaged server implementation (the server does not integrate deeply into Kestrel, although it works fine inside a Kestrel process); service-binding
is via the .ServiceBinder:
var server = new LiteServer();
server.ServiceBinder.Bind(new MyService()); // contract-first example, generated via protoc
// alternative if not also using protobuf-net.Grpc, which provides the Bind API
// YourService.BindService(server.ServiceBinder, new MyService());
_ = server.ListenAsync(ConnectionFactory.ListenSocket(endpoint).AsStream().AsFrames());
// ... note: leave your server running here, until you're ready to exit!
server.Stop();The ListenAsync call will listen for multiple connections; a single server can listen to many connections on many different listeneres at once - for example, you could
listen to multiple TCP ports, with/without TLS. Your MyService instance will be activated just like it would have been with the unmanaged server host.
TLS is provided via SslStream, and works with or without client certificates; the WithTls() connector optionally accepts callbacks for providing user certificates (client), or
validating remote certificates (client or server); the AuthenticateAsServer() connector accepts a server certificate, and optionally demands client certificates; for example:
// TCP server; no TLS
_ = server.ListenAsync(ConnectionFactory.ListenSocket(endpoint).AsStream().AsFrames());
// TCP server; TLS, no client certs
_ = server.ListenAsync(ConnectionFactory.ListenSocket(endpoint).AsStream().WithTls().AuthenticateAsServer(serverCert).AsFrames());
// TCP server; TLS, client certs (validated via userCheck)
_ = server.ListenAsync(ConnectionFactory.ListenSocket(endpoint).AsStream().WithTls(userCheck).AuthenticateAsServer(serverCert, clientCertificateRequired: true).AsFrames());
``` c#
// TCP client; no TLS
using var channel = await ConnectionFactory.ConnectSocket(endPoint).AsFrames().CreateChannelAsync();
// TCP client; TLS, using default server validation and certificate selection
using var channel = await ConnectionFactory.ConnectSocket(endpoint).AsStream().WithTls().AuthenticateAsClient("mytestserver").AsFrames().CreateChannelAsync();
// TCP client; TLS, using custom server validation and certificate selection
using var channel = await ConnectionFactory.ConnectSocket(endpoint).AsStream().WithTls(serverCheck, certSelector).AuthenticateAsClient("mytestserver").AsFrames().CreateChannelAsync();At the client, code-first works exactly as it always has; just use the .CreateClient<TService>() method on the channel.
As the server, binding code-first serves to the custom server is uses the .Binder API:
server.ServiceBinder.AddCodeFirst(...);Client-side interceptors work exactly like they do in all scenarios.
To register a server-side interceptor, the Intercept() API is used alongside the .ServiceBinder:
server.ServiceBinder.Intercept(...).Bind(new MyService()); // contract-first example, generated via protoc
server.ServiceBinder.Intercept(...).AddCodeFirst(...); // code-first
// alternative for contract-first if not also using protobuf-net.Grpc, which provides the Bind API
// YourService.BindService(server.ServiceBinder.Intercept(...), new MyService());It currently targets .NET Framework 4.7.2 up to .NET 6.0, using newer features when available. It is still very experimental - but most core things should work; feedback is welcome.
Known gaps:
- gRPC auth (although transport auth works fine)
- per-stream service activation (rather than singleton)
- testing needs more coverage
- per-stream backoff negotiation; designed, not yet implemented
- for some reason the server implementation isn't working 100% with SAEA currently - hence .AsStream().AsFrames()instead of just.AsFrames()
- open question around interceptor order