Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance. Project price only 1 $
You can buy this project and download/modify it how often you want.
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
package com.microsoft.signalr;
import java.io.StringReader;
import java.lang.reflect.Type;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gson.stream.JsonReader;
import io.reactivex.rxjava3.core.Completable;
import io.reactivex.rxjava3.core.Observable;
import io.reactivex.rxjava3.core.Single;
import io.reactivex.rxjava3.subjects.*;
import okhttp3.OkHttpClient;
/**
* A connection used to invoke hub methods on a SignalR Server.
*/
public class HubConnection implements AutoCloseable {
private static final byte RECORD_SEPARATOR = 0x1e;
private static final List emptyArray = new ArrayList<>();
private static final int MAX_NEGOTIATE_ATTEMPTS = 100;
private final CallbackMap handlers = new CallbackMap();
private final HubProtocol protocol;
private final boolean skipNegotiate;
private final Map headers;
private final int negotiateVersion = 1;
private final Logger logger = LoggerFactory.getLogger(HubConnection.class);
private final HttpClient httpClient;
private final Transport customTransport;
private final OnReceiveCallBack callback;
private final Single accessTokenProvider;
private final TransportEnum transportEnum;
// These are all user-settable properties
private String baseUrl;
private List onClosedCallbackList;
private long keepAliveInterval = 15 * 1000;
private long serverTimeout = 30 * 1000;
private long handshakeResponseTimeout = 15 * 1000;
// Private property, modified for testing
private long tickRate = 1000;
// Holds all mutable state other than user-defined handlers and settable properties.
private final ReconnectingConnectionState state;
/**
* Sets the server timeout interval for the connection.
*
* @param serverTimeoutInMilliseconds The server timeout duration (specified in milliseconds).
*/
public void setServerTimeout(long serverTimeoutInMilliseconds) {
this.serverTimeout = serverTimeoutInMilliseconds;
}
/**
* Gets the server timeout duration.
*
* @return The server timeout duration (specified in milliseconds).
*/
public long getServerTimeout() {
return this.serverTimeout;
}
/**
* Sets the keep alive interval duration.
*
* @param keepAliveIntervalInMilliseconds The interval (specified in milliseconds) at which the connection should send keep alive messages.
*/
public void setKeepAliveInterval(long keepAliveIntervalInMilliseconds) {
this.keepAliveInterval = keepAliveIntervalInMilliseconds;
}
/**
* Gets the keep alive interval.
*
* @return The interval (specified in milliseconds) between keep alive messages.
*/
public long getKeepAliveInterval() {
return this.keepAliveInterval;
}
/**
* Gets the connections connectionId. This value will be cleared when the connection is stopped and
* will have a new value every time the connection is successfully started.
* @return A string representing the the client's connectionId.
*/
public String getConnectionId() {
ConnectionState state = this.state.getConnectionStateUnsynchronized(true);
if (state != null) {
return state.connectionId;
}
return null;
}
// For testing purposes
void setTickRate(long tickRateInMilliseconds) {
this.tickRate = tickRateInMilliseconds;
}
// For testing purposes
Transport getTransport() {
return this.state.getConnectionState().transport;
}
HubConnection(String url, Transport transport, boolean skipNegotiate, HttpClient httpClient, HubProtocol protocol,
Single accessTokenProvider, long handshakeResponseTimeout, Map headers, TransportEnum transportEnum,
Action1 configureBuilder) {
if (url == null || url.isEmpty()) {
throw new IllegalArgumentException("A valid url is required.");
}
this.state = new ReconnectingConnectionState(this.logger);
this.baseUrl = url;
this.protocol = protocol;
if (accessTokenProvider != null) {
this.accessTokenProvider = accessTokenProvider;
} else {
this.accessTokenProvider = Single.just("");
}
if (httpClient != null) {
this.httpClient = httpClient;
} else {
this.httpClient = new DefaultHttpClient(configureBuilder);
}
if (transport != null) {
this.transportEnum = TransportEnum.ALL;
this.customTransport = transport;
} else if (transportEnum != null) {
this.transportEnum = transportEnum;
this.customTransport = null;
} else {
this.transportEnum = TransportEnum.ALL;
this.customTransport = null;
}
if (handshakeResponseTimeout > 0) {
this.handshakeResponseTimeout = handshakeResponseTimeout;
}
this.headers = headers;
this.skipNegotiate = skipNegotiate;
this.callback = (payload) -> ReceiveLoop(payload);
}
private Single handleNegotiate(String url, Map localHeaders) {
HttpRequest request = new HttpRequest();
request.addHeaders(localHeaders);
return httpClient.post(Negotiate.resolveNegotiateUrl(url, this.negotiateVersion), request).map((response) -> {
if (response.getStatusCode() != 200) {
throw new HttpRequestException(String.format("Unexpected status code returned from negotiate: %d %s.",
response.getStatusCode(), response.getStatusText()), response.getStatusCode());
}
JsonReader reader = new JsonReader(new StringReader(new String(response.getContent().array(), StandardCharsets.UTF_8)));
NegotiateResponse negotiateResponse = new NegotiateResponse(reader);
if (negotiateResponse.getError() != null) {
throw new RuntimeException(negotiateResponse.getError());
}
if (negotiateResponse.getAccessToken() != null) {
localHeaders.put("Authorization", "Bearer " + negotiateResponse.getAccessToken());
}
return negotiateResponse;
});
}
/**
* Indicates the state of the {@link HubConnection} to the server.
*
* @return HubConnection state enum.
*/
public HubConnectionState getConnectionState() {
return this.state.getHubConnectionState();
}
// For testing only
String getBaseUrl() {
return this.baseUrl;
}
/**
* Sets a new url for the HubConnection.
* @param url The url to connect to.
*/
public void setBaseUrl(String url) {
if (url == null || url.isEmpty()) {
throw new IllegalArgumentException("The HubConnection url must be a valid url.");
}
if (this.state.getHubConnectionState() != HubConnectionState.DISCONNECTED) {
throw new IllegalStateException("The HubConnection must be in the disconnected state to change the url.");
}
this.baseUrl = url;
}
/**
* Starts a connection to the server.
*
* @return A Completable that completes when the connection has been established.
*/
public Completable start() {
CompletableSubject localStart = CompletableSubject.create();
this.state.lock.lock();
try {
if (this.state.getHubConnectionState() != HubConnectionState.DISCONNECTED) {
logger.debug("The connection is in the '{}' state. Waiting for in-progress start to complete or completing this start immediately.", this.state.getHubConnectionState());
return this.state.getConnectionStateUnsynchronized(false).startTask;
}
this.state.changeState(HubConnectionState.DISCONNECTED, HubConnectionState.CONNECTING);
CompletableSubject tokenCompletable = CompletableSubject.create();
Map localHeaders = new HashMap<>();
localHeaders.put(UserAgentHelper.getUserAgentName(), UserAgentHelper.createUserAgentString());
if (headers != null) {
localHeaders.putAll(headers);
}
ConnectionState connectionState = new ConnectionState(this);
this.state.setConnectionState(connectionState);
connectionState.startTask = localStart;
accessTokenProvider.subscribe(token -> {
if (token != null && !token.isEmpty()) {
localHeaders.put("Authorization", "Bearer " + token);
}
tokenCompletable.onComplete();
}, error -> {
tokenCompletable.onError(error);
});
Single negotiate = null;
if (!skipNegotiate) {
negotiate = tokenCompletable.andThen(Single.defer(() -> startNegotiate(baseUrl, 0, localHeaders)));
} else {
negotiate = tokenCompletable.andThen(Single.defer(() -> Single.just(new NegotiateResponse(baseUrl))));
}
negotiate.flatMapCompletable(negotiateResponse -> {
logger.debug("Starting HubConnection.");
Transport transport = customTransport;
if (transport == null) {
Single tokenProvider = negotiateResponse.getAccessToken() != null ? Single.just(negotiateResponse.getAccessToken()) : accessTokenProvider;
TransportEnum chosenTransport;
if (this.skipNegotiate) {
if (this.transportEnum != TransportEnum.WEBSOCKETS) {
throw new RuntimeException("Negotiation can only be skipped when using the WebSocket transport directly with '.withTransport(TransportEnum.WEBSOCKETS)' on the 'HubConnectionBuilder'.");
}
chosenTransport = this.transportEnum;
} else {
chosenTransport = negotiateResponse.getChosenTransport();
}
switch (chosenTransport) {
case LONG_POLLING:
transport = new LongPollingTransport(localHeaders, httpClient, tokenProvider);
break;
default:
transport = new WebSocketTransport(localHeaders, httpClient);
}
}
connectionState.transport = transport;
transport.setOnReceive(this.callback);
transport.setOnClose((message) -> stopConnection(message));
return transport.start(negotiateResponse.getFinalUrl()).andThen(Completable.defer(() -> {
ByteBuffer handshake = HandshakeProtocol.createHandshakeRequestMessage(
new HandshakeRequestMessage(protocol.getName(), protocol.getVersion()));
this.state.lock();
try {
if (this.state.hubConnectionState != HubConnectionState.CONNECTING) {
return Completable.error(new RuntimeException("Connection closed while trying to connect."));
}
return connectionState.transport.send(handshake).andThen(Completable.defer(() -> {
this.state.lock();
try {
ConnectionState activeState = this.state.getConnectionStateUnsynchronized(true);
if (activeState != null && activeState == connectionState) {
connectionState.timeoutHandshakeResponse(handshakeResponseTimeout, TimeUnit.MILLISECONDS);
} else {
return Completable.error(new RuntimeException("Connection closed while sending handshake."));
}
} finally {
this.state.unlock();
}
return connectionState.handshakeResponseSubject.andThen(Completable.defer(() -> {
this.state.lock();
try {
ConnectionState activeState = this.state.getConnectionStateUnsynchronized(true);
if (activeState == null || activeState != connectionState) {
return Completable.error(new RuntimeException("Connection closed while waiting for handshake."));
}
this.state.changeState(HubConnectionState.CONNECTING, HubConnectionState.CONNECTED);
logger.info("HubConnection started.");
connectionState.resetServerTimeout();
// Don't send pings if we're using long polling.
if (negotiateResponse.getChosenTransport() != TransportEnum.LONG_POLLING) {
connectionState.activatePingTimer();
}
} finally {
this.state.unlock();
}
return Completable.complete();
}));
}));
} finally {
this.state.unlock();
}
}));
// subscribe makes this a "hot" completable so this runs immediately
}).subscribe(() -> {
localStart.onComplete();
}, error -> {
this.state.lock();
try {
ConnectionState activeState = this.state.getConnectionStateUnsynchronized(true);
if (activeState == connectionState) {
this.state.changeState(HubConnectionState.CONNECTING, HubConnectionState.DISCONNECTED);
}
// this error is already logged and we want the user to see the original error
} catch (Exception ex) {
} finally {
this.state.unlock();
}
localStart.onError(error);
});
} finally {
this.state.lock.unlock();
}
return localStart;
}
private Single startNegotiate(String url, int negotiateAttempts, Map localHeaders) {
if (this.state.getHubConnectionState() != HubConnectionState.CONNECTING) {
throw new RuntimeException("HubConnection trying to negotiate when not in the CONNECTING state.");
}
return handleNegotiate(url, localHeaders).flatMap(response -> {
if (response.getRedirectUrl() != null && negotiateAttempts >= MAX_NEGOTIATE_ATTEMPTS) {
throw new RuntimeException("Negotiate redirection limit exceeded.");
}
if (response.getRedirectUrl() == null) {
Set transports = response.getAvailableTransports();
if (this.transportEnum == TransportEnum.ALL) {
if (transports.contains("WebSockets")) {
response.setChosenTransport(TransportEnum.WEBSOCKETS);
} else if (transports.contains("LongPolling")) {
response.setChosenTransport(TransportEnum.LONG_POLLING);
} else {
throw new RuntimeException("There were no compatible transports on the server.");
}
} else if (this.transportEnum == TransportEnum.WEBSOCKETS && !transports.contains("WebSockets") ||
(this.transportEnum == TransportEnum.LONG_POLLING && !transports.contains("LongPolling"))) {
throw new RuntimeException("There were no compatible transports on the server.");
} else {
response.setChosenTransport(this.transportEnum);
}
String connectionToken = "";
if (response.getVersion() > 0) {
this.state.getConnectionState().connectionId = response.getConnectionId();
connectionToken = response.getConnectionToken();
} else {
connectionToken = response.getConnectionId();
this.state.getConnectionState().connectionId = connectionToken;
}
String finalUrl = Utils.appendQueryString(url, "id=" + connectionToken);
response.setFinalUrl(finalUrl);
return Single.just(response);
}
return startNegotiate(response.getRedirectUrl(), negotiateAttempts + 1, localHeaders);
});
}
/**
* Stops a connection to the server.
*
* @param errorMessage An error message if the connected needs to be stopped because of an error.
* @return A Completable that completes when the connection has been stopped.
*/
private Completable stop(String errorMessage) {
ConnectionState connectionState;
Completable startTask;
this.state.lock();
try {
if (this.state.getHubConnectionState() == HubConnectionState.DISCONNECTED) {
return Completable.complete();
}
connectionState = this.state.getConnectionStateUnsynchronized(false);
if (errorMessage != null) {
connectionState.stopError = errorMessage;
logger.error("HubConnection disconnected with an error: {}.", errorMessage);
} else {
logger.debug("Stopping HubConnection.");
}
startTask = connectionState.startTask;
} finally {
this.state.unlock();
}
Completable stopTask = startTask.onErrorComplete().andThen(Completable.defer(() ->
{
Completable stop = connectionState.transport.stop();
stop.onErrorComplete().subscribe();
return stop;
}));
stopTask.onErrorComplete().subscribe();
return stopTask;
}
private void ReceiveLoop(ByteBuffer payload)
{
List messages;
ConnectionState connectionState;
this.state.lock();
try {
connectionState = this.state.getConnectionState();
connectionState.resetServerTimeout();
connectionState.handleHandshake(payload);
// The payload only contained the handshake response so we can return.
if (!payload.hasRemaining()) {
return;
}
messages = protocol.parseMessages(payload, connectionState);
} finally {
this.state.unlock();
}
for (HubMessage message : messages) {
logger.debug("Received message of type {}.", message.getMessageType());
switch (message.getMessageType()) {
case INVOCATION_BINDING_FAILURE:
InvocationBindingFailureMessage msg = (InvocationBindingFailureMessage)message;
logger.error("Failed to bind arguments received in invocation '{}' of '{}'.", msg.getInvocationId(), msg.getTarget(), msg.getException());
break;
case INVOCATION:
InvocationMessage invocationMessage = (InvocationMessage) message;
List handlers = this.handlers.get(invocationMessage.getTarget());
if (handlers != null) {
for (InvocationHandler handler : handlers) {
try {
handler.getAction().invoke(invocationMessage.getArguments());
} catch (Exception e) {
logger.error("Invoking client side method '{}' failed:", invocationMessage.getTarget(), e);
}
}
} else {
logger.warn("Failed to find handler for '{}' method.", invocationMessage.getTarget());
}
break;
case CLOSE:
logger.info("Close message received from server.");
CloseMessage closeMessage = (CloseMessage) message;
stop(closeMessage.getError());
break;
case PING:
// We don't need to do anything in the case of a ping message.
break;
case COMPLETION:
CompletionMessage completionMessage = (CompletionMessage)message;
InvocationRequest irq = connectionState.tryRemoveInvocation(completionMessage.getInvocationId());
if (irq == null) {
logger.warn("Dropped unsolicited Completion message for invocation '{}'.", completionMessage.getInvocationId());
continue;
}
irq.complete(completionMessage);
break;
case STREAM_ITEM:
StreamItem streamItem = (StreamItem)message;
InvocationRequest streamInvocationRequest = connectionState.getInvocation(streamItem.getInvocationId());
if (streamInvocationRequest == null) {
logger.warn("Dropped unsolicited Completion message for invocation '{}'.", streamItem.getInvocationId());
continue;
}
streamInvocationRequest.addItem(streamItem);
break;
case STREAM_INVOCATION:
case CANCEL_INVOCATION:
logger.error("This client does not support {} messages.", message.getMessageType());
throw new UnsupportedOperationException(String.format("The message type %s is not supported yet.", message.getMessageType()));
}
}
}
/**
* Stops a connection to the server.
*
* @return A Completable that completes when the connection has been stopped.
*/
public Completable stop() {
return stop(null);
}
private void stopConnection(String errorMessage) {
RuntimeException exception = null;
this.state.lock();
try {
ConnectionState connectionState = this.state.getConnectionStateUnsynchronized(true);
if (connectionState == null)
{
this.logger.error("'stopConnection' called with a null ConnectionState. This is not expected, please file a bug. https://github.com/dotnet/aspnetcore/issues/new?assignees=&labels=&template=bug_report.md");
return;
}
// errorMessage gets passed in from the transport. An already existing stopError value
// should take precedence.
if (connectionState.stopError != null) {
errorMessage = connectionState.stopError;
}
if (errorMessage != null) {
exception = new RuntimeException(errorMessage);
logger.error("HubConnection disconnected with an error {}.", errorMessage);
}
this.state.setConnectionState(null);
connectionState.cancelOutstandingInvocations(exception);
connectionState.close();
logger.info("HubConnection stopped.");
// We can be in the CONNECTING or CONNECTED state here, depending on if the handshake response was received or not.
// connectionState.close() above will exit the Start call with an error if it's still running
this.state.changeState(HubConnectionState.DISCONNECTED);
} finally {
this.state.unlock();
}
// Do not run these callbacks inside the hubConnectionStateLock
if (onClosedCallbackList != null) {
for (OnClosedCallback callback : onClosedCallbackList) {
try {
callback.invoke(exception);
} catch (Exception ex) {
logger.warn("Invoking 'onClosed' method failed:", ex);
}
}
}
}
/**
* Invokes a hub method on the server using the specified method name.
* Does not wait for a response from the receiver.
*
* @param method The name of the server method to invoke.
* @param args The arguments to be passed to the method.
*/
public void send(String method, Object... args) {
this.state.lock();
try {
if (this.state.getHubConnectionState() != HubConnectionState.CONNECTED) {
throw new RuntimeException("The 'send' method cannot be called if the connection is not active.");
}
sendInvocationMessage(method, args);
} finally {
this.state.unlock();
}
}
private void sendInvocationMessage(String method, Object[] args) {
sendInvocationMessage(method, args, null, false);
}
private void sendInvocationMessage(String method, Object[] args, String id, Boolean isStreamInvocation) {
List streamIds = new ArrayList<>();
List streams = new ArrayList<>();
args = checkUploadStream(args, streamIds, streams);
InvocationMessage invocationMessage;
if (isStreamInvocation) {
invocationMessage = new StreamInvocationMessage(null, id, method, args, streamIds);
} else {
invocationMessage = new InvocationMessage(null, id, method, args, streamIds);
}
sendHubMessageWithLock(invocationMessage);
launchStreams(streamIds, streams);
}
void launchStreams(List streamIds, List streams) {
if (streams.isEmpty()) {
return;
}
for (int i = 0; i < streamIds.size(); i++) {
String streamId = streamIds.get(i);
Observable stream = streams.get(i);
stream.subscribe(
(item) -> sendHubMessageWithLock(new StreamItem(null, streamId, item)),
(error) -> {
sendHubMessageWithLock(new CompletionMessage(null, streamId, null, error.toString()));
},
() -> {
sendHubMessageWithLock(new CompletionMessage(null, streamId, null, null));
});
}
}
Object[] checkUploadStream(Object[] args, List streamIds, List streams) {
if (args == null) {
return new Object[] { null };
}
ConnectionState connectionState = this.state.getConnectionState();
List