GameNet 游戏网络库 C#实现

介绍

很久没有写博客,面试了几次发现自己实现的东西有点少,面试官问觉得自己就是搬砖,没什么意思,就写点东西。

协议用protobuf-net
.net 版本用4.5
socket用SocketAsyncEventArgs
实现网络库
写服务器和客户端测试程序

代码本身不难,很多零碎的知识点穿插起来难度就会提升。每一个模块我尽量列举实现的原因,有可能遇到的坑。

先看测试程序。
服务器监听一个端口
客户端建立1024个连接与服务器通信
在这里插入图片描述

服务器测试程序

using System;
using ZyGame.GameNet.Server;
using ZyGame.GameNet.Componet;
using System.Threading.Tasks;
using ProtoBuf;

namespace TestServer
{
    // 信道id,将协议分类,
    // 会话接收字节流后解析的协议集合
    // server-client连接是一个信道 
    // server-server连接是一个信道
    //信道id需要大于1,1库内部已经占用
    internal static class TestServerChannel
    {
        //自己作为服务器的信道
        public const int Server = 2000;
    }

    //协议号
    internal static class TestServerProtoType
    {
        public const int Test = 200;
    }

    //ProtoContract - conprotobuf-net知识,说明是protobuf-net解析的类型
    //Proto 协议特性 说明 协议的协议号 所属信道,
    // 默认既是接受协议又是发送协议 若是纯发送协议 本特性可以不写
    [ProtoContract]
    [Proto(TestServerProtoType.Test, TestServerChannel.Server)]
    public class ProtoTest : Protocol//继承库协议
    {
        //protobuf-net 成员属性 为测试程序服务 写了一条
        //若是发送协议则需要用户赋值属性 若是接收协议 属性值会被内部赋值
        [ProtoMember(1)]
        public string Info { get; set; }

        //构造函数 需要实现基类构造
        public ProtoTest()
            :base(TestServerProtoType.Test)
        {
        }

        //协议处理 发送协议不用实现 接收协议必须实现
        public override void Process(Session session)
        {
            Console.WriteLine(string.Format("Session id = {0} info = {1}", 
                session.SessionId, Info));
        }
    }

    internal class Program
    {
        async Task Run()
        {
            //监听器 需要指定信道id
            Listener listen = new Listener(TestServerChannel.Server);
            //监听地址
            listen.StartListen("127.0.0.1", 9898);

            while (true)
            {
            	//处理建立 和 移除会话 延迟一帧利于线程同步
                ServerSessionManager.Tick();

                ServerSessionManager.Foreach((session)=> {
                    var proto = new ProtoTest();
                    proto.Info = string.Format("Hello client, rand = {0}",
                        new Random().Next(10000));
                    session.Send(proto);
                });

               await Task.Delay(50);
            }
        }

        static void Main(string[] args)
        {
            try
            {
                new Program().Run().Wait();
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex.ToString());
                Console.ReadLine();
            }
        }
    }
}

服务器测试程序代码很少,只是为了说明如何使用这个库。

客户端测试程序

客户端发起1024个会话测试。网络上很多测试程序就跑一个会话,根本说明不了问题,个人感觉1024的样本空间应该能说明一些问题了吧。

using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using ProtoBuf;
using ZyGame.GameNet.Client;
using ZyGame.GameNet.Componet;
//需要引入ZyGame.GameNet.dll 和 protobuf-net.dll
namespace TestClient
{
    //信道id 服务器测试程序有解释,
    // 这个不用和服务器对应相等,只要大于1即可
    internal static class TestClientChannel
    {
        public const int Client = 1000;
    }
    //协议号,可以多个,写一个是为了测试,多写无益
    internal static class TestClientProtoType
    {
        public const int Test = 200;
    }
    //服务器测试程序有解释
    [ProtoContract]
    [Proto(TestClientProtoType.Test, TestClientChannel.Client)]
    public class ProtoHello : Protocol
    {
        [ProtoMember(1)]
        public string Info { get; set; }

        public ProtoHello()
            :base(TestClientProtoType.Test)
        {
        }

        public override void Process(Session session)
        {
            Console.WriteLine(string.Format("Session id = {0} info = {1}",
                session.SessionId, Info));
        }
    }

    class Program
    {
        async Task Run()
        {
            //一个程序建立1024个会话
            List<ClientSession> sessionList = new List<ClientSession>();
            for (int idx = 0; idx < 1024; idx++)
            {//创建会话
                ClientSession session = new ClientSession(TestClientChannel.Client);
                session.Open("127.0.0.1", 9898);
                sessionList.Add(session);
                await Task.Delay(1);
            }

            while (true)
            {
                foreach (var item in sessionList)
                {
                    item.Tick();
                }

                foreach (var item in sessionList)
                {//为了测试,每个会话都在不停的发送消息
                    var proto = new ProtoHello();
                    proto.Info = string.Format("Hello Server, rand = {0}", new Random().Next(10000));
                    item.Send(proto);
                }

                /*测试关闭会话代码
                string input = Console.ReadLine();
                int num = int.Parse(input);
                if (num >= 0 && num < 256)
                {
                    sessionList[num].Close();
                }
                else
                {
                    foreach (var item in sessionList)
                    {
                        item.Close();
                    }
                }*/

                await Task.Delay(50);
            }
        }

