對UDP服務器的要求
? ? ? ? ? ? 如同TCP通信一樣讓UDP服務端可以服務多個客戶端
? ? ? ? ? ? 需要具備的條件:
? ? ? ? ? ? 1.區分消息類型(不需要處理分包、黏包)
? ? ? ? ? ? 2.能夠接收多個客戶端的消息
? ? ? ? ? ? 3.能夠主動給自己發過消息的客戶端發消息(記錄客戶端信息)
? ? ? ? ? ? 4.主動記錄上次收到客戶端消息的時間,如果長時間沒有收到消息,主動移除記錄的客戶端信息
? ? ? ? ? ? 分析:
? ? ? ? ? ? 1.UDP是無連接的,我們如何記錄連入的客戶端
? ? ? ? ? ? 2.UDP收發消息都是通過一個Socket來處理,我們應該如何和處理收發消息
? ? ? ? ? ? 3.如果不使用心跳消息,如何記錄上次收到消息的時間
基本數據類--封裝序列化和反序列化等方法
此代碼定義了一個抽象基類BaseData
,其中包含抽象方法用于獲取字節數組容器大小、序列化和反序列化成員變量,還提供了一系列受保護的方法用于在字節數組和不同數據類型(如int
、short
、long
等)及字符串、BaseData
子類對象之間進行讀寫操作。
using System;
using System.Collections;
using System.Collections.Generic;
using System.Text;public abstract class BaseData
{//用于子類重寫的 獲取字節數組容器大小的方法public abstract int GetBytesNum();//把成員變量序列化為對應的字節數組public abstract byte[] Writing();public abstract int Reading(byte[] bytes, int beginIndex=0);//bytes指定的字節數組//value具體的int值//index索引位置的變量protected void WriteInt(byte []bytes,int value,ref int index){BitConverter.GetBytes(value).CopyTo(bytes, index);index += sizeof(int);}protected void WriteShort(byte[]bytes,short value,ref int index){BitConverter.GetBytes(value).CopyTo(bytes, index);index += sizeof(short);}protected void WriteLong(byte[]bytes,long value,ref int index){BitConverter.GetBytes(value).CopyTo(bytes, index);index += sizeof(long);}protected void WriteFloat(byte[] bytes, float value, ref int index){BitConverter.GetBytes(value).CopyTo(bytes, index);index += sizeof(float);}protected void WriteByte(byte[]bytes,byte value,ref int index){bytes[index] = value;index += sizeof(byte);}protected void WriteBool(byte[] bytes, bool value, ref int index){BitConverter.GetBytes(value).CopyTo(bytes, index);index += sizeof(bool);}protected void WriteString(byte[]bytes,string value,ref int index){//先存儲string字節數組的長度byte[] strBytes = Encoding.UTF8.GetBytes(value);//BitConverter.GetBytes(strBytes.Length).CopyTo(bytes, index);//index += sizeof(int);WriteInt(bytes, strBytes.Length, ref index);//再存string字節數組strBytes.CopyTo(bytes, index);index += strBytes.Length;}protected void WriteData(byte[]bytes,BaseData data,ref int index){data.Writing().CopyTo(bytes, index);index += data.GetBytesNum();}protected int ReadInt(byte[]bytes,ref int index){int value = BitConverter.ToInt32(bytes, index);index += 4;return value;}protected short ReadShort(byte[] bytes, ref int index){short value = BitConverter.ToInt16(bytes, index);index += 2;return value;}protected long ReadLong(byte[] bytes, ref int index){long value = BitConverter.ToInt64(bytes, index);index += 8;return value;}protected float ReadFloat(byte[] bytes, ref int index){float value = BitConverter.ToSingle(bytes, index);index += sizeof(float);return value;}protected byte ReadByte(byte[] bytes, ref int index){byte value = bytes[index];index += 1;return value;}protected bool ReadBool(byte[] bytes, ref int index){bool value = BitConverter.ToBoolean(bytes, index);index += sizeof(bool);return value;}protected string ReadString(byte[] bytes, ref int index){int length = ReadInt(bytes, ref index);string value = Encoding.UTF8.GetString(bytes, index, length);index += length;return value;}protected T ReadData<T>(byte[] bytes, ref int index) where T : BaseData, new(){T value = new T();index+= value.Reading(bytes,index);return value;}
}
基本消息類
這段代碼定義了一個名為BaseMsg
的類,它繼承自BaseData
類。BaseMsg
類重寫了BaseData
的抽象方法GetBytesNum
、Reading
和Writing
,但這些重寫方法只是簡單拋出NotImplementedException
異常,表明目前未實現具體邏輯。此外,BaseMsg
類還定義了一個虛方法GetID
,默認返回 0。
BaseMsg
類的設計目的主要是作為消息類的基類,為后續具體消息類的實現提供統一的接口和結構框架。
using System.Collections;
using System.Collections.Generic;public class BaseMsg : BaseData
{public override int GetBytesNum(){throw new System.NotImplementedException();}public override int Reading(byte[] bytes, int beginIndex = 0){throw new System.NotImplementedException();}public override byte[] Writing(){throw new System.NotImplementedException();}public virtual int GetID(){return 0;}
}
玩家信息類
這段代碼定義了一個名為PlayerMsg
的類,它繼承自BaseMsg
類。PlayerMsg
類代表了與玩家相關的消息,并且實現了消息的序列化和反序列化功能。
using System.Collections;
using System.Collections.Generic;public class PlayerMsg : BaseMsg
{public int playerID;public PlayerData playerData;public override int GetBytesNum(){return 4 +//消息ID4 +//playerID長度playerData.GetBytesNum();//消息的長度}public override int GetID(){return 1001;}public override int Reading(byte[] bytes, int beginIndex = 0){//反序列化不需要去解析ID,因為在這一步之前,就應該將ID反序列化出來//用來判斷到底使用哪一個自定義類來反序列化int index = beginIndex;playerID = ReadInt(bytes, ref index);playerData = ReadData<PlayerData>(bytes, ref index);return index - beginIndex;}public override byte[] Writing(){int index = 0;byte[] playerBytes = new byte[GetBytesNum()];//先寫消息IDWriteInt(playerBytes, GetID(), ref index);WriteInt(playerBytes, playerID, ref index);WriteData(playerBytes, playerData, ref index);return playerBytes;}
}
using System.Collections;
using System.Collections.Generic;
using System.Text;public class PlayerData : BaseData
{public string name; public int lev;public int atk;public override int GetBytesNum(){return 4 + 4 + 4 + Encoding.UTF8.GetBytes(name).Length;}public override int Reading(byte[] bytes, int beginIndex = 0){int index = beginIndex;name=ReadString(bytes, ref index);lev=ReadInt(bytes, ref index);atk=ReadInt(bytes, ref index);return index - beginIndex;}public override byte[] Writing(){int index = 0;byte[] bytes = new byte[GetBytesNum()];WriteString(bytes, name, ref index);WriteInt(bytes, lev, ref index);WriteInt(bytes, atk, ref index);return bytes;}
}
這段代碼定義了一個名為PlayerData
的類,它繼承自BaseData
類。PlayerData
類的作用是用來表示玩家的相關數據,并且實現了這些數據的序列化與反序列化功能。
服務端類
這段代碼定義了一個名為ServerSocket
的類,用于構建基于 UDP 協議的服務器,它能通過綁定指定 IP 和端口啟動服務,利用線程池實現消息接收與客戶端超時檢查,將客戶端信息存儲在字典中,可處理新客戶端連接,接收客戶端消息并交予對應客戶端對象處理,支持向指定客戶端發送消息、向所有客戶端廣播消息,還能移除超時或指定的客戶端。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;namespace UDPServerExerise
{class ServerSocket{public Socket socket;private bool IsClose;//我們可以通過記錄誰給我們發了消息 把它的IP和端口記錄下來 這樣就認為他是我的客戶端了private Dictionary<string, Client> clientDic = new Dictionary<string, Client>();public void Start(string ip,int port){socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);IPEndPoint ipPoint = new IPEndPoint(IPAddress.Parse(ip), port);try{socket.Bind(ipPoint);IsClose = false;}catch (Exception e){Console.WriteLine("UDP開啟錯誤" + e.Message);}//接收消息,使用線程池ThreadPool.QueueUserWorkItem(ReceiveMsg);//檢測超時的線程ThreadPool.QueueUserWorkItem(CheakTimeOut);}private void CheakTimeOut(object obj){long nowTime=0;List<string> delClient = new List<string>();while (true){//30秒檢查一次Thread.Sleep(30000);//得到當前系統時間nowTime = DateTime.Now.Ticks / TimeSpan.TicksPerSecond;foreach (Client c in clientDic .Values){//超過十秒沒有 收到消息的客戶端需要被移除if(nowTime -c.frontTime >=10){delClient.Add(c.clientID);}}//從待刪除列表中刪除超時客戶端for (int i = 0; i < delClient.Count; i++)RemoveClient(delClient[i]);delClient.Clear();}}private void ReceiveMsg(object obj){byte[] bytes = new byte[512];//記錄誰發的string strID = "";string ip;int port;EndPoint ipPoint = new IPEndPoint(IPAddress.Any, 0);while (!IsClose){if(socket.Available >0){lock(socket)socket.ReceiveFrom(bytes, ref ipPoint);//處理消息 最好不要直接在這里處理,而是交給客戶端對象處理//收到消息時,我們要判斷 是不是記錄了這個客戶端的信息(ip和端口)//出去發送消息給我的IP和端口ip = (ipPoint as IPEndPoint).Address.ToString();port = (ipPoint as IPEndPoint).Port;strID = ip + port;//拼接成唯一一個ID這是我們自定義的規則//判斷有沒有記錄這個客戶端的信息,如果有直接用它處理信息if(clientDic .ContainsKey (strID )){clientDic[strID].ReceiveMsg(bytes);}else//如果沒有 直接添加并處理消息{clientDic.Add(strID, new Client(ip, port));clientDic[strID].ReceiveMsg(bytes);}}}}public void SendTo(BaseMsg msg,IPEndPoint ipPoint){try{lock (socket)socket.SendTo(msg.Writing(), ipPoint);}catch (SocketException s){Console.WriteLine("發消息出現問題" + s.SocketErrorCode + s.Message);}catch (Exception e){Console.WriteLine("發消息出現問題(可能是序列化的問題)" + e.Message);}}private void Close(){if(socket!=null){socket.Shutdown(SocketShutdown.Both);socket.Close();IsClose = true;socket = null;}}public void BoardCast(BaseMsg msg){//廣播給誰foreach (Client c in clientDic .Values){SendTo(msg,c.ipAndPoint);}}public void RemoveClient(string clientID){if(clientDic .ContainsKey (clientID)){Console.WriteLine("客戶端{0}被移除了", clientID);clientDic.Remove(clientID);}}}
}
客戶端類
這段代碼定義了Client
類,用于處理 UDP 服務器端接收到的來自客戶端的消息。Client
類的構造函數通過傳入的 IP 和端口創建IPEndPoint
對象并生成唯一的客戶端 ID;ReceiveMsg
方法接收消息字節數組,拷貝消息到新數組,記錄消息接收時間,并將消息處理任務放入線程池;ReceiceHandleMsg
方法從消息字節數組中解析消息類型、長度和消息體,針對不同消息 ID(如 1001 對應PlayerMsg
消息,1003 對應quitMsg
消息)進行相應處理,如反序列化PlayerMsg
并輸出相關信息,處理quitMsg
時移除對應客戶端,若處理消息出錯也會移除該客戶端。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Text;
using System.Threading;
using System.Threading.Tasks;namespace UDPServerExerise
{class Client{public IPEndPoint ipAndPoint;public string clientID;public float frontTime = -1;public Client (string ip,int port){//規則和外邊一樣 記錄唯一ID 通過ip和port拼接的形式clientID = ip + port;//把客戶端的信息記錄下來ipAndPoint = new IPEndPoint(IPAddress.Parse(ip), port);}public void ReceiveMsg(byte[]bytes){//為了避免處理消息時又接收到了新的消息 所以我們需要在處理消息前 先把消息拷貝出來//處理消息和接收消息用不同容器 避免發生沖突byte[] cacheBytes = new byte[512];bytes.CopyTo(cacheBytes, 0);//記錄發消息的系統時間frontTime = DateTime.Now.Ticks / TimeSpan.TicksPerSecond;ThreadPool.QueueUserWorkItem(ReceiceHandleMsg, cacheBytes);}private void ReceiceHandleMsg(object obj){try{byte[] bytes = obj as byte[];int nowIndex = 0;//解析消息類型int msgID = BitConverter.ToInt32(bytes, nowIndex);nowIndex += 4;//解析消息長度int length = BitConverter.ToInt32(bytes, nowIndex);nowIndex += 4;//解析消息體switch (msgID){case 1001:PlayerMsg playerMsg = new PlayerMsg();playerMsg.Reading(bytes, nowIndex);Console.WriteLine(playerMsg.playerID);Console.WriteLine(playerMsg.playerData.lev);Console.WriteLine(playerMsg.playerData.atk);Console.WriteLine(playerMsg.playerData.name);break;case 1003:quitMsg quitMsg = new quitMsg();//由于它沒有消息體 所以不用反序列化//quitMsg.Reading(bytes, nowIndex);//處理退出Program.serverSocket.RemoveClient(clientID);break;}}catch (Exception e){Console.WriteLine("處理消息出錯" + e.Message);//如果出錯了,就不用記錄客戶端的信息了Program.serverSocket.RemoveClient(clientID);}}}
}
退出消息類
這段代碼定義了一個名為quitMsg
的類,它繼承自BaseMsg
類,用于表示退出消息,重寫了GetBytesNum
方法指定消息字節數為 8,重寫GetID
方法返回消息唯一標識符 1003,重寫Reading
方法調用基類方法進行反序列化,重寫Writing
方法將消息 ID 和消息體長度(這里設為 0)序列化為字節數組。
using System.Collections;
using System.Collections.Generic;public class quitMsg : BaseMsg
{public override int GetBytesNum(){return 8;}public override int GetID(){return 1003;}public override int Reading(byte[] bytes, int beginIndex = 0){return base.Reading(bytes, beginIndex);}public override byte[] Writing(){int index = 0;byte[] bytes = new byte[GetBytesNum()];WriteInt(bytes, GetID(), ref index);WriteInt(bytes, 0, ref index);return bytes;}
}
主函數啟動服務器
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;namespace UDPServerExerise
{class Program{public static ServerSocket serverSocket;static void Main(string[] args){serverSocket = new ServerSocket();serverSocket.Start("127.0.0.1", 8080);Console.WriteLine("UDP服務器啟動了");string input = Console.ReadLine();if(input.Substring (0,2)=="B:"){PlayerMsg msg = new PlayerMsg();msg.playerData = new PlayerData();msg.playerID = 1001;msg.playerData.atk = 999;msg.playerData.lev = 88;msg.playerData.name ="DamnF的服務器";serverSocket.BoardCast(msg);}}}
}