SpringCloud开发、本地联调解决方案-代理中心实现

1. 背景

在SpringCloud架构下,开发过程中的联调成为痛点。

一般情况下,开发人员需要运行多个或全部服务,或干扰其他环境才可正常进行联调,成本极高。

我们希望找到一种简单、快速的方式来解决以下问题。

1.1 仅希望启动正在开发的服务

开发人员通常只希望启动当前正在开发的服务,但微服务存在众多链路,仅启动下游服务是无法通过从上游服务直接访问的。
通常我们需要启动一个上游服务指向本地的下游服务或通过线上负载不断请求,通过一定频率落在本地服务。

所以我们需要一个能够在上游服务直接请求到本地服务的方法,减少不必要的等待。

1.2 希望在联调阶段断点

当开发进入前后端联调时,如开发环境存在问题,开发人员通常希望与前端进行断点调试以快速找到问题。
但我们的服务不仅仅是针对某一人提供的,此时如果他人请求我们所断点的服务,即会出现问题。

所以我们需要一个能够针对某个人所单独提供特定服务(即本地服务)的方法,以便进行点对点调试。

2. 解决方案

为了解决上述问题,我们需要一种代理机制。

  1. 该代理可以将某个客户端ip发出的请求转发到所配置的目标机器。
  2. 配置成功后,该服务的一切请求均将被代理,无论其处于链路的哪个阶段。
  3. 点对点代理,不会影响他人访问。

3. 实现思路及源码

基本思想:对于代理而言,基本思路就是拦截请求方所要请求的地址,转发到我们希望其到达的目的地。

对于本服务而言,我们将客户端所配置的代理均存放在redis中,并且在请求发出前,比对当前请求是否存在于redis内,如果存在,重写其地址为redis中所配置的地址。

基于目前的框架,代理转发服务需要代理的组件有两种。
一种是spring cloud gateway所配置的上游服务的路由。
另外一种是服务间的通讯OpenFeign服务。

我们基于spring boot 2.1.5版本,分别对用于实现代理的两种组件的原理进行介绍。

3.1 SpringCloudGateway

对于Gateway而言,我们只需要为其添加一个全局过滤器,拦截请求后修改url地址即可。

由于我们的gateway是注册进eureka的,所以默认情况下,会存在一个LoadBalancerClientFilter。

LoadBalancerClientFilter从名字就可以看出,它的作用是针对客户端的请求进行负载,它会将以"lb:"开头的请求(由路由配置的)从eureka中通过策略拿出一个可用的远端服务,并且转发请求至改服务。这与我们要做的代理转发相同。

而我们只需要在其过滤器执行前,将url地址改变,从而不经过其过滤器,转而从正常的http请求即可。

全部代码如下:

3.1.1 NetworkUtil

import org.springframework.http.HttpHeaders;
import org.springframework.http.server.reactive.ServerHttpRequest;

import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.util.Objects;

/**
 * @author zhangbowen
 */
public class NetworkUtil {
    public static final String HEADER_X_FORWARDED_FOR = "X-Forwarded-For";
    public static final String PROXY_CLIENT_IP = "Proxy-Client-IP";
    public static final String WL_PROXY_CLIENT_IP = "WL-Proxy-Client-IP";
    public static final String HTTP_CLIENT_IP = "HTTP_CLIENT_IP";
    public static final String HTTP_X_FORWARD_IP = "HTTP_X_FORWARDED_FOR";

    /**
     * 获取请求主机IP地址,如果通过代理进来,则透过防火墙获取真实IP地址;
     *
     * @param request 请求
     * @return ip
     */
    public static String getIp(ServerHttpRequest request) {

        try {
            HttpHeaders httpHeaders = request.getHeaders();
            // 获取请求主机IP地址,如果通过代理进来,则透过防火墙获取真实IP地址
            String ip = httpHeaders.getFirst(HEADER_X_FORWARDED_FOR);

            if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
                if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
                    ip = httpHeaders.getFirst(PROXY_CLIENT_IP);
                }
                if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
                    ip = httpHeaders.getFirst(WL_PROXY_CLIENT_IP);
                }
                if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
                    ip = httpHeaders.getFirst(HTTP_CLIENT_IP);
                }
                if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
                    ip = httpHeaders.getFirst(HTTP_X_FORWARD_IP);
                }
                if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
                    InetSocketAddress remoteAddress = request.getRemoteAddress();
                    ip = Objects.requireNonNull(remoteAddress).getAddress().getHostAddress();
                }
            } else if (ip.length() > 15) {
                String[] ips = ip.split(",");
                for (String ip1 : ips) {
                    if (!("unknown".equalsIgnoreCase(ip1))) {
                        ip = ip1;
                        break;
                    }
                }
            }
            if (ip.equals("0:0:0:0:0:0:0:1")) {
                ip = InetAddress.getLocalHost().getHostAddress();
            }
            return ip;
        } catch (Exception e) {
            return "";
        }
    }
}

3.1.2 FeignClientProxyForward

import lombok.Data;

/**
 * @author zhangbowen
 * @since 2019-06-20
 */
@Data
public class FeignClientProxyForward {
    /**
     * 客户端ip
     */
    private String clientIp;
    /**
     * 代理服务
     */
    private String serviceName;
    /**
     * 目标ip
     */
    private String targetIp;
    /**
     * 目标端口
     */
    private String targetPort;
}

3.1.3 RedisHelper

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.type.TypeFactory;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

import java.lang.reflect.Type;

/**
 * @author zhangbowen
 * @since 2019/5/27
 */
public class RedisHelper {
    private static RedisConnectionFactory factory;
    private static ObjectMapper objectMapper;

