极简教程2:C# Socket客户端框架

如果你不能用最简单的语言来描述,那你就是没有真正领悟。——爱因斯坦

有关Socket原理的文章,网络上可谓是铺天盖地,在这里我就不再赘述,直接上干货。

一、思维导图

先来通过一张脑图,将整体设计结构展现出来。
在这里插入图片描述

二、核心文件

整个框架共九个类文件,分别负责独立的业务逻辑。上图中已经简明的描述了每个类文件的作用。下面,我将列出几个核心文件的代码逻辑。

1、GameSocket

核心类文件,控制着socket连接的整体流程

public class GameSocket
    {
        //Socket连接回调函数
        private static Dictionary<SocketEvent, Action> socketEventHandles = new Dictionary<SocketEvent, Action>();
        //Socket实例对象;
        public Socket socket;
        //最大重连次数;
        private int maxReTryTime = 2;
        //当前重连次数;
        private int reTryTime = 0;
        //服务器地址;
        private string serverIp;
        //服务器端口;
        private int port;
        //连接状态标记;
        private bool connected = false;
        //是否正在重连;
        private bool reConnect = false;



        #region 回调相关函数
        /// <summary>
        /// 注册socket连接回调函数
        /// </summary>
        /// <param name="conSucess">连接成功回调</param>
        /// <param name="conFaild">连接失败回调</param>
        public void RegisterSocketEvent(SocketEvent eventName ,Action action)
        {
            if (!socketEventHandles.ContainsKey(eventName))
                socketEventHandles.Add(eventName, action);
            else
                socketEventHandles[eventName] += action;
        }

        /// <summary>
        /// 移除Socket连接事件回调
        /// </summary>
        /// <param name="type">协议实体类型</param>
        /// <param name="action">回调逻辑</param>
        public void UnRegisterSocketEvent(SocketEvent eventName, Action action)
        {
            if (socketEventHandles.ContainsKey(eventName))
            {
                socketEventHandles[eventName] -= action;
                if (socketEventHandles[eventName] == null)
                    socketEventHandles.Remove(eventName);
            }
        }

        /// <summary>
        /// 执行Socket连接事件回调方法;
        /// </summary>
        /// <param name="type"></param>
        public void ExcuteSocketEventHandle(SocketEvent eventName)
        {
            if (socketEventHandles.ContainsKey(eventName))
            {
                Action action = socketEventHandles[eventName];
                action.Invoke();
            }
        }

        #endregion

        #region 启动与中断
        /// <summary>
        /// 请求连接
        /// </summary>
        /// <param name="ip"></param>
        /// <param name="port"></param>
        public void Connect(string serverIp, int port)
        {
            socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
            try
            {
                //记录当前的地址和端口
                this.serverIp = serverIp;
                this.port = port;
                //只能识别ip
                //IPAddress ipAddress = IPAddress.Parse(server);
                //兼容ip与域名;
                IPHostEntry host = Dns.GetHostEntry(serverIp);
                IPAddress ip = host.AddressList[0];

                //创建连接对象
                IPEndPoint ipEndPoint = new IPEndPoint(ip, port);
                Tracer.Log("启动连接 ip:" + ip + " port:" + port);
                socket.Connect(ipEndPoint);
                if (socket.Connected)
                    OnConnectSucess();
                else
                    OnConnectFaild();
            }
            catch (Exception e)
            {
                OnConnectFaild(e);
            }
        }

        /// <summary>
        /// 重连;
        /// 
        /// TODO,该方法需要被重写;
        /// </summary>
        public void ReConnect()
        {
            if (maxReTryTime > reTryTime)
            {
                reConnect = true;
                reTryTime++;
                Tracer.Log("开始进行第" + reTryTime + "次重连");
                Connect(serverIp, port);
            }
            else
            {
                reConnect = false;
                OnConnectFaild();
            }
        }

        /// <summary>
        /// 停止socket连接
        /// </summary>
        public void Dispose()
        {
            if (socket != null)
            {
                if (socket.Connected)
                    //禁用Socket的发送和接收功能,
                    socket.Shutdown(SocketShutdown.Both);
                //释放托管资源
                socket.Close();
                //释放非托管资源;
                socket.Dispose();
                socket = null;
            }
        }
        #endregion

        #region 连接状态
        /// <summary>
        /// 连接成功;
        /// </summary>
        private void OnConnectSucess()
        {
            Tracer.Log("连接成功");
            connected = true;
            //启动Socket的线程;
            GameSocketMgr.Self.StartThread();
            //执行连接成功的回调
            ExcuteSocketEventHandle(SocketEvent.CONNECT_SUCESS);
        }

        /// <summary>
        /// 连接失败;
        /// </summary>
        private void OnConnectFaild(Exception e = null)
        {
            if (reConnect)
                ReConnect();
            else
            {
                Tracer.Log("连接失败");
                connected = false;
                //执行连接失败的回调
                ExcuteSocketEventHandle(SocketEvent.CONNECT_FAILD);
            }
        }

        /// <summary>
        /// 不明原因引起的连接中断,包含正常关闭以及非正常关闭
        /// </summary>
        public void OnConnectClose()
        {
            Tracer.Log("连接中断");
            connected = false;
            ExcuteSocketEventHandle(SocketEvent.CONNECT_CLOSE);
        }
        #endregion

        /// <summary>
        /// 是否连接中
        /// </summary>
        public bool BeConnected
        {
            get { return connected; }
        }
    }

