SignalR循序漸進(二)泛型Hub
接上一篇,文章末尾丟擲了2個問題:
- 能不能讓客戶端宣告一個強型別的方法列表呢?這樣首先不容易寫錯。
- 同樣的,能不能讓服務端宣告一個強型別的方法列表給客戶端呼叫呢?
如果要讓客戶端的方法以強型別出現在服務端,同樣的,服務端的方法也以強型別出現在客戶端,那就必須宣告類似契約一樣的載體。比如:
public interface IChatClient { void broadcast(string name, string message); }
public interface IChatHub { void Send(string name, string message); }
分別建立ChatClient介面和ChatHub的介面。
public class ChatHub : Hub<IChatClient> { ... }
這是最終的目標,一個泛型Hub。
好,現在需要進行一些分析,怎樣才能讓Hub支援泛型。
首先,看一下Hub是如何操作客戶端方法的:
Clients.AllExcept(Context.ConnectionId).broadcast(name, message);
Hub通過Clients來操作所有客戶端的行為。那麼這個Clients又是什麼型別的呢?
// 摘要: // Gets a dynamic object that represents all clients connected to this hub (not // hub instance). IHubCallerConnectionContext Clients { get; set; }
通過IHub介面看到,Clients的型別是IHubCallerConnectionContext,點進去看:
// 摘要: // Encapsulates all information about an individual SignalR connection for an // Microsoft.AspNet.SignalR.Hubs.IHub. public interface IHubCallerConnectionContext : IHubConnectionContext { [Dynamic] dynamic Caller { get; } [Dynamic] dynamic Others { get; } dynamic OthersInGroup(string groupName); dynamic OthersInGroups(IList<string> groupNames); }
IHubCallerConnectionContext又繼承IHubConnectionContext,再點進去看:
// 摘要: // Encapsulates all information about a SignalR connection for an Microsoft.AspNet.SignalR.Hubs.IHub. public interface IHubConnectionContext { [Dynamic] dynamic All { get; } dynamic AllExcept(params string[] excludeConnectionIds); dynamic Client(string connectionId); dynamic Clients(IList<string> connectionIds); dynamic Group(string groupName, params string[] excludeConnectionIds); dynamic Groups(IList<string> groupNames, params string[] excludeConnectionIds); dynamic User(string userId); }
一目瞭然,所有Clients的操作方法都在這兒了,全是動態型別的,這也是為什麼在Hub中寫到Clients.All.xxx的時候已經是動態的了,那麼執行時,這些操作都是什麼型別的呢?試一下:
執行時,Clients的操作返回的是ClientProxy型別,從程式碼中扒出來:
public class ClientProxy : DynamicObject, IClientProxy { public ClientProxy(IConnection connection, IHubPipelineInvoker invoker, string hubName, IList<string> exclude); public Task Invoke(string method, params object[] args); public override bool TryInvokeMember(InvokeMemberBinder binder, object[] args, out object result); }
// 摘要: // A server side proxy for the client side hub. public interface IClientProxy { // 摘要: // Invokes a method on the connection(s) represented by the Microsoft.AspNet.SignalR.Hubs.IClientProxy // instance. // // 引數: // method: // name of the method to invoke // // args: // argumetns to pass to the client // // 返回結果: // A task that represents when the data has been sent to the client. Task Invoke(string method, params object[] args); } }
可以看到,執行時如果以IClientProxy注入,就一個Invoke方法。
好,挖到這兒,可以有一些思路了。
- Clients所有的操作最終都是通過IClientProxy的Invoke來執行的。
- 如果讓IChatClient通過某種方式和IClientProxy建立起非執行時的聯絡,就能實現強型別了。
- 這樣的話,就需要有一個Hub<T>的類,然後把Clients裡所有的操作在Hub<T>中重新實現一次。
- 然後T又是客戶端的行為介面,因此,需要對Hub<T>進行靜態擴充套件,讓IClientProxy的Invoke方法能夠被T的所有方法自動呼叫。
核心攻克點找到了,解決了4,就能一路解決1。怎樣才能讓IClientProxy的Invoke自動的被T的所有方法呼叫呢?AOP可以!可以用Castle對T進行動態織入。到這兒可以動手了,先建立一個Hub擴充套件類:
public static class HubExtensions { static readonly ProxyGenerator generator = new ProxyGenerator(); public static T GetClientBehavior<T>(this IClientProxy clientProxy) where T : class { return (T)generator.CreateInterfaceProxyWithoutTarget<T>(new ClientBehaviorInterceptor(clientProxy)); } }
讓所有的IClientProxy執行GetClientBehavior方法,然後內部進行攔截器裝載,並將IClientProxy塞進攔截器。
public class ClientBehaviorInterceptor:IInterceptor { public ClientBehaviorInterceptor(IClientProxy clientProxy) { this.clientProxy = clientProxy; } IClientProxy clientProxy; public void Intercept(IInvocation invocation) { clientProxy.Invoke(invocation.Method.Name, invocation.Arguments); } }
攔截器中,每當T執行方法的時候,clientProxy就執行Invoke方法,把T的方法名和T的引數傳入,這就達到了原先動態呼叫客戶端方法傳入引數並執行的效果。
然後就是寫一個Hub<T>了。
public abstract class Hub<T> : Hub where T : class { protected T All { get { return (Clients.All as IClientProxy).GetClientBehavior<T>(); } } protected T Any(params string[] connectionIds) { return (Clients.Clients(connectionIds) as IClientProxy).GetClientBehavior<T>(); } protected T Except(params string[] connectionIds) { return (Clients.AllExcept(connectionIds) as IClientProxy).GetClientBehavior<T>(); } protected T Client(string connectionId) { return (Clients.Client(connectionId) as IClientProxy).GetClientBehavior<T>(); } protected T Caller { get { return (Clients.Caller as IClientProxy).GetClientBehavior<T>(); } } }
把Clients中所有的操作都在這兒寫一遍,例子中就寫了5個。通過剛才的擴充套件方法,返回的T已經是經過AOP的了。最後,把最初的ChatHub改一下:
讓ChatHub繼承Hub<T>,T為IChatClient,如圖示,已經可以通過Except方法用強型別呼叫客戶端方法了。執行一下看看:
到此,服務端改造結束。服務端已經可以接受強型別的客戶端行為。
下一篇將對客戶端部分進行強型別改造。
最後附上一個基於SignalR的聊天室玩具,綠色無毒:http://www.royarea.cn/chatroom