WebSockets、WCF 和 Silverlight 5






4.79/5 (16投票s)
如何实现一个使用(Super)WebSockets 的 Silverlight 应用程序。
引言
在本教程中,我想解释如何创建一个 WebSocket 应用程序。我选择了 Silverlight 作为我的客户端,但您可以使用任何与 JavaScript 交互的框架。
必备组件
- Silverlight 5(如我上面提到的,您不必使用 Silverlight,但我选择了最新版本的 Silverlight 作为我的客户端)
- SuperWebSocket(您不必下载这个 WebSocket 框架,但值得访问,以查看设计者对用法的设想等)
- Visual Studio 2010(您也可以使用 Express 版)
- VS2010 Silverlight 工具
- JMeter(性能测试)
项目结构
- 在上面的解决方案中,有一个“Client”(Silverlight 5 项目),它提示用户输入一个唯一的商店名称。
- “Client.Web”项目托管 Silverlight(Client)项目,并在托管的 ASPX 页面中包含 JavaScript 代码(执行 WebSocket 调用)。
- 一个通用的“SharedClasses”项目,其中一个类在 WCF 服务和 Silverlight “Client”项目之间共享。
- 一个名为“WcfServicereverse”的 WCF 服务,它将执行一些处理。
运行应用程序
如果您运行应用程序,您将看到一个登录屏幕(如下),您只需输入一个唯一的名称 - 此处不对名称进行验证 - 但这可以轻松实现 - 我们只需要一个唯一的名称,以后可以用来指示谁从客户端将更新推送到 GUI。
登录应用程序后,将显示主屏幕。它包含一个网格,其中单元格的前景色根据其单元格值进行转换。网格下方显示了几个仪表,反映了网格本身的值。仪表下方是一个文本框和一个按钮,用于更新“Fan”仪表(将值推送到服务器,然后推送到每个连接的会话)。底部有一个文本块,将显示来自其他商店/用户的所有事务,或由服务器自动生成并推送到所有客户端。
要更新“Fan”并在所有商店中显示,请输入一个介于 -20 和 20 之间的值,然后单击“Update Fan”(我不会验证用户输入)。这将强制更新服务器,服务器会将更新推送到所有客户端;请参见下文,其中温度已更新为 -10。
如果您打开多个浏览器,此更改也会在这些浏览器中反映出来。
下面,您可以看到在 Visual Studio IDE 中生成并推送到客户端的数据
代码解释
客户端(Silverlight)代码
namespace Client
{
[ScriptableType]
public partial class ClientPage : Page
{
private ObservableCollection<ThermoTemps> thermoCollection;
public ClientPage()
{
InitializeComponent();
ThermoCollection = new ObservableCollection<ThermoTemps>();
this.gridThermo.ItemsSource = ThermoCollection;
HtmlPage.RegisterScriptableObject("myObject", this);
}
private void button1_Click(object sender, RoutedEventArgs e)
{
HtmlPage.Window.Invoke("sendMessage", this.txtFan.Text);
}
[ScriptableMember]
public void UpdateText(string result)
{
try
{
string jsonString = result.Substring(result.IndexOf('{'));
ThermoTemps myDeserializedObj = new ThermoTemps();
DataContractJsonSerializer dataContractJsonSerializer =
new DataContractJsonSerializer(typeof(ThermoTemps));
MemoryStream memoryStream =
new MemoryStream(Encoding.Unicode.GetBytes(jsonString));
myDeserializedObj =
(ThermoTemps)dataContractJsonSerializer.ReadObject(memoryStream);
ThermoCollection.Add(myDeserializedObj);
// set the needle
this.radialBarCoolVent.Value = myDeserializedObj.CoolingVent;
this.radialBarFan.Value = myDeserializedObj.Fan; // set the needle
this.radialBarFreezer.Value = myDeserializedObj.Freezer; // set the needle
this.radialBarFridge.Value = myDeserializedObj.Fridge; // set the needle
this.radialBarIceMaker.Value = myDeserializedObj.IceMaker; // set the needle
}
catch (Exception ex) { }
mytextblock.Text += result + Environment.NewLine;
}
}
}
上面的代码在客户端浏览器中作为 Silverlight 对象运行,但实际上所做的只是在用户单击“Update Fan”按钮时,它会调用 JavaScript 方法 SendMessage
,并将值作为对象传递。UpdateText
方法是可脚本化的,这意味着可以从 JavaScript 代码调用它 - 这是 JSON 字符串被传递并反序列化为共享类 Thermotemps
并添加到绑定到 DataGrid
的可观察集合的地方。
DataGrid
对每个单元格执行转换,以使其文本具有颜色(将单元格值作为参数传递)。
服务器端会话管理
public class Global : System.Web.HttpApplication
{
private List<WebSocketSession> m_Sessions = new List<WebSocketSession>();
private List<WebSocketSession> m_SecureSessions = new List<WebSocketSession>();
private object m_SessionSyncRoot = new object();
private object m_SecureSessionSyncRoot = new object();
private Timer m_SecureSocketPushTimer;
private CommunicationControllerService.WebSocketServiceClient commService;
void Application_Start(object sender, EventArgs e)
{
LogUtil.Setup();
StartSuperWebSocketByConfig();
var ts = new TimeSpan(0, 0, 5);
m_SecureSocketPushTimer = new Timer(OnSecureSocketPushTimerCallback,
new object(), ts, ts); // push sdata from the server every 5 seconds
commService = new CommunicationControllerService.WebSocketServiceClient();
}
void OnSecureSocketPushTimerCallback(object state)
{
lock (m_SessionSyncRoot)
{
ThermoTemps temp = commService.GetTemperatures(null);
System.Web.Script.Serialization.JavaScriptSerializer oSerializer =
new System.Web.Script.Serialization.JavaScriptSerializer();
string sJSON = oSerializer.Serialize(temp);
SendToAll("Computer Update: " + sJSON);
}
}
void StartSuperWebSocketByConfig()
{
var serverConfig =
ConfigurationManager.GetSection("socketServer") as SocketServiceConfig;
if (!SocketServerManager.Initialize(serverConfig))
return;
var socketServer =
SocketServerManager.GetServerByName("SuperWebSocket") as WebSocketServer;
Application["WebSocketPort"] = socketServer.Config.Port;
socketServer.CommandHandler += new CommandHandler<WebSocketSession,
WebSocketCommandInfo>(socketServer_CommandHandler);
socketServer.NewSessionConnected +=
new SessionEventHandler<WebSocketSession>(socketServer_NewSessionConnected);
socketServer.SessionClosed +=
new SessionClosedEventHandler<WebSocketSession>(socketServer_SessionClosed);
if (!SocketServerManager.Start()) SocketServerManager.Stop();
}
void socketServer_NewSessionConnected(WebSocketSession session)
{
lock (m_SessionSyncRoot)
m_Sessions.Add(session);
}
void socketServer_SessionClosed(WebSocketSession session, CloseReason reason)
{
lock (m_SessionSyncRoot)
m_Sessions.Remove(session);
if (reason == CloseReason.ServerShutdown)
return;
}
// sends data
void socketServer_CommandHandler(WebSocketSession session,
WebSocketCommandInfo commandInfo)
{
int? value = (int.Parse(commandInfo.Data.ToString()));
ThermoTemps temp = commService.GetTemperatures(value);
System.Web.Script.Serialization.JavaScriptSerializer oSerializer =
new System.Web.Script.Serialization.JavaScriptSerializer();
string sJSON = oSerializer.Serialize(temp);
SendToAll(session.Cookies["name"] + ": " + sJSON);
}
void SendToAll(string message)
{
lock (m_SessionSyncRoot)
{
foreach (var s in m_Sessions) s.SendResponseAsync(message);
}
}
void Application_End(object sender, EventArgs e)
{
m_SecureSocketPushTimer.Change(Timeout.Infinite, Timeout.Infinite);
m_SecureSocketPushTimer.Dispose();
SocketServerManager.Stop();
}
}
在 Global.axa 中,当每个新会话到来时,您将为 WebSocket
感兴趣的每个事件(CommandHandler
、NewSessionConnected
、SessionClosed
)创建新的处理程序。为了改进这一点,您可以将代码移到一个单独的类中,而不是在 Global.axa 中执行所有操作。在这里,我调用 WCF 服务来执行一些处理,然后将返回结果推送到我们集合中的打开(会话)连接 - socketServer_CommandHandler
方法为我们启动了大部分推送。
JavaScript(在托管页面上)
当主页面加载时,将执行以下 JavaScript 代码,从而创建客户端(Silverlight)和服务器端 WebSocket 之间的连接。onMessage
方法将是这里的主要方法,因为它会将它从服务器接收到的数据推送到 Silverlight C# 方法并更新 GUI。
<script type="text/javascript">
var noSupportMessage = "Your browser cannot support WebSocket!";
var ws;
function connectSocketServer() {
if (!("WebSocket" in window)) {
alert(noSupportMessage);
return;
}
// create a new websocket and connect
ws = new WebSocket('ws://<%= Request.Url.Host %>:' +
'<%= WebSocketPort %>/Sample');
// when data is comming from the server, this metod is called
ws.onmessage = function (evt) {
// call to c# code to populate textblock
var control = document.getElementById("silverlightControl");
control.Content.myObject.UpdateText(evt.data);
};
// when the connection is established, this method is called
ws.onopen = function () {
var control = document.getElementById("silverlightControl");
control.Content.myObject.UpdateText('Connection open');
};
// when the connection is closed, this method is called
ws.onclose = function () {
var control = document.getElementById("silverlightControl");
control.Content.myObject.UpdateText('Connection closed');
}
}
function sendMessage(message) {
if (ws) ws.send(message);
else alert(noSupportMessage);
}
window.onload = function () {
connectSocketServer();
}
</script>
共享类
public class ThermoTemps
{
public int IceMaker { get; set; }
public int Fridge { get; set; }
public int Freezer { get; set; }
public int Fan { get; set; }
public int CoolingVent { get; set; }
}
上面的类由服务和客户端使用 - 将 JSON 字符串序列化为类对象,该对象被添加到绑定到网格的集合中。
服务代码
服务只是执行一些操作/处理来操作数据,然后将数据返回到服务器并发送到客户端。在这里,我们通常会监听数据库更改(SQLNotification
或让数据访问层通知我们更改 - 然后我们会将其转发给相应的客户端)。
public class WebSocketService : IWebSocketService
{
public string ReverseCommunication(string communication)
{
return communication.Aggregate("", (acc, c) => c + acc);
}
public ThermoTemps GetTemperatures(int? fan = null)
{
Random rnd = new Random();
ThermoTemps temperatures = new ThermoTemps();
// determine if fan temp past in
if (fan != null) temperatures.Fan = (int)fan;
else temperatures.Fan = rnd.Next(-20, 20);
temperatures.CoolingVent = rnd.Next(-20, 20);
temperatures.Freezer = rnd.Next(-20, 20);
temperatures.Fridge = rnd.Next(-20, 20);
temperatures.IceMaker = rnd.Next(-20, 20);
return temperatures;
}
}
性能测试
使用 WebSockets 的最终注意事项是多个打开的连接。这在 Java 世界中已经存在很多年了(例如,非阻塞 IO.Jar 或使用 DWR)。但是,在 .NET 世界中,这一点一直落后于其他语言,直到现在。要测试您能否处理 1000 多个打开的连接,请下载 JMeter 并运行一个脚本来打开主页的多个实例(修改您的代码,使其已保存 Cookie 并绕过登录页面)。您将看到它可以处理超过 1000 个连接。
改进
改进主要来自于我们可以对 WebSockets 做的事情。在附加的项目中,我们将相同的数据发送给所有连接的会话。理想情况下,我们希望区分不同的会话。这可以通过拥有一个类集合来实现,其中一个属性是 Web 会话对象,但拥有该类中的其他属性可以让我们确定应该将此会话发送什么等。
未来工作
当 Microsoft 发布其可扩展版本的 Web Sockets 时,您可以轻松修改上述项目 - 如 W3C HTML5 标准中所述,任何形式的 Web Sockets 都必须(在服务器端)实现某些方法 - 例如 OnMessage
、OnError
等。上述项目已经实现了这些方法。唯一需要更改的是您将指向的 DLL。