2、SocketSender

发送线程类。该类将所有待发送的协议字节流存入了待发送列表,然后在独立的线程内进行循环,最终将数据送入Socket管道。


    public class SocketSender
    {
        //发送线程;
        private Thread sendThread = null;
        //待发送列表;
        private Queue<byte[]> sendList = new Queue<byte[]>();

        /// <summary>
        /// 启动发送线程
        /// </summary>
        public void Start()
        {
            //清空发送列表;
            sendList.Clear();
            Loom.RunAsync(
              () =>
              {
                  sendThread = new Thread(new ThreadStart(ProtoSend));
                  sendThread.IsBackground = true;
                  sendThread.Start();
              }
              );
        }

        /// <summary>
        /// 将通讯协议压入待发送队列
        /// </summary>
        /// <param name="msg">消息结构体</param>
        public void ProtoEnqueue(CmdNo num,IExtensible msg)
        {
            //将协议进行序列化处理;
            byte[] buff = ConvertTools.ProtoToBytes(num,msg);
            sendList.Enqueue(buff);

        }

        /// <summary>
        /// 将待发送队列中的协议推入通信管道
        /// </summary>
        private void ProtoSend()
        {
            Socket socket = GameSocketMgr.Self.Socket;
            while (GameSocketMgr.Self.BeConnected)
            {
                if (sendList.Count > 0)
                {
                    byte[] buff = sendList.Dequeue();
                    socket.Send(buff, buff.Length, SocketFlags.None);
                }
                //重新分配CPU资源
                Thread.Sleep(0);
            }
        }

        /// <summary>
        /// 释放发送线程
        /// </summary>
        public void Dispose()
        {
            try
            {
                Tracer.Log("释放发送线程");
                sendThread.Abort();
                sendThread.Join();
            }
            //线程异常,不需处理
            catch (ThreadAbortException ex)
            {

            }
            catch (Exception e)
            {
                Tracer.Log("E:" + e, LogLevel.ERROR);
            }
        }
    }

3、SocketReceiver

接受线程类。该类将在独立的线程内将所有接受到的字节流存入了待处理列表,然后转化成协议体实例,执行回调逻辑。

 public class SocketReceiver
    {
        //字节对象;
        private BytesBuff bytesBuff = new BytesBuff();
        //接受字节数组
        private byte[] tmpReceiveBuff = new byte[ReadMax];
        //接受线程
        private Thread receiveThread = null;
        //最大缓存
        private const int ReadMax = 1024;

        /// <summary>
        /// 启动协议接受线程
        /// </summary>
        public void Start()
        {
            //清空接受列表
            bytesBuff.Reset();
            Loom.RunAsync(
              () =>
              {
                  receiveThread = new Thread(new ThreadStart(ProtoEnqueue));
                  receiveThread.IsBackground = true;
                  receiveThread.Start();
              }
              );
        }

        /// <summary>
        /// 将通讯协议压入待处理队列
        /// </summary>
        private void ProtoEnqueue()
        {
            Socket socket = GameSocketMgr.Self.Socket;
            while (GameSocketMgr.Self.BeConnected)
            {
                //socket已经断开,或者不存在可以读取的数据;
                if (!socket.Connected || socket.Available <= 0)
                    continue;
                try
                {
                    int receiveLength = socket.Receive(tmpReceiveBuff, 0, ReadMax, SocketFlags.None);
                    if (receiveLength > 0)
                    {
                        //将接受的数据压入当前字节流;
                        bytesBuff.MergeByte(tmpReceiveBuff, (uint)receiveLength);
                        ProtoExcute();
                    }
                }
                catch (Exception e)
                {
                    Debug.Log(e);
                }
                //重新分配CPU资源
                Thread.Sleep(0);
            }
        }

        /// <summary>
        /// 释放协议接受线程
        /// </summary>
        public void Dispose()
        {
            try
            {
                Tracer.Log("释放接受线程");
                receiveThread.Abort();
                receiveThread.Join();
            }
            //线程异常,不需处理
            catch (ThreadAbortException ex)
            { 
            
            }
            catch (Exception e)
            {
                Tracer.Log("E:" + e, LogLevel.ERROR);
            }
        }

        /// <summary>
        /// 执行协议的逻辑
        /// </summary>
        /// <param name="val"></param>
        /// <param name="len"></param>
        public void ProtoExcute()
        {
            while (bytesBuff.ReadValid)
            {
                byte[] bytes = bytesBuff.ReadByte();
                IExtensible proto = ConvertTools.BytesToProto(bytes);
                if (proto != null)
                {
                    //切换至主线程,进行协议方法的回调
                    Loom.QueueOnMainThread(() =>
                    {
                        GameSocketMgr.Self.ExcuteProtoHandle(proto.GetType(), proto);
                    });
                }
            }
        }
    }