        static void Main(string[] args)
        {
            new Program().Run().Wait();
        }
    }
}

测试程序自评

测试程序我写了三个版本,几乎每个版本都是为了如何方便使用程序,如何方便实现协议来改进的。
第一个版本修改协议特性,第二个版本修改协议注册器。
达到的效果是
第一个版本去掉了让用户实现一个自己的特性的繁琐步骤【proto特性来源】
第二个版本去掉了让用户实现一个自己的注册器的步骤【信道来源】
前面的版本我不可能发出来了,是为了记录一下为什么使用这个库这么简单,并且说明一下 特性和信道的来因。

网络库介绍

网络库实现很精巧,自我感觉。
在这里插入图片描述
网络库一共四个名空间,每一个名空间我会介绍,代码我也会上传。
从简单到复杂介绍。
最简单的是Misc 杂项的意思,取自单词【miscellaneous】很多有名的库也是取这个名字,典型的就是DirectX,也有一些库会取别的名字,例如helper、tools、utils等一些。主要说的就是一些不好分类的小部件工具类。

Misc名空间

在这里插入图片描述
logger类和netsetting很简单,望文知意。

logger类
using System;

namespace ZyGame.GameNet.Misc
{
    public static class Logger
    {
        public static Action<string> LogErrorMethod { get; set; }
        public static Action<string> LogWarnMethod { get; set; }
        public static Action<string> LogInfoMethod { get; set; }

        static Logger()
        {
        }

        public static void LogError(string message)
        {
            if (LogErrorMethod != null)
            {
                LogErrorMethod(message);
            }
            else
            {
                Console.WriteLine(message);
            }
        }

        public static void LogWarn(string message)
        {
            if (LogWarnMethod != null)
            {
                LogWarnMethod(message);
            }
            else
            {
                Console.WriteLine(message);
            }
        }

        public static void LogInfo(string message)
        {
            if (LogInfoMethod != null)
            {
                LogInfoMethod(message);
            }
            else
            {
                Console.WriteLine(message);
            }
        }
    }
}

网络设置类

网络需要的一些常量,在静态和常量之间我衡量了半天,觉得静态有可能被用户修改,如果确定需要修改还是重新编译比较好。所以设定为常量。

namespace ZyGame.GameNet.Misc
{
    public static class NetSetting
    {
        public const int DecodeBufferSize = 2048;
        public const int BufferPrefixSize = 4;
        public const int MsgPrefixSize = 4;
        public const int ProtoPrefixSize = BufferPrefixSize + MsgPrefixSize;
        public const int SocketBufferSize = 1024;
        public const int MaxConnect = 10000;
        public const int InterProtoChannel = 1;
    }
}
ProtobufUtils类

字节流和某个类型的转换器。protobuf-net的知识。

using System;
using System.IO;
using ProtoBuf.Meta;

namespace ZyGame.GameNet.Misc
{
    public static class ProtoBufUtils
    {
        private static RuntimeTypeModel TypeModel { get; set; }

        static ProtoBufUtils()
        {
            TypeModel = RuntimeTypeModel.Create();
            TypeModel.UseImplicitZeroDefaults = false;
        }

        public static object Deserialize(byte[] bytes, Type param)
        {
            using(var stream = new MemoryStream(bytes))
            {
                return TypeModel.Deserialize(stream, null, param);
            }
        }

        public static byte[] Serialize(object value)
        {
            using (var stream = new MemoryStream())
            {
                TypeModel.Serialize(stream, value);
                return stream.ToArray();
            }
        }
    }
}

misc名空间就是琐碎的小部件。

Componet命空间

库核心名空间
在这里插入图片描述
Attribute 协议特性类
Connecter socket连接封装
Internal 库内部逻辑,主要是为了建立连接后服务器和客户端同步会话id。这个会话id有不少网络库是不同步的,客户端和服务器会话id是不一样的。不过id有对应关系就好了。 例如客户端会话id=12对应服务器会话id=45 这个数字是运行时确定的。
Protocol 网络协议,实现极其简单。
Recver 接收器,字节流解码为协议
Register 注册器,协议的注册器,字节流解释成协议类的介绍者
Sender 发送器 协议编码为字节流
Session 会话,抽象会话,服务器和客户端会继承本类,客户端和服务器是有多态性的,但是抽象来看都是一个会话。

Attribute文件
namespace ZyGame.GameNet.Componet
{
    public enum ProtoType
    {
        ProtoSend = 0,
        ProtoRecv = 1,
    }

