springboot+zuul(一)------实现自定义过滤器、动态路由、动态负载。

参考:https://blog.csdn.net/u014091123/article/details/75433656 
https://blog.csdn.net/u013815546/article/details/68944039

Zuul是Netflix开源的微服务网关,他的核心是一系列的过滤器,通过这些过滤器我们可以轻松的实现服务的访问认证、限流、路由、负载、熔断等功能。

基于对已有项目代码零侵入的需求,本文没有将zuul网关项目注册到eureka中心,而是将zuul与springboot结合作为一个独立的项目进行请求转发,因此本项目是非spring cloud架构。

开始编写zuul网关项目 
首先,新建一个spring boot项目。加入zuul依赖,开启@EnableZuulProxy注解。 
pom.xml

1 <dependency>
2     <groupId>org.springframework.cloud</groupId>
3     <artifactId>spring-cloud-starter-zuul</artifactId>
4     <version>1.4.4.RELEASE</version>
5 </dependency>

application.properties

1 server.port=8090
2 eureka.client.enable=false
3 zuul.ribbon.eager-load.enabled=true
4 
5 zuul.SendErrorFilter.post.disable=true

由于后续会使用到动态路由,所以这里我们并不需要在application.properties中做网关地址转发映射。

SpringBootZuulApplication.java

 1 package com.syher.zuul;
 2 
 3 import com.google.common.util.concurrent.ThreadFactoryBuilder;
 4 import com.syher.zuul.core.zuul.router.PropertiesRouter;
 5 import org.springframework.beans.factory.annotation.Autowired;
 6 import org.springframework.boot.CommandLineRunner;
 7 import org.springframework.boot.SpringApplication;
 8 import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
 9 import org.springframework.cloud.netflix.zuul.EnableZuulProxy;
10 import org.springframework.cloud.netflix.zuul.RoutesRefreshedEvent;
11 import org.springframework.cloud.netflix.zuul.filters.RouteLocator;
12 import org.springframework.context.ApplicationEventPublisher;
13 import org.springframework.context.annotation.ComponentScan;
14 
15 import java.io.File;
16 import java.util.concurrent.Executors;
17 import java.util.concurrent.ScheduledExecutorService;
18 import java.util.concurrent.TimeUnit;
19 
20 /**
21  * @author braska
22  * @date 2018/06/25.
23  **/
24 @EnableAutoConfiguration
25 @EnableZuulProxy
26 @ComponentScan(basePackages = {
27         "com.syher.zuul.core",
28         "com.syher.zuul.service"
29 })
30 public class SpringBootZuulApplication implements CommandLineRunner {
31     @Autowired
32     ApplicationEventPublisher publisher;
33     @Autowired
34     RouteLocator routeLocator;
35 
36     private ScheduledExecutorService executor;
37     private Long lastModified = 0L;
38     private boolean instance = true;
39 
40     public static void main(String[] args) {
41         SpringApplication.run(SpringBootZuulApplication.class, args);
42     }
43 
44     @Override
45     public void run(String... args) throws Exception {
46         executor = Executors.newSingleThreadScheduledExecutor(
47                 new ThreadFactoryBuilder().setNameFormat("properties read.").build()
48         );
49         executor.scheduleWithFixedDelay(() -> publish(), 0, 1, TimeUnit.SECONDS);
50     }
51 
52     private void publish() {
53         if (isPropertiesModified()) {
54             publisher.publishEvent(new RoutesRefreshedEvent(routeLocator));
55         }
56     }
57 
58     private boolean isPropertiesModified() {
59         File file = new File(this.getClass().getClassLoader().getResource(PropertiesRouter.PROPERTIES_FILE).getPath());
60         if (instance) {
61             instance = false;
62             return false;
63         }
64         if (file.lastModified() > lastModified) {
65             lastModified = file.lastModified();
66             return true;
67         }
68         return false;
69     }
70 }

一、自定义过滤器

自定义zuul过滤器比较简单。我们先讲过滤器。 
zuul过滤器分为pre、route、post、error四种类型。作用我就不详细讲了,网上资料一大把。本文主要写路由前的过滤,即pre类型。 
要自定义一个过滤器,只需要要继承ZuulFilter,然后指定过滤类型、过滤顺序、是否执行这个过滤器、过滤内容就OK了。

