zoukankan      html  css  js  c++  java
  • Socket开发框架之数据传输协议

    我在前面一篇随笔《Socket开发框架之框架设计及分析》中,介绍了整个Socket开发框架的总体思路,对各个层次的基类进行了一些总结和抽象,已达到重用、简化代码的目的。本篇继续分析其中重要的协议设计部分,对其中消息协议的设计,以及数据的拆包和封包进行了相关的介绍,使得我们在更高级别上更好利用Socket的特性。

    1、协议设计思路

    对Socket传输消息的封装和拆包,一般的Socket应用,多数采用基于顺序位置和字节长度的方式来确定相关的内容,这样的处理方式可以很好减少数据大小,但是这些处理对我们分析复杂的协议内容,简直是一场灾难。对跟踪解决过这样协议的开发人员来说会很好理解其中的难处,协议位置一旦变化或者需要特殊的处理,就是很容易出错的,而且大多数代码充斥着很多位置的数值变量,分析和理解都是非常不便的。随着网络技术的发展,有时候传输的数据稍大一点,损失一些带宽来传输数据,但是能成倍提高开发程序的效率,是我们值得追求的目标。例如,目前Web API在各种设备大行其道,相对Socket消息来说,它本身在数据大小上不占优势,但是开发的便利性和高效性,是众所周知的。

    借鉴了Web API的特点来考虑Socket消息的传输,如果对于整体的内容,Socket应用也使用一种比较灵活的消息格式,如JSON格式来传输数据,那么我们可以很好的把消息封装和消息拆包解析两个部分,交给第三方的JSON解析器来进行,我们只需要关注具体的消息处理逻辑就可以了,而且对于协议的扩展,就如JSON一样,可以自由灵活,这样瞬间,整个世界都会很清静了。

    对于Socket消息的安全性和完整性,加密处理方面我们可以采用 RSA公钥密码系统。平台通过发送平台RSA公钥消息向终端告知自己的RSA公钥,终端回复终端RSA公钥消息,这样平台和终端的消息,就可以通过自身的私钥加密,让对方根据接收到的公钥解密就可以了,虽然加密的数据长度会增加不少,但是对于安全性要求高的,采用这种方式也是很有必要的。

    对于数据的完整性,传统意义的CRC校验码其实没有太多的用处了,因为我们的数据不会发生部分的丢失,而我们更应该关注的是数据是否被篡改过,这点我想到了微信公众号API接口的设计,它们带有一个安全签名的加密字符串,也就是对其中内容进行同样规则的加密处理,然后对比两个签名内容是否一致即可。不过对于非对称的加密传输,这种数据完整性的校验也可以不必要。

    前面介绍了,我们可以参照Web API的方式,以JSON格式作为我们传输的内容,方便序列号和反序列化,这样我们可以大大降低Socket协议的分析难度和出错几率,降低Socket开发难度并提高开发应用的速度。那么我们应该如何设计这个格式呢?

    首先我们需要为Socket消息,定义好开始标识和结束标识,中间部分就是整个通用消息的JSON内容。这样,一条完整的Socket消息内容,除了开始和结束标识位外,剩余部分是一个JSON格式的字符串数据。

    我们准备根据需要,设计好整个JSON字符串的内容,而且最好设计的较为通用一些,这样便于我们承载更多的数据信息。

    2、协议设计分析和演化

    参考微信的API传递消息的定义,我设计了下面的消息格式,包括了送达用户ID,发送用户ID、消息类型、创建时间,以及一个通用的内容字段,这个通用的字段应该是另外一个消息实体的JSON字符串,这样我们整个消息格式不用变化,但是具体的内容不同,我们把这个对象类称之BaseMessage,常用字段如下所示。

    上面的Content字段就是用来承载具体的消息数据的,它会根据不同的消息类型,传送不同的内容的,而这些内容也是具体的实体类序列化为JSON字符串的,我们为了方便,也设计了这些类的基类,也就是Socket传递数据的实体类基类BaseEntity。

    我们在不同的请求和应答消息,都继承于它即可。我们为了方便让它转换为我们所需要的BaseMessage消息,为它增加一个MsgType协议类型的标识,同时增加PackData的方法,让它把实体类转换为JSON字符串。

    例如我们一般情况下的请求Request和应答Response的消息对象,都是继承自BaseEntity的,我们可以把这两类消息对象放在不同的目录下方便管理。

    继承关系示例如下所示。

    其中子类都可以使用基类的PackData方法,直接序列号为JSON字符串即可,那个PacketData的函数主要就是用来组装好待发送的对象BaseMessage的,函数代码如下所示:

            /// <summary>
            /// 封装数据进行发送
            /// </summary>
            /// <returns></returns>
            public BaseMessage PackData()
            {
                BaseMessage info = new BaseMessage()
                {
                    MsgType = this.MsgType,
                    Content = this.SerializeObject()
                };
                return info;
            }

    有时候我们需要根据请求的信息,用来构造返回的应答消息,因为需要把发送者ID和送达者ID逆反过来。

            /// <summary>
            /// 封装数据进行发送(复制请求部分数据)
            /// </summary>
            /// <returns></returns>
            public BaseMessage PackData(BaseMessage request)
            {
                BaseMessage info = new BaseMessage()
                {
                    MsgType = this.MsgType,
                    Content = this.SerializeObject(),
                    CallbackID = request.CallbackID
                };
    
                if(!string.IsNullOrEmpty(request.ToUserId))
                {
                    info.ToUserId = request.FromUserId;
                    info.FromUserId = request.ToUserId;
                }
    
                return info;
            }

    以登陆请求的数据实体对象介绍,它继承自BaseEntity,同时指定好对应的消息类型即可。

        /// <summary>
        /// 登陆请求消息实体
        /// </summary>
        public class AuthRequest : BaseEntity
        {
            #region 字段信息
    
            /// <summary>
            /// 用户帐号
            /// </summary>
            public string UserId { get; set; }
    
            /// <summary>
            /// 用户密码
            /// </summary>
            public string Password { get; set; }
    
            #endregion
    
            /// <summary>
            /// 默认构造函数
            /// </summary>
            public AuthRequest()
            {
                this.MsgType = DataTypeKey.AuthRequest;
            }
    
            /// <summary>
            /// 参数化构造函数
            /// </summary>
            /// <param name="userid">用户帐号</param>
            /// <param name="password">用户密码</param>
            public AuthRequest(string userid, string password) : this()
            {
                this.UserId = userid;
                this.Password = password;
            }
        }

    这样我们的消息内容就很简单,方便我们传递及处理了。

    3、消息的接收和发送

    前面我们介绍过了一些基类,包括Socket客户端基类,和数据接收的基类设计,这些封装能够给我提供很好的便利性。

    在上面的BaseSocketClient里面,我们为了能够解析不同协议的Socket消息,把它转换为我们所需要的基类对象,那么我们这里引入一个解析器MessageSplitter,这个类主要的职责就是用来分析字节数据,并进行整条消息的提取的。

    因此我们把BaseSocketClient的类定义的代码设计如下所示。

        /// <summary>
        /// 基础的Socket操作类,提供连接、断开、接收和发送等相关操作。
        /// </summary>
        /// <typeparam name="TSplitter">对应的消息解析类,继承自MessageSplitter</typeparam>
        public class BaseSocketClient<TSplitter>  where TSplitter : MessageSplitter, new()

    MessageSplitter对象,给我们处理低层次的协议解析,前面介绍了我们除了协议头和协议尾标识外,其余部分就是一个JSON的,那么它就需要根据这个规则来实现字节数据到对象级别的转换。

    首先需要把字节数据进行拆分,把它完整的一条数据加到列表里面后续进行处理。

    其中结尾部分,我们就是需要提取缓存的直接数据到一个具体的对象上了。

    RawMessage msg = this.ConvertMessage(MsgBufferCache, from);

    这个转换的大概规则如下所示。

    这样我们在收到消息后,利用TSplitter对象来进行解析就可以了,如下所示就是对Socket消息的处理。

                        TSplitter splitter = new TSplitter();
                        splitter.InitParam(this.Socket, this.StartByte, this.EndByte);//指定分隔符,用来拆包
                        splitter.DataReceived += splitter_DataReceived;//如果有完整的包处理,那么通过事件通知

    数据接收并获取一条消息的直接数据对象后,我们就进一步把直接对象转换为具体的消息对象了

            /// <summary>
            /// 消息分拆类收到消息事件
            /// </summary>
            /// <param name="data">原始消息对象</param>
            void splitter_DataReceived(RawMessage data)
            {
                ReceivePackCount += 1;//增加收到的包数量
                OnReadRaw(data);
            }
    
            /// <summary>
            /// 接收数据后的处理,可供子类重载
            /// </summary>
            /// <param name="data">原始消息对象(包含原始的字节数据)</param>
            protected virtual void OnReadRaw(RawMessage data)
            {
                //提供默认的包体处理:假设整个内容为Json的方式;
                //如果需要处理自定义的消息体,那么需要在子类重写OnReadMessage方法。
                if (data != null && data.Buffer != null)
                {
                    var json = EncodingGB2312.GetString(data.Buffer);
                    var msg = JsonTools.DeserializeObject<BaseMessage>(json);
    
                    OnReadMessage(msg);//给子类重载
                }
            }

    在更高一层的数据解析上面,我们就可以对对象级别的消息进行处理了

    例如我们收到消息后,它本身解析为一个实体类BaseMessage的,那么我们就可以利用BaseMessage的消息内容,也可以把它的Content内容转换为对应的实体类进行处理,如下代码所示是接收对象后的处理。

            void TextMsgAnswer(BaseMessage message)
            {
                var msg = string.Format("来自【{0}】的消息:", message.FromUserId);
    
                var request = JsonTools.DeserializeObject<TextMsgRequest>(message.Content);
                if (request != null)
                {
                    msg += string.Format("{0}  {1}", request.Message, message.CreateTime.IntToDateTime());
                }
    
                //MessageUtil.ShowTips(msg);
                Portal.gc.MainDialog.AppendMessage(msg);
            }

    对于消息的发送处理,我们可以举一个例子,如果客户端登陆后,需要获取在线用户列表,那么可以发送一个请求命令,那么服务器需要根据这个命令返回列表信息给终端,如下代码所示。

            /// <summary>
            /// 处理客户端请求用户列表的应答
            /// </summary>
            /// <param name="data">具体的消息对象</param>
            private void UserListProcess(BaseMessage data)
            {
                CommonRequest request = JsonTools.DeserializeObject<CommonRequest>(data.Content);
                if (request != null)
                {
                    Log.WriteInfo(string.Format("############
    {0}", data.SerializeObject()));
    
                    List<CListItem> list = new List<CListItem>();
                    foreach(ClientOfShop client in Singleton<ShopClientManager>.Instance.LoginClientList.Values)
                    {
                        list.Add(new CListItem(client.Id, client.Id));
                    }
    
                    UserListResponse response = new UserListResponse(list);
                    Singleton<ShopClientManager>.Instance.AddSend(data.FromUserId, response.PackData(data), true);
                }
            }

  • 相关阅读:
    MVC ORM 架构
    Kubernetes 第八章 Pod 控制器
    Kubernetes 第七章 Configure Liveness and Readiness Probes
    Kubernetes 第六章 pod 资源对象
    Kubernetes 第五章 YAML
    Kubernetes 核心组件
    Kubernetes 架构原理
    Kubernetes 第四章 kubectl
    Kubernetes 第三章 kubeadm
    yum 配置及yum 源配置
  • 原文地址:https://www.cnblogs.com/wuhuacong/p/5321467.html
Copyright © 2011-2022 走看看