当前位置:首页 > 后端开发 > 正文内容

OPCUA讨论(三)——客户端代码解读

邻居的猫1个月前 (12-09)后端开发572

本系列文章:
OPCUA 评论(一)——测验与开发环境树立
OPCUA 评论(二)——服务器节点初探
OPCUA 评论(三)——客户端代码解读
OPCUA 评论(四)——客户端代码解读2

本文开端评论OPCUA客户端源码的结构。
项目地址:https://gitee.com/zuoquangong/opcuaapi

一、项目结构阐明

咱们在Visual Studio2022中翻开项目文件(.sln),检查“解决计划资源管理器”:

该客户端中心功用在OpcUaAPI.cs。

上述结构与咱们的运用流程相对应:

下面咱们逐一进程进行评论。
留意,这儿咱们运用了OPC基金会供给的第三方包(NuGet管理器中可检查详细信息):

点击检查代码
using Opc.Ua.Configuration;
using Utils=Opc.Ua.Utils;
using Opc.Ua;
它们供给了咱们进行开发的底层东西。

二、运用大局装备

2.1 运用实例(Application Instance)

OPCUA服务器和OPCUA客户端都称作OPCUA运用(OPCUA Application)。咱们的客户端软件适当所以客户端的一个实例。
实例化一个Opc.Ua.Configuration.ApplicationInstance类目标m_appInstance,能够运用该目标为咱们的客户端装备运用参数。
m_appInstance有一个成员ApplicationConfiguration,其内部包含了各种运用参数。

点击检查代码
/// <summary>
/// 经过运用实例ApplicationInstance创立运用装备
/// </summary>
public async void buildConfig()
{
    string clientName = "myApp"; //客户端运用称号

    // 运用实例
    m_appInstance = new ApplicationInstance()
    {
        ApplicationType = ApplicationType.Client,  //界说运用类型。此处界说为客户端,也能够界说成服务器等
        ApplicationName = clientName,
    };
    Assert.NotNull(m_appInstance); // 断定内存分配成功;假如不成功。。。
    m_appInstance.ApplicationConfiguration= new Opc.Ua.ApplicationConfiguration();

    //进行运用装备
    CreateConfig();

    //装备证书验证进程
    certificateValidator = new CertificateValidator();
    m_appInstance.ApplicationConfiguration.CertificateValidator = certificateValidator;
    certificateValidator.CertificateValidation += certClient; //设置 证书验证进程 处理函数

    return;
}
以上进程可用下图概略:

2.2 运用装备(Application Configuration)

buildConfig函数中咱们调用了CreateConfig函数,对m_appInstance.ApplicationConfiguration进行了详细设置。

