reckfellENGINE

HttpClient: подключение и чтение ответа по частям

Полный неблокирующий C#-цикл: Poll, заголовки, chunked/EOF, лимит тела и срок ожидания.

На этой странице

Когда нужен низкий уровень

Для JSON API обычно проще HttpRequest. HttpClient нужен, когда вы сами управляете подключением, чтением частей и форматом ответа. Это не автоматическая многопользовательская синхронизация.

Ниже клиент GET: сетевое ожидание не блокирует _Process, тело собирается в памяти до 65536 байт. Это пример чтения частями, а не запись неограниченного файла на диск.

Запустить пример

Запустите LocalHttpFixture.py из руководства HTTP. Добавьте HttpStreamReader.cs и HttpStreamDemo.cs в C#-проект 1.0.0. Создайте уровень с корнем Entity, прикрепите HttpStreamDemo, соберите и нажмите F6. Output покажет status=200, пустой failure и JSON с сообщением. Поле Url по умолчанию указывает на локальный стенд.

HttpStreamDemo.cs
C#
using System.Text;
using Reckfell;

public partial class HttpStreamDemo : Entity
{
    [Export] public string Url { get; set; } = "http://127.0.0.1:8765/message";
    public override void _Ready()
    {
        var reader = new HttpStreamReader(); AddChild(reader);
        reader.Completed += reply => ReckfellApi.Print($"HTTP stream: status={reply.StatusCode}, failure={reply.Failure}, bytes={reply.Body.Length}, body={Encoding.UTF8.GetString(reply.Body)}");
        Error start = reader.Start(Url);
        if (start != Error.Ok) ReckfellApi.Print($"HTTP stream start: {start}");
    }
}
HttpStreamReader.cs
C#
using System;
using System.Collections.Generic;
using System.Diagnostics;
using Reckfell;

public partial class HttpStreamReader : Entity
{
    public sealed record Reply(int StatusCode, byte[] Body, string[] Headers, bool Chunked, long ExpectedBytes, string Failure);
    public int MaxBytes { get; set; } = 65536;
    public double TimeoutSeconds { get; set; } = 5;
    public bool Busy => _client is not null;
    public Reply? LastReply { get; private set; }
    public event Action<Reply>? Completed;
    private HttpClient? _client;
    private readonly Stopwatch _clock = new();
    private readonly List<byte> _body = new();
    private string _path = "/";
    private bool _sent, _headersRead, _chunked;
    private int _statusCode;
    private long _expected;
    private string[] _headers = [];

    public Error Start(string url, TlsOptions? trust = null)
    {
        if (Busy) return Error.Busy;
        if (!IsInsideTree()) return Error.Unconfigured;
        if (!Uri.TryCreate(url, UriKind.Absolute, out Uri? uri) ||
            (uri.Scheme != "http" && uri.Scheme != "https") || uri.UserInfo.Length != 0 ||
            MaxBytes <= 0 || !double.IsFinite(TimeoutSeconds) || TimeoutSeconds <= 0)
            return Error.InvalidParameter;
        _body.Clear(); _headers = []; _sent = _headersRead = _chunked = false;
        _expected = -1; _statusCode = 0; LastReply = null; _path = uri.PathAndQuery;
        _client = new HttpClient { BlockingModeEnabled = false, ReadChunkSize = 4096 };
        Error error = _client.ConnectToHost(uri.DnsSafeHost, uri.Port,
            uri.Scheme == "https" ? trust ?? TlsOptions.Client() : null);
        if (error != Error.Ok) { Cancel(); return error; }
        _clock.Restart(); return Error.Ok;
    }
    public override void _Process(double delta)
    {
        if (_client is null) return;
        if (_clock.Elapsed.TotalSeconds >= TimeoutSeconds) { Finish("timeout"); return; }
        Error error = _client.Poll();
        var state = _client.GetStatus();
        if (error != Error.Ok) { Finish(state.ToString()); return; }
        if (state is HttpClient.Status.Resolving or HttpClient.Status.Connecting) return;
        if (!_sent && state == HttpClient.Status.Connected)
        {
            error = _client.Request(HttpClient.Method.Get, _path, ["Accept-Encoding: identity"]);
            if (error != Error.Ok) { Finish(error.ToString()); return; }
            _sent = true; return;
        }
        if (_sent && !_headersRead && _client.HasResponse())
        {
            _headersRead = true; _statusCode = _client.GetResponseCode();
            _headers = _client.GetResponseHeaders(); _expected = _client.GetResponseBodyLength(); _chunked = _client.IsResponseChunked();
            foreach (string header in _headers)
                if (header.StartsWith("Content-Encoding:", StringComparison.OrdinalIgnoreCase) &&
                    !header[(header.IndexOf(':') + 1)..].Trim().Equals("identity", StringComparison.OrdinalIgnoreCase))
                { Finish("content-encoding"); return; }
            if (_expected > MaxBytes) { Finish("size-limit"); return; }
        }
        if (state == HttpClient.Status.Body)
        {
            byte[] chunk = _client.ReadResponseBodyChunk();
            if (chunk.Length > MaxBytes - _body.Count) { Finish("size-limit"); return; }
            _body.AddRange(chunk); state = _client.GetStatus();
        }
        if (_headersRead && state == HttpClient.Status.Connected)
        { Finish(_expected >= 0 && _body.Count != _expected ? "truncated" : ""); return; }
        if (state == HttpClient.Status.Disconnected)
        {
            bool complete = _headersRead && ((_expected >= 0 && _body.Count == _expected) || (_expected < 0 && !_chunked));
            Finish(complete ? "" : "truncated"); return;
        }
        if (state is HttpClient.Status.CantResolve or HttpClient.Status.CantConnect or
            HttpClient.Status.ConnectionError or HttpClient.Status.TlsHandshakeError)
            Finish(state.ToString());
    }
    private void Finish(string failure)
    {
        var reply = new Reply(_statusCode, failure.Length == 0 ? _body.ToArray() : [], _headers, _chunked, _expected, failure);
        Cancel(); LastReply = reply; Completed?.Invoke(reply);
    }
    public void Cancel() { _client?.Close(); _client?.Dispose(); _client = null; _clock.Stop(); _body.Clear(); }
    public override void _ExitTree() => Cancel();
}

