SpringBoot集成FastDFS+Nginx整合基于Token的防盗链

为什么要用SpringBoot?

SpringBoot是由Pivotal团队提供的全新框架,其设计目的是用来简化新Spring应用的初始搭建以及开发过程。该框架使用了特定的方式来进行配置,从而使开发人员不再需要定义样板化的配置。通过这种方式,Spring Boot致力于在蓬勃发展的快速应用开发领域(rapid application development)成为领导者。

  • 创建独立的Spring应用程序

  • 嵌入的Tomcat,无需部署WAR文件

  • 简化Maven配置

  • 自动配置Spring

  • 提供生产就绪型功能,如指标,健康检查和外部配置

  • 绝对没有代码生成并且对XML也没有配置要求

为什么要用Nginx?

  • 概述

    Nginx(engine x)是一个开源的,支持高并发的www服务和代理服务软件。Nginx是俄罗斯人Igor Sysoev开发的,最初被应用到俄罗斯的大型网站(www.rambler.ru)上。后来作者将源代码以类BSD许可证的形式开源出来供全球使用。在功能应用方面,Nginx不仅是一个优秀的Web服务软件,还具有反向代理负载均衡和缓存的功能。在反向代理负载均衡方面类似于LVS负载均衡及HAProxy等你专业代理软件。Nginx部署起来更加方便简单,在缓存服务功能方面,有类似于Squid等专业的缓存服务软件。Nginx可以运行在UNIX、Linux、MS Windows Server、Mac OS X Server、Solaris等操作系统中。

  • Nginx的重要特性

    1. 可以针对静态资源高速节点并发访问及缓存。
    2. 可以使用反向代理加速,并且可以进行数据缓存。
    3. 具有简单负载均衡,节点健康检查和容错功能。
    4. 支持远程Fast CGI服务的缓存加速。
    5. 支持Fast CGI、Uwsgi、SCGI、Memcached Server的加速和缓存。
    6. 支持SSL、TLS、SNI。
    7. 具有模块化的架构。
    8. 过滤器包括gzip压缩、ranges支持、chunked响应、XSLT、SSL和图像缩放等功能。
    9. 在SSL过滤器中,包含多个SSL页面,如果经由Fast CGI或反向代理处理,可以并行处理。
  • Nginx所具备的WWW服务特性

    1. 支持基于域名、端口和IP的虚拟主机配置。
    2. 支持KeepAlived和piplined连接。
    3. 可进行简单、方便、灵活的配置和管理。
    4. 支持修改Nginx配置,并且在代码上线时,可平滑重启,不中断业务访问。
    5. 可自定义访问日志格式,临时缓冲写日志操作,快速日志轮询及通过rsyslog处理日志。
    6. 可利用信号控制Nginx进程。
    7. 支持3xx-5xxHTTP状态码重定向。
    8. 支持rewrite模块,支持URI重写及正则表达式匹配。
    9. 支持基于客户端IP地址和HTTP基本认证的访问控制。
    10. 支持PUT、DELETE、MKCOL、COPY、MOVE等特殊的HTTP请求方法。
    11. 支持FLV流和MP4流技术产品应用。
    12. 支持HTTP响应速率限制。
    13. 支持同一IP地址的并发连接或请求限制。
    14. 支持邮件服务代理。
    15. 支持高并发,可以支持几百万并发连接。
    16. 资源消耗少,在3万并发连接下,可以开启10个nginx的线程消耗的内存不到200MB。
    17. 可以做HTTP反向代理及加速缓存,及负载均衡功能,内置对RS节点服务器健康检查功能,折现但能够与专业的HAProxy或LVS的功能。
    18. 具备Squid等专业缓存软件等的缓存功能。
    19. 支持异步网络I/O事件模型epoll(Linux2.6+)。
  • Nginx软件主要企业应用

    1. 作为Web服务软件。
    2. 使用Nginx运行HTML、JS、CSS、小图片等静态数据(类似于Lighttpd)。
    3. 结合Fast CGI运行PHP等动态程序(例如使用fastcgi_pass方式)。
    4. Nginx结合Tomcat/Resin等支持Java动态程序(常用proxy_pass)。
    5. 反向代理或负载均衡服务(Nginx从1.9.0开始就开始支持TCP的代理了)。
    6. 前端业务数据缓存服务。
  • Web服务应用产品性能对比

    1. 静态数据的访问上:处理小文件(小于1MB)时,Nginx和Lighttpd比Apache更有优势,Nginx处理小文件的优势明显,Lighttpd综合最强。
    2. 动态数据的访问上:三者差距不大,Apache更有优势,因为处理动态数据的能力在于PHP(Java)和后端数据库的服务能力,也就是说瓶颈不在Web服务器上。
    3. 一般情况下普通PHP引擎支持的并发连接参考值3001000。Java引擎和数据库的并发连接参考值3001500。
  • 为什么Nginx比Apache的性能高?

    1. Nginx使用最新版的eepoll(Linux 2.6内核)和kqueue(FreeBSD)异步网络I/O模型,而Apache使用的是传统的select模型。
    2. 目前Linux下能够承受高并发访问的Squid、Memcached软件采用都是epoll模型。
    3. 处理大量的连接的读写时,Apache所采用的select网络I/O模型比较低。
  • 如何正确采用Web服务器?

    1. 静态业务:如果是高并发场景,尽量采用Nginx或Lighttpd,二者首选Nginx。
    2. 动态业务:理论上采用Nginx和Apache均可,建议使用Nginx,为了避免相同业务服务的软件多样化,增加维护成本,动态业务可以使用Nginx兼做前端代理,再根据页面的元素或目录转发到其他的服务器进行处理。
    3. 既有动态业务又有静态业务,就用Nginx。

关于部署,就不在重复了,如果需要请移步《Java高级架构之FastDFS分布式文件集群》https://blog.51cto.com/xvjunjie/2377669

使用IDEA场景启动器创建工程

  • 创建Maven工程,修改POM.xml文件添加如下依赖:
<dependencies>
    <!-- SpringBoot的自动配置相关依赖 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-autoconfigure</artifactId>
        <version>1.5.20.RELEASE</version>
    </dependency>

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-configuration-processor</artifactId>
        <version>1.5.20.RELEASE</version>
    </dependency>
    <!-- 日志相关的依赖 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-logging</artifactId>
        <version>1.5.20.RELEASE</version>
    </dependency>
    <!-- 对象池相关的依赖 -->
    <dependency>
        <groupId>org.apache.commons</groupId>
        <artifactId>commons-pool2</artifactId>
        <version>2.6.0</version>
    </dependency>

</dependencies>

创建必要的包

  • annotation:存放相关的注解

  • autoconfiguation: 存储自动配置类

  • factory: 存放工厂类

  • properties: 存放配置参数类

  • service: 存放服务类

一般情况下,SpringBoot都会提供相应的@EnableXxx注解标注在应用的主启动类上开启某个功能:

// EnableFastdfsClient.java
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Import(FastdfsAutoConfiguration.class)
@Documented
public @interface EnableFastdfsClient {
}

下面是相关的自动配置类:

// FastdfsAutoConfiguration.java
@Configuration
@EnableConfigurationProperties(FastdfsProperties.class)
public class FastdfsAutoConfiguration {
    @Autowired
    private FastdfsProperties fastdfsProperties;

    @Bean
    @ConditionalOnMissingBean(FastdfsClientService.class)
    public FastdfsClientService fastdfsClientService() throws Exception {
        return new FastdfsClientService(fastdfsProperties);
    }
}

创建相关的工厂类:

// StorageClientFactory.java
// 用于创建连接对象的工厂类
public class StorageClientFactory implements PooledObjectFactory<StorageClient> {

    @Override
    public PooledObject<StorageClient> makeObject() throws Exception {
        TrackerClient client = new TrackerClient();
        TrackerServer server = client.getConnection();
        return new DefaultPooledObject<>(new StorageClient(server, null));
    }

    @Override
    public void destroyObject(PooledObject<StorageClient> p) throws Exception {
        p.getObject().getTrackerServer().close();
    }

    @Override
    public boolean validateObject(PooledObject<StorageClient> p) {
        return false;
    }

    @Override
    public void activateObject(PooledObject<StorageClient> p) throws Exception {

    }

    @Override
    public void passivateObject(PooledObject<StorageClient> p) throws Exception {

    }
}

Properties类用来映射application.properties或者application.yml配置文件:

