標籤:[1] file pack text csdn 系統訊息 個數 訊息 eof
怎麼辦,這很尷尬,為啥呢,因為kbe的某些原因讓我放棄了使用它所以本打算繼續更新的,說一下原因:
在DAY1中我希望kbe能夠開啟一個http服務,並讓php端做一個web請求將訊息傳遞給對應的使用者,可是這個http服務我是寫起來了,發送訊息的函數也寫出來(花了不少時間,kbe的注釋和文檔都不多,特別是kbe把BaseHttpServer這個python庫另外弄了個名字,用http.server import as 才匯入成功)尷尬的就是http服務和發訊息的函數怎麼也放不到一起:
1.一旦某個class不繼承自KBEngine.Base,那麼他就無法訪問KBEngine的幾乎所有靜態函數、屬性,就無法擷取到對應使用者的mailbox完成訊息發送
2.一旦繼承KBEgine.Base,你就做不了HTTP 服務,因為你的handler必須繼承baseHandler,你繼承不了,且即使你繼承到baseHandler去訪問KBEgine.Base的mailbox之類的又回到剛的死邏輯之中
3.系統庫的HTTP服務會阻塞進程,這個文檔還是寫了,不過替代架構太麻煩,且調試太不方便,且文法太熟悉,且…………雖然我想說一萬個且,只能說明我無能啊…………
當然論壇和官網當中也有人反應類似的問題,例如第三方介面訪問KBE的成員/屬性問題,不過看起來好像並沒有現成的解決方案,最後的最後。。。我放棄了
然後呢~~我自己老老實實寫了一個Message Service器(基於socket ,with WPF .net 4.5+)以及訊息協議
訊息協議採用http://msgpack.org/ 基本上支援所有的語言,因此實際上我這個Message Service器可以服務任何類型的用戶端,不管你啥平台啥語言
1.0版協議(還沒名字呢)規定:
1.BasePack代表發送的包,BaseAckPack代表回執包,BaseAckPack繼承自BasePack
2.每個Pack長度為1024位元組,且第0~4位元組轉換成int代表pack類型, BasePack及其子類從1.2.3...10... BaseAckPack 及其子類從1001,1002,1003...1010...(有考慮負數,其實應該也ok)
為啥這樣做? 這裡很奇特,你在把這1024個位元組用msgpack轉成對象之前,你並不知道這個pack是哪個對象,你不能統一按某一個特定的對象去轉,比如LoginPack比BasePack只多了2個屬性,你在不知道它是一個LoginPack還是一個BasePack之前,你無法拆開他,你按任何一種來拆開都有可能出錯(屬性多了或少了,熟悉iOS 的KVC的應該很清楚),所以必須先把前面4個位元組騰出來,可選的,第5~8個位元組放長度(mespack可以長度大於內容拆開沒問題),讀8個位元組之後再讀剩下的1016(當然不一定每個包一定得是1024,可以更大,畢竟我目前夠用了)個位元組
using System;//send包namespace Packs{ //基礎包 public class BasePack<T> { public int packType; public int fromId; public int toId; public int messageId; //將基本包轉bytes public byte[] PackToBytes() { var encode = MsgPack.Serialization.MessagePackSerializer.Get<T>(); byte[] packContent = encode.PackSingleObject(this); byte[] type = BasePack.intToBytes(this.packType); byte[] len = BasePack.intToBytes(packContent.Length); int lenth = packContent.Length; byte[] dest = new byte[1024]; //第一個int空間:類型 Buffer.BlockCopy(type, 0, dest, 0, type.Length); //第二個int空間:長度 Buffer.BlockCopy(len, 0, dest, type.Length, len.Length); //剩餘空間:包內容 Buffer.BlockCopy(packContent, 0, dest, type.Length+len.Length, packContent.Length); Console.WriteLine("打包pack,類型:" + this.packType + "長度:" + packContent.Length); return dest; } //將bytes轉回基本包 public static T BytesToPack(byte[] bytes) { var encode = MsgPack.Serialization.MessagePackSerializer.Get<T>(); return encode.UnpackSingleObject(bytes); } } public class BasePack:BasePack<BasePack> { public const int LOGIN_PACK = 1; public const int REGISTER_PACK = 2; public const int PING_PACK = 3; public const int PONG_PACK = 4; public const int TEXT_PACK = 5; public const int SYSTEM_PUSH_PACK = 6; public const int LOGIN_ACK = 1001; public const int REGISTER_ACK = 1002; public const int PING_ACK = 1003; public const int PONG_ACK = 1004; public const int TEXT_ACK = 1005; public const int SYSTEM_PUSH_ACK = 1006; public const int CONNECTED_ACK = 1007; /** * 將int數值轉換為佔四個位元組的byte數組,本方法適用於(低位在前,高位在後)的順序。 * @param value * 要轉換的int值 * @return byte數組 */ public static byte[] intToBytes(int value) { byte[] byte_src = new byte[4]; byte_src[3] = (byte)((value & 0xFF000000) >> 24); byte_src[2] = (byte)((value & 0x00FF0000) >> 16); byte_src[1] = (byte)((value & 0x0000FF00) >> 8); byte_src[0] = (byte)((value & 0x000000FF)); return byte_src; } /** * byte數組中取int數值,本方法適用於(低位在前,高位在後)的順序。 * * @param ary * byte數組 * @param offset * 從數組的第offset位開始 * @return int數值 */ public static int bytesToInt(byte[] ary, int offset) { int value; value = (int)((ary[offset] & 0xFF) | ((ary[offset + 1] << 8) & 0xFF00) | ((ary[offset + 2] << 16) & 0xFF0000) | ((ary[offset + 3] << 24) & 0xFF000000)); return value; } } //1.登入包 public class LoginPack: BasePack<LoginPack> { public string username; public string password; public LoginPack() { this.packType = BasePack.LOGIN_PACK; } } //5.文字包 public class TextPack:BasePack<TextPack> { public string content; public string toUser; public string fromUser; public TextPack() { this.packType = BasePack.TEXT_PACK; } } //6.系統推送包 public class SystemPushPack: BasePack<SystemPushPack> { public string content; public string toUser; public SystemPushPack() { this.packType = BasePack.SYSTEM_PUSH_PACK; } } }
3.server端Accept之後立即發送ConnectPack,用戶端收到後發送ConnectAckPack完成串連
private void OnAccept() { while (this.isServing) { //非同步Accept 回調ConnEnd //serverSocket.BeginAccept(new System.AsyncCallback(this.ConnEnd), null); //同步Accept Socket clientSocket = serverSocket.Accept(); ReceiveObject obj = new ReceiveObject(); obj.acceptClient = clientSocket; clients.Add(obj); Thread receiveThread = new Thread(OnReceive); receiveThread.Start(obj); cThreads.Add(clientSocket.RemoteEndPoint.ToString(), receiveThread); Console.WriteLine("新的用戶端串連:" + clientSocket.RemoteEndPoint.ToString()); BaseACKPack pack = new BaseACKPack(); pack.packType = BasePack.CONNECTED_ACK; clientSocket.Send(pack.PackToBytes()); } }
4.用戶端發送LoginPack(由於php已經校正了使用者名稱和密碼並且產生了token,所以loginPack實際上我沒有寫校正密碼的邏輯,單純的綁定使用者名稱,用來接收訊息),server端拆開pack將使用者名稱綁定到用戶端對象中,這個對象的內容如下:
using System.Net.Sockets;public class ReceiveObject{ public Socket acceptClient; public byte[] buffer = new byte[1024]; public string userId; public string userName; public int roomId; public ReceiveObject() { }}
整個處理函數:
private void OnReceive(object obj) { while (this.isServing) { ReceiveObject e = obj as ReceiveObject; Socket c = e.acceptClient; e.buffer = new byte[1024]; //判斷包類型,固定包在包之前 int type = c.Receive(e.buffer, 0, sizeof(Int32), SocketFlags.None); if (type == 0) { Console.WriteLine("用戶端中斷連線:" + c.RemoteEndPoint.ToString()); //clients.RemoveAll((ReceiveObject obj) => { return obj.acceptClient == 0 ? true : false; }); clients.Remove(e); cThreads.Remove(c.RemoteEndPoint.ToString()); Thread.CurrentThread.Abort(); //中斷連線 c.Shutdown(SocketShutdown.Both); c.Close(); break; } type = BasePack.bytesToInt(e.buffer, 0); //獲得包大小,固定第2個int int len = c.Receive(e.buffer, 0, sizeof(Int32), SocketFlags.None); len = BasePack.bytesToInt(e.buffer, 0); int receiveNumber = c.Receive(e.buffer, 0, 1024 - sizeof(Int32) * 2, SocketFlags.None); switch (type) { case BasePack.LOGIN_PACK: { LoginPack lPack = LoginPack.BytesToPack(e.buffer); Console.WriteLine("收到登入請求,使用者名稱:" + lPack.username + "密碼:" + lPack.password); e.userName = lPack.username; //發送登入ACK LoginACKPack loginACK = new LoginACKPack(); //loginACK.success = true; c.Send(loginACK.PackToBytes()); } break; case BasePack.TEXT_PACK: { //處理訊息包 TextPack pack = TextPack.BytesToPack(e.buffer); //處理basePack Console.WriteLine("發送給" + pack.toUser + "的訊息:" + pack.content); //從clients組找使用者 List<ReceiveObject> list = clients.FindAll((ReceiveObject o) => { return o.userName == pack.toUser ? true : false; }); foreach (ReceiveObject target in list) { target.acceptClient.Send(pack.PackToBytes()); } } break; case BasePack.TEXT_ACK: { //處理訊息回執 TextACKPack pack = TextACKPack.BytesToPack(e.buffer); //刪除對應的訊息 pusher.DeleteMessageById(pack.messageId); } break; case BasePack.SYSTEM_PUSH_ACK: { SystemPushACKPack pack = SystemPushACKPack.BytesToPack(e.buffer); //刪除對應的訊息 pusher.DeleteMessageById(pack.messageId); } break; default: //處理未知包 { } break; } } }
發送訊息函數,目前寫了2個case 原因:php端的推送類型很多,我直接寫在pushPack的content內部,用戶端用json解析開就行了,然後做了一個單聊的簡訊發送,按群組推還沒來得及做:
public void SendMsg(string from, string to, string body, int type, int messageId) { //從clients組找使用者 ReceiveObject target = clients.FindLast((ReceiveObject o) => { return o.userName == to ? true : false; }); if (target == null) return; //推送一條訊息至用戶端 //收到回執後才能修改sent狀態為1 Console.WriteLine("推送訊息給:" + to + "類型:" + type + "內容:" + body + "id:" + messageId); switch (type) { //推送文字訊息 case BasePack.TEXT_PACK: { TextPack txtPack = new TextPack(); txtPack.fromUser = from; txtPack.toUser = to; txtPack.content = body; txtPack.messageId = messageId; target.acceptClient.Send(txtPack.PackToBytes()); } break; //系統訊息 case BasePack.SYSTEM_PUSH_PACK: { SystemPushPack txtPack = new SystemPushPack(); txtPack.toUser = to; txtPack.content = body; txtPack.messageId = messageId; target.acceptClient.Send(txtPack.PackToBytes()); } break; default: { } break; } }}
然後就是訊息佇列和php《-》c#間的調用問題
1.嚴格按照p2p模型和pubSub模型的訊息佇列,即:
p2p模型: 如果訊息接受者的username在clients數組中,立即發送標,否則存入資料庫作為離線訊息,待該使用者登入時再從資料庫取出該使用者的離線訊息至記憶體中繼續發送,直到收到相應類型的ack或baseAck(用戶端的協議比伺服器端低),從資料庫中徹底移除;
pubSub 模型:不管訊息接受在clients數組中有多少個(相同的roomId標記),0到理論上限個,立即發送且不需要回執且立即從記憶體中移除且不存入資料庫
2.由於php和c#程式是2個不同的進程,所以涉及到處理序間通訊,如果這2個程式運行在同一台電腦上,可行的辦法有:共用記憶體、本地socket、管道等等??但是實際情況可能我們更希望web程式和訊息程式可以不在同一台電腦,因此其他的方法:共用同一個資料庫連接、http輪詢
具體可以根據情況選擇,我這裡兩種都有寫。
且我的期望是php每插入一條訊息,c#馬上推送出去,那麼c#做資料庫輪詢或者http輪詢其實都還好,我只用了一個線程做輪詢。
最後今天寫下遊戲端吧:
終於可以推各種包了,開始遊戲包、出牌包、勝利包 DAY1已經描述,目前在做的: 用戶端牌型校正以及每一局中的每一輪何時判定。
這個遊戲規則就是標準的跑得快,也就是拿到黑桃3的玩家第一局第一輪先出牌,這裡還沒做,可以在所有玩家收到開始遊戲包之後做一個簡單的校正。
過牌直接調用出牌介面,傳一個空的字串即可,目前還沒有主動過牌和結束每一輪的邏輯,做了結束每一局的邏輯,即判定勝負。
最後是幾個測試,玩家id 45 和玩家 id 50玩了一局:
開發日記:KBEngine+Unity+php做個撲克小遊戲-DAY2