GameNet 遊戲網絡庫 C#實現

介紹

很久沒有寫博客,面試了幾次發現自己實現的東西有點少,面試官問覺得自己就是搬磚,沒什麼意思,就寫點東西。

協議用protobuf-net
.net 版本用4.5
socket用SocketAsyncEventArgs
實現網絡庫
寫服務器和客戶端測試程序

代碼本身不難,很多零碎的知識點穿插起來難度就會提升。每一個模塊我儘量列舉實現的原因,有可能遇到的坑。

先看測試程序。
服務器監聽一個端口
客戶端維護64個連接與服務器通信
在這裏插入圖片描述

服務器測試程序

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

//需要引用庫文件ZyGame.GameNet.dll 和 protobuf-net.dll
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.Foreach((session)=> {
                    var proto = new ProtoTest();
                    proto.Info = string.Format("Hello client, time = {0}",
                        DateTime.Now.ToString());
                    session.Send(proto);
                });

               await Task.Delay(50);
            }
        }
        //主函數
        static void Main(string[] args)
        {
            new Program().Run().Wait();
        }
    }
}

服務器測試程序代碼很少,只是爲了說明如何使用這個庫。

客戶端測試程序

客戶端發起64個連接,一個會話維持一個連接,

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()
        {
            //一個程序建立64個會話
            //64個已經說明一些問題了,寫一兩個連接沒有出錯不能說明可以用到生產環境。
            List<ClientSession> sessionList = new List<ClientSession>();
            for (int idx = 0; idx < 64; idx++)
            {//創建會話
                ClientSession session = new ClientSession(TestClientChannel.Client);
                session.Open("127.0.0.1", 9898);
                sessionList.Add(session);
            }

            while (true)
            {
                foreach (var item in sessionList)
                {//爲了測試,每條會話都在不停的發送消息
                    var proto = new ProtoHello();
                    proto.Info = string.Format("Hello Server, time = {0}", DateTime.Now.ToString());
                    item.Send(proto);
                }

                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 InterChannel = 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文件

網絡庫最核心的類實現

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 ManualResetEvent RecvResetEvent { get; set; }
        private SocketAsyncEventArgs SendArgs { get; set; }
        private ManualResetEvent SendResetEvent { get; set; }
        private Sender Sender { get; set; }
        private Recver Recver { 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);
                RecvResetEvent = new ManualResetEvent(true);

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

                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;
                RecvResetEvent = new ManualResetEvent(true);

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

                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
                {
                    ConnectSocket.Close();
                }
            }
            catch (Exception ex)
            {
                Logger.LogWarn(ex.ToString());
            }
        }

        private void DisconnectCompleted(SocketAsyncEventArgs args)
        {
            if (args.SocketError == SocketError.Success)
            {
                Close();
            }
            else
            {
                Close();
                string message = string.Format("Connector DisconnectCompleted error, BytesTransferred = {0}, ErrorCode = {1}", args.BytesTransferred, args.SocketError.ToString());
                Logger.LogError(message);
            }
        }

        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
            {
                RecvResetEvent.WaitOne();
                if (ConnectSocket != null && ConnectSocket.Connected)
                {
                    RecvResetEvent.Reset();
                    if (!ConnectSocket.ReceiveAsync(RecvArgs))
                    {
                        RecvCompleted(RecvArgs);
                    }
                }
            }
            catch (Exception ex)
            {
                RecvResetEvent.Set();
                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)
                        {
                            foreach (var item in protoList)
                            {
                                item.Process(session);
                            }
                        }

                        session.OnRecvSuccess(message);
                    }

                    RecvResetEvent.Set();
                    PostRecv();
                }
                else
                {
                    string message = string.Format("Connectoer RecvCompletd, BytesTransferred = {0}", args.BytesTransferred);
                    if (args.UserToken is Session session)
                    {
                        session.OnRecvSuccess(message);
                    }
                    RecvResetEvent.Set();
                    PostRecv();
                }
            }
            else
            {
                if (args.SocketError == SocketError.ConnectionReset)
                {
                    if (args.UserToken is Session session)
                    {
                        session.OnRemoteDisconnect(ConnectSocket.RemoteEndPoint.ToString());
                    }
                }
                else
                {
                    string message = string.Format("Connector RecvCompleted error, BytesTransferred = {0}, ErrorCode = {1}", args.BytesTransferred, args.SocketError.ToString());
                    if (args.UserToken is Session session)
                    {
                        session.OnRecvError(message);
                    }
                    Logger.LogError(message);
                }
                RecvResetEvent.Set();
            }
        }

        public void PostSend(Protocol proto)
        {
            try
            {
                SendResetEvent.WaitOne();
                if (ConnectSocket != null && ConnectSocket.Connected)
                {
                    byte[] buffer = Sender.Encoded(proto);
                    Array.Copy(buffer, 0, SendArgs.Buffer, 0, buffer.Length);
                    SendArgs.SetBuffer(0, buffer.Length);
                    SendResetEvent.Reset();
                    if (!ConnectSocket.SendAsync(SendArgs))
                    {
                        SendCompleted(SendArgs);
                    }
                }
            }
            catch (Exception ex)
            {
                SendResetEvent.Set();
                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);
                }
            }
            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);
                }
            }
            SendResetEvent.Set();
        }

        public void Close()
        {
            try
            {
                if (ConnectSocket != null)
                {
                    if (ConnectSocket.Connected)
                    {
                        ConnectSocket.Shutdown(SocketShutdown.Both);
                    }

                    ConnectSocket.Close();
                }
            }
            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 InterProtoType
    {
        internal const int SyncSession = 1;

        internal const int MaxLimit = 100;
    }

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

        internal InterProtoSyncSession()
            : base(InterProtoType.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.InterChannel)
                    {
                        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>();
                Array.Copy(recvbuffer, 0, buffer, pos, grow);
                pos += grow;

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

                    int curLength = pos - NetSetting.ProtoPrefixSize;
                    if (curLength >= length)//夠一個消息了
                    {
                        Array.Copy(buffer, NetSetting.BufferPrefixSize, msgPrefix, 0, NetSetting.MsgPrefixSize);
                        int msgid = BitConverter.ToInt32(msgPrefix, 0);

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

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

                        if (msgid < InterProtoType.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 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.PostSend(proto);
            }
        }

        protected void Open()
        {
            Opened = true;
        }

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

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

        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();
            Logger.LogInfo(string.Format("Session Connect success, remote address = {0}", message));
        }

        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 OnRecvError(string message)
        {
            Close();
        }

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

Componet名空間是網絡庫的核心,用這個庫大部分情況需要引入這個命空間。

Client名空間

客戶端程序用到
在這裏插入圖片描述
裏面只有一個文件,主要是爲了變化打開會話,實現很簡單。
這個裏面會解釋信道的作用,通過信號標記的協議類型來確定本會話的協議集合。

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

namespace ZyGame.GameNet.Client
{
    public class ClientSession : Session
    {
        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);
                    }
                }
            }
        }

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

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

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()
        {
            base.Close();
            ServerSessionManager.RemoveSession(SessionId);
        }
    }
}


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 Dictionary<int, ServerSession> SessionDict { get; set; }
        private static ManualResetEvent SessionResetEvent { get; set; }

        static ServerSessionManager()
        {
            SessionDict = new Dictionary<int, ServerSession>();
            SessionResetEvent = new ManualResetEvent(true);
        }

        public static void AddSession(ServerSession session)
        {
            SessionResetEvent.WaitOne();
            SessionResetEvent.Reset();
            SessionDict.Add(session.SessionId, session);
            SessionResetEvent.Set();
        }

        public static void RemoveSession(int sessionId)
        {
            SessionResetEvent.WaitOne();
            SessionResetEvent.Reset();
            SessionDict.Remove(sessionId);
            SessionResetEvent.Set();
        }

        public static void Foreach(Action<Session> func)
        {
            SessionResetEvent.WaitOne();
            SessionResetEvent.Reset();
            foreach (var item in SessionDict)
            {
                func(item.Value);
            }
            SessionResetEvent.Set();
        }

        public static ServerSession GetServerSession(int id)
        {
            SessionResetEvent.WaitOne();
            SessionResetEvent.Reset();
            if (SessionDict.ContainsKey(id))
            {
                SessionResetEvent.Set();
                return SessionDict[id];
            }
            else
            {
                SessionResetEvent.Set();
                return null;
            }
        }
    }
}

總結

這個網絡庫實現寫了有一週左右吧,具體時間我也不知道,想起來就寫,然後就測,改過很多地方,最後感覺有點樣子。

最後,希望能多和您交流,如果您覺得有什麼地方可以修改,希望您留言評論。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章