为了便于扩展,这里用到了模板模式。 
AbstractZuulFilter.java

 1 package com.syher.zuul.core.zuul.filter;
 2 
 3 import com.netflix.zuul.ZuulFilter;
 4 import com.netflix.zuul.context.RequestContext;
 5 import com.syher.zuul.core.zuul.ContantValue;
 6 
 7 /**
 8  * @author braska
 9  * @date 2018/06/29.
10  **/
11 public abstract class AbstractZuulFilter extends ZuulFilter {
12 
13     protected RequestContext context;
14 
15     @Override
16     public boolean shouldFilter() {
17         RequestContext ctx = RequestContext.getCurrentContext();
18         return (boolean) (ctx.getOrDefault(ContantValue.NEXT_FILTER, true));
19     }
20 
21     @Override
22     public Object run() {
23         context = RequestContext.getCurrentContext();
24         return doRun();
25     }
26 
27     public abstract Object doRun();
28 
29     public Object fail(Integer code, String message) {
30         context.set(ContantValue.NEXT_FILTER, false);
31         context.setSendZuulResponse(false);
32         context.getResponse().setContentType("text/html;charset=UTF-8");
33         context.setResponseStatusCode(code);
34         context.setResponseBody(String.format("{"result":"%s!"}", message));
35         return null;
36     }
37 
38     public Object success() {
39         context.set(ContantValue.NEXT_FILTER, true);
40         return null;
41     }
42 }

定义preFilter的抽象类,继承AbstractZuulFilter。指定pre类型,之后所有的pre过滤器都可以继承这个抽象类。 
AbstractPreZuulFilter.java

 1 package com.syher.zuul.core.zuul.filter.pre;
 2 
 3 import com.syher.zuul.core.zuul.FilterType;
 4 import com.syher.zuul.core.zuul.filter.AbstractZuulFilter;
 5 
 6 /**
 7  * @author braska
 8  * @date 2018/06/29.
 9  **/
10 public abstract class AbstractPreZuulFilter extends AbstractZuulFilter {
11     @Override
12     public String filterType() {
13         return FilterType.pre.name();
14     }
15 }

接着编写具体一个具体的过滤器,比如限流。 
RateLimiterFilter.java

 1 package com.syher.zuul.core.zuul.filter.pre;
 2 
 3 import com.google.common.util.concurrent.RateLimiter;
 4 import com.syher.zuul.core.zuul.FilterOrder;
 5 import org.slf4j.Logger;
 6 import org.slf4j.LoggerFactory;
 7 
 8 import javax.servlet.http.HttpServletRequest;
 9 
10 /**
11  * @author braska
12  * @date 2018/06/29.
13  **/
14 public class RateLimiterFilter extends AbstractPreZuulFilter {
15 
16     private static final Logger LOGGER = LoggerFactory.getLogger(RateLimiterFilter.class);
17 
18     /**
19      * 每秒允许处理的量是50
20      */
21     RateLimiter rateLimiter = RateLimiter.create(50);
22 
23     @Override
24     public int filterOrder() {
25         return FilterOrder.RATE_LIMITER_ORDER;
26     }
27 
28     @Override
29     public Object doRun() {
30         HttpServletRequest request = context.getRequest();
31         String url = request.getRequestURI();
32         if (rateLimiter.tryAcquire()) {
33             return success();
34         } else {
35             LOGGER.info("rate limit:{}", url);
36             return fail(401, String.format("rate limit:{}", url));
37         }
38     }
39 }

其他类型的过滤器也一样。创建不同的抽象类,比如AbstractPostZuulFilter,指定filterType,然后具体的postFilter只要继承该抽象类即可。

最后,将过滤器托管给spring。 
ZuulConfigure.java

 1 package com.syher.zuul.core.config;
 2 
 3 import com.netflix.loadbalancer.IRule;
 4 import com.netflix.zuul.ZuulFilter;
 5 import com.syher.zuul.core.ribbon.ServerLoadBalancerRule;
 6 import com.syher.zuul.core.zuul.filter.pre.RateLimiterFilter;
 7 import com.syher.zuul.core.zuul.filter.pre.TokenAccessFilter;
 8 import com.syher.zuul.core.zuul.filter.pre.UserRightFilter;
 9 import com.syher.zuul.core.zuul.router.PropertiesRouter;
10 import org.springframework.beans.factory.annotation.Autowired;
11 import org.springframework.boot.autoconfigure.web.ServerProperties;
12 import org.springframework.cloud.netflix.zuul.filters.ZuulProperties;
13 import org.springframework.context.annotation.Bean;
14 import org.springframework.context.annotation.Configuration;
15 
16 /**
17  * @author braska
18  * @date 2018/07/05.
19  **/
20 @Configuration
21 public class ZuulConfigure {
22 
23     @Autowired
24     ZuulProperties zuulProperties;
25     @Autowired
26     ServerProperties server;
27 
28     /**
29      * 动态路由
30      * @return
31      */
32     @Bean
33     public PropertiesRouter propertiesRouter() {
34         return new PropertiesRouter(this.server.getServletPrefix(), this.zuulProperties);
35     }
36 
37     /**
38      * 动态负载
39      * @return
40      */
41     @Bean
42     public IRule loadBalance() {
43         return new ServerLoadBalancerRule();
44     }
45 
46     /**
47      * 自定义过滤器
48      * @return
49      */
50     @Bean
51     public ZuulFilter rateLimiterFilter() {
52         return new RateLimiterFilter();
53     }
54 }

二、动态路由

接着写动态路由。动态路由需要配置可持久化且能动态刷新。 
zuul默认使用的路由是SimpleRouteLocator,不具备动态刷新的效果。DiscoveryClientRouteLocator具备刷新功能,但是需要已有的项目将服务注册到eureka,这不符合已有项目代码零侵入的需求所以排除。那么还有个办法就是自定义路由然后实现RefreshableRouteLocator类。

部分代码如下: 
AbstractDynamicRouter.java

 1 package com.syher.zuul.core.zuul.router;
 2 
 3 import com.syher.zuul.core.zuul.entity.BasicRoute;
 4 import org.apache.commons.lang.StringUtils;
 5 import org.slf4j.Logger;
 6 import org.slf4j.LoggerFactory;
 7 import org.springframework.beans.BeanUtils;
 8 import org.springframework.cloud.netflix.zuul.filters.RefreshableRouteLocator;
 9 import org.springframework.cloud.netflix.zuul.filters.SimpleRouteLocator;
10 import org.springframework.cloud.netflix.zuul.filters.ZuulProperties;
11 
12 import java.util.LinkedHashMap;
13 import java.util.List;
14 import java.util.Map;
15 
16 /**
17  * @author braska
18  * @date 2018/07/02.
19  **/
20 public abstract class AbstractDynamicRouter extends SimpleRouteLocator implements RefreshableRouteLocator {
21 
22     private static final Logger LOGGER = LoggerFactory.getLogger(AbstractDynamicRouter.class);
23 
24     public AbstractDynamicRouter(String servletPath, ZuulProperties properties) {
25         super(servletPath, properties);
26     }
27 
28     @Override
29     public void refresh() {
30         doRefresh();
31     }
32 
33     @Override
34     protected Map<String, ZuulProperties.ZuulRoute> locateRoutes() {
35         LinkedHashMap<String, ZuulProperties.ZuulRoute> routes = new LinkedHashMap<String, ZuulProperties.ZuulRoute>();
36         routes.putAll(super.locateRoutes());
37 
38         List<BasicRoute> results = readRoutes();
39 
40         for (BasicRoute result : results) {
41             if (StringUtils.isEmpty(result.getPath()) ) {
42                 continue;
43             }
44             ZuulProperties.ZuulRoute zuulRoute = new ZuulProperties.ZuulRoute();
45             try {
46                 BeanUtils.copyProperties(result, zuulRoute);
47             } catch (Exception e) {
48                 LOGGER.error("=============load zuul route info from db with error==============", e);
49             }
50             routes.put(zuulRoute.getPath(), zuulRoute);
51         }
52         return routes;
53     }
54 
55     /**
56      * 读取路由信息
57      * @return
58      */
59     protected abstract List<BasicRoute> readRoutes();
60 }

