2013年9月21日 星期六

SignalR訊息推播總整理

即時推播
   在網頁上要做到即時推播在之前大多採用Ajax的手法完成,但此手法需要較多的伺服器資源亦提高系統硬體建置成本,再者,此一做法需要實做較多細節,也因為造成系統開發上需要較高的建置與維護成本。
   SignalR是一個整合式的套件;它整合了伺服器端與客戶端的系統開發,採用瀏覽器作為客戶端並且使用微軟平臺構建的伺服器就可以借助SignalR來達成多個客戶端同步推播訊息,而這個推播頻道可不受限制的進行單個無狀態請求/回應的資料交換直到明確關閉。
   較特別的是,客戶端除了可以一次傳送多筆訊息給伺服器端之外,亦可發送非同步訊息給伺服器端。

Http 1.1的特性
   Http 1.1具有持續性連接的特性,亦稱作Http Keep-alive或Http connection reuse。是使用同一個TCP連接來發送/接收多個Http的請求/回應,而不是為每一個新的請求/回應就開一條連線。

   採用持續性連接的處理方式可以具有幾個優點:
   1. 因需要啟用的連線較少,相對來說較省資源。
   2. 請求/回應可以達成Http管線化(i.e. 批次處理)
   3. 因連線數少,網路阻塞狀況較少。
   4. 客戶端不需頻繁與伺服器端進行連線建立的交握。
   5. 回報錯誤無需關閉TCP連線。
   以目前較大的頻寬來說,持續性連接並不一定具有優勢;因為,當傳送完需求後連線仍會持續保持連線一段時間,而這段時間卻沒有做任何事。為此,瀏覽器會有一套演算法進行連線的管理。

SignalR四種資料交換方式
   1. WebSocket:
       WebSocket由W3C制定的HTML5標準的API,亦為一種資料通訊協定。其運作方式為瀏覽器開啟一個Socket作為伺服器端與客戶端的雙向通訊頻道,這個頻道會持續存在直到兩方中其中一方主動關閉,Socket此時才會隨之關閉。由於Html5標準當前尚未正式定案,支援的瀏覽器也不多,故,較少採用這種方式。
var ws = new WebSocket("ws://localhost:9999/socket" );
ws.onopen = function () {
    ws.send("Hello");
};

ws.onmessage = function (evt) {
   var recMsg = evt.data;
};

ws.onclose = function () {
   //關閉
};

2. Long Polling
      在Html5定案之前,要做到即時訊息推播以前都是採用這種做法來達成,而這種做法能夠支援較多瀏覽器版本,其內部資料傳輸方式為: 瀏覽器載入網頁之後,先送出一個Request並且為保持連線會在固定時間內送出Request以保持連線,並且等待伺服器傳送訊息;當客戶端要發送一筆訊息給伺服器端時,則會另開一條Http連線傳送資料。這種做法的好處是可以適用多個甚至是舊的瀏覽器版本,但最大的缺點就是所耗費的伺服器資源是最高的。

   3. Server-Send Events
       此一方法亦為Html5所制定的標準API,但目前仍處於草案階段,所以市面上的瀏覽器較少支援此一API。而這個API的內部實做較類似Long Polling,但是它僅接收伺服器端所傳送過來的資料並非是雙向與伺服器端溝通。
var source = new EventSource("/getEvents");
source.onmessage = function (event) {
   //DoSomething
};

4. Forever Frame
      利用iframe標籤指向一個Url,這個網頁要一直處於傳送狀態,接收來自伺服器端的訊息並更新本頁上的Html內容;由於一直連接著,所以可以將最新資料不斷推播到客戶端,缺點是長時間使用後會花掉太多記憶體空間。

SignalR的兩大類別
   SinalR在客戶端與伺服器端之間的資料交換是採用Json格式,而依照需求的不同
SignalR提供了兩種類別:

   1. Persistent Connection: 用於持續式連線,解決長時間連線的需求。客戶端允許主動向伺服器要求資料,而伺服器端在實做上亦不需實做太多細節部份,僅需處理五個事件:
       (1) OnConnected
       (2) OnReconnected
       (3) OnReceived
       (4) OnError
       (5) OnDisconnect

   2. Hub: 訊息交換機,用於解決即時的多客戶端彼此訊息交換。伺服器可以利用Url註冊一或多個Hub,只要連接到其中一個Hub就能與連接到該Hub的所有客戶端交換訊息,除此之外,伺服器端還可以呼叫到客戶端的Js,但其背後仍是以Http作為協定。
SignalR-PersistentConnection實做
步驟1. 安裝必要Nuget套件:
            (1) jQuery
            (2) JSON-js json2
            (3) Microsoft ASP.Net SignalR

步驟2. 撰寫資料傳輸類別(DTO)
public class ChatData
{
   public string Name { get; set; }
   public string Message { get; set; }
   public ChatData()
   {
   }

