A http library in c#
This library is a low-level RFC implementation of HTTP.
It is expected that you implement logic like content serving yourself.
You can consult the test code for code examples/usage.
This library contains both sync and async methods allowing you to use it in both contexts.
For every blocking/waiting method theres an async/sync version.
H2C and WebSocket upgrades can in the library.
In the future I might make another library that uses this one to support a ready to use Web framework with middleware and more.
- allow both sync and async code
- implement HTTP/0.9 (utterly useless, for educational purposes)
- implement HTTP/1.1
- implement HPACK
- implement HTTP/2
- implement WebSocket
- implement QUIC (likely
System.Net.Quicwill be used instead) - implement QPACK
- implement HTTP/3
Here is the HTTP echo server example from the test code.
HTTP/1.1 echo server
using Samicpp.Http;
using Samicpp.Http.Http1;
using System;
using System.Net;
using System.Text;
using System.Net.Sockets;
using System.Threading.Tasks;
using System.Collections.Generic;
// you have to either implement `Samicpp.Http.IDualSocket` yourself
// or use the abstract class `Samicpp.Http.ANetSocket`
public class TcpSocket(NetworkStream stream) : ANetSocket
{
override protected NetworkStream Stream { get { return stream; } }
override public bool IsSecure { get { return false; } }
}
public class Program
{
public async Task Main()
{
// creating listener
IPEndPoint address = new(IPAddress.Parse("0.0.0.0"), 2048);
using Socket listener = new(address.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
// start listener
listener.Bind(address);
listener.Listen(10);
Console.WriteLine("http echo server listening on http://127.0.0.1:2048");
// connection loop
while (true)
{
var shandler = await listener.AcceptAsync();
Console.WriteLine($"\e[32m{shandler.RemoteEndPoint}\e[0m");
var _ = Task.Run(async () =>
{
// first we need to convert it to something we can pass to our class
using NetworkStream stream = new(shandler, ownsSocket: true);
// then we use it to construct `Samicpp.Http.Http1.Http1Socket`
using Http1Socket socket = new(new TcpSocket(stream));
// interface `Samicpp.Http.IDualHttpSocket` can also be used as data type, since the class implements this.
// individual H2 streams also implement this
Console.WriteLine("constructed protocol handler");
// when the client uses `Transfer-Encoding: chunked` each read will only add 1 chunk to the body buffer
// if `Content-Length: n` was provided the library will only read the full body on the second read invocation
// to ensure not enforcing body read
// this is also usefull for Http2Streams where reading client doesnt block
var client = await socket.ReadClientAsync();
// ensures full client has been read
while (!client.HeadComplete || !client.BodyComplete) client = await socket.ReadClientAsync();
// the framework allows for headers to appear multiple times
if (client.Headers.TryGetValue("accept-encoding", out List<string> encoding))
{
foreach (string s in encoding[0].Split(","))
{
// setting `Samicpp.Http.IDualSocket.Compression` automatically ensures the appropriate compression type is used
// the framework does not verify if client accepts the encoding, this was done on purpose to give the code full control
socket.Compression = s switch
{
"gzip" => Compression.Gzip,
"deflate" => Compression.Deflate,
"br" => Compression.Brotli,
_ => Compression.None,
};
if (socket.Compression != Compression.None) break;
}
Console.WriteLine("using compression " + socket.Compression);
}
else
{
Console.WriteLine("no compression");
}
Console.WriteLine(client);
Console.WriteLine($"received {client.Body.Count} bytes");
// the server doesnt decode the client body automatically, it also doesnt decompress it. this is the code's responsibility.
// for decompression you can use `Samicpp.Http.Compressor.Decompress`
var text = Encoding.UTF8.GetString([.. client.Body]);
Console.WriteLine($"received request with body[{text.Length}] \e[36m{text.Trim()}\e[0m");
// the server does ensure you cannot attempt to send data after connection has been closed
// nor does it allow you to send headers after
await socket.CloseAsync(Encoding.UTF8.GetBytes($"<| {text.Trim()} |>\n"));
});
}
}
}HTTP/2 echo server
using Samicpp.Http;
using Samicpp.Http.Http2;
using System;
using System.Net;
using System.Text;
using System.Net.Sockets;
using System.Threading.Tasks;
using System.Collections.Generic;
// you have to either implement `Samicpp.Http.IDualSocket` yourself
// or use the abstract class `Samicpp.Http.ANetSocket`
public class TcpSocket(NetworkStream stream) : ANetSocket
{
override protected NetworkStream Stream { get { return stream; } }
override public bool IsSecure { get { return false; } }
}
public class Program
{
public async Task Main()
{
// creating listener
IPEndPoint address = new(IPAddress.Parse("0.0.0.0"), 2048);
using Socket listener = new(address.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
// start listener
listener.Bind(address);
listener.Listen(10);
Console.WriteLine("http echo server listening on http://127.0.0.1:2048");
// connection loop
while (true)
{
var shandler = await listener.AcceptAsync();
Console.WriteLine($"\e[32m{shandler.RemoteEndPoint}\e[0m");
var _ = Task.Run(async () =>
{
// first we need to convert it to something we can pass to our class
using NetworkStream stream = new(shandler, ownsSocket: true);
// then we use it to construct `Samicpp.Http.Http2.Http2Session`
using Http2Session h2 = new(new TcpSocket(stream), Http2Settings.Default());
// the library doesnt automatically read and check the preface, so we have to invoke it manually
await h2.InitAsync();
Console.WriteLine("initialized http2 connection");
// we also need to send our settings
await h2.SendSettingsAsync(Http2Settings.Default());
// by default it uses the constructor provided settings for the encoder header table size
// if you change this you will need to modify this manually with
// `Samicpp.Http.Http2.Http2Session.hpacke.TableSize = 4096`
// when the library handles a Goaway frame it will store this in `Samicpp.Http.Http2.Http2Session.goaway`
// we can use this as an indicator for open connections
while(h2.goaway == null)
{
// first you need to read the http2 frames
List<Http2Frame> frames = await socket.ReadAllAsync();
// this reads all available buffer, which can contain incomplete frames
// it is recommended you use `Samicpp.Http.Http2.Http2Session.ReadOneAsync` which does wait until it receives a whole frame
// you can handle these manually if you want, but that is not necessary
// we then pass the frames to the handler, which automatically updates stream states and more
List<int> openedStreams = await socket.HandleAsync(frames);
// this returns a list of stream ids for opened streams
// this method also has an overload for single frames which returns `int?`
foreach (int streamID in openedStreams)
{
var _ = Task.Run(async () =>
{
// we can directly use `Samicpp.Http.Http2.Http2Session` to send headers/data but that is not necessary
// we can use the single-stream handler `Samicpp.Http.Http2.Http2Stream`
using Http2Stream stream = new(streamID, h2);
// this implements `Samicpp.Http.IDualHttpSocket` allowing for interopibility
// with functions that accept both Samicpp.Http.Http1.Http1Socket and `Samicpp.Http.Http2.Http2Stream`
// in h2 it is much more realistic that not the whole client has been read
// furthermore the library doesnt block for reading the client
// it queries the stream state to retrieve client data
var client = await stream.ReadClientAsync();
while (!client.HeadComplete || !client.BodyComplete) client = await stream.ReadClientAsync();
// we can use `Samicpp.Http.Http2.Http2Stream` like it is `Samicpp.Http.Http1.Http1Socket`
stream.SetHeader("content-type", "text/plain");
// the client body is also of type `List<byte>`
var text = Encoding.UTF8.GetString([.. client.Body]);
// `Samicpp.Http.Http2.Http2Stream.CloseAsync` and its sync version both include header `Content-Length`
// in the response if the headers havent yet been sent
await stream.CloseAsync(Encoding.UTF8.GetBytes($"<| {text.Trim()} |>\n"));
});
}
}
});
}
}
}