用C#實現UDP服務器

對UDP服務器的要求

? ? ? ? ? ? 如同TCP通信一樣讓UDP服務端可以服務多個客戶端
? ? ? ? ? ? 需要具備的條件:
? ? ? ? ? ? 1.區分消息類型(不需要處理分包、黏包)
? ? ? ? ? ? 2.能夠接收多個客戶端的消息
? ? ? ? ? ? 3.能夠主動給自己發過消息的客戶端發消息(記錄客戶端信息)
? ? ? ? ? ? 4.主動記錄上次收到客戶端消息的時間,如果長時間沒有收到消息,主動移除記錄的客戶端信息

? ? ? ? ? ? 分析:
? ? ? ? ? ? 1.UDP是無連接的,我們如何記錄連入的客戶端
? ? ? ? ? ? 2.UDP收發消息都是通過一個Socket來處理,我們應該如何和處理收發消息
? ? ? ? ? ? 3.如果不使用心跳消息,如何記錄上次收到消息的時間

基本數據類--封裝序列化和反序列化等方法

此代碼定義了一個抽象基類BaseData,其中包含抽象方法用于獲取字節數組容器大小、序列化和反序列化成員變量,還提供了一系列受保護的方法用于在字節數組和不同數據類型(如intshortlong等)及字符串、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的抽象方法GetBytesNumReadingWriting,但這些重寫方法只是簡單拋出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);}}}
}

成功運行程序--等待客戶端通信

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/pingmian/73504.shtml
繁體地址,請注明出處:http://hk.pswp.cn/pingmian/73504.shtml
英文地址,請注明出處:http://en.pswp.cn/pingmian/73504.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

如何在 Postman 中發送 PUT 請求?

在 Postman 中發送 PUT 請求的步驟相對簡單&#xff0c;包括新建接口、選擇 PUT 方法、填寫 URL 和參數等幾個主要步驟。 Postman 發送 put 請求教程

charles抓包軟件免費使用教程

本文將給大家介紹Charles破解教程&#xff0c;支持Windows和Mac系統&#xff0c;操作簡單&#xff0c;永久免費使用。同時&#xff0c;我們也會提到另一款強大的抓包工具——SniffMaster&#xff08;抓包大師&#xff09;&#xff0c;它在網絡調試和數據包分析方面同樣表現出色…

卷積神經網絡 - 參數學習

本文我們通過兩個簡化的例子&#xff0c;展示如何從前向傳播、損失計算&#xff0c;到反向傳播推導梯度&#xff0c;再到參數更新&#xff0c;完整地描述卷積層的參數學習過程。 一、例子一 我們構造一個非常簡單的卷積神經網絡&#xff0c;其結構僅包含一個卷積層和一個輸出…

.NET三層架構詳解

.NET三層架構詳解 文章目錄 .NET三層架構詳解引言什么是三層架構表示層&#xff08;Presentation Layer&#xff09;業務邏輯層&#xff08;Business Logic Layer&#xff0c;BLL&#xff09;數據訪問層&#xff08;Data Access Layer&#xff0c;DAL&#xff09; .NET三層架構…

Redis實戰常用二、緩存的使用

一、什么是緩存 在實際開發中,系統需要"避震器"&#xff0c;防止過高的數據訪問猛沖系統,導致其操作線程無法及時處理信息而癱瘓. 這在實際開發中對企業講,對產品口碑,用戶評價都是致命的。所以企業非常重視緩存技術; 緩存(Cache)&#xff1a;就是數據交換的緩沖區&…

STM32八股【2】-----ARM架構

1、架構包含哪幾部分內容 寄存器處理模式流水線MMU指令集中斷FPU總線架構 2、以STM32為例進行介紹 2.1 寄存器 寄存器名稱作用R0-R3通用寄存器用于數據傳遞、計算及函數參數傳遞&#xff1b;R0 也用于存儲函數返回值。R4-R12通用寄存器用于存儲局部變量&#xff0c;減少頻繁…

effective Java 學習筆記(第二彈)

effective Java 學習筆記&#xff08;第一彈&#xff09; 整理自《effective Java 中文第3版》 本篇筆記整理第3&#xff0c;4章的內容。 重寫equals方法需要注意的地方 自反性&#xff1a;對于任何非空引用 x&#xff0c;x.equals(x) 必須返回 true。對稱性&#xff1a;對于…

mac命令行快捷鍵

光標移動 Ctrl A: 將光標移動到行首。Ctrl E: 將光標移動到行尾。Option 左箭頭: 向左移動一個單詞。Option 右箭頭: 向右移動一個單詞。 刪除和修改 Ctrl K: 刪除從光標到行尾的所有內容。Ctrl U: 刪除從光標到行首的所有內容。Ctrl W: 刪除光標前的一個單詞。Ctrl …

