当前位置:   article > 正文

Spring Cloud zuul与CloseableHttpClient连接池,TLS证书认证_springcloud使用zuul转发请求时携带ssl证书

springcloud使用zuul转发请求时携带ssl证书

前言

最近做项目,需要一个代理逻辑,实际上这种代理NGINX最好,但是有些额外功能的开发,NGINX就需要额外能力支持,比如lua脚本,常见的做法有kong,apisix等,据说apisix的性能较强,界面较好,不过如果需要Java开发(方便二次开发),那么zuul也是可以的,实际上gateway相对主流,但是实现逻辑相对复杂,而且跟zuul(配置连接池和线程)性能差不多,只不过zuul不再被Spring Cloud支持,需要自己维护,但是servlet貌似也没啥维护的了。

zuul

zuul的设计之初是为了微服务网关,但是如果做TCP、websocket等转发就需要自己实现,实际上开源的goproxy就是一个性能较强的代理,go-gateway等,但是开发语言最终选择zuul,因为定制性极强。

zuul改造

zuul默认需要注册注册中心,需要把这一部分剥离出来,做成插件,需要的时候才会注册,拿到zuul starter源码,发现

默认加载这2个配置类,因为zuul被Spring Cloud废弃,所以没有Spring Boot新版本的引入配置类的方式import模式

Cloud模式,支持Cloud的负载均衡,熔断等

域名或者Host模式,可以使用域名,或者APP端负载均衡,限流等

zuul源码分析

zuul的注入依赖EnableZuulProxy还是EnableZuulServer,EnableZuulProxy的能力更强,原因如下

  1. @Target(ElementType.TYPE)
  2. @Retention(RetentionPolicy.RUNTIME)
  3. @Documented
  4. @Import(ZuulServerMarkerConfiguration.class)
  5. public @interface EnableZuulServer {
  6. }
  7. @EnableCircuitBreaker
  8. @Target(ElementType.TYPE)
  9. @Retention(RetentionPolicy.RUNTIME)
  10. @Import(ZuulProxyMarkerConfiguration.class)
  11. public @interface EnableZuulProxy {
  12. }

Import的marker决定的,注意proxy模式有EnableCircuitBreaker注解,这个是过时注解,而且依赖hystrix等熔断器,这个需要去掉,限流熔断自己实现吧,或者依赖Cloud的自定义实现

Server的能力,依赖marker类的bean创建,来源于上面的Enable注解,所以注解开启不同功能,

@ConditionalOnBean(ZuulServerMarkerConfiguration.Marker.class)

这个之上的注解是会执行的,毕竟需要开启ZuulProperties的注入

Proxy的能力更强, 因为继承(简单粗暴)

剥离注册中心相关的操作

 逻辑很简单,实际只需要把robbin的filter和相关的支持类剥离即可

剥离后注册中心相关的可以单独加载,以http转https转发为例,流程分析

HTTPS转发逻辑

  1. @SpringBootApplication
  2. //@EnableDiscoveryClient
  3. @EnableZuulProxy
  4. public class ZuulMain {
  5. public static void main(String[] args) {
  6. SpringApplication.run(ZuulMain.class, args);
  7. }
  8. }

因为剥离注册中心,就不需要服务发现了,但是只能转发Host、IP或者域名