    public class ProtoAttribute : System.Attribute
    {
        public int Id { get; set; }
        public int Channel { get; set; }
        public ProtoType ProtoType { get; set; }
        
        public ProtoAttribute(int id, int channel, ProtoType protoType = ProtoType.ProtoRecv)
        {
            Id = id;
            Channel = channel;
            ProtoType = protoType;
        }
    }
}
Connector文件

网络库最核心的类实现

Connector是网络库最关键的类,需要能读懂。
发送介绍一下

数据来了,如果发送线程空闲则立马发送,如果发送线程正在发送数据,就排队发送。等发送线程完成一次发送任务后会检查发送队列,如果队列有数据会继续发送队列里面的数据。这个可以达到最高的发送效率。

队列里面数据如果排队够多就会一次发送可能发送的最多数据,理论上一次最多可以发送socket规定的缓存大小数据。

using System;
using System.Threading;
using System.Net;
using System.Net.Sockets;
using ZyGame.GameNet.Misc;
using System.Collections.Generic;

namespace ZyGame.GameNet.Componet
{
    public class Connector
    {
        private byte[] RecvBuffer { get; set; }
        private byte[] SendBuffer { get; set; }
        public Socket ConnectSocket { get; set; }
        private IPEndPoint EndPoint { get; set; }
        private SocketAsyncEventArgs RecvArgs { get; set; }
        private SocketAsyncEventArgs SendArgs { get; set; }
        private ManualResetEvent SendResetEvent { get; set; }
        private Sender Sender { get; set; }
        private Recver Recver { get; set; }
        private List<Protocol> SendProtoList { get; set; }

        public Connector(string ip, int port, Session session)
            : this(new IPEndPoint(IPAddress.Parse(ip), port), session)
        {
        }

        public Connector(IPEndPoint endpoint, Session session)
        {
            try
            {
                EndPoint = endpoint;
                ConnectSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
                RecvBuffer = new byte[NetSetting.SocketBufferSize];
                SendBuffer = new byte[NetSetting.SocketBufferSize];

                RecvArgs = new SocketAsyncEventArgs();
                RecvArgs.Completed += IOCompleted;
                RecvArgs.UserToken = session;
                RecvArgs.SetBuffer(RecvBuffer, 0, NetSetting.SocketBufferSize);

                SendArgs = new SocketAsyncEventArgs();
                SendArgs.Completed += IOCompleted;
                SendArgs.UserToken = session;
                SendArgs.SetBuffer(SendBuffer, 0, NetSetting.SocketBufferSize);
                SendResetEvent = new ManualResetEvent(true);

                Sender = new Sender();
                Recver = new Recver();

                SendProtoList = new List<Protocol>();

                Connect(session);
            }
            catch (Exception ex)
            {
                Logger.LogError(ex.ToString());
            }
        }

        public Connector(Socket socket, Session session)
        {
            try
            {
                ConnectSocket = socket;
                RecvBuffer = new byte[NetSetting.SocketBufferSize];
                SendBuffer = new byte[NetSetting.SocketBufferSize];

                RecvArgs = new SocketAsyncEventArgs();
                RecvArgs.Completed += IOCompleted;
                RecvArgs.SetBuffer(RecvBuffer, 0, NetSetting.SocketBufferSize);
                RecvArgs.UserToken = session;

                SendArgs = new SocketAsyncEventArgs();
                SendArgs.Completed += IOCompleted;
                SendArgs.SetBuffer(SendBuffer, 0, NetSetting.SocketBufferSize);
                SendArgs.UserToken = session;
                SendResetEvent = new ManualResetEvent(true);

                Sender = new Sender();
                Recver = new Recver();

                SendProtoList = new List<Protocol>();

                PostRecv();
            }
            catch (Exception ex)
            {
                Logger.LogError(ex.ToString());
            }
        }

        public void Connect(Session session)
        {
            try
            {
                if (ConnectSocket != null && !ConnectSocket.Connected)
                {
                    SocketAsyncEventArgs args = new SocketAsyncEventArgs
                    {
                        UserToken = session
                    };
                    args.Completed += IOCompleted;
                    args.RemoteEndPoint = EndPoint;
                    if (!ConnectSocket.ConnectAsync(args))
                    {
                        ConnectCompleted(args);
                    }
                }                                               
            }
            catch (Exception ex)
            {
                Logger.LogError(ex.ToString());
            }
        }

        private void IOCompleted(object sender, SocketAsyncEventArgs args)
        {
            switch (args.LastOperation)
            {
                case SocketAsyncOperation.Connect:
                    ConnectCompleted(args);
                    break;
                case SocketAsyncOperation.Disconnect:
                    DisconnectCompleted(args);
                    break;
                case SocketAsyncOperation.Receive:
                    RecvCompleted(args);
                    break;
                case SocketAsyncOperation.Send:
                    SendCompleted(args);
                    break;
                default:
                    Logger.LogWarn(string.Format("Connector IOCompleted error, args.LastOperation = {0}", args.LastOperation.ToString()));
                    break;
            }
        }

