com.king.platform.net.http.netty.NettyHttpClient Maven / Gradle / Ivy
// Copyright (C) king.com Ltd 2015
// https://github.com/king/king-http-client
// Author: Magnus Gustafsson
// License: Apache 2.0, https://raw.github.com/king/king-http-client/LICENSE-APACHE
package com.king.platform.net.http.netty;
import com.king.platform.net.http.*;
import com.king.platform.net.http.netty.backpressure.BackPressure;
import com.king.platform.net.http.netty.eventbus.*;
import com.king.platform.net.http.netty.metric.TimeStampRecorder;
import com.king.platform.net.http.netty.pool.ChannelPool;
import com.king.platform.net.http.netty.request.HttpClientRequestHandler;
import com.king.platform.net.http.netty.request.NettyHttpClientRequest;
import com.king.platform.net.http.netty.requestbuilder.HttpClientRequestBuilderImpl;
import com.king.platform.net.http.netty.requestbuilder.HttpClientRequestWithBodyBuilderImpl;
import com.king.platform.net.http.netty.requestbuilder.HttpClientSseRequestBuilderImpl;
import com.king.platform.net.http.netty.response.HttpClientResponseHandler;
import com.king.platform.net.http.netty.response.HttpRedirector;
import com.king.platform.net.http.netty.util.TimeProvider;
import io.netty.buffer.ByteBuf;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.epoll.Epoll;
import io.netty.channel.epoll.EpollEventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.handler.codec.http.HttpHeaders;
import io.netty.handler.codec.http.HttpMethod;
import io.netty.handler.codec.http.HttpResponseStatus;
import io.netty.handler.codec.http.HttpVersion;
import io.netty.util.Timer;
import org.slf4j.Logger;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Executor;
import java.util.concurrent.Future;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import static org.slf4j.LoggerFactory.getLogger;
public class NettyHttpClient implements HttpClient {
private final AtomicBoolean started = new AtomicBoolean();
private final ConfMap confMap = new ConfMap();
private final Executor httpClientCallbackExecutor;
private final Executor httpClientExecuteExecutor;
private final Timer cleanupTimer;
private final TimeProvider timeProvider;
private final Logger logger = getLogger(getClass());
private final int nioThreads;
private final ThreadFactory nioThreadFactory;
private final RootEventBus rootEventBus;
private final ChannelPool channelPool;
private EventLoopGroup group;
private ChannelManager channelManager;
private BackPressure executionBackPressure;
private Boolean executeOnCallingThread;
private List shutdownJobs = new ArrayList<>();
public NettyHttpClient(int nioThreads, ThreadFactory nioThreadFactory, Executor httpClientCallbackExecutor, Executor httpClientExecuteExecutor, Timer
cleanupTimer, TimeProvider timeProvider, final BackPressure executionBackPressure, RootEventBus rootEventBus, ChannelPool channelPool) {
this.httpClientCallbackExecutor = httpClientCallbackExecutor;
this.httpClientExecuteExecutor = httpClientExecuteExecutor;
this.cleanupTimer = cleanupTimer;
this.timeProvider = timeProvider;
this.nioThreads = nioThreads;
this.nioThreadFactory = nioThreadFactory;
this.executionBackPressure = executionBackPressure;
this.rootEventBus = rootEventBus;
this.channelPool = channelPool;
rootEventBus.subscribePermanently(Event.COMPLETED, new EventBusCallback1() {
@Override
public void onEvent(Event1 event, HttpRequestContext httpRequestContext) {
executionBackPressure.releaseSlot(httpRequestContext.getServerInfo());
}
});
rootEventBus.subscribePermanently(Event.ERROR, new EventBusCallback2() {
@Override
public void onEvent(Event2 event, HttpRequestContext httpRequestContext, Throwable throwable) {
executionBackPressure.releaseSlot(httpRequestContext.getServerInfo());
}
});
}
@Override
public void start() {
if (started.get()) {
throw new IllegalStateException("Http client already started!");
}
started.set(true);
executeOnCallingThread = confMap.get(ConfKeys.EXECUTE_ON_CALLING_THREAD);
if (Epoll.isAvailable() && confMap.get(ConfKeys.USE_EPOLL)) {
group = new EpollEventLoopGroup(nioThreads, nioThreadFactory);
} else {
group = new NioEventLoopGroup(nioThreads, nioThreadFactory);
}
HttpClientResponseHandler responseHandler = new HttpClientResponseHandler(new HttpRedirector());
HttpClientRequestHandler requestHandler = new HttpClientRequestHandler();
HttpClientHandler clientHandler = new HttpClientHandler(responseHandler, requestHandler);
channelManager = new ChannelManager(group, clientHandler, cleanupTimer, timeProvider, channelPool, confMap, rootEventBus);
}
@Override
public void shutdown() {
started.set(false);
if (group != null) {
group.shutdownGracefully(0, 10, TimeUnit.SECONDS);
}
for (ShutdownJob shutdownJob : shutdownJobs) {
shutdownJob.onShutdown();
}
}
@Override
public void setConf(ConfKeys key, T value) {
if (started.get()) {
throw new RuntimeException("Can't set global config keys after the client has been started");
}
confMap.set(key, value);
}
public Future> execute(HttpMethod httpMethod, final NettyHttpClientRequest nettyHttpClientRequest, HttpCallback httpCallback, final NioCallback nioCallback, ResponseBodyConsumer responseBodyConsumer, int idleTimeoutMillis, int totalRequestTimeoutMillis, boolean followRedirects, boolean keepAlive, ExternalEventTrigger externalEventTrigger) {
validateStarted();
httpCallback = runOnlyOnceWrapper(httpCallback);
final RequestEventBus requestRequestEventBus = rootEventBus.createRequestEventBus();
if (externalEventTrigger != null) {
externalEventTrigger.registerEventListener(new EventListener() {
@Override
public void onEvent(Event1 event, T payload) {
requestRequestEventBus.triggerEvent(event, payload);
}
@Override
public void onEvent(Event2 event, T1 payload1, T2 payload2) {
requestRequestEventBus.triggerEvent(event, payload1, payload2);
}
});
}
subscribeToHttpCallbackEvents(httpCallback, requestRequestEventBus);
subscribeToNioCallbackEvents(nioCallback, requestRequestEventBus);
if (responseBodyConsumer == null) {
responseBodyConsumer = (ResponseBodyConsumer) EMPTY_RESPONSE_BODY_CONSUMER;
}
final HttpRequestContext httpRequestContext = new HttpRequestContext<>(httpMethod, nettyHttpClientRequest, requestRequestEventBus, responseBodyConsumer,
idleTimeoutMillis, totalRequestTimeoutMillis, followRedirects, keepAlive, new TimeStampRecorder(timeProvider));
ResponseFuture future = new ResponseFuture<>(requestRequestEventBus, httpRequestContext);
if (!executionBackPressure.acquireSlot(nettyHttpClientRequest.getServerInfo())) {
requestRequestEventBus.triggerEvent(Event.ERROR, httpRequestContext, new KingHttpException("Too many concurrent connections"));
return future;
}
logger.trace("Executing httpRequest {}", httpRequestContext);
if (executeOnCallingThread) {
sendRequest(requestRequestEventBus, httpRequestContext);
} else {
httpClientExecuteExecutor.execute(new Runnable() {
@Override
public void run() {
sendRequest(requestRequestEventBus, httpRequestContext);
}
});
}
return future;
}
private HttpCallback runOnlyOnceWrapper(final HttpCallback httpCallback) {
if (httpCallback == null) {
return null;
}
return new HttpCallback() {
private final AtomicBoolean firstExecute = new AtomicBoolean();
@Override
public void onCompleted(HttpResponse httpResponse) {
if (firstExecute.compareAndSet(false, true)) {
httpCallback.onCompleted(httpResponse);
}
}
@Override
public void onError(Throwable throwable) {
if (firstExecute.compareAndSet(false, true)) {
httpCallback.onError(throwable);
}
}
};
}
private void sendRequest(RequestEventBus requestRequestEventBus, HttpRequestContext httpRequestContext) {
try {
channelManager.sendOnChannel(httpRequestContext, requestRequestEventBus);
} catch (Throwable throwable) {
requestRequestEventBus.triggerEvent(Event.ERROR, httpRequestContext, throwable);
}
}
public Executor getHttpClientCallbackExecutor() {
return httpClientCallbackExecutor;
}
@Override
public HttpClientRequestBuilder createGet(String uri) {
return new HttpClientRequestBuilderImpl(this, HttpVersion.HTTP_1_1, HttpMethod.GET, uri, confMap);
}
@Override
public HttpClientRequestWithBodyBuilder createPost(String uri) {
return new HttpClientRequestWithBodyBuilderImpl(this, HttpVersion.HTTP_1_1, HttpMethod.POST, uri, confMap);
}
@Override
public HttpClientRequestWithBodyBuilder createPut(String uri) {
return new HttpClientRequestWithBodyBuilderImpl(this, HttpVersion.HTTP_1_1, HttpMethod.PUT, uri, confMap);
}
@Override
public HttpClientRequestBuilder createDelete(String uri) {
return new HttpClientRequestBuilderImpl(this, HttpVersion.HTTP_1_1, HttpMethod.DELETE, uri, confMap);
}
@Override
public HttpClientRequestBuilder createHead(String uri) {
return new HttpClientRequestBuilderImpl(this, HttpVersion.HTTP_1_1, HttpMethod.HEAD, uri, confMap);
}
@Override
public HttpClientSseRequestBuilder createSSE(String uri) {
return new HttpClientSseRequestBuilderImpl(this, HttpVersion.HTTP_1_1, HttpMethod.GET, uri, confMap);
}
private void validateStarted() {
if (!started.get()) {
throw new IllegalStateException("Http client is not started!");
}
}
private void subscribeToHttpCallbackEvents(final HttpCallback httpCallback, RequestEventBus requestRequestEventBus) {
if (httpCallback == null) {
return;
}
requestRequestEventBus.subscribePermanently(Event.onHttpResponseDone, new RunOnceCallback1() {
@Override
public void onFirstEvent(Event1 event, final HttpResponse httpResponse) {
httpClientCallbackExecutor.execute(new Runnable() {
@Override
public void run() {
httpCallback.onCompleted(httpResponse);
}
});
}
});
requestRequestEventBus.subscribePermanently(Event.ERROR, new RunOnceCallback2() {
@Override
public void onFirstEvent(Event2 event, HttpRequestContext httpRequestContext, final Throwable throwable) {
httpClientCallbackExecutor.execute(new Runnable() {
@Override
public void run() {
httpCallback.onError(throwable);
}
});
}
});
}
public Future> dispatchError(final HttpCallback httpCallback, final Throwable throwable) {
if (httpCallback != null) {
httpClientCallbackExecutor.execute(new Runnable() {
@Override
public void run() {
httpCallback.onError(throwable);
}
});
}
return ResponseFuture.error(throwable);
}
public void addShutdownJob(ShutdownJob shutdownJob) {
shutdownJobs.add(shutdownJob);
}
private void subscribeToNioCallbackEvents(final NioCallback nioCallback, RequestEventBus requestRequestEventBus) {
if (nioCallback == null) {
return;
}
requestRequestEventBus.subscribePermanently(Event.onConnecting, new EventBusCallback1() {
@Override
public void onEvent(Event1 event, Void payload) {
nioCallback.onConnecting();
}
});
requestRequestEventBus.subscribePermanently(Event.onConnected, new EventBusCallback1() {
@Override
public void onEvent(Event1 event, Void payload) {
nioCallback.onConnected();
}
});
requestRequestEventBus.subscribePermanently(Event.onWroteHeaders, new EventBusCallback1() {
@Override
public void onEvent(Event1 event, Void payload) {
nioCallback.onWroteHeaders();
}
});
requestRequestEventBus.subscribePermanently(Event.onWroteContentProgressed, new EventBusCallback2() {
@Override
public void onEvent(Event2 event, Long progress, Long total) {
nioCallback.onWroteContentProgressed(progress, total);
}
});
requestRequestEventBus.subscribePermanently(Event.onWroteContentCompleted, new EventBusCallback1() {
@Override
public void onEvent(Event1 event, Void payload) {
nioCallback.onWroteContentCompleted();
}
});
requestRequestEventBus.subscribePermanently(Event.onReceivedStatus, new EventBusCallback1() {
@Override
public void onEvent(Event1 event, HttpResponseStatus payload) {
nioCallback.onReceivedStatus(payload);
}
});
requestRequestEventBus.subscribePermanently(Event.onReceivedHeaders, new EventBusCallback1() {
@Override
public void onEvent(Event1 event, HttpHeaders payload) {
nioCallback.onReceivedHeaders(payload);
}
});
requestRequestEventBus.subscribePermanently(Event.onReceivedContentPart, new EventBusCallback2() {
@Override
public void onEvent(Event2 event, Integer length, ByteBuf contentPart) {
nioCallback.onReceivedContentPart(length, contentPart);
}
});
requestRequestEventBus.subscribePermanently(Event.onReceivedCompleted, new EventBusCallback2() {
@Override
public void onEvent(Event2 event, HttpResponseStatus httpResponseStatus, HttpHeaders httpHeaders) {
nioCallback.onReceivedCompleted(httpResponseStatus, httpHeaders);
}
});
requestRequestEventBus.subscribePermanently(Event.ERROR, new EventBusCallback2() {
@Override
public void onEvent(Event2 event, HttpRequestContext httpRequestContext, Throwable throwable) {
nioCallback.onError(throwable);
}
});
}
private static final ResponseBodyConsumer EMPTY_RESPONSE_BODY_CONSUMER = new ResponseBodyConsumer() {
@Override
public void onBodyStart(String contentType, String charset, long contentLength) throws Exception {
}
@Override
public void onReceivedContentPart(ByteBuffer buffer) throws Exception {
}
@Override
public void onCompletedBody() throws Exception {
}
@Override
public Void getBody() {
return null;
}
};
interface ShutdownJob {
void onShutdown();
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy