You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用C# OPC UA连接Codesys软PLC失败,求排查方案

OPC UA连接Codesys PLC抛出BadConnectionClosed异常的解决方案

问题描述

我用C#和OPCFoundation.NetStandard.Opc.Ua开发Hello World应用,尝试连接运行中的Codesys PLC。目前能创建会话,但打开会话时抛出BadConnectionClosed异常,找不到足够的Opc.Ua包文档理解细节,求助排查工具、文档指引和调试建议。

原代码

public class OPCAgent
{
    public static void Init()
    {
        UaClient client = new UaClient("opc.tcp://localhost:4840/Gateway-1");
        ReadTag(client, "ns=2;s=Main.dummyCntr").Wait();
        client.Disconnect();
    }

}


public class UaClient
{
    private Opc.Ua.ApplicationConfiguration _config;
    private Session? _session;
    private string _endpointUrl;
    private EndpointDescription? _selectedEndpoint = null;
    private string _dbgStr = "";

    public UaClient(string endpointUrl)
    {
        _endpointUrl = endpointUrl;
        _config = new Opc.Ua.ApplicationConfiguration
        {
            ApplicationName = "POC_CodesysHelloWorld",
            ApplicationType = ApplicationType.Client,
            SecurityConfiguration = new SecurityConfiguration 
            { 
                ApplicationCertificate = new CertificateIdentifier
                { 
                    StoreType = @"Directory", 
                    StorePath = @"%CommonApplicationData%\OPC Foundation\CertificateStores\MachineDefault", 
                    SubjectName = "POC_CodesysHelloWorld"
                },
                TrustedIssuerCertificates = new CertificateTrustList 
                { 
                    StoreType = @"Directory", 
                    StorePath = @"%CommonApplicationData%\OPC Foundation\CertificateStores\UA Certificate Authorities" 
                },
                TrustedPeerCertificates = new CertificateTrustList 
                { 
                    StoreType = @"Directory", 
                    StorePath = @"%CommonApplicationData%\OPC Foundation\CertificateStores\UA Applications" 
                },
                RejectedCertificateStore = new CertificateTrustList 
                { 
                    StoreType = @"Directory", 
                    StorePath = @"%CommonApplicationData%\OPC Foundation\CertificateStores\RejectedCertificates" 
                },
                AutoAcceptUntrustedCertificates = true
            },
            TransportConfigurations = new TransportConfigurationCollection(),
            TransportQuotas = new TransportQuotas { OperationTimeout = 15000 },
            ClientConfiguration = new ClientConfiguration { DefaultSessionTimeout = 60000 },
            TraceConfiguration = new TraceConfiguration(),
            CertificateValidator = new CertificateValidator() { AutoAcceptUntrustedCertificates= true }
        };
        _config.Validate(ApplicationType.Client).GetAwaiter().GetResult();
        if (_config.SecurityConfiguration.AutoAcceptUntrustedCertificates)
        {
            _config.CertificateValidator.CertificateValidation += (s, e) => { e.Accept = e.Error.StatusCode == StatusCodes.BadCertificateUntrusted; };
        }

        ApplicationInstance application = new ApplicationInstance
        {
            ApplicationName = "POC_CodesysHelloWorld",
            ApplicationType = ApplicationType.Client,
            ApplicationConfiguration = _config
        };
        if (!application.CheckApplicationInstanceCertificate(false, 2048).GetAwaiter().GetResult())
        {
            _dbgStr = "CheckApplicationInstanceCertificate returned false";
        }    
        try
        {
            _selectedEndpoint = CoreClientUtils.SelectEndpoint(_endpointUrl, useSecurity: false, 15000);
        }
        catch (Exception ex)
        {
            _dbgStr = ex.Message;
            throw;
        }
        EndpointConfiguration endpointConfiguration = EndpointConfiguration.Create(_config);
        ConfiguredEndpoint endpoint = new ConfiguredEndpoint(null, _selectedEndpoint, endpointConfiguration);
        X509Certificate2 clientCertificate = _config.SecurityConfiguration.ApplicationCertificate.Certificate;
        ServiceMessageContext serviceMessageContext = _config.CreateMessageContext();
        ITransportChannel transportChannel = SessionChannel.Create(
            _config,
            UaClient.CreateEndpointDescription(clientCertificate),
            endpointConfiguration,
            clientCertificate,
            serviceMessageContext
            );
        try
        {
            _session = new Session(
                transportChannel,
                _config,
                endpoint,
                null
                );
        }
        catch (Exception ex)
        {
            _dbgStr = ex.Message;
            throw;
        }

        try
        {
            //UserIdentity userIdentity = null;
            UserIdentity userIdentity = new UserIdentity();
            //UserIdentity userIdentity = new UserIdentity("doug", "Accip!23!23");
            _session.Open("My Session Name", userIdentity);

            if (_session != null && _session.Connected)
            {
                _dbgStr = "connected";
            }
            else
            {
                _dbgStr = "not connected";
            }
        }
        catch (Exception ex)
        {
            _dbgStr = ex.Message;
            throw;
        }
    }