点击检查代码
private void CreateClientConfiguration()
{
    // 运用程序装备能够从任何文件加载。
    // ApplicationConfiguration.Load()办法经过在App.config中查找文件途径来加载装备。
    // 这种办法答应运用程序同享装备文件并对其进行更新。
    // 此示例运用其默许结构函数创立最小ApplicationConfiguration。

    Opc.Ua.ApplicationConfiguration configuration = m_appInstance.ApplicationConfiguration;
    //地址赋值,两个变量指向同一个存储区。对configuration的设置等价于对m_appInstance.ApplicationConfiguration设置

    // Step 1 - 指定客户端标识.
    configuration.ApplicationName = m_appInstance.ApplicationName;
    configuration.ApplicationType = m_appInstance.ApplicationType;
    configuration.ApplicationUri = "urn:MyClient";
    configuration.ProductUri = "myApp1.0";

    // Step 2 - 进行安全装备,并指定客户端的运用程序实例证书。
    configuration.SecurityConfiguration.AutoAcceptUntrustedCertificates = true;
    configuration.SecurityConfiguration.RejectSHA1SignedCertificates = false;
    // 运用程序实例证书有必要放在windows证书存储中,由于这是维护私钥的最佳办法。存储中的证书由4个参数标识:
    configuration.SecurityConfiguration = new SecurityConfiguration();
    configuration.SecurityConfiguration.ApplicationCertificate = new CertificateIdentifier();
    configuration.SecurityConfiguration.ApplicationCertificate.StoreType = CertificateStoreType.X509Store;
    configuration.SecurityConfiguration.ApplicationCertificate.StorePath = "CurrentUser\\My";
    configuration.SecurityConfiguration.ApplicationCertificate.SubjectName = configuration.ApplicationName;

    // 为服务器证书检查界说受信赖的根存储
    configuration.SecurityConfiguration.TrustedIssuerCertificates.StoreType = CertificateStoreType.X509Store;
    configuration.SecurityConfiguration.TrustedIssuerCertificates.StorePath = "CurrentUser\\Root";
    configuration.SecurityConfiguration.TrustedPeerCertificates.StoreType = CertificateStoreType.X509Store;
    configuration.SecurityConfiguration.TrustedPeerCertificates.StorePath = "CurrentUser\\Root";

    // 在存储中查找客户端证书
    Task<X509Certificate2> clientCertificate = configuration.SecurityConfiguration.ApplicationCertificate.Find(true);

    // 假如找不到,请创立一个新的自签名证书
    if (clientCertificate.Result == null)
    {
        CreateCertificateAndAddToStore(configuration.ApplicationUri, configuration.ApplicationName, configuration.SecurityConfiguration.ApplicationCertificate.StoreType, configuration.SecurityConfiguration.ApplicationCertificate.StorePath);
    }

    // Step 3 - 指定支撑的传输配额
    // 传输配额用于设置对音讯内容的约束,并用于避免DOS进犯和流氓客户端。它们应该设置为合理的值。
    configuration.TransportQuotas = new TransportQuotas();
    configuration.TransportQuotas.OperationTimeout = 360000;
    configuration.TransportQuotas.SecurityTokenLifetime = 86400000;
    configuration.TransportQuotas.MaxStringLength = 67108864;
    configuration.TransportQuotas.MaxByteStringLength = 16777216; //Needed, i.e. for large TypeDictionarys

    // Step 4 - 指定客户端特定的装备
    configuration.ClientConfiguration = new ClientConfiguration();
    configuration.ClientConfiguration.DefaultSessionTimeout = 360000;

    // Step 5 - 验证装备
    // 此进程检查装备是否共同,并分配SDK运用的一些内部变量。假如运用ApplicationConfiguration.Load()办法从文件加载装备,则会主动调用此函数。
    _ = configuration.Validate(ApplicationType.Client);

    return;
}
上述运用装备与下图对应:


左面四个参数相似本运用的身份ID,当时能够恣意设置。
右边的客户端装备(ClientConfiguration)装备了一个DefalutSessionTimeOut=360000,意为默许情况下,超时360000ms(6分钟)无回应则会话主动断开。
安全装备(SecurityConfiguration)装备内容如下:

  1. 假如需求较高安全性,主张不要主动接纳不信赖证书,设置AutoAcceptUntrustedCertificates = False;
  2. 之所以有RejectSHA1SignedCertificates这个参数,是由于SHA1算法安全性不高,假如在安全上有较高要求,主张设置RejectSHA1SignedCertificates = True;
  3. 一般公钥/私钥长度越长,越难暴力破解,安全性越高,因而MinimumCertificateKeySize能够设置为较大的数,但一起加密、解密时刻会变长;
  4. 最重要的是咱们要经过设置ApplicationCertification确保咱们的客户端有证书可用(没有则主动创立自签名证书),这个证书用于服务器承认通讯者的身份,是树立安全通道的条件。
    这儿简略阐明下,在OPCUA里,服务器与客户端树立信息安全通道有三种安全形式(Security Mode):
    None,无安全策略,不查验对方通讯者的身份,也不对通讯内容进行加密,安全性为零,仅用于测验,实际情况不要运用;
    Sign,仅签名形式,经过证书签名验证对方通讯者身份,但通讯内容不加密,签名能够确保信息完好未经篡改;
    SignAndEncrypt,签名且信息内容加密,最安全的形式。
    咱们这儿装备的运用证书是完成SignSignAndEncrypt安全形式的根底。
    一般运用自签名证书作为咱们的客户端运用证书,以下为证书生成代码:
