All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.joynr.messaging.websocket.jetty.client.WebSocketJettyClient Maven / Gradle / Ivy

There is a newer version: 1.25.0
Show newest version
/*
 * #%L
 * %%
 * Copyright (C) 2020 BMW Car IT GmbH
 * %%
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * #L%
 */
package io.joynr.messaging.websocket.jetty.client;

import java.io.IOException;
import java.net.URI;
import java.nio.ByteBuffer;
import java.time.Duration;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicBoolean;

import io.joynr.messaging.websocket.MessageHelper;
import org.eclipse.jetty.websocket.api.Session;
import org.eclipse.jetty.websocket.api.WebSocketAdapter;
import org.eclipse.jetty.websocket.api.WriteCallback;
import org.eclipse.jetty.websocket.api.exceptions.WebSocketException;
import org.eclipse.jetty.websocket.client.WebSocketClient;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.fasterxml.jackson.core.JsonProcessingException;

import io.joynr.exceptions.JoynrCommunicationException;
import io.joynr.exceptions.JoynrDelayMessageException;
import io.joynr.exceptions.JoynrIllegalStateException;
import io.joynr.exceptions.JoynrMessageExpiredException;
import io.joynr.exceptions.JoynrShutdownException;
import io.joynr.messaging.FailureAction;
import io.joynr.messaging.SuccessAction;
import io.joynr.messaging.websocket.IWebSocketMessagingSkeleton;
import io.joynr.messaging.websocket.JoynrWebSocketEndpoint;
import io.joynr.util.ObjectMapper;
import joynr.system.RoutingTypes.Address;
import joynr.system.RoutingTypes.WebSocketAddress;
import joynr.system.RoutingTypes.WebSocketClientAddress;

public class WebSocketJettyClient extends WebSocketAdapter implements JoynrWebSocketEndpoint {
    private static final Logger logger = LoggerFactory.getLogger(WebSocketJettyClient.class);

    private Timer reconnectTimer = new Timer();
    private AtomicBoolean reconnectTimerRunning = new AtomicBoolean(false);
    private long reconnectDelay;

    private WebSocketClient jettyClient;
    private int maxMessageSize;
    private long websocketIdleTimeout;
    CompletableFuture sessionFuture;
    private WebSocketAddress serverAddress;
    private IWebSocketMessagingSkeleton messageListener;
    private ObjectMapper objectMapper;
    private WebSocketClientAddress ownAddress;

    private boolean shutdown = false;

    public WebSocketJettyClient(WebSocketAddress serverAddress,
                                WebSocketClientAddress ownAddress,
                                int maxMessageSize,
                                long reconnectDelay,
                                long websocketIdleTimeout,
                                ObjectMapper objectMapper) {
        this.serverAddress = (serverAddress != null) ? new WebSocketAddress(serverAddress) : null;
        this.ownAddress = (ownAddress != null) ? new WebSocketClientAddress(ownAddress) : null;
        this.maxMessageSize = maxMessageSize;
        this.reconnectDelay = reconnectDelay;
        this.websocketIdleTimeout = websocketIdleTimeout;
        this.objectMapper = new ObjectMapper(objectMapper);
    }

    @Override
    public synchronized void start() {
        if (jettyClient == null) {
            jettyClient = new WebSocketClient();
            jettyClient.setMaxTextMessageSize(maxMessageSize);
            jettyClient.setMaxBinaryMessageSize(maxMessageSize);
            jettyClient.setIdleTimeout(Duration.ofMillis(websocketIdleTimeout));
        }

        try {
            logger.debug("Starting WebSocket client ...");
            jettyClient.start();
            URI toUri = toUrl(serverAddress);
            logger.debug("Connecting to {} ... ", toUri);
            sessionFuture = jettyClient.connect(this, toUri);

            Session session = sessionFuture.get(30, TimeUnit.SECONDS);
            logger.debug("WebSocket client connected");
            sendInitializationMessage(session);
        } catch (JoynrShutdownException | JoynrIllegalStateException e) {
            logger.error("Unrecoverable error starting WebSocket client: {}", e);
            return;
        } catch (InterruptedException e) {
            logger.error("Thread interrupted while start.", e);
            Thread.currentThread().interrupt();
        } catch (Exception e) {
            // TODO which exceptions are recoverable? Only catch those ones
            // JoynrCommunicationExeption is thrown if the initialization message could not be sent
            logger.debug("Error starting WebSocket client. Will retry", e);
            if (shutdown) {
                return;
            }

            if (reconnectTimerRunning.compareAndSet(false, true)) {
                reconnectTimer.schedule(new TimerTask() {
                    @Override
                    public void run() {
                        reconnectTimerRunning.set(false);
                        start();
                    }
                }, reconnectDelay);
            }
        }
    }

