云计算百科
云计算领域专业知识百科平台

用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 +//消息ID
4 +//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()];
//先写消息ID
WriteInt(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);
}
}
}
}

成功运行程序–等待客户端通信

赞(0)
未经允许不得转载:网硕互联帮助中心 » 用C#实现UDP服务器
分享到: 更多 (0)

评论 抢沙发

评论前必须登录!