点击检查代码
 /// <summary>
 /// 创立一个新的自签名证书并存储,用于树立安全数据通道进程中的身份验证
 /// </summary>
 /// <param name="applicationUri">运用ID</param>
 /// <param name="applicationName">运用称号</param>
 /// <param name="storeType">存储类型</param>
 /// <param name="storePath">存储途径</param>
 private void CreateCertificateAndAddToStore(string applicationUri, string applicationName, string storeType, string storePath)
 {
     List<string> localIps = GetLocalIpAddressAndDns(); // Get local interface ip addresses and DNS name
     ushort keySize = 2048; //must be multiples of 1024
     ushort lifeTimeInMonths = 24; //month till certificate expires
     ushort hashSizeInBits = 256; //0 = SHA1; 1 = SHA256
     var startTime = System.DateTime.Now; //starting point of time when certificate is valid

     var certificateBuilder = CertificateFactory.CreateCertificate(
         applicationUri,
         applicationName,
         null,
         localIps);

     X509Certificate2 clientCertificate2 = certificateBuilder
         .SetNotBefore(startTime)
         .SetNotAfter(startTime.AddMonths(lifeTimeInMonths))
         .SetHashAlgorithm(X509Utils.GetRSAHashAlgorithmName(hashSizeInBits))
         .SetRSAKeySize(keySize)
         .CreateForRSA();
     clientCertificate2.FriendlyName = m_appInstance.ApplicationName;
     clientCertificate2.AddToStore(
             storeType,
             storePath,
             null
         );
 }

 /// <summary>
 /// 获取本地IP地址,用于创立证书
 /// </summary>
 /// <returns></returns>
 /// <exception cref="Exception"></exception>
 private List<string> GetLocalIpAddressAndDns()
 {
     List<string> localIps = new List<string>();
     var host = Dns.GetHostEntry(Dns.GetHostName());
     foreach (var ip in host.AddressList)
     {
         if (ip.AddressFamily == AddressFamily.InterNetwork)
         {
             localIps.Add(ip.ToString());
         }
     }
     if (localIps.Count == 0)
     {
         throw new Exception("Local IP Address Not Found!");
     }
     localIps.Add(Dns.GetHostName());
     return localIps;
 }

传输配额(TransportQuotas),用的不多,按默许的设置,先不说了。

2.3 证书验证器(CertificateValidator)

在需求签名的通讯办法中,客户端和服务器两边都要验证对方的身份,因而咱们需求为咱们的客户端设置验证服务器证书(Server Certification)的进程。
m_appInstance.ApplicationConfiguration.CertificateValidator是运用装备的证书验证器,为其增加咱们自界说的验证进程certClient

点击检查代码
/// <summary>
/// 处理证书认证事情
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void certClient(object sender, CertificateValidationEventArgs e)
{
    //惯例认证流程:
    if (certStep == 0)
    {
        X509Store store = new X509Store(StoreName.Root, StoreLocation.CurrentUser);
        store.Open(OpenFlags.ReadOnly);

        // 先找本机是否有现成的证书
        X509CertificateCollection certCol = store.Certificates.Find(X509FindType.FindByThumbprint, e.Certificate.Thumbprint, true);
        store.Close();
        if (certCol.Capacity > 0)
        {
            e.Accept = true;
        }
        //假如本机没有保存证书,则敞开证书概况窗口
        else
        {
            FormCertClient formCertClient = new FormCertClient(e);
            formCertClient.ShowDialog();
        }

        if (e.Accept == true)
        {
            certStep++;
        }
    }
    //这儿设置了一个certStep,由于发现在认证进程中会呈现两次弹窗,第一次承认后,会弹出第二个证书认证窗口
    //而第二个窗口不会影响认证成果,因而certStep=1时直接越过即可,然后将certStep归零
    else
    {
        e.Accept = true;
        certStep = 0;
    }
}

其间 FormCertClient 是一个自界说的验证窗口类:

2.4 在windows电脑上检查已装置的证书

win + R组合键,唤出运转窗口,输入certmgr.msc,进入证书管理器。


咱们的客户成功运转过一次后,能够检查到自签名证书:

假如与服务器进行过衔接,则能够看到服务器证书。
西门子Sinumerik的证书:

Prosys的证书:

三、树立会话

OPCUA的大部分功用(变量阅读、读写、监控等)都树立在会话(Sessions)根底上:


下面评论怎么树立会话。

3.1 端点挑选与会话树立

首要咱们得知道OPCUA服务器的IP和端口号,例如是opc.tcp://192.168.215.1:4840
之后,树立会话流程如下:


端点(Endpoint)是指可与服务器树立安全衔接的一个计划,不同端点给出不同的安全策略。
例如,某OPCUA服务器供给以下端点(Endpoint):


(这儿的Basic128Rsa15Basic256Basic256Sha256是加密算法。)
在第一次衔接到服务器时,无法树立会话(Session),但此刻能够获取端点描绘(EndpointDescriptions,一个端点描绘能够理解为一个详细安全策略,包含选用什么样的安全形式、什么样的加密办法等),然后断开衔接;第2次衔接时,依照端点描绘运用相应安全策略,这时树立的衔接才干创立会话。因而,树立会话需求树立两次衔接。


咱们在客户端代码里先进行会话的根本装备,然后再开端树立衔接,代码如下:

点击检查代码
/// <summary>
/// 创立会话
/// 给出Url即 创立一个会话(Session)
/// (是否需求支撑多个会话标签,像阅读器相同?)
/// 现在仅支撑一个Session
/// </summary>
/// <param name="url"></param>
/// <returns></returns>
public async Task CreateSession(Uri url)
{
    var endpointsDescription = SelectEndpoint(url, true); //第一次衔接:获取端点(Endpoint)信息,并挑选一个适宜的端点(默许挑选安全性最高的端点)
    try
    {
        Opc.Ua.Client.Session new_session = await Opc.Ua.Client.Session.Create( //第2次衔接:运用所选端点树立会话
      configuration: m_appInstance.ApplicationConfiguration,

      //装备endpoint相关设置
      endpoint: new ConfiguredEndpoint(
          collection: null,
          description: endpointsDescription,
          configuration: Opc.Ua.EndpointConfiguration.Create(applicationConfiguration: m_appInstance.ApplicationConfiguration)
          ),

      updateBeforeConnect: false,
      checkDomain: false,
      sessionName: "Session" + DateTime.Now.ToString(),  //会话称号默许为Session+时刻戳(准确到秒)
      sessionTimeout: 60000U,//SessionTimeout
      identity: UserIdentity,
      preferredLocales: new string[] { "zh-CN" } //首选区域
      );
        //MessageBox.Show(new_session.Connected.ToString());

        current_session = new_session; //设置客户端当时会话
        m_sessions.Add(new_session); //将新会话参加会话列表
    }
    catch ( Exception ex )
    {
        //MessageBox.Show( ex.ToString() );
        return;
    }
    return;
}