    public RedisHelper(RedisConnectionFactory factory, ObjectMapper objectMapper) {
        RedisHelper.factory = factory;
        RedisHelper.objectMapper = objectMapper;
    }

    /**
     * 创建泛型template
     *
     * @param clazz 泛型类
     * @param <T>   定义泛型
     * @return RedisTemplate
     */
    public static <T> RedisTemplate<String, T> template(Class<T> clazz) {
        RedisTemplate<String, T> template = new RedisTemplate<>();
        template.setConnectionFactory(factory);
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(clazz);
        jackson2JsonRedisSerializer.setObjectMapper(objectMapper);
        template.setValueSerializer(jackson2JsonRedisSerializer);
        template.setKeySerializer(new StringRedisSerializer());
        template.setHashKeySerializer(new StringRedisSerializer());
        template.setHashValueSerializer(jackson2JsonRedisSerializer);
        template.afterPropertiesSet();
        return template;
    }

    /**
     * 创建泛型template
     *
     * @param <T> 定义泛型
     * @return RedisTemplate
     */
    public static <T> RedisTemplate<String, T> template() {
        RedisTemplate<String, T> template = new RedisTemplate<>();
        template.setConnectionFactory(factory);
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);
        jackson2JsonRedisSerializer.setObjectMapper(objectMapper);
        template.setValueSerializer(jackson2JsonRedisSerializer);
        template.setKeySerializer(new StringRedisSerializer());
        template.setHashKeySerializer(new StringRedisSerializer());
        template.setHashValueSerializer(jackson2JsonRedisSerializer);
        template.afterPropertiesSet();
        return template;
    }

    /**
     * 创建泛型template
     *
     * @param type 泛型
     * @param <T>  定义泛型
     * @return RedisTemplate
     */
    public static <T> RedisTemplate<String, T> template(Type type) {
        RedisTemplate<String, T> template = new RedisTemplate<>();
        template.setConnectionFactory(factory);
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(TypeFactory.defaultInstance().constructType(type));
        jackson2JsonRedisSerializer.setObjectMapper(objectMapper);
        template.setValueSerializer(jackson2JsonRedisSerializer);
        template.setKeySerializer(new StringRedisSerializer());
        template.setHashKeySerializer(new StringRedisSerializer());
        template.setHashValueSerializer(jackson2JsonRedisSerializer);
        template.afterPropertiesSet();
        return template;
    }
}

3.1.4 ProxyForwardRequestFilter

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.TypeReference;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.data.redis.core.BoundHashOperations;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

import java.net.URI;
import java.util.Map;

import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.*;

/**
 * @author zhangbowen
 * @since 2019-06-21
 */
@Slf4j
public class ProxyForwardRequestFilter implements GlobalFilter, Ordered {
    public static final String CLIENT_PROXY_FORWARD_KEY = "feign-client-proxy-forward";
    public static final int PROXY_FORWARD_REQUEST_FILTER = 10099;

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        URI url = exchange.getAttribute(GATEWAY_REQUEST_URL_ATTR);
        String schemePrefix = exchange.getAttribute(GATEWAY_SCHEME_PREFIX_ATTR);

        if (url == null || (!"lb".equals(url.getScheme()) && !"lb".equals(schemePrefix))) {
            return chain.filter(exchange);
        }

        //保留原始url
        addOriginalRequestUrl(exchange, url);

        String clientIp = NetworkUtil.getIp(exchange.getRequest());
        //要请求的服务名称
        String clientName = url.getHost();

        //获取代理
        FeignClientProxyForward proxyForward = getProxyForward(clientIp, clientName);

        //如果目标请求未代理,走原请求
        if (proxyForward == null) {
            //不做任何操作
            return chain.filter(exchange);
        }
        //如果被代理,修改代理的ip,重新构建URI
        URI newUrl = replaceHostName(url.toString(), clientName, proxyForward.getTargetIp() + ":" + proxyForward.getTargetPort());

        exchange.getAttributes().put(GATEWAY_REQUEST_URL_ATTR, newUrl);

        return chain.filter(exchange);
    }

    private URI replaceHostName(String originalUrl, String host, String newHost) {
        String newUrl = originalUrl;
        if (originalUrl.startsWith("lb://")) {
            newUrl = originalUrl.substring(0, 5)
                    + newHost
                    + originalUrl.substring(5 + host.length());
        }
        StringBuffer buffer = new StringBuffer(newUrl);
        if ((newUrl.startsWith("lb://") && newUrl.length() == 5)) {
            buffer.append("/");
        }
        return URI.create("http" + buffer.substring(2));
    }

    /**
     * 根据请求获取该请求的代理,如果为null,请求源路径
     *
     * @return
     */
    private FeignClientProxyForward getProxyForward(String requestIp, String clientName) {
        try {
            BoundHashOperations<String, String, String> boundHashOperations = RedisHelper.template().boundHashOps(CLIENT_PROXY_FORWARD_KEY);
            //获取代理json列表
            String clientProxyTableJson = boundHashOperations.get(requestIp);
            //代理列表为空
            if (StringUtils.isEmpty(clientProxyTableJson)) {
                //走原方法
                return null;
            }
            Map<String, FeignClientProxyForward> clientProxyTable = JSON.parseObject(clientProxyTableJson, new TypeReference<Map<String, FeignClientProxyForward>>() {
            });
            if (CollectionUtils.isEmpty(clientProxyTable)) {
                //走原方法
                return null;
            }
            return clientProxyTable.get(clientName.toLowerCase());
        } catch (Exception e) {
            return null;
        }
    }

    @Override
    public int getOrder() {
        return PROXY_FORWARD_REQUEST_FILTER;
    }

}

3.1.5 RequestProxyForwardAutoConfiguration

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * @author zhangbowen
 * @since 2019-06-20
 */
