Skip to content

Commit

Permalink
唯一客户端基类ClientBase,废除Http基类和Rpc基类,否则最终业务基类无法同时支持二者(因只能继承其中一个);
Browse files Browse the repository at this point in the history
拆分WebSocket通道,以支持netcore和net45
  • Loading branch information
nnhy committed Jun 11, 2024
1 parent 9e93c31 commit 03104e0
Show file tree
Hide file tree
Showing 9 changed files with 331 additions and 216 deletions.
80 changes: 76 additions & 4 deletions NewLife.Remoting/Clients/ClientBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using NewLife.Caching;
using NewLife.Log;
using NewLife.Model;
using NewLife.Net;
using NewLife.Reflection;
using NewLife.Remoting.Models;
using NewLife.Security;
Expand All @@ -29,6 +30,10 @@ public abstract class ClientBase : DisposeBase, ICommandClient, IEventProvider,
/// <summary>服务提供者</summary>
public IServiceProvider? ServiceProvider { get; set; }

private IApiClient? _client;
/// <summary>Api客户端</summary>
public IApiClient? Client => _client;

/// <summary>是否已登录</summary>
public Boolean Logined { get; set; }

Expand Down Expand Up @@ -123,14 +128,51 @@ protected virtual void OnInit()
}

PasswordProvider ??= GetService<IPasswordProvider>() ?? new SaltPasswordProvider { Algorithm = "md5" };

if (Client == null)
{
var urls = Setting?.Server;
if (urls.IsNullOrEmpty()) throw new ArgumentNullException(nameof(Setting), "未指定服务端地址");

_client = urls.StartsWithIgnoreCase("http", "https") ? CreateHttp(urls) : CreateRpc(urls);
}
}

/// <summary>创建Http客户端</summary>
/// <param name="urls"></param>
/// <returns></returns>
protected virtual ApiHttpClient CreateHttp(String urls) => new(urls) { Log = Log };

/// <summary>创建RPC客户端</summary>
/// <param name="urls"></param>
/// <returns></returns>
protected virtual ApiClient CreateRpc(String urls) => new MyApiClient { Client = this, Servers = urls.Split(","), Log = Log };

class MyApiClient : ApiClient
{
public ClientBase Client { get; set; } = null!;

protected override async Task<Object?> OnLoginAsync(ISocketClient client, Boolean force) => await InvokeWithClientAsync<Object>(client, Client.Prefix + "Login", Client.BuildLoginRequest());
}

/// <summary>异步调用</summary>
/// <param name="action">动作</param>
/// <param name="args">参数</param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
public abstract Task<TResult> OnInvokeAsync<TResult>(String action, Object? args, CancellationToken cancellationToken);
public virtual async Task<TResult> OnInvokeAsync<TResult>(String action, Object? args, CancellationToken cancellationToken)
{
if (_client is ApiHttpClient http)
{
var method = System.Net.Http.HttpMethod.Post;
if (args == null || action.StartsWithIgnoreCase("Get") || action.ToLower().Contains("/get"))
method = System.Net.Http.HttpMethod.Get;

return await http.InvokeAsync<TResult>(method, action, args, null, cancellationToken);

Check warning on line 171 in NewLife.Remoting/Clients/ClientBase.cs

View workflow job for this annotation

GitHub Actions / test

Possible null reference return.
}

return await _client.InvokeAsync<TResult>(action, args, cancellationToken);

Check warning on line 174 in NewLife.Remoting/Clients/ClientBase.cs

View workflow job for this annotation

GitHub Actions / test

Dereference of a possibly null reference.

Check warning on line 174 in NewLife.Remoting/Clients/ClientBase.cs

View workflow job for this annotation

GitHub Actions / test

Possible null reference return.
}

/// <summary>远程调用拦截,支持重新登录</summary>
/// <typeparam name="TResult"></typeparam>
Expand Down Expand Up @@ -170,7 +212,10 @@ protected virtual void OnInit()

