Skip to content
Open
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
95 changes: 95 additions & 0 deletions samples/KestrelHttp2DetachDemo/Http2Frame.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
using System.Buffers;
using System.Text;

namespace KestrelHttp2DetachDemo;

internal enum Http2FrameType : byte
{
Data = 0x0,
Settings = 0x4
}

internal enum Http2FrameFlags : byte
{
None = 0x0,
Ack = 0x1,
EndStream = 0x1
}

internal sealed class Http2Frame
{
public const int HeaderLength = 9;

public Http2Frame(int length, Http2FrameType type, byte flags, int streamId, byte[] payload)
{
Length = length;
Type = type;
Flags = flags;
StreamId = streamId;
Payload = payload;
}

public int Length { get; }

public Http2FrameType Type { get; }

public byte Flags { get; }

public int StreamId { get; }

public byte[] Payload { get; }

public bool IsSettingsAck => Type == Http2FrameType.Settings && (Flags & (byte)Http2FrameFlags.Ack) != 0;

public string PayloadText => Encoding.UTF8.GetString(Payload);

public static Http2Frame Read(ReadOnlySequence<byte> buffer)
{
Span<byte> header = stackalloc byte[HeaderLength];
buffer.Slice(0, HeaderLength).CopyTo(header);

var length = ReadUInt24(header);
var type = (Http2FrameType)header[3];
var flags = header[4];
var streamId = ReadInt31(header.Slice(5, 4));
var payload = buffer.Slice(HeaderLength, length).ToArray();

return new Http2Frame(length, type, flags, streamId, payload);
}

public static byte[] Write(Http2FrameType type, byte flags, int streamId, ReadOnlySpan<byte> payload)
{
var frame = new byte[HeaderLength + payload.Length];
WriteUInt24(frame, payload.Length);
frame[3] = (byte)type;
frame[4] = flags;
WriteInt31(frame.AsSpan(5, 4), streamId);
payload.CopyTo(frame.AsSpan(HeaderLength));
return frame;
}

private static int ReadUInt24(ReadOnlySpan<byte> source)
{
return (source[0] << 16) | (source[1] << 8) | source[2];
}

private static void WriteUInt24(Span<byte> destination, int value)
{
destination[0] = (byte)((value >> 16) & 0xff);
destination[1] = (byte)((value >> 8) & 0xff);
destination[2] = (byte)(value & 0xff);
}

private static int ReadInt31(ReadOnlySpan<byte> source)
{
return ((source[0] & 0x7f) << 24) | (source[1] << 16) | (source[2] << 8) | source[3];
}

private static void WriteInt31(Span<byte> destination, int value)
{
destination[0] = (byte)((value >> 24) & 0x7f);
destination[1] = (byte)((value >> 16) & 0xff);
destination[2] = (byte)((value >> 8) & 0xff);
destination[3] = (byte)(value & 0xff);
}
}
78 changes: 78 additions & 0 deletions samples/KestrelHttp2DetachDemo/Http2PipelineFilter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
using System.Buffers;
using System.Text;
using SuperSocket.ProtoBase;

namespace KestrelHttp2DetachDemo;

internal sealed class Http2PipelineFilter : PipelineFilterBase<Http2Frame>

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should it be in SuperSocket.Http?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point. I kept it in the sample because it is currently only a minimal HTTP/2 frame parser for demonstrating the Kestrel detach handoff scenario, not a reusable/general HTTP implementation yet. If you prefer, I can move it under SuperSocket.Http and wire the sample to consume it from there.

{
private static readonly byte[] ClientPreface = Encoding.ASCII.GetBytes("PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n");

private readonly bool _expectClientPreface;
private bool _clientPrefaceRead;
private bool _foundHeader;
private int _totalSize;

public Http2PipelineFilter(bool expectClientPreface = true)
{
_expectClientPreface = expectClientPreface;
}

public override Http2Frame Filter(ref SequenceReader<byte> reader)
{
if (_expectClientPreface && !_clientPrefaceRead)
{
if (reader.Remaining < ClientPreface.Length)
return null!;

Span<byte> preface = stackalloc byte[24];
reader.Sequence.Slice(reader.Position, ClientPreface.Length).CopyTo(preface);

if (!preface.SequenceEqual(ClientPreface))
throw new ProtocolException("The client did not send a valid HTTP/2 connection preface.");

reader.Advance(ClientPreface.Length);
_clientPrefaceRead = true;
}

if (!_foundHeader)
{
if (reader.Remaining < Http2Frame.HeaderLength)
return null!;

var header = reader.Sequence.Slice(reader.Position, Http2Frame.HeaderLength);
var bodyLength = GetBodyLengthFromHeader(header);

if (bodyLength < 0)
throw new ProtocolException("Failed to get body length from the HTTP/2 frame header.");

_foundHeader = true;
_totalSize = Http2Frame.HeaderLength + bodyLength;
}

if (reader.Remaining < _totalSize)
return null!;

var package = reader.Sequence.Slice(reader.Position, _totalSize);
var frame = Http2Frame.Read(package);
reader.Advance(_totalSize);
_foundHeader = false;
_totalSize = 0;

return frame;
}

public override void Reset()
{
base.Reset();
_foundHeader = false;
_totalSize = 0;
}

private static int GetBodyLengthFromHeader(ReadOnlySequence<byte> headerBuffer)
{
Span<byte> header = stackalloc byte[3];
headerBuffer.Slice(0, 3).CopyTo(header);
return (header[0] << 16) | (header[1] << 8) | header[2];
}
}
18 changes: 18 additions & 0 deletions samples/KestrelHttp2DetachDemo/KestrelHttp2DetachDemo.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>$(SamplesTargetFramework)</TargetFramework>
<IsPackable>false</IsPackable>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<Description>Demonstrates a minimal HTTP/2 preface and SETTINGS handshake over SuperSocket.Kestrel, then DetachAsync and exact raw data transfer over the same Kestrel transport.</Description>
</PropertyGroup>
<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
<ProjectReference Include="../../src/SuperSocket.Connection/SuperSocket.Connection.csproj" />
<ProjectReference Include="../../src/SuperSocket.Kestrel/SuperSocket.Kestrel.csproj" />
<ProjectReference Include="../../src/SuperSocket.ProtoBase/SuperSocket.ProtoBase.csproj" />
<PackageReference Include="Microsoft.Extensions.Hosting" />
<PackageReference Include="Microsoft.Extensions.Logging.Console" />
</ItemGroup>
</Project>
Loading