由于本人比较懒。不想每次写个demo都要重新配置一大堆数据库信息。所以本文很多数据比如路由信息、比如负载策略。要么写在文本里面,要么直接java代码构造。 
本demo的路由信息就是从properties里面读取。嗯,继承AbstractDynamicRouter即可。 
PropertiesRouter.java

 1 package com.syher.zuul.core.zuul.router;
 2 
 3 import com.google.common.collect.Lists;
 4 import com.google.common.util.concurrent.ThreadFactoryBuilder;
 5 import com.syher.zuul.common.Context;
 6 import com.syher.zuul.core.zuul.entity.BasicRoute;
 7 import org.apache.commons.lang.StringUtils;
 8 import org.slf4j.Logger;
 9 import org.slf4j.LoggerFactory;
10 import org.springframework.cloud.netflix.zuul.filters.ZuulProperties;
11 
12 import java.io.File;
13 import java.io.IOException;
14 import java.util.HashMap;
15 import java.util.List;
16 import java.util.Map;
17 import java.util.Properties;
18 import java.util.concurrent.Executors;
19 import java.util.concurrent.ScheduledExecutorService;
20 import java.util.stream.Collectors;
21 
22 /**
23  * @author braska
24  * @date 2018/07/02.
25  **/
26 public class PropertiesRouter extends AbstractDynamicRouter {
27 
28     private static final Logger LOGGER = LoggerFactory.getLogger(PropertiesRouter.class);
29     public static final String PROPERTIES_FILE = "router.properties";
30     private static final String ZUUL_ROUTER_PREFIX = "zuul.routes";
31 
32 
33     public PropertiesRouter(String servletPath, ZuulProperties properties) {
34         super(servletPath, properties);
35     }
36 
37     @Override
38     protected List<BasicRoute> readRoutes() {
39         List<BasicRoute> list = Lists.newArrayListWithExpectedSize(3);
40         try {
41             Properties prop = new Properties();
42             prop.load(
43                     this.getClass().getClassLoader().getResourceAsStream(PROPERTIES_FILE)
44             );
45 
46             Context context = new Context(new HashMap<>((Map) prop));
47             Map<String, String> data = context.getSubProperties(ZUUL_ROUTER_PREFIX);
48             List<String> ids = data.keySet().stream().map(s -> s.substring(0, s.indexOf("."))).distinct().collect(Collectors.toList());
49             ids.stream().forEach(id -> {
50                 Map<String, String> router = context.getSubProperties(String.join(".", ZUUL_ROUTER_PREFIX, id));
51 
52                 String path = router.get("path");
53                 path = path.startsWith("/") ? path : "/" + path;
54 
55                 String serviceId = router.getOrDefault("serviceId", null);
56                 String url = router.getOrDefault("url", null);
57 
58                 BasicRoute basicRoute = new BasicRoute();
59                 basicRoute.setId(id);
60                 basicRoute.setPath(path);
61                 basicRoute.setUrl(router.getOrDefault("url", null));
62                 basicRoute.setServiceId((StringUtils.isBlank(url) && StringUtils.isBlank(serviceId)) ? id : serviceId);
63                 basicRoute.setRetryable(Boolean.parseBoolean(router.getOrDefault("retry-able", "false")));
64                 basicRoute.setStripPrefix(Boolean.parseBoolean(router.getOrDefault("strip-prefix", "false")));
65                 list.add(basicRoute);
66             });
67         } catch (IOException e) {
68             LOGGER.info("error to read " + PROPERTIES_FILE + " :{}", e);
69         }
70         return list;
71     }
72 }

既然是动态路由实时刷新,那肯定需要一个定时器定时监控properties文件。所以我在启动类SpringBootZuulApplication加了个定时器监控properties是否发生过变更(之前有疑问的现在可以解惑了)。一旦文件被修改过就重新发布一下, 然后会触发routeLocator的refresh方法。

1 public void publish() {
2         if (isPropertiesModified()) {
3             publisher.publishEvent(new RoutesRefreshedEvent(routeLocator));
4         }
5     }

当然,如果是从数据库或者其他地方比如redis读取就不需要用到定时器,只要在增删改的时候直接publish就好了。

最后,记得PropertiesRouter类交由spring托管(在ZuulConfigure类中配置bean)。

router.properties文件:

