開學了,去圖書館借了幾本書,沒有找到想要的C++網絡編程,卻是找到了幾本LINUX的書,以及一本《Visual C#經典遊戲編程開發》。翻了翻發現裏面有個能夠聯網對弈的中國象棋遊戲,一直寫ASP.NET的網頁,也想試着寫寫Winform的圖形程序了,並且能夠了解一下.NET的網絡編程相關的內容。編程
象棋方面,先從網上找了張棋盤圖片,在PS裏把棋子一一扣了出來做爲素材。編寫對應的棋子類和棋盤類,前者實現棋子身份的識別和棋子圖片的繪製,後者實現棋盤繪製、走棋規則、棋譜生成等功能。c#
網絡通訊的協議使用的是UDP協議,發送的信息規則是:「命令|參數1|參數2……」,主要命令:join表示用戶想要聯機,處於接受對方鏈接的狀態;conn表示收到對方聯機命令,已準備好開始遊戲,對方能夠開局了;new表示其中一方提出從新開始遊戲;move|id|x|y表示移動棋盤上編號爲id的棋子到x,y處;succ|贏方代號 表示此局已分出勝負;exit表示退出遊戲。網絡
遊戲開始後建立了一個線程,在read方法中建立UdpClient對象,循環偵聽指定端口中由指定IP主機傳來的信息。主要代碼是:socket
udpClient = new UdpClient(Convert.ToInt32(txt_port.Text)); int id, x, y; while(readFlag == true) { try { byte[] data = udpClient.Receive(ref remote); string strData = Encoding.UTF8.GetString(data); string[] a = strData.Split('|'); // 分割命令與參數 switch(a[0]) // 判斷命令 { case "join": case "conn": //...... } } catch { break; } }
遊戲過程當中發送數據則是經過send方法,建立UdpClient網絡服務對象,向指定IP的主機發送消息到設定的端口號。完成後發送後,關閉UDP網絡服務。主要代碼:spa
UdpClient sendUdp = new UdpClient(); UPAddress remoteIP; try { remoteIP = IPAddress.Parse(txt_IP.Text); } catch { MessageBox.Show("請輸入正確的IP地址!", "錯誤"); return; } remote = new IPEndPoint(remoteIP, Convert.ToInt32(txt_remoteport.Text)); byte[] buffer = Encoding.UTF8.GetBytes(str); sendUdp.Send(buffer, buffer.Length, remote); sendUdp.Close();
原書中的代碼有一些小問題,因此作了點改動和簡化。不過基本的思路仍是和原書上是同樣的。線程
通訊過程方面,每次聯機都須要雙方填寫好對方的IP、端口號以及本身的端口號。並且,因爲沒有明顯的遊戲過程標記,因此形成了一些通訊問題,致使在不正確的階段收到對方錯誤的請求時遊戲出現意外的狀況。3d
因而在完成後,就開始了對原程序的改造。考慮到內網用戶鏈接外網的問題,就將遊戲中的UDP協議通訊改成了使用Socket經過TCP協議進行遊戲通訊。添加遊戲狀態state字段,包括如下幾個主要狀態:code
public const short WAITING_CLIENT = 1; // 做爲主機,等待客戶端鏈接 public const short SERVING = 2; // 做爲主機,客戶端已鏈接,遊戲中 public const short WAITING_SERVER = 3; // 做爲從機,等待主機接受鏈接 public const short CONNECTING = 4; // 做爲從機,鏈接到主機,遊戲中
遊戲的命令和監聽流程也作了對應的改動。orm
主機創建遊戲等待客戶端鏈接的過程主要包括:監聽端口、創建鏈接、開始監聽消息,這三個步驟。首先建立線程,在線程中執行acceptClientConnect監聽端口等待鏈接,檢測到鏈接後創建鏈接,轉入Read監聽階段。對象
private void WaitClient() { // 啓動一個線程來接受請求 thread = new Thread(acceptClientConnect); thread.Start(); SetState(WAITING_CLIENT); } // 接受請求 private void acceptClientConnect() { IPEndPoint localEndPoint = new IPEndPoint(IPAddress.Any, DEFAULT_PORT); listener = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); toolStripStatusLabel1.Text = "正在等待其餘玩家鏈接……"; try { if (timeoutThread != null) { timeoutThread.Abort(); } timeoutThread = new Thread(WaiteTimeOut); timeoutThread.Start(); listener.Bind(localEndPoint); listener.Listen(1); connection = listener.Accept(); listener.Close(); listener = null; Read(); } catch { toolStripStatusLabel1.Text = "鏈接中斷,遊戲終止。"; SetState(FREE); } } private void WaiteTimeOut() { Thread.Sleep(30000); if (connection == null || !connection.Connected) { if (listener != null) { SetState(FREE); toolStripStatusLabel1.Text = "鏈接超時,無玩家鏈接遊戲。"; } } timeoutThread = null; }
預約的監聽超時時間是30秒,30秒後沒有鏈接時,轉到FREE狀態並終止監聽。終止的實現思路是在主機自己建立一個TCP協議的Socket對象,向自身端口發送終止鏈接的消息,listener創建鏈接後在Read中接受到此消息中止阻塞,結束遊戲完全轉爲FREE狀態,線程結束。
private void SetState(int newState) // 支線程 { state = newState; switch (state) { case FREE: btnCreate.Text = "建立遊戲"; btnCreate.Enabled = true; btnJoin.Text = "加入遊戲"; btnJoin.Enabled = true; if (listener != null) { Socket socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); socket.Connect(IPAddress.Parse("127.0.0.1"), DEFAULT_PORT); if (socket.Connected) { socket.Send(Encoding.UTF8.GetBytes("end")); } } if (connection != null) { connection.Close(); connection = null; } break; case WAITING_CLIENT: btnCreate.Text = "關閉遊戲"; btnJoin.Enabled = false; break; case SERVING: break; case WAITING_SERVER: btnJoin.Text = "斷開遊戲"; btnCreate.Enabled = false; break; case CONNECTING: break; } }
接受消息的Read方法和發送消息的Send方法方面,本來的UdpClient收發消息改成經過建立的connection(Socket)鏈接來通訊。
private void Read() { int t, x1, y1, x2, y2; while (state != FREE) { try { byte[] data = new byte[1024]; int len = connection.Receive(data); string msg = Encoding.UTF8.GetString(data); string[] a = msg.Split('|'); switch (a[0]) { case "end": // 終止listener的監聽 if (state != WAITING_CLIENT) continue; connection.Close(); connection = null; SetState(FREE); return; case "join": // 客戶端請求加入遊戲 break; case "acce": // 服務端接受加入的請求 break; case "exit": // 對方方退出遊戲 break; case "new": // 對方方提出從新開局 break; case "succ": // 一方獲勝 break; case "lose": // 一方認輸 break; case "move": // 對方移動棋子 break; } } catch { break; } } } private void Send(string msg) { if (connection != null) { byte[] buffer = Encoding.UTF8.GetBytes(msg); connection.Send(buffer); } }
具體遊戲效果截圖:
項目工程下載: