The streaming plug-in is very similar to the traffic forwarding function of nginx, or reverse proxy.
background
Although the traffic forwarding function of nginx is also very powerful, some changes in the business may lead to various configurations of nginx and make it tired of coping. For example, an app evolves with the development of the business and has many business lines: hotel business line, air ticket business line, catering business line and local travel business line. Behind these lines of business are often different departments and different technical teams, so they will provide different services for app docking. If every new service is added, it needs to be configured in nginx, the configuration of nginx will become heavy and difficult to maintain with the development of the business. Therefore, we can define nginx as a traffic gateway, which has nothing to do with the specific business and is only responsible for the domain name based request forwarding, while the service gateway is responsible for the different services of the back-end business line.
The other is the requirement of logical grouping of services. For example, some important requests are divided into a group of servers, which are high-performance; the requests of other edge services are divided into another group of servers, which are relatively low in configuration. For example, our gray environment needs some basic rules to allocate traffic.
programme
Capture the traffic of Webflux, then forward the request to the corresponding service in the background, and finally return the result of the response to the client.
realization
Capture traffic
Implement a custom webhandler,
@Override
public Mono handle(final ServerWebExchange exchange) {
return new DefaultDiabloPluginChain(plugins).execute(exchange);
}
Implement a plug-in responsibility chain, call different plug-ins, and finally return the response results.
private static class DefaultDiabloPluginChain implements DiabloPluginChain {
private int index;
private final List plugins;
DefaultDiabloPluginChain(final List plugins) {
this.plugins = plugins;
}
@Override
public Mono execute(final ServerWebExchange exchange) {
if (this.index < plugins.size()) {
DiabloPlugin plugin = plugins.get(this.index++);
try {
return plugin.execute(exchange, this);
} catch (Exception ex) {
log.error("DefaultDiabloPluginChain.execute, traceId: {}, uri: {}, error:{}", exchange.getAttribute(Constants.CLIENT_RESPONSE_TRACE_ID), exchange.getRequest().getURI().getPath(), Throwables.getStackTraceAsString(ex));
throw ex;
}
} else {
return Mono.empty(); // complete
}
}
}
Implementation of streaming plug in
public class DividePlugin extends AbstractDiabloPlugin {
private final UpstreamCacheManager upstreamCacheManager;
private final WebClient webClient;
public DividePlugin(final LocalCacheManager localCacheManager, final UpstreamCacheManager upstreamCacheManager, final WebClient webClient) {
super(localCacheManager);
this.upstreamCacheManager = upstreamCacheManager;
this.webClient = webClient;
}
@Override
protected Mono doExecute(final ServerWebExchange exchange, final DiabloPluginChain chain, final SelectorData selector, final RuleData rule) {
final RequestDTO requestDTO = exchange.getAttribute(Constants.REQUESTDTO);
final String traceId = exchange.getAttribute(Constants.CLIENT_RESPONSE_TRACE_ID);
final DivideRuleHandle ruleHandle = GsonUtils.getInstance().fromJson(rule.getHandle(), DivideRuleHandle.class);
String ruleId = rule.getId();
final List upstreamList = upstreamCacheManager.findUpstreamListByRuleId(ruleId);
if (CollectionUtils.isEmpty(upstreamList)) {
log.warn("DividePlugin.doExecute upstreamList is empty, traceId: {}, uri: {}, ruleName:{}", traceId, exchange.getRequest().getURI().getPath(), rule.getName());
exchange.getResponse().setStatusCode(HttpStatus.SERVICE_UNAVAILABLE);
return chain.execute(exchange);
}
final String ip = Objects.requireNonNull(exchange.getRequest().getRemoteAddress()).getAddress().getHostAddress();
DivideUpstream divideUpstream =
LoadBalanceUtils.selector(upstreamList, ruleHandle.getLoadBalance(), ip);
if (Objects.isNull(divideUpstream)) {
log.warn("DividePlugin.doExecute divideUpstream is empty, traceId: {}, uri: {}, loadBalance:{}, ruleName:{}, upstreamSize: {}", traceId, exchange.getRequest().getURI().getPath(), ruleHandle.getLoadBalance(), rule.getName(), upstreamList.size());
exchange.getResponse().setStatusCode(HttpStatus.SERVICE_UNAVAILABLE);
return chain.execute(exchange);
}
if (exchange.getAttributeOrDefault(Constants.GATEWAY_ALREADY_ROUTED_ATTR, false)) {
log.warn("DividePlugin.doExecute alread routed, traceId: {}, uri: {}, ruleName:{}", traceId, exchange.getRequest().getURI().getPath(), rule.getName());
exchange.getResponse().setStatusCode(HttpStatus.FORBIDDEN);
return chain.execute(exchange);
}
exchange.getAttributes().put(Constants.GATEWAY_ALREADY_ROUTED_ATTR, true);
exchange.getAttributes().put(Constants.GATEWAY_CONTEXT_UPSTREAM_HOST, divideUpstream.getUpstreamHost());
exchange.getAttributes().put(Constants.GATEWAY_CONTEXT_RULE_ID, ruleId);
HttpCommand command = new HttpCommand(exchange, chain,
requestDTO, divideUpstream, webClient, ruleHandle.getTimeout());
return command.doHttpInvoke();
}
public SelectorData filterSelector(final List selectors, final ServerWebExchange exchange) {
return selectors.stream()
.filter(selector -> selector.getEnabled() && filterCustomSelector(selector, exchange))
.findFirst().orElse(null);
}
private Boolean filterCustomSelector(final SelectorData selector, final ServerWebExchange exchange) {
if (selector.getType() == SelectorTypeEnum.CUSTOM_FLOW.getCode()) {
List conditionList = selector.getConditionList();
if (CollectionUtils.isEmpty(conditionList)) {
return false;
}
//The background is initially defined as host and the expression is=
if (MatchModeEnum.AND.getCode() == selector.getMatchMode()) {
ConditionData conditionData = conditionList.get(0);
return Objects.equals(exchange.getRequest().getHeaders().getFirst("Host"), conditionData.getParamValue().trim());
} else {
return conditionList.stream().anyMatch(c -> Objects.equals(exchange.getRequest().getHeaders().getFirst("Host"), c.getParamValue().trim()));
}
}
return true;
}
@Override
public String named() {
return PluginEnum.DIVIDE.getName();
}
@Override
public Boolean skip(final ServerWebExchange exchange) {
final RequestDTO body = exchange.getAttribute(Constants.REQUESTDTO);
return !Objects.equals(Objects.requireNonNull(body).getRpcType(), RpcTypeEnum.HTTP.getName());
}
@Override
public PluginTypeEnum pluginType() {
return PluginTypeEnum.FUNCTION;
}
@Override
public int getOrder() {
return PluginEnum.DIVIDE.getCode();
}
}
WebClient
Forwarding HTTP requests mainly relies on webclient, which provides a responsive interface. The specific operation is encapsulated in the httpcommand tool class, and the core code is as follows:
public Mono doHttpInvoke() {
URI uri = buildRealURL(divideUpstream, exchange);
traceId = exchange.getAttribute(Constants.CLIENT_RESPONSE_TRACE_ID);
if (uri == null) {
log.warn("HttpCommand.doNext real url is null, traceId: {}, uri: {}", traceId, exchange.getRequest().getURI().getPath());
exchange.getResponse().setStatusCode(HttpStatus.SERVICE_UNAVAILABLE);
return chain.execute(exchange).then(Mono.defer(() -> Mono.empty()));
}
//If you have time to add todo later, it will not be cleared
// IssRpcContext.commitParams(IssRpcContextParamKey.TRACE_ID, traceId);
if (requestDTO.getHttpMethod().equals(HttpMethodEnum.GET.getName())) {
return webClient.get().uri(f -> uri)
.headers(httpHeaders -> {
httpHeaders.add(Constants.TRACE_ID, traceId);
httpHeaders.addAll(exchange.getRequest().getHeaders());
})
.exchange()
//The default doonerror exception is passed
.doOnError(e -> log.error("HttpCommand.doHttpInvoke Failed to webClient get execute, traceId: {}, uri: {}, cause:{}", traceId, uri, Throwables.getStackTraceAsString(e)))
.timeout(Duration.ofMillis(timeout))
.flatMap(this::doNext);
} else if (requestDTO.getHttpMethod().equals(HttpMethodEnum.POST.getName())) {
return webClient.post().uri(f -> uri)
.headers(httpHeaders -> {
httpHeaders.add(Constants.TRACE_ID, traceId);
httpHeaders.addAll(exchange.getRequest().getHeaders());
})
.contentType(buildMediaType())
.body(BodyInserters.fromDataBuffers(exchange.getRequest().getBody()))
.exchange()
.doOnError(e -> log.error("HttpCommand.doHttpInvoke Failed to webClient post execute, traceId: {}, uri: {}, cause:{}", traceId, uri, Throwables.getStackTraceAsString(e)))
.timeout(Duration.ofMillis(timeout))
.flatMap(this::doNext);
} else if (requestDTO.getHttpMethod().equals(HttpMethodEnum.OPTIONS.getName())) {
return webClient.options().uri(f -> uri)
.headers(httpHeaders -> {
httpHeaders.add(Constants.TRACE_ID, traceId);
httpHeaders.addAll(exchange.getRequest().getHeaders());
})
.exchange()
.doOnError(e -> log.error("HttpCommand.doHttpInvoke Failed to webClient options execute, traceId: {}, uri: {}, cause:{}", traceId, uri, Throwables.getStackTraceAsString(e)))
.timeout(Duration.ofMillis(timeout))
.flatMap(this::doNext);
} else if (requestDTO.getHttpMethod().equals(HttpMethodEnum.HEAD.getName())) {
return webClient.head().uri(f -> uri)
.headers(httpHeaders -> {
httpHeaders.add(Constants.TRACE_ID, traceId);
httpHeaders.addAll(exchange.getRequest().getHeaders());
})
.exchange()
.doOnError(e -> log.error("HttpCommand.doHttpInvoke Failed to webClient head execute, traceId: {}, uri: {}, cause:{}", traceId, uri, Throwables.getStackTraceAsString(e)))
.timeout(Duration.ofMillis(timeout))
.flatMap(this::doNext);
} else if (requestDTO.getHttpMethod().equals(HttpMethodEnum.PUT.getName())) {
return webClient.put().uri(f -> uri)
.headers(httpHeaders -> {
httpHeaders.add(Constants.TRACE_ID, traceId);
httpHeaders.addAll(exchange.getRequest().getHeaders());
})
.contentType(buildMediaType())
.body(BodyInserters.fromDataBuffers(exchange.getRequest().getBody()))
.exchange()
.doOnError(e -> log.error("HttpCommand.doHttpInvoke Failed to webClient put execute, traceId: {}, uri: {}, cause:{}", traceId, uri, Throwables.getStackTraceAsString(e)))
.timeout(Duration.ofMillis(timeout))
.flatMap(this::doNext);
} else if (requestDTO.getHttpMethod().equals(HttpMethodEnum.DELETE.getName())) {
return webClient.delete().uri(f -> uri)
.headers(httpHeaders -> {
httpHeaders.add(Constants.TRACE_ID, traceId);
httpHeaders.addAll(exchange.getRequest().getHeaders());
})
.exchange()
.doOnError(e -> log.error("HttpCommand.doHttpInvoke Failed to webClient delete execute, traceId: {}, uri: {}, cause:{}", traceId, uri, Throwables.getStackTraceAsString(e)))
.timeout(Duration.ofMillis(timeout))
.flatMap(this::doNext);
} else if (requestDTO.getHttpMethod().equals(HttpMethodEnum.PATCH.getName())) {
return webClient.patch().uri(f -> uri)
.headers(httpHeaders -> {
httpHeaders.add(Constants.TRACE_ID, traceId);
httpHeaders.addAll(exchange.getRequest().getHeaders());
})
.contentType(buildMediaType())
.body(BodyInserters.fromDataBuffers(exchange.getRequest().getBody()))
.exchange()
.doOnError(e -> log.error("HttpCommand.doHttpInvoke Failed to webClient patch execute, traceId: {}, uri: {}, cause:{}", traceId, uri, Throwables.getStackTraceAsString(e)))
.timeout(Duration.ofMillis(timeout))
.flatMap(this::doNext);
}
log.warn("HttpCommand doHttpInvoke Waring no match doHttpInvoke end, traceId: {}, httpMethod: {}, uri: {}", traceId, requestDTO.getHttpMethod(), uri.getPath());
return Mono.empty();
}
Gateway project open source
The above content is based on a small module of service gateway. Please see here for details:Diablo is here