        public void Disconnect()
        {
            try
            {
                if (ConnectSocket != null && ConnectSocket.Connected)
                {
                    SocketAsyncEventArgs args = new SocketAsyncEventArgs();
                    args.Completed += IOCompleted;
                    if (!ConnectSocket.DisconnectAsync(args))
                    {
                        DisconnectCompleted(args);
                    }
                }
                else
                {
                    Close();
                }
            }
            catch (Exception ex)
            {
                Logger.LogWarn(ex.ToString());
            }
        }

        private void DisconnectCompleted(SocketAsyncEventArgs args)
        {
            if (args.SocketError != SocketError.Success)
            {
                Close();
            }
        }

        private void ConnectCompleted(SocketAsyncEventArgs args)
        {
            if (args.SocketError == SocketError.Success)
            {
                if (args.UserToken is Session session)
                {
                    session.OnConnectSuccess(ConnectSocket.RemoteEndPoint.ToString());
                }

                PostRecv();
            }
            else
            {
                if (args.UserToken is Session session)
                {
                    string message = string.Format("ErrorCode = {0}, Remote address = {1}", args.SocketError, args.RemoteEndPoint.ToString());
                    session.OnConnectError(message);
                }
            }
        }

        private void PostRecv()
        {
            try
            {
                if (ConnectSocket != null && ConnectSocket.Connected)
                {
                    if (!ConnectSocket.ReceiveAsync(RecvArgs))
                    {
                        RecvCompleted(RecvArgs);
                    }
                }
            }
            catch (Exception ex)
            {
                Logger.LogError(ex.ToString());
            }
        }

        private void RecvCompleted(SocketAsyncEventArgs args)
        {
            if (args.SocketError == SocketError.Success)
            {
                if (args.BytesTransferred > 0)
                {
                    string message = string.Format("Connectoer RecvCompletd, BytesTransferred = {0}", args.BytesTransferred);
                    if (args.UserToken is Session session)
                    {
                        List<Protocol> protoList = Recver.Decode(args.Buffer, args.BytesTransferred, session.Register);
                        if (protoList != null)
                        {
                            session.RecvProtoList(protoList);
                        }

                        session.OnRecvSuccess(message);
                    }

                    PostRecv();
                }
                else
                {
                    if (args.UserToken is Session session)
                    {
                        session.OnRemoteDisconnect(string.Format("SocketError = {0}", args.SocketError));
                    }
                }
            }
            else
            {
                string message = string.Format(" SocketError = {0}", args.SocketError);
                if (args.UserToken is Session session)
                {
                    session.OnRemoteDisconnect(message);
                }
            }
        }

        public void Send(Protocol proto)
        {
            if (SendResetEvent.WaitOne(0))
            {
                PostSend(proto);
            }
            else
            {
                lock (SendProtoList)
                {
                    SendProtoList.Add(proto);
                }
            }
        }

        public void PostSend()
        {
            int copyLen = 0;
            int copyIdx = 0;
            lock (SendProtoList)
            {
                if (SendProtoList.Count> 0)
                {
                    for (; copyIdx < SendProtoList.Count; copyIdx++)
                    {
                        byte[] buffer = Sender.Encoded(SendProtoList[copyIdx]);
                        if (copyLen + buffer.Length <= NetSetting.SocketBufferSize)
                        {
                            Buffer.BlockCopy(buffer,0, SendArgs.Buffer, copyLen, buffer.Length);
                            copyLen += buffer.Length;
                        }
                        else
                        {
                            break;
                        }
                    }

                    SendProtoList.RemoveRange(0, copyIdx);
                }
            }

            if (copyLen > 0)
            {
                SendArgs.SetBuffer(0, copyLen);
                if (ConnectSocket != null && ConnectSocket.Connected)
                {
                    if (!ConnectSocket.SendAsync(SendArgs))
                    {
                        SendCompleted(SendArgs);
                    }
                }
            }
            else
            {
                SendResetEvent.Set();
            }
        }

        private void PostSend(Protocol proto)
        {
            try
            {
                SendResetEvent.Reset();
                if (ConnectSocket != null && ConnectSocket.Connected)
                {
                    byte[] buffer = Sender.Encoded(proto);
                    Buffer.BlockCopy(buffer, 0, SendArgs.Buffer, 0, buffer.Length);
                    SendArgs.SetBuffer(0, buffer.Length);
                    if (!ConnectSocket.SendAsync(SendArgs))
                    {
                        SendCompleted(SendArgs);
                    }
                }
            }
            catch (Exception ex)
            {
                Logger.LogError(ex.ToString());
            }
        }

