3D游戏中的网络层设计

note 目录

  • 游戏开发中选用哪种组件来设计网络层

  • Tcp_Client

  • 实际效果和工程实例子


1 选用哪种组件来设计网络层

在unity的引擎中,我们可以选用3种组件来实现网络层。

  • 第一个是 unity本身自带的NetWork组件

  • 第二个是 C# 层的socket组件

  • 第三个是 C# 层的TcpClient组件

分析在实际项目中我们该如何选择。

(1)untiy NetWork

untiy本身自带有NetWork组件,但是,局限性比较到,到了实际项目中基本上不会去unity本身提供的NetWork组件来设计网络层。

(2)C# Socket

在OSI网络七层协议,知道TCP是属于传输层协议,,那么如何从应用程序中获取来自传输层的数据,就是通过套接字Socket来实现的,套接字就像是传输层打开的一扇门,应用程序通过这扇门向远程发送和接受数据。Socket属于比较底层的API,微软提供出的SocketAPI也比较多,比较难掌握,有同步的,还有异步的接收,发送API等。

(3)C# TcpClient

在.net中还提供了TcpClient TcpListener,这2个类对套接字Socket进行了封装,使得操作变得更为简单。使用TcpClient设计网络层会比Socket更方便,不需要了解那么多复杂的API。代码量也会少很多。在实际项目中更加偏向于选取TcpClient是实现网络层。

可以看到在实际项目中,如果需要快速开发的话,会选择TcpClient来实现我们的网络层设计。TcpClientSocket并没有性能上的区别,因为TcpClient只是对Socket进行了封装操作,假如我们选用Socket,我们自己的项目也会对Socket进行封装处理。关于TcpClientSocket的区别,这篇博客写的很清楚TcpClientSocket的概念和区别

参考链接:
http://www.cnblogs.com/fxair/articles/1534853.html

3D游戏中的网络层设计_第1张图片



2 选用C# TcpClient组件来设计客户端网络层

3D游戏中的网络层设计_第2张图片

由这几个类来完成网络层

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()
    {

    }
}

3 实际效果和工程下载

上述可以构成完整的收发了。将服务器,客户端运行起来,都在同一台机器上,ip填写”localhost”,port填写”8500”。在客户端按下A键,客户端会发送一条登入协议到服务器。服务器收到这条协议后会把所有的字段进行转发到客户端。

3D游戏中的网络层设计_第3张图片

这个项目有2个工程,一个是服务器工程,一个是客户端工程。本篇主要介绍的是客户端的网络层设计。
工程下载连接(Tcp_Server_Client)

你可能感兴趣的:(NetWork)