// FastdfsProperties.java
@ConfigurationProperties(prefix = "fastdfs")
public class FastdfsProperties {
    // 连接超时时间
    // 网络超时时间
    // 字符集编码
    // 是否使用Token
    // Token加密密钥
    // 跟踪器IP地址,多个使用分号隔开
    // 连接池的连接对象最大个数
    // 连接池的最大空闲对象个数
    // 连接池的最小空闲对象个数
    // Nginx服务器IP,多个使用分号分割
    // 获取连接对象时可忍受的等待时长(毫秒)
    private String connectTimeout = "5";
    private String networkTimeout = "30";
    private String charset = "UTF-8";
    private String httpAntiStealToken = "false";
    private String httpSecretKey = "";
    private String httpTrackerHttpPort = "";
    private String trackerServers = "";
    private String connectionPoolMaxTotal = "18";
    private String connectionPoolMaxIdle = "18";
    private String connectionPoolMinIdle = "2";
    private String nginxServers = "";

    // 需要创建相关的Setter和Getter方法
}

在Service类中封装方法, 下面仅展示3个常用的方法:

// FastdfsClientSerivce.java
public class FastdfsClientService {
    // SpringBoot加载的配置文件
    // 连接池配置项
    // 转换后的配置条目
    // 连接池
    // Nginx服务器地址
    private FastdfsProperties fdfsProp;
    private GenericObjectPoolConfig config;
    private Properties prop;
    private GenericObjectPool<StorageClient> pool;
    private String[] nginxServers;
    private Logger logger;

    public FastdfsClientService(FastdfsProperties fdfsProp) throws Exception {
        this.fdfsProp = fdfsProp;
        this.logger = LoggerFactory.getLogger(getClass());
        init();
        create();
        info();
    }

    /**
     * 初始化全局客户端
     */
    private void init() throws Exception {
        this.prop = new Properties();
        this.logger.info("FastDFS: reading config file...");
        this.logger.info("FastDFS: fastdfs.connect_timeout_in_seconds=" + this.fdfsProp.getConnectTimeout());
        this.logger.info("FastDFS: fastdfs.network_timeout_in_seconds=" + this.fdfsProp.getNetworkTimeout());
        this.logger.info("FastDFS: fastdfs.charset=" + this.fdfsProp.getCharset());
        this.logger.info("FastDFS: fastdfs.http_anti_steal_token=" + this.fdfsProp.getHttpAntiStealToken());
        this.logger.info("FastDFS: fastdfs.http_secret_key=" + this.fdfsProp.getHttpSecretKey());
        this.logger.info("FastDFS: fastdfs.http_tracker_http_port=" + this.fdfsProp.getHttpTrackerHttpPort());
        this.logger.info("FastDFS: fastdfs.tracker_servers=" + this.fdfsProp.getTrackerServers());
        this.logger.info("FastDFS: fastdfs.connection_pool_max_total=" + this.fdfsProp.getConnectionPoolMaxTotal());
        this.logger.info("FastDFS: fastdfs.connection_pool_max_idle=" + this.fdfsProp.getConnectionPoolMaxIdle());
        this.logger.info("FastDFS: fastdfs.connection_pool_min_idle=" + this.fdfsProp.getConnectionPoolMinIdle());
        this.logger.info("FastDFS: fastdfs.nginx_servers=" + this.fdfsProp.getNginxServers());

        this.prop.put("fastdfs.connect_timeout_in_seconds", this.fdfsProp.getConnectTimeout());
        this.prop.put("fastdfs.network_timeout_in_seconds", this.fdfsProp.getNetworkTimeout());
        this.prop.put("fastdfs.charset", this.fdfsProp.getCharset());
        this.prop.put("fastdfs.http_anti_steal_token", this.fdfsProp.getHttpAntiStealToken());
        this.prop.put("fastdfs.http_secret_key", this.fdfsProp.getHttpSecretKey());
        this.prop.put("fastdfs.http_tracker_http_port", this.fdfsProp.getHttpTrackerHttpPort());
        this.prop.put("fastdfs.tracker_servers", this.fdfsProp.getTrackerServers());
        ClientGlobal.initByProperties(this.prop);
    }
    /**
     * 显示初始化信息
     */
    private void info() {
        this.logger.info("FastDFS parameter: ConnectionPoolMaxTotal ==> " + this.pool.getMaxTotal());
        this.logger.info("FastDFS parameter: ConnectionPoolMaxIdle ==> " + this.pool.getMaxIdle());
        this.logger.info("FastDFS parameter: ConnectionPoolMinIdle ==> " + this.pool.getMinIdle());
        this.logger.info("FastDFS parameter: NginxServer ==> " + Arrays.toString(this.nginxServers));
        this.logger.info(ClientGlobal.configInfo());
    }