        private void SendCompleted(SocketAsyncEventArgs args)
        {
            if (args.SocketError == SocketError.Success)
            {
                string message = string.Format("Connector SendCompleted, BytesTransferred = {0}", args.BytesTransferred);
                if (args.UserToken is Session session)
                {
                    session.OnSendSuccess(message);
                }

                if (ConnectSocket != null && ConnectSocket.Connected)
                {
                    PostSend();
                }
            }
            else
            {
                string message = string.Format("Connector SendCompleted error, BytesTransferred = {0}, ErrorCode = {1}", args.BytesTransferred, args.SocketError.ToString());
                if (args.UserToken is Session session)
                {
                    session.OnSendError(message);
                }
            }
        }

        public void Close()
        {
            try
            {
                if (ConnectSocket != null)
                {
                    lock (SendProtoList)
                    {
                        SendProtoList.Clear();
                    }

                    if (ConnectSocket.Connected)
                    {
                        ConnectSocket.Shutdown(SocketShutdown.Both);
                    }

                    ConnectSocket.Close();
                    ConnectSocket = null;
                }
            }
            catch (Exception ex)
            {
                Logger.LogWarn(ex.ToString());
            }
        }
    }
}

这个类里面三个构造函数。 两个是为客户端准备的,一个是为服务器准备的,调用不能混了。
当然用户不应该直接调用本类。用户能看到只是session

Internal类

网络库内部系统,同步sessionid使用

using ProtoBuf;
using System.Reflection;
using ZyGame.GameNet.Misc;

namespace ZyGame.GameNet.Componet
{
    internal static class InterProtoId
    {
        internal const int SyncSession = 1;

        internal const int MaxLimit = 100;
    }

    [Proto(InterProtoId.SyncSession, NetSetting.InterProtoChannel)]
    [ProtoContract]
    internal class InterProtoSyncSession : Protocol
    {
        [ProtoMember(1)]
        internal int SessionId { get; set; }

        internal InterProtoSyncSession()
            : base(InterProtoId.SyncSession)
        {
        }

        public override void Process(Session session)
        {
            session.OnCreateSuccess(SessionId, string.Empty);
        }
    }

    internal static class InternalRegister
    {
        internal static Register Register { get; private set; }

        static InternalRegister()
        {
            Register = new Register();
            var assembly = Assembly.GetExecutingAssembly();
            foreach (var item in assembly.GetTypes())
            {
                var attr = item.GetCustomAttribute<ProtoAttribute>();
                if (attr != null)
                {
                    if (attr.Channel == NetSetting.InterProtoChannel)
                    {
                        if (attr.ProtoType == ProtoType.ProtoSend)
                        {
                            Register.SendDict.Add(attr.Id, item);
                        }
                        else
                        {
                            Register.RecvDict.Add(attr.Id, item);
                        }
                    }
                    
                }
            }
        }
    }
}
Protocol 文件

协议实现文件,您一定惊叹实现的简单

namespace ZyGame.GameNet.Componet
{
    public class Protocol
    {
        public int Id { get; private set; }

        public Protocol(int id)
        {
            Id = id;
        }

        public virtual void Process(Session session)
        {
        }
    }
}

我看着都很清爽。代码简单明了,就是为了说明协议是有协议号的。

Recver类

利用ProtobufUtil和Registe将字节流解释成为协议流。
一次socket接受不一定是一条协议,有可能是很多条协议一起接受到的,所以是协议链表

using System;
using System.Collections.Generic;
using ZyGame.GameNet.Misc;

namespace ZyGame.GameNet.Componet
{
    public class Recver
    {
        int pos;
        int length;

        byte[] buffer;
        byte[] bufferPrefix;
        byte[] msgPrefix;

        public Recver()
        {
            buffer = new byte[NetSetting.DecodeBufferSize];
            bufferPrefix = new byte[NetSetting.BufferPrefixSize];
            msgPrefix = new byte[NetSetting.MsgPrefixSize];

            Reset();
        }

        private void Reset()
        {
            pos = 0;
            length = 0;
        }

        public List<Protocol> Decode(byte[] recvbuffer, int grow, Register register)
        {
            try
            {
                List<Protocol> list = new List<Protocol>();
                Buffer.BlockCopy(recvbuffer, 0, buffer, pos, grow);
                pos += grow;

                while (pos > 4)
                {
                    if (length <= 0)
                    {
                        Buffer.BlockCopy(buffer, 0, bufferPrefix, 0, NetSetting.BufferPrefixSize);
                        length = BitConverter.ToInt32(bufferPrefix, 0);
                    }

                    int curLength = pos - NetSetting.ProtoPrefixSize;
                    if (curLength >= length)//够一个消息了
                    {
                        Buffer.BlockCopy(buffer, NetSetting.BufferPrefixSize, msgPrefix, 0, NetSetting.MsgPrefixSize);
                        int msgid = BitConverter.ToInt32(msgPrefix, 0);

                        byte[] msgBuffer = new byte[length];
                        Buffer.BlockCopy(buffer, NetSetting.ProtoPrefixSize, msgBuffer, 0, length);

                        int len = pos - NetSetting.ProtoPrefixSize - length;
                        if (len > 0)
                        {
                            Buffer.BlockCopy(buffer, NetSetting.ProtoPrefixSize + length, buffer, 0, len);
                            pos = len;
                            length = 0;
                        }
                        else
                        {
                            Reset();
                        }

                        if (msgid < InterProtoId.MaxLimit)
                        {
                            if (ProtoBufUtils.Deserialize(msgBuffer, InternalRegister.Register.RecvDict[msgid]) is Protocol proto)
                            {
                                list.Add(proto);
                            }
                        }
                        else
                        {
                            if (ProtoBufUtils.Deserialize(msgBuffer, register.RecvDict[msgid]) is Protocol proto)
                            {
                                list.Add(proto);
                            }
                        }
                    }
                    else
                    {
                        break;
                    }
                }
                return list;
            }
            catch (Exception ex)
            {
                Logger.LogWarn(ex.ToString());
                return null;
            }
        }
    }
}
Register文件