@Configuration
public class RequestProxyForwardAutoConfiguration {

    @Bean
    public ProxyForwardRequestFilter proxyForwardRequestFilter() {
        return new ProxyForwardRequestFilter();
    }
    @Bean
    public RedisHelper redisHelper(RedisConnectionFactory factory, ObjectMapper objectMapper) {
        return new RedisHelper(factory, objectMapper);
    }
}

3.2 OpenFeign

针对于OpenFeign的拦截会复杂一些,接下来的所涉及到的内容会冗长且繁杂,希望大家可以多一些耐心。
先来看一张OpenFeign在SpringCloud使用过程中基本的类图关系:

image.png

简单描述下上述流程:

  1. Spring启动时,注册Client,Client为Feign的具体调用类。由于我们使用的eureka,故会注册LoadBalancerFeignClient。
  2. 当我们加入@EnableFeignClients注解后,Spring启动时会执行FeignClientsRegistrar,扫描包下的@FeignClient注解,创建FeignClientFactoryBean。FeignClientFactoryBean为Feign的工厂生成类,会生成多个Feign的实现类注入到Spring中。
  3. 由于我们使用Hystrix,故会生成HystrixFeign。该类引用并扩展了ReflectiveFeign,ReflectiveFeign为继承Feign的具体实现类。
  4. HystrixFeign中创建HystrixInvocationHandler,HystrixInvocationHandler为代理方法,会在执行真实方法前执行所附加的方法,该类会附加hystrix的command策略。由于我们使用的sleuth,故默认注入的是SleuthHystrixConcurrencyStrategy策略,该策略包装了请求,并传递了trace,以供整个链路使用。
  5. ReflectiveFeign同时会创建一个SynchronousMethodHandler代理类,该类会feign的拦截器(如果存在)。
  6. 最终当我们调用FeignClient所注解的接口中的方法时,会首先调用SynchronousMethodHandler执行拦截器,接着调用HystrixInvocationHandler执行包装请求,最终通过LoadBalancerFeignClient发出请求。

3.2.1 如何扩展

当我们理解整个过程中,我们得到以下几个信息:

  1. 在hystrix包装过程中,会创建线程,从而与当前请求到controller所在的线程隔离,如果要传输当前请求线程中的数据,需要在包装类创建的线程中传入,sleuth就是这样做的。
  2. 拦截器是在hystrix包装前之前执行的,并且两者在同一线程。
  3. 我们需要在发出请求前改变url,也就是LoadBalancerFeignClient内。

3.2.2 FeignClientProxyForward

@Data
public class FeignClientProxyForward {
    /**
     * 客户端ip
     */
    private String clientIp;
    /**
     * 代理服务
     */
    private String serviceName;
    /**
     * 目标ip
     */
    private String targetIp;
    /**
     * 目标端口
     */
    private String targetPort;
}

3.2.3 传递请求方ip

HystrixConcurrencyStrategy为我们提供了扩展口,它允许我们自己实现一个策略,以允许在传输过程中传递参数,sleuth已经为我们实现了,因此我们可以仿照其代码进行修改:

import com.netflix.hystrix.HystrixThreadPoolKey;
import com.netflix.hystrix.HystrixThreadPoolProperties;
import com.netflix.hystrix.strategy.HystrixPlugins;
import com.netflix.hystrix.strategy.concurrency.HystrixConcurrencyStrategy;
import com.netflix.hystrix.strategy.concurrency.HystrixRequestVariable;
import com.netflix.hystrix.strategy.concurrency.HystrixRequestVariableLifecycle;
import com.netflix.hystrix.strategy.eventnotifier.HystrixEventNotifier;
import com.netflix.hystrix.strategy.executionhook.HystrixCommandExecutionHook;
import com.netflix.hystrix.strategy.metrics.HystrixMetricsPublisher;
import com.netflix.hystrix.strategy.properties.HystrixPropertiesStrategy;
import com.netflix.hystrix.strategy.properties.HystrixProperty;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;

import java.util.concurrent.BlockingQueue;
import java.util.concurrent.Callable;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

/**
 * @author zhangbowen
 * @since 2019-06-20
 */
@Slf4j
public class TransferHeaderHystrixConcurrencyStrategy extends HystrixConcurrencyStrategy {
    private HystrixConcurrencyStrategy delegate;

    public TransferHeaderHystrixConcurrencyStrategy() {
        try {
            this.delegate = HystrixPlugins.getInstance().getConcurrencyStrategy();
            if (this.delegate instanceof TransferHeaderHystrixConcurrencyStrategy) {
                return;
            }
            HystrixCommandExecutionHook commandExecutionHook =
                    HystrixPlugins.getInstance().getCommandExecutionHook();
            HystrixEventNotifier eventNotifier = HystrixPlugins.getInstance().getEventNotifier();
            HystrixMetricsPublisher metricsPublisher = HystrixPlugins.getInstance().getMetricsPublisher();
            HystrixPropertiesStrategy propertiesStrategy =
                    HystrixPlugins.getInstance().getPropertiesStrategy();
            this.logCurrentStateOfHystrixPlugins(eventNotifier, metricsPublisher, propertiesStrategy);
            HystrixPlugins.reset();
            HystrixPlugins.getInstance().registerConcurrencyStrategy(this);
            HystrixPlugins.getInstance().registerCommandExecutionHook(commandExecutionHook);
            HystrixPlugins.getInstance().registerEventNotifier(eventNotifier);
            HystrixPlugins.getInstance().registerMetricsPublisher(metricsPublisher);
            HystrixPlugins.getInstance().registerPropertiesStrategy(propertiesStrategy);
        } catch (Exception e) {
            log.error("Failed to register Sleuth Hystrix Concurrency Strategy", e);
        }
    }

    private void logCurrentStateOfHystrixPlugins(HystrixEventNotifier eventNotifier,
                                                 HystrixMetricsPublisher metricsPublisher, HystrixPropertiesStrategy propertiesStrategy) {
        if (log.isDebugEnabled()) {
            log.debug("Current Hystrix plugins configuration is [" + "concurrencyStrategy ["
                    + this.delegate + "]," + "eventNotifier [" + eventNotifier + "]," + "metricPublisher ["
                    + metricsPublisher + "]," + "propertiesStrategy [" + propertiesStrategy + "]," + "]");
            log.debug("Registering Sleuth Hystrix Concurrency Strategy.");
        }
    }

    @Override
    public BlockingQueue<Runnable> getBlockingQueue(int maxQueueSize) {
        return delegate != null
                ? delegate.getBlockingQueue(maxQueueSize)
                : super.getBlockingQueue(maxQueueSize);
    }

    @Override
    public <T> HystrixRequestVariable<T> getRequestVariable(
            HystrixRequestVariableLifecycle<T> rv) {
        return delegate != null
                ? delegate.getRequestVariable(rv)
                : super.getRequestVariable(rv);
    }

    @Override
    public ThreadPoolExecutor getThreadPool(HystrixThreadPoolKey threadPoolKey,
                                            HystrixProperty<Integer> corePoolSize,
                                            HystrixProperty<Integer> maximumPoolSize,
                                            HystrixProperty<Integer> keepAliveTime, TimeUnit unit,
                                            BlockingQueue<Runnable> workQueue) {
        return delegate != null
                ? delegate.getThreadPool(threadPoolKey, corePoolSize,
                maximumPoolSize, keepAliveTime, unit, workQueue)
                : super.getThreadPool(threadPoolKey, corePoolSize, maximumPoolSize,
                keepAliveTime, unit, workQueue);
    }

    @Override
    public ThreadPoolExecutor getThreadPool(HystrixThreadPoolKey threadPoolKey,
                                            HystrixThreadPoolProperties threadPoolProperties) {
        return delegate != null
                ? delegate.getThreadPool(threadPoolKey,
                threadPoolProperties)
                : super.getThreadPool(threadPoolKey, threadPoolProperties);
    }

    @Override
    public <T> Callable<T> wrapCallable(Callable<T> callable) {

        if (callable instanceof TransferCallable) {
            return callable;
        }
        Callable<T> wrappedCallable = this.delegate != null
                ? this.delegate.wrapCallable(callable) : callable;
        if (wrappedCallable instanceof TransferCallable) {
            return wrappedCallable;
        }
        //将当前请求信息拿到,放入包装类所在线程中
        RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
        return new TransferCallable<>(wrappedCallable, requestAttributes);
    }


    static class TransferCallable<T> implements Callable<T> {
        private final Callable<T> delegate;

        private final RequestAttributes requestAttributes;


        public TransferCallable(Callable<T> delegate, RequestAttributes requestAttributes) {
            this.requestAttributes = requestAttributes;
            this.delegate = delegate;
        }

        @Override
        public T call() throws Exception {
            try {
                //将当前请求信息拿到,放入包装类所在线程中
                RequestContextHolder.setRequestAttributes(requestAttributes);
                return delegate.call();
            } finally {
                RequestContextHolder.resetRequestAttributes();
            }
        }
    }

}

3.2.4 NetWorkUtils

import javax.servlet.http.HttpServletRequest;
import java.net.InetAddress;

/**
 * @author zhangbowen
 */
public class NetworkUtil {
    public static final String HEADER_X_FORWARDED_FOR = "X-Forwarded-For";
    public static final String PROXY_CLIENT_IP = "Proxy-Client-IP";
    public static final String WL_PROXY_CLIENT_IP = "WL-Proxy-Client-IP";
    public static final String HTTP_CLIENT_IP = "HTTP_CLIENT_IP";
    public static final String HTTP_X_FORWARD_IP = "HTTP_X_FORWARDED_FOR";


    /**
     * 获取请求主机IP地址,如果通过代理进来,则透过防火墙获取真实IP地址;
     *
     * @param request 请求
     * @return ip
     */
    public static String getIp(HttpServletRequest request) {
        try {
            // 获取请求主机IP地址,如果通过代理进来,则透过防火墙获取真实IP地址
            String ip = request.getHeader(HEADER_X_FORWARDED_FOR);

            if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
                if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
                    ip = request.getHeader(PROXY_CLIENT_IP);
                }
                if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
                    ip = request.getHeader(WL_PROXY_CLIENT_IP);
                }
                if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
                    ip = request.getHeader(HTTP_CLIENT_IP);
                }
                if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
                    ip = request.getHeader(HTTP_X_FORWARD_IP);
                }
                if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
                    ip = request.getRemoteAddr();
                }
            } else if (ip.length() > 15) {
                String[] ips = ip.split(",");
                for (String ip1 : ips) {
                    if (!("unknown".equalsIgnoreCase(ip1))) {
                        ip = ip1;
                        break;
                    }
                }
            }
            if (ip.equals("0:0:0:0:0:0:0:1")) {
                ip = InetAddress.getLocalHost().getHostAddress();
            }
            return ip;
        } catch (Exception e) {
            return "";
        }
    }
}

3.2.5 在拦截器的header中放入真实ip

实现拦截器,将包装类所在线程中的信息,拿出放入header中传递。

import feign.RequestInterceptor;
import feign.RequestTemplate;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;

/**
 * @author zhangbowen
 * @since 2019-06-20
 * 转发ip
 */