4、ConvertTools

转换工具类,将字节流与ProtoBuff协议实例进行相互转换。注意,本类中有关协议体的结构是依据我目前项目的结构而进行读取的。各位在使用时,需要根据自己项目中的协议结构进行变通。

public class ConvertTools
    {
        //协议解析器;
        private static ProtobufSerializer protobufSerializer = new ProtobufSerializer();

        #region 字节转化成Protobuff
        /// <summary>
        /// 将ProtoMessage转化为proto
        /// </summary>
        /// <param name="message"></param>
        /// <returns></returns>
        public static IExtensible BytesToProto(byte[] bytes)
        {
            //第三位开始的两字节代表协议号
            CmdNo num = (CmdNo)BitConverter.ToUInt16(bytes, sizeof(ushort));
            //将通讯管道中的字节流进行初步解析,读取其中协议体部分
            byte[] buffer = DecodeByte(bytes);
            //将真实的协议字节流转化为协议体对象
            IExtensible proto  = DecodeProto(num, buffer);
            //协议实体可能存在缺失注册的情况;
            if (proto == null)
                Tracer.Log("尚未注册协议:" + num, LogLevel.ERROR);
            else
                Tracer.LogProto("【接受协议】", (int)num, proto);
            return proto;
        }

        /// <summary>
        /// 将通讯管道中的字节流进行初步解析,读取其中协议体部分
        /// </summary>
        /// <returns></returns>
        private static byte[] DecodeByte(byte[] bytes)
        {
            ushort dataSize = GetCurDataSize(bytes);
            //协议体的实际长度要减去头部的四个字节
            int len = dataSize - sizeof(uint);
            byte[] buffer = new byte[len];
            Array.Copy(bytes, sizeof(int), buffer, 0, len);
            return buffer;
        }

        /// <summary>
        /// 将真实的协议字节流转化为协议体对象
        /// </summary>
        /// <param name="num"></param>
        /// <param name="buffer"></param>
        /// <returns></returns>
        private static IExtensible DecodeProto(CmdNo num, byte[] buffer)
        {
            IExtensible proto = null;
            Type type = GameSocketMgr.Self.GetProtobufType(num);
            if (type != null)
            {
                using (System.IO.MemoryStream ms = new System.IO.MemoryStream())
                {
                    ms.Write(buffer, 0, buffer.Length);
                    ms.Seek(0, System.IO.SeekOrigin.Begin);
                    proto = protobufSerializer.Deserialize(ms, null, type) as ProtoBuf.IExtensible;
                    ms.Close();
                }
            }
            return proto;
        }

        #endregion

        #region Prptobuff转化成字节
        /// <summary>
        /// 将协议体转化为字节流
        /// </summary>
        /// <param name="msg"></param>
        /// <returns></returns>
        public static byte[] ProtoToBytes(CmdNo num, IExtensible msg)
        {
            Tracer.LogProto("【发送协议】", (int)num, msg);
            //将协议实体转化为字节流对象
            byte[] buffs = EncodeProto(msg);
            //将协议字节流转化为最终要发送的字节流
            byte[] bytes = EncodeBytes(num, buffs);
            return bytes;
        }

        /// <summary>
        /// 将协议实体转化为字节流对象
        /// </summary>
        /// <param name="num"></param>
        /// <param name="msg"></param>
        /// <returns></returns>
        private static byte[] EncodeProto(IExtensible msg)
        {
            byte[] data = null;
            using (System.IO.MemoryStream ms = new System.IO.MemoryStream())
            {
                protobufSerializer.Serialize(ms, msg);
                if (ms.Length > 0)
                {
                    data = new byte[(int)ms.Length];
                    ms.Seek(0, System.IO.SeekOrigin.Begin);
                    ms.Read(data, 0, data.Length);
                }
                ms.Close();
            }
            if (data == null)
                data = new byte[0];
            return data;
        }

        /// <summary>
        /// 将协议字节流转化为最终要发送的字节流
        /// </summary>
        /// <param name="message"></param>
        private static byte[] EncodeBytes(CmdNo num,byte[] datas)
        {
            //获取协议字节长度,ushort占用两字节
            ushort cmd_len = datas == null ? (ushort)0 : (ushort)datas.Length;
            //获取协议号
            ushort cmd_no = (ushort)num;
            //定义头部长度
            const int head_len = sizeof(int);
            //定义整个包长
            int pack_len = cmd_len + head_len;
            //定义待发送字节长度;
            byte[] send = new byte[pack_len];
            //将协议长度转化成字节数组
            byte[] len_byte = BitConverter.GetBytes(cmd_len);
            //将协议号转化为字节数组;
            byte[] no_byte = BitConverter.GetBytes(cmd_no);
            //将协议长度写入最前面的两位
            Array.Copy(len_byte, 0, send, 0, len_byte.Length);
            //将协议号写入后两位;
            Array.Copy(no_byte, 0, send, len_byte.Length, no_byte.Length);
            if (datas != null)
                Array.Copy(datas, 0, send, head_len, cmd_len);
            return send;
        }

        /// <summary>
        /// 获取指定字节中首包的长度;
        /// </summary>
        /// <param name="bytes"></param>
        /// <returns></returns>
        public static ushort GetCurDataSize(byte[] bytes)
        {
            return (ushort)(BitConverter.ToUInt16(bytes, 0) + sizeof(uint));
        }

        #endregion
    }