配置转发博客为例:

  1. zuul:
  2. routes:
  3. rule1:
  4. path: /demo/**
  5. url: https://blog.csdn.net/

笔者很早讲了SCI模式,不通过配置文件注入servlet和filter,那么zuul也是这种方式注入的

安装加载顺序一般情况下注入servlet,而不是filter,那么在http请求时

经过类型,然后执行zuulfilter

以http为例,笔者访问http://localhost:8766/demo/hello ,返回了csdn的地址

关键逻辑1:路径匹配

org.springframework.cloud.netflix.zuul.filters.pre.PreDecorationFilter

预处理会设置后面需要的url转发

threadlocal线程安全,在post的filter执行后会被uset

然后再取出使用转发

 

逻辑简单就是一条链传递,filter各个阶段的转发,中间做逻辑处理,收到Http请求,在通过“httpclient”发送出去。

连接池研究

笔者使用的是Apache的httpclient 4.5,实际上现在最新版本是5.2.x,可能略有不同,研究

CloseableHttpClient

的用法,连接池的用法,连接是怎么关闭的,看看Spring怎么做的

  1. @PostConstruct
  2. private void initialize() {
  3. if (!customHttpClient) {
  4. this.connectionManager = newConnectionManager();
  5. this.httpClient = newClient();
  6. this.connectionManagerTimer.schedule(new TimerTask() {
  7. @Override
  8. public void run() {
  9. if (SimpleHostRoutingFilter.this.connectionManager == null) {
  10. return;
  11. }
  12. SimpleHostRoutingFilter.this.connectionManager
  13. .closeExpiredConnections();
  14. }
  15. }, 30000, 5000);
  16. }
  17. }

3步:

1. 创建连接池

2. 创建httpclient(可关闭)

3. 定时关闭过期连接(实际是应该pool自己定时清除)

但是没看到在优雅停机时关闭连接池的代码,只有关闭定时器的代码

  1. @PreDestroy
  2. public void stop() {
  3. this.connectionManagerTimer.cancel();
  4. }

或者可以在这个里面加入关闭连接池的代码,但是流量能不能做到无损就需要外部支持了,不让外部进,内部流量消耗完

创建连接池 

  1. protected HttpClientConnectionManager newConnectionManager() {
  2. return connectionManagerFactory.newConnectionManager(
  3. !this.sslHostnameValidationEnabled,
  4. this.hostProperties.getMaxTotalConnections(),
  5. this.hostProperties.getMaxPerRouteConnections(),
  6. this.hostProperties.getTimeToLive(), this.hostProperties.getTimeUnit(),
  7. null);
  8. }

过度代码,封装参数,zuul的调参数就可以调这里参数,注意

!this.sslHostnameValidationEnabled

坑啊,sslenable取反,表示ssl不验证,Spring Cloud commons封装创建流程

  1. public HttpClientConnectionManager newConnectionManager(boolean disableSslValidation,
  2. int maxTotalConnections, int maxConnectionsPerRoute, long timeToLive,
  3. TimeUnit timeUnit, RegistryBuilder registryBuilder) {
  4. if (registryBuilder == null) {
  5. //支持HTTP,注册的是map,可以注册多种协议
  6. registryBuilder = RegistryBuilder.<ConnectionSocketFactory>create()
  7. .register(HTTP_SCHEME, PlainConnectionSocketFactory.INSTANCE);
  8. }
  9. if (disableSslValidation) {//刚刚的标记,不验证ssl
  10. try {
  11. final SSLContext sslContext = SSLContext.getInstance("SSL");
  12. sslContext.init(null,
  13. new TrustManager[] { new DisabledValidationTrustManager() },
  14. new SecureRandom());//不验证信任
  15. registryBuilder.register(HTTPS_SCHEME, new SSLConnectionSocketFactory(
  16. sslContext, NoopHostnameVerifier.INSTANCE));
  17. }
  18. catch (NoSuchAlgorithmException e) {
  19. LOG.warn("Error creating SSLContext", e);
  20. }
  21. catch (KeyManagementException e) {
  22. LOG.warn("Error creating SSLContext", e);
  23. }
  24. }
  25. else {
  26. //验证信任,默认是验证的
  27. registryBuilder.register("https",
  28. SSLConnectionSocketFactory.getSocketFactory());
  29. }
  30. final Registry<ConnectionSocketFactory> registry = registryBuilder.build();
  31. //连接池,相对的就是basic模式,单链接
  32. PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager(
  33. registry, null, null, null, timeToLive, timeUnit);
  34. connectionManager.setMaxTotal(maxTotalConnections);//最大连接数
  35. connectionManager.setDefaultMaxPerRoute(maxConnectionsPerRoute);//每个路由最大连接
  36. return connectionManager;
  37. }

先看看怎么验证的

  1. public static SSLConnectionSocketFactory getSocketFactory() throws SSLInitializationException {
  2. return new SSLConnectionSocketFactory(SSLContexts.createDefault(), getDefaultHostnameVerifier());
  3. }

 读取火狐的认证的后缀:Public Suffix List - MozillaWiki

 

验证逻辑,host和x509,如果是自定义证书,比如我们自己做的jdk或者openssl,可以自定义验证,或者不验证

 

再看看创建PoolingHttpClientConnectionManager的过程,创建了CPool,继承自AbstractConnPool,有创建和回收方法,池子就可以循环

  1. public PoolingHttpClientConnectionManager(
  2. final HttpClientConnectionOperator httpClientConnectionOperator,
  3. final HttpConnectionFactory<HttpRoute, ManagedHttpClientConnection> connFactory,
  4. final long timeToLive, final TimeUnit timeUnit) {
  5. super();
  6. this.configData = new ConfigData();
  7. //默认值每个路由最大2个连接,最大20个连接
  8. this.pool = new CPool(new InternalConnectionFactory(
  9. this.configData, connFactory), 2, 20, timeToLive, timeUnit);
  10. this.pool.setValidateAfterInactivity(2000);
  11. this.connectionOperator = Args.notNull(httpClientConnectionOperator, "HttpClientConnectionOperator");
  12. this.isShutDown = new AtomicBoolean(false);
  13. }

居然没用Apache的commons-pools,自己实现了,造轮子

httpclient的创建 

  1. protected CloseableHttpClient newClient() {
  2. final RequestConfig requestConfig = RequestConfig.custom()
  3. .setConnectionRequestTimeout(
  4. this.hostProperties.getConnectionRequestTimeoutMillis())
  5. .setSocketTimeout(this.hostProperties.getSocketTimeoutMillis())
  6. .setConnectTimeout(this.hostProperties.getConnectTimeoutMillis())
  7. .setCookieSpec(CookieSpecs.IGNORE_COOKIES).build();
  8. return httpClientFactory.createBuilder().setDefaultRequestConfig(requestConfig)
  9. .setConnectionManager(this.connectionManager).disableRedirectHandling()
  10. .build();
  11. }

核心是通过刚刚创建的连接管理对象创建Client,执行client的时候可以创建连接和回收复用,里面封装的很复杂,考虑

 

涉及权限和user agent,尤其是user agent,这个在很多地方有限制,比如浏览器 

获取连接,发送请求

发送请求需要封装method,jdk8自带的urlconnection不能支持patch:[JDK-8207840] HTTPUrlConnection does not accept PATCH method - Java Bug System,所以jdk8只能使用httpclient或者okhttp,httpclient使用serversocket自己实现的

根据实际verb写入method

那么在哪里去池子获取连接的呢,httpclient是自己封装的,装载获取连接超时

关闭连接 

异常关闭

那么正常情况下呢,response的body流关闭时

参考API,实际上就是读取流结束,关闭response的输入流

还包括经常用的toString的API

toString

那么zuul呢,注释写的很明白,释放连接

线程栈,zuul是读取结束就直接关闭连接了,实际上是EofSensorWatcher在生效,httpclient的ResponseEntityProxy实现了EofSensorWatcher接口

  1. "http-nio-8766-exec-4@8603" daemon prio=5 tid=0x1c nid=NA runnable
  2. java.lang.Thread.State: RUNNABLE
  3. at org.apache.http.impl.execchain.ResponseEntityProxy.releaseConnection(ResponseEntityProxy.java:76)
  4. at org.apache.http.impl.execchain.ResponseEntityProxy.eofDetected(ResponseEntityProxy.java:121)
  5. at org.apache.http.conn.EofSensorInputStream.checkEOF(EofSensorInputStream.java:199)
  6. at org.apache.http.conn.EofSensorInputStream.read(EofSensorInputStream.java:136)
  7. at org.apache.http.conn.EofSensorInputStream.read(EofSensorInputStream.java:148)
  8. at org.springframework.cloud.netflix.zuul.filters.post.SendResponseFilter.writeResponse(SendResponseFilter.java:259)
  9. at org.springframework.cloud.netflix.zuul.filters.post.SendResponseFilter.writeResponse(SendResponseFilter.java:162)
  10. at org.springframework.cloud.netflix.zuul.filters.post.SendResponseFilter.run(SendResponseFilter.java:112)
  11. at com.netflix.zuul.ZuulFilter.runFilter(ZuulFilter.java:117)

流在读取结束时也会调起钩子,实际上流关闭也是同理 

 

这个设计真不错,各个环节解决内存泄漏问题,有C++的味道

流关闭时

总结

实际上,对于技术而言,无论使用任何框架,设计思路都是有异曲同工的地方,对于HTTP代理,无论是zuul(servlet)还是gateway(netty),或者NGINX;本质处理逻辑还是IO的区别,HTTPS协议对于所有的逻辑都是一样的,关键在于定制化吧,zuul对于简单应用还是很不错的,方便定制化,也可以使用gateway,相对要复杂一点。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/从前慢现在也慢/article/detail/907821
推荐阅读
相关标签
  

闽ICP备14008679号