note 目录
游戏开发中选用哪种组件来设计网络层
Tcp_Client
实际效果和工程实例子
在unity的引擎中,我们可以选用3种组件来实现网络层。
第一个是 unity本身自带的NetWork
组件
第二个是 C# 层的socket
组件
第三个是 C# 层的TcpClient
组件
分析在实际项目中我们该如何选择。
(1)untiy NetWork
untiy本身自带有NetWork
组件,但是,局限性比较到,到了实际项目中基本上不会去unity本身提供的NetWork
组件来设计网络层。
(2)C# Socket
在OSI网络七层协议,知道TCP是属于传输层协议,,那么如何从应用程序中获取来自传输层的数据,就是通过套接字Socket
来实现的,套接字就像是传输层打开的一扇门,应用程序通过这扇门向远程发送和接受数据。Socket
属于比较底层的API,微软提供出的Socket
API也比较多,比较难掌握,有同步的,还有异步的接收,发送API等。
(3)C# TcpClient
在.net中还提供了TcpClient
TcpListener
,这2个类对套接字Socket
进行了封装,使得操作变得更为简单。使用TcpClient
设计网络层会比Socket
更方便,不需要了解那么多复杂的API。代码量也会少很多。在实际项目中更加偏向于选取TcpClient
是实现网络层。
可以看到在实际项目中,如果需要快速开发的话,会选择TcpClient来实现我们的网络层设计。TcpClient
和Socket
并没有性能上的区别,因为TcpClient
只是对Socket
进行了封装操作,假如我们选用Socket
,我们自己的项目也会对Socket
进行封装处理。关于TcpClient
和Socket
的区别,这篇博客写的很清楚TcpClient
和Socket
的概念和区别
参考链接:
http://www.cnblogs.com/fxair/articles/1534853.html
TcpClient
组件来设计客户端网络层由这几个类来完成网络层
ByteBuffer
PackageIn
SocketClient
NetManager
BaseSocketHandler
2.1:NetManager
负责对外的接口,提供出4个接口
(1)Connect(string ip,int port)
连接服务器
(2)HandlePackage(PackageIn pkg)
收协议:处理服务器发过来的协议
(3)PackageOut GetPackageOut(int msgId)
获取一个发出去的包
(4)SendMessage(PackageOut pkg)
发协议:客户端发送一条协议出去
using System;
using System.Collections.Generic;
using System.Text;
//对外层的接口
//
public class NetManager
{
private static NetManager _instance = null;
private SocketClient _socketClient;
private Dictionary<int, BaseNetHandler> _netDic;
public NetManager()
{
_netDic = new Dictionary<int, BaseNetHandler>();
_socketClient = new SocketClient();
AddNetHandler(new LoginNetHandler());
}
public static NetManager Instance
{
get
{
if (_instance == null)
_instance = new NetManager();
return _instance;
}
}
public SocketClient socketClient
{
get { return _socketClient; }
set { _socketClient = value; }
}
private void AddNetHandler(BaseNetHandler net)
{
int key = net.GetCode();
if (!_netDic.ContainsKey(key))
{
_netDic.Add(key, net);
}
else
{
Console.WriteLine("Add same net handler");
}
}
public void Connect(string ip, int port)
{
if (_socketClient != null)
{
_socketClient.Connect(ip, port, ConnectCallback);
}
else
{
Console.WriteLine("SocketClient is null");
}
}
private void ConnectCallback(ConnectState connectState)
{
Console.WriteLine("connectState:" + connectState);
}
//处理服务器发过来的协议
//
public void HandlePackage(PackageIn pkg)
{
int msgId = pkg.code;
if (_netDic.ContainsKey(msgId))
{
_netDic[msgId].Configure(pkg);
_netDic[msgId].HandlePackage();
}
else
{
Console.WriteLine("not exist protocal,msg id is:" + msgId);
}
}
//根据协议ID获取一个发送出去的包
//
public PackageOut GetPackageOut(int msgId)
{
PackageOut pkg = new PackageOut(msgId);
return pkg;
}
//发送协议
//
public void SendMessage(PackageOut pkg)
{
if (socketClient != null)
{
socketClient.WriteMessage(pkg.SetPackage());
}
else
{
Console.WriteLine("socket client is null");
}
}
}
2.2:ByteBuffer
在字节流中负责读写操作。客户端和服务器2边进行通信发送的都是字节流,因为.net本身没有提供直接从字节流中读取int,ushort,long,float,double这些类型的方法。需要自己封装相应的接口出来。
using System;
using System.Collections.Generic;
using System.Text;
using System.IO;
public class ByteBuffer
{
//内存流对象用来进行字节流的操作
protected MemoryStream stream = null;
//写对象
protected BinaryWriter writer = null;
//读对象
protected BinaryReader reader = null;
public ByteBuffer()
{
stream = new MemoryStream();
writer = new BinaryWriter(stream);
}
public ByteBuffer(byte[] data)
{
if (data != null)//初始化ByteBuffer对象只负责读字节流
{
stream = new MemoryStream(data);
reader = new BinaryReader(stream);
}
else//初始化ByteBuffer对象只负责写字节流
{
stream = new MemoryStream();
writer = new BinaryWriter(stream);
}
}
public void Close()
{
if (writer != null)
writer.Close();
if (reader != null)
reader.Close();
if (stream != null)
stream.Close();
writer = null;
reader = null;
stream = null;
}
//往字节流中写入一个字节
public void WriteByte(byte value)
{
writer.Write(value);
}
//往字节流中写入一个int值
public void WriteInt(int value)
{
writer.Write((int)value);
}
//往字节流中写入一个ushort值
public void WriteShort(ushort value)
{
writer.Write((ushort)value);
}
//往字节流中写入一个long值
public void WriteLong(long value)
{
writer.Write((long)value);
}
//往字节流中写入一个float值
public void WriteFloat(float value)
{
byte[] temp = BitConverter.GetBytes(value);
Array.Reverse(temp);
writer.Write(BitConverter.ToSingle(temp,0));
}
//往字节流中写入一个double值
public void WriteDouble(double value)
{
byte[] temp = BitConverter.GetBytes(value);
Array.Reverse(temp);
writer.Write(BitConverter.ToDouble(temp,0));
}
//往字节流中写入一个string值
public void WriteString(string value)
{
byte[] bytes = Encoding.UTF8.GetBytes(value);
writer.Write((ushort)bytes.Length);
writer.Write(bytes);
}
//往字节流中写入一个字节数组byte[]
public void WriteBytes(byte[] value)
{
writer.Write((int)value.Length);
writer.Write(value);
}
//在字节流中读取一个字节
public byte ReadByte()
{
return reader.ReadByte();
}
//在字节流中读取一个int值
public int ReadInt()
{
return (int)reader.ReadInt32();
}
//在字节流中读取一个ushort值
public ushort ReadShort()
{
return (ushort)reader.ReadInt16();
}
//在字节流中读取一个long值
public long ReadLong()
{
return (long)reader.ReadInt64();
}
//在字节流中读取一个float值
public float ReadFloat()
{
byte[] temp = BitConverter.GetBytes(reader.ReadSingle());
Array.Reverse(temp);
return BitConverter.ToSingle(temp, 0);
}
//在字节流中读取一个double值
public double ReadDouble()
{
byte[] temp = BitConverter.GetBytes(reader.ReadDouble());
Array.Reverse(temp);
return BitConverter.ToDouble(temp, 0);
}
//在字节流中读取一个string值
public string ReadString()
{
ushort len = ReadShort();
byte[] buffer = new byte[len];
buffer = reader.ReadBytes(len);
return Encoding.UTF8.GetString(buffer);
}
//在字节流中读取一个字节数组
public byte[] ReadBytes()
{
int len = ReadInt();
return reader.ReadBytes(len);
}
//将写入到内存流中转换为字节数组
public byte[] ToBytes()
{
writer.Flush();
return stream.ToArray();
}
public void Flush()
{
writer.Flush();
}
}
2.3:SocketClient
SocketClient
这个类主要负责连接上服务器,收到服务器发来的包进行解包和组包。
Tcp每次发过来的包可能不是一条的协议包,需要自己对服务器发过来的字节流进行组织,先要读取出包头发过来的长度字段,根据这个长度,判断发过来的字节流是否够一条协议的包的字节流。
msgLength | msgId | 包内容 | 包内容 | 包内容 | ... | ... |
一般协议组成由包头和包内容组成。
一条完整协议 = 包头 + 协议内容
包头 = 这个协议的字节流长度 + 协议ID
using System;
using System.IO;
using System.Collections.Generic;
using System.Text;
using System.Net;
using System.Net.Sockets;
//连接服务器的状态
public enum ConnectState
{
Connected,//连接成功
Disconnect,//连接不上服务器
}
public class SocketClient
{
//缓存中接收最大的字节
private const int BufferSize = 8192;
//缓存中接收到的字节数组
private byte[] byteBuffer;
//TcpClient对象
private TcpClient client;
//客户端到服务器的流对象,用这个对象和远程服务器来接收和发送字节流
private NetworkStream streamToServer;
//内存流对象,用MemoryStream对象可以很方便的对内存中的字节流进行操作
private MemoryStream memoStream;
//从内存流对象memoStream中读取字节流的对象
private BinaryReader reader;
//客户端的连接状态
private ConnectState _connectState;
//连接服务器后的回调
private Action _connectedCallback;
public SocketClient()
{
}
//连接服务器
//ip:可以是数字IP字段,也可以是域名
//port:服务器的端口
//callback:连接后的回调
public void Connect(string ip, int port,Action callback)
{
_connectedCallback = callback;
try
{
client = new TcpClient();
IPAddress[] addrIps = Dns.GetHostAddresses(ip);
client.BeginConnect(addrIps, port, new AsyncCallback(OnConnect), null);
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
return;
}
}
void OnConnect(IAsyncResult asr)
{
//不管是否连接到服务器,把连接服务器的状态回调出去
if (_connectedCallback != null)
{
_connectedCallback(client.Connected ? ConnectState.Connected:ConnectState.Disconnect);
}
if (!client.Connected)
{
Console.WriteLine(" 连接服务器失败");
return;
}
byteBuffer = new byte[BufferSize];
memoStream = new MemoryStream();
reader = new BinaryReader(memoStream);
//打印连接到服务器的信息
Console.WriteLine("Server Connected!{0} -- > {1}",client.Client.LocalEndPoint,client.Client.RemoteEndPoint);
streamToServer = client.GetStream();
AsyncCallback callback = new AsyncCallback(ReadComplete);
streamToServer.BeginRead(byteBuffer,0,BufferSize,callback,null);
}
private void ReadComplete(IAsyncResult ar)
{
int bytesRead = 0;
try
{
lock (streamToServer)
{
bytesRead = streamToServer.EndRead(ar);
}
if (bytesRead == 0)
throw new Exception("读取到0字节");
OnReceive(byteBuffer,bytesRead);
//读取完后在进行异步读取操作,使得读的操作形成一个无限循环
lock (streamToServer)
{
Array.Clear(byteBuffer,0,byteBuffer.Length);
AsyncCallback callback = new AsyncCallback(ReadComplete);
streamToServer.BeginRead(byteBuffer,0,BufferSize,callback,null);
}
}
catch(Exception ex)
{
if (streamToServer != null)
streamToServer.Dispose();
client.Close();
Console.WriteLine(ex.Message);
}
}
private void OnReceive(byte[] bytes, int length)
{
memoStream.Seek(0,SeekOrigin.End);
memoStream.Write(bytes,0,length);
memoStream.Seek(0,SeekOrigin.Begin);
while (RemainingBytes() > ProtocalHead.HeadSize)
{
ushort messageLen = reader.ReadUInt16();//先读取出长度字段(定义为2个字节)
if (RemainingBytes() >= ProtocalHead.ProtocalIdLength + messageLen)//读取了长度,剩下协议ID字段 + 协议本身内容
{
MemoryStream ms = new MemoryStream();
BinaryWriter writer = new BinaryWriter(ms);
writer.Write(reader.ReadBytes(ProtocalHead.ProtocalIdLength + messageLen));
ms.Seek(0, SeekOrigin.Begin);
Console.WriteLine("receive length:" + messageLen);
OnReceivedMessage(messageLen, ms);
}
else
{
memoStream.Position = memoStream.Position - ProtocalHead.HeadSize;
break;
}
}
//不够一条协议的时候,需要将服务器发过来的字节流缓存起来,等到下一次发包过来,组织成一个完整的包。
byte[] leftover = reader.ReadBytes((int)RemainingBytes());
memoStream.SetLength(0);
memoStream.Write(leftover,0,leftover.Length);
}
private void OnReceivedMessage(ushort length, MemoryStream ms)
{
BinaryReader r = new BinaryReader(ms);
byte[] message = r.ReadBytes((int)(ms.Length - ms.Position));
//
PackageIn packageIn = new PackageIn(length,message);
NetManager.Instance.HandlePackage(packageIn);
}
//客户端发送协议的唯一出口
//将字节流发送到服务器
public void WriteMessage(byte[] message)
{
MemoryStream ms = null;
using (ms = new MemoryStream())
{
ms.Position = 0;
BinaryWriter writer = new BinaryWriter(ms);
writer.Write(message);
writer.Flush();
if (client != null && client.Connected)
{
byte[] payLoaded = ms.ToArray();
streamToServer.BeginWrite(payLoaded,0,payLoaded.Length,new AsyncCallback(OnWrite),null);
}
else
{
Console.WriteLine("client.connected -->> false");
}
}
}
private void OnWrite(IAsyncResult r)
{
try
{
streamToServer.EndWrite(r);
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
}
///
/// 剩余的字节
///
private long RemainingBytes()
{
return memoStream.Length - memoStream.Position;
}
}
2.4:PackageIn
PackageIn
继承ByteBuffer
,负责客户端的这边接收到服务器发送过来的字节流,判断得到一个完整的包的时候,对这个包的字节流进一步封装处理。得到它的协议ID,长度。
using System;
using System.Collections.Generic;
public class PackageIn : ByteBuffer
{
private int _code;
private int _packageLength;
public PackageIn(int length, byte[] data)
:base(data)
{
_code = ReadInt();//先读取出协议的ID
_packageLength = length;//得到协议包内容的长度
}
public int code
{
get { return _code; }
}
public int packageLength
{
get { return _packageLength; }
}
}
2.5:PackageOut
PackageOut
继承ByteBuffer
,负责客户端的这边的协议内容发送到服务器。要对协议的字节流进行处理,加上包头信息。
using System;
using System.IO;
using System.Collections.Generic;
using System.Linq;
using System.Text;
public class PackageOut : ByteBuffer
{
private int _code;
private int _packageLength;
public PackageOut(int code)
: base()
{
_code = code;
}
public int code
{
get { return _code; }
}
public int packageLength
{
get { return _packageLength; }
}
//进行封包处理
//将协议的长度放在包头的前2个字节(1-2)
//将协议的唯一ID放在包头的第3字节--到第6字节
//将协议的内容放在最后
public byte[] SetPackage()
{
byte[] message = stream.ToArray();
Console.WriteLine("send content:" + message.Length);
MemoryStream ms = null;
using (ms = new MemoryStream())
{
ms.Position = 0;
BinaryWriter writer = new BinaryWriter(ms);
writer.Write((ushort)(message.Length));//协议包的长度
writer.Write((int)_code);//协议包的唯一id
writer.Write(message);//协议内容
writer.Flush();
}
return ms.ToArray();
}
}
2.6:BaseSocketHandler
BaseSocketHandler
这个已经到了逻辑层的每条协议的一个基类。逻辑层的每条协议继承这个基类,重写这2
个方法就可以,int GetCode()
,HandlePackage()
using System;
using System.Collections.Generic;
using System.Text;
public class BaseNetHandler
{
protected PackageIn _data;
//给每条协议顶一个唯一ID
public virtual int GetCode()
{
return 0;
}
//给协议的字节流赋值
public void Configure(PackageIn data)
{
_data = data;
}
//每条协议处理发过来的包
public virtual void HandlePackage()
{
}
}
上述可以构成完整的收发了。将服务器,客户端运行起来,都在同一台机器上,ip填写”localhost”,port填写”8500”。在客户端按下A键,客户端会发送一条登入协议到服务器。服务器收到这条协议后会把所有的字段进行转发到客户端。
这个项目有2个工程,一个是服务器工程,一个是客户端工程。本篇主要介绍的是客户端的网络层设计。
工程下载连接(Tcp_Server_Client)