前言

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

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

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

新书宣传

有关新书的更多介绍欢迎查看《C#与.NET6 开发从入门到实践》上市,作者亲自来打广告了!

正文

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配置的原始版本。从这里也可以看出单机主机和网络主机的切换非常方便。



结语

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

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

代码包:InProcessAspNetCoreApp.rar

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

QQ群

读者交流QQ群:540719365

欢迎读者和广大朋友一起交流,如发现本书错误也欢迎通过博客园、QQ群等方式告知我。

本文地址:https://www.cnblogs.com/coredx/p/17998563.html

利用 ASP.NET Core 开发单机应用的更多相关文章

  1. C# -- HttpWebRequest 和 HttpWebResponse 的使用 C#编写扫雷游戏 使用IIS调试ASP.NET网站程序 WCF入门教程 ASP.Net Core开发(踩坑)指南 ASP.Net Core Razor+AdminLTE 小试牛刀 webservice创建、部署和调用 .net接收post请求并把数据转为字典格式

    C# -- HttpWebRequest 和 HttpWebResponse 的使用 C# -- HttpWebRequest 和 HttpWebResponse 的使用 结合使用HttpWebReq ...

  2. [转]ASP.NET Core 开发-Logging 使用NLog 写日志文件

    本文转自:http://www.cnblogs.com/Leo_wl/p/5561812.html ASP.NET Core 开发-Logging 使用NLog 写日志文件. NLog 可以适用于 . ...

  3. ASP.NET Core 开发-中间件(Middleware)

    ASP.NET Core开发,开发并使用中间件(Middleware). 中间件是被组装成一个应用程序管道来处理请求和响应的软件组件. 每个组件选择是否传递给管道中的下一个组件的请求,并能之前和下一组 ...

  4. ASP.NET Core开发-Docker部署运行

    ASP.NET Core开发Docker部署,.NET Core支持Docker 部署运行.我们将ASP.NET Core 部署在Docker 上运行. 大家可能都见识过Docker ,今天我们就详细 ...

  5. ASP.NET Core开发-后台任务利器Hangfire使用

    ASP.NET Core开发系列之后台任务利器Hangfire 使用. Hangfire 是一款强大的.NET开源后台任务利器,无需Windows服务/任务计划程序. 可以使用于ASP.NET 应用也 ...

  6. ASP.NET Core开发-读取配置文件Configuration

    ASP.NET Core 是如何读取配置文件,今天我们来学习. ASP.NET Core的配置系统已经和之前版本的ASP.NET有所不同了,之前是依赖于System.Configuration和XML ...

  7. ASP.NET Core 开发-Entity Framework (EF) Core 1.0 Database First

    ASP.NET Core 开发-Entity Framework Core 1.0 Database First,ASP.NET Core 1.0 EF Core操作数据库. Entity Frame ...

  8. ASP.NET Core 开发-Logging 使用NLog 写日志文件

    ASP.NET Core 开发-Logging 使用NLog 写日志文件. NLog 可以适用于 .NET Core 和 ASP.NET Core . ASP.NET Core已经内置了日志支持,可以 ...

  9. ASP.NET Core 开发-中间件(StaticFiles)使用

    ASP.NET Core 开发,中间件(StaticFiles)的使用,我们开发一款简易的静态文件服务器. 告别需要使用文件,又需要安装一个web服务器.现在随时随地打开程序即可使用,跨平台,方便快捷 ...

  10. ASP.NET Core 开发 - Entity Framework (EF) Core

    EF Core 1.0 Database First http://www.cnblogs.com/linezero/p/EFCoreDBFirst.html ASP.NET Core 开发 - En ...

随机推荐

  1. 备忘 springboot 整合ehcache,注入CacheManager时提示 required a bean of type 'org.springframework.cache.CacheManager' that could not be found

    问题因人而异,此处仅做备忘 整合过程: 1.添加maven依赖 <dependency> <groupId>net.sf.ehcache</groupId> < ...

  2. 【真送礼物】1 分钟 Serverless 极速部署盲盒平台,自己部署自己抽!

    当前,Serverless 在移动应用.游戏等场景已经实现规模化应用,Serverless 技术可以更好的帮助开发者只关注应用创新,减少对开发与运维的过度关注. 为了让更多开发者在真实场景中体验 Se ...

  3. 【译】 双向流传输和ASP.NET Core 3.0上的gRPC简介

    原文相关 原文作者:Eduard Los 原文地址:https://medium.com/@eddyf1xxxer/bi-directional-streaming-and-introduction- ...

  4. iframe访问页面,出现 ERR_BLOCKED_BY_RESPONSE

    那是因为服务器输出了 X-Frame-Options 头,只要把这个头删除掉,就没问题了

  5. [转帖]oracle查询表变化量

    根据变化量,可确定表的繁忙度,以及作为判断可能数据增长的对象. select obj.owner, obj.object_name, to_char(sn.BEGIN_INTERVAL_TIME,'y ...

  6. [转帖]nginx中的if和else语法

    https://www.dyxmq.cn/it/nginx/nginx-if.html nginx支持if语法,语法和平常的代码格式差不多:   1 2 3 if ($xxx = xxx) {     ...

  7. 阿里云IPV6 创建虚拟机的过程

    阿里云IPV6 创建虚拟机的过程 背景 IPV6 已经越来越广泛的应用. 想在外网开通一下IPV6,发现还有一些坑. 这里总结一下. 备忘. 开通方式 1. 登录阿里云的控制台, 打开云服务器ECS的 ...

  8. Harbor修改默认网段以及设置开机启动的方法

    Harbor修改默认网段以及设置开机启动的方法 背景 docker 默认的网段是 172.16.xx.xx 网段. harbor进行设置时会自动加1 设置为 172.17.xx.xx 有时候这个网段是 ...

  9. [转帖]初识SkyWalking

    https://zhuanlan.zhihu.com/p/361579294 一.SkyWalking 是什么? 一个开源的可观测平台,用于从服务和云原生基础设施收集,分析, 聚合及可视化数据. Sk ...

  10. [转帖] GC耗时高,原因竟是服务流量小?

      原创:扣钉日记(微信公众号ID:codelogs),欢迎分享,转载请保留出处. 简介# 最近,我们系统配置了GC耗时的监控,但配置上之后,系统会偶尔出现GC耗时大于1s的报警,排查花了一些力气,故 ...