Enable client support for multiple sessions on a single channel (#1659)

* Client API changes to support multiple sessions on a single channel
* Tested against freeopcua:python:server and ProSys simulation server
This commit is contained in:
Zigby
2022-04-01 14:27:32 +02:00
committed by GitHub
parent 0085597be2
commit cbe886dcb0
4 changed files with 208 additions and 31 deletions
+124 -22
View File
@@ -799,28 +799,41 @@ namespace Opc.Ua.Client
}
/// <summary>
/// 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
/// </summary>
/// <param name="configuration">The configuration for the client application.</param>
/// <param name="connection">The client endpoint for the reverse connect.</param>
/// <param name="channel">The channel for the server.</param>
/// <param name="endpoint">The endpoint for the server.</param>
/// <param name="updateBeforeConnect">If set to <c>true</c> the discovery endpoint is used to update the endpoint description before connecting.</param>
/// <param name="checkDomain">If set to <c>true</c> then the domain in the certificate must match the endpoint used.</param>
/// <param name="sessionName">The name to assign to the session.</param>
/// <param name="sessionTimeout">The timeout period for the session.</param>
/// <param name="identity">The user identity to associate with the session.</param>
/// <param name="preferredLocales">The preferred locales.</param>
/// <returns>The new session object.</returns>
public static async Task<Session> Create(
/// <param name="clientCertificate">The certificate to use for the client.</param>
/// <param name="availableEndpoints">The list of available endpoints returned by server in GetEndpoints() response.</param>
/// <param name="discoveryProfileUris">The value of profileUris used in GetEndpoints() request.</param>
/// <returns></returns>
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);
}
/// <summary>
/// Creates a secure channel to the specified endpoint.
/// </summary>
/// <param name="configuration">The application configuration.</param>
/// <param name="connection">The client endpoint for the reverse connect.</param>
/// <param name="endpoint">A configured endpoint to connect to.</param>
/// <param name="updateBeforeConnect">Update configuration based on server prior connect.</param>
/// <param name="checkDomain">Check that the certificate specifies a valid domain (computer) name.</param>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
public static async Task<ITransportChannel> CreateChannelAsync(
ApplicationConfiguration configuration,
ITransportWaitingConnection connection,
ConfiguredEndpoint endpoint,
bool updateBeforeConnect,
bool checkDomain,
string sessionName,
uint sessionTimeout,
IUserIdentity identity,
IList<string> preferredLocales)
bool checkDomain)
{
endpoint.UpdateBeforeConnect = updateBeforeConnect;
@@ -888,6 +901,36 @@ namespace Opc.Ua.Client
messageContext);
}
return channel;
}
/// <summary>
/// Creates a new communication session with a server using a reverse connection.
/// </summary>
/// <param name="configuration">The configuration for the client application.</param>
/// <param name="connection">The client endpoint for the reverse connect.</param>
/// <param name="endpoint">The endpoint for the server.</param>
/// <param name="updateBeforeConnect">If set to <c>true</c> the discovery endpoint is used to update the endpoint description before connecting.</param>
/// <param name="checkDomain">If set to <c>true</c> then the domain in the certificate must match the endpoint used.</param>
/// <param name="sessionName">The name to assign to the session.</param>
/// <param name="sessionTimeout">The timeout period for the session.</param>
/// <param name="identity">The user identity to associate with the session.</param>
/// <param name="preferredLocales">The preferred locales.</param>
/// <returns>The new session object.</returns>
public static async Task<Session> Create(
ApplicationConfiguration configuration,
ITransportWaitingConnection connection,
ConfiguredEndpoint endpoint,
bool updateBeforeConnect,
bool checkDomain,
string sessionName,
uint sessionTimeout,
IUserIdentity identity,
IList<string> 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;
}
/// <summary>
/// Recreates a session based on a specified template using the provided channel.
/// </summary>
/// <param name="template">The Session object to use as template</param>
/// <param name="transportChannel">The waiting reverse connection.</param>
/// <returns>The new session object.</returns>
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
/// </summary>
public void Reconnect()
{
Reconnect(null);
Reconnect(null, null);
}
/// <summary>
/// Reconnects to the server after a network failure using a waiting connection.
/// </summary>
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
/// </summary>
public override StatusCode Close()
{
return Close(m_keepAliveInterval);
return Close(m_keepAliveInterval, true);
}
/// <summary>
/// Close the session with the server and optionally closes the channel.
/// </summary>
/// <param name="closeChannel"></param>
/// <returns></returns>
public StatusCode Close(bool closeChannel)
{
return Close(m_keepAliveInterval, closeChannel);
}
/// <summary>
/// Disconnects from the server and frees any network resources with the specified timeout.
/// </summary>
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;
+35 -9
View File
@@ -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
/// <summary>
/// Attach the channel to an already created client.
/// </summary>
/// <param name="channel">Channel to be used by the client</param>
public void AttachChannel(ITransportChannel channel)
{
InitializeChannel(channel);
}
/// <summary>
/// Detach the channel.
/// </summary>
public void DetachChannel()
{
m_channel = null;
}
/// <summary>
/// Closes the channel.
/// </summary>
@@ -282,6 +291,23 @@ namespace Opc.Ua
#endregion
#region Protected Methods
/// <summary>
/// Initializes the channel.
/// </summary>
/// <param name="channel"></param>
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;
}
}
/// <summary>
/// Closes the channel.
/// </summary>
@@ -207,6 +207,27 @@ namespace Opc.Ua.Client.Tests
return session;
}
/// <summary>
/// Create a channel using the specified endpoint.
/// </summary>
/// <param name="endpoint">The configured endpoint</param>
/// <returns></returns>
public async Task<ITransportChannel> CreateChannelAsync(ConfiguredEndpoint endpoint)
{
return await Session.CreateChannelAsync(Config, null, endpoint, true, false).ConfigureAwait(false);
}
/// <summary>
/// Create a session using the specified channel.
/// </summary>
/// <param name="channel">The channel to use</param>
/// <param name="endpoint">The configured endpoint</param>
/// <returns></returns>
public Session CreateSession(ITransportChannel channel, ConfiguredEndpoint endpoint)
{
return Session.Create(Config, channel, endpoint, null);
}
/// <summary>
/// Get configured endpoint from url with security profile.
/// </summary>
+28
View File
@@ -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()
{