利用 ASP.NET Core 开发单机应用

前言

现在是分布式微服务开发的时代,除了小工具和游戏之类刚需本地运行的程序已经很少见到纯单机应用。现在流行的Web应用由于物理隔离天然形成了分布式架构,核心业务由服务器运行,边缘业务由客户端运行。对于消费终端应用,为了应付庞大的流量,服务端本身也要进行再切分以满足多实例和不同业务独立运行的需要。

在单机应用中,架构设计的必要性则弱很多,精心设计架构的应用基本是为适应团队开发的需要。单机程序因为没有物理隔离很容易写成耦合的代码,给未来的发展埋下隐患。如果能利用Web应用的思路设计应用,可以轻松做到最基本的模块化,把界面和数据传输同核心业务逻辑分离。Web服务的分布式架构等设计也能用最简单的方式复用到单机程序。

ASP.NET Core为这个设想提供了原生支持。基本思路是利用TestServer承载服务,然后用TestServer提供的用内存流直接和服务通信的特殊HttpClient完成交互。这样就摆脱了网络和进程间通信的基本开销以最低的成本实现虚拟的C/S架构。

正文

TestServer本是为ASP.NET Core集成测试而开发的特殊IServer实现,这个服务器并不使用任何网络资源,因此也无法从网络访问。访问TestServer的唯一途径是使用由TestServer的成员方法创建的特殊HttpClient,这个Client的底层不使用SocketsHttpMessageHandler而是使用专用Handler由内存流传输数据。

TestServerMicrosoft.AspNetCore.TestHost包中定义,可以用于集成测试,但是官方建议使用Microsoft.AspNetCore.Mvc.Testing包来进行测试。这个包在基础包之上进行了一些封装,简化了单元测试类的定义,并为Client增加了自动重定向和Cookie处理以兼容带重定向和Cookie的测试。笔者之前也一直在研究如何用这个包实现目标,但是无奈这个包的一些强制规则不适用测试之外的情况。最终只能用基础包来开发。

为了实现集成测试包的额外Client功能,从源代码中复制这些类的代码来用。开源项目就是好啊!

特殊Client在本地使用时有非常大的优势,但是如果其中的某些情况需要和真实网络交互就做不到了。为此笔者开发了一个使用网络通信的HttpMessageHandler来处理这种情况。

RedirectHandler

/// <summary>
/// A <see cref="DelegatingHandler"/> that follows redirect responses.
/// </summary>
public class RedirectHandler : DelegatingHandler
{
    internal const int DefaultMaxRedirects = 7;
 
    /// <summary>
    /// Creates a new instance of <see cref="RedirectHandler"/>.
    /// </summary>
    public RedirectHandler()
        : this(maxRedirects: DefaultMaxRedirects)
    {
    }
 
    /// <summary>
    /// Creates a new instance of <see cref="RedirectHandler"/>.
    /// </summary>
    /// <param name="maxRedirects">The maximum number of redirect responses to follow. It must be
    /// equal or greater than 0.</param>
    public RedirectHandler(int maxRedirects)
    {
        ArgumentOutOfRangeException.ThrowIfNegativeOrZero(maxRedirects);
 
        MaxRedirects = maxRedirects;
    }
 
    /// <summary>
    /// Gets the maximum number of redirects this handler will follow.
    /// </summary>
    public int MaxRedirects { get; }
 
    /// <inheritdoc />
    protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        var remainingRedirects = MaxRedirects;
        var redirectRequest = new HttpRequestMessage();
        var originalRequestContent = HasBody(request) ? await DuplicateRequestContentAsync(request) : null;
        CopyRequestHeaders(request.Headers, redirectRequest.Headers);
        var response = await base.SendAsync(request, cancellationToken);
        while (IsRedirect(response) && remainingRedirects > 0)
        {
            remainingRedirects--;
            UpdateRedirectRequest(response, redirectRequest, originalRequestContent);
            originalRequestContent = HasBody(redirectRequest) ? await DuplicateRequestContentAsync(redirectRequest) : null;
            response = await base.SendAsync(redirectRequest, cancellationToken);
        }
 
        return response;
    }
 
    protected internal static bool HasBody(HttpRequestMessage request) =>
        request.Method == HttpMethod.Post || request.Method == HttpMethod.Put;
 
    protected internal static async Task<HttpContent?> DuplicateRequestContentAsync(HttpRequestMessage request)
    {
        if (request.Content == null)
        {
            return null;
        }
        var originalRequestContent = request.Content;
        var (originalBody, copy) = await CopyBody(request);
 
        var contentCopy = new StreamContent(copy);
        request.Content = new StreamContent(originalBody);
 
        CopyContentHeaders(originalRequestContent, request.Content, contentCopy);
 
        return contentCopy;
    }
 
    protected internal static void CopyContentHeaders( 
        HttpContent originalRequestContent,
        HttpContent newRequestContent,
        HttpContent contentCopy)
    {
        foreach (var header in originalRequestContent.Headers)
        {
            contentCopy.Headers.TryAddWithoutValidation(header.Key, header.Value);
            newRequestContent.Headers.TryAddWithoutValidation(header.Key, header.Value);
        }
    }
 
    protected internal static void CopyRequestHeaders( 
        HttpRequestHeaders originalRequestHeaders,
        HttpRequestHeaders redirectRequestHeaders)
    {
        foreach (var header in originalRequestHeaders)
        {
            // Avoid copying the Authorization header to match the behavior
            // in the HTTP client when processing redirects
            // https://github.com/dotnet/runtime/blob/69b5d67d9418d672609aa6e2c418a3d4ae00ad18/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/SocketsHttpHandler.cs#L509-L517
            if (!header.Key.Equals(HeaderNames.Authorization, StringComparison.OrdinalIgnoreCase))
            {
                redirectRequestHeaders.TryAddWithoutValidation(header.Key, header.Value);
            }
        }
    }
 
    protected internal static async Task<(Stream originalBody, Stream copy)> CopyBody(HttpRequestMessage request)
    {
        var originalBody = await request.Content!.ReadAsStreamAsync();
        var bodyCopy = new MemoryStream();
        await originalBody.CopyToAsync(bodyCopy);
        bodyCopy.Seek(0, SeekOrigin.Begin);
        if (originalBody.CanSeek)
        {
            originalBody.Seek(0, SeekOrigin.Begin);
        }
        else
        {
            originalBody = new MemoryStream();
            await bodyCopy.CopyToAsync(originalBody);
            originalBody.Seek(0, SeekOrigin.Begin);
            bodyCopy.Seek(0, SeekOrigin.Begin);
        }
 
        return (originalBody, bodyCopy);
    }
 
    protected internal static void UpdateRedirectRequest( 
        HttpResponseMessage response,
        HttpRequestMessage redirect,
        HttpContent? originalContent)
    {
        Debug.Assert(response.RequestMessage is not null);
 
        var location = response.Headers.Location;
        if (location != null)
        {
            if (!location.IsAbsoluteUri && response.RequestMessage.RequestUri is Uri requestUri)
            {
                location = new Uri(requestUri, location);
            }
 
            redirect.RequestUri = location;
        }
 
        if (!ShouldKeepVerb(response))
        {
            redirect.Method = HttpMethod.Get;
        }
        else
        {
            redirect.Method = response.RequestMessage.Method;
            redirect.Content = originalContent;
        }
 
        foreach (var property in response.RequestMessage.Options)
        {
            var key = new HttpRequestOptionsKey<object?>(property.Key);
            redirect.Options.Set(key, property.Value);
        }
    }
 
    protected internal static bool ShouldKeepVerb(HttpResponseMessage response) =>
        response.StatusCode == HttpStatusCode.RedirectKeepVerb ||
            response.StatusCode == HttpStatusCode.PermanentRedirect;
 
    protected internal static bool IsRedirect(HttpResponseMessage response) =>
        response.StatusCode == HttpStatusCode.MovedPermanently ||
            response.StatusCode == HttpStatusCode.Redirect ||
            response.StatusCode == HttpStatusCode.RedirectMethod ||
            response.StatusCode == HttpStatusCode.RedirectKeepVerb ||
            response.StatusCode == HttpStatusCode.PermanentRedirect;
}

这是从原项目复制后修改的重定向处理器,主要是把部分方法的访问级别稍微放宽。从代码可以看出这个处理器使用内存流复制来实现消息体复制和重定向,如果请求包含大文件上传可能出现复制操作把文件内容缓冲到内存导致内存溢出。不过这种情况应该非常少见,这里不考虑处理这种情况。

RemoteLocalAutoSwitchWithRedirectHandler

public class RemoteLocalAutoSwitchWithRedirectHandler : DelegatingHandler
{
    private readonly Uri _localAddress;
    private readonly RedirectHandler? _localRedirectHandler;
    private readonly string _nameOfNamedClient;
    private readonly IServiceScope _scope;
    private volatile bool _disposed;
 
    private HttpClient _remoteHttpClient;
    private HttpClient? _localHttpClient;
 
    public RemoteLocalAutoSwitchWithRedirectHandler( 
        Uri localAddress,
        RedirectHandler? localRedirectHandler,
        IServiceScope scope,
        string nameOfNamedClient)
    {
        ArgumentNullException.ThrowIfNull(localAddress);
        ArgumentNullException.ThrowIfNull(scope);
 
        _localAddress = localAddress;
        _localRedirectHandler = localRedirectHandler;
        _scope = scope;
        _nameOfNamedClient = nameOfNamedClient;
 
        _remoteHttpClient = _scope.ServiceProvider
            .GetRequiredService<IHttpClientFactory>()
            .CreateClient(_nameOfNamedClient);
    }
 
    protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        ObjectDisposedException.ThrowIf(_disposed, this);
 
        if (IsLocalAddress(request.RequestUri, _localAddress))
        {
            return await base.SendAsync(request, cancellationToken);
        }
        else
        {
            var response = await _remoteHttpClient.SendAsync(request, cancellationToken);
 
            if (_localRedirectHandler is null) return response;
 
            var remainingRedirects = _localRedirectHandler.MaxRedirects;
            var redirectRequest = new HttpRequestMessage();
            var originalRequestContent = RedirectHandler.HasBody(request) ? await RedirectHandler.DuplicateRequestContentAsync(request) : null;
            RedirectHandler.CopyRequestHeaders(request.Headers, redirectRequest.Headers);
            while (RedirectHandler.IsRedirect(response) && remainingRedirects > 0)
            {
                remainingRedirects--;
                RedirectHandler.UpdateRedirectRequest(response, redirectRequest, originalRequestContent);
                originalRequestContent = RedirectHandler.HasBody(request) ? await RedirectHandler.DuplicateRequestContentAsync(request) : null;
                RedirectHandler.CopyRequestHeaders(request.Headers, redirectRequest.Headers);
 
                if (IsLocalAddress(response.Headers.Location, _localAddress))
                {
                    _localHttpClient ??= new HttpClient(_localRedirectHandler);
                    response = await _localHttpClient.SendAsync(redirectRequest, cancellationToken);
                }
                else
                {
                    response = await _remoteHttpClient.SendAsync(redirectRequest, cancellationToken);
                }
            }
 
            return response;
        }
    }
 
    protected override void Dispose(bool disposing)
    {
        if (disposing && !_disposed)
        {
            _disposed = true;
 
            _scope.Dispose();
        }
 
        base.Dispose(disposing);
    }
 
    private static bool IsLocalAddress(Uri? uri, Uri? localAddress) =>
        uri is not null && localAddress is not null
            && uri.Scheme == localAddress.Scheme
            && uri.Host == localAddress.Host
            && uri.Port == localAddress.Port;
}

这是笔者为处理网络请求编写的处理器,并且这个处理器自带重定向功能,逻辑基本是抄的官方代码。然后做了一些本地请求和外部网络请求的区分处理。

网络请求处理器从主机的依赖注入服务获取客户端,因此要提前在主机服务中注册客户端,并且要关闭网络客户端自带的重定向。

TestServerClientHandlerOptions

/// <summary>
/// The default options to use to when creating
/// <see cref="HttpMessageHandler"/> instances by calling
/// <see cref="TestServerExtensions.CreateHandlers(TestServer, TestServerClientHandlerOptions)"/>.
/// </summary>
public class TestServerClientHandlerOptions
{
    public const string DefaultTestServerRemoteRequestClientName = "DefaultTestServerRemoteRequestClient";
 
    /// <summary>
    /// Initializes a new instance of <see cref="TestServerClientHandlerOptions"/>.
    /// </summary>
    public TestServerClientHandlerOptions()
    {
    }
 
    // Copy constructor
    internal TestServerClientHandlerOptions(TestServerClientHandlerOptions clientOptions)
    {
        AllowAutoRedirect = clientOptions.AllowAutoRedirect;
        MaxAutomaticRedirections = clientOptions.MaxAutomaticRedirections;
        HandleCookies = clientOptions.HandleCookies;
        ProcessRemoteRequest = clientOptions.ProcessRemoteRequest;
        RemoteRequestClientName = clientOptions.RemoteRequestClientName;
    }
 
    /// <summary>
    /// Gets or sets whether or not <see cref="HttpMessageHandler"/> instances created by calling
    /// <see cref="TestServerExtensions.CreateHandlers(TestServer, TestServerClientHandlerOptions)"/>
    /// should automatically follow redirect responses.
    /// The default is <c>true</c>.
    /// </summary>
    public bool AllowAutoRedirect { get; set; } = true;
 
    /// <summary>
    /// Gets or sets the maximum number of redirect responses that <see cref="HttpMessageHandler"/> instances
    /// created by calling <see cref="TestServerExtensions.CreateHandlers(TestServer, TestServerClientHandlerOptions)"/>
    /// should follow.
    /// The default is <c>7</c>.
    /// </summary>
    public int MaxAutomaticRedirections { get; set; } = RedirectHandler.DefaultMaxRedirects;
 
    /// <summary>
    /// Gets or sets whether <see cref="HttpMessageHandler"/> instances created by calling
    /// <see cref="TestServerExtensions.CreateHandlers(TestServer, TestServerClientHandlerOptions)"/>
    /// should handle cookies.
    /// The default is <c>true</c>.
    /// </summary>
    public bool HandleCookies { get; set; } = true;
 
    public bool ProcessRemoteRequest { get; set; } = false;
 
    public string? RemoteRequestClientName { get; set; } = DefaultTestServerRemoteRequestClientName;
}

这是从集成测试包中复制后改造的处理器选项类,用于控制客户端实例化时要启用的功能。ProcessRemoteRequest控制是否启用网络请求处理。RemoteRequestClientName用于指定在主机中注册的命名客户端的名字。

TestServerClientOptions

/// <summary>
/// The default options to use to when creating
/// <see cref="HttpClient"/> instances by calling
/// <see cref="TestServerExtensions.GetTestClient(IHost, TestServerClientOptions)"/>.
/// </summary>
public class TestServerClientOptions : TestServerClientHandlerOptions
{
    /// <summary>
    /// Initializes a new instance of <see cref="TestServerClientOptions"/>.
    /// </summary>
    public TestServerClientOptions() { }
 
    // Copy constructor
    internal TestServerClientOptions(TestServerClientOptions clientOptions)
        : base(clientOptions)
    {
        BaseAddress = clientOptions.BaseAddress;
        DefaultRequestVersion = clientOptions.DefaultRequestVersion;
    }
 
    /// <summary>
    /// Gets or sets the base address of <see cref="HttpClient"/> instances created by calling
    /// <see cref="TestServerExtensions.GetTestClient(IHost, TestServerClientOptions)"/>.
    /// The default is <c>http://localhost</c>.
    /// </summary>
    public Uri BaseAddress { get; set; } = new Uri("http://localhost");
 
    public Version DefaultRequestVersion { get; set; } = new Version(2, 0);
}

这是对应的客户端选项类,继承处理器选项并增加HttpClient相关的内容。

TestServerExtensions

public static class TestServerExtensions
{
    public static Action<IWebHostBuilder> ConfigureTestServer( 
        Action<IWebHostBuilder>? configureTestWebBuilder = null,
        RemoteRequestClientOptions? options = null
    ) =>
        webBuilder =>
        {
            configureTestWebBuilder?.Invoke(webBuilder);
 
            webBuilder.ConfigureAppConfiguration(configurationBuilder =>
            {
                List<KeyValuePair<string, string?>> memoryAppConfiguration = [new("HostInTestServer", "true")];
                configurationBuilder.AddInMemoryCollection(memoryAppConfiguration);
            });
 
            webBuilder.UseTestServer();
            webBuilder.ConfigureServices(services =>
            {
                var testServerRemoteRequestClientBuilder = services.AddHttpClient(options?.RemoteRequestClientName ?? TestServerClientHandlerOptions.DefaultTestServerRemoteRequestClientName)
                    .SetHandlerLifetime(TimeSpan.FromMinutes(5))
                    .ConfigurePrimaryHttpMessageHandler(provider =>
                    {
                        return new SocketsHttpHandler()
                        {
                            // 禁用内置的自动重定向,由 RemoteLocalAutoSwitchWithRedirectHandler 处理重定向实现本地请求和远程请求之间的相互重定向
                            AllowAutoRedirect = false,
                            PooledConnectionLifetime = TimeSpan.FromMinutes(2),
                        };
                    });
 
                foreach (var func in options?.AppendHttpMessageHandlers ?? Enumerable.Empty<Func<IServiceProvider, DelegatingHandler>>())
                {
                    testServerRemoteRequestClientBuilder.AddHttpMessageHandler(func);
                }
 
                if(options?.ConfigureAdditionalHttpMessageHandlers is not null)
                    testServerRemoteRequestClientBuilder.ConfigureAdditionalHttpMessageHandlers(options.ConfigureAdditionalHttpMessageHandlers);
            });
        };
 
    public static HttpClient CreateTestClient(this TestServer server, TestServerClientOptions options)
    {
        HttpClient client;
        var handlers = server.CreateHandlers(options);
        if (handlers == null || handlers.Length == 0)
        {
            client = server.CreateClient();
        }
        else
        {
            for (var i = handlers.Length - 1; i > 0; i--)
            {
                handlers[i - 1].InnerHandler = handlers[i];
            }
 
            var testServerHandler = server.CreateHandler(options);
 
            client = new HttpClient(testServerHandler)
            {
                BaseAddress = options.BaseAddress,
                DefaultRequestVersion = options.DefaultRequestVersion
            };
        }
 
        return client;
    }
 
    public static HttpClient GetTestClient(this IHost host, TestServerClientOptions options)
    {
        return host.GetTestServer().CreateTestClient(options);
    }
 
    public static HttpMessageHandler CreateHandler( 
        this TestServer server,
        TestServerClientHandlerOptions options,
        Action<HttpContext>? additionalContextConfiguration = null)
    {
        HttpMessageHandler handler;
        var handlers = server.CreateHandlers(options);
        if (handlers == null || handlers.Length == 0)
        {
            handler = additionalContextConfiguration is null
                ? server.CreateHandler()
                : server.CreateHandler(additionalContextConfiguration);
        }
        else
        {
            for (var i = handlers.Length - 1; i > 0; i--)
            {
                handlers[i - 1].InnerHandler = handlers[i];
            }
 
            var testServerHandler = additionalContextConfiguration is null
                ? server.CreateHandler()
                : server.CreateHandler(additionalContextConfiguration);
 
            handlers[^1].InnerHandler = testServerHandler;
            handler = handlers[0];
        }
 
        return handler;
    }
 
    internal static DelegatingHandler[] CreateHandlers(this TestServer server,TestServerClientHandlerOptions options)
    {
        return CreateHandlersCore(server, options).ToArray();
 
        static IEnumerable<DelegatingHandler> CreateHandlersCore(TestServer server, TestServerClientHandlerOptions options)
        {
            RedirectHandler? redirectHandler = null;
            if (options.AllowAutoRedirect)
            {
                redirectHandler = new RedirectHandler(options.MaxAutomaticRedirections);
                yield return redirectHandler;
            }
 
            if (options.ProcessRemoteRequest)
            {
                if (string.IsNullOrEmpty(options.RemoteRequestClientName))
                    throw new ArgumentException($"{nameof(options.RemoteRequestClientName)} must have content when {nameof(options.ProcessRemoteRequest)} is true.", nameof(options));
 
                yield return new RemoteLocalAutoSwitchWithRedirectHandler( 
                    server.BaseAddress,
                    redirectHandler,
                    server.Services.CreateScope(),
                    options.RemoteRequestClientName);
            }
 
            if (options.HandleCookies)
            {
                yield return new CookieContainerHandler();
            }
        }
    }
}
 
public class RemoteRequestClientOptions
{
    public string? RemoteRequestClientName { get; set; }
    public IEnumerable<Func<IServiceProvider, DelegatingHandler>>? AppendHttpMessageHandlers { get; set; }
    public Action<IList<DelegatingHandler>, IServiceProvider>? ConfigureAdditionalHttpMessageHandlers { get; set; }
}

这是用于配置TestServer主机的扩展。其中定义的几个委托用于追加自定义配置提高灵活性。

MyHub

public class MyHub : Hub
{
    public async Task SendMessage(string user, string message)
    {
        await Clients.All.SendAsync("ReceiveMessage", user, message);
    }
 
    public async Task SendBinary(string user, byte[] bytes)
    {
        await Clients.All.SendAsync("ReceiveBinary", user, bytes);
    }
}

为了测试单机模式下是否能使用SignalR功能,写了一个简单的集线器。

Startup(节选)

// 服务注册部分
services.AddSignalR(options => options.StatefulReconnectBufferSize = 100_000);
 
// 管道配置部分
var hostInTestServer = configuration.GetValue("HostInTestServer", false);
if (!hostInTestServer)
{
    app.UseHsts();
    app.UseHttpsRedirection();
}
 
// 端点配置部分
endpoints.MapHub<MyHub>("MyHub", options =>
{
    options.AllowStatefulReconnects = true;
});
 
var redirectToHome = static (HttpContext context) => Task.FromResult(Results.Redirect("/"));
endpoints.Map("/re", redirectToHome);
 
var redirectToBaidu = static (HttpContext context) => Task.FromResult(Results.Redirect("https://www.baidu.com/"));
endpoints.Map("/reBaidu", redirectToBaidu);
 
var redirectToOutRe = static (HttpContext context) => Task.FromResult(Results.Redirect("https://localhost:7215/inRe", preserveMethod: true));
endpoints.Map("/outRe", redirectToOutRe);
 
var redirectToInRe = static (HttpContext context, TestParam? param) => Task.FromResult(Results.Redirect($"http://localhost/{param?.Path?.TrimStart('/')}", preserveMethod: true));
endpoints.Map("/inRe", redirectToInRe);

Startup只是在RazorPages模版的基础上追加了以上内容,为了方便使用没有使用新模版的写法。新模版完全是对老模版的包装,还导致了少量功能无法使用,笔者这边的用法刚好是新模版不好用的情况。

为了避免不必要的HTTPS重定向,在单机模式下不注册跳转中间件和严格传输模式中间件。

Program

public class Program
{
    public static async Task Main(string[] args)
    {
        using var kestrelServerHost = CreateHostBuilder(args).Build();
        await kestrelServerHost.StartAsync();
 
        using var testServerHost = CreateHostBuilder(args, ConfigureTestServer()).Build();
        await testServerHost.StartAsync();
 
        var testServer = testServerHost.GetTestServer();
        var testServerClient = testServerHost.GetTestClient(new()
        {
            ProcessRemoteRequest = true,
            DefaultRequestVersion = new(3, 0)
        });
 
        var multiRedirectResponse = await testServerClient.PostAsJsonAsync("/outRe", new TestParam { Path = "/reBaidu" });
        var multiRedirectContent = await multiRedirectResponse.Content.ReadAsStringAsync();
        Console.WriteLine(multiRedirectContent);
 
        var connection = new HubConnectionBuilder()
            .WithUrl(
                new Uri(testServer.BaseAddress, "/MyHub"),
                HttpTransportType.WebSockets,
                options =>
                {
                    options.HttpMessageHandlerFactory = handler =>
                    {
                        var newHandler = testServer.CreateHandler(options: new());
                        return newHandler;
                    };
                    options.WebSocketFactory = (context, cancellationToken) =>
                    {
                        var webSocketClient = testServer.CreateWebSocketClient();
                        var webSocket = webSocketClient.ConnectAsync(context.Uri, cancellationToken);
                        return new(webSocket);
                    };
                }
            )
            .WithStatefulReconnect()
            .WithAutomaticReconnect()
            .Build();
 
        connection.On<string, string>("ReceiveMessage", (user, message) =>
        {
            var newMessage = $"{user}: {message}";
            Console.WriteLine(newMessage);
        });
 
        var times = 0;
        connection.On<string, byte[]>("ReceiveBinary", (user, bytes) =>
        {
            Interlocked.Increment(ref times);
            var newMessage = $"{user}: No.{times,10}: {bytes.Length} bytes";
            Console.WriteLine(newMessage);
        });
 
        await connection.StartAsync();
        await connection.InvokeAsync("SendMessage", "ConsoleClient", "ConsoleClientMessage");
 
        Console.WriteLine("内存压力测试开始");
        Stopwatch sw = Stopwatch.StartNew();
        var tenMinutes = TimeSpan.FromMinutes(10);
        while (sw.Elapsed < tenMinutes)
        {
            await connection.InvokeAsync("SendBinary", "ConsoleClient", new byte[1024 * 10]);
            await Task.Delay(10);
        }
        Console.WriteLine("内存压力测试结束");
 
        Console.Write("按任意键继续...");
        Console.ReadKey();
 
        await connection.StopAsync();
        await testServerHost.StopAsync();
        await kestrelServerHost.StopAsync();
    }
 
    public static IHostBuilder CreateHostBuilder(string[] args) => CreateHostBuilder(args, null);
 
    public static IHostBuilder CreateHostBuilder(string[] args, Action<IWebHostBuilder>? configureWebBuilder) =>
        Host.CreateDefaultBuilder(args)
            .ConfigureWebHostDefaults(webBuilder =>
            {
                webBuilder
                    .UseStartup<Startup>();
                configureWebBuilder?.Invoke(webBuilder);
            });
}
 
public class TestParam
{
    public string? Path { get; set; }
}

这里使用Post一个Json到/outRe的请求测试连续相互跳转。其中的Json用于测试是否能正常处理多次请求流的数据发送。outRe会返回一个到网络主机的地址的重定向,网络主机又会返回到单机主机的/inRe地址的重定向,这里会读取Json的内容决定最后一次跳转的地址,两个跳转地址分别用来测试本地跳转和网络跳转。

然后连接SignalR测试是否能连接成功以及内存泄漏测试,其中内存泄漏测试用VS的诊断面板来看比较方便。

效果测试

全部准备完成后就可以测试效果了。经过实测,本地SignalR客户端在连接单机WebSocket时无法处理HTTPS跳转,TestServer创建的WebSocketClient没有配置途径,内置Handler没有处理重定向请求。每秒100次每次10K的二进制数据传输的10分钟测试也没有出现内存泄漏,内存会在一定增长后保持稳定。根据SignalR的测试结果和官网文档,gRPC理论上应该也能完整支持。最后是刻意构造的带数据Post的多次本地、网络交叉重定向测试,结果验证成功。