    private void sendInitializationMessage(Session session) throws JoynrCommunicationException {
        String serializedAddress;
        try {
            serializedAddress = objectMapper.writeValueAsString(ownAddress);
        } catch (JsonProcessingException e) {
            throw new JoynrIllegalStateException("unable to serialize WebSocket Client address: " + ownAddress, e);
        }

        try {
            session.getRemote().sendBytes(ByteBuffer.wrap(serializedAddress.getBytes(CHARSET)));
        } catch (IOException e) {
            throw new JoynrCommunicationException(e.getMessage(), e);
        }
    }

    private URI toUrl(WebSocketAddress address) {
        try {
            return URI.create(address.getProtocol() + "://" + address.getHost() + ":" + address.getPort() + ""
                    + address.getPath());
        } catch (IllegalArgumentException e) {
            throw new JoynrIllegalStateException("unable to parse WebSocket Server Address", e);
        }
    }

    @Override
    public void setMessageListener(IWebSocketMessagingSkeleton messaging) {
        this.messageListener = messaging;
    }

    @Override
    public synchronized void shutdown() {
        shutdown = true;
        closeSession();
        try {
            if (jettyClient != null) {
                jettyClient.stop();
            }
        } catch (Exception e) {
            logger.error("Error stopping WebSocket client: ", e);
        }

    }

    private void closeSession() {
        try {
            if (sessionFuture != null) {
                Session session = sessionFuture.get();
                if (session != null) {
                    session.close();
                }
                sessionFuture = null;
            }
        } catch (InterruptedException ex) {
            logger.error("Thread interrupted while closing session.", ex);
            Thread.currentThread().interrupt();
        } catch (Exception e) {
            logger.error("Error while closing websocket connection: ", e);
        }
    }

    @Override
    public synchronized void reconnect() {

        try {
            if (sessionFuture != null && sessionFuture.get().isOpen()) {
                return;
            }
        } catch (ExecutionException e) {
            // continue reconnecting if there was a problem
            logger.debug("Error getting session future", e);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }

        if (shutdown) {
            return;
        }
        closeSession();
        start();
    }

    @Override
    public void onWebSocketText(String message) {
        logger.error("Received text Message: {}", message);
    }

    @Override
    public void onWebSocketBinary(byte[] payload, int offset, int len) {
        final byte[] message = MessageHelper.extractMessage(payload, offset, len);
        if (logger.isTraceEnabled()) {
            logger.trace("Received message: {}", new String(message, CHARSET));
        }
        messageListener.transmit(message, error -> {
            if (error instanceof JoynrMessageExpiredException) {
                logger.warn("WebSocket message not processed: ", error);
            } else {
                logger.error("WebSocket message not processed: ", error);
            }
        });
    }

    @Override
    public synchronized void writeBytes(Address to,
                                        byte[] message,
                                        long timeout,
                                        TimeUnit unit,
                                        final SuccessAction successAction,
                                        final FailureAction failureAction) {
        if (messageListener == null) {
            throw new JoynrDelayMessageException(20, "WebSocket write failed: receiver has not been set yet");
        }

        if (sessionFuture == null) {
            try {
                reconnect();
            } catch (Exception e) {
                throw new JoynrDelayMessageException(10, "WebSocket reconnect failed. Will try later", e);
            }
        }

        try {
            Session session = sessionFuture.get(timeout, unit);
            session.getRemote().sendBytes(ByteBuffer.wrap(message), new WriteCallback() {

                @Override
                public void writeSuccess() {
                    successAction.execute();
                }

                @Override
                public void writeFailed(Throwable error) {
                    if (error instanceof WebSocketException) {
                        reconnect();
                        failureAction.execute(new JoynrDelayMessageException(reconnectDelay, error.getMessage()));
                    } else {
                        failureAction.execute(error);
                    }
                }
            });
        } catch (WebSocketException | ExecutionException e) {
            reconnect();
            throw new JoynrDelayMessageException(10, "WebSocket write failed", e);
        } catch (TimeoutException e) {
            throw new JoynrDelayMessageException("WebSocket write timed out", e);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }

    @Override
    public void onWebSocketClose(int statusCode, String reason) {
        super.onWebSocketClose(statusCode, reason);
        /* maybe the socket has been disconnected by the server, so let's retry */
        if (!shutdown) {
            reconnect();
        }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy