使用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),重点关注会话建立、安全模型、传输协议部分。
其他排查点
- Codesys OPC UA配置:确认Codesys中已启用OPC UA服务器,端点地址为
opc.tcp://localhost:4840/Gateway-1,允许匿名访问,安全策略设置为None(和代码中useSecurity: false匹配)。 - 防火墙与权限:检查本地防火墙是否允许4840端口的TCP连接,运行程序的用户是否有访问OPC UA证书存储目录的权限。
- 异步代码规范:避免在异步方法中使用
.Wait(),改用async/await模式,防止死锁。
内容的提问来源于stack exchange,提问作者Dogulas
相关产品推荐
相关产品推荐

