Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/nuget-publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ jobs:
- name: Test
run: dotnet test --no-build --configuration Release --verbosity normal
- name: Pack
run: dotnet pack src/Yamux/Yamux.csproj --no-build --configuration Release --output ./nupkg
run: dotnet pack src/Yamux.csproj --no-build --configuration Release --output ./nupkg
- name: Set Version Variable
run: |
VERSION=$(grep '<VersionPrefix>' Directory.Build.props | sed -E 's/.*>(.*)<.*/\1/')-$(grep '<VersionSuffix>' Directory.Build.props | sed -E 's/.*>(.*)<.*/\1/')
Expand Down
4 changes: 3 additions & 1 deletion Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<VersionPrefix>0.0.1</VersionPrefix>
<VersionSuffix>beta3</VersionSuffix>
<VersionSuffix>rc1</VersionSuffix>
<Authors>Paul Bleess</Authors>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<RepositoryUrl>https://github.com/pableess/yamux-dotnet</RepositoryUrl>
Expand All @@ -18,6 +18,8 @@
<EmbedUntrackedSources>true</EmbedUntrackedSources>
<IncludeSymbols>true</IncludeSymbols>
<SymbolPackageFormat>snupkg</SymbolPackageFormat>


</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="8.0.0" PrivateAssets="All" />
Expand Down
12 changes: 12 additions & 0 deletions Yamux.sln
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{8EC462FD-D22E-90A8-E5CE-7E832BA40C5D}"
ProjectSection(SolutionItems) = preProject
Directory.Build.props = Directory.Build.props
README.md = README.md
EndProjectSection
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".github", ".github", "{79365D20-9CC6-4DBB-A747-3965F5D77ABE}"
Expand All @@ -31,6 +32,17 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "workflows", "workflows", "{
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Yamux", "src\Yamux.csproj", "{077347A1-66A3-1807-CB3C-F247195DFE47}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "docs", "docs", "{15A0A680-B554-4726-BB82-8D5090925BDF}"
ProjectSection(SolutionItems) = preProject
docs\Channels.md = docs\Channels.md
docs\Exceptions.md = docs\Exceptions.md
docs\README.md = docs\README.md
docs\Session.md = docs\Session.md
docs\SessionChannelOptions.md = docs\SessionChannelOptions.md
docs\SessionOptions.md = docs\SessionOptions.md
docs\Statistics.md = docs\Statistics.md
EndProjectSection
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down
29 changes: 29 additions & 0 deletions docs/Channels.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Channel Interfaces

Yamux defines several interfaces for working with channels:

## ISessionChannel
Represents a logical Yamux channel.

### Properties
- `uint Id` � Channel ID.
- `Statstistics? Stats` � Bandwidth and byte statistics if enabled.`

### Methods
- `Task CloseAsync(TimeSpan? timeout = null, CancellationToken? cancel = null)` � Closes the channel. Throws `InvalidOperationException` if already closed.
- `Task FlushWritesAsync(CancellationToken? cancel)` � Ensures all written data is flushed.
- `void WaitForRemoteClose(TimeSpan timeout)` � Waits for the channel close acknowledgement from the remote side.
- `Task WhenRemoteCloseAsync(TimeSpan timeout)` � Waits for the channel close acknowledgement from the remote side.
- `void Abort()` � Aborts the channel immediately, sending a RST to the remote peer if the channel is not already closed`
## IWriteOnlySessionChannel
- `ValueTask WriteAsync(ReadOnlyMemory<byte> buffer, CancellationToken? token = null)` � Writes data to the channel.

## IReadOnlySessionChannel
- `PipeReader Input` � Gets the input pipe reader.

## IDuplexSessionChannel
- `Stream AsStream(bool leaveOpen = false)` � Gets a duplex stream for the channel.

---

See also: [Session](Session.md) for how to open and accept channels.
16 changes: 16 additions & 0 deletions docs/Exceptions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Exceptions

Yamux defines several exception types for error handling:

## YamuxException
Base exception for Yamux errors.

## SessionException
Thrown for session-level errors (e.g., protocol errors, session shutdown).

## SessionChannelException
Thrown for channel-specific errors (e.g., channel rejected, session closed).

---

All exceptions provide meaningful messages and, where appropriate, error codes. See the XML documentation in the code for details on when exceptions are thrown by public APIs.
201 changes: 201 additions & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
# Yamux .NET Library Documentation

This documentation provides an overview of the public APIs for the Yamux .NET library, which implements the Yamux multiplexing protocol for .NET applications.

## Table of Contents
- [Session](Session.md)
- [SessionChannelOptions](SessionChannelOptions.md)
- [SessionOptions](SessionOptions.md)
- [Statistics](Statistics.md)
- [ISessionChannel and Channel Interfaces](Channels.md)
- [Exceptions](Exceptions.md)

---

## Getting Started

Yamux allows you to multiplex multiple logical streams over a single network connection, stream, or custom transport. This is useful for building efficient networked applications that require multiple independent data streams over a single stream.

### Basic Setup Example

```csharp
using Yamux;
using System.Net.Sockets;

// Server side
var listener = new Socket(SocketType.Stream, ProtocolType.Tcp);
listener.Bind(new IPEndPoint(IPAddress.Loopback, 5000));
listener.Listen();
var clientSocket = await listener.AcceptAsync();
using var yamuxSession = new NetworkStream(clientSocket).AsYamuxSession(false);
yamuxSession.Start();

// Client side
var socket = new Socket(SocketType.Stream, ProtocolType.Tcp);
await socket.ConnectAsync(new IPEndPoint(IPAddress.Loopback, 5000));
using var yamuxSession = new NetworkStream(socket).AsYamuxSession(true);
yamuxSession.Start();
```

### Working with Channels

Once you have a Yamux session established, you can create and manage multiple channels over the same connection:

#### Opening and Accepting Channels

```csharp
// Opening a new channel (Client side)
var channel = await yamuxSession.OpenChannelAsync();

// Accepting incoming channels (Server side)
await foreach (var incomingChannel in yamuxSession.AcceptChannelsAsync())
{
// Process the new channel
_ = HandleChannelAsync(incomingChannel);
}
```

#### Reading from a Channel

You can read data from the channel using its `Input` pipe:
(or use `AsStream()` to get a Stream interface))

```csharp
async Task HandleChannelAsync(ISessionChannel channel)
{
try
{
var reader = channel.Input;
while (true)
{
var result = await reader.ReadAsync();
var buffer = result.Buffer;
try
{
if (result.IsCanceled)
break;
// Process the data in the buffer
foreach (var segment in buffer)
{
// Work with the data...
}
if (result.IsCompleted)
break;
}
finally
{
reader.AdvanceTo(buffer.Start, buffer.End);
}
}
}
finally
{
// Properly close and dispose the channel
await channel.CloseAsync();
channel.Dispose();
}
}
```

#### Writing to a Channel

You can write to a channel WriteAsync method:

#### Converting a Channel to a Stream

If you prefer working with streams, you can convert a channel to a `Stream`:

```csharp
using System.IO;

// Convert the channel to a Stream
using var stream = channel.AsStream();

// Now you can use standard Stream methods
await stream.WriteAsync(buffer, 0, buffer.Length);
await stream.FlushAsync();
```

#### Closing and Disposing Channels

It is important to properly close and dispose of channels when you are done with them to free resources and signal the remote peer:

```csharp
// Gracefully close the channel
channel.Close();

// either continue reading data from the input pipe until it is completed
// or you can separately wait for the remote peer to acknowledge the close
channel.WhenRemoteCloseAsync(TimeSpan.FromSeconds(5));

// Dispose the channel to release resources
channel.Dispose();
```

You can also use a `using` statement for automatic disposal, although if the channel is not closed before a dispose it is a forced close operation:

```csharp
using (var channel = await yamuxSession.OpenChannelAsync())
{
// Use the channel
// ...
await channel.CloseAsync();
}
```

### Channel Options

When opening a new channel, you can specify custom options:

```csharp
var options = new SessionChannelOptions
{
WindowSize = 256 * 1024, // 256KB window size
ReceiveBufferSize = 64 * 1024 // 64KB receive buffer
};

var channel = await yamuxSession.OpenChannelAsync(options);
```

### Monitoring Statistics with the Sampled Event

Yamux supports tracking statistics such as transfer speeds and throughput at both the session and channel level. To enable statistics, set the `EnableStatistics` property in `SessionOptions` when creating a session:

```csharp
var sessionOptions = new SessionOptions
{
EnableStatistics = true
};

using var yamuxSession = new NetworkStream(socket).AsYamuxSession(true, sessionOptions);
yamuxSession.Start();
```

You can subscribe to the `Sampled` event on the session or on individual channels to receive periodic updates:

```csharp
// Subscribe to session statistics
if (yamuxSession.Stats != null)
{
yamuxSession.Stats.Sampled += (sender, args) =>
{
Console.WriteLine($"[Session] Bytes sent: {yamuxSession.Stats.TotalBytesSent}, Bytes received: {yamuxSession.Stats.TotalBytesReceived}");
Console.WriteLine($"[Session] Send rate: {yamuxSession.Stats.SendRate}/sec, Receive rate: {yamuxSession.Stats.ReceiveRate}/sec");
};
}

// Subscribe to channel statistics after opening a channel
var channel = await yamuxSession.OpenChannelAsync();
if (channel.Stats != null)
{
channel.Stats.Sampled += (sender, args) =>
{
Console.WriteLine($"[Channel] Bytes sent: {channel.Stats.TotalBytesSent}, Bytes received: {channel.Stats.TotalBytesReceived}");
Console.WriteLine($"[Channel] Send rate: {channel.Stats.SendRate}/sec, Receive rate: {channel.Stats.ReceiveRate}/sec");
};
}
```

This allows you to monitor transfer speeds and throughput in real time for both the session and each channel.

See the individual API documentation for more details.
45 changes: 45 additions & 0 deletions docs/Session.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# Session

The `Session` class represents a Yamux session, allowing multiple logical streams over a single connection.

## Public API

### Constructors
- `internal Session(FrameFormatterBase frameFormatter, bool isClient, SessionOptions? options = null)`

### Properties
- `TimeSpan? RTT` � Most recent round-trip time measurement.
- `Statistics? Stats` � Bandwidth and byte statistics.
- `bool IsClosed` � Indicates if the session is closed.

### Methods
- `ValueTask<IDuplexSessionChannel> OpenChannelAsync(SessionChannelOptions options, bool waitForAcknowledgement = false, CancellationToken? cancel = null)`
- Opens a new channel. Throws `SessionException` if the session is closed or remote sent GoAway.
- `ValueTask<IDuplexSessionChannel> OpenChannelAsync(bool waitForAcknowledgement = false, CancellationToken? cancel = null)`
- Opens a new channel with default options.
- `ValueTask<IDuplexSessionChannel> AcceptAsync(CancellationToken? cancel = null)`
- Accepts a new channel.
- `ValueTask<IDuplexSessionChannel> AcceptAsync(SessionChannelOptions channelOptions, CancellationToken? cancel)`
- Accepts a new channel with custom options.
- `ValueTask<IReadOnlySessionChannel> AcceptReadOnlyChannelAsync(CancellationToken? cancel)`
- Accepts a new channel with read-only semantics.
- `Task<TimeSpan> PingAsync(CancellationToken cancellation)`
- Sends a ping and returns the round-trip time.
- `void Start()`
- Starts the session. Throws `SessionException` if already closed.
- `Task Close()`
- Closes the session.
- `Task GoAwayAsync()`
- Sends a GoAway frame to the remote peer indication no new channels.`
- `Task CloseOpenChannelsAsync`
- Tries to gracefully close all open channels on the session.
- `void Dispose()` / `ValueTask DisposeAsync()`
- Disposes the session and resources.

### Exceptions
- `SessionException` � Thrown for protocol or session errors.
- `SessionChannelException` � Thrown for channel-specific errors.

---

See [SessionOptions](SessionOptions.md) and [SessionChannelOptions](SessionChannelOptions.md) for configuration details.
28 changes: 28 additions & 0 deletions docs/SessionChannelOptions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# SessionChannelOptions

The `SessionChannelOptions` class configures the behavior of a Yamux session channel.

## Properties
- `uint ReceiveWindowSize` � Initial receive window size (default: 256 KB). Yamux spec default is 256 KB.
- `uint ReceiveWindowUpperBound` � Maximum receive window size (default: 16 MiB).
- `uint MaxDataFrameSize` � Maximum data frame payload size (default: 16 KiB).
- `bool AutoTuneReceiveWindowSize` � Automatically increase window size for throughput (default: true).
- `bool EnableStatistics` � Enables statistics collection.
- `int StatisticsSampleInterval` � How often to sample statistics (ms, default: 1000).

## Methods
- `void Validate()` � Validates the options. Throws `ValidationException` if invalid.

## Example
```csharp
var options = new SessionChannelOptions {
ReceiveWindowSize = 512 * 1024,
ReceiveWindowUpperBound = 8 * 1024 * 1024,
MaxDataFrameSize = 32 * 1024,
AutoTuneReceiveWindowSize = true
};
options.Validate();
```

## Exceptions
- `ValidationException` � Thrown if options are invalid.
Loading