   public ChatData( string name, string message)
    {
        Name = name;
        Message = message;
     }
}

步驟3. 撰寫PersistentConnection類別
public class ChatConnection : PersistentConnection
{
   private static readonly Dictionary< string, string> _clients = new Dictionary<string , string >();

   protected override Task OnConnected( IRequest request, string connectionId)
   {
       _clients.Add(connectionId, string.Empty);
       ChatData chatData = new ChatData( "Server", "A new user has joined the room.");
       return Connection.Broadcast(chatData);
    }

   protected override Task OnReceived( IRequest request, string connectionId, string data)
   {
       ChatData chatData = JsonConvert.DeserializeObject< ChatData>(data);
       _clients[connectionId] = chatData.Name;
       return Connection.Broadcast(chatData);
   }

   protected override Task OnDisconnected( IRequest request, string connectionId)
   {
       string name = _clients[connectionId];
       ChatData chatData = new ChatData( "Server", string .Format("{0} has left the room.", name));
       _clients.Remove(connectionId);
       return Connection.Broadcast(chatData);
    }
 }

步驟4. 添加Action
public ActionResult ChatR()
{
   var vm = new ChatData();
   return View(vm);
}

步驟5. 添加View
@{
    ViewBag.Title = "ChatR";
}

@using (Html.BeginForm())
{
   @ Html.EditorForModel();
   <input id ="send" value ="send" type ="button" />
   <ul id ="messages" style ="list-style: none;"></ul >
}

@section scripts{
   <script src ="~/Scripts/json2.js"></script>
   <script src ="~/Scripts/jquery.signalR-1.1.3.js"></ script>
   <script type="text/javascript">

   $(document).ready(function () {
      var con = $.connection( "/chat");
      on.received(function (data) {
         $( "#messages").append("<li>" + data.Name + ': ' + data.Message + "</li>");
      });

      con.start()
            .promise()
            .done(function () {
                  $( "#send").click(function () {
                      var myName = $( "#Name").val();
                      var myMessage = $( "#Message").val();
                       con.send(JSON.stringify({ name: myName, message: myMessage }));
    })
  });
}

步驟6. 添加路由規則(BundleConfig.cs)
public class RouteConfig
{
   public static void RegisterRoutes( RouteCollection routes)
   {
      RouteTable.Routes.MapConnection< ChatConnection>("chat" , "/chat" );
          routes.IgnoreRoute( "{resource}.axd/{*pathInfo}");
          routes.MapRoute(
              name: "Default",
              url: "{controller}/{action}/{id}",
               defaults: new { controller = "Home", action = "ChatR" , id = UrlParameter.Optional }
          );
    }
}

(實做畫面)
Image(1)

圖1. PersistentConnection實做結果
SignalR-Hub實做
步驟1. 安裝必要Nuget套件:
            (1) jQuery
            (2) JSON-js json2
            (3) Microsoft ASP.Net SignalR
步驟2. 撰寫Hub類別
[HubName("chatHub")]
public class ChatHub : Hub
{
   private static Dictionary< string, string> dicClients = new Dictionary<string , string >();
   //使用者連現時呼叫
   public void userConnected( string name)
   {
      //進行編碼,防止XSS攻擊
      name = HttpUtility.HtmlEncode(name);
      string message = "歡迎使用者 " + name + " 加入聊天室" ;
      //發送訊息給除了自己的其他
      Clients.Others.addList(Context.ConnectionId, name);
      Clients.Others.hello(message);
      //發送訊息至自己,並且取得上線清單
      Clients.Caller.getList(dicClients.Select(p => new { id = p.Key, name = p.Value }).ToList());
      //新增目前使用者至上線清單
      dicClients.Add(Context.ConnectionId, name);
     }

   public void SendMessageToAll( string msg)
   {
      string strMsg = Encoder.HtmlEncode(msg);
      string strName = dicClients.Where(c => c.Key == Context.ConnectionId).FirstOrDefault().Value;
      strMsg = string.Concat(strName, "說: ", strMsg);
      Clients.All.sendMessageToAll(strMsg);
   }

   public void SendMessageToSomeone( string to, string msg)
   {
       msg = Encoder.HtmlEncode(msg);
       string strFrom = dicClients.Where(c => c.Key == Context.ConnectionId).FirstOrDefault().Value;
       msg = string.Format( @"{0} <span style='color:red'>悄悄對你說:</span>: {1}" , strFrom, msg);
       Clients.Client(to).sendMessageToSomeone(msg);
    }

    public override Task OnDisconnected()
    {
        Clients.All.removeList(Context.ConnectionId);
        dicClients.Remove(Context.ConnectionId);
        return base.OnDisconnected();
     }
 }

步驟3. 添加Action
public ActionResult ChatR()
{
   return View();
}

步驟4. 添加CSS
<style>
#userName{
   display: none;
   color: red;
}

#messageBox, #chatList{
   float: left;
   height: 300px;
   width: 300px;
   overflow: auto;
}