    /**
     * 创建连接池
     */
    private void create() {
        this.config = new GenericObjectPoolConfig();
        this.logger.info("FastDFS Client: Creating connection pool...");
        this.config.setMaxTotal(Integer.parseInt(this.fdfsProp.getConnectionPoolMaxTotal()));
        this.config.setMaxIdle(Integer.parseInt(this.fdfsProp.getConnectionPoolMaxIdle()));
        this.config.setMinIdle(Integer.parseInt(this.fdfsProp.getConnectionPoolMinIdle()));
        StorageClientFactory factory = new StorageClientFactory();
        this.pool = new GenericObjectPool<StorageClient>(factory, this.config);
        this.nginxServers = this.fdfsProp.getNginxServers().split(",");
    }

    /**
     * Nginx服务器负载均衡算法
     *
     * @param servers 服务器地址
     * @param address 客户端IP地址
     * @return 可用的服务器地址
     */
    private String getNginxServer(String[] servers, String address) {
        int size = servers.length;
        int i = address.hashCode();
        int index = abs(i % size);
        return servers[index];
    }

    /**
     * 带有防盗链的下载
     *
     * @param fileGroup       文件组名
     * @param remoteFileName  远程文件名称
     * @param clientIpAddress 客户端IP地址
     * @return 完整的URL地址
     */
    public String autoDownloadWithToken(String fileGroup, String remoteFileName, String clientIpAddress) throws Exception {
        int ts = (int) (System.currentTimeMillis() / 1000);
        String token = ProtoCommon.getToken(remoteFileName, ts, ClientGlobal.getG_secret_key());
        String nginx = this.getNginxServer(this.nginxServers, clientIpAddress);
        return "http://" + nginx + "/" + fileGroup + "/" + remoteFileName + "?token=" + token + "&ts=" + ts;
    }

    /**
     * 上传文件,适合上传图片
     *
     * @param buffer 字节数组
     * @param ext    扩展名
     * @return 文件组名和ID
     */
    public String[] autoUpload(byte[] buffer, String ext) throws Exception {
        String[] upload = this.upload(buffer, ext, null);
        return upload;
    }

    /**
     * 不带防盗链的下载,如果开启防盗链会导致该方法抛出异常
     *
     * @param fileGroup       文件组名
     * @param remoteFileName  远程文件ID
     * @param clientIpAddress 客户端IP地址,根据客户端IP来分配Nginx服务器
     * @return 完整的URL地址
     */
    public String autoDownloadWithoutToken(String fileGroup, String remoteFileName, String clientIpAddress) throws Exception {
        if (ClientGlobal.getG_anti_steal_token()) {
            this.logger.error("FastDFS Client: You've turned on Token authentication.");
            throw new Exception("You've turned on Token authentication.");
        }
        String nginx = this.getNginxServer(this.nginxServers, clientIpAddress);
        return "http://" + nginx + fileGroup + "/" + remoteFileName;
    }

    // 后面还有好多方法,就不一一展示了
}

为了在IDEA中使用便捷的配置提示功能,我们需要创建元数据文件(resources/spring-configuration-metadata.json):

{
  "groups": [
    {
      "name": "fastdfs",
      "type": "com.bluemiaomiao.properties.FastdfsProperties",
      "sourceType": "com.bluemiaomiao.properties.FastdfsProperties"
    }
  ],
  "properties": [
    {
      "name": "connectTimeout",
      "type": "java.lang.String",
      "sourceType": "com.bluemiaomiao.properties.FastdfsProperties",
      "defaultValue": "5"
    },
    {
      "name": "networkTimeout",
      "type": "java.lang.String",
      "sourceType": "com.bluemiaomiao.properties.FastdfsProperties",
      "defaultValue": "30"
    },
    {
      "name": "charset",
      "type": "java.lang.String",
      "defaultValue": "UTF-8"
    },
    {
      "name": "httpAntiStealToken",
      "type": "java.lang.String",
      "sourceType": "com.bluemiaomiao.properties.FastdfsProperties",
      "defaultValue": "false"
    },
    {
      "name": "httpSecretKey",
      "type": "java.lang.String",
      "sourceType": "com.bluemiaomiao.properties.FastdfsProperties"
    },
    {
      "name": "httpTrackerHttpPort",
      "type": "java.lang.Integer",
      "sourceType": "com.bluemiaomiao.properties.FastdfsProperties"
    },
    {
      "name": "trackerServers",
      "type": "java.lang.String",
      "sourceType": "com.bluemiaomiao.properties.FastdfsProperties"
    },
    {
      "name": "connectionPoolMaxTotal",
      "type": "java.lang.Integer",
      "sourceType": "com.bluemiaomiao.properties.FastdfsProperties",
      "defaultValue": "18"
    },
    {
      "name": "connectionPoolMaxIdle",
      "type": "java.lang.Integer",
      "sourceType": "com.bluemiaomiao.properties.FastdfsProperties",
      "defaultValue": "18"
    },
    {
      "name": "connectionPoolMinIdle",
      "type": "java.lang.Integer",
      "sourceType": "com.bluemiaomiao.properties.FastdfsProperties",
      "defaultValue": "2"
    },
    {
      "name": "nginxServers",
      "type": "java.lang.String",
      "sourceType": "com.bluemiaomiao.properties.FastdfsProperties"
    }
  ],
  "hints": [
    {
      "name": "http_anti_steal_token",
      "values": [
        {
          "value": "false"
        },
        {
          "value": "true"
        }
      ]
    }
  ]
}

