ioxidedocs

HTTP client Get started

Quick start

Two complete programs: a server whose handlers call one upstream through a pool, and a program that fetches any URL with RingHttpClient.

dotnet add package ioxide.httpclient

One upstream#

A server on port 8080 whose handler calls an upstream API for every request it receives. Each reactor gets its own pool, so the call goes out and comes back on the thread the request arrived on.

using ioxide;
using ioxide.httpclient;
using ioxide.utils;
using System.Text;

var config = new ServerConfig
{
    ReactorCount = Environment.ProcessorCount,
    Tcp = new TcpOptions { Port = 8080 },
};

var upstream = new HttpClientOptions
{
    Host     = "api.internal",   // a name, an IPv4 or an IPv6 address
    Port     = 9000,
    PoolSize = 64,               // per reactor: the most requests in flight to the upstream
};
byte[] things = "/things"u8.ToArray();   // a path is bytes, built once

for (int id = 0; id < config.ReactorCount; id++)
{
    var reactor = new Reactor(id, config);

    // The pool opens its connections on THIS reactor's ring.
    reactor.OnStart = r => HttpClientPool.Start(r, upstream);

    reactor.TcpHandle = async (r, conn) =>
    {
        HttpClientPool api = r.GetService<HttpClientPool>();
        try
        {
            while (true)
            {
                RecvSnapshot snapshot = await conn.ReadAsync();
                while (conn.TryGetItem(snapshot, out SpscRecvRing.Item item))
                {
                    if (item.HasBuffer) conn.ReturnBuffer(in item);   // a real proxy parses the request here
                }

                using (HttpClientResponse response = await api.GetAsync(things))
                {
                    conn.Write(Encoding.ASCII.GetBytes(
                        $"HTTP/1.1 {response.Status} OK\r\nContent-Length: {response.Body.Length}\r\n\r\n"));
                    conn.Write(response.Body.Span);   // the bytes straight through
                }

                await conn.FlushAsync();
                if (snapshot.IsClosed) return;
                conn.ResetRead();
            }
        }
        finally
        {
            conn.DecRef();
        }
    };

    new Thread(reactor.Run) { Name = $"reactor-{id}" }.Start();
}

The repository's Playground/Clients/Http sample is this program, with a 502 for an upstream that fails, and Playground/Proxy/* forward real request targets from HTTP/1.1, HTTP/2 and HTTP/3 front ends.

Any URL#

A program that only makes requests needs a reactor with no listener. Start RingHttpClient on it and call it like .NET's HttpClient:

// Nothing listens, and the client brings its own receive buffers, so the shared ring stays small.
var config = new ServerConfig { ReactorCount = 1, Tcp = null, Udp = null, RecvBufferSize = 4096, RecvSlots = 256 };
var done = new ManualResetEventSlim();

var reactor = new Reactor(0, config)
{
    OnStart = r => _ = FetchAsync(RingHttpClient.Start(r)),
};
new Thread(reactor.Run) { IsBackground = true }.Start();
done.Wait();

async Task FetchAsync(RingHttpClient http)
{
    using HttpClientResponse page = await http.GetAsync("https://www.example.com/");
    Console.WriteLine($"{page.Status}: {page.Body.Length} bytes");
    done.Set();
}

The name is resolved, the certificate is checked against www.example.com and the system trust store, and a redirect would be followed. A server's handlers reach it the same way as the pool: r.GetService<RingHttpClient>().

Before you ship, read the rules that keep it fast and correct: one client per reactor, await and never block, and dispose every response.