测试本地、网络相互跳转是打开一个监听本地端口的普通主机来提供从网络跳转回本地的服务。而这个普通主机只是个没有调用过TestServer配置的原始版本。从这里也可以看出单机主机和网络主机的切换非常方便。

image

image

结语

使用这个方法可以在单机程序中虚构出一个C/S架构,利用特制的HttpClient强制隔离业务逻辑和界面数据。这样还能获得一个免费的好处,如果将来要把程序做成真的网络应用,几乎可以0成本完成迁移改造。同样的,熟悉网络程序的开发者也可以在最大程度上利用已有经验开发单机应用。

又是很久没有写文章了,一直没有找到什么好选题,难得找到一个,经过将近1周的研究开发终于搞定了。

代码包:InProcessAspNetCoreApp.rar

代码包调整了直接运行exe的一些设置,主要和HTTPS有关,制作证书还是比较麻烦的,所以直接关闭了HTTPS。当然方法很简单粗暴,理论上应该通过主机设置来调整,演示就用偷懒方法处理了。

文章转载自:coredx

原文链接:https://www.cnblogs.com/coredx/p/17998563

体验地址:引迈 - JNPF快速开发平台_低代码开发平台_零代码开发平台_流程设计器_表单引擎_工作流引擎_软件架构

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/373027.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Nginx使用详解

简介Nginx的优缺点Nginx的应用场景Nginx支持的模块Nginx模块配置示例1. **HTTP Access Log 模块**2. **HTTP SSL 模块**3. **HTTP Gzip 模块**4. **HTTP Rewrite 模块** Nginx支持的反向代理协议Nginx反向代理配置Nginx反向代理优点Nginx反向代理配置示例Nginx常用配置参数Ngin…

一文搞懂 springboot 如何融合数据源

1、简介 springboot 支持关系型数据库的相关组件进行配置&#xff0c;包括数据源、连接池、事务管理器等的自动配置。降低了数据库使用的难度&#xff0c;除了 mysql 还支持 Derby、H2等嵌入式数据库的自动配置&#xff0c;MongoDB、Redis、elasticsearch等常用的 NoSQL 的数据…

uWSGI、灰度发布、网站数据指标分析、网站限速

1 案例1&#xff1a;部署Python网站项目 1.1 问题 配置Nginx使其可以将动态访问转交给uWSGI&#xff1a; 1.2 方案 安装Python工具及依赖 安装uWSGI并编写配置文件 1.3 步骤 实现此案例需要按照如下步骤进行。 步骤一&#xff1a; 首先$教学资料目录/python拷贝到虚拟…

Python程序设计 函数

简单函数 函数&#xff1a;就是封装了一段可被重复调用执行的代码块。通过此代码块可以实现大量代码的重复使用。 函数的使用包含两个步骤&#xff1a; 定义函数 —— 封装 独立的功能 调用函数 —— 享受 封装 的成果 函数的作用&#xff0c;在开发程序时&#xff0c;使用…

Unity3d Shader篇(一)— 顶点漫反射着色器解析

文章目录 前言一、顶点漫反射着色器是什么&#xff1f;1. 顶点漫反射着色器的工作原理 二、编写顶点漫反射着色器1. 定义属性2. 创建 SubShader3. 编写着色器程序段4. 完成顶点着色器5. 完成片段着色器 三、效果四、总结 前言 在 Unity 中&#xff0c;Shader 可以用来实现各种…

jmeter设置关联

一、为什么要设置关联&#xff1f; http协议本身是无状态的&#xff0c;客户端只需要简单向服务器请求下载某些文件&#xff0c;无论是客户端还是服务端都不去记录彼此过去的行为&#xff0c;每一次请求之间都是独立的。如果jmeter需要设置跨线程组脚本&#xff0c;就必须设置…

【问题篇】activiti工作流转办并处理备注问题

当处理activiti转办问题时&#xff0c;需要做的就是处理审批人和备注问题。 处理的思路是&#xff0c;先将当前环节标志成转办标签&#xff0c;再通过BUSINESS_KEY_找到流程实例的历史记录&#xff0c;找到最新的一条复制一份出来&#xff0c;表示需要转办到的人的历史记录并设…

APP专项测试方法总结

APP专项测试 1、网络测试 可使用抓包工具辅助网格测试推荐&#xff1a;fiddler&#xff0c;Charles 网络切换&#xff1a; 2G-3G-4G-wifi-网络信号差–无网 网络信号弱&#xff1a; 关注是否出现ANR、crash 2、中断测试 意外中断&#xff1a; 来电&#xff1b;短信&am…

不需英文基础也可以轻松学编程,中文编程开发工具免费版下载,编程工具构件箱之扩展控制面板构件用法