5、BytesBuff

字节流对象,主要用于接收协议时对数据进行解析处理。

 public class BytesBuff
    {
        //最小尺寸
        private uint minSize;
        //字节流数组
        private byte[] bytes;
        //当前字节位置
        private uint curPosition;

        /// <summary>
        /// 构造函数
        /// </summary>
        public BytesBuff()
        {
            Reset();
        }

        /// <summary>
        /// 重置字节流
        /// </summary>
        public void Reset()
        {
            curPosition = 0;
            minSize = 256;
            bytes = new byte[minSize];
        }

        /// <summary>
        /// 合并字节流
        /// </summary>
        /// <param name="val"></param>
        /// <param name="len"></param>
        public void MergeByte(byte[] val, uint len)
        {
            //如果当前剩余尺寸小于即将压入的字节尺寸
            if (resetSize < len)
            {
                //对当前尺寸进行扩容
                minSize += len;
                //按照当前的新尺寸定义一个接受数组
                byte[] newBuffer = new byte[minSize];
                //将当前数组的内容拷贝至新的数组;
                Array.Copy(bytes, 0, newBuffer, 0, curPosition);
                //将新数组赋值给原数组,使原数组拥有更大的尺寸;
                bytes = newBuffer;
            }
            //向当前字节数组存储新的数据;
            Array.Copy(val, 0, bytes, curPosition, len);
            //增加已使用的字节数
            curPosition += len;
        }

        /// <summary>
        /// 读取字节流;
        /// </summary>
        public byte[] ReadByte()
        {
            //获取最近一条协议的长度
            ushort dataSize = ConvertTools.GetCurDataSize(bytes);
            //定义当前协议的字节数组;
            byte[] buffs = new byte[dataSize];
            //将指定的长度拷贝至新的字节数组
            Array.Copy(bytes, 0, buffs, 0, dataSize);
            //==================  变更字节读取流  ========================//
            //将当前字节位置减少消耗的长度;
            curPosition -= dataSize;
            //当前已经使用的字节数是否小于最小字节数
            uint size = curPosition < minSize ? minSize : curPosition;
            //创建新的字节数组
            byte[] newBuff = new byte[size];
            //将旧字节数组拷贝至新的字节数组
            Array.Copy(bytes, dataSize, newBuff, 0, curPosition);
            //赋值;
            bytes = newBuff;
            return buffs;
        }

        /// <summary>
        /// 是否能够读取下一条协议;
        /// </summary>
        public bool ReadValid
        {
            get
            {
                //可能存在当前接收不完整,所以需要对包体大小进行判断;
                //只有当前字节数组的位置数大于下一条协议的长度时,才可以进行读取;
                if (curPosition != 0 && curPosition >= ConvertTools.GetCurDataSize(bytes))
                    return true;
                return false;
            }
        }

        /// <summary>
        /// 当前剩余字节数
        /// </summary>
        private uint resetSize
        {
            get {
                return minSize - curPosition;
            }
        }
    }

本套框架对常见的Socket问题进行了处理,并使用独立的发送线程与接受线程进行优化。

完整框架代码下载地址
注:该代码因未上传协议体部分,故不可运行,仅供学习参考使用。

  • 0
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值