    public void Disconnect()
    {
        if (_session != null )
        {
            _session.Close();
            _session.Dispose();
            _session = null;
        }
    }

    public async Task<object> ReadNodeAsync(string nodeId)
    {
        if (_session == null) throw new Exception("Session is null");
        var node = new NodeId(nodeId);
        var dataValue = await _session.ReadValueAsync(node);
        return dataValue.Value;
    }

    /// <summary>
    /// Creates a minimal endpoint description which allows a client to connect to a server.
    /// </summary>
    /// <remarks>
    /// In most cases the client will use the server's discovery endpoint to fetch the information
    /// constained in this structure.
    /// </remarks>
    public static EndpointDescription CreateEndpointDescription(X509Certificate2 certificate)
    {
        // create the endpoint description.
        EndpointDescription endpointDescription = new EndpointDescription();

        //endpointDescription.EndpointUrl = Utils.Format("http://{0}:61211/UA/SampleClient", System.Net.Dns.GetHostName());
        endpointDescription.EndpointUrl = Utils.Format("opc.tcp://{0}:51210/UA/SampleServer", System.Net.Dns.GetHostName());
        // endpointDescription.EndpointUrl = Utils.Format("http://{0}:51211/UA/SampleServer/None", System.Net.Dns.GetHostName());
        // endpointDescription.EndpointUrl = Utils.Format("http://{0}:51211/UA/SampleServer", System.Net.Dns.GetHostName());

        // specify the security policy to use.
        // endpointDescription.SecurityPolicyUri = SecurityPolicies.None;
        // endpointDescription.SecurityMode      = MessageSecurityMode.None;;
        endpointDescription.SecurityPolicyUri = SecurityPolicies.Basic256Sha256;
        endpointDescription.SecurityMode = MessageSecurityMode.SignAndEncrypt;

        // specify the transport profile.
        //endpointDescription.TransportProfileUri = Profiles.WsHttpXmlOrBinaryTransport;
        //endpointDescription.TransportProfileUri = Profiles.WsHttpXmlTransport;
        endpointDescription.TransportProfileUri = Profiles.UaTcpTransport;

        endpointDescription.Server.DiscoveryUrls.Add(Utils.Format("http://{0}:61211/UA/SampleClient/discovery", System.Net.Dns.GetHostName()));

        // load the the server certificate from the local certificate store.
        CertificateIdentifier certificateIdentifier = new CertificateIdentifier();

        //certificateIdentifier.StoreType = CertificateStoreType.Windows;
        //certificateIdentifier.StoreType = CertificateStoreType.X509Store;
        certificateIdentifier.StoreType = CertificateStoreType.Directory;
        certificateIdentifier.StorePath = "LocalMachine\\My";
        certificateIdentifier.SubjectName = "UA Sample Client";

        X509Certificate2 serverCertificate = certificate;

        if (serverCertificate == null)
        {
            throw ServiceResultException.Create(StatusCodes.BadCertificateInvalid, "Could not find server certificate: {0}", certificateIdentifier.SubjectName);
        }

        endpointDescription.ServerCertificate = serverCertificate.RawData;

        return endpointDescription;
    }
}

核心代码修复

你的代码中最大的问题是手动构造了错误的EndpointDescription(硬编码了SampleServer地址和安全配置),导致连接到错误的端点。应该使用CoreClientUtils.SelectEndpoint返回的正确端点,并用官方推荐的Session.Create方法建立会话,无需手动创建TransportChannel。

修复后的UaClient构造函数

public class UaClient : IDisposable
{
    private Opc.Ua.ApplicationConfiguration _config;
    private Session? _session;
    private string _endpointUrl;
    private string _dbgStr = "";

