public static CompletableFuture<Channel> bootstrap(UaTcpStackClient client) { CompletableFuture<Channel> handshake = new CompletableFuture<>(); Bootstrap bootstrap = new Bootstrap(); bootstrap .group(Stack.sharedEventLoop()) .channel(NioSocketChannel.class) .option(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT) .option(ChannelOption.TCP_NODELAY, true) .handler( new ChannelInitializer<SocketChannel>() { @Override protected void initChannel(SocketChannel channel) throws Exception { channel.pipeline().addLast(new UaTcpClientAcknowledgeHandler(client, handshake)); } }); try { URI uri = URI.create(client.getEndpointUrl()); bootstrap .connect(uri.getHost(), uri.getPort()) .addListener( (ChannelFuture f) -> { if (!f.isSuccess()) { handshake.completeExceptionally(f.cause()); } }); } catch (Throwable t) { UaException failure = new UaException( StatusCodes.Bad_TcpEndpointUrlInvalid, "endpoint URL invalid: " + client.getEndpointUrl()); handshake.completeExceptionally(failure); } return handshake; }
public class UaTcpStackClient implements UaStackClient { private static final long DEFAULT_TIMEOUT_MS = 60000; private final Logger logger = LoggerFactory.getLogger(getClass()); private final Map<UInteger, CompletableFuture<UaResponseMessage>> pending = Maps.newConcurrentMap(); private final Map<UInteger, Timeout> timeouts = Maps.newConcurrentMap(); private final HashedWheelTimer wheelTimer = Stack.sharedWheelTimer(); private volatile ClientSecureChannel secureChannel; private final ApplicationDescription application; private final ChannelManager channelManager; private final UaTcpStackClientConfig config; public UaTcpStackClient(UaTcpStackClientConfig config) { this.config = config; application = new ApplicationDescription( config.getApplicationUri(), config.getProductUri(), config.getApplicationName(), ApplicationType.Client, null, null, null); secureChannel = new ClientSecureChannel(SecurityPolicy.None, MessageSecurityMode.None); channelManager = new ChannelManager(this); } @Override public CompletableFuture<UaStackClient> connect() { CompletableFuture<UaStackClient> future = new CompletableFuture<>(); channelManager .getChannel() .whenComplete( (ch, ex) -> { if (ch != null) future.complete(this); else future.completeExceptionally(ex); }); return future; } @Override public CompletableFuture<UaStackClient> disconnect() { channelManager.disconnect(); return CompletableFuture.completedFuture(this); } @SuppressWarnings("unchecked") public <T extends UaResponseMessage> CompletableFuture<T> sendRequest(UaRequestMessage request) { return channelManager .getChannel() .thenCompose( ch -> { CompletableFuture<T> future = new CompletableFuture<>(); RequestHeader requestHeader = request.getRequestHeader(); pending.put( requestHeader.getRequestHandle(), (CompletableFuture<UaResponseMessage>) future); scheduleRequestTimeout(requestHeader); ch.writeAndFlush(request) .addListener( f -> { if (!f.isSuccess()) { UInteger requestHandle = request.getRequestHeader().getRequestHandle(); pending.remove(requestHandle); future.completeExceptionally(f.cause()); logger.debug("Write failed, requestHandle={}", requestHandle, f.cause()); } }); return future; }); } @SuppressWarnings("unchecked") public void sendRequests( List<? extends UaRequestMessage> requests, List<CompletableFuture<? extends UaResponseMessage>> futures) { Preconditions.checkArgument( requests.size() == futures.size(), "requests and futures parameters must be same size"); channelManager .getChannel() .whenComplete( (ch, ex) -> { if (ch != null) { Iterator<? extends UaRequestMessage> requestIterator = requests.iterator(); Iterator<CompletableFuture<? extends UaResponseMessage>> futureIterator = futures.iterator(); while (requestIterator.hasNext() && futureIterator.hasNext()) { UaRequestMessage request = requestIterator.next(); CompletableFuture<UaResponseMessage> future = (CompletableFuture<UaResponseMessage>) futureIterator.next(); RequestHeader requestHeader = request.getRequestHeader(); pending.put(requestHeader.getRequestHandle(), future); scheduleRequestTimeout(requestHeader); } ch.eventLoop() .execute( () -> { for (UaRequestMessage request : requests) { ch.write(request) .addListener( f -> { if (!f.isSuccess()) { UInteger requestHandle = request.getRequestHeader().getRequestHandle(); CompletableFuture<?> future = pending.remove(requestHandle); if (future != null) future.completeExceptionally(f.cause()); logger.debug( "Write failed, requestHandle={}", requestHandle, f.cause()); } }); } ch.flush(); }); } else { futures.forEach(f -> f.completeExceptionally(ex)); } }); } public CompletableFuture<Channel> getChannelFuture() { return channelManager.getChannel(); } private void scheduleRequestTimeout(RequestHeader requestHeader) { UInteger requestHandle = requestHeader.getRequestHandle(); long timeoutHint = requestHeader.getTimeoutHint() != null ? requestHeader.getTimeoutHint().longValue() : DEFAULT_TIMEOUT_MS; Timeout timeout = wheelTimer.newTimeout( t -> { timeouts.remove(requestHandle); if (!t.isCancelled()) { CompletableFuture<UaResponseMessage> f = pending.remove(requestHandle); if (f != null) { String message = "request timed out after " + timeoutHint + "ms"; f.completeExceptionally(new UaException(StatusCodes.Bad_Timeout, message)); } } }, timeoutHint, TimeUnit.MILLISECONDS); timeouts.put(requestHandle, timeout); } public void receiveResponse(UaResponseMessage response) { ResponseHeader header = response.getResponseHeader(); UInteger requestHandle = header.getRequestHandle(); CompletableFuture<UaResponseMessage> future = pending.remove(requestHandle); if (future != null) { if (header.getServiceResult().isGood()) { future.complete(response); } else { ServiceFault serviceFault; if (response instanceof ServiceFault) { serviceFault = (ServiceFault) response; } else { serviceFault = new ServiceFault(header); } future.completeExceptionally(new UaServiceFaultException(serviceFault)); } Timeout timeout = timeouts.remove(requestHandle); if (timeout != null) timeout.cancel(); } else { logger.debug( "Received {} for unknown requestHandle: {}", response.getClass().getSimpleName(), requestHandle); } } @Override public Optional<X509Certificate> getCertificate() { return config.getCertificate(); } @Override public Optional<KeyPair> getKeyPair() { return config.getKeyPair(); } @Override public ChannelConfig getChannelConfig() { return config.getChannelConfig(); } @Override public UInteger getChannelLifetime() { return config.getChannelLifetime(); } @Override public ClientSecureChannel getSecureChannel() { return secureChannel; } @Override public ApplicationDescription getApplication() { return application; } @Override public Optional<EndpointDescription> getEndpoint() { return config.getEndpoint(); } @Override public String getEndpointUrl() { return config .getEndpoint() .map(EndpointDescription::getEndpointUrl) .orElse(config.getEndpointUrl().orElse("")); } @Override public ExecutorService getExecutorService() { return config.getExecutor(); } public void setSecureChannel(ClientSecureChannel secureChannel) { this.secureChannel = secureChannel; } public static CompletableFuture<Channel> bootstrap(UaTcpStackClient client) { CompletableFuture<Channel> handshake = new CompletableFuture<>(); Bootstrap bootstrap = new Bootstrap(); bootstrap .group(Stack.sharedEventLoop()) .channel(NioSocketChannel.class) .option(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT) .option(ChannelOption.TCP_NODELAY, true) .handler( new ChannelInitializer<SocketChannel>() { @Override protected void initChannel(SocketChannel channel) throws Exception { channel.pipeline().addLast(new UaTcpClientAcknowledgeHandler(client, handshake)); } }); try { URI uri = URI.create(client.getEndpointUrl()); bootstrap .connect(uri.getHost(), uri.getPort()) .addListener( (ChannelFuture f) -> { if (!f.isSuccess()) { handshake.completeExceptionally(f.cause()); } }); } catch (Throwable t) { UaException failure = new UaException( StatusCodes.Bad_TcpEndpointUrlInvalid, "endpoint URL invalid: " + client.getEndpointUrl()); handshake.completeExceptionally(failure); } return handshake; } /** * Query the FindServers service at the given endpoint URL. * * <p>The endpoint URL(s) for each server {@link ApplicationDescription} returned can then be used * in a {@link #getEndpoints(String)} call to discover the endpoints for that server. * * @param endpointUrl the endpoint URL to find servers at. * @return the {@link ApplicationDescription}s returned by the FindServers service. */ public static CompletableFuture<ApplicationDescription[]> findServers(String endpointUrl) { UaTcpStackClientConfig config = UaTcpStackClientConfig.builder().setEndpointUrl(endpointUrl).build(); UaTcpStackClient client = new UaTcpStackClient(config); FindServersRequest request = new FindServersRequest( new RequestHeader(null, DateTime.now(), uint(1), uint(0), null, uint(5000), null), endpointUrl, null, null); return client .<FindServersResponse>sendRequest(request) .whenComplete((r, ex) -> client.disconnect()) .thenApply(FindServersResponse::getServers); } /** * Query the GetEndpoints service at the given endpoint URL. * * @param endpointUrl the endpoint URL to get endpoints from. * @return the {@link EndpointDescription}s returned by the GetEndpoints service. */ public static CompletableFuture<EndpointDescription[]> getEndpoints(String endpointUrl) { UaTcpStackClientConfig config = UaTcpStackClientConfig.builder().setEndpointUrl(endpointUrl).build(); UaTcpStackClient client = new UaTcpStackClient(config); GetEndpointsRequest request = new GetEndpointsRequest( new RequestHeader(null, DateTime.now(), uint(1), uint(0), null, uint(5000), null), endpointUrl, null, new String[] {Stack.UA_TCP_BINARY_TRANSPORT_URI}); return client .<GetEndpointsResponse>sendRequest(request) .whenComplete((r, ex) -> client.disconnect()) .thenApply(GetEndpointsResponse::getEndpoints); } }