1 zuul.routes.dashboard.path=/**
2 zuul.routes.dashboard.strip-prefix=true
3 
4 ##不使用动态负载需指定url
5 ##zuul.routes.dashboard.url=http://localhost:9000/
6 ##zuul服务部署后,动态增加网关映射,无需重启即可实时路由到新的网关
7 ##zuul.routes.baidu.path=/**

三、动态负载

负载也算比较简单,复杂点的是写负载算法。 
动态负载主要分两个步骤: 
1、根据网关项目配置的host和port去数据库(我是java直接造的数据)查找负载策略,比如轮询、比如随机、比如iphash等等。 
2、根据策略结合每台服务器分配的权重选出合适的服务。

实现动态负载需要自定义rule类然后继承AbstractLoadBalancerRule类。 
首先看负载策略的选择: 
ServerLoadBalancerRule.java

 1 package com.syher.zuul.core.ribbon;
 2 
 3 import com.google.common.base.Preconditions;
 4 import com.netflix.client.config.IClientConfig;
 5 import com.netflix.loadbalancer.AbstractLoadBalancerRule;
 6 import com.netflix.loadbalancer.ILoadBalancer;
 7 import com.netflix.loadbalancer.Server;
 8 import com.syher.zuul.common.util.SystemUtil;
 9 import com.syher.zuul.core.ribbon.balancer.LoadBalancer;
10 import com.syher.zuul.core.ribbon.balancer.RandomLoadBalancer;
11 import com.syher.zuul.core.ribbon.balancer.RoundLoadBalancer;
12 import com.syher.zuul.entity.GatewayAddress;
13 import com.syher.zuul.service.GatewayService;
14 import org.apache.commons.lang.StringUtils;
15 import org.slf4j.Logger;
16 import org.slf4j.LoggerFactory;
17 import org.springframework.beans.factory.annotation.Autowired;
18 import org.springframework.beans.factory.annotation.Value;
19 
20 /**
21  * @author braska
22  * @date 2018/07/05.
23  **/
24 public class ServerLoadBalancerRule extends AbstractLoadBalancerRule {
25 
26     private static final Logger LOGGER = LoggerFactory.getLogger(ServerLoadBalancerRule.class);
27 
28     @Value("${server.host:127.0.0.1}")
29     private String host;
30     @Value("${server.port:8080}")
31     private Integer port;
32 
33     @Autowired
34     private GatewayService gatewayService;
35 
36     @Override
37     public void initWithNiwsConfig(IClientConfig iClientConfig) {
38     }
39 
40     @Override
41     public Server choose(Object key) {
42         return getServer(getLoadBalancer(), key);
43     }
44 
45     private Server getServer(ILoadBalancer loadBalancer, Object key) {
46         if (StringUtils.isBlank(host)) {
47             host = SystemUtil.ipList().get(0);
48         }
49         //Preconditions.checkArgument(host != null, "server.host must be specify.");
50         //Preconditions.checkArgument(port != null, "server.port must be specify.");
51 
52         GatewayAddress address = gatewayService.getByHostAndPort(host, port);
53         if (address == null) { //这里的逻辑可以改,找不到网关配置信息可以指定默认的负载策略
54             LOGGER.error(String.format("must be config a gateway info for the server[%s:%s].", host, String.valueOf(port)));
55             return null;
56         }
57 
58         LoadBalancer balancer = LoadBalancerFactory.build(address.getFkStrategyId());
59 
60         return balancer.chooseServer(loadBalancer);
61     }
62 
63     static class LoadBalancerFactory {
64 
65         public static LoadBalancer build(String strategy) {
66             GatewayAddress.StrategyType type = GatewayAddress.StrategyType.of(strategy);
67             switch (type) {
68                 case ROUND:
69                     return new RoundLoadBalancer();
70                 case RANDOM:
71                     return new RandomLoadBalancer();
72                 default:
73                     return null;
74             }
75         }
76     }
77 }

然后是负载算法接口代码。 
LoadBalancer.java

 1 package com.syher.zuul.core.ribbon.balancer;
 2 
 3 import com.netflix.loadbalancer.ILoadBalancer;
 4 import com.netflix.loadbalancer.Server;
 5 
 6 /**
 7  * @author braska
 8  * @date 2018/07/06.
 9  **/
10 public interface LoadBalancer {
11 
12     /**
13      * choose a loadBalancer
14      * @param loadBalancer
15      * @return
16      */
17     Server chooseServer(ILoadBalancer loadBalancer);
18 }

