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;
            }
        }
    }
}

总结

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

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

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