public class TransferClientIpRequestInterceptor implements RequestInterceptor {
    @Override
    public void apply(RequestTemplate template) {
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder
                .getRequestAttributes();
        if (attributes == null) {
            return;
        }
        HttpServletRequest request = attributes.getRequest();
        //放入ip
        template.header(NetworkUtil.HEADER_X_FORWARDED_FOR, NetworkUtil.getIp(request));
    }
}

3.2.6 实现client,重写url

我们需要实现自己的client,以便由redis中读取所配置的代理信息,并且重写url。
请注意,由于某些方法需要在feign包下才可使用,故我们建立与feign包一致的包名。

package org.springframework.cloud.openfeign.ribbon;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.TypeReference;
import com.netflix.client.ClientException;
import com.netflix.client.config.CommonClientConfigKey;
import com.netflix.client.config.DefaultClientConfigImpl;
import com.netflix.client.config.IClientConfig;
import feign.Client;
import feign.Request;
import feign.Response;
import org.springframework.cloud.netflix.ribbon.SpringClientFactory;
import org.springframework.data.redis.core.BoundHashOperations;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;

import java.io.IOException;
import java.net.URI;
import java.util.Collections;
import java.util.Map;

/**
 * @author zhangbowen
 * 设置开启该配置的开关,以便在生产环境不开启代理。
 * 否则,注入原有客户端,不进行覆盖。
 */
public class ProxyForwardLoadBalancerFeignClient implements Client {
    public static final String CLIENT_PROXY_FORWARD_KEY = "feign-client-proxy-forward";

    static final Request.Options DEFAULT_OPTIONS = new Request.Options();

    private final Client delegate;

    private CachingSpringLoadBalancerFactory lbClientFactory;

    private SpringClientFactory clientFactory;

    public ProxyForwardLoadBalancerFeignClient(Client delegate,
                                               CachingSpringLoadBalancerFactory lbClientFactory,
                                               SpringClientFactory clientFactory) {
        this.delegate = delegate;
        this.lbClientFactory = lbClientFactory;
        this.clientFactory = clientFactory;
    }

    static URI cleanUrl(String originalUrl, String host) {
        String newUrl = originalUrl;
        if (originalUrl.startsWith("https://")) {
            newUrl = originalUrl.substring(0, 8)
                    + originalUrl.substring(8 + host.length());
        } else if (originalUrl.startsWith("http")) {
            newUrl = originalUrl.substring(0, 7)
                    + originalUrl.substring(7 + host.length());
        }
        StringBuffer buffer = new StringBuffer(newUrl);
        if ((newUrl.startsWith("https://") && newUrl.length() == 8)
                || (newUrl.startsWith("http://") && newUrl.length() == 7)) {
            buffer.append("/");
        }
        return URI.create(buffer.toString());
    }

    static URI replaceHostName(String originalUrl, String host, String newHost) {
        String newUrl = originalUrl;
        if (originalUrl.startsWith("https://")) {
            newUrl = originalUrl.substring(0, 8)
                    + newHost
                    + originalUrl.substring(8 + host.length());
        } else if (originalUrl.startsWith("http")) {
            newUrl = originalUrl.substring(0, 7)
                    + newHost
                    + originalUrl.substring(7 + host.length());
        }
        StringBuffer buffer = new StringBuffer(newUrl);
        if ((newUrl.startsWith("https://") && newUrl.length() == 8)
                || (newUrl.startsWith("http://") && newUrl.length() == 7)) {
            buffer.append("/");
        }
        return URI.create(buffer.toString());
    }

    /**
     * 根据请求获取该请求的代理,如果为null,请求源路径
     *
     * @param request
     * @return
     */
    private FeignClientProxyForward getProxyForward(Request request, String clientName) {
        try {
            String requestIp = request.headers().getOrDefault(NetworkUtil.HEADER_X_FORWARDED_FOR, Collections.emptyList()).iterator().next();
            BoundHashOperations<String, String, String> boundHashOperations = RedisHelper.template().boundHashOps(CLIENT_PROXY_FORWARD_KEY);
            //获取代理json列表
            String clientProxyTableJson = boundHashOperations.get(requestIp);
            //代理列表为空
            if (StringUtils.isEmpty(clientProxyTableJson)) {
                //走原方法
                return null;
            }
            Map<String, FeignClientProxyForward> clientProxyTable = JSON.parseObject(clientProxyTableJson, new TypeReference<Map<String, FeignClientProxyForward>>() {
            });
            if (CollectionUtils.isEmpty(clientProxyTable)) {
                //走原方法
                return null;
            }
            return clientProxyTable.get(clientName.toLowerCase());
        } catch (Exception e) {
            return null;
        }
    }

    @Override
    public Response execute(Request request, Request.Options options) throws IOException {
        //获取当前请求方的ip。
        //根据ip获取该ip设置的代理列表,判断是否存在当前clientName。
        //如果存在,重写url,转向访问代理。
        try {
            URI asUri = URI.create(request.url());
            String clientName = asUri.getHost();
            //判断该次请求的目标服务是否代理
            FeignClientProxyForward proxyForward = getProxyForward(request, clientName);
            //如果目标请求未代理,走原请求
            if (proxyForward == null) {
                //走原方法
                return original(request, options);
            }
            //如果被代理,修改代理的ip,重新构建URI
            URI uriWithoutHost = replaceHostName(request.url(), clientName, proxyForward.getTargetIp() + ":" + proxyForward.getTargetPort());
            FeignLoadBalancer.RibbonRequest ribbonRequest = new FeignLoadBalancer.RibbonRequest(
                    this.delegate, request, uriWithoutHost);
            IClientConfig requestConfig = getClientConfig(options, clientName);
            return lbClient(clientName)
                    .executeWithLoadBalancer(ribbonRequest, requestConfig).toResponse();
        } catch (ClientException e) {
            IOException io = findIOException(e);
            if (io != null) {
                throw io;
            }
            throw new RuntimeException(e);
        }
    }

    public Response original(Request request, Request.Options options) throws IOException {
        try {
            URI asUri = URI.create(request.url());
            String clientName = asUri.getHost();
            URI uriWithoutHost = cleanUrl(request.url(), clientName);
            FeignLoadBalancer.RibbonRequest ribbonRequest = new FeignLoadBalancer.RibbonRequest(
                    this.delegate, request, uriWithoutHost);

            IClientConfig requestConfig = getClientConfig(options, clientName);
            return lbClient(clientName)
                    .executeWithLoadBalancer(ribbonRequest, requestConfig).toResponse();
        } catch (ClientException e) {
            IOException io = findIOException(e);
            if (io != null) {
                throw io;
            }
            throw new RuntimeException(e);
        }
    }


    IClientConfig getClientConfig(Request.Options options, String clientName) {
        IClientConfig requestConfig;
        if (options == DEFAULT_OPTIONS) {
            requestConfig = this.clientFactory.getClientConfig(clientName);
        } else {
            requestConfig = new FeignOptionsClientConfig(options);
        }
        return requestConfig;
    }

    protected IOException findIOException(Throwable t) {
        if (t == null) {
            return null;
        }
        if (t instanceof IOException) {
            return (IOException) t;
        }
        return findIOException(t.getCause());
    }

    public Client getDelegate() {
        return this.delegate;
    }

    private FeignLoadBalancer lbClient(String clientName) {
        return this.lbClientFactory.create(clientName);
    }

    static class FeignOptionsClientConfig extends DefaultClientConfigImpl {

        FeignOptionsClientConfig(Request.Options options) {
            setProperty(CommonClientConfigKey.ConnectTimeout,
                    options.connectTimeoutMillis());
            setProperty(CommonClientConfigKey.ReadTimeout, options.readTimeoutMillis());
        }

        @Override
        public void loadProperties(String clientName) {

        }

        @Override
        public void loadDefaultValues() {

        }

    }

}

3.2.7 RequestProxyForwardAutoConfiguration

import feign.Client;
import feign.httpclient.ApacheHttpClient;
import org.apache.http.client.HttpClient;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.cloud.netflix.ribbon.SpringClientFactory;
import org.springframework.cloud.openfeign.ribbon.CachingSpringLoadBalancerFactory;
import org.springframework.cloud.openfeign.ribbon.ProxyForwardLoadBalancerFeignClient;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * @author zhangbowen
 * @since 2019-06-20
 */
@Configuration
public class RequestProxyForwardAutoConfiguration {

    @Bean
    public Client feignClient(CachingSpringLoadBalancerFactory cachingFactory,
                              SpringClientFactory clientFactory, HttpClient httpClient) {
        ApacheHttpClient delegate = new ApacheHttpClient(httpClient);
        return new ProxyForwardLoadBalancerFeignClient(delegate, cachingFactory, clientFactory);
    }

    @Bean
    public TransferHeaderHystrixConcurrencyStrategy transferHeaderHystrixConcurrencyStrategy() {
        return new TransferHeaderHystrixConcurrencyStrategy();
    }

    @Bean
    public TransferClientIpRequestInterceptor transferHeaderRequestInterceptor() {
        return new TransferClientIpRequestInterceptor();
    }
}

3.3 管理端

基础设施搭建完以后,我们需要有一个简单的管理端来管理我们的代理服务。
这里我们使用bootstrap简单搭建。

3.3.1 增删改查接口

import com.whdx.framework.web.bean.MessageBody;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

/**
 * @author zhangbowen
 * @since 2019-06-20
 */
@RequestMapping("/proxy")
@RestController
public class RequestProxyForwardController {
    @Autowired
    private RequestProxyForwardService proxyForwardService;


    /**
     * 列表数据
     */
    @GetMapping("/list")
    public MessageBody list() {
        return proxyForwardService.list();
    }

    /**
     * 设置数据
     */
    @PostMapping("/put")
    public MessageBody put(FeignClientProxyForward feignClientProxyForward) {
        return proxyForwardService.put(feignClientProxyForward);
    }

    /**
     * 清空某个ip数据
     */
    @DeleteMapping("/clearByIp")
    public MessageBody clearByIp() {
        return proxyForwardService.clearByIp();
    }

    /**
     * 根据服务名与ip删除
     */
    @DeleteMapping("/deleteWithTargetNameAndIp")
    public MessageBody deleteWithTargetNameAndIp(FeignClientProxyForward feignClientProxyForward) {
        return proxyForwardService.deleteWithTargetNameAndIp(feignClientProxyForward);
    }


    /**
     * 清空全部数据
     */
    @DeleteMapping("/clearAll")
    public MessageBody clearAll() {
        return proxyForwardService.clearAll();
    }
}

3.3.2 RequestProxyForwardService

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.TypeReference;
import org.springframework.data.redis.core.BoundHashOperations;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

/**
 * @author zhangbowen
 * @since 2019-06-20
 */
@Service
public class RequestProxyForwardService {
    public static final String CACHE_KEY = "feign-client-proxy-forward";

    /**
     * 全部数据
     *
     * @return
     */
    public MessageBody list() {
        BoundHashOperations<String, String, String> boundHashOperations = RedisHelper.template().boundHashOps(CACHE_KEY);
        MessageBody<List<FeignClientProxyForward>> messageBody = new MessageBody<>();
        List<String> list = boundHashOperations.values();
        if (CollectionUtils.isEmpty(list)) {
            messageBody.setRetData(new ArrayList<>());
            return messageBody;
        }
        List<FeignClientProxyForward> result = list.stream().flatMap(item -> JSON.parseObject(item, new TypeReference<Map<String, FeignClientProxyForward>>() {
        }).values().stream()).collect(Collectors.toList());
        messageBody.setRetData(result);
        return messageBody;
    }


