package com.firefly.client.http2; import com.codahale.metrics.*; import com.firefly.codec.http2.encode.UrlEncoded; import com.firefly.codec.http2.model.*; import com.firefly.codec.http2.model.MetaData.Response; import com.firefly.codec.http2.stream.HTTPOutputStream; import com.firefly.utils.StringUtils; import com.firefly.utils.concurrent.Promise; import com.firefly.utils.function.Action1; import com.firefly.utils.function.Action3; import com.firefly.utils.io.BufferUtils; import com.firefly.utils.io.EofException; import com.firefly.utils.io.IO; import com.firefly.utils.json.Json; import com.firefly.utils.lang.AbstractLifeCycle; import com.firefly.utils.lang.pool.AsynchronousPool; import com.firefly.utils.lang.pool.BoundedAsynchronousPool; import com.firefly.utils.lang.pool.PooledObject; import com.firefly.utils.time.Millisecond100Clock; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; import java.net.MalformedURLException; import java.net.URISyntaxException; import java.net.URL; import java.nio.ByteBuffer; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.List; import java.util.Objects; import java.util.concurrent.ConcurrentHashMap; import java.util.function.Supplier; public class SimpleHTTPClient extends AbstractLifeCycle { private static Logger log = LoggerFactory.getLogger("firefly-system"); private final HTTP2Client http2Client; private final ConcurrentHashMap<RequestBuilder, AsynchronousPool<HTTPClientConnection>> poolMap = new ConcurrentHashMap<>(); private final SimpleHTTPClientConfiguration simpleHTTPClientConfiguration; private final Timer responseTimer; private final Meter errorMeter; public SimpleHTTPClient() { this(new SimpleHTTPClientConfiguration()); } public SimpleHTTPClient(SimpleHTTPClientConfiguration http2Configuration) { this.simpleHTTPClientConfiguration = http2Configuration; http2Client = new HTTP2Client(http2Configuration); MetricRegistry metrics = http2Configuration.getTcpConfiguration().getMetrics(); responseTimer = metrics.timer("http2.SimpleHTTPClient.response.time"); errorMeter = metrics.meter("http2.SimpleHTTPClient.error.count"); metrics.register("http2.SimpleHTTPClient.error.ratio.1m", new RatioGauge() { @Override protected Ratio getRatio() { return Ratio.of(errorMeter.getOneMinuteRate(), responseTimer.getOneMinuteRate()); } }); start(); } public class RequestBuilder { String host; int port; MetaData.Request request; List<ByteBuffer> requestBody = new ArrayList<>(); Action1<Response> headerComplete; Action1<ByteBuffer> content; Action1<Response> contentComplete; Action1<Response> messageComplete; Action3<Integer, String, Response> badMessage; Action1<Response> earlyEof; Promise<HTTPOutputStream> promise; Action1<HTTPOutputStream> output; MultiPartContentProvider multiPartProvider; UrlEncoded formUrlEncoded; Promise.Completable<SimpleResponse> future; SimpleResponse simpleResponse; public RequestBuilder cookies(List<Cookie> cookies) { request.getFields().put(HttpHeader.COOKIE, CookieGenerator.generateCookies(cookies)); return this; } public RequestBuilder put(String name, List<String> list) { request.getFields().put(name, list); return this; } public RequestBuilder put(HttpHeader header, String value) { request.getFields().put(header, value); return this; } public RequestBuilder put(String name, String value) { request.getFields().put(name, value); return this; } public RequestBuilder put(HttpField field) { request.getFields().put(field); return this; } public RequestBuilder addAll(HttpFields fields) { request.getFields().addAll(fields); return this; } public RequestBuilder add(HttpField field) { request.getFields().add(field); return this; } public Supplier<HttpFields> getTrailerSupplier() { return request.getTrailerSupplier(); } public RequestBuilder setTrailerSupplier(Supplier<HttpFields> trailers) { request.setTrailerSupplier(trailers); return this; } public RequestBuilder jsonBody(Object obj) { return put(HttpHeader.CONTENT_TYPE, MimeTypes.Type.APPLICATION_JSON.asString()).body(Json.toJson(obj)); } public RequestBuilder body(String content) { return body(content, StandardCharsets.UTF_8); } public RequestBuilder body(String content, Charset charset) { return write(BufferUtils.toBuffer(content, charset)); } public RequestBuilder write(ByteBuffer buffer) { requestBody.add(buffer); return this; } public RequestBuilder output(Action1<HTTPOutputStream> output) { this.output = output; return this; } public RequestBuilder output(Promise<HTTPOutputStream> promise) { this.promise = promise; return this; } MultiPartContentProvider multiPartProvider() { if (multiPartProvider == null) { multiPartProvider = new MultiPartContentProvider(); put(HttpHeader.CONTENT_TYPE, multiPartProvider.getContentType()); } return multiPartProvider; } public RequestBuilder addFieldPart(String name, ContentProvider content, HttpFields fields) { multiPartProvider().addFieldPart(name, content, fields); return this; } public RequestBuilder addFilePart(String name, String fileName, ContentProvider content, HttpFields fields) { multiPartProvider().addFilePart(name, fileName, content, fields); return this; } UrlEncoded formUrlEncoded() { if (formUrlEncoded == null) { formUrlEncoded = new UrlEncoded(); put(HttpHeader.CONTENT_TYPE, "application/x-www-form-urlencoded"); } return formUrlEncoded; } public RequestBuilder addFormParam(String name, String value) { formUrlEncoded().add(name, value); return this; } public RequestBuilder addFormParam(String name, List<String> values) { formUrlEncoded().addValues(name, values); return this; } public RequestBuilder putFormParam(String name, String value) { formUrlEncoded().put(name, value); return this; } public RequestBuilder putFormParam(String name, List<String> values) { formUrlEncoded().putValues(name, values); return this; } public RequestBuilder removeFormParam(String name) { formUrlEncoded().remove(name); return this; } public RequestBuilder headerComplete(Action1<Response> headerComplete) { this.headerComplete = headerComplete; return this; } public RequestBuilder messageComplete(Action1<Response> messageComplete) { this.messageComplete = messageComplete; return this; } public RequestBuilder content(Action1<ByteBuffer> content) { this.content = content; return this; } public RequestBuilder contentComplete(Action1<Response> contentComplete) { this.contentComplete = contentComplete; return this; } public RequestBuilder badMessage(Action3<Integer, String, Response> badMessage) { this.badMessage = badMessage; return this; } public RequestBuilder earlyEof(Action1<Response> earlyEof) { this.earlyEof = earlyEof; return this; } public Promise.Completable<SimpleResponse> submit() { submit(new Promise.Completable<>()); return future; } public void submit(Promise.Completable<SimpleResponse> future) { this.future = future; send(this); } public void submit(Action1<SimpleResponse> action) { Promise.Completable<SimpleResponse> future = new Promise.Completable<SimpleResponse>() { public void succeeded(SimpleResponse t) { super.succeeded(t); action.call(t); } public void failed(Throwable c) { super.failed(c); log.error("http request exception", c); } }; submit(future); } public void end() { send(this); } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; RequestBuilder that = (RequestBuilder) o; return port == that.port && Objects.equals(host, that.host); } @Override public int hashCode() { return Objects.hash(host, port); } } public void removeConnectionPool(String url) { try { removeConnectionPool(new URL(url)); } catch (MalformedURLException e) { log.error("url exception", e); throw new IllegalArgumentException(e); } } public void removeConnectionPool(URL url) { RequestBuilder req = new RequestBuilder(); req.host = url.getHost(); req.port = url.getPort() < 0 ? url.getDefaultPort() : url.getPort(); removePool(req); } public void removeConnectionPool(String host, int port) { RequestBuilder req = new RequestBuilder(); req.host = host; req.port = port; removePool(req); } private void removePool(RequestBuilder req) { AsynchronousPool<HTTPClientConnection> pool = poolMap.remove(req); pool.stop(); } public int getConnectionPoolSize(String host, int port) { RequestBuilder req = new RequestBuilder(); req.host = host; req.port = port; return _getPoolSize(req); } public int getConnectionPoolSize(String url) { try { return getConnectionPoolSize(new URL(url)); } catch (MalformedURLException e) { log.error("url exception", e); throw new IllegalArgumentException(e); } } public int getConnectionPoolSize(URL url) { RequestBuilder req = new RequestBuilder(); req.host = url.getHost(); req.port = url.getPort() < 0 ? url.getDefaultPort() : url.getPort(); return _getPoolSize(req); } private int _getPoolSize(RequestBuilder req) { AsynchronousPool<HTTPClientConnection> pool = poolMap.get(req); if (pool != null) { return pool.size(); } else { return 0; } } public RequestBuilder get(String url) { return request(HttpMethod.GET.asString(), url); } public RequestBuilder post(String url) { return request(HttpMethod.POST.asString(), url); } public RequestBuilder head(String url) { return request(HttpMethod.HEAD.asString(), url); } public RequestBuilder put(String url) { return request(HttpMethod.PUT.asString(), url); } public RequestBuilder delete(String url) { return request(HttpMethod.DELETE.asString(), url); } public RequestBuilder request(HttpMethod method, String url) { return request(method.asString(), url); } public RequestBuilder request(String method, String url) { try { return request(method, new URL(url)); } catch (MalformedURLException e) { log.error("url exception", e); throw new IllegalArgumentException(e); } } public RequestBuilder request(String method, URL url) { try { RequestBuilder req = new RequestBuilder(); req.host = url.getHost(); req.port = url.getPort() < 0 ? url.getDefaultPort() : url.getPort(); req.request = new MetaData.Request(method, new HttpURI(url.toURI()), HttpVersion.HTTP_1_1, new HttpFields()); return req; } catch (URISyntaxException e) { log.error("url exception", e); throw new IllegalArgumentException(e); } } protected void send(RequestBuilder r) { Timer.Context resTimerCtx = responseTimer.time(); AsynchronousPool<HTTPClientConnection> pool = getPool(r); pool.take().thenAccept(o -> { HTTPClientConnection connection = o.getObject(); connection.close(conn -> pool.release(o)) .exception((conn, exception) -> pool.release(o)); if (connection.getHttpVersion() == HttpVersion.HTTP_2) { pool.release(o); } log.debug("take the connection {} from pool, released: {}", connection.getSessionId(), o.isReleased()); ClientHTTPHandler handler = new ClientHTTPHandler.Adapter() .headerComplete((req, resp, outputStream, conn) -> { if (r.headerComplete != null) { r.headerComplete.call(resp); } if (r.future != null) { if (r.simpleResponse == null) { r.simpleResponse = new SimpleResponse(resp); } } return false; }).content((buffer, req, resp, outputStream, conn) -> { if (r.content != null) { r.content.call(buffer); } if (r.future != null && r.simpleResponse != null) { r.simpleResponse.responseBody.add(buffer); } return false; }).contentComplete((req, resp, outputStream, conn) -> { if (r.contentComplete != null) { r.contentComplete.call(resp); } return false; }).messageComplete((req, resp, outputStream, conn) -> { pool.release(o); log.debug("complete request of the connection {} , released: {}", connection.getSessionId(), o.isReleased()); if (r.messageComplete != null) { r.messageComplete.call(resp); } if (r.future != null) { r.future.succeeded(r.simpleResponse); } resTimerCtx.stop(); return true; }).badMessage((errCode, reason, req, resp, outputStream, conn) -> { pool.release(o); log.debug("bad message of the connection {} , released: {}", connection.getSessionId(), o.isReleased()); if (r.badMessage != null) { r.badMessage.call(errCode, reason, resp); } if (r.future != null) { if (r.simpleResponse == null) { r.simpleResponse = new SimpleResponse(resp); } r.future.failed(new BadMessageException(errCode, reason)); } if (r.badMessage == null && r.future == null) { IO.close(o.getObject()); } errorMeter.mark(); resTimerCtx.stop(); }).earlyEOF((req, resp, outputStream, conn) -> { pool.release(o); log.debug("eafly EOF of the connection {} , released: {}", connection.getSessionId(), o.isReleased()); if (r.earlyEof != null) { r.earlyEof.call(resp); } if (r.future != null) { if (r.simpleResponse == null) { r.simpleResponse = new SimpleResponse(resp); } r.future.failed(new EofException("early eof")); } if (r.earlyEof == null && r.future == null) { IO.close(o.getObject()); } errorMeter.mark(); resTimerCtx.stop(); }); if (r.requestBody != null && !r.requestBody.isEmpty()) { connection.send(r.request, r.requestBody.toArray(BufferUtils.EMPTY_BYTE_BUFFER_ARRAY), handler); } else if (r.promise != null) { connection.send(r.request, r.promise, handler); } else if (r.output != null) { Promise<HTTPOutputStream> p = new Promise<HTTPOutputStream>() { public void succeeded(HTTPOutputStream out) { r.output.call(out); } }; connection.send(r.request, p, handler); } else if (r.multiPartProvider != null) { IO.close(r.multiPartProvider); r.multiPartProvider.setListener(() -> log.debug("multi part content listener")); if (r.multiPartProvider.getLength() > 0) { r.put(HttpHeader.CONTENT_LENGTH, String.valueOf(r.multiPartProvider.getLength())); } Promise.Completable<HTTPOutputStream> p = new Promise.Completable<>(); connection.send(r.request, p, handler); p.thenAccept(output -> { try (HTTPOutputStream out = output) { for (ByteBuffer buf : r.multiPartProvider) { out.write(buf); } } catch (IOException e) { log.error("SimpleHTTPClient writes data exception", e); } }).exceptionally(t -> { log.error("SimpleHTTPClient gets output stream exception", t); resTimerCtx.stop(); errorMeter.mark(); return null; }); } else if (r.formUrlEncoded != null) { String body = r.formUrlEncoded.encode(Charset.forName(simpleHTTPClientConfiguration.getCharacterEncoding()), true); byte[] content = StringUtils.getBytes(body, simpleHTTPClientConfiguration.getCharacterEncoding()); connection.send(r.request, ByteBuffer.wrap(content), handler); } else { connection.send(r.request, handler); } }).exceptionally(e -> { log.error("SimpleHTTPClient sends message exception", e); resTimerCtx.stop(); errorMeter.mark(); return null; }); } private AsynchronousPool<HTTPClientConnection> getPool(RequestBuilder request) { return poolMap.computeIfAbsent(request, req -> new BoundedAsynchronousPool<>(simpleHTTPClientConfiguration.getPoolSize(), simpleHTTPClientConfiguration.getConnectTimeout(), () -> { Promise.Completable<PooledObject<HTTPClientConnection>> r = new Promise.Completable<>(); Promise.Completable<HTTPClientConnection> c = http2Client.connect(request.host, request.port); c.thenAccept(conn -> r.succeeded(new PooledObject<>(conn))) .exceptionally(e -> { r.failed(e); return null; }); return r; }, o -> o.getObject().isOpen(), (o) -> { try { o.getObject().close(); } catch (IOException e) { log.error("close http connection exception", e); } })); } @Override protected void init() { } @Override protected void destroy() { http2Client.stop(); poolMap.forEach((k, v) -> v.stop()); } }