定义抽象类,实现LoadBalancer接口 
AbstractLoadBalancer.java

 1 package com.syher.zuul.core.ribbon.balancer;
 2 
 3 import com.netflix.loadbalancer.ILoadBalancer;
 4 import com.netflix.loadbalancer.Server;
 5 import com.syher.zuul.core.SpringContext;
 6 import com.syher.zuul.service.ServerService;
 7 import org.slf4j.Logger;
 8 import org.slf4j.LoggerFactory;
 9 
10 /**
11  * @author braska
12  * @date 2018/07/06.
13  **/
14 public abstract class AbstractLoadBalancer implements LoadBalancer {
15     private static final Logger LOGGER = LoggerFactory.getLogger(AbstractLoadBalancer.class);
16     protected ServerService serverService;
17 
18     @Override
19     public Server chooseServer(ILoadBalancer loadBalancer) {
20         this.serverService = SpringContext.getBean(ServerService.class);
21         Server server = choose(loadBalancer);
22         if (server != null) {
23             LOGGER.info(String.format("the server[%s:%s] has been select.", server.getHost(), server.getPort()));
24         } else {
25             LOGGER.error("could not find any server.");
26         }
27         return server;
28     }
29 
30     public abstract Server choose(ILoadBalancer loadBalancer);
31 }

轮询负载算法 
RoundLoadBalancer.java

 1 package com.syher.zuul.core.ribbon.balancer;
 2 
 3 import com.netflix.loadbalancer.ILoadBalancer;
 4 import com.netflix.loadbalancer.Server;
 5 import com.syher.zuul.common.Constant;
 6 import com.syher.zuul.core.GlobalCache;
 7 import com.syher.zuul.core.ribbon.LoadBalancerRuleUtil;
 8 import com.syher.zuul.entity.ServerAddress;
 9 
10 import java.util.List;
11 
12 /**
13  * 权重轮询
14  * 首次使用取最大权重的服务器。而后通过权重的不断递减,寻找适合的服务器。
15  * @author braska
16  * @date 2018/07/06.
17  **/
18 public class RoundLoadBalancer extends AbstractLoadBalancer {
19 
20     private Integer currentServer;
21     private Integer currentWeight;
22     private Integer maxWeight;
23     private Integer gcdWeight;
24 
25     @Override
26     public Server choose(ILoadBalancer loadBalancer) {
27         List<ServerAddress> addressList = serverService.getAvailableServer();
28         if (addressList != null && !addressList.isEmpty()) {
29             maxWeight = LoadBalancerRuleUtil.getMaxWeightForServers(addressList);
30             gcdWeight = LoadBalancerRuleUtil.getGCDForServers(addressList);
31             currentServer = Integer.parseInt(GlobalCache.instance().getOrDefault(Constant.CURRENT_SERVER_KEY, -1).toString());
32             currentWeight = Integer.parseInt(GlobalCache.instance().getOrDefault(Constant.CURRENT_WEIGHT_KEY, 0).toString());
33 
34             Integer serverCount = addressList.size();
35 
36             if (1 == serverCount) {
37                 return new Server(addressList.get(0).getHost(), addressList.get(0).getPort());
38             } else {
39                 while (true) {
40                     currentServer = (currentServer + 1) % serverCount;
41                     if (currentServer == 0) {
42                         currentWeight = currentWeight - gcdWeight;
43                         if (currentWeight <= 0) {
44                             currentWeight = maxWeight;
45                             if (currentWeight == 0) {
46                                 GlobalCache.instance().put(Constant.CURRENT_SERVER_KEY, currentServer);
47                                 GlobalCache.instance().put(Constant.CURRENT_WEIGHT_KEY, currentWeight);
48                                 Thread.yield();
49                                 return null;
50                             }
51                         }
52                     }
53 
54                     ServerAddress address = addressList.get(currentServer);
55                     if (address.getWeight() >= currentWeight) {
56                         GlobalCache.instance().put(Constant.CURRENT_SERVER_KEY, currentServer);
57                         GlobalCache.instance().put(Constant.CURRENT_WEIGHT_KEY, currentWeight);
58                         return new Server(address.getHost(), address.getPort());
59                     }
60                 }
61             }
62 
63         }
64         return null;
65     }
66 }

最后,ServerLoadBalancerRule交由spring托管。

至此,springboot+zuul实现自定义过滤器、动态路由、动态负载就都完成了。 
源码:https://github.com/rxiu/study-on-road/tree/master/trickle-gateway

原文地址:https://www.cnblogs.com/braska/p/9645200.html