    /**
     * 设置数据
     * redis map 各key含义: CACHE_KEY,clientIp,serviceName
     * @return
     */
    public MessageBody put(FeignClientProxyForward feignClientProxyForward) {
        feignClientProxyForward.setServiceName(feignClientProxyForward.getServiceName().toLowerCase());
        BoundHashOperations<String, String, String> boundHashOperations = RedisHelper.template().boundHashOps(CACHE_KEY);
        String clientProxyMapJson = boundHashOperations.get(feignClientProxyForward.getClientIp());
        Map<String, FeignClientProxyForward> clientProxyMap;
        if (clientProxyMapJson == null) {
            clientProxyMap = new HashMap<>();
        } else {
            clientProxyMap = JSON.parseObject(clientProxyMapJson, new TypeReference<Map<String, FeignClientProxyForward>>() {
            });
        }
        clientProxyMap.put(feignClientProxyForward.getServiceName(), feignClientProxyForward);
        boundHashOperations.put(feignClientProxyForward.getClientIp(), JSON.toJSONString(clientProxyMap));
        return new MessageBody();
    }

    /**
     * 清空某个ip下的代理
     *
     * @return
     */
    public MessageBody clearByIp() {
        BoundHashOperations<String, String, String> boundHashOperations = RedisHelper.template().boundHashOps(CACHE_KEY);
        boundHashOperations.delete(WebContextFacade.getRequestContext().getIp());
        return new MessageBody();
    }

    /**
     * 根据ip+serviceName删除
     *
     * @return
     */
    public MessageBody deleteWithTargetNameAndIp(FeignClientProxyForward feignClientProxyForward) {
        BoundHashOperations<String, String, String> boundHashOperations = RedisHelper.template().boundHashOps(CACHE_KEY);
        String clientProxyJson = boundHashOperations.get(feignClientProxyForward.getClientIp());
        Map<String, FeignClientProxyForward> clientProxyMap;
        if (clientProxyJson == null) {
            return new MessageBody();
        } else {
            clientProxyMap = JSON.parseObject(clientProxyJson, new TypeReference<Map<String, FeignClientProxyForward>>() {
            });
        }
        String key = feignClientProxyForward.getServiceName();
        if (clientProxyMap == null || !clientProxyMap.containsKey(key)) {
            return new MessageBody();
        }
        clientProxyMap.remove(key);
        boundHashOperations.put(feignClientProxyForward.getClientIp(), JSON.toJSONString(clientProxyMap));
        return new MessageBody();
    }

    /**
     * 清空全部
     *
     * @return
     */
    public MessageBody clearAll() {
        RedisHelper.template().delete(CACHE_KEY);
        return new MessageBody();
    }
}

3.3.3 FeignClientProxyForward

import lombok.Data;

/**
 * @author zhangbowen
 * @since 2019-06-20
 */
@Data
public class FeignClientProxyForward {
    /**
     * 客户端ip
     */
    private String clientIp;
    /**
     * 代理服务
     */
    private String serviceName;
    /**
     * 目标ip
     */
    private String targetIp;
    /**
     * 目标端口
     */
    private String targetPort;
}

3.3.4 RequestProxyForwardViewController

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;

/**
 * @author zhangbowen
 * @since 2019-06-20
 */
@RequestMapping("/proxy")
@Controller
public class RequestProxyForwardViewController {

    /**
     * 主页
     *
     * @return
     */
    @GetMapping
    public String index(Model model) {
        model.addAttribute("clientIp", WebContextFacade.getRequestContext().getIp());
        return "proxyIndex";
    }
}

3.3.5 proxyIndex.ftl

页面中引用的js和css大家自行寻找吧。
bootstrap4.x

<!DOCTYPE html>
<html>
<head>
    <link href="/static/bootstrap-table.min.css" rel="stylesheet">
    <link href="/static/bootstrap.min.css" rel="stylesheet">
    <script src="/static/jquery-3.4.1.min.js"></script>
    <script src="/static/bootstrap.min.js"></script>
    <script src="/static/bootstrap-table.min.js"></script>
    <script src="/static/bootstrap-table-zh-CN.min.js"></script>
    <script src="/static/layer.js"></script>
    <style>
        body {
            margin: 20px;
        }

        .title {
            text-align: center;
        }

        .op {
            margin-top: 20px;
        }

        .op button {
            margin-right: 10px;
        }

        .content {
            margin-top: 20px;
        }

        .layui-layer-btn0 {
            color: white !important;
        }
    </style>
</head>
<div class="title">
    <h1>微服务代理配置中心</h1>
</div>

<div class="op">
    <button type="button" data-toggle="modal" onclick="openAddProxyModal()" class="btn btn-outline-primary">添加代理
    </button>
    <button type="button" data-toggle="modal" onclick="clearProxyIp()" class="btn btn-outline-danger">清空本机ip代理
    </button>
    <button type="button" data-toggle="modal" onclick="clearAllProxy()" class="btn btn-outline-danger">清空全部代理
    </button>
</div>
<div class="content">
    <table
            id="table"
            data-toggle="table"
            data-search="true"
            data-locale="zh-CN"
    >
        <thead>
        <tr>
            <th data-field="clientIp">客户端ip</th>
            <th data-field="serviceName">代理服务</th>
            <th data-field="targetIp">目标ip</th>
            <th data-field="targetPort">目标端口</th>
            <th data-field="operate" data-formatter="operateFormatter" data-events="operateEvents">操作</th>
        </tr>
        </thead>
    </table>