将自定义starter添加到项目

  • 创建SpringBoot项目,勾选Web选项,版本选择1.5.20

  • 进入场景启动器的项目目录执行mvn clean install 将其安装到本地

  • 在POM.xml文件中添加依赖:

<dependency>
    <groupId>com.bluemiaomiao</groupId>
    <artifactId>fastdfs-spring-boot-starter</artifactId>
    <version>1.0-SNAPSHOT</version>
</dependency>

记得开启IDEA的自动导入功能

  • 创建配置文件application.properties
fastdfs.nginx-servers=192.168.80.2:8000,192.168.80.3:8000,192.168.80.4:8000
fastdfs.tracker-servers=192.168.80.2:22122,192.168.80.3:22122,192.168.80.4:22122
fastdfs.http-secret-key=2scPwMPctXhbLVOYB0jyuyQzytOofmFCBIYe65n56PPYVWrntxzLIDbPdvDDLJM8QHhKxSGWTcr+9VdG3yptkw
fastdfs.http-anti-steal-token=true
fastdfs.http-tracker-http-port=8080
fastdfs.network-timeout=30
fastdfs.connect-timeout=5
fastdfs.connection-pool-max-idle=18
fastdfs.connection-pool-min-idle=2
fastdfs.connection-pool-max-total=18
fastdfs.charset=UTF-8

或者使用application.yml

fastdfs:
  charset: UTF-8
  connect-timeout: 5
  http-secret-key: 2scPwMPctXhbLVOYB0jyuyQzytOofmFCBIYe65n56PPYVWrntxzLIDbPdvDDLJM8QHhKxSGWTcr+9VdG3yptkw
  network-timeout: 30
  http-anti-steal-token: true
  http-tracker-http-port: 8080
  connection-pool-max-idle: 20
  connection-pool-max-total: 20
  connection-pool-min-idle: 2
  nginx-servers: 192.168.80.2:8000,192.168.80.3:8000,192.168.80.4:8000
  tracker-servers: 192.168.80.2:22122,192.168.80.3:22122,192.168.80.4:22122
  • 创建控制器类测试方法
// controllers.DownloadController.java
@Controller
@RequestMapping(value = "/download")
public class DownloadController {

    @Autowired
    private FastdfsClientService service;

    @ResponseBody
    @RequestMapping(value = "/image")
    public String image() throws Exception {
        // 之前上传过的数据,实际应用场景应该使用SQL数据库来存储
        return service.autoDownloadWithToken("group1", "M00/00/00/wKhQA1ysjSGAPjXbAAVFOL7FJU4.tar.gz", "192.168.80.1");
    }
}

最后

针对于上面的面试题我总结出了互联网公司java程序员面试涉及到的绝大部分面试题及答案做成了文档和架构视频资料免费分享给大家(包括Dubbo、Redis、Netty、zookeeper、Spring cloud、分布式、高并发等架构技术资料),希望能帮助到您面试前的复习且找到一个好的工作,也节省大家在网上搜索资料的时间来学习.

资料领取方式:

Qq-u-n:219--571--750,领取往期Java高级架构资料、源码、笔记、视频

Dubbo、Redis、设计模式、Netty、zookeeper、Spring cloud、分布式、

高并发等架构技术



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

推荐阅读更多精彩内容