客户端
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Net.Sockets;
using UnityEngine;
public class NetClient : Singleton<NetClient>
{
public Socket m_Sockets;//客户端的通讯类
public Queue<byte[]> m_que = new Queue<byte[]>();//客户端队列用于保存流数据ID和包
public byte[] m_Data = new byte[1024];//缓存
public byte[] m_Stream = new byte[0];//流
public void Init()
{
m_Sockets = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);//创建通讯类
m_Sockets.BeginConnect("127.0.0.1", 12345, OnConnect, null);//给服务器发消息127.0.0.1是本机ID 3000是服务器端口
}
private void OnConnect(IAsyncResult ar)
{
m_Sockets.EndConnect(ar);//
m_Sockets.BeginReceive(m_Data, 0, m_Data.Length, SocketFlags.None, OnReceive, null);//接收服务器发送的数据
}
private void OnReceive(IAsyncResult ar)
{
int len = m_Sockets.EndReceive(ar);//接收的长度
if (len > 0)
{
byte[] data = new byte[len];
Buffer.BlockCopy(m_Data, 0, data, 0, len);
m_Stream = m_Stream.Concat(data).ToArray();
while (m_Stream.Length > 2)
{
ushort bodyLen = BitConverter.ToUInt16(m_Stream, 0);
int allLen = bodyLen + 2;
if (m_Stream.Length >= allLen)
{
byte[] oneData = new byte[bodyLen];
Buffer.BlockCopy(m_Stream, 2, oneData, 0, bodyLen);
m_que.Enqueue(oneData);
int syLen = m_Stream.Length - allLen;
if (syLen > 0)
{
byte[] syBody = new byte[syLen];
Buffer.BlockCopy(m_Stream, allLen, syBody, 0, syLen);
m_Stream = syBody;
}
else
{
m_Stream = new byte[0];
break;
}
}
else
{
break;
}
}
m_Sockets.BeginReceive(m_Data, 0, m_Data.Length, SocketFlags.None, OnReceive, null);
}
}
public void Send(int id, byte[] body)
{
byte[] head = BitConverter.GetBytes(id);
byte[] len = BitConverter.GetBytes((ushort)(head.Length + body.Length));
byte[] data = new byte[0];
data = data.Concat(len).ToArray();
data = data.Concat(head).ToArray();
data = data.Concat(body).ToArray();
m_Sockets.BeginSend(data, 0, data.Length, SocketFlags.None, OnSend, null);
}
private void OnSend(IAsyncResult ar)
{
int len = m_Sockets.EndSend(ar);
Debug.Log("客户端发送长度:" + len);
}
public void Updata()
{
if (m_que.Count > 0)
{
byte[] oneData = m_que.Dequeue();
int id = BitConverter.ToInt32(oneData, 0);
byte[] body = new byte[oneData.Length - 4];
Buffer.BlockCopy(oneData, 4, body, 0, body.Length);
MessageCenter .Ins.Send(id, body);
}
}
}
我们这边的客户端呢,有俩种方法一个是Init()方法也就是初始化客户端的方法。还有一个则是Updata()方法这个东西呢需要我们实时更新客户端消息状态所以我们要将这个方法在另一些脚本如GameMgr这种游戏管理类里面进行调用Updata的方法使得我们可以实时的接受和更新客户端的状态。
服务器
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Net;
using System.Net.Sockets;
using Google.Protobuf;
namespace ZG5_Server
{
class NetManager: Singleton<NetManager>
{
Socket server;
public List<Client> clients = new List<Client>();
public void Init()
{
server = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
IPEndPoint ip = new IPEndPoint(IPAddress.Any, 12345);
server.Bind(ip);
server.Listen(20);
Console.WriteLine("服务器开启");
server.BeginAccept(OnAccept, null);
}
private void OnAccept(IAsyncResult ar)
{
Socket client = server.EndAccept(ar);
IPEndPoint iP = client.RemoteEndPoint as IPEndPoint;
Client cli = new Client();
cli.socket = client;
cli.name = iP.Address + ":" + iP.Port;
clients.Add(cli);
Console.WriteLine(DateTime.Now.ToString()+ " " + iP.Port + "链接");
cli.socket.BeginReceive(cli.data, 0, cli.data.Length, SocketFlags.None, OnReceive, cli);
server.BeginAccept(OnAccept, null);
}
private void OnReceive(IAsyncResult ar)
{
try
{
Client cli = ar.AsyncState as Client;
int len = cli.socket.EndReceive(ar);
if (len <= 0)
{
cli.socket.Shutdown(SocketShutdown.Both);
cli.socket.Close();
clients.Remove(cli);
Console.WriteLine(DateTime.Now.ToString() + " " + cli.name + "下线");
Leave s2c = new Leave();
s2c.PlayerId = cli.player.Id;
foreach (var item in clients)
{
if (item.player != null)
{
Send(MsgID.S2C_LEAVE, s2c.ToByteArray(), item);
}
}
}
else if (cli.socket.Connected)
{
byte[] data = new byte[len];
Buffer.BlockCopy(cli.data, 0, data, 0, data.Length);
//创建一个临时流
byte[] liu = new byte[cli.stream.Length + data.Length];
Buffer.BlockCopy(cli.stream, 0, liu, 0, cli.stream.Length);
Buffer.BlockCopy(data, 0, liu, cli.stream.Length, data.Length);
//把流替换为临时流
cli.stream = liu;
//流长度大于等于2说明流内有数据
while (cli.stream.Length >= 2)
{
//获取一个包的长度
ushort onelen = BitConverter.ToUInt16(cli.stream, 0);
//如果流的长度大于等于包的长度+2说明有一个完整包
if (cli.stream.Length >= 2 + onelen)
{
//获取一个完整包
byte[] onedata = new byte[onelen];
Buffer.BlockCopy(cli.stream, 2, onedata, 0, onedata.Length);
//获取包的消息号
int id = BitConverter.ToInt32(onedata, 0);
//获取包的消息体
byte[] body = new byte[onedata.Length - 4];
Buffer.BlockCopy(onedata, 4, body, 0, body.Length);
//消息分发
Console.WriteLine("收到消息:id" + id);
MessageCenter.Ins.Send(id, body, cli);
//剩余长度
int sylen = cli.stream.Length - (2 + onelen);
//剩余长度大于0说明还有包
if (sylen > 0)
{
byte[] sydata = new byte[sylen];
Buffer.BlockCopy(cli.stream, 2 + onelen, sydata, 0, sylen);
cli.stream = sydata;
}
else
{
cli.stream = new byte[0];
}
}
else
{
break;
}
}
#region MyRegion
//byte[] data = new byte[len];
//Buffer.BlockCopy(cli.data, 0, data, 0, len);
将收到的消息写入流
//cli.my.Write(data, 0, data.Length);
流长度大于等于2说明有内容(2是包长度ushort的长度)
//while (cli.my.Length >= 2)
//{
// //读取的起始点
// cli.my.Position = 0;
// int oneLen = cli.my.ReadUshort();
// //判断是否有完整包
// if (cli.my.Length >= 2 + oneLen)
// {
// //获取一个包
// byte[] oneData = new byte[oneLen];
// cli.my.Read(oneData, 0, oneData.Length);
// //解析并分发
// int id = BitConverter.ToInt32(oneData, 0);
// byte[] body = new byte[oneLen - 4];
// Buffer.BlockCopy(oneData, 4, body, 0, body.Length);
// MessageCenter.Ins().Broadcast(id, body, cli);
// int syLen = (int)cli.my.Length - (2 + oneLen);
// //判断流内是否还有剩余内容
// if (syLen > 0)
// {
// //复制出剩余内容
// byte[] syDate = new byte[syLen];
// cli.my.Read(syDate, 0, syDate.Length);
// //清空流
// cli.my.SetLength(0);
// cli.my.Position = 0;
// //将剩余内容写入流
// cli.my.Write(syDate, 0, syDate.Length);
// }
// else
// {
// //清空流
// cli.my.SetLength(0);
// cli.my.Position = 0;
// }
// }
// else
// {
// cli.my.Position = cli.my.Length;
// break;
// }
#endregion
cli.socket.BeginReceive(cli.data, 0, cli.data.Length, SocketFlags.None, OnReceive, cli);
}
}
catch (Exception e)
{
Console.WriteLine(e);
throw;
}
}
public void Send(int id, byte[] body, Client cli)
{
byte[] head = BitConverter.GetBytes(id);
byte[] data = new byte[head.Length + body.Length];
Buffer.BlockCopy(head, 0, data, 0, head.Length);
Buffer.BlockCopy(body, 0, data, head.Length, body.Length);
//手动拼接长度
byte[] mylen = BitConverter.GetBytes((ushort)data.Length);
byte[] myData = new byte[mylen.Length + data.Length];
Buffer.BlockCopy(mylen, 0, myData, 0, mylen.Length);
Buffer.BlockCopy(data, 0, myData, mylen.Length, data.Length);
cli.socket.BeginSend(myData, 0, myData.Length, SocketFlags.None, OnSend, cli);
}
private void OnSend(IAsyncResult ar)
{
Client cli = ar.AsyncState as Client;
int len = cli.socket.EndSend(ar);
//Console.WriteLine(len);
}
}
}
而我们在服务器中会封装一个这样的方法,也就是会频繁使用字节数组,因为每次我们发送消息他都是通过一个字节数组来接受消息。而一个数组默认我们给的容量是一次消息会占用1024,所以这样非常的消耗内存空间,我这边使用了分包和粘包两种方法进行了些许优化。
通过在服务器和客户端制定对应的MsgID消息号来实现服务器与客户端之间的沟通。好比说你去银行取自己账户的钱前提是你得有你的银行卡和你自己的预留信息才可以进行取钱等一系列的操作。如果说我们这边的消息号没有认证对接成功的话那么我们的服务器或者客户端则接受不到我们发出去的消息或者想要实现的一系列功能。
如上图所示我的这个命名会有俩种,一个是S2C一个是C2S,而S2C的意思则是服务器向客户端发送的消息,而C2S则是客户端向服务器发送的消息,S也就是Sever服务器的缩写,C呢则是Client客户端的缩写。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace ZG5_Server
{
class MessageCenter : Singleton<MessageCenter>
{
public Dictionary<int, Action<object>> dic = new Dictionary<int, Action<object>>();
public void Add(int id, Action<object> obj)
{
if (dic.ContainsKey(id))
{
dic[id] += obj;
}
else
{
dic.Add(id, obj);
}
}
public void Remove(int id, Action<object> obj)
{
if (dic.ContainsKey(id))
{
dic[id] -= obj;
if (dic[id] == null)
{
dic.Remove(id);
}
}
}
public void Send(int id, params object[] arr)
{
if (dic.ContainsKey(id))
{
dic[id](arr);
}
}
}
}
如上图是我封装的一个消息中心类,这个方法就是用来监听事件方法。
还有一个是我们的单例,上面消息中心继承过这个类。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace ZG5_Server
{
class Singleton<T> where T :class ,new()
{
static T ins;
public static T Ins {
get{
if (ins == null)
{
ins = new T();
}
return ins;
}
}
}
}
这个单例类非常的方便类似于静态。
我们的服务器和客户端都需要使用这个类,因为我们发送消息时可以用客户端或者服务器类里封装的Send方法。而我们需要监听这个方法也就是对应我们MsgID的消息号,就好像我跟一个人说话了,他肯定得回我一句话这样的话我们就需要竖起耳朵这里的耳朵就指的是我们通过消息中心来监听他跟我说话发送过来的方法。
如上图是我在客户端写的一个人物移动类,可以看到代码有这个UnityEngine的这个命名空间,我们这边的服务器是通过c#直接打开创建一个项目进行编制的。而我们的客户端则需要和unity进行交互通过运行服务器来进行交互。
可以看到我这边人物移动类里的Start()方法里就用到了消息中心来监听消息,他会发过来一个带有参数的方法。而这个参数我门不能直接使用,因为他的这个类型我们需要将他转变成我们要用的东西。
syntax = "proto3";
message V3 {
float x = 1;
float y = 2;
float z = 3;
}
message PlayerData {
int32 id = 1;
string nick = 2;
int32 hpmax = 3;
int32 hp = 4;
int32 shield = 5;//护盾
int32 tou = 6;
int32 yifu = 7;
int32 tui = 8;
V3 pos = 9;
V3 rot = 10;
repeated int32 skills = 11;
repeated int32 buffs = 12;
}
//注册
message C2S_Register {
string account = 1;
string password = 2;
}
//注册返回
message S2C_Register {
bool result = 1;
string error = 2;
}
//登录
message C2S_Login {
string account = 1;
string password = 2;
}
//登录返回
message S2C_Login {
bool result = 1;
string error = 2;
int32 userid = 3;
repeated PlayerData players = 4;
}
//技能
message C2S_Skill {
int32 skillId = 1;
int32 playerId = 2;
repeated int32 targetId = 3;
}
//buff
message S2C_Buff {
int32 buffId = 1;
int32 targetId = 2;
}
//移动
message C2S_Move {
int32 playerId = 1;
float ang = 2;
float dis = 3;
V3 pos = 4;
V3 rot = 5;
}
//离开
message Leave {
int32 playerId = 1;
}
这边我门就需要用到另一个东西,因为你从服务器发送到客户端的数据不可能一样,假如说你在客户端需要人物移动是不是就要用到V3坐标或者他的GameObject类型之类,而我们的服务器不具备这样的类型而且服务器只能传输一些数据和基础的变量类型,上图的这个是PB包,也可以说是一种不同的数据类。可以看到每一个Message 后面的名字跟我们刚刚MsgID里对应的常量名。在这个PB代码里就相当于方法名每一个对应的消息号他返回的变量和数据都是不相同的所以会封装很多这样的方法。
而这样的代码写好之后我们还无法使用,需要Lib包和Tools工具将他转成CS脚本从而让我们使用,这边如果要使用调用他就要继承这个命名空间,跟上面的消息中心同样的他也需要客户端和服务器一致都拖进去,也需要将lib包一同拖进去,并且要在那个代码类里面使用就要继承这个命名空间,否则无法调用到我们上面编制的PB包里的变量和集合。
using Google.Protobuf;