#messageBox{
   border: 1px solid #000;
}

#chatList{
   width: 150px;
   overflow: scroll;
}

#list li{cursor: pointer;}

#bar{clear: both;}

p{margin: 0;}

</style>

步驟5. 添加View
<p id="userName"> Hi! </ p>
<div id="messageBox">
   <p >聊天室內容 </p>
   <ul id ="messageList"></ul>
</div>

<div id="chatList">
   <p >上線清單 </p>
   <ul id ="list">
   </ul >
</div>

<div id="bar">
   <select id ="box">
      <option value="all"> 所有人</option >
   </select >
   <input type ="text" id ="message" />
   <input type ="button" id ="send" value ="發送" />
</div>

@section scripts{
   <script src ="~/Scripts/json2.js"></script>
   <script src ="~/Scripts/jquery.signalR-1.1.3.js"></ script>
   <script type ="text/javascript"></script>
   <script src ="/signalr/hubs"></script>
   <script src ="~/Scripts/Self/InitialSignalR.js"></ script>
}

步驟5. 添加路由規則(BundleConfig.cs)
public class RouteConfig
{
   public static void RegisterRoutes( RouteCollection routes)
   {
      RouteTable.Routes.MapConnection< ChatConnection>("chat" , "/chat" );
          routes.IgnoreRoute( "{resource}.axd/{*pathInfo}");
          routes.MapRoute(
             name: "Default",
             url: "{controller}/{action}/{id}",
             defaults: new { controller = "Home", action = "ChatR" , id = UrlParameter.Optional }
       );
    }
}

步驟6. 註冊Hub(Global.asax.cs)
protected void Application_Start()
{
   RouteTable.Routes.MapHubs();
    .....
}

步驟7. 撰寫Js
var userID = "" ;

$(function () {
   while (userID.length == 0) {
      userID = window.prompt( "請輸入使用者名稱" );
      if (!userID)
         userID = "";
      }

     $("#userName").append(userID).show();

     //建立與Server端的Hub的物件,注意Hub的開頭字母一定要為小寫
     var chat = $.connection.chatHub;

     //取得所有上線清單
    chat.client.getList = function (userList) {
       var li = "";
       $.each(userList, function (index, data) {
           li += "<li id='" + data.id + "'>" + data.name + "</li>";
       });
       $( "#list").html(li);
    }

   //新增一筆上線人員
    chat.client.addList = function (id, name) {
       var li = "<li id='" + id + "'>" + name + "</li>" ;
       $( "#list").append(li);
    }

   //移除一筆上線人員
    chat.client.removeList = function (id) {
       $( "#" + id).remove();
    }

   //全體聊天
    chat.client.sendMessageToAll = function (message) {
       $( "#messageList").append("<li>" + message + "</li>");
    }

   //密語聊天
    chat.client.SendMessageToSomeone = function (message) {
       $( "#messageList").append("<li>" + message + "</li>");
    }

    chat.client.hello = function (message) {
        $( "#messageList").append("<li>" + message + "</li");
    }

    //將連線打開
    $.connection.hub.start().done( function () {
       //當連線完成後,呼叫Server端的userConnected方法,並傳送使用者姓名給Server
       chat.server.userConnected(userID);
    });

    $("#send").click( function () {
      var to = $( "#box").val();
      //當to為all代表全體聊天,否則為私密聊天
      if (to == "all") {
          chat.server.sendMessageToAll($( "#message").val());
      } else {
          chat.server.sendMessage(to, $( "#message").val());
      }
      $( "#message").val('' );
   });

    $("#list").on( "click","li" , function () {
      var $this = $( this);
      var id = $this.attr( "id");
      var text = $this.text();

      //防止重複加入密語清單
     if ($( "#box").has("." + id).length > 0)
        return false;
     var option = "<option></option>"
        $( "#box").append(option).find("option:last" ).val(id).text(text).attr({ "selected": "selected" }).addClass(id);
    });
});


(成果畫面)
Image(2)

圖2. Hub實做結果
 
WebConfig的修改

   Hub在實做上較特別的部份在於<script src ="/signalr/hubs"></script>這個Js;該Js檔是由伺服器端動態產生,但實際佈署到伺服器上後會發生錯誤,主要是因為IIS誤任這個Request是要指向某一個Controller的Action,為避免此一錯誤,需在WebConfig中確認下面這一段:

<system.webServer>
  <validation validateIntegratedModeConfiguration="false" />
  <modules runAllManagedModulesForAllRequests="true" />
  ...
</system.webServer>


參考項目
1. http://www.dotblogs.com.tw/jasonyah/archive/2013/05/30/chatroom-with-signalr-realtime-web-application.aspx


沒有留言:

張貼留言