<output id="qn6qe"></output>

    1. <output id="qn6qe"><tt id="qn6qe"></tt></output>
    2. <strike id="qn6qe"></strike>

      亚洲 日本 欧洲 欧美 视频,日韩中文字幕有码av,一本一道av中文字幕无码,国产线播放免费人成视频播放,人妻少妇偷人无码视频,日夜啪啪一区二区三区,国产尤物精品自在拍视频首页,久热这里只有精品12
      Fork me on GitHub

      SignalR + KnockoutJS + ASP.NET MVC4 實現井字游戲

      1.1.1 摘要

      今天,我們將使用SignalR + KnockoutJS + ASP.NET MVC實現一個實時HTML5的井字棋游戲。

      首先,網絡游戲平臺一定要讓用戶登陸進來,所以需要一個登陸模塊,然后就是游戲設計并且在游戲過程中保持用戶連接有效性,假設用戶玩著玩著突然掉線這肯定會使用戶很不爽;因此,保持客戶端和服務端通訊的穩定性變得至關重要了,這里我們將使用SignalR和Html5保持通訊實時和穩定。

      近一、兩年來HTML5的發展是沸沸揚揚,在這其中你也許聽過HTML5的規劃給瀏覽器與服務器之間進行全雙工通訊的WebSocket的通訊協定,并提供了的WebSocket API,這一套完整的API設計,在規格的部分,WebSocket的通訊協定已經于2011年被IETF(國際網路工程研究團隊)定為標準的RFC 6455,而的WebSocket API則被W3C定為標準。目前各平臺的瀏覽器的主流版本皆已經支援HTML5的WebSocket / WebSocket API。

      WebSocket / WebSocket API企圖解決開發者長久以來實現服務器推送技術幾乎都依賴于輪詢的方式所造成的明顯缺點,使得服務器接受到太多請求,導致服務器資源過度占用以及帶寬的浪費。

      那么,我們使用WebSocket / WebSocket API就可以確保客戶端和服務器通訊的穩定性,但我們要面對一個事實是不是每個用戶的瀏覽器都支持HTML5,我們必須提高舊的瀏覽器支持方案。

      SignalR的出現讓ASP.NET的開發者得到了救贖,兼容的通訊協議設計將Comet Programming概念和WebSocket技術都放在SignalR整個通訊架構中;SignalR會針對目前執行的瀏覽器進行判斷,找到客戶端(瀏覽器)與服務器最合適的建立鏈接方式。

      SignalR會優先選用WebSocket技術與服務器溝通,開發人員就不需要針對瀏覽器而做出特殊的處理,所有的代碼都通過ASP.NET SignalR高級的API進行信息傳遞。

      signalr0

      圖1 SignalR通訊的鏈接方式

      目錄

      1.1.2 正文

      首先,我們將使用ASP.NET MVC和SignalR實現服務端,客戶端使用KnockoutJS和Html5獲取和綁定數據到頁面,具體設計如下圖:

       signalr5

      圖2 井字棋游戲設計

      我們使用SignalR提供一個簡單的API用于創建服務器端到客戶端的遠程過程調用(RPC),以便從服務器端.NET代碼中調用客戶端瀏覽器(以及其他客戶端平臺)中的JavaScript函數;客戶端瀏覽器也可以通過SigalR來調用服務端.NET代碼。

      ASP.NET MVC4 服務端

      接下來,我們要實現.NET服務器端,由于我們游戲平臺是讓用戶登陸后進行游戲的,所以我們將實現用戶帳戶管理的模塊。

      首先,我們創建一個ASP.NET MVC4 Web Application。

       signalr6

      圖3 ASP.NET MVC4 Web Application

      這里我們選擇Empty模板就可以了。

       signarl7

      圖4 ASP.NET MVC4 Web Application

      然后,我們在項目中使用以下Nuget包:

      • install-package Microsoft.AspNet.SignalR
      • install-package jQuery
      • install-package KnockoutJS

      用戶權限管理

      我們知道ASP.NET MVC自帶的權限表的創建是在InitializeSimpleMembershipAttribute.cs中實現的,所以我們在程序中添加Filters文件夾,然后創建InitializeSimpleMembershipAttribute類,具體定義如下:

      namespace OnlineTicTacTor.Filters
      {
          /// <summary>
          /// Simple Membership initializer.
          /// </summary>
          [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
          public sealed class InitializeSimpleMembershipAttribute : ActionFilterAttribute
          {
              private static SimpleMembershipInitializer _initializer;
              private static object _initializerLock = new object();
              private static bool _isInitialized;
      
              public override void OnActionExecuting(ActionExecutingContext filterContext)
              {
                  // Ensure ASP.NET Simple Membership is initialized only once per app start
      
                  LazyInitializer.EnsureInitialized(ref _initializer, ref _isInitialized, ref _initializerLock);
              }
      
              private class SimpleMembershipInitializer
              {
                  public SimpleMembershipInitializer()
                  {
                      Database.SetInitializer<UsersContext>(null);
      
                      try
                      {
                          using (var context = new UsersContext())
                          {
                              if (!context.Database.Exists())
                              {
                                  // Create the SimpleMembership database without Entity Framework migration schema
                                  ((IObjectContextAdapter)context).ObjectContext.CreateDatabase();
                              }
                          }
      
                          WebSecurity.InitializeDatabaseConnection("DefaultConnection", "UserProfile", "UserId", "UserName", autoCreateTables: true);
                      }
                      catch (Exception ex)
                      {
                          throw new InvalidOperationException("The ASP.NET Simple Membership database could not be initialized.", ex);
                      }
                  }
              }
          } 
      }
      

      上面,我們定義了Web.ConfigInitializeSimpleMembershipAttribute類,接著我們在Web.config中的配置數據庫。

        <connectionStrings>
          <add name="DefaultConnection" connectionString="Data Source=(LocalDb)\v11.0;Initial Catalog=GamesDB;Integrated Security=SSPI;AttachDBFilename=|DataDirectory|\GamesDB.mdf" providerName="System.Data.SqlClient" />
        </connectionStrings>
      

      我們定義了數據庫GamesDB,現在我們運行整個項目,看到localdb中生成了GamesDB數據庫。

        signarl8

      圖5 GamesDB數據庫

      由于,我們使用ASP.NET MVC自帶的權限表來管理用戶賬號,這里會使用到表UserProfile和webpages_Membership,當然,如果有更復雜的權限管理,我們可以使用表webpages_Roles和webpages_UsersInRoles等。

      現在,我們已經創建了數據庫GamesDB,接下來定義對應于數據表的DTO,首先,我們在Models文件夾中創建AccountModels.cs文件,然后定義類LoginModel具體定義如下:

          /// <summary>
          /// The DTO for user account.
          /// </summary>
          public class LoginModel
          {
              [Required]
              [Display(Name = "User name")]
              public string UserName { get; set; }
      
              [Required]
              [DataType(DataType.Password)]
              [Display(Name = "Password")]
              public string Password { get; set; }
      
              [Display(Name = "Remember me?")]
              public bool RememberMe { get; set; }
          }
      

       

      上面,我們定義了數據傳輸類LoginModel,它包含了UserName、Password和RememberMe等信息。

      接下來,我們在Account文件中創建用戶登陸頁面Login.cshtml,由于時間的關系我們已經把頁面設計好了,具體定下如下:

      @model OnlineTicTacTor.Models.LoginModel
      
      @{
          ViewBag.Title = "Login";
      }
      
      
      <section>
          @using (Html.BeginForm(new { ReturnUrl = ViewBag.ReturnUrl }))
          {
              @Html.AntiForgeryToken();
              @Html.ValidationSummary();
              <div class="container">
                  <div class="content">
                      <div class="form-group">
                          @Html.LabelFor(m => m.UserName)
                          @Html.TextBoxFor(m => m.UserName, new { @class = "form-control", @placeholder = "UserName" })
                          @Html.ValidationMessageFor(m => m.UserName)
                      </div>
                      <div class="form-group">
                          @Html.LabelFor(m => m.Password)
                          @Html.PasswordFor(m => m.Password, new { @class = "form-control", @placeholder = "Password" })
                          @Html.ValidationMessageFor(m => m.Password)
                      </div>
                      <div class="checkbox">
                          @Html.CheckBoxFor(m => m.RememberMe)
                          @Html.LabelFor(m => m.RememberMe, new { @class = "checkbox" })
                      </div>
                      <button type="submit" class="btn btn-primary">Login</button>
                  </div>
                  <p>
                      @Html.ActionLink("Register", "Register") if you don't have an account.
                  </p>
              </div>
          }
      </section>

      我們在登陸頁面定義了用戶名、密碼、登陸按鈕和注冊賬號超鏈接等控件。

      signarl10

      圖6 登陸頁面

      接下來,我們同樣在Account文件中創建用戶注冊頁面Register.cshtml,具體定義如下:

      @*The Register view.*@
      @model OnlineTicTacTor.Models.RegisterModel
      @{
          ViewBag.Title = "Register";
      }
      
      @using (Html.BeginForm())
      {
          
       @Html.AntiForgeryToken()
       @Html.ValidationSummary()
      
          <div class="container">
              <div class="content">
                  <div class="form-group">
                      @Html.LabelFor(m => m.UserName)
                      @Html.TextBoxFor(m => m.UserName, new { @class = "form-control", @placeholder = "UserName" })
                  </div>
                  <div class="form-group">
                      @Html.LabelFor(m => m.Password)
                      @Html.PasswordFor(m => m.Password, new { @class = "form-control", @placeholder = "Password" })
                  </div>
                  <div class="form-group">
                      @Html.LabelFor(m => m.ConfirmPassword)
                      @Html.PasswordFor(m => m.ConfirmPassword, new { @class = "form-control", @placeholder = "ConfirmPassword" })
                  </div>
                  <button type="submit" class="btn btn-primary">Register</button>
              </div>
          </div>
      }
      

       signarl11

      圖7 注冊頁面

      現在,我們已經實現了用戶注冊和登陸的頁面了,接下來,我們要把用戶注冊和登陸數據提交到數據庫中。

      我們在Controllers文件夾中創建AccountController類,在其中分別定義注冊和登陸方法,具體定義如下:

      /// <summary>
      /// Logins with LoginModel.
      /// </summary>
      /// <param name="model">The user information.</param>
      /// <param name="returnUrl">The return URL.</param>
      /// <returns></returns>
      [HttpPost]
      [AllowAnonymous]
      [ValidateAntiForgeryToken]
      public ActionResult Login(LoginModel model, string returnUrl)
      {
          if (ModelState.IsValid && WebSecurity.Login(model.UserName, model.Password, persistCookie: model.RememberMe))
          {
              return RedirectToLocal(returnUrl);
          }
      
          // If we got this far, something failed, redisplay form
          ModelState.AddModelError("", "The user name or password provided is incorrect.");
          return View(model);
      }
      
      
      /// <summary>
      /// Registers with LoginModel.
      /// </summary>
      /// <param name="model">The user information.</param>
      /// <returns></returns>
      [HttpPost]
      [AllowAnonymous]
      [ValidateAntiForgeryToken]
      public ActionResult Register(RegisterModel model)
      {
          if (ModelState.IsValid)
          {
              // Attempt to register the user
              try
              {
                  WebSecurity.CreateUserAndAccount(model.UserName, model.Password);
                  WebSecurity.Login(model.UserName, model.Password);
                  return RedirectToAction("Login", "Account");
              }
              catch (MembershipCreateUserException e)
              {
                  ModelState.AddModelError("", ErrorCodeToString(e.StatusCode));
              }
          }
      
          // If we got this far, something failed, redisplay form
          return View(model);
      }

      上面,我們定義了方法Register ()和 Login(),通過Register()方法把用戶信息保存到表UserProfile和webpages_Membership中;Login()方法判斷該用戶信息是否有效。

      現在,我們在Register頁面中輸入賬號名JKhuang和密碼,當我們點擊創建賬號之后,在數據表中查看到已經創建相應的賬號。

       

      signarl12

      signarl13

      圖8 用戶信息表

      游戲對象模型

      現在,我們已經完成了登陸功能了,接下來我們將實現井字游戲功能;首先,在Models文件夾中創建類ConnectionSession和UserCredential。

      /// <summary>
      /// The connection session model
      /// </summary>
      public class ConnectionSession
      {
          public int Id { get; set; }
          public string ConnectionId { get; set; }
          public long ConnectedTime { get; set; }
          public long DisconnectedTime { get; set; }
      }
      
      /// <summary>
      /// The user credential model.
      /// </summary>
      public class UserCredential
      {
          public int Id { get; set; }
          public string UserId { get; set; }
          public ConnectionStatus ConnectionStatus { get; set; }
          // Stores a list of connection session.
          public List<ConnectionSession> Sessions { get; set; }
      
          /// <summary>
          /// Gets the session length in ticks.
          /// </summary>
          /// <returns></returns>
          public long GetSessionLengthInTicks()
          {
              long totalSession = 0;
              foreach (var session in Sessions)
              {
                  if (session.DisconnectedTime != 0)
                  {
                      totalSession += session.DisconnectedTime - session.ConnectedTime;
                  }
                  else
                  {
                      totalSession += DateTime.Now.Ticks - session.ConnectedTime;
                  }
              }
              return totalSession;
          }
      
          /// <summary>
          /// Initializes a new instance of the <see cref="UserCredential"/> class.
          /// </summary>
          public UserCredential()
          {
              Sessions = new List<ConnectionSession>();
          }
      }

      上面,我們定義了ConnectionSession類,它用于存儲SignalR的ConnectionId、連接時間以及斷開連接時間,該對象充當著SignalR和實際的用戶連接之間溝通橋梁。

      UserCredential用來保存所有用戶的會話信息。

      接下來,讓我們定義GameDetails,它用于保存當前游戲的狀態和用戶等信息。

      /// <summary>
      /// The game details model.
      /// </summary>
      public class GameDetails
      {
          public Guid GameId { get; set; }
          public int[,] GameMatrix { get; set; }
          public string NextTurn { get; set; }
          public string Message { get; set; }
          public Status GameStatus { get; set; }
          public UserCredential User1Id { get; set; }
          public UserCredential User2Id { get; set; }
      
          /// <summary>
          /// Initializes a new instance of the <see cref="GameDetails"/> class.
          /// </summary>
          public GameDetails()
          {
              GameMatrix = new int[3,3];
          }
      
          /// <summary>
          /// Checks the game status.
          /// </summary>
          /// <returns></returns>
          private void CheckGameStatus()
          {
              string status = CheckRows();
              if (string.IsNullOrEmpty(status))
              {
                  status = CheckCols();
              }
              if (string.IsNullOrEmpty(status))
              {
                  status = CheckDiagonal();
              }
              Message = !string.IsNullOrEmpty(status) ? status + " wins!" : string.Empty;
              if (string.IsNullOrEmpty(status))
              {
                  status = CheckDraw();
                  Message = status;
              }
          }
      
          /// <summary>
          /// Checks the game is draw or not.
          /// </summary>
          /// <returns></returns>
          private string CheckDraw()
          {
              bool isDefault = false;
              for (int row = 0; row < 3; row++)
              {
                  for (int col = 0; col < 3; col++)
                  {
                      if (GameMatrix[row, col] == default(int))
                      {
                          isDefault = true;
                          GameStatus = Status.Progress;
                          break;
                      }
                  }
                  if (isDefault)
                  {
                      break;
                  }
              }
              if (!isDefault)
              {
                  GameStatus = Status.Draw;
              }
              return isDefault ? "In Progress" : "Game Drawn";
          }
      
          /// <summary>
          /// Sets the player move step.
          /// </summary>
          /// <param name="rowCol">The board cell</param>
          /// <param name="currentPlayerId">The current player identifier.</param>
          /// <returns>The step mark</returns>
          public string SetPlayerMove(dynamic rowCol, string currentPlayerId)
          {
              int x = int.Parse(rowCol.row.ToString());
              int y = int.Parse(rowCol.col.ToString());
              string returnString = string.Empty;
      
              if (!string.IsNullOrEmpty(currentPlayerId) &&
                  GameMatrix[x - 1, y - 1] == default(int))
              {
                  if (currentPlayerId == User1Id.UserId)
                  {
                      returnString = "O";
                      GameMatrix[x - 1, y - 1] = 1;
                      NextTurn = User2Id.UserId;
                  }
                  else
                  {
                      returnString = "X";
                      GameMatrix[x - 1, y - 1] = 10;
                      NextTurn = User1Id.UserId;
                  }
              }
              CheckGameStatus();
              return returnString;
          }
      
          /// <summary>
          /// Checks the game status rows.
          /// </summary>
          /// <returns></returns>
          protected string CheckRows()
          {
              for (int r = 0; r < 3; r++)
              {
                  int value = 0;
                  for (int c = 0; c < 3; c++)
                  {
                      value += GameMatrix[r, c];
                  }
                  if (3 == value)
                  {
                      GameStatus = Status.Result;
                      return User1Id.UserId;
                  }
                  else if (30 == value)
                  {
                      GameStatus = Status.Result;
                      return User2Id.UserId;
                  }
              }
              return string.Empty;
          }
      
          /// <summary>
          /// Checks the game status with cols.
          /// </summary>
          /// <returns></returns>
          protected string CheckCols()
          {
              for (int c = 0; c < 3; c++)
              {
                  int value = 0;
                  for (int r = 0; r < 3; r++)
                  {
                      value += GameMatrix[r, c];
                  }
                  if (3 == value)
                  {
                      GameStatus = Status.Result;
                      return User1Id.UserId;
                  }
                  else if (30 == value)
                  {
                      GameStatus = Status.Result;
                      return User2Id.UserId;
                  }
              }
              return string.Empty;
          }
      
          /// <summary>
          /// Checks the game status in diagonal direction.
          /// </summary>
          /// <returns></returns>
          protected string CheckDiagonal()
          {
              int diagValueF = 0;
              int diagValueB = 0;
              for (int positonF = 0, positonB = 2; positonF < 3; positonF++, positonB--)
              {
                  diagValueF += GameMatrix[positonF, positonF];
                  diagValueB += GameMatrix[positonF, positonB];
              }
              if (diagValueF == 3)
              {
                  GameStatus = Status.Result;
                  return User1Id.UserId;
              }
              else if (diagValueF == 30)
              {
                  GameStatus = Status.Result;
                  return User2Id.UserId;
              }
              if (diagValueB == 3)
              {
                  GameStatus = Status.Result;
                  return User1Id.UserId;
              }
              else if (diagValueB == 30)
              {
                  GameStatus = Status.Result;
                  return User2Id.UserId;
              }
              return string.Empty;
          }
      }
      
      /// <summary>
      /// The game status.
      /// </summary>
      public enum Status
      {
          Progress = 0,
          Result,
          Draw
      }

      上面,我們定義了井字游戲對象類GameDetails,它保存游戲用戶信息,以及包含判斷游戲狀態的邏輯,當游戲開始時Manager會創建一個游戲實例,用來保存游戲雙方和游戲的信息。

      在我們日常的工作中經常需要在應用程序中保持一個唯一的實例,如:IO處理,數據庫操作等,由于這些對象都要占用重要的系統資源,所以我們必須限制這些實例的創建或始終使用一個公用的實例,這就是我們今天要介紹的——單例模式(Singleton),所以我們把Manager定義為單例類,它包含字段_connections和_games用于保存SignalR的鏈接和游戲對象到緩存中,具體定義如下:

          /// <summary>
          ///  A manager of games (actions to create games) 
          /// </summary>
          public class Manager
          {
              // The single object.
              private static readonly Manager _instance = new Manager();
              private Dictionary<string, UserCredential> _connections;
              private Dictionary<Guid, GameDetails> _games;
      
              /// <summary>
              /// Prevents a default instance of the class from being created.
              /// </summary>
              static Manager()
              {
              }
      
              /// <summary>
              /// Prevents a default instance of the <see cref="Manager"/> class from being created.
              /// </summary>
              private Manager()
              {
                  _connections = new Dictionary<string, UserCredential>();
                  _games = new Dictionary<Guid, GameDetails>();
              }
      
              public static Manager Instance
              {
                  get { return _instance; }
              }
      
              /// <summary>
              /// When the challenge started, create a game instance.
              /// </summary>
              /// <param name="gameId">The game identifier.</param>
              /// <returns>a game instance</returns>
              public GameDetails Game(Guid gameId)
              {
                  if (!_games.ContainsKey(gameId))
                  {
                      _games[gameId] = new GameDetails { GameId = gameId };
                  }
                  return _games[gameId];
              }
      
              /// <summary>
              /// Gets all users in the connection.
              /// </summary>
              /// <returns></returns>
              public object AllUsers()
              {
                  var u = _connections.Values.Select(s => new
                  {
                      UserId = s.UserId,
                      ConnectionStatus = (int)s.ConnectionStatus,
                      ConnectionId = s.Sessions[s.Sessions.Count - 1].ConnectionId
                  });
                  return u;
              }
      
              /// <summary>
              /// Creates the new user session.
              /// </summary>
              /// <param name="userId">The user identifier.</param>
              /// <param name="connectionId">The connection identifier.</param>
              private void CreateNewUserSession(string userId, string connectionId)
              {
                  UserCredential curCred = new UserCredential
                      {
                          ConnectionStatus = ConnectionStatus.Connected,
                          UserId = userId
                      };
      
                  curCred.Sessions.Add(new ConnectionSession
                      {
                          ConnectionId = connectionId,
                          ConnectedTime = DateTime.Now.Ticks,
                          DisconnectedTime = 0L
                      });
      
                  _connections.Add(userId, curCred);
      
              }
      
      
              /// <summary>
              /// Updates the user session.
              /// </summary>
              /// <param name="userId">The user identifier.</param>
              /// <param name="connectionId">The connection identifier.</param>
              /// <param name="status">The status.</param>
              private void UpdateUserSession(string userId, string connectionId, ConnectionStatus status)
              {
                  UserCredential curCred = _connections[userId];
                  ExpireSession(curCred);
                  curCred.Sessions.Add(new ConnectionSession
                  {
                      // The connection ID of the calling client.
                      ConnectionId = connectionId,
                      ConnectedTime = DateTime.Now.Ticks,
                      DisconnectedTime = 0L
                  });
                  curCred.ConnectionStatus = status;
              }
      
              /// <summary>
              /// Expires the session.
              /// </summary>
              /// <param name="curCred">The current cred.</param>
              private static void ExpireSession(UserCredential curCred)
              {
                  var curSession = curCred.Sessions.Find
                      (s => s.DisconnectedTime == 0);
      
                  if (curSession != null)
                  {
                      curSession.DisconnectedTime = DateTime.Now.Ticks;
                  }
              }
      
              /// <summary>
              /// Updates the cache.
              /// </summary>
              /// <param name="userId">The user identifier.</param>
              /// <param name="connectionId">The connection identifier.</param>
              /// <param name="status">The status.</param>
              /// <returns></returns>
              internal GameDetails UpdateCache(string userId, string connectionId, ConnectionStatus status)
              {
                  if (!string.IsNullOrWhiteSpace(userId) && _connections.ContainsKey(userId))
                  {
                      UpdateUserSession(userId, connectionId, status);
                  }
                  else
                  {
                      CreateNewUserSession(userId, connectionId);
                  }
      
                  var gd = _games.Values.LastOrDefault<GameDetails>(g => g.User1Id.UserId == userId || g.User2Id.UserId == userId);
                  return gd;
              }
      
              /// <summary>
              /// Disconnects the specified connection identifier.
              /// </summary>
              /// <param name="connectionId">The connection identifier.</param>
              internal void Disconnect(string connectionId)
              {
                  ConnectionSession session = null;
      
                  if (_connections.Values.Count > 0)
                  {
                      foreach (var userCredential in _connections.Values)
                      {
                          session = userCredential.Sessions.Find(s => s.ConnectionId == connectionId);
                          if (session != null)
                          {
                              session.DisconnectedTime = DateTime.Now.Ticks;
                              break;
                          }
                      }
                  }
              }
      
              internal void Logout(string userId)
              {
                  ExpireSession(_connections[userId]);
      
                  // Removes the connection.
                  _connections.Remove(userId);
              }
      
              /// <summary>
              /// News the game.
              /// </summary>
              /// <param name="playerAId">The player a identifier.</param>
              /// <param name="playerBId">The player b identifier.</param>
              /// <returns>The GameDetails object</returns>
              internal GameDetails NewGame(string playerAId, string playerBId)
              {
                  // Gets the playerA user credential.
                  var playerA = _connections.Values.FirstOrDefault<UserCredential>
                      (c => c.Sessions.FirstOrDefault<ConnectionSession>
                          (s => s.ConnectionId == playerAId) != null);
      
                  // Gets the playerB user credential.
                  var playerB = _connections.Values.FirstOrDefault<UserCredential>
                      (c => c.Sessions.FirstOrDefault<ConnectionSession>
                          (s => s.ConnectionId == playerBId) != null);
      
                  // When the game started, created a game instance.
                  var newGame = new GameDetails
                      {
                          GameId = Guid.NewGuid(),
                          User1Id = playerA,
                          User2Id = playerB,
                          NextTurn = playerA.UserId
                      };
      
                  // Stores the game instance into cache.
                  _games[newGame.GameId] = newGame;
                  return newGame;
              }
          }
      

      上面,我們在服務器端中定義了的對象模型和方法,接下來,我們要公開這些方法讓客戶端瀏覽器調用。

      SignalR的Hub模式

      SignalR內部有兩類對象:

      Persistent Connection(HTTP持久鏈接):持久性連接,用來解決長時間連接的能力,而且還可以由客戶端主動向服務器要求數據,而服務器端也不需要實現太多細節,只需要處理PersistentConnection內所提供的五個事件:OnConnected、OnReconnected, OnReceived、OnError和OnDisconnect即可。

      Hub:信息交換器,用來解決realtime信息交換的功能,服務器端可以利用URL來注冊一個或多個Hub,只要連接到這個Hub,就能與所有的客戶端共享發送到服務器上的信息,同時服務器端可以調用客戶端的腳本,不過它背后還是不離HTTP的標準,所以它看起來神奇,但它并沒有那么神奇,只是JavaScript更強,強到可以用像eval()或是動態解釋執行的方式,允許JavaScript能夠動態的加載與執行方法調用而己。

      由于,我們要通過服務端調用客戶端瀏覽器,所以我們使用Hub方式建立服務器和客戶端瀏覽器的鏈接,我們在文件夾SignalrHubs中創建類GameNotificationHub,它繼承了Hub類并且實現方法OnConnected()、OnDisconnected()和OnReconnected(),具體定義如下:

      // specifies the hub name for client to use.
      [HubName("gameNotificationHub")]
      [Authorize]
      public class GameNotificationHub : Hub
      {
          /// <summary>
          /// Challenges the specified connection identifier.
          /// </summary>
          /// <param name="connectionId">The connection identifier.</param>
          /// <param name="userId">The user identifier.</param>
          public void Challenge(string connectionId, string userId)
          {
              // Calls the specified client by connectionId.
              this.Clients.Client(connectionId).getChallengeResponse(Context.ConnectionId, userId);
              // The calling client wait user response.
              this.Clients.Caller.waitForResponse(userId);
          }
      
          /// <summary>
          /// Acceptes the challenge.
          /// </summary>
          /// <param name="connectionId">The connection identifier.</param>
          public void ChallengeAccepted(string connectionId)
          {
              // Creates a game instance.
              var details = Manager.Instance.NewGame(Context.ConnectionId, connectionId);
              // Adds the part a and b in the same group by game id.
              this.Groups.Add(Context.ConnectionId, details.GameId.ToString());
              this.Groups.Add(connectionId, details.GameId.ToString());
              // Starts the game between connection client.
              this.Clients.All.beginGame(details);
          }
      
          /// <summary>
          /// Refuses the challenge.
          /// </summary>
          /// <param name="connectionId">The connection identifier.</param>
          public void ChallengeRefused(string connectionId)
          {
              // Refuses the challenge by connectionId.
              this.Clients.Client(connectionId).challengeRefused();
          }
      
          /// <summary>
          /// Games the move.
          /// </summary>
          /// <param name="gameGuid">The game unique identifier.</param>
          /// <param name="rowCol">The row col.</param>
          public void GameMove(string gameGuid, dynamic rowCol)
          {
              var game = Manager.Instance.Game(new Guid(gameGuid));
              if (game != null)
              {
                  string result = game.SetPlayerMove(rowCol, Context.User.Identity.Name);
                  if (!string.IsNullOrEmpty(result))
                  {
                      // Calls group to draw the user step.
                      this.Clients.Group(game.GameId.ToString()).drawPlay(rowCol, game, result);
                  }
              }
          }
      
          /// <summary>
          /// Creates a connection.
          /// </summary>
          /// <returns>
          /// A <see cref="T:System.Threading.Tasks.Task" />
          /// </returns>
          public override System.Threading.Tasks.Task OnConnected()
          {
              string connectionId = Context.ConnectionId;
              string connectionName = string.Empty;
      
              GameDetails gd = null;
      
              if (Context.User != null && Context.User.Identity.IsAuthenticated)
              {
                  // Retrieves user session in the cache.
                  // If not found, create a new one.
                  gd = Manager.Instance.UpdateCache(
                      Context.User.Identity.Name,
                      Context.ConnectionId,
                      ConnectionStatus.Connected);
                  connectionName = Context.User.Identity.Name;
              }
              if (gd != null && gd.GameStatus == Status.Progress)
              {
                  // Creates a group.
                  this.Groups.Add(connectionId, gd.GameId.ToString());
                  //// No need to update the client by specified id.
                  ////this.Clients.Client(connectionId).rejoinGame(Manager.Instance.AllUsers(), connectionName, gd);
                  this.Clients.Group(gd.GameId.ToString()).rejoinGame(Manager.Instance.AllUsers(), connectionName, gd);
              }
              else
              {
                  // Update the user list in the client.
                  this.Clients.Caller.updateSelf(Manager.Instance.AllUsers(), connectionName);
              }
              this.Clients.Others.joined(
                  new
                      {
                          UserId = connectionName,
                          ConnectionStatus = (int)ConnectionStatus.Connected,
                          ConnectionId = connectionId
                      },
                      DateTime.Now.ToString());
              return base.OnConnected();
          }
      
          public override System.Threading.Tasks.Task OnDisconnected()
          {
              Manager.Instance.Disconnect(Context.ConnectionId);
              return Clients.All.leave(Context.ConnectionId,
                  DateTime.Now.ToString());
          }
      
          public override System.Threading.Tasks.Task OnReconnected()
          {
              string connectionName = string.Empty;
              if (!string.IsNullOrEmpty(Context.User.Identity.Name))
              {
                  Manager.Instance.UpdateCache(
                      Context.User.Identity.Name,
                      Context.ConnectionId,
                      ConnectionStatus.Connected);
                  connectionName = Context.User.Identity.Name;
              }
              return Clients.All.rejoined(connectionName);
          }
      }

      我們看到GameNotificationHub繼承與Hub類,并且我們定義了擴展方法Challenge()、ChallengeAccepted()、ChallengeRefused()和GameMove()。

      • Challenge:客戶端瀏覽器通過調用該方法向其他用戶發出游戲請求。
      • ChallengeAccepted:被請求用戶接受游戲請求并且創建游戲對象。
      • ChallengeRefused:被請求用戶拒絕游戲請求。
      • GameMove:當用戶點擊canvas中的格時,向游戲雙方發送canvas更新操作。

      還有我們給GameNotificationHub類添加了HubName屬性,這樣客戶端瀏覽器只能通過HubName訪問到服務器端的方法;如果沒有指定HubName屬性,那么默認通過類名調用服務器端方法。

      也許有人會問:“我們是怎樣在服務器端(C#)調用Javascript的方法”。

      這是由于在服務器端聲明的所有hub的信息,一般都會生成JavaScript輸出到客戶端,.NET則是依賴Proxy來生成代理對象,這點就和WCF/.NET Remoting十分類似,而Proxy的內部則是將JSON轉換成對象,以讓客戶端可以看到對象。

      Javascript客戶端

      現在,我們已經完成了服務端的功能了,接下來,我們將Knockout JS實現客戶端功能,我們創建tictactor-signalr.js文件,具體定義如下:

      // The game viem model.
      var GameViewModel = function () {
          var self = this;
          
          // The connection user information.
          self.Users = ko.observableArray();
          
          // The user connection.
          self.UserConnections = [];
          
          // Stores the game instances.
          self.Game = {};
          
          // Gets the current user.
          self.CurrentPlayer = ko.observable('Game not started');
          
          // If the game started, Challenge is disabled.
          self.ChallengeDisabled = ko.observable(false);
      };

      上面,我們定義了game的ViewModel類型,并且在其中定義了一系列屬性和方法。

      其中,ChallengeDisabled()方法,判斷游戲是否開始,游戲已經開始了就不再接受其他用戶的游戲請求,反之,還可以接受用戶的游戲請求。

      接下來,我們將實現客戶端Javascript調用服務器端的方法,具體定義如下:

      $(function () {
          
          // Create a game view model.
          var vm = new GameViewModel();
          ko.applyBindings(vm);
         
          var $canvas = document.getElementById('gameCanvas'); //$('gameCanvas')[0];
          
          if ($canvas) {
              var hSpacing = $canvas.width / 3,
                  vSpacing = $canvas.height / 3;
          }
      
          // Declares a proxy to reference the server hub. 
          // The connection name is the same as our declared in server side.
          var hub = $.connection.gameNotificationHub;
      
          // Draws the game with 'X' or 'O'.
          hub.client.drawPlay = function (rowCol, game, letter) {
              vm.Game = game;
              var row = rowCol.row,
                  col = rowCol.col,
                  hCenter = (col - 1) * hSpacing + (hSpacing / 2),
                  vCenter = (row - 1) * vSpacing + (vSpacing / 2);
              writeMessage($canvas, letter, hCenter, vCenter);
              if (game.GameStatus == 0) {
                  vm.CurrentPlayer(game.NextTurn);
              } else {
                  vm.CurrentPlayer(game.Message);
                  alert("Game Over - " + game.Message);
                  location.reload();
              }
          };
      
          // Adds the online user.
          hub.client.joined = function (connection) {
              
              // Remove the connection by userid.
              vm.Users.remove(function(item) {
                  return item.UserId == connection.UserId;
              });
              vm.Users.push(connection);  
          };
      
          // Gets the challenge response.
          hub.client.getChallengeResponse = function (connectionId, userId) {
              
              vm.ChallengeDisabled(true);
              refreshConnections();
              var cnf = confirm('You have been challenged to a game of Tic-Tac-ToR by \'' + userId + '\'. Ok to Accept!');
      
              if (cnf) {
                  hub.server.challengeAccepted(connectionId);
              } else {
                  hub.server.challengeRefused(connectionId);
                  vm.ChallengeDisabled(false);
                  refreshConnections();
              }
      
          };
      
          // Refreshs the user connection.
          function refreshConnections() {
              var oldItems = vm.Users.removeAll();
              vm.Users(oldItems);
          }
      
      
          // Stores all connection into the user list, expect the current login user.
          hub.client.updateSelf = function (connections, connectionName) {
              for (var i = 0; i < connections.length; i++) {
                  if (connections[i].UserId != connectionName) {
                      vm.Users.push(connections[i]);
                  }
              } 
          };
      
          // Handles other client refuses the chanllenge.
          hub.client.challengeRefused = function () {
              vm.ChallengeDisabled(false);
              vm.CurrentPlayer('Challenge not accepted!');
              refreshConnections();
          };
      
          hub.client.waitForResponse = function (userId) {
              vm.ChallengeDisabled(true);
              vm.CurrentPlayer('Waiting for ' + userId + ' to accept challenge');
              refreshConnections();
          };
      
          // Keeps the connection still alive.
          hub.client.rejoinGame = function (connections, connectionName, gameDetails) {
              if (gameDetails != null) {
                  vm.ChallengeDisabled(true);
                  refreshConnections();
                  vm.Game = gameDetails;
                  
                  // Sets the current player.
                  vm.CurrentPlayer(gameDetails.NextTurn);
                  
                  for (var row = 0; row < 3; row++)
                      for (var col = 0; col < 3; col++) {
                          var letter = '';
                          if (gameDetails.GameMatrix[row][col] == 1) {
                              letter = 'O';
                          }
                          else if (gameDetails.GameMatrix[row][col] == 10) {
                              letter = 'X';
                          }
                          if (letter != '') {
                              var hCenter = (col) * hSpacing + (hSpacing / 2);
                              var vCenter = (row) * vSpacing + (vSpacing / 2);
                              writeMessage($canvas, letter, hCenter, vCenter);
                          }
                      }
      
                  vm.Users = ko.observableArray();
                  for (var i = 0; i < connections.length; i++) {
                      if (connections[i].UserId != connectionName) {
                          vm.Users.push(connections[i]);
                      }
                  }
                  vm.Users.remove(function (item) { return item.UserId == gameDetails.User1Id.UserId; });
                  vm.Users.remove(function (item) { return item.UserId == gameDetails.User2Id.UserId; });
                  
              }
          };
      
          // The game begins.
          hub.client.beginGame = function (gameDetails) {
              vm.ChallengeDisabled(true);
              refreshConnections();
              if (gameDetails.User1Id.UserId == clientId ||
                  gameDetails.User2Id.UserId == clientId) {
                  clearCanvas();
                  vm.Game = gameDetails;
                  vm.CurrentPlayer(gameDetails.NextTurn);
              }
              var oldArray = vm.Users;
              vm.Users.remove(function (item) { return item.UserId == gameDetails.User1Id.UserId; });
              vm.Users.remove(function (item) { return item.UserId == gameDetails.User2Id.UserId; });
          };
      
          // Removes the leave user from the user list.
          hub.client.leave = function (connectionId) {
              vm.Users.remove(function (item) { return item.ConnectionId == connectionId; });
          };
      
          $.connection.hub.start().done(function () {
              var canvasContext;
              $('#activeUsersList').delegate('.challenger', 'click', function () {
                  vm.ChallengeDisabled(true);
                  
                  // TODO:
                  var challengeTo = ko.dataFor(this);
                  vm.CurrentPlayer('Waiting for ' + challengeTo.UserId + ' to accept challenge');
                  hub.server.challenge(challengeTo.ConnectionId, clientId);
                  refreshConnections();
              });
      
              if ($canvas && $canvas.getContext) {
                  canvasContext = $canvas.getContext('2d');
                  var rect = $canvas.getBoundingClientRect();
                  $canvas.height = rect.height;
                  $canvas.width = rect.width;
                  hSpacing = $canvas.width / 3;
                  vSpacing = $canvas.height / 3;
      
                  // canvas click event handle.
                  $canvas.addEventListener('click', function (evt) {
                      if (vm.CurrentPlayer() == clientId) {
                          var rowCol = getRowCol(evt);
                          rowCol.Player = 'O';
                          hub.server.gameMove(vm.Game.GameId, rowCol);
                      }
                  }, false);
      
                  drawGrid(canvasContext);
              }
              
              // Gets the user clicks on grid row and column position.
              function getRowCol(evt) {
                  var hSpacing = $canvas.width / 3;
                  var vSpacing = $canvas.height / 3;
                  var mousePos = getMousePos($canvas, evt);
                  return {
                      row: Math.ceil(mousePos.y / vSpacing),
                      col: Math.ceil(mousePos.x / hSpacing)
                  };
              }
      
              // Gets the user mouse click relative poisition in the canvas. 
              function getMousePos($canvas, evt) {
                  var rect = $canvas.getBoundingClientRect();
                  return {
                      x: evt.clientX - rect.left,
                      y: evt.clientY - rect.top
                  };
              }
          });
          
          // When the game end, clear the canvas.
          function clearCanvas() {
              if ($canvas && $canvas.getContext) {
                  var canvasContext = $canvas.getContext('2d');
                  var rect = $canvas.getBoundingClientRect();
                  $canvas.height = rect.height;
                  $canvas.width = rect.width;
      
                  if (canvasContext) {
                      canvasContext.clearRect(rect.left, rect.top, rect.width, rect.height);
                  }
                  drawGrid(canvasContext);
              }
          }
      
          // Draws the grid.
          function drawGrid(canvasContext) {
              var hSpacing = $canvas.width / 3;
              var vSpacing = $canvas.height / 3;
              canvasContext.lineWidth = "2.0";
              for (var i = 1; i < 3; i++) {
                  canvasContext.beginPath();
                  canvasContext.moveTo(0, vSpacing * i);
                  canvasContext.lineTo($canvas.width, vSpacing * i);
                  canvasContext.stroke();
      
                  canvasContext.beginPath();
                  canvasContext.moveTo(hSpacing * i, 0);
                  canvasContext.lineTo(hSpacing * i, $canvas.height);
                  canvasContext.stroke();
              }
          }
      
          // Update the grid with 'X' or 'O'.
          function writeMessage($canvas, message, x, y) {
              var canvasContext = $canvas.getContext('2d');
              canvasContext.font = '40pt Calibri';
              canvasContext.fillStyle = 'red';
              var textSize = canvasContext.measureText(message);
              canvasContext.fillText(message, x - (textSize.width / 2), y + 10);
      
          }
      });
      
      
      

      drawPlay:每當用戶點擊單元格時,繪制相應的標記‘X’或‘O’。

      • joined:獲取在線用戶,保存到User屬性中。
      • getChallengeResponse:獲取用戶是否接受挑戰請求。
      • updateSelf:更新User屬性中的在線用戶。
      • challengeRefused:拒絕游戲請求。
      • waitForResponse:等待用戶回復請求。
      • rejoinGame:保存用戶游戲連接狀態。
      • getRowCol:獲取用戶點擊的單元格位置。
      • getMousePos:獲取用戶鼠標點擊相對于Canvas的坐標。

      這里我們實現了SignalR JS的done()方法,它用戶檢測SignalR JS是否加載完畢這相對于jQuery的$.ready()。

      我們在里面定義了用戶列表和canvas的事件處理方法;當SignalR JS加載完畢后,調用drawGrid()方法繪制井字游戲。

      頁面設計

      現在,我們已經實現了客戶端的通過SignalR和Knockout JS調用服務端的方法,接著我們需要把數據綁定到頁面中,首先我們在View中創建Index.cshtml頁面,具體定義如下:

      <div class="container">
          <div class="content">
              @{
                  if (Request.IsAuthenticated)
                  {
                      <!-- Game board START -->
                      <div class="game-container">
                          <div id="grid" style="height: 400px">
                              <canvas id="gameCanvas" style="width: 100%; height: 100%"></canvas>
                          </div>
                      </div>
                      <!-- Game board END -->
                  
                      <!-- User list START -->
                      <div class="game-player-container">
                          <div class="game-player-header">Online Users</div>
                          <div>
                              <ul id="activeUsersList" class="game-player-list" data-bind="foreach: Users">
                                  <li class="game-list-item">
                                      <div style="height: 30px">
                                          <div style="float: left; padding-top: 5px">
                                              <span data-bind="text: UserId"></span>
                                          </div>
                                          <div class="game-list-item-button">
                                              <div>
                                                  <button data-bind="attr: { disabled: $parent.ChallengeDisabled() }" 
                                                          class="challenger game-list-button">Challenge</button>
                                              </div>
                                          </div>
                                          <input type="hidden" data-bind="value: ConnectionId"/>
                                      </div>
                                  </li>
                              </ul>
                          </div>
                      </div>
                      <!-- User list END -->
                  
                      <div style="width: 100%; text-align: center; font-size: 20px";>
                          Next Turn: <label data-bind="text: CurrentPlayer()"></label>
                      </div>
                  }
                  else
                  {
                              <div class="login-placeholder">
                  <div id="gridNoLogin" style="height: 400px; text-align: center">
                      <h1><a href="@Url.Action("Login", "Account")">Login</a>
                      </h1>
                  </div>
      
              </div>
                  }
              }
          </div>
      </div>

      我們在Index頁面中定義了三個元素,第一個game-container井字游戲的區域,第二個game-player-container當前在線用戶列表,第三個當前游戲操作用戶。

       signarl14

      圖9 井字游戲

      現在,我們已經基本實現了井字游戲了,用戶登陸后就可以查看到當前在線的用戶,然后點擊該用戶就可以發起游戲請求了,如果其他用戶接受游戲請求就可以開始游戲了。

      SignalR代理方法

      前面,我們說到SignalR在服務器端聲明的所有hub的信息,都會一般生成 JavaScript 輸出到客戶端。

      現在,我們查看客戶端Javascript文件發現多了一個文件signalr,并且生存了一個hubs.js文件,它里面包含了對應于服務端的方法,這樣客戶端瀏覽器就可以通過這些Proxy方法調用我們服務器端中的方法了。

      signarl17

      圖10 SignalR代理對象

       

      $.hubConnection.prototype.createHubProxies = function () {
          var proxies = {};
          this.starting(function () {
              // Register the hub proxies as subscribed
              // (instance, shouldSubscribe)
              registerHubProxies(proxies, true);
      
              this._registerSubscribedHubs();
          }).disconnected(function () {
              // Unsubscribe all hub proxies when we "disconnect".  This is to ensure that we do not re-add functional call backs.
              // (instance, shouldSubscribe)
              registerHubProxies(proxies, false);
          });
      
          proxies.gameNotificationHub = this.createHubProxy('gameNotificationHub'); 
          proxies.gameNotificationHub.client = { };
          proxies.gameNotificationHub.server = {
              challenge: function (connectionId, userId) {
                  return proxies.gameNotificationHub.invoke.apply(proxies.gameNotificationHub, $.merge(["Challenge"], $.makeArray(arguments)));
               },
      
              challengeAccepted: function (connectionId) {
                  return proxies.gameNotificationHub.invoke.apply(proxies.gameNotificationHub, $.merge(["ChallengeAccepted"], $.makeArray(arguments)));
               },
      
              challengeRefused: function (connectionId) {
                  return proxies.gameNotificationHub.invoke.apply(proxies.gameNotificationHub, $.merge(["ChallengeRefused"], $.makeArray(arguments)));
               },
      
              gameMove: function (gameGuid, rowCol) {
                  return proxies.gameNotificationHub.invoke.apply(proxies.gameNotificationHub, $.merge(["GameMove"], $.makeArray(arguments)));
               }
          };
      
          return proxies;
      };

      CSS樣式

      我們使用bootstrap樣式,在文件夾Content中添加bootstrap-responsive.css和bootstrap.css文件。

      然后,在BundleConfig中添加css文件引用,具體定義如下:

                  bundles.Add(new StyleBundle("~/Styles/bootstrap/css").Include(
                      "~/Content/bootstrap-responsive.css",
                      "~/Content/bootstrap.css"));
      

       

        signarl15

      signarl16

      圖10 井字游戲

      1.1.3 總結

      本文我們通過實現一個實時的井字游戲,介紹了通過ASP.NET MVC構建服務器端,并且提供游戲接口讓客戶端瀏覽器調用;然后,通過SignalR的Hub方式確保客戶端和服務端鏈接的有效性;最后我們通過KnockoutJS實現頁面的實時更新。

      Demo

      參考

      [1] http://www.rzrgm.cn/shanyou/archive/2012/07/28/2613693.html

      [2] http://www.asp.net/signalr/overview/signalr-20/getting-started-with-signalr-20/introduction-to-signalr

      [3] http://blogs.msdn.com/b/scott_hanselman/archive/2011/11/10/signalr.aspx

      [4] http://www.asp.net/signalr/overview/signalr-20/getting-started-with-signalr-20/tutorial-getting-started-with-signalr-20-and-mvc-5

      [5] http://www.rzrgm.cn/rush/p/3574429.html

      [6] https://github.com/dotnetcurry/signalr-tictactoe-dncmag5

      posted @ 2014-04-23 22:08  JK_Rush  閱讀(8368)  評論(13)    收藏  舉報
      主站蜘蛛池模板: 特黄aaaaaaaaa毛片免费视频| а∨天堂一区中文字幕| 亚洲精品精华液一区二区| 亚洲色一区二区三区四区| 国产一区国产精品自拍| 国产欧美性成人精品午夜| 国产精品一二三区蜜臀av| 蜜臀av久久国产午夜| 国产偷窥熟女高潮精品视频| 亚洲色一区二区三区四区| 强奷漂亮少妇高潮麻豆| 精品一日韩美女性夜视频| 欧美大胆老熟妇乱子伦视频| 男人猛躁进女人免费播放| 国产精品一区二区中文| 国产无遮挡猛进猛出免费软件| 国产清纯在线一区二区| 少妇人妻激情乱人伦| 精品国产一区二区三区av性色| 免费特黄夫妻生活片| 在线人成免费视频69国产| 四川丰满少妇无套内谢| 亚洲中文字幕av天堂| 国产一区二区视频在线看| 国产老熟女国语免费视频| 日韩美女亚洲性一区二区| 好硬好湿好爽好深视频| 亚洲国产成人不卡高清麻豆 | 久久无码中文字幕免费影院蜜桃| 午夜福利日本一区二区无码| 欧美日韩在线视频| 亚洲乱码国产乱码精品精| 久久人与动人物a级毛片| 国产免费午夜福利在线播放| 国产中文字幕日韩精品| 强奷漂亮人妻系列老师| 久播影院无码中文字幕| 国产午夜亚洲精品久久| 午夜综合网| 97免费人妻在线视频| 色噜噜在线视频免费观看|