这个就是一个dictionary,通过协议号找到对应的协议类型

using System;
using System.Collections.Generic;
using ZyGame.GameNet.Misc;

namespace ZyGame.GameNet.Componet
{
    public class Register
    {
        public Dictionary<int, Type> SendDict { get; private set; }
        public Dictionary<int, Type> RecvDict { get; private set; }

        public Register()
        {
            SendDict = new Dictionary<int, Type>();
            RecvDict = new Dictionary<int, Type>();
        }

        public bool RegistSendProto(int id, Type proto)
        {
            try
            {
                SendDict.Add(id, proto);
                return true;
            }
            catch (Exception ex)
            {
                Logger.LogWarn(ex.ToString());
                return false;
            }
        }

        public bool RegistRecvProto(int id, Type proto)
        {
            try
            {
                RecvDict.Add(id, proto);
                return true;
            }
            catch (Exception ex)
            {
                Logger.LogWarn(ex.ToString());
                return false;
            }
        }
    }
}

Session 文件

库第二核心文件,对conncter进行封装,让用户可以有自己的实现。嵌入网络库。

把网络库设想成一台机子,当发生某些事件时用户想做出某些对应行为就需要继承这个类,实现自己的会话行为。

using System.Collections.Generic;
using ZyGame.GameNet.Misc;

namespace ZyGame.GameNet.Componet
{
    public class Session
    {
        public Connector Connector { get; protected set; }
        public int SessionId { get; private set; }
        public Register Register { get; protected set; }
        public object User { get; set; }
        public bool Opened { get; private set; }
        
        public Session()
        {
        }

        public void Send(Protocol proto)
        {
            if (Opened && Connector != null)
            {
                Connector.Send(proto);
            }
        }

        protected void Open()
        {
            Opened = true;
        }

        public virtual void Close(bool local = true)
        {
            if (local)
            {
                if (Opened)
                {
                    if (Connector != null)
                    {
                        Connector.Disconnect();
                    }

                    Opened = false;
                }
                else
                {
                    if (Connector != null)
                    {
                        Connector.Close();
                    }
                }
            }
            else
            {
                if (Connector != null)
                {
                    Connector.Close();
                }

                Opened = false;
            }
        }

        public virtual void RecvProtoList(List<Protocol> protoList)
        {
            foreach (var item in protoList)
            {
                item.Process(this);
            }
        }

        public virtual void OnCreateSuccess(int sessionId, string message)
        {
            SessionId = sessionId;

            if (string.IsNullOrEmpty(message))
            {
                Logger.LogInfo(string.Format("Session Create success, id = {0}", SessionId));
            }
            else
            {
                Logger.LogInfo(string.Format("Session Created success, id = {0}, remote address = {1}", SessionId, message));
            }
        }

        public virtual void OnConnectSuccess(string message)
        {
            Open();
        }

        public virtual void OnConnectError(string message)
        {
            Close();
            Logger.LogInfo(string.Format("Session Connected Failed, {0}", message));
        }

        public virtual void OnSendSuccess(string message)
        {
        }

        public virtual void OnSendError(string message)
        {
            Close();
        }

        public virtual void OnRecvSuccess(string message)
        {
        }

        public virtual void OnRemoteDisconnect(string message)
        {
            Logger.LogInfo(string.Format("Session Remote disconnect, id = {0},  {1}", SessionId, message));
            Close(false);
        }
    }
}

Componet名空间是网络库的核心,用这个库大部分情况需要引入这个命空间。

Client名空间

客户端程序用到
在这里插入图片描述
里面只有一个文件,主要是为了变化打开会话,实现很简单。
这个里面会解释信道的作用,通过信号标记的协议类型来确定本会话的协议集合。

网上找真正高质量代码不容易的,客户端接收和服务器接收是不一样的。由于客户端一帧时间一般很少, 就是著名的延迟一帧技术,所以就会在游戏心跳里面处理接收。

由于网络接收是在不同线程,所以需要把数据从网络线程拷贝出来,在主线程处理。这个是产品级别代码,不是写一个demo糊弄鬼。