CentOS 7部署主域名服務器 DNS

1. 安裝 BIND 服務和工具 yum install -y bind bind-utils 2. 配置 BIND 服務 vim /etc/named.conf 修改以下配置項: listen-on port 53 { any; }; # 監聽所有接口allow-query { any; }; # 允許所有設備查詢 3 . 添加你的域名區域配置 …

優化 SQL 語句方向和提升性能技巧

優化 SQL 語句是提升 MySQL 性能的關鍵步驟之一。通過優化 SQL 語句,可以減少查詢時間、降低服務器負載、提高系統吞吐量。以下是優化 SQL 語句的方法、策略和技巧: 一、優化 SQL 語句的方法 1. 使用 EXPLAIN 分析查詢 作用:查看 SQL 語句的執行計劃,了解查詢是如何執行的…

C++ 多線程簡要講解

std::thread是 C11 標準庫中用于多線程編程的核心類&#xff0c;提供線程的創建、管理和同步功能。下面我們一一講解。 一.構造函數 官網的構造函數如下&#xff1a; 1.默認構造函數和線程創建 thread() noexcept; 作用&#xff1a;創建一個 std::thread 對象&#xff0c;但…

Vscode HTML5新增元素及屬性

一、?HTML5 語義化標簽 HTML5 語義化標簽&#xff08;Semantic Elements&#xff09;是一組 ?具有明確含義的 HTML 元素?&#xff0c;通過標簽名稱直接描述其內容或結構的功能&#xff0c;而非僅作為樣式容器&#xff08;如 <div> 或 <span>&#xff09;。它們旨…

【PostgreSQL教程】PostgreSQL 特別篇之 語言接口Python

博主介紹:?全網粉絲22W+,CSDN博客專家、Java領域優質創作者,掘金/華為云/阿里云/InfoQ等平臺優質作者、專注于Java技術領域? 技術范圍:SpringBoot、SpringCloud、Vue、SSM、HTML、Nodejs、Python、MySQL、PostgreSQL、大數據、物聯網、機器學習等設計與開發。 感興趣的可…

Three學習入門(四)

9-Three.js 貼圖與材質學習指南 環境準備 <!DOCTYPE html> <html> <head><title>Three.js Texture Demo</title><style> body { margin: 0; } </style> </head> <body><script src"https://cdnjs.cloudflare.…

前端NVM安裝

https://v0.dev/chat/settings 本地啟動環境 1安裝 nvm 2安裝node nvm install v18.19.0 nvm install v20.9.0 nvm use 18 node -v 3安裝 pnpm npm install -g pnpm 或者 npm i -g pnpm 4啟動 代碼 目錄下 執行 pnpm i pnpm run dev 4.1到代碼目錄下 4.2直接cmd…

藍橋杯算法精講:二分查找實戰與變種解析

適合人群&#xff1a;藍橋杯備考生 | 算法競賽入門者 | 二分查找進階學習者 目錄 一、二分查找核心要點 1. 算法思想 2. 適用條件 3. 算法模板 二、藍橋杯真題實戰 例題&#xff1a;分巧克力&#xff08;藍橋杯2017省賽&#xff09; 三、二分查找變種與技巧 1. 查找左邊…

cmd命令查看電腦的CPU、內存、存儲量

目錄 獲取計算機硬件的相關信息的命令分別的功能結果展示結果說明獲取計算機硬件的相關信息的命令 wmic cpu get name wmic memorychip get capacity wmic diskdrive get model,size,mediaType分別的功能 獲取計算機中央處理器(CPU)的名稱 獲取計算機內存(RAM)芯片的容量…

SCI論文閱讀指令(特征工程)

下面是一個SCI論文閱讀特征工程V3.0&#xff0c;把指令輸入大模型中&#xff0c;并上傳PDF論文&#xff0c;就可以幫你快速閱讀論文。 優先推薦kimi&#xff0c;當然DeepSeek、QwQ-32B等大語言模型也可以。測試了一下總結的還不錯&#xff0c;很詳細。 請仔細并深入地閱讀所提…

如何監控 SQL Server

監控 SQL Server 對于維護數據庫性能、確保數據可用性和最大限度地減少停機時間至關重要。隨著企業越來越依賴數據驅動的決策&#xff0c;高效的SQL Server監控策略能顯著提升組織生產力和用戶滿意度。 為什么要監控 SQL Server SQL Server 是許多關鍵應用程序的支柱&#xf…

python腳本處理excel文件

1.對比perl和python 分別嘗試用perl和python處理excel文件&#xff0c;發現perl的比較復雜&#xff0c;比如說read excel就有很多方式 Spreadsheet::Read use Spreadsheet::ParseExcel 不同的method&#xff0c;對應的取sheet的cell方式也不一樣。更復雜的是處理含有中文內…