不需英文基础也可以轻松学编程&#xff0c;中文编程开发工具免费版下载&#xff0c;编程工具构件箱之扩展控制面板构件用法 一、前言 编程入门视频教程链接 https://edu.csdn.net/course/detail/39036 编程工具及实例源码文件下载可以点击最下方官网卡片——软件下载——常…

ShardingSphere 5.x 系列【3】分库分表中间件技术选型

有道无术&#xff0c;术尚可求&#xff0c;有术无道&#xff0c;止于术。 本系列Spring Boot 版本 3.1.0 本系列ShardingSphere 版本 5.4.0 源码地址&#xff1a;https://gitee.com/pearl-organization/study-sharding-sphere-demo 文章目录 1. 前言2. My Cat3. ShardingSphe…

C++ 类与对象(下)

目录 1. 再谈构造函数 1.1 构造函数体赋值 1.2 初始化列表 1.3 explicit关键字 2. static成员 2.1 概念 2.2 特性 3.友元 3.1友元函数 3.2 友元类 4. 内部类 5.匿名对象 6.拷贝对象时的一些编译器优化 7. 再次理解类和对象 【本节目标】 1. 再谈构造函数 2. Static成员…

【产品升级】SmartPipe升级到版本2.0

在近一个月的攻关和测试下&#xff0c;SmartPipe软件轴线自动识别算法的性能大幅提升&#xff0c;鲁棒性和稳定性进一步增强。近一年来客户累计反馈的多种复杂管路&#xff08;包括带有支管管路、带有压瘪段管路、推弯管、装配管、带有复杂孔洞管路等&#xff09;现在均能够正确…

通过消息队列实现进程之间通信代码

#include <myhead.h> struct msgbuf {long int mtype; char mtext[1024]; }; //定义一个消息大小 #define MSGSIZE sizeof(struct msgbuf)-sizeof(long int) int main(int argc, const char *argv[]) {//1、创建key值以便创建消息队列key_t key ftok("/", k)…

Bootstrap5 图片轮播

Bootstrap5 轮播样式表使用的是CDN资源 <title>亚丁号</title><!-- 自定义样式表 --><link href"static/front/css/front.css" rel"stylesheet" /><!-- 新 Bootstrap5 核心 CSS 文件 --><link rel"stylesheet"…

STM32WLE5JC

Sub-GHz 无线电介绍 sub-GHz无线电是一种超低功耗sub-GHz无线电&#xff0c;工作在150-960MHz ISM频段。 在发送和接收中采用LoRa和&#xff08;G&#xff09;FSK调制&#xff0c;仅在发送中采用BPSK/(G)MSK调制&#xff0c;可以在距离、数据速率和功耗之间实现最佳权衡。 这…

freeswitch对接FunASR实时语音听写

1、镜像启动 通过下述命令拉取并启动FunASR软件包的docker镜像&#xff1a; sudo docker pull \registry.cn-hangzhou.aliyuncs.com/funasr_repo/funasr:funasr-runtime-sdk-online-cpu-0.1.7 mkdir -p ./funasr-runtime-resources/models sudo docker run -p 10096:10095 -i…

【Gephi项目实战-带数据集】利用gephi绘制微博肖战超话120位用户关系图,并计算整体网络指标与节点指标

数据集在评论区&#xff0c;B站演示视频在评论区&#xff01; 简介 最近2天需要用到gephi做社会网络分析&#xff0c;于是从0开始接触gephi并摸索出了gephi的基本使用指南。下面将结合真实的节点文件与边文件&#xff0c;利用gephi绘制社会网络并计算相关测量指标。整个过程会…

我们都是宇宙的奇迹

我们都是独一无二的个体&#xff0c;是宇宙的奇迹 如果我不关注自我&#xff0c;那我在这个宏大的宇宙中有什么意义&#xff1f; 关于你的问题&#xff0c;我想没有一个简单的答案&#xff0c;因为不同的人可能有不同的看法和感受。有些人可能认为&#xff0c;如果不关注自我&…

jbdc的简单了解

JDBC JDBC所处的位置 JDBC的本质 Java操作数据库的一套接口。 补充 ddl:数据库定义语言,例如建表,创建数据库等。 dml:数据库操作语言,例如增删改。 dql:数据库查询语言,例如查询语句。 注意 在创建Java项目后的第一个步骤是导入jar包。 导入jar包的步骤 1 创建l…

【C语言】const修饰指针的不同作用

目录 const修饰变量 const修饰指针变量 ①不用const修饰 ②const放在*的左边 ③const放在*的右边 ④*的左右两边都有const 结论 const修饰变量 变量是可以修改的&#xff0c;如果把变量的地址交给⼀个指针变量&#xff0c;通过指针变量的也可以修改这个变量。 但…
最新文章