/// <summary>设置令牌。派生类可重定义逻辑</summary>
/// <param name="token"></param>
protected virtual void SetToken(String? token) { }
protected virtual void SetToken(String? token)
{
if (_client != null) _client.Token = token;
}

/// <summary>获取相对于服务器的当前时间,避免两端时间差</summary>
/// <returns></returns>
Expand Down Expand Up @@ -439,18 +484,45 @@ protected virtual void StopTimer()
_timerUpgrade = null;
_eventTimer.TryDispose();
_eventTimer = null;

_ws.TryDispose();
_ws = null;
}

private WsChannel? _ws;
/// <summary>定时心跳</summary>
/// <param name="state"></param>
/// <returns></returns>
protected virtual Task OnPing(Object state) => Ping();
protected virtual async Task OnPing(Object state)
{
DefaultSpan.Current = null;
using var span = Tracer?.NewSpan("DevicePing");
try
{
var rs = await Ping();

if (_client is ApiHttpClient http)
{
#if NETCOREAPP
_ws ??= new WsChannelCore(this);
#else
_ws ??= new WsChannel(this);
#endif
if (_ws != null) await _ws.ValidWebSocket(http);
}
}
catch (Exception ex)
{
span?.SetError(ex, null);
Log?.Debug("{0}", ex);
}
}

/// <summary>收到命令</summary>
/// <param name="model"></param>
/// <param name="source"></param>
/// <returns></returns>
protected async Task ReceiveCommand(CommandModel model, String source)
public async Task ReceiveCommand(CommandModel model, String source)
{
if (model == null) return;

Expand Down
30 changes: 0 additions & 30 deletions NewLife.Remoting/Clients/HttpClientBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -86,36 +86,6 @@ protected override void SetToken(String? token)
}
#endregion

#region 登录
/// <summary>登录</summary>
/// <param name="request">登录信息</param>
/// <returns></returns>
protected override async Task<ILoginResponse?> LoginAsync(ILoginRequest request)
{
// 登录前清空令牌,避免服务端使用上一次信息
_client.Token = null;

var rs = await base.LoginAsync(request);

// 登录后设置用于用户认证的token
_client.Token = rs?.Token;

return rs;
}

/// <summary>注销</summary>
/// <returns></returns>
protected override async Task<ILogoutResponse?> LogoutAsync(String reason)
{
var rs = await base.LogoutAsync(reason);

// 更新令牌
_client.Token = rs?.Token;

return rs;
}
#endregion

#region 心跳
/// <summary>心跳后建立WebSocket长连接</summary>
/// <param name="state"></param>
Expand Down
63 changes: 1 addition & 62 deletions NewLife.Remoting/Clients/RpcClientBase.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
using System.Diagnostics.CodeAnalysis;
using System.Net.Http;
using NewLife.Log;
using NewLife.Log;
using NewLife.Net;
using NewLife.Remoting.Models;

namespace NewLife.Remoting.Clients;

Expand Down Expand Up @@ -60,62 +57,4 @@ protected override void SetToken(String? token)
if (_client != null) _client.Token = token;
}
#endregion

#region 登录
/// <summary>登录</summary>
/// <returns></returns>
public override async Task<ILoginResponse?> Login()
{
_client.Token = null;

var rs = await base.Login();

_client.Token = rs?.Token;

return rs;
}

/// <summary>登录</summary>
/// <param name="request">登录信息</param>
/// <returns></returns>
protected override async Task<ILoginResponse?> LoginAsync(ILoginRequest request)
{
// 登录前清空令牌,避免服务端使用上一次信息
_client.Token = null;

var rs = await base.LoginAsync(request);

// 登录后设置用于用户认证的token
_client.Token = rs?.Token;

return rs;
}

/// <summary>注销</summary>
/// <returns></returns>
protected override async Task<ILogoutResponse?> LogoutAsync(String reason)
{
var rs = await base.LogoutAsync(reason);

// 更新令牌
_client.Token = rs?.Token;

return rs;
}
#endregion