using System.Collections.Generic;
using System.Reflection;
using ZyGame.GameNet.Componet;

namespace ZyGame.GameNet.Client
{
    public class ClientSession : Session
    {
        private List<Protocol> RecvList { get; set; }

        public ClientSession(int channel)
        {
            Register = new Register();
            var assembly = Assembly.GetEntryAssembly();
            if (assembly != null)
            {
                foreach (var item in assembly.GetTypes())
                {
                    var attr = item.GetCustomAttribute<ProtoAttribute>();
                    if (attr == null)
                    {
                        continue;
                    }

                    if (attr.Channel != channel)
                    {
                        continue;
                    }

                    if (attr.ProtoType == ProtoType.ProtoSend)
                    {
                        Register.SendDict.Add(attr.Id, item);
                    }
                    else
                    {
                        Register.RecvDict.Add(attr.Id, item);
                    }
                }
            }

            RecvList = new List<Protocol>();
        }

        public bool Open(string ip, int port)
        {
            if (Register == null)
            {
                return false;
            }

            Connector = new Connector(ip, port, this);
            return true;
        }

        public override void RecvProtoList(List<Protocol> protoList)
        {
            lock (RecvList)
            {
                RecvList.AddRange(protoList);
            }
        }

        public void Tick()
        {
            lock (RecvList)
            {
                foreach (var item in RecvList)
                {
                    item.Process(this);
                }

                RecvList.Clear();
            }
        }
    }
}

Server 名空间

服务器程序用到
在这里插入图片描述
里面三个文件,ServerSession和ServerSessionManager是一组。listener是服务用到的。

Listener文件

监听器,需要信道,通过本监听器建立的会话共享一个信道,协议集合。

如果用户实现了自己的ServerSession【继承自ServerSession】,则可以设置CreateSessionMethod 构建自定义会话。

using System;
using System.Threading;
using System.Net;
using System.Net.Sockets;
using ZyGame.GameNet.Componet;
using ZyGame.GameNet.Misc;
using System.Reflection;

namespace ZyGame.GameNet.Server
{
    public class Listener
    {
        private Socket AcceptSocket { get; set; }
        public Register Register { get; private set; }
        public Func<Register, ServerSession> CreateSessionMethod { get; set; }
        private ManualResetEvent AcceptResetEvent { get; set; }
        private SocketAsyncEventArgs AcceptArgs { get; set; }

        public Listener(int channel)
        {
            Register = new Register();
            var assembly = Assembly.GetEntryAssembly();
            if (assembly != null)
            {
                foreach (var item in assembly.GetTypes())
                {
                    var attr = item.GetCustomAttribute<ProtoAttribute>();
                    if (attr == null)
                    {
                        continue;
                    }

                    if (attr.Channel != channel)
                    {
                        continue;
                    }

                    if (attr.ProtoType == ProtoType.ProtoSend)
                    {
                        Register.SendDict.Add(attr.Id, item);
                    }
                    else
                    {
                        Register.RecvDict.Add(attr.Id, item);
                    }
                }
            }
        }

        public bool StartListen(string ip, int port)
        {
            return StartListen(new IPEndPoint(IPAddress.Parse(ip), port));
        }

        public bool StartListen(IPEndPoint endpoint)
        {
            try
            {
                if (Register != null)
                {
                    if (CreateSessionMethod == null)
                    {
                        Logger.LogWarn("Listener CreateSessionMethod is null, will create ServerNet default Session.");
                    }

                    AcceptArgs = new SocketAsyncEventArgs();
                    AcceptArgs.Completed += IOCompleted;

                    AcceptResetEvent = new ManualResetEvent(true);

                    AcceptSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
                    AcceptSocket.Bind(endpoint);
                    AcceptSocket.Listen(NetSetting.MaxConnect);
                    PostAccept();
                    Logger.LogInfo(string.Format("Start listen ip = {0}, port = {1}", endpoint.Address.ToString(), endpoint.Port));
                    return true;
                }
                else
                {
                    Logger.LogError("Listener Register is null");
                    return false;
                }
            }
            catch (Exception ex)
            {
                Logger.LogError(ex.ToString());
                return false;
            }
        }

        private void PostAccept()
        {
            try
            {
                AcceptResetEvent.WaitOne();
                AcceptResetEvent.Reset();

                AcceptArgs.AcceptSocket = null;
                if (!AcceptSocket.AcceptAsync(AcceptArgs))
                {
                    AcceptCompleted(AcceptArgs);
                }
            }
            catch (Exception ex)
            {
                Logger.LogError(ex.ToString());
            }
        }

        private void IOCompleted(object sender, SocketAsyncEventArgs args)
        {
            switch (args.LastOperation)
            {
                case SocketAsyncOperation.Accept:
                    AcceptCompleted(args);
                    break;
                default:
                    Logger.LogWarn(string.Format("The last operation completed on the socket was not a valid operation, error = {0}", args.LastOperation.ToString()));
                    break;
            }
        }

