diff --git a/Libraries/Opc.Ua.Client/Session.cs b/Libraries/Opc.Ua.Client/Session.cs index befe45c397..67cdb32819 100644 --- a/Libraries/Opc.Ua.Client/Session.cs +++ b/Libraries/Opc.Ua.Client/Session.cs @@ -799,28 +799,41 @@ namespace Opc.Ua.Client } /// - /// Creates a new communication session with a server using a reverse connection. + /// Creates a new session with a server using the specified channel by invoking the CreateSession service /// /// The configuration for the client application. - /// The client endpoint for the reverse connect. + /// The channel for the server. /// The endpoint for the server. - /// If set to true the discovery endpoint is used to update the endpoint description before connecting. - /// If set to true then the domain in the certificate must match the endpoint used. - /// The name to assign to the session. - /// The timeout period for the session. - /// The user identity to associate with the session. - /// The preferred locales. - /// The new session object. - public static async Task Create( + /// The certificate to use for the client. + /// The list of available endpoints returned by server in GetEndpoints() response. + /// The value of profileUris used in GetEndpoints() request. + /// + public static Session Create( + ApplicationConfiguration configuration, + ITransportChannel channel, + ConfiguredEndpoint endpoint, + X509Certificate2 clientCertificate, + EndpointDescriptionCollection availableEndpoints = null, + StringCollection discoveryProfileUris = null) + { + return new Session(channel, configuration, endpoint, clientCertificate, availableEndpoints, discoveryProfileUris); + } + + /// + /// Creates a secure channel to the specified endpoint. + /// + /// The application configuration. + /// The client endpoint for the reverse connect. + /// A configured endpoint to connect to. + /// Update configuration based on server prior connect. + /// Check that the certificate specifies a valid domain (computer) name. + /// A representing the asynchronous operation. + public static async Task CreateChannelAsync( ApplicationConfiguration configuration, ITransportWaitingConnection connection, ConfiguredEndpoint endpoint, bool updateBeforeConnect, - bool checkDomain, - string sessionName, - uint sessionTimeout, - IUserIdentity identity, - IList preferredLocales) + bool checkDomain) { endpoint.UpdateBeforeConnect = updateBeforeConnect; @@ -888,6 +901,36 @@ namespace Opc.Ua.Client messageContext); } + return channel; + } + + /// + /// Creates a new communication session with a server using a reverse connection. + /// + /// The configuration for the client application. + /// The client endpoint for the reverse connect. + /// The endpoint for the server. + /// If set to true the discovery endpoint is used to update the endpoint description before connecting. + /// If set to true then the domain in the certificate must match the endpoint used. + /// The name to assign to the session. + /// The timeout period for the session. + /// The user identity to associate with the session. + /// The preferred locales. + /// The new session object. + public static async Task Create( + ApplicationConfiguration configuration, + ITransportWaitingConnection connection, + ConfiguredEndpoint endpoint, + bool updateBeforeConnect, + bool checkDomain, + string sessionName, + uint sessionTimeout, + IUserIdentity identity, + IList preferredLocales) + { + // initialize the channel which will be created with the server. + ITransportChannel channel = await Session.CreateChannelAsync(configuration, connection, endpoint, updateBeforeConnect, checkDomain).ConfigureAwait(false); + // create the session object. Session session = new Session(channel, configuration, endpoint, null); @@ -1074,6 +1117,45 @@ namespace Opc.Ua.Client return session; } + + /// + /// Recreates a session based on a specified template using the provided channel. + /// + /// The Session object to use as template + /// The waiting reverse connection. + /// The new session object. + public static Session Recreate(Session template, ITransportChannel transportChannel) + { + var messageContext = template.m_configuration.CreateMessageContext(); + messageContext.Factory = template.Factory; + + // create the session object. + Session session = new Session(transportChannel, template, true); + + try + { + // open the session. + session.Open( + template.m_sessionName, + (uint)template.m_sessionTimeout, + template.m_identity, + template.m_preferredLocales, + template.m_checkDomain); + + // create the subscriptions. + foreach (Subscription subscription in session.Subscriptions) + { + subscription.Create(); + } + } + catch (Exception e) + { + session.Dispose(); + throw ServiceResultException.Create(StatusCodes.BadCommunicationError, e, "Could not recreate session. {0}", template.m_sessionName); + } + + return session; + } #endregion #region Delegates and Events @@ -1100,13 +1182,13 @@ namespace Opc.Ua.Client /// public void Reconnect() { - Reconnect(null); + Reconnect(null, null); } /// /// Reconnects to the server after a network failure using a waiting connection. /// - public void Reconnect(ITransportWaitingConnection connection) + public void Reconnect(ITransportWaitingConnection connection, ITransportChannel transportChannel = null) { try { @@ -1204,6 +1286,10 @@ namespace Opc.Ua.Client TransportChannel = channel; } } + else if (transportChannel != null) + { + TransportChannel = transportChannel; + } else { // check if the channel supports reconnect. @@ -3206,13 +3292,23 @@ namespace Opc.Ua.Client /// public override StatusCode Close() { - return Close(m_keepAliveInterval); + return Close(m_keepAliveInterval, true); + } + + /// + /// Close the session with the server and optionally closes the channel. + /// + /// + /// + public StatusCode Close(bool closeChannel) + { + return Close(m_keepAliveInterval, closeChannel); } /// /// Disconnects from the server and frees any network resources with the specified timeout. /// - public virtual StatusCode Close(int timeout) + public virtual StatusCode Close(int timeout, bool closeChannel = true) { // check if already called. if (Disposed) @@ -3255,7 +3351,10 @@ namespace Opc.Ua.Client CloseSession(null, m_deleteSubscriptionsOnClose); this.OperationTimeout = existingTimeout; - CloseChannel(); + if (closeChannel) + { + CloseChannel(); + } // raised notification indicating the session is closed. SessionCreated(null, null); @@ -3278,7 +3377,11 @@ namespace Opc.Ua.Client } // clean up. - Dispose(); + if (closeChannel) + { + Dispose(); + } + return result; } #endregion @@ -3469,7 +3572,6 @@ namespace Opc.Ua.Client Utils.LogInfo("No subscriptions. Transfersubscription skipped."); } - } return true; diff --git a/Stack/Opc.Ua.Core/Stack/Client/ClientBase.cs b/Stack/Opc.Ua.Core/Stack/Client/ClientBase.cs index 568ea2c8cc..de474751d3 100644 --- a/Stack/Opc.Ua.Core/Stack/Client/ClientBase.cs +++ b/Stack/Opc.Ua.Core/Stack/Client/ClientBase.cs @@ -30,15 +30,7 @@ namespace Opc.Ua { if (channel == null) throw new ArgumentNullException(nameof(channel)); - m_channel = channel; - m_useTransportChannel = true; - - UaChannelBase uaChannel = channel as UaChannelBase; - - if (uaChannel != null) - { - m_useTransportChannel = uaChannel.m_uaBypassChannel != null || uaChannel.UseBinaryEncoding; - } + InitializeChannel(channel); } #endregion @@ -245,6 +237,23 @@ namespace Opc.Ua #endregion #region Public Methods + /// + /// Attach the channel to an already created client. + /// + /// Channel to be used by the client + public void AttachChannel(ITransportChannel channel) + { + InitializeChannel(channel); + } + + /// + /// Detach the channel. + /// + public void DetachChannel() + { + m_channel = null; + } + /// /// Closes the channel. /// @@ -282,6 +291,23 @@ namespace Opc.Ua #endregion #region Protected Methods + /// + /// Initializes the channel. + /// + /// + protected void InitializeChannel(ITransportChannel channel) + { + m_channel = channel; + m_useTransportChannel = true; + + UaChannelBase uaChannel = channel as UaChannelBase; + + if (uaChannel != null) + { + m_useTransportChannel = uaChannel.m_uaBypassChannel != null || uaChannel.UseBinaryEncoding; + } + } + /// /// Closes the channel. /// diff --git a/Tests/Opc.Ua.Client.Tests/ClientFixture.cs b/Tests/Opc.Ua.Client.Tests/ClientFixture.cs index 703dbcd8c0..f78225dba5 100644 --- a/Tests/Opc.Ua.Client.Tests/ClientFixture.cs +++ b/Tests/Opc.Ua.Client.Tests/ClientFixture.cs @@ -207,6 +207,27 @@ namespace Opc.Ua.Client.Tests return session; } + /// + /// Create a channel using the specified endpoint. + /// + /// The configured endpoint + /// + public async Task CreateChannelAsync(ConfiguredEndpoint endpoint) + { + return await Session.CreateChannelAsync(Config, null, endpoint, true, false).ConfigureAwait(false); + } + + /// + /// Create a session using the specified channel. + /// + /// The channel to use + /// The configured endpoint + /// + public Session CreateSession(ITransportChannel channel, ConfiguredEndpoint endpoint) + { + return Session.Create(Config, channel, endpoint, null); + } + /// /// Get configured endpoint from url with security profile. /// diff --git a/Tests/Opc.Ua.Client.Tests/ClientTest.cs b/Tests/Opc.Ua.Client.Tests/ClientTest.cs index 9740694423..a077068bb0 100644 --- a/Tests/Opc.Ua.Client.Tests/ClientTest.cs +++ b/Tests/Opc.Ua.Client.Tests/ClientTest.cs @@ -242,6 +242,34 @@ namespace Opc.Ua.Client.Tests session.Dispose(); } + [Test, Order(240)] + public async Task ConnectMultipleSessionsAsync() + { + var endpoint = await ClientFixture.GetEndpointAsync(this.ServerUrl, SecurityPolicies.Basic256Sha256, this.Endpoints); + Assert.NotNull(endpoint); + + var channel = await ClientFixture.CreateChannelAsync(endpoint).ConfigureAwait(false); + Assert.NotNull(channel); + + var session1 = ClientFixture.CreateSession(channel, endpoint); + session1.Open("Session1", null); + + var session2 = ClientFixture.CreateSession(channel, endpoint); + session2.Open("Session2", null); + + session1.Close(closeChannel: false); + session1.DetachChannel(); + session1.Dispose(); + + _ = session2.ReadValue(VariableIds.Server_ServerStatus, typeof(ServerStatusDataType)); + + session2.Close(closeChannel: false); + session2.DetachChannel(); + session2.Dispose(); + + channel.Dispose(); + } + [Test, Order(300)] public new void GetOperationLimits() {