#region 心跳
/// <summary>心跳</summary>
/// <returns></returns>
public override async Task<IPingResponse?> Ping()
{
var rs = await base.Ping();

// 令牌
if (rs != null && rs.Token.IsNullOrEmpty())
_client.Token = rs.Token;

return rs;
}
#endregion
}
124 changes: 124 additions & 0 deletions NewLife.Remoting/Clients/WsChannel.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
using NewLife.Log;
using NewLife.Net;
using NewLife.Remoting.Models;
using NewLife.Serialization;

namespace NewLife.Remoting.Clients;

/// <summary>WebSocket</summary>
class WsChannel : DisposeBase
{
private readonly ClientBase _client;

public WsChannel(ClientBase client) => _client = client;

protected override void Dispose(Boolean disposing)
{
base.Dispose(disposing);

StopWebSocket();
}

public virtual async Task ValidWebSocket(ApiHttpClient http)
{
var svc = http.Current;
if (svc == null) return;

// 使用过滤器内部token,因为它有过期刷新机制
var token = http.Token;
if (http.Filter is NewLife.Http.TokenHttpFilter thf) token = thf.Token?.AccessToken;

var span = DefaultSpan.Current;
span?.AppendTag($"svc={svc.Address} Token=[{token?.Length}]");

if (token.IsNullOrEmpty()) return;

if (_websocket != null && !_websocket.Disposed)
{
try
{
// 在websocket链路上定时发送心跳,避免长连接被断开
await _websocket.SendTextAsync("Ping");
}
catch (Exception ex)
{
span?.SetError(ex, null);
_client.WriteLog("{0}", ex);
}
}

if (_websocket == null || _websocket.Disposed)
{
var url = svc.Address.ToString().Replace("http://", "ws://").Replace("https://", "wss://");
var uri = new Uri(new Uri(url), "/Device/Notify");

using var span2 = _client.Tracer?.NewSpan("WebSocketConnect", uri + "");

var client = uri.CreateRemote() as WebSocketClient;
//todo 设置令牌
//client.Options.SetRequestHeader("Authorization", "Bearer " + token);

span?.AppendTag($"WebSocket.Connect {uri}");
//await client.ConnectAsync(uri, default);
client.Open();

Check warning on line 63 in NewLife.Remoting/Clients/WsChannel.cs

View workflow job for this annotation

GitHub Actions / test

Dereference of a possibly null reference.

_websocket = client;

_source = new CancellationTokenSource();
_ = Task.Run(() => DoPull(client, _source.Token));
}
}

private WebSocketClient? _websocket;
private CancellationTokenSource? _source;
private async Task DoPull(WebSocketClient socket, CancellationToken cancellationToken)
{
DefaultSpan.Current = null;
try
{
var buf = new Byte[4 * 1024];
while (!cancellationToken.IsCancellationRequested && !socket.Disposed)
{
var rs = await socket.ReceiveMessageAsync(cancellationToken);
var txt = rs.Payload.ToStr();
await OnReceive(txt);
}
}
catch (Exception ex)
{
XTrace.WriteException(ex);
}

if (!socket.Disposed) await socket.CloseAsync(1000, "finish", default);
}

/// <summary>收到服务端主动下发消息。默认转为CommandModel命令处理</summary>
/// <param name="message"></param>
/// <returns></returns>
private async Task OnReceive(String message)
{
if (message.StartsWithIgnoreCase("Pong"))
{
}
else
{
var model = message.ToJsonEntity<CommandModel>();
if (model != null) await _client.ReceiveCommand(model, "WebSocket");
}
}

private void StopWebSocket()
{
#if NETCOREAPP
_source?.Cancel();
try
{
if (_websocket != null && !_websocket.Disposed)
_websocket.CloseAsync(1000, "finish", default);
}
catch { }

_websocket = null;
#endif
}
}
Loading

0 comments on commit 03104e0

Please sign in to comment.