使用MSMQ/Duplex WCF/SignalR/jQuery将实时结果流媒体到一个网站
Table目录 介绍视频如何运行演示的先决条件一般设计 测试发布者双工WCF服务 接受订阅服务器,删除订阅并回调 Web UI SignalR Hub Javascript Hub通信订阅WCF服务响应回调自动取消订阅谷歌地球集成 就是这样 介绍 设置场景……我工作的地方是一家外汇公司 为世界各地的客户进行外汇交易,有一天我的老板过来跟我说 他希望能够实时地看到交易在哪里发生,但是他 希望它看起来很酷,一种闪亮的展示类型的东西(我相信你 知道我的意思)。他明确表示没有网格。我很高兴。 我的队友Richard和我被分配了这个任务,所以我们考虑了一下,看看是什么 我们的资料,并想知道我们是否可以 一些通用的实时事件观察者,也会产生一些 闪亮的界面供我们炫耀。 我们没有足够的信息,我们几乎只有 后 例如,Tcpip处理一个描述实时事件类型的任意字符串 “ClientDeal”、“ExchangeFund”等客户名称 所以我们想了想,最后我们得出了一个结论 沿着这个场景的思路。 我们可以扩展日志框架(我们使用 Log4Net), 我们可以创建一个自定义MessageQueue (MSMQ) appender,它将记录日志 某些事件和一些额外的数据(例如Tcpip地址)用于隐藏。复制CodeMessageQueue 。 显然,我们不能共享整个应用程序,因此我们使用提供者a 测试消息发布程序,它简单地将测试消息写入一个隐藏文件中。复制CodeMessageQueue 。 这部分应该很容易算出来 用自己的东西来生成实时事件。我们可以让WCF服务实际读取这些MessageQueue条目 时间。此服务可以接受订阅者,每个订阅者都可以 使用事件名称订阅单个事件,或通过订阅多个事件 通过在订阅中传递一个事件名称数组,我们可以让WCF服务使用回调来推送订阅者 我们还可以使用谷歌Earth来显示这些事件(如果我们可以获得的话) 为事件的地理位置数据)在实时 这大部分都是相当标准的东西,真正有趣的部分是推动 实时地从服务器端web代码返回到浏览器的通知。我 不知道你们有多少人见过这个,它就像谷歌 当你打开一个谷歌搜索页面,搜索一些相当的东西 populaar(说一些有新闻价值的项目)和谷歌将实际直播结果 直接进入你打开的搜索页面。 它非常酷,通常是通过长轮询或其他方式完成的 使用Ajax/Comet技术的其他技术,所有这些技术都很难设置 起来,开始工作吧(至少在我们看来)。 我们最终使用了一个名为 SignalR, 我要说的是相当酷。 我们会讲所有不同的移动部分 关于这一点,在文章后面。有一件事要记住,对于我们的 我们想要的要求 在谷歌地球上展示东西, 你可能不想这么做。但是 SignalR可能 应用于任何想要将实时数据直接传递给用户的场景 浏览器,搜索结果之类的东西,某种流媒体变化的数据, 比如实时市场汇率,或者奇怪的外汇汇率。有趣的。 演示视频 这个演示视频显示了web项目实时接收来自 测试出版项目。正在发生的事情的完整路径是这样的: Msmq→WCF→Xml解析→地理位置查找→WCF调→网站 →SignalR→Javascript→谷歌地球API 需要注意的一件事是,测试发布者是从一个小的随机选择 一组已知的TcpIp地址,因此您可能会看到相同的TcpIp(因此) (地理定位)互相挑选。这就是a的随机性 小的数据集。 不管怎样,点击下面的图片下载这个视频(它大约180Mb抱歉,屏幕截图软件会产生大文件),应该很清楚了 发生了什么,测试发布者项目向其发布事件 一个消息和这些消息被推到谷歌地球 实时(流到它)通过WCF和使用 SignalR 图书馆。 如何运行演示 演示代码y包含一些必须按特定顺序运行的项目,因此下面是运行时需要执行的操作 演示代码正确: 运行Codeproject.EventBroker.TestMessagePublisher 项目,并选择自动模式,运行Codeproject.EventBroker。主机项目(做它在 调试,因为它将是自托管的控制台WCF应用程序,然后)运行Codeproject.EventBroker。请稍等 而你应该看到谷歌地球 航行到世界的不同地方 先决条件 有几个先决条件,但是大多数都包括在as中 附加的演示代码的一部分。下表显示了所有这些内容并告诉您 它们是否包含在附带的演示代码中,或者你是否真的 必须 有它们来运行代码吗 你必须有一个名为“eventbroker”的队列(或在TestMessagePublisher和WebUI项目配置文件中配置的任何队列)IIS Express 7.5NO 你必须从微软下载页面安装:http://www.microsoft.com/download/en/details.aspx?id=1038Castle WindsorYESSee Lib\Castle\1.2.0.6623 folderLog4NetYESSee Lib\Log4Net\1.2.10.0\ Log4Net微软dllSignalRYESSee Lib \ \ SignalR \ SignalR.dll 总体设计 我认为最好的开始方式是考虑下面的图表 试图概述所附的演示代码的不同部分: 每个黑色的方框表示演示中的一个项目 项目,而橙色框表示公开的功能 通过使用 SignalR dll。 我们将进入这些项目和使用 SignalR更多 细节在下面,但现在这里是一个非常简短的描述,每个这些 项目。 Codeproject.EventBroker。TestMessagePublisher:这是废弃代码。这个项目的唯一任务是 模拟实时消息的生成。服务:这是一个双工WCF 从隐藏的消息中读取消息的服务。复制CodeTestMessagePublisher 是写作。要运行此服务,您将需要 启动WCF主机项目Codeproject.EventBroker。主机Codeproject.EventBroker。WebUI:这是stndard ASP .NET 项目的主机实例的谷歌地球 在网页中。web页面还调用服务器端 SignalR代码 然后订阅WCF服务并接受回调 哪些提供实时值,然后显示在谷歌地球上的实时 使用 SignalR。 测试的出版商 正如我们已经说过的,隐藏复制Code
Codeproject.EventBroker.TestMessagePublisher
project是一个扔掉 用于模拟发生的实时消息的代码。 当您运行这个项目时,它看起来像这样。 可以看到有两个单选按钮和一个开始按钮。两个收音机 按钮用于确定如何将测试消息写入隐藏。Code
MessageQueue
副本。 自动:点击后每隔x秒就会创建一条新消息 启动手册:只有在单击启动时才会创建一个新消息 下面是对MessageQueue进行写入的相关代码 隐藏,收缩,复制Code
using System; using System.Collections.Generic; using System.ComponentModel; using System.Configuration; using System.Data; using System.Diagnostics; using System.Drawing; using System.Linq; using System.Messaging; using System.Text; using System.Threading; using System.Windows.Forms; using System.Threading.Tasks; using Message = System.Windows.Forms.Message; namespace Codeproject.EventBroker.TestMessagePublisher { public partial class MainWindow : Form { private enum RunMode { Automatic = 1, Manual } private RunMode CurrentRunMode = RunMode.Automatic; private string inputQueueName = ConfigurationManager.AppSettings["eventBrokerQueueName"]; private List<string> places = new List<string>(); private List<int> waits = new List<int>(); private Random rand = new Random(); private bool listenToSelectionChanges = true; private bool stopAuto = false; public MainWindow() { InitializeComponent(); places.Add("220.233.19.142"); places.Add("64.233.160.0"); places.Add("91.135.229.5"); waits.Add(1000); waits.Add(2000); waits.Add(4000); waits.Add(5000); waits.Add(8000); } public void SendMessages() { Task.Factory.StartNew(() => { while (!stopAuto) { SendMessage(); Thread.Sleep(10000); } }, TaskCreationOptions.LongRunning); } public string GetXmlData(string tcpIpAddress) { return string.Format( "<realtimeEvent>" + "<originatingIp>{0}</originatingIp>" + "<eventName>ClientDealEvent</eventName>" + "<entityIdType>ClientDeal</entityIdType>" + "<description>Someone bought something</description>" + "<date>{1}</date>" + "<additionalData></additionalData>" + "</realtimeEvent>", tcpIpAddress, DateTime.Now); } private void btnCreateManual_Click(object sender, EventArgs e) { if (radAuto.Checked) { SendMessages(); } else { SendMessage(); } } private void SendMessage() { using (MessageQueue queue = new MessageQueue(inputQueueName, QueueAccessMode.Send)) { queue.Formatter = new XmlMessageFormatter(new[] { typeof(string) }); try { System.Messaging.Message message = new System.Messaging.Message( GetXmlData(places[rand.Next(places.Count)])); Debug.WriteLine("Producing message {0}", message.Body.ToString()); queue.Send(message); } catch (MessageQueueException mex) { if (mex.MessageQueueErrorCode != MessageQueueErrorCode.IOTimeout) { Debug.WriteLine("Message queue exception occured", mex); } } catch (Exception ex) { // Write the message details to the Error queue Debug.WriteLine("Exception occured", ex); } } } private void CheckedChanged(object sender, EventArgs e) { if (radAuto.Checked) { stopAuto = false; } else { stopAuto = true; } } } }
可以看到,我们使用的消息结构是少量的XML 就像这样 隐藏,复制Code
<realtimeEvent> <originatingIp>192.168.0.1</originatingIp> <eventName>ClientDealEvent</eventName> <entityIdType>ClientDeal</entityIdType> <description>Someone bought something</description> <date>02/01/2012</date> <additionalData></additionalData> </realtimeEvent>
配置要使用的MessageQueue队列是在 隐藏的App.Config复制Code
Codeproject.EventBroker.TestMessagePublisher
project 隐藏,复制Code
<?xmlversion="1.0"?> <configuration> <appSettings> <addkey="eventBrokerQueueName"value="FormatName:Direct=OS:localhost\private$\eventbroker"/> </appSettings> <startup> <supportedRuntimeversion="v4.0"sku=".NETFramework,Version=v4.0"/> </startup> </configuration>
可以看到,默认的队列名称是“eventbroker”,它应该是本地机器上的一个私有队列。 但这个队列可以在任何地方,这里只是告诉你可以在哪里 配置。 另一个重要的注意事项是“eventbroker”MessageQueue 队列不能是事务性的。正如演示代码所假设的那样 不是事务性的,如果您想使队列成为事务性的,您将需要这样做 改变Codeproject.EventBroker。TestMessagePublisher MessageQueue 编写代码和Codeproject.EventBroker。服务MessageQueue 阅读代码。 如果你想让它变成事务性的,那很好,但你必须改变 这样的代码。还要注意,您将必须授予访问权限 新创建的“eventbroker”MessageQueue队列的用户。我 倾向于用我自己的登录和授予所有权利。 双WCF服务 Codeproject.EventBroker。服务是一个双工的WCF服务 基本上有以下客户可以调用的合同 隐藏,复制Code
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.ServiceModel; using Codeproject.EventBroker.Contracts.Faults; namespace Codeproject.EventBroker.Contracts.Service { [ServiceContract(Namespace = "http://Codeproject.EventBroker.Contracts", SessionMode=SessionMode.Required, CallbackContract=typeof(IEventBrokerCallback))] public interface IEventBroker { [OperationContract(IsOneWay = false)] [FaultContract(typeof(EventBrokerException))] void Subscribe(Guid subscriptionId, string[] eventNames); [OperationContract(IsOneWay = true)] void EndSubscription(Guid subscriptionId); } }
此服务还期望客户端提供一个满足要求的回调契约 这个接口 隐藏,复制Code
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.ServiceModel; using Codeproject.EventBroker.Contracts.Data; namespace Codeproject.EventBroker.Contracts.Service { public interface IEventBrokerCallback { [OperationContract(IsOneWay = true)] void ReceiveStreamingResult(RealTimeEventMessage streamingResult); } }
该服务托管在Codeproject.EventBroker中。主持的项目, 在释放模式下运行时,它将是隐藏的Windows服务主机。复制code
Codeproject.EventBroker.Contracts.Service.EventBroker,
,当运行在调试模式将是一个简单的控制台应用程序,主机 鳕鱼eproject.EventBroker.Contracts.Service。EventBroker WCF服务。 藏在哪里复制Code
Codeproject.EventBroker.Contracts.Service.EventBroker
serviceskeleton实现如下所示 隐藏,收缩,复制Code
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.ServiceModel; using Codeproject.EventBroker.Contracts.Service; using Codeproject.EventBroker.Service.Data; using System.Threading.Tasks; using System.Configuration; using Codeproject.EventBroker.Contracts.Faults; using System.Messaging; using Codeproject.EventBroker.Common; using Codeproject.EventBroker.Service.Utils; using Codeproject.EventBroker.Contracts.Data; using Codeproject.EventBroker.Service.Extensions; using Codeproject.EventBroker.Service.Services.Contracts; using System.Threading; namespace Codeproject.EventBroker.Service { [ServiceBehavior( InstanceContextMode = InstanceContextMode.Single, ConcurrencyMode = ConcurrencyMode.Multiple)] public class EventBroker : IEventBroker { .... .... .... public EventBroker() { inputQueueName = ConfigurationManager.AppSettings["eventBrokerQueueName"].ToString(); StartCollectingMessage(); xmlParser = IOCManager.Instance.Container.Resolve<IXmlParser>(); } public void StartCollectingMessage() { .... .... .... } public void Subscribe(Guid subscriptionId, string[] eventNames) { .... .... .... } public void EndSubscription(Guid subscriptionId) { .... .... .... } } }
接受用户 当新的订阅者订阅此WCF时,将发生以下情况 对于订阅者希望订阅的每个事件名称,执行 后: 如果当前没有该事件名称的订阅, 创建空的订阅列表,查看是否已经有该事件名称的订阅列表 有添加订阅者Id和回调上下文(IEventBrokerCallback) 到事件名称的订阅者的全局字典 这里是最相关的代码发生订阅: 隐藏,复制Code
private void CreateSubscription(Guid subscriptionId, string[] eventNames) { //Ensure that a subscription is created for each message type the subscriber wants to receive lock (syncObj) { foreach (string eventName in eventNames) { if (!eventNameToCallbackLookups.ContainsKey(eventName)) { List<UniqueCallbackHandle> currentCallbacks = new List<UniqueCallbackHandle>(); eventNameToCallbackLookups[eventName] = currentCallbacks; } eventNameToCallbackLookups[eventName].Add( new UniqueCallbackHandle(subscriptionId, OperationContext.Current.GetCallbackChannel<IEventBrokerCallback>())); } } }
取消订阅 一旦sunscriber选择结束他们的订阅,他们可以使用无效的EndSubscription(Guid subscriptionId)操作合同。 当订阅服务器结束订阅时,将发生以下情况 对于事件名称的全局订阅服务器字典中的每个事件名称 的subscriptionId不相同的所有子语句 正在取消订阅的订阅者创建这些订阅的订阅者的新全局字典 在删除所有不再需要到期的订阅后,还会保留 发送到结束订阅的订阅服务器 这里是最相关的代码时,EndSubscription发生: 隐藏,复制Code
public void EndSubscription(Guid subscriptionId) { lock (syncObj) { //create new dictionary that will be populated by those remaining Dictionary<string, List<UniqueCallbackHandle>> remainingEventNameToCallbackLookups = new Dictionary<string, List<UniqueCallbackHandle>>(); foreach (KeyValuePair<string,List<UniqueCallbackHandle>> kvp in eventNameToCallbackLookups) { //get all the remaining subscribers whos session id is not the same as the one we wish to remove List<UniqueCallbackHandle> remainingMessageSubscriptions = kvp.Value.Where(x => x.CallbackSessionId != subscriptionId).ToList(); if (remainingMessageSubscriptions.Any()) { remainingEventNameToCallbackLookups.Add(kvp.Key, remainingMessageSubscriptions); } } //now left with only the subscribers that are subscribed eventNameToCallbackLookups = remainingEventNameToCallbackLookups; } }
做回调 对象的实际回调是WCF服务中有趣的部分 订阅者。这个回调应该什么时候发生? 对订阅者的回调应该只在我们有东西时发生 传送给他们,也就是当我们从隐藏处收到信息时。复制code
MessageQueue
,它匹配一个订户订阅要求(基本上 传入消息事件名称匹配订阅服务器隐藏复制他们订阅时使用的code
EventName
。 由于这个WCF服务打算由许多客户端使用,所以有几个客户端 线程运行时,有主WCF线程,也有一个新线程 加速处理从MessageQueue读取和发送的操作 返回给订阅者的消息应该是传入事件的事件名 有活跃的用户。 这个过程可以在以下两种WCF方法中看到 读取传入的MessageQueue消息 隐藏,收缩,复制Code
private void GetMessageFromQueue() { try { Task messageQueueReaderTask = Task.Factory.StartNew(() => { using (MessageQueue queue = new MessageQueue(inputQueueName, QueueAccessMode.Receive)) { queue.Formatter = new XmlMessageFormatter(new[] { typeof(string) }); while (shouldRun) { Message message = null; try { if (!queue.IsEmpty()) { LogManager.Log.Debug("Receiving queue message"); message = queue.Receive(queueReadTimeOut); ProcessMessage(message); } } catch (MessageQueueException e) { if (e.MessageQueueErrorCode != MessageQueueErrorCode.IOTimeout) { LogManager.Log.Warn("Message queue exception occured", e); } } catch (Exception e) { // Write the message details to the Error queue LogManager.Log.Warn("Exception occured", e); } } } }, TaskCreationOptions.LongRunning); } catch (AggregateException ex) { throw; } }
回调订阅者吗 这段代码所做的另一件聪明的事情是,当我们试图获取CommunicationObjectAbortedException时 向订阅服务器发送消息,则假定该订阅服务器出错并被删除。您将看到订阅者也有机制 用于处理出现故障的通道,这在发布/订阅时就不那么容易了。例如,一个订户可能出现故障,但所有订户都可能出现故障 其他的可能没问题,所以我们应该重新启动ServiceHost,但可能不行。这就是我们在这里用到的评价法 我们尽量做到容错,只有在通道完全出错时才重新启动ServiceHost。 隐藏,收缩,复制Code
private void ProcessMessage(Message msmqMessage) { string messageBody = (string)msmqMessage.Body; LogManager.Log.DebugFormat("ProcessMessage : {0}", messageBody); RealTimeEventMessage messageToSendToSubscribers = xmlParser.ParseRawMsmqXml(messageBody); if (messageToSendToSubscribers != null) { lock (syncObj) { List<Guid> deadSubscribers = new List<Guid>(); if (eventNameToCallbackLookups.ContainsKey(messageToSendToSubscribers.EventName)) { List<UniqueCallbackHandle> uniqueCallbackHandles = eventNameToCallbackLookups[messageToSendToSubscribers.EventName]; foreach (UniqueCallbackHandle uniqueCallbackHandle in uniqueCallbackHandles) { try { uniqueCallbackHandle.Callback.ReceiveStreamingResult(messageToSendToSubscribers); } catch(CommunicationObjectAbortedException coaex) { deadSubscribers.Add(uniqueCallbackHandle.CallbackSessionId); } } } //end all subcriptions for dead subscribers foreach (Guid deadSubscriberId in deadSubscribers) { EndSubscription(deadSubscriberId); } } } }
可以看出,处理传入隐藏的代码。复制Code
MessageQueue
message解析xml传输消息体到一个实际隐藏在将Code
RealTimeEventMessage
object发送给订阅者之前复制它。这个xml解析代码是 如下所示 隐藏,收缩,复制Code
public class XmlParser : IXmlParser { private IGeoLocator geoLocator; public XmlParser(IGeoLocator geoLocator) { this.geoLocator = geoLocator; } public RealTimeEventMessage ParseRawMsmqXml(string messageBody) { //<realtimeEvent> // <originatingIp></originatingIp> // <eventName>ClientDealEvent</eventName> // <entityIdType>ClientDeal</entityIdType> // <description>Someone bought something</description> // <date>2012-01-16T15:31:31</date> // <additionalData></additionalData> //</realtimeEvent> try { RealTimeEventMessage info = new RealTimeEventMessage(); XElement xelement = XElement.Parse(messageBody); string ipAddress = GetSafeString(xelement, "originatingIp"); if (!string.IsNullOrEmpty(ipAddress)) { info.Location = geoLocator.ObtainLocationForIPAddress(ipAddress); } info.EventName = GetSafeString(xelement, "eventName"); info.EntityIdType = GetSafeString(xelement, "entityIdType"); info.Description = GetSafeString(xelement, "description").Replace("\n\n", "\n
"); info.Date = GetSafeDate(xelement, "date"); info.AdditionalData = GetSafeString(xelement, "additionalData"); return info; } catch (Exception ex) { LogManager.Log.Error(ex); return null; } } public static Int32 GetSafeInt32(XElement root, string elementName) { try { XElement element = root.Elements().Where(node => node.Name.LocalName == elementName).Single(); return Convert.ToInt32(element.Value); } catch { return 0; } } private static DateTime? GetSafeDate(XElement root, string elementName) { try { XElement element = root.Elements().Where(node => node.Name.LocalName == elementName).Single(); return DateTime.Parse(element.Value); } catch { return null; } } public static String GetSafeString(XElement root, string elementName) { try { XElement element = root.Elements().Where(node => node.Name.LocalName == elementName).Single(); return element.Value; } catch { return String.Empty; } } }
xml解析代码还使用了另一个实用程序代码 从一个TcpIp地址获取地理位置信息。这项服务是免费的,但是 偶尔会错过一些TcpIp地址。在工作中,我们实际上使用web服务 由MindMap提供,非常可靠,查找1万次需要20美元 是一个简单的GET http请求。但是在本文中,我们有演示代码 提供给你的免费版本有点不可靠,抱歉。 也就是说TestMessageQueuePublisher总是随机选择 我们知道的TcpIp地址与免费的地理位置查找服务一起工作 这个演示代码使用的,你应该没问题。 不管怎样,这里是免费的(但有点不可靠)地理位置查找代码: 隐藏,收缩,复制Code
public class GeoLocator : IGeoLocator { public GeoLocation ObtainLocationForIPAddress(string ipAddress) { try { WebClient client = new WebClient(); string locationDump = client.DownloadString( string.Format("http://api.hostip.info/get_html.php?ip={0}&position=true", ipAddress)); string[] locationDumpSplit = locationDump.Split( new string[] { @"
\n", @"\n" }, StringSplitOptions.RemoveEmptyEntries); decimal latitude = -1; decimal longitude = -1; int found=0; using (StringReader sr = new StringReader(locationDump)) { found = 0; while (sr.Peek() >= 0) { string line = sr.ReadLine().ToLower(); if (line.StartsWith("latitude:")) { line = line.Replace("latitude:","").Trim(); latitude = decimal.Parse(line); found++; } if (line.StartsWith("longitude:")) { line = line.Replace("longitude:", "").Trim(); longitude = decimal.Parse(line); found++; } } } if (found == 2) { return new GeoLocation(latitude, longitude); } else return null; } catch (Exception ex) { LogManager.Log.ErrorFormat( "Could not obtain Latitude/Longitude data for IpAddress {0}
\n Exception : {1}", ipAddress, ex); return null; } } }
Web UI 这个谜题的最后一点是一个用于显示的简单web站点 实时的(或者接近该死的,有一个轻微的延迟延迟通过 不同的层,事实上这些是实时事件所经过的层, 这样您就可以看到网站适合它的位置 Msmq→WCF→Xml解析→地理位置查找→WCF调→网站 →SignalR→Javascript→谷歌地球API 好一条路不! 无论如何,说网站是相当简单的唯一有点异国情调的东西 它使用了一个相当新的免费库 SignalR这 我们将在下面更详细地讨论。在西文这个网站做的是托管 这是谷歌地球的一个例子 一个标准的HTML页面中的插件,它被jQuery操作了一点 Javascript。它还利用 SignalR图书馆 允许推送通知到浏览器。 SignalR中心 本质上, SignalR是, 一个用于ASP的异步信令库。NET来帮助构建实时、多用户的 交互式web应用程序。它用一种非常聪明的方式做到了这一点。它 基本上允许您编写服务器端代码继承从 SignalR中心 对象。然后还可以创建Javascript来与 服务器端 Hub对象,反之亦然。 没错,我们可以写Javascript 方法通过服务器端代码,这实际上是相当疯狂的。 当然有一些魔法,但一旦你弄明白发生了什么,它是 不是很神奇,而是很聪明。下面是隐藏的情况 SignalR将 创建一个轻量级的Javascript代理,它允许Javascript与 服务器端代码信号将 还要在您的中心创建动态的“客户端”和“调用者”对象,以便您 可以通过代码直接调用用Javascript编写的客户端方法吗 在服务器端,SignalR将 检查您的浏览器代理功能,并执行以下操作之一 第一次尝试使用WebSockets会允许Javascript吗 SignalR代理 当WebSockets失效时,与服务器端代码进行对话 SignalR将 恢复使用长轮询来允许Javascript SignalR代理 与服务器端代码对话 所有这些都很隐蔽,有点神奇。 有一个很好的 SignalRa 快速入门 在Hub快速入门示例中,我们建议您在继续之前阅读该示例。 一旦你阅读了它,你就会理解下面的代码片段。 对于演示项目,我们有以下内容 SignalR中心 隐藏,收缩,复制Code
using System; using System.Collections.Generic; using System.Linq; using System.Web; using SignalR.Hubs; namespace Codeproject.EventBroker.WebUI.GeoLocation { [HubName("eventTicker")] public class EventTickerHub : Hub { private int counter; private readonly EventTicker eventTicker; public EventTickerHub() : this(EventTicker.Instance) { } public EventTickerHub(EventTicker eventTicker) { this.eventTicker = eventTicker; eventTicker.Subscribe(); } public void Register() { //Do nothing, but is crucially important to establish comms } } }
在自定义 SignalR中心 还使用了EventTicker对象。我们会看到更多 对象之后。现在,您需要知道的就是创建自定义 SignalR中心 额外的材料 Scott Hanselman有一个很棒的博客 SignalR在这 链接,值得一读: http://www.hanselman.com/blog/AsynchronousScalableWebApplicationsWithRealtimePersistentLongrunningConnectionsWithSignalR.aspx 代码项目自己的Anoop Madhusudanan也刚刚击败了我们 编写第1篇codeproject文章 SignalR这 也值得一读: http://www.codeproject.com/Articles/322154/ASP-NET-MVC-SIngalR-and-Knockout-based-Real-time-U Javascript中心通讯 JavaScript与定制通信 SignalR中心 剩下的魔法发生在哪里,但在我们看之前,让我们看看什么 你需要做一个托管页面,HTML页面在我们的情况下(可以 ASP / ASPX / CSHTML等等) 可以看到,我们有以下脚本标签 SignalR启用 页面 隐藏,复制Code
<!DOCTYPEhtmlPUBLIC"-//W3C//DTDXHTML1.0Transitional//EN""http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <htmlxmlns="http://www.w3.org/1999/xhtml"> <head> <scriptsrc="../Scripts/jquery-1.6.4.js"></script> <scriptsrc="../Scripts/jquery.signalR.js"></script> <scripttype="text/javascript"src="https://www.google.com/jsapi?key=ABCDEFG"> </script> <scriptsrc="../Scripts/jquery.color.js"type="text/javascript"></script> <scriptsrc="../signalr/hubs"></script> <scriptsrc="GeoLocationView.js"></script> </head> <body> <divid="map3d"style="height: 100%; width: 100%;"> </div> </body> </html>
现在,如果您查看演示项目文件夹,您将看不到signalr/hub文件夹。这就是魔法,还有你 必须接受这一点 并且知道 SignalR将 把东西。一定程度的信任是必需的。 因此,一旦你接受了编码中存在独角兽/小妖精和精灵的说法,我们就可以做到 现在我们来看看实际情况如何让JavaScript和a对话 SignalR中心。在 如果你检查文件“GeoLocationView”演示代码。你将看到以下内容 负责初始化通信的一段JavaScript代码 与 SignalR中心。 隐藏,收缩,复制Code
var eventHub = $.connection.eventTicker; //************************************************* // Initialise SignalR Hub //************************************************* function InitialiseSignalRHub() { eventHub = $.connection.eventTicker; // Declare a function on the eventHub so the server can invoke it eventHub.addMessage = function (message) { ProcessGeoLocationCallbackMessage(message); } //callback that does nothing, simply here to establish link to Hub eventHub.registerCallback = function () { }; // Start the connection $.connection.hub.start(); //wait for 3s before we register with Hub window.setTimeout(function () { eventHub.register(); }, 3000) }
对象对JavaScript还执行了一个回调 信号集线器,我们一会就会看到。 订阅WCF服务 订阅双工WCF服务是一个相当标准的事情,我们所需要的 to是这样的 隐藏,收缩,复制Code
using System; using System.Collections.Generic; using System.Linq; using System.Web; using SignalR.Hubs; using System.Timers; using Codeproject.EventBroker.Contracts.Data; using Codeproject.EventBroker.Contracts.Service; using System.ServiceModel; using Codeproject.EventBroker.WebUI.Wcf; using System.Threading; using System.ServiceModel.Channels; namespace Codeproject.EventBroker.WebUI.GeoLocation { public class EventTicker : IEventBrokerCallback { private InstanceContext instanceContext; private Guid subscriptionId; EventBrokerProxy proxy; public EventTicker() { instanceContext = new InstanceContext(this); CreateProxy(); } public void CreateProxy() { proxy = new EventBrokerProxy(instanceContext); } public void Subscribe() { ThreadPool.QueueUserWorkItem(x => { try { subscriptionId = Guid.NewGuid(); proxy.Subscribe(subscriptionId, new string[] { "ClientDealEvent", "PaymentOutEvent" }); isSubscribed = true; } catch { } }); } } }
其中唯一重要的部分是: 我们使用一个新的InstanceContext来提供WCF 带有回调对象上下文的服务。我们必须使用一个新的线程来进行订阅。 这是非常重要的,因为我们需要保留ASP .NET 工作线程空闲,否则回调将无法工作。 如果您感到好奇,这里是网站代码用于的代理代码 与双工WCF服务通信 隐藏,收缩,复制Code
using System; using System.Collections.Generic; using System.Linq; using System.Web; namespace Codeproject.EventBroker.WebUI.Wcf { public partial class EventBrokerProxy : System.ServiceModel.DuplexClientBase<Codeproject.EventBroker.Contracts.Service.IEventBroker>, Codeproject.EventBroker.Contracts.Service.IEventBroker { public EventBrokerProxy(System.ServiceModel.InstanceContext callbackInstance) : base(callbackInstance) { } public EventBrokerProxy(System.ServiceModel.InstanceContext callbackInstance, string endpointConfigurationName) : base(callbackInstance, endpointConfigurationName) { } public EventBrokerProxy(System.ServiceModel.InstanceContext callbackInstance, string endpointConfigurationName, string remoteAddress) : base(callbackInstance, endpointConfigurationName, remoteAddress) { } public EventBrokerProxy(System.ServiceModel.InstanceContext callbackInstance, string endpointConfigurationName, System.ServiceModel.EndpointAddress remoteAddress) : base(callbackInstance, endpointConfigurationName, remoteAddress) { } public EventBrokerProxy(System.ServiceModel.InstanceContext callbackInstance, System.ServiceModel.Channels.Binding binding, System.ServiceModel.EndpointAddress remoteAddress) : base(callbackInstance, binding, remoteAddress) { } public void Subscribe(Guid subscriptionId, string[] eventNames) { base.Channel.Subscribe(subscriptionId, eventNames); } public void EndSubscription(Guid subscriptionId) { base.Channel.EndSubscription(subscriptionId); } } }
响应回调 在我们看来,这是解决方案中真正有趣的部分 发生在双工WCF服务调用EventTicker时 使用sunscriber提供的InstanceContext,是由 使用 SignalR中心我们 能够直接调用到Javascript方法吗 下面是相关的Codeproject.EventBroker。WebUI服务器端 代码(参见EventTicker),这是什么双工WCF回调 属性提供的初始InstanceContext调用 用户: 隐藏,复制Code
public void ReceiveStreamingResult(RealTimeEventMessage streamingResult) { if (streamingResult.Location != null) { Hub.GetClients<EventTickerHub>().addMessage(streamingResult); } }
只是作为一个提醒,这里是什么订户callback界面看起来 如: 隐藏,复制Code
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.ServiceModel; using Codeproject.EventBroker.Contracts.Data; namespace Codeproject.EventBroker.Contracts.Service { public interface IEventBrokerCallback { [OperationContract(IsOneWay = true)] void ReceiveStreamingResult(RealTimeEventMessage streamingResult); } }
这是相关的Codeproject.EventBroker。WebUI客户 JavaScript代码: 隐藏,收缩,复制Code
//************************************************* // Initialise SignalR Hub //************************************************* function InitialiseSignalRHub() { .... .... .... // Declare a function on the eventHub so the server can invoke it eventHub.addMessage = function (message) { ProcessGeoLocationCallbackMessage(message); } .... .... .... } //************************************************* // Process SignalR Hub callback //************************************************* function ProcessGeoLocationCallbackMessage(message) { //now add the items to the earth ShowPosition(message.Location.Latitude, message.Location.Longitude); CreateMarker(message.Location.Latitude, message.Location.Longitude, message.Description); }
请特别注意JavaScript函数名,并查看服务器如何 在信号中心旁边 代码可以直接调用它,然后传递给。net对象 客户端将JavaScript作为JSON接收,我们认为这是相当理智的。 相当精神,向信号致敬 家伙/ chapesses。 正如我们之前所说的 SignalR是 足够聪明,可以检测浏览器的性能,并尝试以下操作 首先尝试使用WebSockets(如果它们是受支持的),如果不受支持,就使用长轮询 自动取消订阅 do发布/订阅的一个问题是,通道可能出现a错误 特定订户和该订户及其回调的通道是 这几乎没什么用,但用户无法知道。那么我们如何 战斗。 如果我们上网看看。配置Codeproject.EventBroker.WebUI 您将看到以下WCF配置 隐藏,收缩,复制Code
<system.serviceModel> <client> <endpointname="Codeproject.EventBroker.Service.EventBroker"address="net.tcp://localhost:63747/EventBroker"binding="netTcpBinding"bindingConfiguration="DuplexBinding"contract="Codeproject.EventBroker.Contracts.Service.IEventBroker"/> </client> <bindings> <netTcpBinding> <bindingname="DuplexBinding"sendTimeout="00:00:10"receiveTimeout="00:00:10"> <reliableSessionenabled="true"/> <securitymode="None"/> </binding> </netTcpBinding> </bindings> </system.serviceModel>
我们看到两个超时值Send和Receive,它们都被设置为10 分钟。因此我们采用的方法是,从 WCF绑定,并启动一个定时器,当定时器到期时我们做一个自动 取消对WCF双工服务的订阅,然后再次订阅。用这个 方法是在接收超时时只释放最大的一组数据 如果订阅者通道出现故障,则为。 EventTicker最相关的代码如下所示。 隐藏,收缩,复制Code
using System; using System.Collections.Generic; using System.Linq; using System.Web; using SignalR.Hubs; using System.Timers; using Codeproject.EventBroker.Contracts.Data; using Codeproject.EventBroker.Contracts.Service; using System.ServiceModel; using Codeproject.EventBroker.WebUI.Wcf; using System.Threading; using System.ServiceModel.Channels; namespace Codeproject.EventBroker.WebUI.GeoLocation { public class EventTicker : IEventBrokerCallback { private InstanceContext instanceContext; private Guid subscriptionId; private bool isSubscribed; private TimeSpan receiveTimeout; private System.Timers.Timer subscriberLeaseRenewalTimer; EventBrokerProxy proxy; public EventTicker() { instanceContext = new InstanceContext(this); CreateProxy(); Binding binding = proxy.Endpoint.Binding; receiveTimeout = binding.ReceiveTimeout; subscriberLeaseRenewalTimer = new System.Timers.Timer(receiveTimeout.TotalMilliseconds); subscriberLeaseRenewalTimer.Enabled = true; subscriberLeaseRenewalTimer.Start(); subscriberLeaseRenewalTimer.Elapsed += SubscriberLeaseRenewalTimer_Elapsed; } private void SubscriberLeaseRenewalTimer_Elapsed(object sender, ElapsedEventArgs e) { subscriberLeaseRenewalTimer.Enabled = false; subscriberLeaseRenewalTimer.Stop(); EndSubscription(); CreateProxy(); Subscribe(); subscriberLeaseRenewalTimer.Enabled = true; subscriberLeaseRenewalTimer.Start(); } public void CreateProxy() { proxy = new EventBrokerProxy(instanceContext); } public void Subscribe() { .... .... .... .... } public void EndSubscription() { .... .... .... .... } } }
谷歌地球的集成 谷歌地球 积分是很标准的东西你可以通过阅读 各种各样的谷歌地球 文档/ API参考页。但是为了完整性,这里是什么 代码类似于谷歌Earth 集成。 就像我们说的,这都是非常标准的东西你使用谷歌地球 API,本质上我们所做的就是在隐藏中使用以下代码复制Code
Codeproject.EventBroker.WebUI
项目GeoLocationView.js 文件。 初始化谷歌地球 创建谷歌地球 插件导航控制导航谷歌地球 插件到一个特定的纬度/经度(由回调提供) 到 SignalR中心通过 订阅者WCF回调上下文)显示了一个谷歌地球 属性提供的当前纬度/经度的placemark插件 回调到 SignalR中心通过 订阅者WCF回调上下文) 隐藏,收缩,复制Code
//************************************************ // Global Vars //************************************************ google.load('earth', '1'); var ge = null; var placemark; //************************************************ // Document Ready //************************************************ $(function () { GlobalInit(); }); //************************************************* // Global initialisation, hooks up Google Earth // callback //************************************************* function GlobalInit() { google.setOnLoadCallback(EarthInit); } //************************************************* // Initialise Googe Earth //************************************************* function EarthInit() { google.earth.createInstance( 'map3d', function InitialisationPassed(instance) { ge = instance; console.log("InitialisationPassed " + ge); InitialiseSignalRHub(); ge.getWindow().setVisibility(true); CreateNavigationControl(); }, function InitialisationFailed(errorCode) { console.log("InitialisationFailed " + errorCode); alert("there was an error initialising Google Earth
\n" + errorCode); }); } //************************************************* // Creates Google Earth Navigation Control //************************************************* function CreateNavigationControl() { console.log("CreateNavigationControl " + ge); var geNavigationControl = ge.getNavigationControl(); geNavigationControl.setVisibility(true); geNavigationControl.setStreetViewEnabled(true); } //************************************************* // Navigates Google Earth To Particular Lat/Long //************************************************* function ShowPosition(lat, long) { // Get the current view var lookAt = ge.getView().copyAsLookAt(ge.ALTITUDE_RELATIVE_TO_GROUND); lookAt.setRange(lookAt.getRange() * 0.25); // Set new latitude and longitude values lookAt.setLatitude(lat); lookAt.setLongitude(long); // Update the view in Google Earth ge.getView().setAbstractView(lookAt); // Get the current view var camera = ge.getView().copyAsCamera(ge.ALTITUDE_RELATIVE_TO_GROUND); // Zoom out to twice the current distance camera.setAltitude(5000000); camera.setLatitude(lat); camera.setLongitude(long); // Update the view in Google Earth ge.getView().setAbstractView(camera); } //************************************************* // Creates Google Earth Marker for Lat/Long // with description //************************************************* function CreateMarker(lat, long, desc) { //remove last placemark, only want to show 1 at a time if (placemark != undefined) { ge.getFeatures().removeChild(placemark); } placemark = ge.createPlacemark(''); placemark.setName(desc); placemark.setDescription("Some cool stuff right here"); // Define a custom icon. var icon = ge.createIcon(''); var imageUrl = window.location.protocol + "//" + window.location.host + "/content/Images/person.png"; console.log(imageUrl); icon.setHref(imageUrl); var style = ge.createStyle(''); //create a new style style.getIconStyle().setIcon(icon); //apply the icon to the style style.getIconStyle().setScale(1.5); style.getLabelStyle().setScale(2.0); placemark.setStyleSelector(style); //apply the style to the placemark // Set the placemark's location. var point = ge.createPoint(''); point.setLatitude(lat); point.setLongitude(long); placemark.setGeometry(point); // Add the placemark to Earth. ge.getFeatures().appendChild(placemark); }
就是这样 无论如何,这就是我们这次真正想说的。Richard和我可能会在某个阶段为你准备更多的东西,但我现在就要准备了 回到最后的5%这个OSS项目,皮特奥汉隆和我正在工作 上。最后的5%是最难的部分,但我们都很喜欢 期待它很快出现在这里。已经来了好一阵子了,但是我们没有 他们都喜欢它,也觉得它会有用。所以在那之前…Dum Dum Dum。 然而,如果你喜欢这篇文章,可以不厌其烦地给予评论/投票 他们总是受到赞赏。感谢你的阅读。恭喜恭喜 本文转载于:http://www.diyabc.com/frontweb/news17323.html