        private void AcceptCompleted(SocketAsyncEventArgs args)
        {
            if (args.SocketError == SocketError.Success)
            {
                if (CreateSessionMethod != null)
                {
                    var session = CreateSessionMethod(Register);
                    if (session != null)
                    {
                        session.Open(args.AcceptSocket);
                    }
                }
                else
                {
                    var session = new ServerSession(Register);
                    session.Open(args.AcceptSocket);
                }

                AcceptResetEvent.Set();
                PostAccept();
            }
            else
            {
                AcceptResetEvent.Set();
                Logger.LogError(string.Format("AcceptCompleted error, BytesTransferred = {0}, error = {1}", args.BytesTransferred, args.SocketError.ToString()));
            }
        }
    }
}
ServerSession和ServerSessionManager

这两个文件是对服务器端会话管理的。

ServerSession是服务器监听器建立的会话抽象

using System.Net.Sockets;
using ZyGame.GameNet.Componet;

namespace ZyGame.GameNet.Server
{
    public class ServerSession : Session
    {
        private static int SessionIdSeed = 0;

        public ServerSession(Register register)
        {
            Register = register;
        }

        private int GenerateSessionId()
        {
            return ++SessionIdSeed;
        }

        public bool Open(Socket socket)
        {
            if (Register == null)
            {
                return false;
            }

            Open();
            Connector = new Connector(socket, this);
            OnCreateSuccess(GenerateSessionId(), socket.RemoteEndPoint.ToString());
            ServerSessionManager.AddSession(this);
            return true;
        }

        public override void OnCreateSuccess(int sessionId, string message)
        {
            base.OnCreateSuccess(sessionId, message);

            var proto = new InterProtoSyncSession
            {
                SessionId = SessionId
            };
            Send(proto);
        }

        public override void Close(bool local = true)
        {
            base.Close(local);
            ServerSessionManager.RemoveSession(this);
        }
    }
}

ServerSessionManager是对所有ServerSession 的管理,线程安全。

using System.Collections.Generic;
using System.Threading;
using System;
using ZyGame.GameNet.Componet;

namespace ZyGame.GameNet.Server
{
    public static class ServerSessionManager
    {
        private static List<ServerSession> SessionList { get; set; }
        private static Queue<ServerSession> RemoveQueue { get; set; }
        private static Queue<ServerSession> AddQueue { get; set; }

        static ServerSessionManager()
        {
            SessionList = new List<ServerSession>();
            RemoveQueue = new Queue<ServerSession>();
            AddQueue = new Queue<ServerSession>();
        }

        public static void AddSession(ServerSession session)
        {
            lock (AddQueue)
            {
                AddQueue.Enqueue(session);
            }
        }

        public static void RemoveSession(ServerSession session)
        {
            lock (RemoveQueue)
            {
                RemoveQueue.Enqueue(session);
            }
        }

        public static void Tick()
        {
            lock (AddQueue)
            {
                while (AddQueue.Count > 0)
                {
                    SessionList.Add(AddQueue.Dequeue());
                }
            }

            lock (RemoveQueue)
            {
                while (RemoveQueue.Count > 0)
                {
                    SessionList.Remove(RemoveQueue.Dequeue());
                }
            }
        }

        public static void Foreach(Action<Session> func)
        {
            foreach (var item in SessionList)
            {
                func(item);
            }
        }

        public static ServerSession GetServerSession(int id)
        {
            return SessionList.Find((session) => {
                return session.SessionId == id;
            });
        }
    }
}

总结

网络库核心主要是五个部分。
Accept 这个行为因为有可能在某一时刻很多连接同时发生,所以一定要有线程同步逻辑。我用信号控制排队。

Connet 由于是客户端发起,并且是有限次行为【几乎就是一次】所以可以不用考虑线程同步,回调完成即可。

Send 发送想达到最优还是需要协议队列的。如果想等-发-等-发,虽然能实现功能,但是会有延迟。

Recv 接收需要考虑主线程和网络线程哪里处理协议的问题。如果想在主线程处理就需要接收队列,想在网络线程处理就不需要接收队列。要考虑接收一次可能是很多条协议的情况。

模块之间尽量不要共享数据, 要共享就是模块和主线程共享。比如网络线程和主线程还有假设一个IO线程吧,虽然可以整一个巨大的事件分发器三者互斥,但是效率下降很多。我们要的效果是主线程和单一模块互斥,不是子模块之间互斥。唉,扯不明白,不过是一个提升效率的地方。只可意会不可言传。

Close 优雅的关闭会话,不管是出错还是用户发起。我这个网络库感觉还需要在琢磨一下。

这个网络库实现写了有一周左右吧,具体时间我也不知道,想起来就写,然后就测,改过很多地方,最后感觉有点样子。

最后,希望能多和您交流,如果您觉得有什么地方可以修改,希望您留言评论。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

当当小螳螂

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值