RPC远程过程调用,二进制封装,提供高吞吐低延迟的高性能RPC框架
大石头 authored at 2022-08-10 13:26:19
8.84 KiB
NewLife.Remoting
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using NewLife;
using NewLife.Data;
using NewLife.Log;
using NewLife.Net;
using NewLife.Remoting;
using NewLife.Security;
using Xunit;

namespace XUnitTest.Integration;

/// <summary>TCP与HTTP双协议混合集成测试</summary>
/// <remarks>
/// 验证同一控制器可同时通过 TCP/HTTP 访问,TCP 和 HTTP 客户端
/// 并发混合调用、UseHttpStatus 模式、ApiException 错误码穿透等行为。
/// </remarks>
public class DualProtocolIntegrationTests : DisposeBase
{
    private readonly ApiServer _TcpServer;
    private readonly ApiServer _HttpServer;
    private readonly Int32 _TcpPort;
    private readonly String _HttpAddress;

    public DualProtocolIntegrationTests()
    {
        // TCP 服务器
        _TcpServer = new ApiServer(0)
        {
            Log = XTrace.Log,
            ShowError = true,
        };
        _TcpServer.Register<DualController>();
        _TcpServer.Start();
        _TcpPort = _TcpServer.Port;

        // HTTP 服务器
        _HttpServer = new ApiServer(new NetUri(NetType.Http, "*", 0))
        {
            Log = XTrace.Log,
            ShowError = true,
        };
        _HttpServer.Register<DualController>();
        _HttpServer.Start();
        _HttpAddress = $"http://127.0.0.1:{_HttpServer.Port}";
    }

    protected override void Dispose(Boolean disposing)
    {
        base.Dispose(disposing);
        _TcpServer.TryDispose();
        _HttpServer.TryDispose();
    }

    #region 同一控制器双协议访问
    [Fact(DisplayName = "双协议_TCP调用成功")]
    public async Task TcpCallTest()
    {
        using var client = new ApiClient($"tcp://127.0.0.1:{_TcpPort}");

        var result = await client.InvokeAsync<String>("Dual/Hello", new { name = "TCP" });
        Assert.Equal("Hello, TCP!", result);
    }

    [Fact(DisplayName = "双协议_HTTP调用成功")]
    public async Task HttpCallTest()
    {
        IApiClient client = new ApiHttpClient(_HttpAddress);
        using var _ = client as IDisposable;

        var result = await client.InvokeAsync<String>("Dual/Hello", new { name = "HTTP" });
        Assert.Equal("Hello, HTTP!", result);
    }

    [Fact(DisplayName = "双协议_计算结果一致")]
    public async Task SameResultAcrossProtocolsTest()
    {
        using var tcpClient = new ApiClient($"tcp://127.0.0.1:{_TcpPort}");
        IApiClient httpClient = new ApiHttpClient(_HttpAddress);
        using var _ = httpClient as IDisposable;

        var tcpResult = await tcpClient.InvokeAsync<Int32>("Dual/Compute", new { a = 100, b = 200 });
        var httpResult = await httpClient.InvokeAsync<Int32>("Dual/Compute", new { a = 100, b = 200 });

        Assert.Equal(300, tcpResult);
        Assert.Equal(300, httpResult);
        Assert.Equal(tcpResult, httpResult);
    }

    [Fact(DisplayName = "双协议_复杂对象序列化一致")]
    public async Task ComplexObjectConsistencyTest()
    {
        using var tcpClient = new ApiClient($"tcp://127.0.0.1:{_TcpPort}");
        IApiClient httpClient = new ApiHttpClient(_HttpAddress);
        using var _ = httpClient as IDisposable;

        var tcpResult = await tcpClient.InvokeAsync<DualResult>("Dual/GetInfo", new { id = 42 });
        var httpResult = await httpClient.InvokeAsync<DualResult>("Dual/GetInfo", new { id = 42 });

        Assert.NotNull(tcpResult);
        Assert.NotNull(httpResult);
        Assert.Equal(tcpResult.Id, httpResult.Id);
        Assert.Equal(tcpResult.Name, httpResult.Name);
    }
    #endregion

    #region 异常传播
    [Fact(DisplayName = "双协议_TCP异常传播")]
    public async Task TcpExceptionPropagationTest()
    {
        using var client = new ApiClient($"tcp://127.0.0.1:{_TcpPort}");

        var ex = await Assert.ThrowsAsync<ApiException>(
            () => client.InvokeAsync<Object>("Dual/Fail"));
        Assert.Equal(500, ex.Code);
    }

    [Fact(DisplayName = "双协议_HTTP异常传播")]
    public async Task HttpExceptionPropagationTest()
    {
        IApiClient client = new ApiHttpClient(_HttpAddress);
        using var _ = client as IDisposable;

        var ex = await Assert.ThrowsAsync<ApiException>(
            () => client.InvokeAsync<Object>("Dual/Fail"));
        Assert.Equal(500, ex.Code);
    }

    [Fact(DisplayName = "双协议_自定义ApiException穿透TCP")]
    public async Task TcpCustomApiExceptionTest()
    {
        using var client = new ApiClient($"tcp://127.0.0.1:{_TcpPort}");

        var ex = await Assert.ThrowsAsync<ApiException>(
            () => client.InvokeAsync<Object>("Dual/CustomFail"));
        Assert.Equal(1001, ex.Code);
        Assert.Equal("业务异常", ex.Message);
    }

    [Fact(DisplayName = "双协议_自定义ApiException穿透HTTP")]
    public async Task HttpCustomApiExceptionTest()
    {
        IApiClient client = new ApiHttpClient(_HttpAddress);
        using var _ = client as IDisposable;

        var ex = await Assert.ThrowsAsync<ApiException>(
            () => client.InvokeAsync<Object>("Dual/CustomFail"));
        Assert.Equal(1001, ex.Code);
        Assert.Equal("业务异常", ex.Message);
    }
    #endregion

    #region TCP与HTTP并发混合
    [Fact(DisplayName = "双协议_混合并发调用")]
    public async Task MixedConcurrentCallsTest()
    {
        using var tcpClient = new ApiClient($"tcp://127.0.0.1:{_TcpPort}");
        IApiClient httpClient = new ApiHttpClient(_HttpAddress);
        using var _ = httpClient as IDisposable;

        var tasks = new List<Task<Int32>>();

        // TCP 和 HTTP 交替并发
        for (var i = 0; i < 20; i++)
        {
            var index = i;
            if (i % 2 == 0)
                tasks.Add(tcpClient.InvokeAsync<Int32>("Dual/Compute", new { a = index, b = 1 }));
            else
                tasks.Add(httpClient.InvokeAsync<Int32>("Dual/Compute", new { a = index, b = 1 }));
        }

        var results = await Task.WhenAll(tasks);

        for (var i = 0; i < 20; i++)
        {
            Assert.Equal(i + 1, results[i]);
        }
    }
    #endregion

    #region UseHttpStatus模式
    [Fact(DisplayName = "UseHttpStatus_TCP正常工作")]
    public async Task UseHttpStatusTcpModeTest()
    {
        using var server = new ApiServer(0)
        {
            Log = XTrace.Log,
            ShowError = true,
            UseHttpStatus = true,
        };
        server.Register<DualController>();
        server.Start();

        using var client = new ApiClient($"tcp://127.0.0.1:{server.Port}");

        // 正常调用应成功
        var result = await client.InvokeAsync<String>("Dual/Hello", new { name = "Status" });
        Assert.Equal("Hello, Status!", result);

        // 异常仍可传播
        var ex = await Assert.ThrowsAsync<ApiException>(
            () => client.InvokeAsync<Object>("Dual/Fail"));
        Assert.Equal(500, ex.Code);
    }
    #endregion

    #region API列表双协议
    [Fact(DisplayName = "双协议_TCP和HTTP获取一致的API列表")]
    public async Task ApiListConsistencyTest()
    {
        using var tcpClient = new ApiClient($"tcp://127.0.0.1:{_TcpPort}");
        IApiClient httpClient = new ApiHttpClient(_HttpAddress);
        using var _ = httpClient as IDisposable;

        var tcpApis = await tcpClient.InvokeAsync<String[]>("Api/All");
        var httpApis = await httpClient.InvokeAsync<String[]>("Api/All");

        Assert.NotNull(tcpApis);
        Assert.NotNull(httpApis);

        // 同一控制器注册到不同服务器,API 列表应包含相同的业务方法
        Assert.Contains(tcpApis, a => a.Contains("Dual/Hello"));
        Assert.Contains(httpApis, a => a.Contains("Dual/Hello"));
    }
    #endregion

    #region 大数据双协议
    [Fact(DisplayName = "双协议_TCP传输二进制数据")]
    public async Task TcpBinaryDataTest()
    {
        using var client = new ApiClient($"tcp://127.0.0.1:{_TcpPort}");

        var data = Rand.NextBytes(4096);
        var result = await client.InvokeAsync<Packet>("Dual/Echo", data);

        Assert.NotNull(result);
        Assert.Equal(data.Length, result.Total);
        Assert.True(data.SequenceEqual(result.ToArray()));
    }
    #endregion

    #region 辅助类
    class DualController
    {
        public String Hello(String name) => $"Hello, {name}!";

        public Int32 Compute(Int32 a, Int32 b) => a + b;

        public DualResult GetInfo(Int32 id) => new()
        {
            Id = id,
            Name = $"Item_{id}",
            Created = DateTime.Now,
        };

        public void Fail() => throw new InvalidOperationException("服务端异常");

        public void CustomFail() => throw new ApiException(1001, "业务异常");

        public IPacket Echo(IPacket pk) => pk;
    }

    class DualResult
    {
        public Int32 Id { get; set; }
        public String Name { get; set; } = "";
        public DateTime Created { get; set; }
    }
    #endregion
}