• 分享一个与硬件通讯的分布式监控与远程控制程序的设计(下:通讯协议设计与实现)


    4 基于RoundTrip(往返)的通讯协议设计

    通讯服务器插件的核心为3部分:(1)与通讯方式、业务逻辑无关的通讯协议实现;(2)和通讯方式、业务逻辑有关的通讯业务逻辑的实现;(3)远程通讯消息队列。在这里我将重点描述通讯协议的实现。这个通讯协议的实现比较灵巧。

    4.1 通讯协议基本单元——消息

    通讯协议的通讯单元是消息,以下是来自硬件开发工程师编写的协议,消息包由前导符、起始符、消息头、校验码、消息体、结束符等部分组成。不同的通讯指令,发出的消息和接收到消息均不相同。

    clip_image002

    通讯协议必须能够发出正确的消息和解析响应的消息包,此外,硬件能接受的消息是字节格式,而通讯服务器软件能够正确识别的则是各个有意义的字段。为此,我们为消息设计了如下的基类。消息较小的单元是一个MessagePart,它提供了ToContent和ToMessage方法分别用于转换成字节码和字符串,此外,它还定义了TryParse方法用于将字节码解析成有意义的MessagePart对象。这里定义了ParseMessageException异常,当消息解析失败时,抛出该异常。下面是消息头、消息体以及消息基类的定义。消息基类由前缀、起始、头、体和后缀部分组成。

    image

    接着我们根据硬件开发工程师提供的SCATA 3.0协议,定义与通讯协议相关的消息基类Scata30Message。这个消息提供了一个默认的消息头的实现,但是消息体则需要根据指令进一步实现。下图是通讯协议涉及的大部分消息体的实现,消息体基本都是一对的,即消息体和响应消息体。

    clip_image005

    消息体一般是指服务器发出给硬件的指令,这样的消息体需要构造所有的字段,并要实现ToContent方法,将消息转换成字节码,发送给硬件;而响应消息一般是由硬件发送给服务器的消息,它至少需要实现TryParse方法,将硬件字节码解析成有意义的字段,供业务逻辑层访问。

    下面是一个消息的定义。

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using UIShell.CommServerService.Utility;
    using System.ComponentModel;
    
    namespace UIShell.CommServerService.Protocol.Scata30.Message
    {
        [Description("读取单一表")]
        public class Scata30ReadMeterMessageBody : Scata30MessageBody
        {
            public byte MeterProtocolCategory;
            public byte Channel;
            public byte[] MeterAddressBCD;
            public long MeterAddress;
    
            internal Scata30ReadMeterMessageBody()
            { 
            }
    
            public Scata30ReadMeterMessageBody(byte meterProtocol, byte channel, 
    long meterAddress)
            {
                MeterProtocolCategory = meterProtocol;
                Channel = channel;
                MeterAddress = meterAddress;
                MeterAddressBCD = ProtocolUtility.MeterAddressFromLong(meterAddress, true);
            }
    
            protected override bool TryParseWithoutCheckCode(byte[] bodyContent)
            {
                throw new NotImplementedException();
            }
    
            protected override byte[] ToContentWithoutCheckCode()
            {
                return new byte[] { 
    MeterProtocolCategory, Channel }.Concat(MeterAddressBCD).ToArray();
            }
    
            public override string ToString()
            {
                return string.Format("协议类型={0},通道号={1},表地址={2}", 
    MeterProtocolCategory, Channel, MeterAddress);
            }
        }
    }
    

      

    下面则是响应消息的实现。

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using UIShell.CommServerService.Utility;
    using System.ComponentModel;
    
    namespace UIShell.CommServerService.Protocol.Scata30.Message
    {
        [Description("读取单一表响应")]
        public class Scata30ReadMeterResponseMessageBody : Scata30MessageBody
        {
            public Scata30ResponseStatus ResponseStatus;
            /// <summary>
            /// 表数据,同读取多表的数据类似。
            /// </summary>
            public byte[] MeterBodyContent;
    
            public Scata30ReadMeterResponseMessageBody()
            {
                
            }
    
            protected override bool TryParseWithoutCheckCode(byte[] bodyContent)
            {
                if (bodyContent == null || bodyContent.Length == 0)
                {
                    _log.Error(string.Format(UIShell.CommServerService.Properties.Resources.
    ParseMessageBodyFailed, ProtocolUtility.BytesToHexString(bodyContent)));
                    return false;
                }
    
                if (bodyContent.Length == 1)
                {
                    if (bodyContent[0] != (byte)Scata30ResponseStatus.Failed)
                    {
    _log.Error(string.Format(UIShell.CommServerService.Properties.Resources.
    ParseMessageBodyFailed, ProtocolUtility.BytesToHexString(bodyContent)));
                        return false;
                    }
                    else
                    {
                        ResponseStatus = (Scata30ResponseStatus)bodyContent[0];
                    }
                }
                else
                {
                    ResponseStatus = Scata30ResponseStatus.Success;
                    MeterBodyContent = bodyContent;
                }
                return true;
            }
    
            protected override byte[] ToContentWithoutCheckCode()
            {
                if (ResponseStatus == Scata30ResponseStatus.Failed)
                {
                    return new byte[] { (byte)ResponseStatus };
                }
                return MeterBodyContent;
            }
    
            public override string ToString()
            {
                return string.Format("状态={0},表数据={1}", 
    EnumDescriptionHelper.GetDescription(ResponseStatus), ProtocolUtility.BytesToHexString(MeterBodyContent));
            }
        }
    }
    

      

    4.2 通讯协议的组成——RoundTrip(往返)

    通讯服务器与硬件的通讯过程是由一组的对话来实现的,每一组对话都是问答式的方式来完成。我们把一次问答式的对话用RoundTripBase这个类型来表示。问答式的对话又分成主动式(ActiveRoundTrip)和被动式(PassiveRoundTrip),即服务器发起然后硬件响应,或者硬件发起服务器响应。有时,一次问答式的对话可能需要由若干组的子对话来实现,我们称其为组合对话(CompositeRoundTripBase)。有关通讯协议对话过程涉及的基类设计如下。

    image

    对话RoundTripBase的详细设计如下所示,它由优先级、时间戳属性组成,提供了Start方法表示会话开始,以及OnCompleted和OnError事件。RoundTripQueue则是对话队列,它严格限制通讯协议每次只能执行一个RoundTrip,不能交叉运行,这个RoundTripQueue是一个线程安全的,因为通讯协议会被远程通讯线程、协议线程、UI线程等线程来访问。

    clip_image008

    4.3 协议的RoundTrip实现

    在本系统中,我们使用SCATA 3.0通讯协议,这里我们实现了2个基类:Scata30ActiveRoundTrip和Scata30PassiveRoundTrip。

    image

    在Scata30ActiveRoundTrip中,它在Start方法中,将利用StreamAdapter来从通讯信道中获取一条消息,一旦消息解析成功后,将发送响应消息包。这个对话,一旦中间发生错误或者超时,将重试若干次。同理,Scata30PassiveRoundTrip也是如此实现。

    接下来,我们根据通讯协议,定义了如下的对话。

    clip_image011

    下面我们来看一个对话的实现。

    using System;
     using System.Collections.Generic;
     using System.Linq;
     using System.Text;
     using UIShell.CommServerService.Protocol.Scata30.Message;
     using UIShell.CommServerService.Utility;
     using System.ComponentModel;
     
     namespace UIShell.CommServerService.Protocol.Scata30.RoundTrip
     {
         [Description("读取指定时间点表数据")]
         public class Scata30ReadHistoricalMeterRoundTrip : Scata30HasNextActiveRoundTrip<Scata30ReadHistoricalMeterMessageBody, Scata30ReadHistoricalMeterResponseMessageBody>
         {
             public DateTime HistoricalDateTime;
     
             public Scata30ReadHistoricalMeterRoundTrip(
                 ushort destinationAddress,
                 ushort destinationZigbeeAddress,
                 DateTime timeStamp,
                 Scata30Protocol protocol)
                 : base(destinationAddress, destinationZigbeeAddress, new Scata30Message<Scata30ReadHistoricalMeterMessageBody>(Scata30MessageType.ReadMeterByDate, protocol.MasterStationAddress, destinationAddress, 0, DateTime.Now, new Scata30ReadHistoricalMeterMessageBody(timeStamp)), Scata30MessageType.ReadMeterByDateResponse, protocol)
             {
                 HistoricalDateTime = timeStamp;
             }
     
             public override void ReceiveResponseMessages()
             {
                 base.ReceiveResponseMessages();
                 foreach (var message in ReceivedResponseMessages)
                 {
                     if (!message.Body.HistoricalDateTime.Equals(HistoricalDateTime))
                     {
                         _log.Error(string.Format("Read the historical meter content error since the date time mismatched. The require date time is '{0}', return by concentrator is '{1}'", HistoricalDateTime.ToString("yyyy-MM-dd HH:mm:ss"), message.Body.HistoricalDateTime.ToString("yyyy-MM-dd HH:mm:ss")));
                         // throw new Exception("Parse message error since historical date time mismatched.");
                     }
                 }
             }
         }
     }
    

      

    4.4 通讯协议的实现

    通讯协议的实现类图如下所示,由于通讯协议与通讯方式、业务逻辑无关,因此,在这里我们引入StreamAdapter和StreamProvider来屏蔽这些上下文。StreamAdapter的功能是获取一条消息和发送一条消息,StreamProvider则是为不同通讯方式提供通讯流。

    protocol

    下面我来描述协议类的关键实现。协议类内部有一个线程来实现与硬件的通讯。这个线程会一直运行,然后从对话队列中不停获取RoundTrip,一旦获取的RoundTrip不会空,则运行这个RoundTrip,否则线程进入休眠状态。

    public bool Start()
    {
        if (_started)
        {
            return true;
        }
    
        FireOnStarting();
    
        try
        {
            CommStreamProvider.Start();
        }
        catch (Exception ex)
        {
            _log.Error("Start the communication provider failed.", ex);
            return false;
        }
                
        _thread = new Thread(() => {
            RoundTripBase roundTrip;
            while (!_exited)
            {
                Monitor.Enter(_queue.SyncRoot);
    
                roundTrip = Dequeue();
                if (roundTrip != null)
                {
                    try
                    {
                        Monitor.Exit(_queue.SyncRoot);
                        OnRoundTripStartingHandler(this, 
    new RoundTripEventArgs() { RoundTrip = roundTrip });
                        roundTrip.Start();
                    }
                    catch (ThreadAbortException)
                    {
                        Trace("通讯线程被终止。");
                        throw;
                    }
                    catch (Scata30StreamException ex) // 无法获取Stream的时候,直接退出
                    {
                        _exited = true;
                        roundTrip.Trace("会话失败,因为:连接已经关闭。");
                    }
                    catch (Exception ex)
                    {
                        string error = GetErrorMessage(ex);
                        roundTrip.Trace(string.Format("会话失败,因为:{0}。", error));
                    }
                    if (!_exited)
                    {
                        roundTrip.Trace(Environment.NewLine);
                        OnRoundTripStartedHandler(this, 
    new RoundTripEventArgs() { RoundTrip = roundTrip });
                    }
                    else
                    {
                        // 1 将当前失败的RoundTrip保存入队
                        FailedRoundTrips.Enqueue(roundTrip);
                        // 2 保存其它没有处理的RoundTrip
                        do
                        {
                            roundTrip = _queue.Dequeue();
                            if (roundTrip != null)
                            {
                                FailedRoundTrips.Enqueue(roundTrip);
                            }
                        } while (roundTrip != null);
                        // 3 停止当前协议
                        Stop();
                    }
                    // 执行完RoundTrip后,开始清理资源
                    roundTrip.Dispose();
                }
                else
                {
                    Monitor.Exit(_queue.SyncRoot);
                    OnIdleHandler(this, new RoundTripEventArgs());
                    _autoResetEvent.WaitOne();
                }
            }
        });
    
        _thread.Start();
        _started = true;
    
        FireOnStarted();
        return true;
    }
    

      

    执行对话,是以异步的方式来进行,通过事件进行通知。如下所示。

    using System; 
    using System.Collections.Generic; 
    using System.Linq; 
    using System.Text; 
    using UIShell.CommServerService.Protocol.Scata30.RoundTrip; 
    using UIShell.CommServerService.Protocol.Scata30.Message;
    
    namespace UIShell.CommServerService.Protocol.Scata30 
    { 
        public partial class Scata30Protocol 
        { 
            public Scata30SetConcentratorTimeRoundTrip SetConcentratorTime( 
                ushort concentratorAddress, 
                ushort concentratorZigbeeAddress, 
                DateTime timeStamp, 
                EventHandler<RoundTripEventArgs> onMessageSend, 
                EventHandler<RoundTripEventArgs> onCompleted, 
                EventHandler<RoundTripEventArgs> onError) 
            { 
                var roundTrip = new Scata30SetConcentratorTimeRoundTrip( 
                    concentratorAddress, 
                    concentratorZigbeeAddress, 
                    timeStamp, 
                    this);
    
                if (onMessageSend != null) 
                { 
                    roundTrip.OnMessageSend += onMessageSend; 
                } 
                if (onCompleted != null) 
                { 
                    roundTrip.OnCompleted += onCompleted; 
                } 
                if (onError != null) 
                { 
                    roundTrip.OnError += onError; 
                } 
                Enqueue(roundTrip); 
                return roundTrip; 
            } 
        } 
    }
    

    这个通讯协议的实现非常优雅,在维护的过程中,通讯指令的变更和通讯方式的转变,都不需要再修改协议和RoundTrip本身,只需要对消息体进行变更并增加新的StreamProvider,并在上层的业务逻辑进行实现。

     

    Creative Commons License 关于iOpenWorksSDK下载和疑问,访问:https://www.cnblogs.com/baihmpgy/p/11818026.html

    本文基于Creative Commons Attribution 2.5 China Mainland License发布,欢迎转载,演绎或用于商业目的,但是必须保留本文的署名道法自然(包含链接)。如您有任何疑问或者授权方面的协商,请给我留言。
  • 相关阅读:
    微信小程序发送模板消息
    Swoole-WebSocket服务端主动推送消息
    git 批量删除分支
    RdKafka使用
    Kakfa安装,PHP安装RdKafka扩展
    Zookeeper安装、启动、启动失败原因
    Hyperf-事件机制+异常处理
    Hyperf-JsonRpc使用
    hyperf-环境搭建
    CGI、FastCGI、PHPFPM
  • 原文地址:https://www.cnblogs.com/baihmpgy/p/2836122.html
Copyright © 2020-2023  润新知