65.9K
CodeProject 正在变化。 阅读更多。
Home

WebSockets、WCF 和 Silverlight 5

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.79/5 (16投票s)

2011年7月4日

CPOL

5分钟阅读

viewsIcon

95096

downloadIcon

4704

如何实现一个使用(Super)WebSockets 的 Silverlight 应用程序。

引言

在本教程中,我想解释如何创建一个 WebSocket 应用程序。我选择了 Silverlight 作为我的客户端,但您可以使用任何与 JavaScript 交互的框架。

必备组件

  1. Silverlight 5(如我上面提到的,您不必使用 Silverlight,但我选择了最新版本的 Silverlight 作为我的客户端)
  2. SuperWebSocket(您不必下载这个 WebSocket 框架,但值得访问,以查看设计者对用法的设想等)
  3. Visual Studio 2010(您也可以使用 Express 版
  4. VS2010 Silverlight 工具
  5. JMeter(性能测试)

项目结构

WebSocketsSilverlight/ProjectStructure.JPG

  • 在上面的解决方案中,有一个“Client”(Silverlight 5 项目),它提示用户输入一个唯一的商店名称。
  • “Client.Web”项目托管 Silverlight(Client)项目,并在托管的 ASPX 页面中包含 JavaScript 代码(执行 WebSocket 调用)。
  • 一个通用的“SharedClasses”项目,其中一个类在 WCF 服务和 Silverlight “Client”项目之间共享。
  • 一个名为“WcfServicereverse”的 WCF 服务,它将执行一些处理。

运行应用程序

如果您运行应用程序,您将看到一个登录屏幕(如下),您只需输入一个唯一的名称 - 此处不对名称进行验证 - 但这可以轻松实现 - 我们只需要一个唯一的名称,以后可以用来指示谁从客户端将更新推送到 GUI。

WebSocketsSilverlight/LoginScreen.JPG

登录应用程序后,将显示主屏幕。它包含一个网格,其中单元格的前景色根据其单元格值进行转换。网格下方显示了几个仪表,反映了网格本身的值。仪表下方是一个文本框和一个按钮,用于更新“Fan”仪表(将值推送到服务器,然后推送到每个连接的会话)。底部有一个文本块,将显示来自其他商店/用户的所有事务,或由服务器自动生成并推送到所有客户端。

WebSocketsSilverlight/MainScreen.JPG

要更新“Fan”并在所有商店中显示,请输入一个介于 -20 和 20 之间的值,然后单击“Update Fan”(我不会验证用户输入)。这将强制更新服务器,服务器会将更新推送到所有客户端;请参见下文,其中温度已更新为 -10。

WebSocketsSilverlight/UpDatedFan.JPG

如果您打开多个浏览器,此更改也会在这些浏览器中反映出来。

下面,您可以看到在 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 感兴趣的每个事件(CommandHandlerNewSessionConnectedSessionClosed)创建新的处理程序。为了改进这一点,您可以将代码移到一个单独的类中,而不是在 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 都必须(在服务器端)实现某些方法 - 例如 OnMessageOnError 等。上述项目已经实现了这些方法。唯一需要更改的是您将指向的 DLL。

© . All rights reserved.