Protocols
The protocol layer constructs connections, decoders, and outbound encoders from an explicit configuration. Protocol.Create(...) produces an IProtocolFactory; that factory can create routers, standalone parsers, and virtual connections.
Core Contracts
Contract | Responsibility |
|---|---|
| Registers protocol parsers, optional encoders, port types, features, formatters, logging, metrics, and time services. |
| Exposes available registrations and creates configured protocol objects. |
| Supplies shared configuration to connection and parser implementations. |
| Consumes bytes through |
| Encodes messages for one outbound stream, reporting a capacity bound and the actual number of bytes written. |
| Adds optional encoder registration to a protocol parser builder. |
| Exposes optional encoder factories keyed by protocol ID. |
| Carries a protocol ID and display name. |
The current builder registers the standard transport types and simple formatter by default. It does not select an application protocol automatically. ClearAll() clears the registrations and resets logging, metrics, and time configuration; register everything needed after calling it.
Configuration
Production protocol packages provide their own registration extensions. The application selects those packages; the shared transport and inspection code can continue to use IProtocolMessage.
Outbound Encoders
Existing protocols need no new registration. DefaultProtocolEncoder delegates to the message's GetByteSize() and Serialize(ref Span<byte>), then reports the consumed span length. It serializes the message once and preserves the message's existing format, including serializers that consume fewer bytes than their reserved capacity.
A protocol that needs outbound stream state can register an encoder alongside its parser. For example, given the package's ProtocolInfo and encoder implementation:
The factory must return a fresh stateful encoder on each call. Each endpoint creates a registered encoder on its first send for that protocol and retains it for subsequent sends. Protocols without a custom registration use the shared stateless DefaultProtocolEncoder.Instance. Receive parsers have separate state. Registering an encoder does not register a parser or add a protocol to AvailableProtocols; the usual parser registration remains necessary. builder.Protocols.Clear() clears both parser and encoder registrations. ClearAll() also performs this reset.
IProtocolMessage, IProtocolBuilder, and IProtocolContext keep their existing members. Encoder registration is an optional capability: RegisterEncoder requires a builder implementing IProtocolEncoderBuilder and throws NotSupportedException otherwise. A context without IProtocolEncoderContext, or without a factory for the requested protocol ID, uses DefaultProtocolEncoder. Existing custom builders and contexts therefore continue to support their existing protocols without implementing the new interfaces.
For a manually owned stream, context.CreateEncoder(protocolId) creates the appropriate encoder. Retain that instance for the stream lifetime; calling it for every message would lose stream state. See Serialization for the encoder contract and explicit encoding helpers.
Parsing
A parser receives individual bytes. OnMessage publishes a decoded message. The ProtocolParser<TMessage, TMessageId> base class reports parsing failures through the OnError observable's R3 error-resume channel, so subscribe with an onErrorResume callback to observe them. Push returns a boolean recognition result rather than the decoded object. A true result does not guarantee that OnMessage was raised: for example, ExampleParser can recognize a complete frame whose message ID or payload is rejected by the base parser.
An endpoint can run several registered parsers over incoming data. In the current endpoint read loop, a true parser result resets the parser set and stops dispatching that byte to the remaining parsers. Do not assume that every parser sees one independently framed stream or that every input byte produces a message.
Ownership And Diagnostics
Dispose standalone parsers, routers, and virtual connections when their owner finishes. A router owns its ports; ports own their endpoints. Keep subscriber lifetimes explicit as well.
VirtualConnection.Dispose() and DisposeAsync() disconnect the byte-forwarding and logging subscriptions before disposing either port. This prevents stream completion from being forwarded to an already disposed subject. Each port still completes its own public message observables. Dispose the virtual connection as the owner of both sides.
Use the configured TimeProvider, logger factory, and meter factory when implementing protocol components. This keeps timing and diagnostics consistent and permits deterministic tests.
Source Files
src/Asv.IO/Protocol/Protocol.csandIProtocolBuilder.cs.src/Asv.IO/Protocol/Parser/IProtocolParser.csandProtocolParser.cs.src/Asv.IO/Protocol/Encoder/IProtocolEncoder.cs,ProtocolEncoderHelper.cs, andDefaultProtocolEncoder.cs.src/Asv.IO/Example/Protocol/ExampleProtocol.cs.
See Connections for transport composition and Features and formatters for message processing.