Cysharp / MagicOnion

Unified Realtime/API framework for .NET platform and Unity.
MIT License
3.9k stars 433 forks source link

StreamingHub has already been disconnected from the server #777

Open licentia88 opened 6 months ago

licentia88 commented 6 months ago

Hello, I'm using magiconion hubs in my blazor project, I have a hub implementation for a Ticket System, the goal is to perform crud operations on tickets and display the data for all company members. therefore my TicketHub is registered as a Singleton Service

I connect to the server on appstart and use the same service in the hub.

.My hub is derived from a base class that has the below methods, and use the below configuration for simplicity sake I'm not sharing all the code, but in case anyone asks for it I can gladly share.

The Problem: after deploying the app in IIS and launch it after a while when I try to use the Hub Methods I get an exception telling me that StreamingHub has already been disconnected from the server.

I also want to mention that I do not manually disconnect /dispose the connection at anypoint.

My code declaration is below:

/// <summary>
/// Connects to the service hub.
/// </summary>
/// <returns>A task representing the async operation.</returns>
public virtual async Task<Guid> ConnectAsync()
{
    //AppContext.SetSwitch("System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport", true);

    string baseUrl = Configuration.GetValue<string>("API_BASE_URL:HTTPS");

    var SslAuthOptions = CreateSslClientAuthOptions();

    var socketHandler = CreateHttpClientWithSocketsHandler(SslAuthOptions, Timeout.InfiniteTimeSpan, TimeSpan.FromSeconds(60), TimeSpan.FromSeconds(30));

    var channelOptions = CreateGrpcChannelOptions(socketHandler);

    var channel = GrpcChannel.ForAddress(baseUrl, channelOptions);

    Client = await StreamingHubClient.ConnectAsync<THub, TReceiver>(
        channel, this as TReceiver, null, default, MemoryPackMagicOnionSerializerProvider.Instance);

    var connectionId = await Client.ConnectAsync();

    return connectionId;
}

/// <summary>
/// Creates SSL client authentication options for gRPC client communication.
/// </summary>
/// <param name="certificate">The X509 certificate used for client authentication.</param>
/// <returns>An instance of <see cref="SslClientAuthenticationOptions"/> configured with the certificate and validation callback.</returns>
public SslClientAuthenticationOptions CreateSslClientAuthOptions(X509Certificate2 certificate = default)
{
    return new SslClientAuthenticationOptions
    {
        RemoteCertificateValidationCallback = (sender, cert, _, _) =>
        {
            X509Chain x509Chain = new X509Chain();
            x509Chain.ChainPolicy.RevocationMode = X509RevocationMode.NoCheck;
            return certificate is null || x509Chain.Build(new X509Certificate2(cert));
            //bool isChainValid = x509Chain.Build(new X509Certificate2(cert));
            //return isChainValid;
        },
        //ClientCertificates = new X509Certificate2Collection { certificate }
    };
}

/// <summary>
/// Creates an instance of <see cref="SocketsHttpHandler"/> configured with provided options.
/// </summary>
/// <param name="sslClientAuthenticationOptions">The SSL client authentication options.</param>
/// <param name="pooledConnectionIdleTimeout">The timeout for idle pooled connections.</param>
/// <param name="keepAlivePingDelay">The delay between keep-alive pings.</param>
/// <param name="keepAlivePingTimeout">The timeout for keep-alive pings.</param>
/// <param name="enableMultipleHttp2Connections">Indicates whether multiple HTTP/2 connections are enabled.</param>
/// <returns>An instance of <see cref="SocketsHttpHandler"/> configured with the provided options.</returns>
public SocketsHttpHandler CreateHttpClientWithSocketsHandler(SslClientAuthenticationOptions sslClientAuthenticationOptions,
 TimeSpan pooledConnectionIdleTimeout,
 TimeSpan keepAlivePingDelay,
 TimeSpan keepAlivePingTimeout,
 bool enableMultipleHttp2Connections = true)
{
    var socketsHandler = new SocketsHttpHandler
    {
        SslOptions = sslClientAuthenticationOptions,
        PooledConnectionIdleTimeout = pooledConnectionIdleTimeout,
        KeepAlivePingDelay = keepAlivePingDelay,
        KeepAlivePingTimeout = keepAlivePingTimeout,
        EnableMultipleHttp2Connections = enableMultipleHttp2Connections
    };

    return socketsHandler;
}

/// <summary>
/// Creates gRPC channel options with the provided <see cref="SocketsHttpHandler"/>.
/// </summary>
/// <param name="socketsHandler">The configured <see cref="SocketsHttpHandler"/>.</param>
/// <returns>An instance of <see cref="GrpcChannelOptions"/> with the specified HTTP handler and message size limits.</returns>
public GrpcChannelOptions CreateGrpcChannelOptions(SocketsHttpHandler socketsHandler)
{
    var channelOptions = new GrpcChannelOptions
    {
        HttpHandler = socketsHandler,
        MaxReceiveMessageSize = null,
        MaxSendMessageSize = null
    };

    return channelOptions;
}
Nektarinchik commented 1 month ago

I have exactly the same problem only when app deployed on IIS. Client tells, that server reset the stream, but server tells, that client has been disconnected. @licentia88 may be you have any updates?

licentia88 commented 1 month ago

Hi, unfortunately, I couldn't fix the issue on the IIS side, but I came up with a workaround on the client side.

I created a method that runs every minute using a scheduler (similar to a ping).

You can check out a template project that shows how to implement MagicOnion and other Cysharp projects. If you look at MagicHubClientBase.cs in Client/Hubs/Base, you'll find the solution I came up with.

Here's the link to the repo: https://github.com/licentia88/MagicOnionGenericTemplate