</div>
<div>
    <div class="modal fade" id="addProxy" tabindex="-1" role="dialog" aria-hidden="true">
        <div class="modal-dialog" role="document">
            <div class="modal-content">
                <div class="modal-header">
                    <h5 class="modal-title">添加代理</h5>
                    <button type="button" class="close" data-dismiss="modal" aria-label="Close">
                        <span aria-hidden="true">&times;</span>
                    </button>
                </div>
                <div class="modal-body">
                    <form>
                        <div class="form-group row">
                            <label for="clientIp" class="col-md-3 col-form-label">客户端ip</label>
                            <div class="col-md-6">
                                <input type="text" class="form-control" id="clientIp">
                            </div>
                        </div>
                        <div class="form-group row">
                            <label for="targetIp" class="col-md-3 col-form-label">目标ip</label>
                            <div class="col-md-6">
                                <input type="text" class="form-control" id="targetIp">
                            </div>
                        </div>
                        <div class="form-group row">
                            <label for="targetPort" class="col-md-3 col-form-label">目标端口</label>
                            <div class="col-md-6">
                                <input type="text" class="form-control" id="targetPort">
                            </div>
                        </div>
                        <div class="form-group row">
                            <label for="serviceName" class="col-md-3 col-form-label">代理服务</label>
                            <div class="col-md-6">
                                <input type="text" class="form-control" id="serviceName">
                            </div>
                        </div>
                    </form>
                </div>
                <div class="modal-footer">
                    <button type="button" class="btn btn-secondary" data-dismiss="modal">关闭</button>
                    <button type="button" class="btn btn-primary" id="saveProxy">确定</button>
                </div>
            </div>
        </div>
    </div>
</div>
<script>
    window.operateEvents = {
        'click .deleteOne': function (e, value, row, index) {
            layer.confirm('确定删除本条代理?', function (index) {
                $.ajax({
                    type: "DELETE",
                    url: "/proxy/deleteWithTargetNameAndIp",
                    data:{
                        clientIp: row.clientIp,
                        serviceName: row.serviceName
                    },
                    dataType: "json",
                    success: function () {
                        layer.close(index);
                        layer.msg("删除成功");
                        setTimeout(function(){loadData();},100);
                    }
                });

            });
        }
    }
    var $table = $('#table');
    function operateFormatter(value, row, index) {
        return [
            '<button type="button" data-toggle="modal" class="deleteOne btn btn-outline-danger">删除</button>'
        ].join('')
    }
    function clearProxyIp() {
        layer.confirm('确定清空本机全部代理?', function (index) {
            $.ajax({
                type: "DELETE",
                url: "/proxy/clearByIp",
                dataType: "json",
                success: function () {
                    layer.close(index);
                    layer.msg("清理成功");
                    setTimeout(function(){loadData();},100);
                }
            });

        });
    }

    function clearAllProxy() {
        layer.confirm('是否清空全部代理?(不可还原)', function (index) {
            $.ajax({
                type: "DELETE",
                url: "/proxy/clearAll",
                dataType: "json",
                success: function () {
                    layer.close(index);
                    layer.msg("清理成功");
                    setTimeout(function(){loadData();},100);
                }
            });

        });
    }

    function openAddProxyModal() {
        $("#clientIp").val("${clientIp}");
        $("#targetIp").val("");
        $("#targetName").val("");
        $("#targetPort").val("");
        $('#addProxy').modal('show')
    }

    function loadData() {
        $.get("/proxy/list", function (res) {
            var list = res.retData;
            $table.bootstrapTable('load', list)
        })
    }

    $(function () {
        loadData();
        $("#saveProxy").on("click", function () {
            var clientIp = $("#clientIp").val();
            var targetIp = $("#targetIp").val();
            var targetPort = $("#targetPort").val();
            var serviceName = $("#serviceName").val();

            if (!(clientIp && targetIp && serviceName && targetPort)) {
                layer.msg("请完善信息");
                return false;
            }
            $.post("/proxy/put", {
                "clientIp": clientIp,
                "targetIp": targetIp,
                "targetPort": targetPort,
                "serviceName": serviceName,
            }, function (res) {
                layer.msg("保存成功");
                $('#addProxy').modal('hide');
                loadData();
            });
        });
    })
</script>
</html>

3.4 快速使用

进入页面后,点击左上方添加代理。在弹出的页面中录入相关数据。
代理添加成功后,本地客户端正常请求接口即可。

添加.png

客户端ip:这里的客户端指的是最初的访问者,即打开浏览器或打开某个应用的用户所使用的终端ip。
目标ip:要访问的机器的ip地址,即某个后端开发人员的机器ip。
目标端口:要访问的机器端口,即某个后端开发人员所开启的应用端口。端口号为数字,如:8080
代理服务:要代理的开发环境服务名称,如:paper、common、user。具体某个服务经过链路最终所到达的服务名称需询问相关后端人员。

全部服务名可以从此地址查看:eureka。进入后,application即服务名

添加相同ip、服务名的代理,即会覆盖已经存在的代理。

3.4.1 筛选代理

在页面右上方可以进行筛选。


列表.png

3.4.2 清空本机代理

点击按钮可以清空本机ip所设置的全部代理,联调结束后,请及时清空代理,以免为后续调试造成影响。

3.4.3 清空全部代理

点击按钮可以清空全部代理,请谨慎使用。

3.4.4 删除单个代理

在表格后方操作栏中可删除单个代理。


删除.png

想不想看看墙外的世界
高质量图片压缩工具

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,992评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,212评论 3 388
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,535评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,197评论 1 287
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,310评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,383评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,409评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,191评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,621评论 1 306
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,910评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,084评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,763评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,403评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,083评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,318评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,946评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,967评论 2 351