/// <summary>
/// 挑选衔接时运用的Endpoint
/// 默许挑选安全性最高的Endpoint
/// </summary>
/// <param name="discoveryUrl"></param>
/// <param name="useSecurity"></param>
/// <returns></returns>
private EndpointDescription SelectEndpoint(Uri discoveryUrl, //服务器Url
                                           bool useSecurity  //是否运用安全措施(SecurityMode)
                                           )
{
    var configuration = Opc.Ua.EndpointConfiguration.Create();
    configuration.OperationTimeout = 5000;  // 操作超时约束(5s)(为了不长时间占用网络资源)
    EndpointDescription endpointDescriptionMain = null; //终究回来的endpoint的描绘
    try
    {
        using (var discoveryClient = DiscoveryClient.Create(discoveryUrl, configuration))
        {

            m_endpoints = discoveryClient.GetEndpoints(null);

            //int count = 0;
            foreach (var endpointDescriptionAlternate in
                m_endpoints.Where(endpointDescriptionAlternate =>
                endpointDescriptionAlternate.EndpointUrl.StartsWith(discoveryUrl.Scheme))
                //挑选scheme前缀和discoveryUrl的scheme(例如http,ftp等)相匹的endpoint
                )
            // 遍历一切endpoint,挑选契合安全要求的,安全等级最高的
            {
                string securityPolicyTMP = endpointDescriptionAlternate.SecurityPolicyUri.Remove(0, 42);
                //MessageBox.Show(ecount.ToString()+count.ToString()+securityPolicy);
                string keyTMP = "[" + m_appInstance.ApplicationName + "] " +
                    " [" + endpointDescriptionAlternate.SecurityMode + "] " +
                    " [" + securityPolicyTMP + "] " +
                    " [" + endpointDescriptionAlternate.EndpointUrl + "]";
                //MessageBox.Show((count++).ToString()+". "+keyTMP);

                if (useSecurity) //是否运用信息安全措施
                {
                    //禁用安全策略None
                    //if (endpointDescriptionAlternate.SecurityMode == MessageSecurityMode.None)
                    //    continue;
                }
                else if (endpointDescriptionAlternate.SecurityMode != MessageSecurityMode.None)
                    continue;

                //假如当时没有主计划,则初始化一个主计划
                if (endpointDescriptionMain == null) // endpointDescriptionMain初始化
                {
                    endpointDescriptionMain = endpointDescriptionAlternate;
                    //MessageBox.Show("我初始化了");
                }

                //每次比较当时计划和主计划的安全系数,运用更安全的计划替代主计划
                //因而终究计划为最安全计划
                //if (endpointDescriptionAlternate.SecurityLevel < endpointDescriptionMain.SecurityLevel) //主动挑选最高安全等级
                if (endpointDescriptionAlternate.SecurityMode > endpointDescriptionMain.SecurityMode ||
                    (endpointDescriptionAlternate.SecurityMode == endpointDescriptionMain.SecurityMode && endpointDescriptionAlternate.SecurityLevel > endpointDescriptionMain.SecurityLevel)
                   )
                    {
                    //MessageBox.Show(endpointDescriptionAlternate.SecurityMode.ToString() + " > " + endpointDescriptionMain.SecurityMode.ToString() + "\r\n"
                    //    + endpointDescriptionAlternate.SecurityLevel.ToString() + " > " + endpointDescriptionMain.SecurityLevel.ToString()
                    //    + "\r\n我晋级了");
                    endpointDescriptionMain = endpointDescriptionAlternate;
                    
                }
            }//完毕遍历foreach

            if (endpointDescriptionMain == null)
            {
                
                if (m_endpoints.Count > 0) //找不到计划(scheme)相匹配的,直接拿第一个endpoint来用
                {
                    //MessageBox.Show("没有满意条件的endpoint");
                    endpointDescriptionMain = m_endpoints[0];
                }

            }
        }
    }
    catch(Exception ex)
    {
        MessageBox.Show("获取接入点(endpoints)时呈现过错:\r\n" + ex.ToString());
        return null;
    }
    

    var uri = Utils.ParseUri(endpointDescriptionMain.EndpointUrl); //回来一个Uri(url)实例

    //到这儿,uri的取值能够是null,和discoveryUrl的scheme共同的uri,和discoveryUrl的scheme不共同的uri

    if (uri != null && uri.Scheme == discoveryUrl.Scheme) //scheme指http,file,git,ftp之类
        endpointDescriptionMain.EndpointUrl = new UriBuilder(uri)
        {
            Host = discoveryUrl.DnsSafeHost,
            Port = discoveryUrl.Port
        }.ToString();

    string securityPolicy = endpointDescriptionMain.SecurityPolicyUri.Remove(0, 42);
    
    //显现运用的Endpoint的详细信息
    //string key = "[" + m_appInstance.ApplicationName + "] " +
    //    " [" + endpointDescriptionMain.SecurityMode + "] " +
    //    " [" + securityPolicy + "] " +
    //    " [" + endpointDescriptionMain.EndpointUrl + "]";
    //MessageBox.Show(key);

    return endpointDescriptionMain;
}

