Skip to content

Commit

Permalink
v1 stable (#38)
Browse files Browse the repository at this point in the history
  • Loading branch information
goncalo-oliveira authored Jul 2, 2024
1 parent a417d26 commit 77934d9
Show file tree
Hide file tree
Showing 102 changed files with 3,360 additions and 1,306 deletions.
5 changes: 5 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
[*.{cs,vb}]

# IDE0130: Namespace does not match folder structure
dotnet_diagnostic.IDE0130.severity = none
[*.{cs,vb}]
156 changes: 94 additions & 62 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -178,66 +178,63 @@ Install the package from NuGet
dotnet add package Faactory.Channels
```

To quickly bootstrap a server, we need to inject a *hosted service*. Then we need to configure the listening options and set up the input and output pipelines. Here's an example
The first step is to add the library to the DI container and configure the channel pipelines. These configurations are always *named*, which means that we can have multiple channel configurations for different purposes. Nonetheless, if we only need one channel pipeline, we can do it all at once by configuring a *default` configuration.

```csharp
IServiceCollection services = ...;

// add our hosted service
services.AddChannels( channel =>
{
// configure options
channel.Configure( options =>
{
options.Port = 8080;
options.Backlog = 30;
} );

// set up input pipeline
channel.AddInputAdapter<ExampleDecoderChannelAdapter>()
.AddInputHandler<MyChannelHandler>();
channel.AddInputAdapter<ExampleAdapter>()
.AddInputHandler<ExampleHandler>();

// set up output pipeline
channel.AddOutputAdapter<ExampleEncoderAdapter>();
channel.AddOutputAdapter<ExampleAdapter>();
} );
```

To boostrap the client, we'll need to register the factory with a service provider. Then, similarly to the server, we need to configure the channel options and set up the input and output pipelines. Here's an example
If we need to configure multiple channel pipelines, we use the parameterless method, which returns a builder that allows us to configure named channels.

```csharp
IServiceCollection services = ...
IServiceCollection services = ...;

// add our client factory
services.AddClientChannelFactory( channel =>
{
// configure options
channel.Configure( options =>
services.AddChannels()
.Add( "channel1", channel =>
{
options.Host = "localhost";
options.Port = 8080;
} );

// set up input pipeline
channel.AddInputAdapter<ExampleDecoderChannelAdapter>()
.AddInputHandler<MyChannelHandler>();
// set up input pipeline
channel.AddInputAdapter<ExampleAdapter>()
.AddInputHandler<ExampleHandler1>();

// set up output pipeline
channel.AddOutputAdapter<ExampleAdapter>();
} )
.Add( "channel2", channel =>
{
// set up input pipeline
channel.AddInputAdapter<ExampleAdapter>()
.AddInputHandler<ExampleHandler2>();

// set up output pipeline
channel.AddOutputAdapter<ExampleEncoderAdapter>();
} );
// set up output pipeline
channel.AddOutputAdapter<ExampleAdapter>();
} );
```

Then, where needed, we can create a client channel by using the factory
## Listeners

After the channels are configured, we need to add the listener services. The library provides listeners for TCP and UDP channels. When adding a listener, we can specify the channel name and the options, or we can use the default channel configuration.

```csharp
IClientChannelFactory channelFactory = ...;
var channel = await channelFactory.CreateAsync();
IServiceCollection services = ...;

await channel.WriteAsync( new MyData
{
// ...
} );
services.AddTcpChannelListener( 8080 ); // TCP listener with default channel configuration
// services.AddTcpChannelListener( "channel1", 8080 ); // TCP listener with named channel configuration
// services.AddUdpChannelListener( 7701 ); // UDP listener with default channel configuration
```

We can use multiple listeners in the same application, each with its own configuration.

## Adapters and Buffers

Although raw data handling in the adapters can be done with `Byte[]`, it is recommended to use a `IByteBuffer` instance instead, particularly for reading data. You can read more about it [here](README.buffers.md).
Expand Down Expand Up @@ -355,47 +352,82 @@ public class SampleIdentityHandler : ChannelHandler<IdentityInformation>

## Idle Channels

> [!NOTE]
> On previous releases, the idle detection mechanism was available and active by default. Since version 0.5 this is no longer true and the idle detection service needs to be added explicitly.
There's a ready-made service that monitors channel activity and detects if a channel has become idle or unresponsive. When that happens, the underlying socket is disconnected and the channel closed.

To enable this service, just add it as a channel service using the builder extensions
By default, channels are initialized with an idle detection mechanism that closes the channel if no data is received or sent after a certain amount of time, which defaults to 60 seconds. This mechanism can be disabled or customized through the channel options.

```csharp
IChannelBuilder channel = ...;
IServiceCollection services = ...;

services.AddChannels( channel =>
{
channel.Configure( options =>
{
// this is the default setting; added here just for clarity
options.IdleTimeout = TimeSpan.FromSeconds( 60 );
} );

channel.AddIdleChannelService();
// ...
} );
```

The default detection mode is `IdleDetectionMode.Auto`, which attempts to actively verify if the underlying socket is still connected and if not, closes the channel.
To disable the idle detection mechanism, set the `IdleTimeout` property to `TimeSpan.Zero`.

> [!TIP]
> The idle detection mechanism is available for all channel types: TCP, UDP and WebSockets.
## Client

There's also a hard timeout of 60 seconds by default; if no data is received or sent through the underlying socket before the timeout, the channel is closed. This timeout can be disabled when using the `IdleDetectionMode.Auto` method by setting its value to `TimeSpan.Zero`.
The library also provides a TCP/UDP client that can be used to connect to a server. This client automatically connects to the server and creates a channel instance when the connection is established. Connection drops are automatically handled and the client will attempt to reconnect.

Clients use the same channel configuration as the listeners, but they require additional configuration.

```csharp
IChannelBuilder channel = ...;
IServiceCollection services = ...;

channel.AddIdleChannelService( options =>
/*
this registers a default client with the default channel configuration
*/
services.AddChannelsClient( "tcp://example.host:8080" );

/*
we could also register the default client with a named channel configuration
*/
// services.AddChannelsClient( "channel1", "tcp://example.host:8080" );
/*
when we need to create a client, we only need to inject the `IChannelsClientFactory` interface
*/
public class MyClient
{
// these are the default settings; added here just for clarity
options.DetectionMode = IdleDetectionMode.Auto;
options.Timeout = TimeSpan.FromSeconds( 60 );
// to use Auto method without the hard timeout
//options.Timeout = TimeSpan.Zero;
} );
private readonly IChannelsClient client;

public MyClient( IChannelsClientFactory factory )
{
client = factory.CreateClient();
}

public Task ExecuteAsync()
{
// ...
}
}
```

Other detection modes only use the hard timeout on received and.or sent data.
If we need to configure multiple clients with different channel configurations, we need to register them as named clients instead.

```csharp
IChannelBuilder channel = ...;
IServiceCollection services = ...;

channel.AddIdleChannelService( options =>
{
// timeout (30s) to both received and sent data
options.DetectionMode = IdleDetectionMode.Both;
options.Timeout = TimeSpan.FromSeconds( 30 );
} );
/*
this registers a named client (client1) with the default channel configuration
*/
services.AddChannelsNamedClient( "client1", "tcp://example.host:8080" );

/*
this registers a named client (client2) with a named channel configuration (channel1)
*/
services.AddChannelsNamedClient( "client2", "channel1", "tcp://example.host:8080" );
```

The recommended detection mode depends on the nature of the communication and the specific requirements of the application. For most cases, the `IdleDetectionMode.Auto` is a good choice. If the quality of the connection is known to be poor (particularly mobile networks), applying a hard timeout on received and/or sent data might be more reliable.
## Web Sockets

Support for web sockets is available through the `Faactory.Channels.WebSockets` package. It provides ASP.NET Core routing and middleware for easy integration with this library. Read more about it [here](README.websockets.md).
120 changes: 120 additions & 0 deletions README.websockets.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
# Channels over WebSockets

An extension library to use the Channels middleware with WebSockets.

## Design

Unlike TCP or UDP Channels, the library does not provide a low-level server to handle WebSocket connections. Instead, it provides a factory to create a Channel on top of an existing WebSocket connection. It also provides extension methods to make it super easy to integrate with the ASP.NET Core middleware.

Unlike the TCP and UDP Channels, the WebSockets middleware does not deliver raw byte buffers to the input pipeline. Instead, it delivers a `WebSocketMessage` instance, which contains the message type and payload, since web sockets support both binary and text messages.

> [!TIP]
> Messages delivered to the input pipeline are always complete messages (`WebSocketMessage.EndOfMessage: true`). If a fragmented message is received, the channel will buffer the fragments until it is complete and only then deliver it to the input pipeline.
When writing to the output pipeline, the library provides built-in middleware for sending binary, text or fragmented messages, so either of the following types can be directly written to the output pipeline:

- `byte[]` which is sent as a (complete) binary message
- `IByteBuffer` also sent as a (complete) binary message
- `string` which is sent as a (complete) text message (utf-8 encoded)
- `WebSocketMessage` which gives you full control over the message type and content

## Usage

To make use of the library, you first need to add the NuGet package to your project:

```bash
dotnet add package Faactory.Channels.WebSockets
```

The channel pipeline configuration is done in the same way as with the TCP and UDP channels; WebSocket channels will use this same configuration. Nonetheless, additional services are required to set up the web sockets middleware.

```csharp
IServiceCollection services = ...;

/*
Configure the channel or channels as usual
*/
services.AddChannels( ... );

/*
register web sockets middleware services
*/
services.AddWebSocketChannels();
```

With the middleware in place, we now need to bind the web sockets endpoint to the middleware, which is done through route mapping:

```csharp
WebApplication app = ...;

// required WebSockets middleware
app.UseWebSockets();

// map the web socket endpoint to the Channels default pipeline
app.MapWebSocketChannel( "/ws" );
```

## Multiple Endpoints

It is possible to have multiple web sockets endpoints with different Channels pipeline configuration. This can be achieved by using named channels:

```csharp
IServiceCollection services = ...;

services.AddChannels()
.Add( "channel1", channel =>
{
// set up input pipeline
channel.AddInputAdapter<ExampleAdapter>()
.AddInputHandler<ExampleHandler1>();

// set up output pipeline
channel.AddOutputAdapter<ExampleAdapter>();
} )
.Add( "channel2", channel =>
{
// set up input pipeline
channel.AddInputAdapter<ExampleAdapter>()
.AddInputHandler<ExampleHandler2>();

// set up output pipeline
channel.AddOutputAdapter<ExampleAdapter>();
} );
```

The above configuration sets up two named pipelines, each with its own middleware. To bind the named pipelines to the web sockets endpoints, you now need to include the pipeline name in the route mapping:

```csharp
WebApplication app = ...;

// required WebSockets middleware
app.UseWebSockets();

// map the web socket endpoints
app.MapWebSocketChannel( "/ws/foo", "channel1" );
app.MapWebSocketChannel( "/ws/bar", "channel2" );
```

## Usage without ASP.NET Core

If you are not using ASP.NET Core, you can still use the library with any HTTP server that can produce a `System.Net.WebSockets.WebSocket` instance. In this case, you won't be using the `MapWebSocketChannel` extension method, but instead, you will need to manually create the channel by using the factory.

```csharp
WebSocket webSocket = ...;
IWebSocketChannelFactory factory = ...; // get the factory from the DI container
CancellationToken cancellationToken = ...; // optional: graceful shutdown if using the WaitAsync method
// create named channel using the pre-configured "channel1" pipeline
var channel = factory.CreateChannel( webSocket, "channel1" );

// optional: wait until the channel is closed or cancellation token is triggered
try
{
await channel.WaitAsync( cts.Token );
}
catch ( OperationCanceledException )
{ }

// recommended: close the channel and release resources
await channel.CloseAsync();
```
25 changes: 25 additions & 0 deletions channels.sln
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,12 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "core", "src\core\core.cspro
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "channels", "src\channels\channels.csproj", "{4AA0ABB5-EDA9-4F4F-B62C-55FCE32FB6A9}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "websockets-server", "examples\websockets-server\websockets-server.csproj", "{B68C48AA-BDC5-4CB0-9037-291280817813}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "websockets", "src\websockets\websockets.csproj", "{449490A7-130F-4756-88E6-A18C3C89EFB6}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "client-channel", "examples\client-channel\client-channel.csproj", "{379BD759-753B-40AD-833E-D84D2A5E35AE}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -55,6 +61,22 @@ Global
{4AA0ABB5-EDA9-4F4F-B62C-55FCE32FB6A9}.Debug|Any CPU.Build.0 = Debug|Any CPU
{4AA0ABB5-EDA9-4F4F-B62C-55FCE32FB6A9}.Release|Any CPU.ActiveCfg = Release|Any CPU
{4AA0ABB5-EDA9-4F4F-B62C-55FCE32FB6A9}.Release|Any CPU.Build.0 = Release|Any CPU
{B68C48AA-BDC5-4CB0-9037-291280817813}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{B68C48AA-BDC5-4CB0-9037-291280817813}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B68C48AA-BDC5-4CB0-9037-291280817813}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B68C48AA-BDC5-4CB0-9037-291280817813}.Release|Any CPU.Build.0 = Release|Any CPU
{449490A7-130F-4756-88E6-A18C3C89EFB6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{449490A7-130F-4756-88E6-A18C3C89EFB6}.Debug|Any CPU.Build.0 = Debug|Any CPU
{449490A7-130F-4756-88E6-A18C3C89EFB6}.Release|Any CPU.ActiveCfg = Release|Any CPU
{449490A7-130F-4756-88E6-A18C3C89EFB6}.Release|Any CPU.Build.0 = Release|Any CPU
{C23EDFA4-67FB-4F03-8C5E-98A9BF1FAE18}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C23EDFA4-67FB-4F03-8C5E-98A9BF1FAE18}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C23EDFA4-67FB-4F03-8C5E-98A9BF1FAE18}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C23EDFA4-67FB-4F03-8C5E-98A9BF1FAE18}.Release|Any CPU.Build.0 = Release|Any CPU
{379BD759-753B-40AD-833E-D84D2A5E35AE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{379BD759-753B-40AD-833E-D84D2A5E35AE}.Debug|Any CPU.Build.0 = Debug|Any CPU
{379BD759-753B-40AD-833E-D84D2A5E35AE}.Release|Any CPU.ActiveCfg = Release|Any CPU
{379BD759-753B-40AD-833E-D84D2A5E35AE}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand All @@ -66,6 +88,9 @@ Global
{74575D86-ADCA-485C-8157-4BEFC956F4F5} = {19EDD421-BE87-45A5-9D2D-17450725EA81}
{16A57310-D353-4B92-AC6A-8D35332D7CB6} = {19EDD421-BE87-45A5-9D2D-17450725EA81}
{4AA0ABB5-EDA9-4F4F-B62C-55FCE32FB6A9} = {19EDD421-BE87-45A5-9D2D-17450725EA81}
{B68C48AA-BDC5-4CB0-9037-291280817813} = {5BC48E10-DF9A-483F-B14A-C0E54D413530}
{449490A7-130F-4756-88E6-A18C3C89EFB6} = {19EDD421-BE87-45A5-9D2D-17450725EA81}
{379BD759-753B-40AD-833E-D84D2A5E35AE} = {5BC48E10-DF9A-483F-B14A-C0E54D413530}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {F3142138-D901-4324-B80A-70DD8C40863F}
Expand Down
Loading

0 comments on commit 77934d9

Please sign in to comment.