Жизненный цикл соединения

СостояниеДействие
Resolving / ConnectingПродолжать Poll в следующих кадрах
Connected до запросаПередать метод, путь с query и заголовки в Request
RequestingПродолжать Poll, дождаться заголовков
BodyЧитать ReadResponseBodyChunk и сохранять байты
Connected после телаПроверить полноту и завершить
Disconnected при чтении до EOFУспех только для подходящего способа определения длины; проверить известную длину
CantResolve / CantConnect / ConnectionError / TlsHandshakeErrorЗавершить с ошибкой, закрыть соединение

ConnectToHost получает host и port; Request получает /path?query, а не полный URL. Uri в примере разделяет эти части. QueryStringFromDict кодирует значения query; не вставляйте пробелы и пользовательские строки напрямую в URL.

GetResponseBodyLength может вернуть -1. Это не «пустой ответ»: длина может определяться chunked-кодированием или закрытием соединения. Пустая часть чтения также не означает конец: данных пока может не быть. Статус и способ определения длины проверяются вместе.

При известном Content-Length недобор байтов — ошибка. При chunked нужно дождаться завершения framing, а не просто закрытия сокета. В стенде /message, /chunked и /eof дают одинаковый JSON разными способами; /truncated намеренно обрывает неполное тело и должен завершиться ошибкой.

Байты, HTTP-статус и сжатие

Клиент возвращает StatusCode отдельно от Failure. Получить полный ответ 404 технически возможно без ошибки передачи; решение о допустимом статусе принимает вызывающий код. HttpStreamDemo выводит тело для диагностики локального стенда; в рабочей игре не печатайте секретные ответы.

Здесь посылается Accept-Encoding: identity, а неожиданный Content-Encoding отвергается. HttpClient не выполняет распаковку HttpRequest автоматически. Путь /gzip намеренно игнорирует предпочтение и должен дать content-encoding. Если нужна распаковка, добавьте её с отдельным лимитом выходных данных или используйте HttpRequest.

Храните части как байты; UTF-8 символ может пересечь границу. Пример декодирует полное тело один раз. Для действительно большого потока нужны последовательная обработка/запись, проверка ошибок накопителя и отдельный бюджет памяти.

Ограничения и завершение

MaxBytes ограничивает собранное тело. ReadChunkSize = 4096 не доказывает такой же предел для любого внутреннего буфера: входящий HTTP chunk может иметь другой размер. Не описывайте этот пример как защиту от любого расхода памяти или всех повреждённых протокольных ответов.

Срок ожидания измеряется Stopwatch, а не игровым delta. Проверка выполняется в _Process; если обработка приостановлена, просроченный запрос завершится после её возобновления. Для продолжения работы во время паузы согласуйте ProcessMode владельца.

Cancel закрывает и освобождает клиент без Completed. Повторный Start допускается после завершения/отмены; конкурентный запуск получает Error.Busy. _ExitTree закрывает соединение. В коде нет автоматического перенаправления, cookie-хранилища, авторизации, пула соединений, повторов или HTTP/2-клиента.

Проверены реальные TCP-ответы с длиной, chunked и EOF, UTF-8, 404, сжатие как отказ, лимит, усечённое тело, срок ожидания и повтор после отмены. HTTPS использует TlsOptions; доступность других платформ и сетевых условий проверяется отдельно.

Документация Reckfell EngineНаверх ↑
БЫСТРЫЙ ПЕРЕХОДНачало работыEntity и иерархияC# & .NET 10Сцены и префабы
Руководства и полный API Reckfell K
reckfellENGINE
RECKFELL ACCOUNT

Вход в аккаунт

Войдите в аккаунт Reckfell.

Один аккаунт для всей платформы Reckfell.