IP和端口
//我们知道想要进行网络通信,进行网络连接
//首先我们需要找到对应设备,IP和端口号是定位网络中设备必不可少的关键元素
//C#中提供了对应的IP和端口相关的类 来声明对应信息
IPAddress类
//命名空间:System.Net;
//类名:IPAddress
//初始化IP信息的方式
//1.用byte数组进行初始化
byte[] ipAddress = new byte[] { 118, 102, 111, 11 };
IPAddress ip1 = new IPAddress(ipAddress);
//2.用long长整型进行初始化
//4字节对应的长整型 一般不建议大家使用
IPAddress ip2 = new IPAddress(0x79666F0B);
//3.推荐使用的方式 使用字符串转换
IPAddress ip3 = IPAddress.Parse("118.102.111.11");
//特殊IP地址
//127.0.0.1代表本机地址
//一些静态成员
//获取可用的IPv6地址
//IPAddress.IPv6Any
IPEndPoint类
//命名空间:System.Net;
//类名:IPEndPoint
//IPEndPoint类将网络端点表示为IP地址和端口号,表现为IP地址和端口号的组合
//初始化方式
IPEndPoint ipPoint = new IPEndPoint(0x79666F0B, 8080);
IPEndPoint ipPoint2 = new IPEndPoint(IPAddress.Parse("118.102.111.11"), 8080);
域名解析
//域名解析就是域名到IP地址的转换过程。域名的解析工作由DNS服务器完成
//我们在进行通信时有时会有需求通过域名获取IP
IPHostEntry类
//命名空间:System.Net
//类名:IPHostEntry
//主要作用:域名解析后的返回值 可以通过该对象获取IP地址、主机名等等信息
//该类不会自己声明,都是作为某些方法的返回值返回信息,我们主要通过该类对象获取返回的信息
//获取关联IP 成员变量:AddressList
//获取主机别名列表 成员变量:Aliases
//获取DNS名称 成员变量:HostName
Dns类
//命名空间:System.Net
//类名:Dns
//主要作用:Dns是一个静态类,提供了很多静态方法,可以使用它来根据域名获取IP地址
//常用方法
//1.获取本地系统的主机名
print(Dns.GetHostName());
//2.获取指定域名的IP信息
//根据域名获取
//同步获取
//注意:由于获取远程主机信息是需要进行网路通信,所以可能会阻塞主线程
//IPHostEntry entry = Dns.GetHostEntry("www.baidu.com");
//for (int i = 0; i < entry.AddressList.Length; i++)
//{
// print("IP地址:" + entry.AddressList[i]);
//}
//for (int i = 0; i < entry.Aliases.Length; i++)
//{
// print("主机别名" + entry.Aliases[i]);
//}
//print("DNS服务器名称" + entry.HostName);
//异步获取
GetHostEntry();
private async void GetHostEntry()
{
Task<IPHostEntry> task = Dns.GetHostEntryAsync("www.baidu.com");
await task;
for (int i = 0; i < task.Result.AddressList.Length; i++)
{
print("IP地址:" + task.Result.AddressList[i]);
}
for (int i = 0; i < task.Result.Aliases.Length; i++)
{
print("主机别名" + task.Result.Aliases[i]);
}
print("DNS服务器名称" + task.Result.HostName);
}
序列化
非字符串类型转字节数组
//关键类:BitConverter
//所在命名空间:System
//主要作用:除字符串的其它常用类型和字节数组相互转换
byte[] bytes = BitConverter.GetBytes(1);
字符串类型转字节数组
//关键类:Encoding
//所在命名空间:System.Text
//主要作用:将字符串类型和字节数组相互转换,并且决定转换时使用的字符编码类型,网络通信时建议大家使用UTF-8类型
byte[] byte2 = Encoding.UTF8.GetBytes("的卡萨福利卡决胜巅峰卡视角的副驾驶的");
将一个类对象转换为二进制
//注意:网络通信中我们不能直接使用BinaryFormatter 2进制格式化类
//因为客户端和服务器使用的语言可能不一样,BinaryFormatter是C#的序列化规则,和其它语言之间的兼容性不好
//如果使用它,那么其它语言开发的服务器无法对其进行反序列化
//我们需要自己来处理将类对象数据序列化为字节数组
//字符串需要记录字符串的长度
public class PlayerInfo
{
public int lev;
public string name;
public short atk;
public bool sex;
public byte[] GetBytes()
{
//1.得到的 这个类数据 如果转换成 字节数组 那么字节数组容器需要的容量
int indexNum = sizeof(int) + //lev int类型 4
sizeof(int) + //代表 name字符串转换成字节数组后 数组的长度 4
Encoding.UTF8.GetBytes(name).Length + //字符串具体字节数组的长度
sizeof(short) + //atk short类型 2
sizeof(bool); //sex bool类型 1
//2.申明一个装载信息的字节数组容器
byte[] playerBytes = new byte[indexNum];
int index = 0;//从 playerBytes数组中的第几个位置去存储数据
//3.将对象中的所有信息转为字节数组并放入该容器当中(可以利用数组中的CopeTo方法转存字节数组)
//CopyTo方法的第二个参数代表 从容器的第几个位置开始存储
//等级
BitConverter.GetBytes(lev).CopyTo(playerBytes, index);
index += sizeof(int);
//姓名
byte[] strBytes = Encoding.UTF8.GetBytes(name);
int num = strBytes.Length;
//存储的是姓名转换成字节数组后 字节数组的长度
BitConverter.GetBytes(num).CopyTo(playerBytes, index);
index += sizeof(int);
//存储字符串的字节数组
strBytes.CopyTo(playerBytes, index);
index += num;
//攻击力
BitConverter.GetBytes(atk).CopyTo(playerBytes, index);
index += sizeof(short);
//性别
BitConverter.GetBytes(sex).CopyTo(playerBytes, index);
index += sizeof(bool);
return playerBytes;
}
}
反序列化
字节数组转非字符串类型
//关键类:BitConverter
//所在命名空间:System
//主要作用:除字符串的其它常用类型和字节数组相互转换
byte[] bytes = BitConverter.GetBytes(99);
int i = BitConverter.ToInt32(bytes, 0);
print(i);
字节数组转字符串类型
//关键类:Encoding
//所在命名空间:System.Text
//主要作用:将字符串类型和字节数组相互转换,并且决定转换时使用的字符编码类型,网络通信时建议大家使用UTF-8类型
byte[] bytes2 = Encoding.UTF8.GetBytes("123123空间大撒了房间阿斯利康放大镜");
string str = Encoding.UTF8.GetString(bytes2, 0, bytes2.Length);
print(str);
将二进制数据转为一个类对象
类对象为序列化当中声明的PlayerInfo类,且封装了一个得到类对象字节数组的方法
//1.获取到对应的字节数组
PlayerInfo info = new PlayerInfo();
info.lev = 10;
info.name = "唐老狮";
info.atk = 88;
info.sex = false;
byte[] playerBytes = info.GetBytes();
//2.将字节数组按照序列化时的顺序进行反序列化(将对应字节分组转换为对应类型变量)
PlayerInfo info2 = new PlayerInfo();
//等级
int index = 0;
info2.lev = BitConverter.ToInt32(playerBytes, index);
index += 4;
print(info2.lev);
//姓名的长度
int length = BitConverter.ToInt32(playerBytes, index);
index += 4;
//姓名字符串
info2.name = Encoding.UTF8.GetString(playerBytes, index, length);
index += length;
print(info2.name);
//攻击力
info2.atk = BitConverter.ToInt16(playerBytes, index);
index += 2;
print(info2.atk);
//性别
info2.sex = BitConverter.ToBoolean(playerBytes, index);
index += 1;
print(info2.sex);
Socket
Socket套接字的作用
//它是C#提供给我们用于网络通信的一个类(在其它语言当中也有对应的Socket类)
//类名:Socket
//命名空间:System.Net.Sockets
//Socket套接字是支持TCP/IP网络通信的基本操作单位
//一个套接字对象包含以下关键信息
//1.本机的IP地址和端口
//2.对方主机的IP地址和端口
//3.双方通信的协议信息
//一个Sccket对象表示一个本地或者远程套接字信息
//它可以被视为一个数据通道
//这个通道连接与客户端和服务端之间
//数据的发送和接受均通过这个通道进行
//一般在制作长连接游戏时,我们会使用Socket套接字作为我们的通信方案
//我们通过它连接客户端和服务端,通过它来收发消息
//你可以把它抽象的想象成一根管子,插在客户端和服务端应用程序上,通过这个管子来传递交换信息
Socket的类型
//Socket套接字有3种不同的类型
//1.流套接字
// 主要用于实现TCP通信,提供了面向连接、可靠的、有序的、数据无差错且无重复的数据传输服务
//2.数据报套接字
// 主要用于实现UDP通信,提供了无连接的通信服务,数据包的长度不能大于32KB,不提供正确性检查,不保证顺序,可能出现重发、丢失等情况
//3.原始套接字(不常用,不深入讲解)
// 主要用于实现IP数据包通信,用于直接访问协议的较低层,常用于侦听和分析数据包
//通过Socket的构造函数 我们可以申明不同类型的套接字
//Socket s = new Socket()
//参数一:AddressFamily 网络寻址 枚举类型,决定寻址方案
// 常用:
// 1.InterNetwork IPv4寻址
// 2.InterNetwork6 IPv6寻址
// 做了解:
// 1.UNIX UNIX本地到主机地址
// 2.ImpLink ARPANETIMP地址
// 3.Ipx IPX或SPX地址
// 4.Iso ISO协议的地址
// 5.Osi OSI协议的地址
// 7.NetBios NetBios地址
// 9.Atm 本机ATM服务地址
//参数二:SocketType 套接字枚举类型,决定使用的套接字类型
// 常用:
// 1.Dgram 支持数据报,最大长度固定的无连接、不可靠的消息(主要用于UDP通信)
// 2.Stream 支持可靠、双向、基于连接的字节流(主要用于TCP通信)
// 做了解:
// 1.Raw 支持对基础传输协议的访问
// 2.Rdm 支持无连接、面向消息、以可靠方式发送的消息
// 3.Seqpacket 提供排序字节流的面向连接且可靠的双向传输
//参数三:ProtocolType 协议类型枚举类型,决定套接字使用的通信协议
// 常用:
// 1.TCP TCP传输控制协议
// 2.UDP UDP用户数据报协议
// 做了解:
// 1.IP IP网际协议
// 2.Icmp Icmp网际消息控制协议
// 3.Igmp Igmp网际组管理协议
// 4.Ggp 网关到网关协议
// 5.IPv4 Internet协议版本4
// 6.Pup PARC通用数据包协议
// 7.Idp Internet数据报协议
// 8.Raw 原始IP数据包协议
// 9.Ipx Internet数据包交换协议
// 10.Spx 顺序包交换协议
// 11.IcmpV6 用于IPv6的Internet控制消息协议
//2、3参数的常用搭配:
// SocketType.Dgram + ProtocolType.Udp = UDP协议通信(常用,主要学习)
// SocketType.Stream + ProtocolType.Tcp = TCP协议通信(常用,主要学习)
// SocketType.Raw + ProtocolType.Icmp = Internet控制报文协议(了解)
// SocketType.Raw + ProtocolType.Raw = 简单的IP包通信(了解)
//我们必须掌握的
//TCP流套接字
Socket socketTcp = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
//UDP数据报套接字
Socket socketUdp = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
Socket的常用属性
//1.套接字的连接状态
if(socketTcp.Connected)
{
}
//2.获取套接字的类型
print(socketTcp.SocketType);
//3.获取套接字的协议类型
print(socketTcp.ProtocolType);
//4.获取套接字的寻址方案
print(socketTcp.AddressFamily);
//5.从网络中获取准备读取的数据数据量
print(socketTcp.Available);
//6.获取本机EndPoint对象(注意 :IPEndPoint继承EndPoint)
//socketTcp.LocalEndPoint as IPEndPoint
//7.获取远程EndPoint对象
//socketTcp.RemoteEndPoint as IPEndPoint
Socket的常用方法
//1.主要用于服务端
// 1-1:绑定IP和端口
IPEndPoint ipPoint = new IPEndPoint(IPAddress.Parse("127.0.0.1"), 8080);
socketTcp.Bind(ipPoint);
// 1-2:设置客户端连接的最大数量
socketTcp.Listen(10);
// 1-3:等待客户端连入
socketTcp.Accept();
//2.主要用于客户端
// 1-1:连接远程服务端
socketTcp.Connect(IPAddress.Parse("118.12.123.11"), 8080);
//3.客户端服务端都会用的
// 1-1:同步发送和接收数据
// 1-2:异步发送和接收数据
// 1-3:释放连接并关闭Socket,先与Close调用
socketTcp.Shutdown(SocketShutdown.Both);
// 1-4:关闭连接,释放所有Socket关联资源
socketTcp.Close();
TCP服务端
TCP服务端需要做的事情
//1.创建套接字Socket
//2.用Bind方法将套接字与本地地址绑定
//3.用Listen方法监听
//4.用Accept方法等待客户端连接
//5.建立连接,Accept返回新套接字
//6.用Send和Receive相关方法收发数据
//7.用Shutdown方法释放连接
//8.关闭套接字
最基础的实现方式
服务端的 Accept、Send、Receive是会阻塞主线程的,要等到执行完毕才会继续执行后面的内容
//1.创建套接字Socket(TCP)
Socket socketTcp = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
//2.用Bind方法将套接字与本地地址绑定
try
{
IPEndPoint ipPoint = new IPEndPoint(IPAddress.Parse("127.0.0.1"), 8080);
socketTcp.Bind(ipPoint);
}
catch (Exception e)
{
Console.WriteLine("绑定报错" + e.Message);
return;
}
//3.用Listen方法监听
socketTcp.Listen(1024);
Console.WriteLine("服务端绑定监听结束,等待客户端连入");
//4.用Accept方法等待客户端连接
//5.建立连接,Accept返回新套接字
Socket socketClient = socketTcp.Accept();
Console.WriteLine("有客户端连入了");
//6.用Send和Receive相关方法收发数据
//发送
socketClient.Send(Encoding.UTF8.GetBytes("欢迎连入服务端"));
//接受
byte[] result = new byte[1024];
//返回值为接受到的字节数
int receiveNum = socketClient.Receive(result);
Console.WriteLine("接受到了{0}发来的消息:{1}",
socketClient.RemoteEndPoint.ToString(),
Encoding.UTF8.GetString(result, 0, receiveNum));
//7.用Shutdown方法释放连接
socketClient.Shutdown(SocketShutdown.Both);
//8.关闭套接字
socketClient.Close();
TCP客户端
TCP客户端需要做的事情
//1.创建套接字Socket
//2.用Connect方法与服务端相连
//3.用Send和Receive相关方法收发数据
//4.用Shutdown方法释放连接
//5.关闭套接字
实现客户端基本逻辑
//1.创建套接字Socket
Socket socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
//2.用Connect方法与服务端相连
//确定服务端的IP和端口
IPEndPoint ipPoint = new IPEndPoint(IPAddress.Parse("127.0.0.1"), 8080);
try
{
socket.Connect(ipPoint);
}
catch (SocketException e)
{
if (e.ErrorCode == 10061)
print("服务器拒绝连接");
else
print("连接服务器失败" + e.ErrorCode);
return;
}
//3.用Send和Receive相关方法收发数据
//接收数据
byte[] receiveBytes = new byte[1024];
int receiveNum = socket.Receive(receiveBytes);
print("收到服务端发来的消息:" + Encoding.UTF8.GetString(receiveBytes, 0, receiveNum));
//发送数据
socket.Send(Encoding.UTF8.GetBytes("你好,我是唐老狮的客户端"));
//4.用Shutdown方法释放连接
socket.Shutdown(SocketShutdown.Both);
//5.关闭套接字
socket.Close();
TCP常用异步通信方法(补充)
Socket TCP通信中的异步方法(Begin开头方法)
//回调函数参数IAsyncResult
//AsyncState 调用异步方法时传入的参数 需要转换
//AsyncWaitHandle 用于同步等待
Socket socketTcp = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
//服务器相关
//BeginAccept
//EndAccept
socketTcp.BeginAccept(AcceptCallBack, socketTcp);
private void AcceptCallBack(IAsyncResult result)
{
try
{
//获取传入的参数
Socket s = result.AsyncState as Socket;
//通过调用EndAccept就可以得到连入的客户端Socket
Socket clientSocket = s.EndAccept(result);
s.BeginAccept(AcceptCallBack, s);
}
catch (SocketException e)
{
print(e.SocketErrorCode);
}
}
//客户端相关
//BeginConnect
//EndConnect
IPEndPoint ipPoint = new IPEndPoint(IPAddress.Parse("127.0.0.1"), 8080);
socketTcp.BeginConnect(ipPoint, (result) =>
{
Socket s = result.AsyncState as Socket;
try
{
s.EndConnect(result);
print("连接成功");
}
catch (SocketException e)
{
print("连接出错" + e.SocketErrorCode + e.Message);
}
}, socketTcp);
//服务器客户端通用
//接收消息
//BeginReceive
//EndReceive
socketTcp.BeginReceive(resultBytes, 0, resultBytes.Length, SocketFlags.None, ReceiveCallBack, socketTcp);
private void ReceiveCallBack(IAsyncResult result)
{
try
{
Socket s = result.AsyncState as Socket;
//这个返回值是你受到了多少个字节
int num = s.EndReceive(result);
//进行消息处理
Encoding.UTF8.GetString(resultBytes, 0, num);
//我还要继续接受
s.BeginReceive(resultBytes, 0, resultBytes.Length, SocketFlags.None, ReceiveCallBack, s);
}
catch (SocketException e)
{
print("接受消息处问题" + e.SocketErrorCode + e.Message);
}
}
//发送消息
//BeginSend
//EndSend
byte[] bytes = Encoding.UTF8.GetBytes("1231231231223123123");
socketTcp.BeginSend(bytes, 0, bytes.Length, SocketFlags.None, (result) =>
{
try
{
socketTcp.EndSend(result);
print("发送成功");
}
catch (SocketException e)
{
print("发送错误" + e.SocketErrorCode + e.Message);
}
}, socketTcp);
Socket TCP通信中的异步方法2(Async结尾方法)
//关键变量类型
//SocketAsyncEventArgs
//它会作为Async异步方法的传入值
//我们需要通过它进行一些关键参数的赋值
//服务器端
//AcceptAsync
SocketAsyncEventArgs e = new SocketAsyncEventArgs();
e.Completed += (socket, args) =>
{
//首先判断是否成功
if (args.SocketError == SocketError.Success)
{
//获取连入的客户端socket
Socket clientSocket = args.AcceptSocket;
(socket as Socket).AcceptAsync(args);
}
else
{
print("连入客户端失败" + args.SocketError);
}
};
socketTcp.AcceptAsync(e);
//客户端
//ConnectAsync
SocketAsyncEventArgs e2 = new SocketAsyncEventArgs();
e2.Completed += (socket, args) =>
{
if (args.SocketError == SocketError.Success)
{
//连接成功
}
else
{
//连接失败
print(args.SocketError);
}
};
socketTcp.ConnectAsync(e2);
//服务端和客户端
//发送消息
//SendAsync
SocketAsyncEventArgs e3 = new SocketAsyncEventArgs();
byte[] bytes2 = Encoding.UTF8.GetBytes("123123的就是拉法基萨克两地分居");
e3.SetBuffer(bytes2, 0, bytes2.Length);
e3.Completed += (socket, args) =>
{
if (args.SocketError == SocketError.Success)
{
print("发送成功");
}
else
{
}
};
socketTcp.SendAsync(e3);
//接受消息
//ReceiveAsync
SocketAsyncEventArgs e4 = new SocketAsyncEventArgs();
//设置接受数据的容器,偏移位置,容量
e4.SetBuffer(new byte[1024 * 1024], 0, 1024 * 1024);
e4.Completed += (socket, args) =>
{
if(args.SocketError == SocketError.Success)
{
//收取存储在容器当中的字节
//Buffer是容器
//BytesTransferred是收取了多少个字节
Encoding.UTF8.GetString(args.Buffer, 0, args.BytesTransferred);
args.SetBuffer(0, args.Buffer.Length);
//接收完消息 再接收下一条
(socket as Socket).ReceiveAsync(args);
}
else
{
}
};
socketTcp.ReceiveAsync(e4);
//C#中网络通信 异步方法中 主要提供了两种方案
//1.Begin开头的API
//内部开多线程,通过回调形式返回结果,需要和End相关方法 配合使用
//2.Async结尾的API
//内部开多线程,通过回调形式返回结果,依赖SocketAsyncEventArgs对象配合使用
//可以让我们更加方便的进行操作
TCP客户端与服务端一般实现(同步)
问题
1.如何发送之前的自定义类的2进制信息
自己封装一个方法类,让每一个数据类继承并实现其中的方法
2.如何区分消息
//为发送的信息添加标识,比如添加消息ID
//在所有发送的消息的头部加上消息ID(int、short、byte、long都可以,根据实际情况选择)
3.如何处理分包黏包
在标识里面在添加长度信息,得到不同的长度信息之后进行不同的处理
4.客户端socket无论使用ShutDown和Close方法还是Disconnect方法主动断开连接,服务端都无法得知客户端已经断开
自定义退出消息 让服务端收到该消息就知道客户端想断开
5.客户端非正常退出时以及长期不发送消息,防火墙或者路由器会断开连接
在客户端与服务端当中实现心跳消息,固定时间产生联络
TCP服务端的实现(同步)
BaseDate为一个抽象类,封装了常用变量序列化与反序列化的方法,自定义数据类以及对应的数据管理类需继承实现其中的抽象方法,实现自定义数据的传输
using System;
using System.Collections;
using System.Collections.Generic;
using System.Text;
using UnityEngine;
public abstract class BaseData
{
/// <summary>
/// 用于子类重写的 获取字节数组容器大小的方法
/// </summary>
/// <returns></returns>
public abstract int GetBytesNum();
/// <summary>
/// 把成员变量 序列化为 对应的字节数组
/// </summary>
/// <returns></returns>
public abstract byte[] Writing();
/// <summary>
/// 把2进制字节数组 反序列化到 成员变量当中
/// </summary>
/// <param name="bytes">反序列化使用的字节数组</param>
/// <param name="beginIndex">从该字节数组的第几个位置开始解析 默认是0</param>
public abstract int Reading(byte[] bytes, int beginIndex = 0);
/// <summary>
/// 存储int类型变量到指定的字节数组当中
/// </summary>
/// <param name="bytes">指定字节数组</param>
/// <param name="value">具体的int值</param>
/// <param name="index">每次存储后用于记录当前索引位置的变量</param>
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();
}
/// <summary>
/// 根据字节数组 读取整形
/// </summary>
/// <param name="bytes">字节数组</param>
/// <param name="index">开始读取的索引数</param>
/// <returns></returns>
protected int ReadInt(byte[] bytes, ref int index)
{
int value = BitConverter.ToInt32(bytes, index);
index += sizeof(int);
return value;
}
protected short ReadShort(byte[] bytes, ref int index)
{
short value = BitConverter.ToInt16(bytes, index);
index += sizeof(short);
return value;
}
protected long ReadLong(byte[] bytes, ref int index)
{
long value = BitConverter.ToInt64(bytes, index);
index += sizeof(long);
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 += sizeof(byte);
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
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管理类 继承BaseDate 主要添加了GetID方法,方便不同处理类型的消息
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;
}
}
玩家数据类 示范数据
using System.Collections;
using System.Collections.Generic;
using System.Text;
/// <summary>
/// 玩家数据类
/// </summary>
public class PlayerData : BaseData
{
public string name;
public int atk;
public int lev;
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);
atk = ReadInt(bytes, ref index);
lev = 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, atk, ref index);
WriteInt(bytes, lev, ref index);
return bytes;
}
}
玩家管理类
using System.Collections;
using System.Collections.Generic;
public class PlayerMsg : BaseMsg
{
public int playerID;
public PlayerData playerData;
public override byte[] Writing()
{
int index = 0;
int bytesNum = GetBytesNum();
byte[] bytes = new byte[bytesNum];
//先写消息ID
WriteInt(bytes, GetID(), ref index);
//写如消息体的长度 我们-8的目的 是只存储 消息体的长度 前面8个字节 是我们自己定的规则 解析时按照这个规则处理就行了
WriteInt(bytes, bytesNum - 8, ref index);
//写这个消息的成员变量
WriteInt(bytes, playerID, ref index);
WriteData(bytes, playerData, ref index);
return bytes;
}
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 int GetBytesNum()
{
return 4 + //消息ID的长度
4 + //消息体的长度
4 + //playerID的字节数组长度
playerData.GetBytesNum();//playerData的字节数组长度
}
/// <summary>
/// 自定义的消息ID 主要用于区分是哪一个消息类
/// </summary>
/// <returns></returns>
public override int GetID()
{
return 1001;
}
}
退出消息管理类 无实质信息 当客户端退出时使用
using System.Collections;
using System.Collections.Generic;
public class QuitMsg : BaseMsg
{
public override int GetBytesNum()
{
return 8;
}
public override int Reading(byte[] bytes, int beginIndex = 0)
{
return 0;
}
public override byte[] Writing()
{
int index = 0;
byte[] bytes = new byte[GetBytesNum()];
WriteInt(bytes, GetID(), ref index);
WriteInt(bytes, 0, ref index);
return bytes;
}
public override int GetID()
{
return 1003;
}
}
心跳信息管理类 无实质信息 用于客户端与服务端的定时沟通
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class HeartMsg : BaseMsg
{
public override int GetBytesNum()
{
return 8;
}
public override int Reading(byte[] bytes, int beginIndex = 0)
{
return 0;
}
public override byte[] Writing()
{
int index = 0;
byte[] bytes = new byte[GetBytesNum()];
WriteInt(bytes, GetID(), ref index);
WriteInt(bytes, 0, ref index);
return bytes;
}
public override int GetID()
{
return 999;
}
}
服务端对于客户端Socket一些功能封装类
using System;
using System.Collections.Generic;
using System.Net.Sockets;
using System.Text;
using System.Threading;
namespace TeachTcpServerExercises2
{
class ClientSocket
{
private static int CLIENT_BEGIN_ID = 1;
public int clientID;
public Socket socket;
//用于处理分包时 缓存的 字节数组 和 字节数组长度
private byte[] cacheBytes = new byte[1024 * 1024];
private int cacheNum = 0;
//上一次收到消息的时间
private long frontTime = -1;
//超时时间
private static int TIME_OUT_TIME = 10;
public ClientSocket(Socket socket)
{
this.clientID = CLIENT_BEGIN_ID;
this.socket = socket;
++CLIENT_BEGIN_ID;
//我们现在为了方便大家理解 所以开了一个线程专门计时 但是这种方式比较消耗性能 不建议这样使用
//ThreadPool.QueueUserWorkItem(CheckTimeOut);
}
/// <summary>
/// 间隔一段时间 检测一次超时 如果超时 就会主动断开该客户端的连接
/// </summary>
/// <param name="obj"></param>
private void CheckTimeOut(/*object obj*/)
{
//while (Connected)
//{
if (frontTime != -1 &&
DateTime.Now.Ticks / TimeSpan.TicksPerSecond - frontTime >= TIME_OUT_TIME)
{
Program.socket.AddDelSocket(this);
//break;
}
//Thread.Sleep(5000);
//}
}
/// <summary>
/// 是否是连接状态
/// </summary>
public bool Connected => socket.Connected;
//我们应该封装一些方法
//关闭
public void Close()
{
if(socket != null)
{
socket.Shutdown(SocketShutdown.Both);
socket.Close();
socket = null;
}
}
//发送
public void Send(BaseMsg info)
{
if (Connected)
{
try
{
socket.Send(info.Writing());
}
catch (Exception e)
{
Console.WriteLine("发消息出错" + e.Message);
Program.socket.AddDelSocket(this);
}
}
else
Program.socket.AddDelSocket(this);
}
//接收
public void Receive()
{
if (!Connected)
{
Program.socket.AddDelSocket(this);
return;
}
try
{
if(socket.Available > 0)
{
byte[] result = new byte[1024 * 5];
int receiveNum = socket.Receive(result);
HandleReceiveMsg(result, receiveNum);
收到数据后 先读取4个字节 转为ID 才知道用哪一个类型去处理反序列化
//int msgID = BitConverter.ToInt32(result, 0);
//BaseMsg msg = null;
//switch (msgID)
//{
// case 1001:
// msg = new PlayerMsg();
// msg.Reading(result, 4);
// break;
//}
//if (msg == null)
// return;
//ThreadPool.QueueUserWorkItem(MsgHandle, msg);
}
//检测 是否超时
CheckTimeOut();
}
catch (Exception e)
{
Console.WriteLine("收消息出错" + e.Message);
//解析消息出错 也认为 要把socket断开了
Program.socket.AddDelSocket(this);
}
}
//处理接受消息 分包、黏包问题的方法
private void HandleReceiveMsg(byte[] receiveBytes, int receiveNum)
{
int msgID = 0;
int msgLength = 0;
int nowIndex = 0;
//收到消息时 应该看看 之前有没有缓存的 如果有的话 我们直接拼接到后面
receiveBytes.CopyTo(cacheBytes, cacheNum);
cacheNum += receiveNum;
while (true)
{
//每次将长度设置为-1 是避免上一次解析的数据 影响这一次的判断
msgLength = -1;
//处理解析一条消息
if (cacheNum - nowIndex >= 8)
{
//解析ID
msgID = BitConverter.ToInt32(cacheBytes, nowIndex);
nowIndex += 4;
//解析长度
msgLength = BitConverter.ToInt32(cacheBytes, nowIndex);
nowIndex += 4;
}
if (cacheNum - nowIndex >= msgLength && msgLength != -1)
{
//解析消息体
BaseMsg baseMsg = null;
switch (msgID)
{
case 1001:
baseMsg = new PlayerMsg();
baseMsg.Reading(cacheBytes, nowIndex);
break;
case 1003:
baseMsg = new QuitMsg();
//由于该消息没有消息体 所以都不用反序列化
break;
case 999:
baseMsg = new HeartMsg();
//由于该消息没有消息体 所以都不用反序列化
break;
}
if (baseMsg != null)
ThreadPool.QueueUserWorkItem(MsgHandle, baseMsg);
nowIndex += msgLength;
if (nowIndex == cacheNum)
{
cacheNum = 0;
break;
}
}
else
{
//如果不满足 证明有分包
//那么我们需要把当前收到的内容 记录下来
//有待下次接受到消息后 再做处理
//receiveBytes.CopyTo(cacheBytes, 0);
//cacheNum = receiveNum;
//如果进行了 id和长度的解析 但是 没有成功解析消息体 那么我们需要减去nowIndex移动的位置
if (msgLength != -1)
nowIndex -= 8;
//就是把剩余没有解析的字节数组内容 移到前面来 用于缓存下次继续解析
Array.Copy(cacheBytes, nowIndex, cacheBytes, 0, cacheNum - nowIndex);
cacheNum = cacheNum - nowIndex;
break;
}
}
}
private void MsgHandle(object obj)
{
BaseMsg msg = obj as BaseMsg;
if(msg is PlayerMsg)
{
PlayerMsg playerMsg = msg as PlayerMsg;
Console.WriteLine(playerMsg.playerID);
Console.WriteLine(playerMsg.playerData.name);
Console.WriteLine(playerMsg.playerData.lev);
Console.WriteLine(playerMsg.playerData.atk);
}
else if(msg is QuitMsg)
{
//收到断开连接消息 把自己添加到待移除的列表当中
Program.socket.AddDelSocket(this);
}
else if(msg is HeartMsg)
{
//收到心跳消息 记录收到消息的时间
frontTime = DateTime.Now.Ticks / TimeSpan.TicksPerSecond;
Console.WriteLine("收到心跳消息");
}
}
}
}
服务端对于服务端Socket功能的一些封装
using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
namespace TeachTcpServerExercises2
{
class ServerSocket
{
//服务端Socket
public Socket socket;
//客户端连接的所有Socket
public Dictionary<int, ClientSocket> clientDic = new Dictionary<int, ClientSocket>();
//有待移除的客户端socket 避免 在foreach时直接从字典中移除 出现问题
private List<ClientSocket> delList = new List<ClientSocket>();
private bool isClose;
//开启服务器端
public void Start(string ip, int port, int num)
{
isClose = false;
socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
IPEndPoint ipPoint = new IPEndPoint(IPAddress.Parse(ip), port);
socket.Bind(ipPoint);
socket.Listen(num);
ThreadPool.QueueUserWorkItem(Accept);
ThreadPool.QueueUserWorkItem(Receive);
}
//关闭服务器端
public void Close()
{
isClose = true;
foreach (ClientSocket client in clientDic.Values)
{
client.Close();
}
clientDic.Clear();
socket.Shutdown(SocketShutdown.Both);
socket.Close();
socket = null;
}
//接受客户端连入
private void Accept(object obj)
{
while (!isClose)
{
try
{
//连入一个客户端
Socket clientSocket = socket.Accept();
ClientSocket client = new ClientSocket(clientSocket);
lock(clientDic)
clientDic.Add(client.clientID, client);
}
catch (Exception e)
{
Console.WriteLine("客户端连入报错" + e.Message);
}
}
}
//接收客户端消息
private void Receive(object obj)
{
while (!isClose)
{
if(clientDic.Count > 0)
{
lock (clientDic)
{
foreach (ClientSocket client in clientDic.Values)
{
client.Receive();
}
CloseDelListSocket();
}
}
}
}
public void Broadcast(BaseMsg info)
{
lock (clientDic)
{
foreach (ClientSocket client in clientDic.Values)
{
client.Send(info);
}
}
}
//添加待移除的 socket内容
public void AddDelSocket(ClientSocket socket)
{
if (!delList.Contains(socket))
delList.Add(socket);
}
判断有没有 断开连接的 把其 移除
public void CloseDelListSocket()
{
//判断有没有 断开连接的 把其 移除
for (int i = 0; i < delList.Count; i++)
CloseClientSocket(delList[i]);
delList.Clear();
}
//关闭客户端连接的 从字典中移除
public void CloseClientSocket(ClientSocket socket)
{
lock (clientDic)
{
socket.Close();
if (clientDic.ContainsKey(socket.clientID))
{
clientDic.Remove(socket.clientID);
Console.WriteLine("客户端{0}主动断开连接了", socket.clientID);
}
}
}
}
}
服务端主程序
using System;
namespace TeachTcpServerExercises2
{
class Program
{
public static ServerSocket socket;
static void Main(string[] args)
{
socket = new ServerSocket();
socket.Start("127.0.0.1", 8080, 1024);
Console.WriteLine("服务器开启成功");
while (true)
{
string input = Console.ReadLine();
if(input == "Quit")
{
socket.Close();
}
else if( input.Substring(0,2) == "B:" )
{
if(input.Substring(2) == "1001")
{
PlayerMsg msg = new PlayerMsg();
msg.playerID = 9876;
msg.playerData = new PlayerData();
msg.playerData.name = "服务器端发来的消息";
msg.playerData.lev = 99;
msg.playerData.atk = 80;
socket.Broadcast(msg);
}
}
}
}
}
}
TCP客户端的实现(同步)
tcp服务端中各种消息类型也都客户端需要这边实现,重复部分省略
using System;
using System.Collections;
using System.Collections.Generic;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using UnityEngine;
public class NetMgr : MonoBehaviour
{
private static NetMgr instance;
public static NetMgr Instance => instance;
//客户端Socket
private Socket socket;
//用于发送消息的队列 公共容器 主线程往里面放 发送线程从里面取
private Queue<BaseMsg> sendMsgQueue = new Queue<BaseMsg>();
//用于接收消息的对象 公共容器 子线程往里面放 主线程从里面取
private Queue<BaseMsg> receiveQueue = new Queue<BaseMsg>();
用于收消息的水桶(容器)
//private byte[] receiveBytes = new byte[1024 * 1024];
返回收到的字节数
//private int receiveNum;
//用于处理分包时 缓存的 字节数组 和 字节数组长度
private byte[] cacheBytes = new byte[1024 * 1024];
private int cacheNum = 0;
//是否连接
private bool isConnected = false;
//发送心跳消息的间隔时间
private int SEND_HEART_MSG_TIME = 2;
private HeartMsg hearMsg = new HeartMsg();
void Awake()
{
instance = this;
DontDestroyOnLoad(this.gameObject);
//客户端循环定时给服务端发送心跳消息
InvokeRepeating("SendHeartMsg", 0, SEND_HEART_MSG_TIME);
}
private void SendHeartMsg()
{
if (isConnected)
Send(hearMsg);
}
// Update is called once per frame
void Update()
{
if(receiveQueue.Count > 0)
{
BaseMsg msg = receiveQueue.Dequeue();
if(msg is PlayerMsg)
{
PlayerMsg playerMsg = (msg as PlayerMsg);
print(playerMsg.playerID);
print(playerMsg.playerData.name);
print(playerMsg.playerData.lev);
print(playerMsg.playerData.atk);
}
}
}
//连接服务端
public void Connect(string ip, int port)
{
//如果是连接状态 直接返回
if (isConnected)
return;
if (socket == null)
socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
//连接服务端
IPEndPoint ipPoint = new IPEndPoint(IPAddress.Parse(ip), port);
try
{
socket.Connect(ipPoint);
isConnected = true;
//开启发送线程
ThreadPool.QueueUserWorkItem(SendMsg);
//开启接收线程
ThreadPool.QueueUserWorkItem(ReceiveMsg);
}
catch (SocketException e)
{
if (e.ErrorCode == 10061)
print("服务器拒绝连接");
else
print("连接失败" + e.ErrorCode + e.Message);
}
}
//发送消息
public void Send(BaseMsg msg)
{
sendMsgQueue.Enqueue(msg);
}
/// <summary>
/// 用于测试 直接发字节数组的方法
/// </summary>
/// <param name="bytes"></param>
public void SendTest(byte[] bytes)
{
socket.Send(bytes);
}
private void SendMsg(object obj)
{
while (isConnected)
{
if (sendMsgQueue.Count > 0)
{
socket.Send(sendMsgQueue.Dequeue().Writing());
}
}
}
//不停的接受消息
private void ReceiveMsg(object obj)
{
while (isConnected)
{
if(socket.Available > 0)
{
byte[] receiveBytes = new byte[1024 * 1024];
int receiveNum = socket.Receive(receiveBytes);
HandleReceiveMsg(receiveBytes, receiveNum);
首先把收到字节数组的前4个字节 读取出来得到ID
//int msgID = BitConverter.ToInt32(receiveBytes, 0);
//BaseMsg baseMsg = null;
//switch (msgID)
//{
// case 1001:
// PlayerMsg msg = new PlayerMsg();
// msg.Reading(receiveBytes, 4);
// baseMsg = msg;
// break;
//}
如果消息为空 那证明是不知道类型的消息 没有解析
//if (baseMsg == null)
// continue;
收到消息 解析消息为字符串 并放入公共容器
//receiveQueue.Enqueue(baseMsg);
}
}
}
//处理接受消息 分包、黏包问题的方法
private void HandleReceiveMsg(byte[] receiveBytes, int receiveNum)
{
int msgID = 0;
int msgLength = 0;
int nowIndex = 0;
//收到消息时 应该看看 之前有没有缓存的 如果有的话 我们直接拼接到后面
receiveBytes.CopyTo(cacheBytes, cacheNum);
cacheNum += receiveNum;
while (true)
{
//每次将长度设置为-1 是避免上一次解析的数据 影响这一次的判断
msgLength = -1;
//处理解析一条消息
if(cacheNum - nowIndex >= 8)
{
//解析ID
msgID = BitConverter.ToInt32(cacheBytes, nowIndex);
nowIndex += 4;
//解析长度
msgLength = BitConverter.ToInt32(cacheBytes, nowIndex);
nowIndex += 4;
}
if(cacheNum - nowIndex >= msgLength && msgLength != -1)
{
//解析消息体
BaseMsg baseMsg = null;
switch (msgID)
{
case 1001:
PlayerMsg msg = new PlayerMsg();
msg.Reading(cacheBytes, nowIndex);
baseMsg = msg;
break;
}
if (baseMsg != null)
receiveQueue.Enqueue(baseMsg);
nowIndex += msgLength;
if (nowIndex == cacheNum)
{
cacheNum = 0;
break;
}
}
else
{
//如果不满足 证明有分包
//那么我们需要把当前收到的内容 记录下来
//有待下次接受到消息后 再做处理
//receiveBytes.CopyTo(cacheBytes, 0);
//cacheNum = receiveNum;
//如果进行了 id和长度的解析 但是 没有成功解析消息体 那么我们需要减去nowIndex移动的位置
if (msgLength != -1)
nowIndex -= 8;
//就是把剩余没有解析的字节数组内容 移到前面来 用于缓存下次继续解析
Array.Copy(cacheBytes, nowIndex, cacheBytes, 0, cacheNum - nowIndex);
cacheNum = cacheNum - nowIndex;
break;
}
}
}
public void Close()
{
if(socket != null)
{
print("客户端主动断开连接");
//主动发送一条断开连接的消息给服务端
//QuitMsg msg = new QuitMsg();
//socket.Send(msg.Writing());
//socket.Shutdown(SocketShutdown.Both);
//socket.Disconnect(false);
//socket.Close();
socket = null;
isConnected = false;
}
}
private void OnDestroy()
{
Close();
}
}
TCP客户端与服务端一般实现(异步)
问题与同步相同,仍需考虑分包,粘包,断开连接等问题
仍需需要同步实现中所有的消息类型
只是将同步中实现方法换成异步通信方法
TCP客户端的实现(异步)
using System;
using System.Collections;
using System.Collections.Generic;
using System.Net;
using System.Net.Sockets;
using System.Text;
using UnityEngine;
public class NetAsyncMgr : MonoBehaviour
{
private static NetAsyncMgr instance;
public static NetAsyncMgr Instance => instance;
//和服务器进行连接的 Socket
private Socket socket;
//接受消息用的 缓存容器
private byte[] cacheBytes = new byte[1024 * 1024];
private int cacheNum = 0;
private Queue<BaseMsg> receiveQueue = new Queue<BaseMsg>();
//发送心跳消息的间隔时间
private int SEND_HEART_MSG_TIME = 2;
private HeartMsg hearMsg = new HeartMsg();
// Start is called before the first frame update
void Awake()
{
instance = this;
//过场景不移除
DontDestroyOnLoad(this.gameObject);
//客户端循环定时给服务端发送心跳消息
InvokeRepeating("SendHeartMsg", 0, SEND_HEART_MSG_TIME);
}
private void SendHeartMsg()
{
if (socket != null && socket.Connected)
Send(hearMsg);
}
// Update is called once per frame
void Update()
{
if (receiveQueue.Count > 0)
{
BaseMsg baseMsg = receiveQueue.Dequeue();
switch (baseMsg)
{
case PlayerMsg msg:
print(msg.playerID);
print(msg.playerData.name);
print(msg.playerData.lev);
print(msg.playerData.atk);
break;
}
}
}
//连接服务器的代码
public void Connect(string ip, int port)
{
if (socket != null && socket.Connected)
return;
IPEndPoint ipPoint = new IPEndPoint(IPAddress.Parse(ip), port);
socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
SocketAsyncEventArgs args = new SocketAsyncEventArgs();
args.RemoteEndPoint = ipPoint;
args.Completed += (socket, args) =>
{
if(args.SocketError == SocketError.Success)
{
print("连接成功");
//收消息
SocketAsyncEventArgs receiveArgs = new SocketAsyncEventArgs();
receiveArgs.SetBuffer(cacheBytes, 0, cacheBytes.Length);
receiveArgs.Completed += ReceiveCallBack;
this.socket.ReceiveAsync(receiveArgs);
}
else
{
print("连接失败" + args.SocketError);
}
};
socket.ConnectAsync(args);
}
//收消息完成的回调函数
private void ReceiveCallBack(object obj, SocketAsyncEventArgs args)
{
if(args.SocketError == SocketError.Success)
{
HandleReceiveMsg(args.BytesTransferred);
//继续去收消息
args.SetBuffer(cacheNum, args.Buffer.Length - cacheNum);
//继续异步收消息
if (this.socket != null && this.socket.Connected)
socket.ReceiveAsync(args);
else
Close();
}
else
{
print("接受消息出错" + args.SocketError);
//关闭客户端连接
Close();
}
}
public void Close()
{
if(socket != null)
{
QuitMsg msg = new QuitMsg();
socket.Send(msg.Writing());
socket.Shutdown(SocketShutdown.Both);
socket.Disconnect(false);
socket.Close();
socket = null;
}
}
public void SendTest(byte[] bytes)
{
SocketAsyncEventArgs args = new SocketAsyncEventArgs();
args.SetBuffer(bytes, 0, bytes.Length);
args.Completed += (socket, args) =>
{
if (args.SocketError != SocketError.Success)
{
print("发送消息失败" + args.SocketError);
Close();
}
};
this.socket.SendAsync(args);
}
public void Send(BaseMsg msg)
{
if(this.socket != null && this.socket.Connected)
{
byte[] bytes = msg.Writing();
SocketAsyncEventArgs args = new SocketAsyncEventArgs();
args.SetBuffer(bytes, 0, bytes.Length);
args.Completed += (socket, args) =>
{
if (args.SocketError != SocketError.Success)
{
print("发送消息失败" + args.SocketError);
Close();
}
};
this.socket.SendAsync(args);
}
else
{
Close();
}
}
//处理接受消息 分包、黏包问题的方法
private void HandleReceiveMsg(int receiveNum)
{
int msgID = 0;
int msgLength = 0;
int nowIndex = 0;
cacheNum += receiveNum;
while (true)
{
//每次将长度设置为-1 是避免上一次解析的数据 影响这一次的判断
msgLength = -1;
//处理解析一条消息
if (cacheNum - nowIndex >= 8)
{
//解析ID
msgID = BitConverter.ToInt32(cacheBytes, nowIndex);
nowIndex += 4;
//解析长度
msgLength = BitConverter.ToInt32(cacheBytes, nowIndex);
nowIndex += 4;
}
if (cacheNum - nowIndex >= msgLength && msgLength != -1)
{
//解析消息体
BaseMsg baseMsg = null;
switch (msgID)
{
case 1001:
baseMsg = new PlayerMsg();
baseMsg.Reading(cacheBytes, nowIndex);
break;
}
if (baseMsg != null)
receiveQueue.Enqueue(baseMsg);
nowIndex += msgLength;
if (nowIndex == cacheNum)
{
cacheNum = 0;
break;
}
}
else
{
if (msgLength != -1)
nowIndex -= 8;
//就是把剩余没有解析的字节数组内容 移到前面来 用于缓存下次继续解析
Array.Copy(cacheBytes, nowIndex, cacheBytes, 0, cacheNum - nowIndex);
cacheNum = cacheNum - nowIndex;
break;
}
}
}
private void OnDestroy()
{
Close();
}
}
TCP服务端的实现(异步)
客户端socket功能的封装
using System;
using System.Collections.Generic;
using System.Net.Sockets;
using System.Text;
using System.Threading;
namespace TeachTcpServerAsync
{
class ClientSocket
{
public Socket socket;
public int clientID;
private static int CLIENT_BEGIN_ID = 1;
private byte[] cacheBytes = new byte[1024];
private int cacheNum = 0;
//上一次收到消息的时间
private long frontTime = -1;
//超时时间
private static int TIME_OUT_TIME = 10;
public ClientSocket(Socket socket)
{
this.clientID = CLIENT_BEGIN_ID++;
this.socket = socket;
//开始收消息
this.socket.BeginReceive(cacheBytes, cacheNum, cacheBytes.Length, SocketFlags.None, ReceiveCallBack, null);
ThreadPool.QueueUserWorkItem(CheckTimeOut);
}
/// <summary>
/// 间隔一段时间 检测一次超时 如果超时 就会主动断开该客户端的连接
/// </summary>
/// <param name="obj"></param>
private void CheckTimeOut(object obj)
{
while (this.socket != null && this.socket.Connected)
{
if (frontTime != -1 &&
DateTime.Now.Ticks / TimeSpan.TicksPerSecond - frontTime >= TIME_OUT_TIME)
{
Program.serverSocket.CloseClientSocket(this);
break;
}
Thread.Sleep(5000);
}
}
private void ReceiveCallBack(IAsyncResult result)
{
try
{
if (this.socket != null && this.socket.Connected)
{
//消息成功
int num = this.socket.EndReceive(result);
//处理分包黏包
HandleReceiveMsg(num);
this.socket.BeginReceive(cacheBytes, cacheNum, cacheBytes.Length - cacheNum, SocketFlags.None, ReceiveCallBack, this.socket);
}
else
{
Console.WriteLine("没有连接,不用再收消息了");
Program.serverSocket.CloseClientSocket(this);
}
}
catch (SocketException e)
{
Console.WriteLine("接受消息错误" + e.SocketErrorCode + e.Message);
Program.serverSocket.CloseClientSocket(this);
}
}
public void Send(BaseMsg msg)
{
if(socket != null && socket.Connected)
{
byte[] bytes = msg.Writing();
socket.BeginSend(bytes, 0, bytes.Length, SocketFlags.None, SendCallBack, null);
}
else
{
Program.serverSocket.CloseClientSocket(this);
}
}
private void SendCallBack(IAsyncResult result)
{
try
{
if (socket != null && socket.Connected)
this.socket.EndSend(result);
else
Program.serverSocket.CloseClientSocket(this);
}
catch (SocketException e)
{
Console.WriteLine("发送失败" + e.SocketErrorCode + e.Message);
Program.serverSocket.CloseClientSocket(this);
}
}
public void Close()
{
if(socket != null)
{
socket.Shutdown(SocketShutdown.Both);
socket.Close();
socket = null;
}
}
//处理分包黏包
private void HandleReceiveMsg(int receiveNum)
{
int msgID = 0;
int msgLength = 0;
int nowIndex = 0;
//由于消息接收后是直接存储在 cacheBytes中的 所以不需要进行什么拷贝操作
//收到消息的字节数量
cacheNum += receiveNum;
while (true)
{
//每次将长度设置为-1 是避免上一次解析的数据 影响这一次的判断
msgLength = -1;
//处理解析一条消息
if (cacheNum - nowIndex >= 8)
{
//解析ID
msgID = BitConverter.ToInt32(cacheBytes, nowIndex);
nowIndex += 4;
//解析长度
msgLength = BitConverter.ToInt32(cacheBytes, nowIndex);
nowIndex += 4;
}
if (cacheNum - nowIndex >= msgLength && msgLength != -1)
{
//解析消息体
BaseMsg baseMsg = null;
switch (msgID)
{
case 1001:
baseMsg = new PlayerMsg();
baseMsg.Reading(cacheBytes, nowIndex);
break;
case 1003:
baseMsg = new QuitMsg();
//由于该消息没有消息体 所以都不用反序列化
break;
case 999:
baseMsg = new HeartMsg();
//由于该消息没有消息体 所以都不用反序列化
break;
}
if (baseMsg != null)
ThreadPool.QueueUserWorkItem(MsgHandle, baseMsg);
nowIndex += msgLength;
if (nowIndex == cacheNum)
{
cacheNum = 0;
break;
}
}
else
{
//如果不满足 证明有分包
//那么我们需要把当前收到的内容 记录下来
//有待下次接受到消息后 再做处理
//receiveBytes.CopyTo(cacheBytes, 0);
//cacheNum = receiveNum;
//如果进行了 id和长度的解析 但是 没有成功解析消息体 那么我们需要减去nowIndex移动的位置
if (msgLength != -1)
nowIndex -= 8;
//就是把剩余没有解析的字节数组内容 移到前面来 用于缓存下次继续解析
Array.Copy(cacheBytes, nowIndex, cacheBytes, 0, cacheNum - nowIndex);
cacheNum = cacheNum - nowIndex;
break;
}
}
}
//消息处理
private void MsgHandle(object obj)
{
switch (obj)
{
case PlayerMsg msg:
PlayerMsg playerMsg = msg as PlayerMsg;
Console.WriteLine(playerMsg.playerID);
Console.WriteLine(playerMsg.playerData.name);
Console.WriteLine(playerMsg.playerData.lev);
Console.WriteLine(playerMsg.playerData.atk);
break;
case QuitMsg msg:
//收到断开连接消息 把自己添加到待移除的列表当中
Program.serverSocket.CloseClientSocket(this);
break;
case HeartMsg msg:
//收到心跳消息 记录收到消息的时间
frontTime = DateTime.Now.Ticks / TimeSpan.TicksPerSecond;
Console.WriteLine("收到心跳消息");
break;
}
}
}
}
服务端socket功能的封装
using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Sockets;
using System.Text;
namespace TeachTcpServerAsync
{
class ServerSocket
{
private Socket socket;
private Dictionary<int, ClientSocket> clientDic = new Dictionary<int, ClientSocket>();
public void Start(string ip, int port, int num)
{
socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
IPEndPoint ipPoint = new IPEndPoint(IPAddress.Parse(ip), port);
try
{
socket.Bind(ipPoint);
socket.Listen(num);
//通过异步接受客户端连入
socket.BeginAccept(AcceptCallBack, null);
}
catch (Exception e)
{
Console.WriteLine("启动服务器失败" + e.Message);
}
}
private void AcceptCallBack(IAsyncResult result)
{
try
{
//获取连入的客户端
Socket clientSocket = socket.EndAccept(result);
ClientSocket client = new ClientSocket(clientSocket);
//记录客户端对象
clientDic.Add(client.clientID, client);
//继续去让别的客户端可以连入
socket.BeginAccept(AcceptCallBack, null);
}
catch (Exception e)
{
Console.WriteLine("客户端连入失败" + e.Message);
}
}
public void Broadcast(BaseMsg msg)
{
foreach (ClientSocket client in clientDic.Values)
{
client.Send(msg);
}
}
//关闭客户端连接的 从字典中移除
public void CloseClientSocket(ClientSocket socket)
{
lock (clientDic)
{
socket.Close();
if (clientDic.ContainsKey(socket.clientID))
{
clientDic.Remove(socket.clientID);
Console.WriteLine("客户端{0}主动断开连接了", socket.clientID);
}
}
}
}
}
主程序
using System;
namespace TeachTcpServerAsync
{
class Program
{
public static ServerSocket serverSocket;
static void Main(string[] args)
{
serverSocket = new ServerSocket();
serverSocket.Start("127.0.0.1", 8080, 1024);
Console.WriteLine("开启服务器成功");
while (true)
{
string input = Console.ReadLine();
if (input.Substring(2) == "1001")
{
PlayerMsg msg = new PlayerMsg();
msg.playerID = 9876;
msg.playerData = new PlayerData();
msg.playerData.name = "服务器端发来的消息";
msg.playerData.lev = 99;
msg.playerData.atk = 80;
serverSocket.Broadcast(msg);
}
}
}
}
}
UDP服务端
UDP服务端基本逻辑
using System;
using System.Net;
using System.Net.Sockets;
using System.Text;
namespace TeachUdpServer
{
class Program
{
static void Main(string[] args)
{
#region 实现UDP服务端通信 收发字符串
//1.创建套接字
Socket socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
//2.绑定本机地址
IPEndPoint ipPoint = new IPEndPoint(IPAddress.Parse("127.0.0.1"), 8081);
socket.Bind(ipPoint);
Console.WriteLine("服务器开启");
//3.接受消息
byte[] bytes = new byte[512];
//这个变量主要是用来记录 谁发的信息给你 传入函数后 在内部 它会帮助我们进行赋值
EndPoint remoteIpPoint2 = new IPEndPoint(IPAddress.Any, 0);
int length = socket.ReceiveFrom(bytes, ref remoteIpPoint2);
Console.WriteLine("IP:" + (remoteIpPoint2 as IPEndPoint).Address.ToString() +
"port:" + (remoteIpPoint2 as IPEndPoint).Port +
"发来了" +
Encoding.UTF8.GetString(bytes, 0, length));
//4.发送到指定目标
//由于我们先收 所以 我们已经知道谁发了消息给我 我直接发给它就行了
socket.SendTo(Encoding.UTF8.GetBytes("欢迎发送消息给服务器"), remoteIpPoint2);
//5.释放关闭
socket.Shutdown(SocketShutdown.Both);
socket.Close();
#endregion
Console.ReadKey();
}
}
}
UDP客户端
UDP客户端基本逻辑
using System.Collections;
using System.Collections.Generic;
using System.Net;
using System.Net.Sockets;
using System.Text;
using UnityEngine;
public class Lesson14 : MonoBehaviour
{
// Start is called before the first frame update
void Start()
{
#region 实现UDP客户端通信 收发字符串
//1.创建套接字
Socket socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
//2.绑定本机地址
IPEndPoint ipPoint = new IPEndPoint(IPAddress.Parse("127.0.0.1"), 8080);
socket.Bind(ipPoint);
//3.发送到指定目标
IPEndPoint remoteIpPoint = new IPEndPoint(IPAddress.Parse("127.0.0.1"), 8081);
//指定要发送的字节数 和 远程计算机的 IP和端口
socket.SendTo(Encoding.UTF8.GetBytes("来了"), remoteIpPoint);
//4.接受消息
byte[] bytes = new byte[512];
//这个变量主要是用来记录 谁发的信息给你 传入函数后 在内部 它会帮助我们进行赋值
EndPoint remoteIpPoint2 = new IPEndPoint(IPAddress.Any, 0);
int length = socket.ReceiveFrom(bytes, ref remoteIpPoint2);
print("IP:" + (remoteIpPoint2 as IPEndPoint).Address.ToString() +
"port:" + (remoteIpPoint2 as IPEndPoint).Port +
"发来了" +
Encoding.UTF8.GetString(bytes, 0, length));
//5.释放关闭
socket.Shutdown(SocketShutdown.Both);
socket.Close();
#endregion
}
// Update is called once per frame
void Update()
{
}
}
UDP异步通信常用方法
using System;
using System.Collections;
using System.Collections.Generic;
using System.Net;
using System.Net.Sockets;
using System.Text;
using UnityEngine;
public class Lesson16 : MonoBehaviour
{
private byte[] cacheBytes = new byte[512];
// Start is called before the first frame update
void Start()
{
#region UDP通信中Begin相关异步方法
Socket socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
//BeginSendTo
byte[] bytes = Encoding.UTF8.GetBytes("123123lkdsajlfjas");
EndPoint ipPoint = new IPEndPoint(IPAddress.Parse("127.0.0.1"), 8080);
socket.BeginSendTo(bytes, 0, bytes.Length, SocketFlags.None, ipPoint, SendToOver, socket);
private void SendToOver(IAsyncResult result)
{
try
{
Socket s = result.AsyncState as Socket;
s.EndSendTo(result);
print("发送成功");
}
catch (SocketException s)
{
print("发送失败" + s.SocketErrorCode + s.Message);
}
}
//BeginReceiveFrom
socket.BeginReceiveFrom(cacheBytes, 0, cacheBytes.Length, SocketFlags.None, ref ipPoint, ReceiveFromOver, (socket, ipPoint));
private void ReceiveFromOver(IAsyncResult result)
{
try
{
(Socket s, EndPoint ipPoint) info = ((Socket, EndPoint))result.AsyncState;
//返回值 就是接收了多少个 字节数
int num = info.s.EndReceiveFrom(result, ref info.ipPoint);
//处理消息
//处理完消息 又继续接受消息
info.s.BeginReceiveFrom(cacheBytes, 0, cacheBytes.Length, SocketFlags.None, ref info.ipPoint, ReceiveFromOver, info);
}
catch (SocketException s)
{
print("接受消息出问题" + s.SocketErrorCode + s.Message);
}
}
#endregion
#region UDP通信中Async相关异步方法
//SendToAsync
SocketAsyncEventArgs args = new SocketAsyncEventArgs();
//设置要发送的数据
args.SetBuffer(bytes, 0, bytes.Length);
//设置完成事件
args.Completed += SendToAsync;
socket.SendToAsync(args);
private void SendToAsync(object s, SocketAsyncEventArgs args)
{
if (args.SocketError == SocketError.Success)
{
print("发送成功");
}
else
{
print("发送失败");
}
}
//ReceiveFromAsync
SocketAsyncEventArgs args2 = new SocketAsyncEventArgs();
//这是设置接受消息的容器
args2.SetBuffer(cacheBytes, 0, cacheBytes.Length);
args2.Completed += ReceiveFromAsync;
socket.ReceiveFromAsync(args2);
private void ReceiveFromAsync(object s, SocketAsyncEventArgs args)
{
if (args.SocketError == SocketError.Success)
{
print("接收成功");
//具体收了多少个字节
//args.BytesTransferred
//可以通过以下两种方式获取到收到的字节数组内容
//args.Buffer
//cacheBytes
//解析消息
Socket socket = s as Socket;
//只需要设置 从第几个位置开始接 能接多少
args.SetBuffer(0, cacheBytes.Length);
socket.ReceiveFromAsync(args);
}
else
{
print("接收失败");
}
}
#endregion
}
}
UDP客户端与服务端一般实现(同步)
问题
TCP需要考虑的问题UDP一般也要考虑,但是UDP不需要考虑粘包问题
其余思路与TCP大致相同,细节部分有所不同
UDP客户端实现(同步)
using System;
using System.Collections;
using System.Collections.Generic;
using System.Net;
using System.Net.Sockets;
using System.Threading;
using UnityEngine;
public class UdpNetMgr : MonoBehaviour
{
private static UdpNetMgr instance;
public static UdpNetMgr Instance => instance;
private EndPoint serverIpPoint;
private Socket socket;
//客户端socket是否关闭
private bool isClose = true;
//两个容器 队列
//接受和发送消息的队列 在多线程里面可以操作
private Queue<BaseMsg> sendQueue = new Queue<BaseMsg>();
private Queue<BaseMsg> receiveQueue = new Queue<BaseMsg>();
private byte[] cacheBytes = new byte[512];
// Start is called before the first frame update
void Awake()
{
instance = this;
DontDestroyOnLoad(this.gameObject);
}
// Update is called once per frame
void Update()
{
if(receiveQueue.Count > 0)
{
BaseMsg baseMsg = receiveQueue.Dequeue();
switch (baseMsg)
{
case PlayerMsg msg:
print(msg.playerID);
print(msg.playerData.name);
print(msg.playerData.atk);
print(msg.playerData.lev);
break;
}
}
}
/// <summary>
/// 启动客户端socket相关的方法
/// </summary>
/// <param name="ip">远端服务器的IP</param>
/// <param name="port">远端服务器的port</param>
public void StartClient(string ip, int port)
{
//如果当前是开启状态 就不用再开了
if (!isClose)
return;
//先记录服务器地址,一会发消息时会使用
serverIpPoint = new IPEndPoint(IPAddress.Parse(ip), port);
IPEndPoint clientIpPort = new IPEndPoint(IPAddress.Parse("127.0.0.1"), 8081);
try
{
socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
socket.Bind(clientIpPort);
isClose = false;
print("客户端网络启动");
ThreadPool.QueueUserWorkItem(ReceiveMsg);
ThreadPool.QueueUserWorkItem(SendMsg);
}
catch (System.Exception e)
{
print("启动Socket出问题" + e.Message);
}
}
private void ReceiveMsg(object obj)
{
EndPoint tempIpPoint = new IPEndPoint(IPAddress.Any, 0);
int nowIndex;
int msgID;
int msgLength;
while (!isClose)
{
if(socket != null && socket.Available > 0)
{
try
{
socket.ReceiveFrom(cacheBytes, ref tempIpPoint);
//为了避免处理 非服务器发来的 骚扰消息
if(!tempIpPoint.Equals(serverIpPoint))
continue;//如果发现 发消息给你的 不是服务器 那么证明是骚扰消息 就不用处理
//处理服务器发来的消息
nowIndex = 0;
//解析ID
msgID = BitConverter.ToInt32(cacheBytes, nowIndex);
nowIndex += 4;
//解析长度
msgLength = BitConverter.ToInt32(cacheBytes, nowIndex);
nowIndex += 4;
//解析消息体
BaseMsg msg = null;
switch (msgID)
{
case 1001:
msg = new PlayerMsg();
//反序列化消息体
msg.Reading(cacheBytes, nowIndex);
break;
}
if (msg != null)
receiveQueue.Enqueue(msg);
}
catch (SocketException s)
{
print("接受消息出问题" + s.SocketErrorCode + s.Message);
}
catch (Exception e)
{
print("接受消息出问题(非网络问题)" + e.Message);
}
}
}
}
private void SendMsg(object obj)
{
while (!isClose)
{
if (socket != null && sendQueue.Count > 0)
{
try
{
socket.SendTo(sendQueue.Dequeue().Writing(), serverIpPoint);
}
catch (SocketException s)
{
print("发送消息出错" + s.SocketErrorCode + s.Message);
}
}
}
}
//发送消息
public void Send(BaseMsg msg)
{
sendQueue.Enqueue(msg);
}
//关闭socket
public void Close()
{
if(socket != null)
{
isClose = true;
QuitMsg msg = new QuitMsg();
//发送一个退出消息给服务器 让其移除记录
socket.SendTo(msg.Writing(), serverIpPoint);
socket.Shutdown(SocketShutdown.Both);
socket.Close();
socket = null;
}
}
private void OnDestroy()
{
Close();
}
}
UDP服务端实现(同步)
客户端socket功能封装
using System;
using System.Collections.Generic;
using System.Net;
using System.Text;
using System.Threading;
namespace TeachUdpServerExercises
{
//它是用于记录和服务器通信过的客户端的IP和端口
class Client
{
public IPEndPoint clientIPandPort;
public string clientStrID;
//上一次收到消息的时间
public long frontTime = -1;
public Client(string ip, int port)
{
//规则和外面一样 记录唯一ID 通过 ip + port 拼接的形式
clientStrID = ip + port;
//就把客户端的信息记录下来了
clientIPandPort = 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(ReceiveHandle, cacheBytes);
}
//多线程处理消息
private void ReceiveHandle(object obj)
{
try
{
//取出传进来的字节
byte[] bytes = obj as byte[];
int nowIndex = 0;
//先处理 ID
int msgID = BitConverter.ToInt32(bytes, nowIndex);
nowIndex += 4;
//再处理 长度
int msgLength = 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.name);
Console.WriteLine(playerMsg.playerData.atk);
Console.WriteLine(playerMsg.playerData.lev);
break;
case 1003:
QuitMsg quitMsg = new QuitMsg();
//由于它没有消息体 所以不用反序列化
//quitMsg.Reading(bytes, nowIndex);
//处理退出
Program.serverSocket.RemoveClient(clientStrID);
break;
}
}
catch (Exception e)
{
Console.WriteLine("处理消息时出错" + e.Message);
//如果出错 就不用记录这个客户端信息
Program.serverSocket.RemoveClient(clientStrID);
}
}
}
}
服务端socket功能封装
using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
namespace TeachUdpServerExercises
{
class ServerSocket
{
private Socket socket;
private bool isClose;
//我们可以通过记录谁给我发了消息 把它的 ip和端口记下来 这样就认为它是我的客户端了嘛
private Dictionary<string, Client> clientDic = new Dictionary<string, Client>();
public void Start(string ip, int port)
{
IPEndPoint ipPoint = new IPEndPoint(IPAddress.Parse(ip), port);
//声明一个用于UDP通信的Socket
socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
try
{
socket.Bind(ipPoint);
isClose = false;
//消息接收的处理
ThreadPool.QueueUserWorkItem(ReceiveMsg);
//定时检测超时线程
ThreadPool.QueueUserWorkItem(CheckTimeOut);
}
catch (Exception e)
{
Console.WriteLine("UDP开启出错" + e.Message);
}
}
private void CheckTimeOut(object obj)
{
long nowTime = 0;
List<string> delList = new List<string>();
while (true)
{
//每30s检测一次 是否移除长时间没有接收到消息的客户端信息
Thread.Sleep(30000);
//得到当前系统时间
nowTime = DateTime.Now.Ticks / TimeSpan.TicksPerSecond;
foreach (Client c in clientDic.Values)
{
//超过10秒没有收到消息的 客户端信息 需要被移除
if (nowTime - c.frontTime >= 10)
delList.Add(c.clientStrID);
}
//从待删除列表中移除 超时的客户端信息
for (int i = 0; i < delList.Count; i++)
RemoveClient(delList[i]);
delList.Clear();
}
}
private void ReceiveMsg(object obj)
{
//接收消息的容器
byte[] bytes = new byte[512];
//记录谁发的
EndPoint ipPoint = new IPEndPoint(IPAddress.Any, 0);
//用于拼接字符串 位移ID 是由 IP + 端口构成的
string strID = "";
string ip;
int port;
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);
}
}
public void Broadcast(BaseMsg msg)
{
//广播消息 给谁广播
foreach (Client c in clientDic.Values)
{
SendTo(msg, c.clientIPandPort);
}
}
public void Close()
{
if(socket != null)
{
isClose = true;
socket.Shutdown(SocketShutdown.Both);
socket.Close();
socket = null;
}
}
public void RemoveClient(string clientID)
{
if(clientDic.ContainsKey(clientID))
{
Console.WriteLine("客户端{0}被移除了" + clientDic[clientID].clientIPandPort);
clientDic.Remove(clientID);
}
}
}
}
主程序
using System;
namespace TeachUdpServerExercises
{
class Program
{
public static ServerSocket serverSocket;
static void Main(string[] args)
{
serverSocket = new ServerSocket();
serverSocket.Start("127.0.0.1", 8080);
Console.WriteLine("UDP服务器启动了");
while (true)
{
string input = Console.ReadLine();
if(input.Substring(0,2) == "B:")
{
PlayerMsg msg = new PlayerMsg();
msg.playerData = new PlayerData();
msg.playerID = 1001;
msg.playerData.name = "UDP服务器";
msg.playerData.atk = 88;
msg.playerData.lev = 66;
serverSocket.Broadcast(msg);
}
}
#endregion
}
}
}
UDP客户端与服务端一般实现(异步)
UDP客户端实现(异步)
using System;
using System.Collections;
using System.Collections.Generic;
using System.Net;
using System.Net.Sockets;
using UnityEngine;
public class UdpAsyncNetMgr : MonoBehaviour
{
private static UdpAsyncNetMgr instance;
public static UdpAsyncNetMgr Instance => instance;
private EndPoint serverIpPoint;
private Socket socket;
//客户端socket是否关闭
private bool isClose = true;
//发送消息的队列 在多线程里面可以操作
private Queue<BaseMsg> receiveQueue = new Queue<BaseMsg>();
private byte[] cacheBytes = new byte[512];
// Start is called before the first frame update
void Awake()
{
instance = this;
DontDestroyOnLoad(this.gameObject);
}
// Update is called once per frame
void Update()
{
if (receiveQueue.Count > 0)
{
BaseMsg baseMsg = receiveQueue.Dequeue();
switch (baseMsg)
{
case PlayerMsg msg:
print(msg.playerID);
print(msg.playerData.name);
print(msg.playerData.atk);
print(msg.playerData.lev);
break;
}
}
}
/// <summary>
/// 启动客户端socket相关的方法
/// </summary>
/// <param name="ip">远端服务器的IP</param>
/// <param name="port">远端服务器的port</param>
public void StartClient(string ip, int port)
{
//如果当前是开启状态 就不用再开了
if (!isClose)
return;
//先记录服务器地址,一会发消息时会使用
serverIpPoint = new IPEndPoint(IPAddress.Parse(ip), port);
IPEndPoint clientIpPort = new IPEndPoint(IPAddress.Parse("127.0.0.1"), 8081);
try
{
socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
socket.Bind(clientIpPort);
isClose = false;
SocketAsyncEventArgs args = new SocketAsyncEventArgs();
args.SetBuffer(cacheBytes, 0, cacheBytes.Length);
args.RemoteEndPoint = new IPEndPoint(IPAddress.Any, 0);
args.Completed += ReceiveMsg;
socket.ReceiveFromAsync(args);
print("客户端网络启动");
}
catch (System.Exception e)
{
print("启动Socket出问题" + e.Message);
}
}
private void ReceiveMsg(object obj, SocketAsyncEventArgs args)
{
int nowIndex;
int msgID;
int msgLength;
if(args.SocketError == SocketError.Success)
{
try
{
//要是服务器发的才处理
if (args.RemoteEndPoint.Equals(serverIpPoint))
{
//处理服务器发来的消息
nowIndex = 0;
//解析ID
msgID = BitConverter.ToInt32(args.Buffer, nowIndex);
nowIndex += 4;
//解析长度
msgLength = BitConverter.ToInt32(args.Buffer, nowIndex);
nowIndex += 4;
//解析消息体
BaseMsg msg = null;
switch (msgID)
{
case 1001:
msg = new PlayerMsg();
//反序列化消息体
msg.Reading(args.Buffer, nowIndex);
break;
}
if (msg != null)
receiveQueue.Enqueue(msg);
}
//再次接收消息
if (socket != null && !isClose)
{
args.SetBuffer(0, cacheBytes.Length);
socket.ReceiveFromAsync(args);
}
}
catch (SocketException s)
{
print("接收消息出错" + s.SocketErrorCode + s.Message);
Close();
}
catch (Exception e)
{
print("接收消息出错(可能是反序列化问题)" + e.Message);
Close();
}
}
else
{
print("接收消息失败" + args.SocketError);
}
}
//发送消息
public void Send(BaseMsg msg)
{
try
{
if(socket != null && !isClose)
{
SocketAsyncEventArgs args = new SocketAsyncEventArgs();
byte[] bytes = msg.Writing();
args.SetBuffer(bytes, 0, bytes.Length);
args.Completed += SendToCallBack;
//设置远端目标
args.RemoteEndPoint = serverIpPoint;
socket.SendToAsync(args);
}
}
catch (SocketException s)
{
print("发送消息出错" + s.SocketErrorCode + s.Message);
}
catch (Exception e)
{
print("发送消息出错(可能是序列化问题)" + e.Message);
}
}
private void SendToCallBack(object o, SocketAsyncEventArgs args)
{
if (args.SocketError != SocketError.Success)
print("发送消息失败" + args.SocketError);
}
//关闭socket
public void Close()
{
if (socket != null)
{
isClose = true;
QuitMsg msg = new QuitMsg();
//发送一个退出消息给服务器 让其移除记录
socket.SendTo(msg.Writing(), serverIpPoint);
socket.Shutdown(SocketShutdown.Both);
socket.Close();
socket = null;
}
}
private void OnDestroy()
{
Close();
}
}
UDP服务端实现(异步)
客户端socke功能实现
using System;
using System.Collections.Generic;
using System.Net;
using System.Text;
using System.Threading;
namespace TeachUdpAsyncServerExercises
{
class Client
{
public IPEndPoint clientIPandPort;
public string clientStrID;
//上一次收到消息的时间
public long frontTime = -1;
public Client(string ip, int port)
{
//规则和外面一样 记录唯一ID 通过 ip + port 拼接的形式
clientStrID = ip + port;
//就把客户端的信息记录下来了
clientIPandPort = 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(ReceiveHandle, cacheBytes);
}
//多线程处理消息
private void ReceiveHandle(object obj)
{
try
{
//取出传进来的字节
byte[] bytes = obj as byte[];
int nowIndex = 0;
//先处理 ID
int msgID = BitConverter.ToInt32(bytes, nowIndex);
nowIndex += 4;
//再处理 长度
int msgLength = 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.name);
Console.WriteLine(playerMsg.playerData.atk);
Console.WriteLine(playerMsg.playerData.lev);
break;
case 1003:
QuitMsg quitMsg = new QuitMsg();
//由于它没有消息体 所以不用反序列化
//quitMsg.Reading(bytes, nowIndex);
//处理退出
Program.serverSocket.RemoveClient(clientStrID);
break;
}
}
catch (Exception e)
{
Console.WriteLine("处理消息时出错" + e.Message);
//如果出错 就不用记录这个客户端信息
Program.serverSocket.RemoveClient(clientStrID);
}
}
}
}
服务端socket功能实现
using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
namespace TeachUdpAsyncServerExercises
{
class ServerSocket
{
private Socket socket;
private bool isClose;
//我们可以通过记录谁给我发了消息 把它的 ip和端口记下来 这样就认为它是我的客户端了嘛
private Dictionary<string, Client> clientDic = new Dictionary<string, Client>();
//用于接收消息的容器
private byte[] cacheBytes = new byte[512];
public void Start(string ip, int port)
{
EndPoint ipPoint = new IPEndPoint(IPAddress.Parse(ip), port);
//声明一个用于UDP通信的Socket
socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
try
{
socket.Bind(ipPoint);
isClose = false;
//消息接收的处理
socket.BeginReceiveFrom(cacheBytes, 0, cacheBytes.Length, SocketFlags.None, ref ipPoint, ReceiveMsg, ipPoint);
//定时检测超时线程
ThreadPool.QueueUserWorkItem(CheckTimeOut);
}
catch (Exception e)
{
Console.WriteLine("UDP开启出错" + e.Message);
}
}
private void CheckTimeOut(object obj)
{
long nowTime = 0;
List<string> delList = new List<string>();
while (true)
{
//每30s检测一次 是否移除长时间没有接收到消息的客户端信息
Thread.Sleep(30000);
//得到当前系统时间
nowTime = DateTime.Now.Ticks / TimeSpan.TicksPerSecond;
foreach (Client c in clientDic.Values)
{
//超过10秒没有收到消息的 客户端信息 需要被移除
if (nowTime - c.frontTime >= 10)
delList.Add(c.clientStrID);
}
//从待删除列表中移除 超时的客户端信息
for (int i = 0; i < delList.Count; i++)
RemoveClient(delList[i]);
delList.Clear();
}
}
private void ReceiveMsg(IAsyncResult result)
{
//接收消息的容器
//记录谁发的
//用于拼接字符串 位移ID 是由 IP + 端口构成的
EndPoint ipPoint = result.AsyncState as IPEndPoint;
string ip = (ipPoint as IPEndPoint).Address.ToString();
int port = (ipPoint as IPEndPoint).Port;
string strID = ip + port;//拼接成一个唯一ID 这个是我们自定义的规则
try
{
socket.EndReceiveFrom(result, ref ipPoint);
//判断有没有记录这个客户端信息 如果有 用它直接处理消息
if (clientDic.ContainsKey(strID))
clientDic[strID].ReceiveMsg(cacheBytes);
else//如果没有 直接添加并且处理消息
{
clientDic.Add(strID, new Client(ip, port));
clientDic[strID].ReceiveMsg(cacheBytes);
}
//继续接受消息
socket.BeginReceiveFrom(cacheBytes, 0, cacheBytes.Length, SocketFlags.None, ref ipPoint, ReceiveMsg, ipPoint);
}
catch (SocketException s)
{
Console.WriteLine("接受消息出错" + s.SocketErrorCode + s.Message);
}
catch (Exception e)
{
Console.WriteLine("接受消息出错(非Socket错误)" + e.Message);
}
}
//指定发送一个消息给某个目标
public void SendTo(BaseMsg msg, IPEndPoint ipPoint)
{
try
{
byte[] bytes = msg.Writing();
socket.BeginSendTo(bytes, 0, bytes.Length, SocketFlags.None, ipPoint, (result) =>
{
try
{
socket.EndSendTo(result);
}
catch (SocketException s)
{
Console.WriteLine("发消息出现问题" + s.SocketErrorCode + s.Message);
}
catch (Exception e)
{
Console.WriteLine("发送消息出问题(可能是序列化问题)" + e.Message);
}
}, null);
}
catch (SocketException s)
{
Console.WriteLine("发消息出现问题" + s.SocketErrorCode + s.Message);
}
catch (Exception e)
{
Console.WriteLine("发送消息出问题(可能是序列化问题)" + e.Message);
}
}
public void Broadcast(BaseMsg msg)
{
//广播消息 给谁广播
foreach (Client c in clientDic.Values)
{
SendTo(msg, c.clientIPandPort);
}
}
public void Close()
{
if (socket != null)
{
isClose = true;
socket.Shutdown(SocketShutdown.Both);
socket.Close();
socket = null;
}
}
public void RemoveClient(string clientID)
{
if (clientDic.ContainsKey(clientID))
{
Console.WriteLine("客户端{0}被移除了" + clientDic[clientID].clientIPandPort);
clientDic.Remove(clientID);
}
}
}
}
主程序
using System;
namespace TeachUdpAsyncServerExercises
{
class Program
{
public static ServerSocket serverSocket;
static void Main(string[] args)
{
serverSocket = new ServerSocket();
serverSocket.Start("127.0.0.1", 8080);
Console.WriteLine("UDP服务器启动了");
while (true)
{
string input = Console.ReadLine();
if (input.Substring(0, 2) == "B:")
{
PlayerMsg msg = new PlayerMsg();
msg.playerData = new PlayerData();
msg.playerID = 1001;
msg.playerData.name = "UDP服务器";
msg.playerData.atk = 88;
msg.playerData.lev = 66;
serverSocket.Broadcast(msg);
}
}
}
}
}