代码里的discoveryUrl指第一次衔接获取端点描绘(EndpointDescriptions)时运用的Url。

3.2 完毕当时会话

完毕会话之前需求把里边的订阅使命(监控使命)先删去去。

点击检查代码
/// <summary>
/// 2.2 断开当时衔接
/// </summary>
public void Disconnect()
{
    if (current_session != null)
    {
        if(current_session.Connected)
        {
            string name = current_session.SessionName;
            current_session.RemoveSubscriptions(current_session.Subscriptions.ToList()); //删去会话中的悉数订阅使命(监控使命)
            current_session.Close();
            m_sessions.Remove( current_session );
            MessageBox.Show(name+"会话完毕");
            if(m_sessions.Count > 0)
            {
                current_session=m_sessions.First();
            }
        }
        else
        {
            MessageBox.Show("当时会话未衔接");
        }
    }
    else
    {
        MessageBox.Show("当时无会话");
    }
}

扫描二维码推送至手机访问。

版权声明:本文由51Blog发布,如需转载请注明出处。

本文链接:https://www.51blog.vip/?id=150

标签: OPCUA
分享给朋友:

“OPCUA讨论(三)——客户端代码解读” 的相关文章

python免费,免费资源与学习路径

python免费,免费资源与学习路径

当然可以,我随时准备为您提供免费的Python编程帮助!您有任何问题或需要帮助的地方,请随时告诉我。Python编程语言入门指南:免费资源与学习路径Python作为一种简单易学、功能强大的编程语言,已经成为全球范围内最受欢迎的编程语言之一。对于想要学习Python的新手来说,以下是一篇详细的入门指南...

go翻译成中文,从基础到实践

Go 是一种编程语言,中文译名为“Go语言”或“戈语言”。Go语言由Google开发,旨在提高编程效率和软件的可维护性。它是一种静态类型、编译型语言,具有简洁、高效、并发性强的特点。Go语言入门指南:从基础到实践Go语言,也被称为Golang,是由Google开发的一种静态类型、编译型、并发型编程语...

java工具,提升效率的利器

java工具,提升效率的利器

1. 集成开发环境(IDEs): IntelliJ IDEA:由 JetBrains 开发,功能强大,适合大型项目。 Eclipse:开源的 IDE,广泛用于 Java 开发。 NetBeans:另一个开源的 IDE,适合初学者。 Visual Studio Code:虽然不...

java开源项目,助力开发者高效编程的利器

java开源项目,助力开发者高效编程的利器

1. JavaGuide 提供了丰富的Java开源项目资源,包括框架、工具和教程等,灵感来源于 awesomejava 项目。你可以访问以下链接了解 2. CSDN 上有多篇文章介绍了基于Spring Boot的优质Java开源项目,涵盖了电商、微服务、支付、秒杀、博客、管理后台等多个...

r语言apply函数,数据处理与计算的利器

`apply` 函数是 R 语言中的一个强大工具,它允许用户对矩阵或数据框的列或行应用一个函数。这个函数特别适用于需要对矩阵或数据框的每一列或每一行进行相同的操作,比如计算每一列或每一行的平均值、标准差、最大值、最小值等。 基本语法`apply` `X`: 一个矩阵或数据框。 `MARGIN`: 应...

go反编译, Android应用软件游戏汉化概述

go反编译, Android应用软件游戏汉化概述

Go语言的反编译通常是指将Go语言的编译后的二进制文件转换回Go源代码的过程。Go语言的二进制文件并不包含原始源代码的足够信息,这使得完全准确的反编译变得非常困难。尽管如此,还是有一些工具和技术可以尝试从Go的二进制文件中提取出有用的信息。 常用的Go反编译工具1. Gobuster:一个用于暴力破...