    public UaClient(string endpointUrl)
    {
        _endpointUrl = endpointUrl;
        _config = new Opc.Ua.ApplicationConfiguration
        {
            ApplicationName = "POC_CodesysHelloWorld",
            ApplicationType = ApplicationType.Client,
            SecurityConfiguration = new SecurityConfiguration 
            { 
                ApplicationCertificate = new CertificateIdentifier
                { 
                    StoreType = @"Directory", 
                    StorePath = @"%CommonApplicationData%\OPC Foundation\CertificateStores\MachineDefault", 
                    SubjectName = "POC_CodesysHelloWorld"
                },
                TrustedIssuerCertificates = new CertificateTrustList 
                { 
                    StoreType = @"Directory", 
                    StorePath = @"%CommonApplicationData%\OPC Foundation\CertificateStores\UA Certificate Authorities" 
                },
                TrustedPeerCertificates = new CertificateTrustList 
                { 
                    StoreType = @"Directory", 
                    StorePath = @"%CommonApplicationData%\OPC Foundation\CertificateStores\UA Applications" 
                },
                RejectedCertificateStore = new CertificateTrustList 
                { 
                    StoreType = @"Directory", 
                    StorePath = @"%CommonApplicationData%\OPC Foundation\CertificateStores\RejectedCertificates" 
                },
                AutoAcceptUntrustedCertificates = true
            },
            TransportConfigurations = new TransportConfigurationCollection(),
            TransportQuotas = new TransportQuotas { OperationTimeout = 15000 },
            ClientConfiguration = new ClientConfiguration { DefaultSessionTimeout = 60000 },
            // 开启OPC UA日志,方便排查
            TraceConfiguration = new TraceConfiguration { OutputFilePath = "opcua_client.log", TraceMasks = Utils.TraceMasks.All },
            CertificateValidator = new CertificateValidator() { AutoAcceptUntrustedCertificates= true }
        };
        _config.Validate(ApplicationType.Client).GetAwaiter().GetResult();
        if (_config.SecurityConfiguration.AutoAcceptUntrustedCertificates)
        {
            _config.CertificateValidator.CertificateValidation += (s, e) => 
            {
                e.Accept = e.Error?.StatusCode == StatusCodes.BadCertificateUntrusted;
                if (!e.Accept)
                {
                    _dbgStr = $"证书验证失败:{e.Error?.StatusCode} - {e.Error?.Message}";
                }
            };
        }

        ApplicationInstance application = new ApplicationInstance
        {
            ApplicationName = "POC_CodesysHelloWorld",
            ApplicationType = ApplicationType.Client,
            ApplicationConfiguration = _config
        };
        if (!application.CheckApplicationInstanceCertificate(false, 2048).GetAwaiter().GetResult())
        {
            _dbgStr = "客户端证书检查失败";
        }    
        try
        {
            // 选择正确的端点,关闭安全策略适配Codesys配置
            var selectedEndpoint = CoreClientUtils.SelectEndpoint(_endpointUrl, useSecurity: false, 15000);
            var endpointConfiguration = EndpointConfiguration.Create(_config);
            var configuredEndpoint = new ConfiguredEndpoint(null, selectedEndpoint, endpointConfiguration);

            // 使用官方推荐的Session.Create方法建立会话
            _session = Session.Create(
                _config,
                configuredEndpoint,
                false,
                "My Session Name",
                60000,
                new UserIdentity(), // 匿名身份,需和Codesys配置匹配
                null
            ).GetAwaiter().GetResult();

            if (_session != null && _session.Connected)
            {
                _dbgStr = "连接成功";
            }
            else
            {
                _dbgStr = "连接未建立";
            }
        }
        catch (Exception ex)
        {
            _dbgStr = $"连接异常:{ex.Message}";
            throw;
        }
    }

    public async Task<object> ReadNodeAsync(string nodeId)
    {
        if (_session == null) throw new InvalidOperationException("会话未初始化");
        var node = new NodeId(nodeId);
        var dataValue = await _session.ReadValueAsync(node);
        return dataValue.Value;
    }

    // 实现IDisposable自动释放资源
    public void Dispose()
    {
        if (_session != null )
        {
            try
            {
                _session.Close().GetAwaiter().GetResult();
            }
            catch
            {
                // 忽略关闭时的异常
            }
            _session.Dispose();
            _session = null;
        }
    }
}

修复后的OPCAgent

public class OPCAgent
{
    // 改用异步方法避免死锁
    public static async Task Init()
    {
        using (var client = new UaClient("opc.tcp://localhost:4840/Gateway-1"))
        {
            var value = await client.ReadNodeAsync("ns=2;s=Main.dummyCntr");
            Console.WriteLine($"读取到的值:{value}");
        }
    }
}

调试工具建议

  • Wireshark:抓取4840端口的TCP数据包,分析会话建立过程中的传输层错误,比如服务器是否主动关闭连接、数据包是否丢失。
  • UA Expert:用这款免费的OPC UA测试客户端先连接Codesys PLC,确认服务器端点地址、安全配置、节点ID是否正确。如果UA Expert也连不上,优先排查Codesys的OPC UA服务器配置。
  • OPC UA日志:在代码中开启TraceConfiguration,生成详细的客户端日志,查看证书验证、会话握手、消息传输等环节的具体错误信息。

文档指引

  • NuGet包注释:在Visual Studio中直接查看OPCFoundation.NetStandard.Opc.Ua的类、方法XML注释,这是最直接的API使用说明。
  • 官方仓库示例:参考OPCFoundation.NetStandard.Opc.Ua仓库中的SampleClient项目,学习官方推荐的客户端实现方式,避免手动操作底层传输通道。
  • OPC UA规范:如果需要深入理解原理,可参考OPC UA官方规范(Part 4-10),重点关注会话建立、安全模型、传输协议部分。

其他排查点

  1. Codesys OPC UA配置:确认Codesys中已启用OPC UA服务器,端点地址为opc.tcp://localhost:4840/Gateway-1,允许匿名访问,安全策略设置为None(和代码中useSecurity: false匹配)。
  2. 防火墙与权限:检查本地防火墙是否允许4840端口的TCP连接,运行程序的用户是否有访问OPC UA证书存储目录的权限。
  3. 异步代码规范:避免在异步方法中使用.Wait(),改用async/await模式,防止死锁。

内容的提问来源于stack exchange,提问作者Dogulas

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.16 11:09:49