Overview
Silky's RPC communication is built on DotNetty (.NET port of Netty), using long-lived TCP connections to transport serialized messages. The server listens on a dedicated RPC port (default 2200), receives call requests from other microservice instances, executes the business logic, and writes the result back to the connection.
DotNetty Channel (network layer)
│ Receives TCP byte stream
▼
DecoderHandler
│ TransportMessageDecoder: bytes → TransportMessage
▼
ServerHandler (dispatch layer)
│ Passes TransportMessage to MessageListenerBase
▼
DefaultServerMessageReceivedHandler (processing layer)
│ Locate ServiceEntry → parse parameters → execute → build response
▼
EncoderHandler
│ TransportMessageEncoder: RemoteResultMessage → bytes
▼
DotNetty Channel writes response back
DotNetty Server Startup
DotNettyTcpServerMessageListener starts the DotNetty server during module Initialize():
public async Task Listen()
{
var bootstrap = new ServerBootstrap();
// I/O model selection
if (_rpcOptions.UseLibuv)
{
m_bossGroup = new DispatcherEventLoopGroup();
m_workerGroup = new WorkerEventLoopGroup((DispatcherEventLoopGroup)m_bossGroup);
bootstrap.Channel<TcpServerChannel>();
}
else
{
m_bossGroup = new MultithreadEventLoopGroup(1); // 1 Accept thread
m_workerGroup = new MultithreadEventLoopGroup(); // CPU cores × 2 Worker threads
bootstrap.Channel<TcpServerSocketChannel>();
}
bootstrap
.Group(m_bossGroup, m_workerGroup)
.Option(ChannelOption.SoBacklog, 128)
.ChildHandler(new ActionChannelInitializer<IChannel>(channel =>
{
var pipeline = channel.Pipeline;
// Optional TLS
if (_rpcOptions.IsSsl)
{
var cert = new X509Certificate2(
_rpcOptions.SslCertificateName,
_rpcOptions.SslCertificatePassword);
pipeline.AddLast(TlsHandler.Server(cert));
}
// Idle detection (heartbeat)
pipeline.AddLast(new IdleStateHandler(0, 0, _rpcOptions.HeartbeatWatchIntervalSeconds));
// Framing (length-prefix protocol)
pipeline.AddLast(new LengthFieldPrepender(4));
pipeline.AddLast(new LengthFieldBasedFrameDecoder(int.MaxValue, 0, 4, 0, 4));
// Message codec
pipeline.AddLast(_transportMessageDecoder);
pipeline.AddLast(_transportMessageEncoder);
// Business handler
pipeline.AddLast(new ServerHandler(async (ctx, message) =>
{
await OnReceived(ctx.Channel, message);
}));
}));
m_boundChannel = await bootstrap.BindAsync(_server.RpcEndpoint.Port);
}
Channel Pipeline Summary
| Handler | Role |
|---|---|
TlsHandler (optional) | SSL/TLS encryption/decryption |
IdleStateHandler | Idle detection — triggers heartbeat events after inactivity |
LengthFieldPrepender / LengthFieldBasedFrameDecoder | Length-prefix framing — prevents TCP packet fragmentation/sticking |
TransportMessageDecoder | Deserializes JSON bytes into TransportMessage |
TransportMessageEncoder | Serializes TransportMessage into JSON bytes |
ServerHandler | Dispatches received messages to DefaultServerMessageReceivedHandler |
TransportMessage — Wire Format
Every RPC message (request and response) is wrapped in a TransportMessage:
| Field | Type | Description |
|---|---|---|
Id | string | UUID — used to correlate responses to requests |
ContentType | string | Message content type (e.g., RemoteInvokeMessage, RemoteResultMessage) |
Content | string | JSON-serialized payload |
DefaultServerMessageReceivedHandler
This handler executes on the worker thread after a message is decoded:
public async Task ReceivedAsync(IMessageSender sender, TransportMessage message)
{
// 1. Deserialize the RemoteInvokeMessage from message.Content
var invokeMessage = message.GetContent<RemoteInvokeMessage>();
// 2. Locate the ServiceEntry by ServiceEntryId
var serviceEntry = _serviceEntryLocator.GetServiceEntryById(invokeMessage.ServiceEntryId);
// 3. Populate RpcContext from message attachments (UserId, TenantId, TraceId, ...)
RpcContext.Context.SetAttachments(invokeMessage.Attachments);
RpcContext.Context.SetTransAttachments(invokeMessage.TransAttachments);
// 4. Parse parameters according to ParameterDescriptors
var parameters = _parameterResolver.Resolve(serviceEntry, invokeMessage);
// 5. Execute (local executor)
var result = await _executor.Execute(serviceEntry, parameters, invokeMessage.ServiceKey);
// 6. Build and send RemoteResultMessage
var resultMessage = new RemoteResultMessage { Result = result };
await sender.SendMessageAsync(
new TransportMessage(message.Id, resultMessage)); // same UUID for correlation
}
RpcContext — Implicit Call Chain Propagation
RpcContext is a thread-local (actually AsyncLocal) context that carries cross-cutting data across the call chain without explicit parameter threading:
| Key | Description |
|---|---|
UserId | Current authenticated user ID |
TenantId | Current tenant ID (multi-tenancy) |
TraceId | Distributed tracing correlation ID |
ServiceKey | Target service implementation key |
TransactionContext | TCC distributed transaction context |
Propagation: On the client side, RpcContext.Attachments are copied into RemoteInvokeMessage.Attachments before sending. On the server side, they are restored from the message into the server-side RpcContext — transparently propagating user identity, tenant, and trace information across microservice boundaries.
