Wentong's Blog

绝知此事要躬行


  • 首页

  • 归档

  • 关于

  • 分类

  • 标签

Java并发编程之任务执行

发表于 2016-01-25   |   分类于 note   |  

串行执行任务.

在单个线程中串行执行各项任务,

class SingleThreadServer {
public static void main(String[] args) throws IOException {
ServerSocket socket = new ServerSocket(80);
while(true) {
Socket connection = sockect.accept();
handleRequest(connection);
}
}
}

主线程在接受连接与处理相关请求操作间交替运行, 服务器在处理请求时, 新到来的连接必须等待直到请求处理完成. Web应用多为IO bound, 这种方式浪费了宝贵的CPU资源.

显式为请求创立线程

通过为每个请求创立新的线程, 实现更高的响应性.

class ThreadPerTaskWebServer {
public static void main(String[] args) throws IOException {
ServerSocket socket = new ServerSocket(80);
while (true) {
final Socket connection = socket.accept();
Runnable task = new Runnable() {
public void run() {
handleRequest(connection);
}
}
new Thread(task).start();
}
}
}

这个版本有如下几个特点:

  1. 任务从主线程分离出来, 主线程将处理连接的任务交给了子线程, 主循环不必等待任务处理结束, 从而主线程能够更快地响应新连接
  2. 任务可以并行, 从而能够同时服务多个请求, 如果有多个处理器, 能够更好地利用多处理器的优势.
  3. 任务代码必须是线程安全的, 因为有多个任务会并发调用这段代码.

无限制创建线程的不足

在生产环境中, “为每个任务分配一个线程”这种方法存在一些缺陷, 尤其是当需要创建大量线程时.

  1. 线程生命周期的开销非常高:

  2. 资源消耗: 活跃的线程会消耗系统资源, 尤其是内存, 如果可运行的线程数量多于可用的处理器数量,那么有些线程将闲置, 大量空闲的线程会占用许多内存, 给垃圾回收带来压力, 而且大量线程在竞争CPU资源时还将产生其他性能开销.

    如果已经拥有足够多的线程使CPU保持忙碌状态, 那么再创建更多的线程, 反而会降低性能.

  3. 稳定性: 可创建线程的数量存在一个限制. 这个闲置将随着平台的不同而不同. 并且受多个因素制约, 包括JVM的启动参数,

    Thread构造函数中请求的栈大小, 以及底层操作系统对线程的限制等。 如果破坏了这些限制, 那么很可能抛出OutOfMemoryError,

    要想从这种错误中恢复过来是非常危险的, 更简单的方法是通过构造程序来避免超出这些限制.

在一定范围内, 增加线程可以提高系统的吞吐率, 但如果超过欧这个范围, 再创建更多的线程只会降低程序的执行速度,

并且如果过多地创建一个线程, 整个应用程序将崩溃, 如果想避免这种危险,应该对应用程序创建的线程数量进行限制,

并且全面地测试应用程序,从而确保在线程数量达到限制时, 程序也不会耗尽资源.

Excutor 框架

串行执行的问题在于其糟糕的响应性和吞吐量, 而”为每个线程分配一个线程”的问题在于资源管理的复杂性.

java.util.concurrent提供了一种灵活的线程池实现作为Executor框架的一部分. 在Java类库中, 任务执行的主要抽象不是Thread, 而是Executor.

public interface Executor {
void execute(Runnable command);
}

Executor 基于生产者——消费者模式, 提交任务的操作相当于生产者, 执行任务的线程相当于消费者.

基于Executor的Web服务器:

class TaskExecutionWebServer {
private static final int NTHREADS = 100;
private static final Executor exec = Excutors.newFixedThreadPool(NTHREADS);

public static void main(String[] args) {
ServerSocket socket = new ServerSocket(80);
while(true) {
final Socket connection = socket.accept();
Runable task = new Runable() {
public void run() {
handelRequest(connection);
}
};
exec.execute()
}
}
}

通过Executor,可以将任务提交与执行解耦开来, 从而无需太大的困难就可以为某种类型的任务指定和修改执行策略.

  • 在什么线程执行任务?
  • 按照什么顺序执行任务 (FIFO, LIFO, 优先级)?
  • 有多少任务能并发执行
  • 在队列中有多少任务在等待执行
  • 如果系统由于过载需要拒绝一个任务, 那么应该选择哪一个任务? 另外, 如何通知应用程序有任务被拒绝.
  • 执行一个任务之前或之后需要进行哪些动作 ?

可以通过Executors中的静态工厂方法之一来创建一个线程池:

  • newFixedThreadPool: 创建一个固定长度的线程池, 每当提交一个任务是就创建一个线程, 直到达到达到线程池的最大数量, 这时线程池的规模将不再变化.

    如果某一线程由于发生了未预期的Exception而结束, 那么线程池会补充一个新的线程.

  • newCachedThreadPoll: 创建一个可缓存的线程池, 如果线程池的规模超过了处理需求时, 那么将回收空闲的线程, 而当需求增加时, 可以添加新的线程池, 线程池的规模不存在任务和限制.

  • newSingleThreadExecutor: 创建单个工作者线程来执行任务, 如果这个线程异常结束, 会创建另一个线程来替代.

  • newScheduledThreadPoll: 创建固定长度的线程池, 而且以延迟或者定时的方式执行任务, 类似于Timer

线程池解决了服务器因为创建过多线程而失败的问题, 但在足够长时间内, 如果任务到达的速度总是超过任务执行的速度, 那么服务器仍有可能耗尽内存,

因为等待执行的Runnable队列将不断增长, 可以通过使用一个有界工作队列在Excutor内部解决这个问题.

Executor生命周期

Executor采用异步方式执行任务,因此在任何时刻,之前提交的任务状态不是立即可见的. 有些可能已经完成,有些正在运行, 而其他任务可能在队列中等待执行.

为解决执行服务的生命周期问题, Executor扩展了ExecutorService接口, 添加了一些用于生命周期管理的方法(同时还有一些用于提交任务的便利方法).

pubic interface ExecutorService extends Exectuor {
void shutdown();
List<Runnable> shutdownNow();
boolean isShutdown();
boolean isTerminated();
boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException;
Future<T> <T> submit(Callable<T>);
Future<T> <T> submit(Runnable, T);
Future<?> submit(Runnable);
List<Future<T>> <T> invokeAll(Collection<? extends Callable<T>>);
...
}

ExecutorService的生命周期有3种状态:运行,关闭和已终止. ExecutorService初始创建处于运行状态, shutdown方法将执行平缓的关闭过程: 不再接受新的任务, 同时等待已经提交的任务执行完成——包括哪些还未开始执行的任务. shutdownNow方法将执行粗暴的关闭过程: 它将尝试取消所有运行中的任务,并且不再启动队列中尚未开始的任务.

Executor框架中, 已提交尚未开始的任务可以取消, 但是对那些已经开始执行的任务, 只有它们能够响应中断时, 才能取消.

携带结果的任务Callable与Future

Executor框架使用Runnable作为其基本的任务表达形式. Runnable是一种有很大局限的抽象,

虽然run能写入到日志文件或者将结果放入某个共享的数据结构中, 但是不能返回一个值, 或者抛出一个受检查的异常.

CompletionService

CompletionService将Executor和BlockingQueue的功能结合起来, 可以将 Callable或者Runnable任务提交(submit)给它,

使用类似于队列操作的take和poll等方法获取已完成的结果.

public class Renderer {
private final ExecutorService executor;
Renderer (ExecutorService executor) { this.executor = executor; }
void renderPage(CharSequence source) {
List<imageInfo> info = scanForImageInfo(source);
CompletionService<ImageData> completionsService = new ExecutorCompletionService<ImageData>(executor);

for (final ImageInfo imageInfo: info) {
completionService.submit(new Callable<ImageData>() {
public ImageData call() { // 下载图片任务
return imageInfo.downloadImage();
}
});
}
renderText(source);
try {
for (int t = 0, n = info.size(); t < n; t++) {
Future<ImageData> f = completionService.take(); // 阻塞操作, 可抛异常
ImageData imageData = f.get();
renderImage(ImgData);
}
} catch (InterruptedException e) { // 响应中断
Thread.currentThread.interrupt();
} catch (ExecutionException e) { // 运行错误处理
throw launderThrowable(e.getCause());
}
}
}

为任务设置时限

Future.get 支持限时, 如果在限定时间内没有得到计算结果, 将抛出 TimeoutException。 在使用限时任务时应当注意, 当这些任务超时后应当立即停止, 从而避免继续计算一个不在使用的结果而浪费资源. 为此, 可以再次使用Future, 如果限时的get方法抛出了TimeoutException, 那么可以通过Future来取消任务, 如果编写的任务是可取消的, 那么就可以提前终止它, 以免消耗过多的资源.

在指定时间获取广告信息的例子

Page renderPageWithAd() throws InterruptedException {
long endNanos = System.nonoTime() + TIME+BUDGET;
Future<Ad> f = exec.submit(new FetchAdTask{});
Page page = renderPageBody();
Ad ad;
try {
long timeLeft = endNanos - System.nanoTime();
ad = f.get(timeLeft, NANOSECONDS);
} catch (ExecutionException e) {
ad = DEFALUT_AD:
} catch (TimeoutException e) {
ad = DEFALUT_AD;
f.cancel(true); // 超时,取消任务
}
page.sendAd(ad);
return page;
}

摘自: Java Concurrency In Practice

几款消息中间的调研

发表于 2016-01-19   |   分类于 sumary   |  

消息队列调研

消息系统简介

本次主要调研业界使用广泛的两款消息队列——RabbitMQ, Kafka, 以及阿里云的提供的两个服务, MNS和ONS.

RabbitMQ

RabbitMQ 是使用Erlang编写的一个开源的消息队列,本身支持很多的协议:AMQP,XMPP, SMTP, STOMP,也正因如此,它非常重量级,更适合于企业级的开发。同时实现了Broker构架,这意味着消息在发送给客户端时先在中心队列排队。对路由,负载均衡或者数据持久化都有很好的支持。

阿里云MNS

  • MNS产品介绍

阿里云消息服务(Message Service,原MQS)是阿里云唯一商用的消息中间件服务。与传统的消息中间件不同,消息服务一开始就是基于阿里云自主研发的飞天分布式系统来设计和实现,具有大规模,高可靠、高并发访问和超强消息堆积能力的特点。消息服务API采用HTTP RESTful标准,接入方便,跨网络能力强;已全面接入资源访问控制服务(RAM)、专有网络(VPC),支持各种安全访问控制;接入云监控,提供完善的监控及报警机制。消息服务提供丰富的SDK、解决方案、最佳实践和7x24小时的技术支持,帮助应用开发者在应用组件之间自由地传递数据和构建松耦合、分布式、高可用系统。

阿里云ONS / RocketMQ

  • ONS产品介绍
  • ONS开源社区对应产品——RocketMQ

消息队列(Message Queue,简称MQ)是企业级互联网架构的核心服务,基于高可用分布式集群技术,搭建了包括发布订阅、接入、管理、监控报警等一套完整的高性能消息云服务,帮您实现分布式计算场景中所有异步解耦功能。经过多年积累,在交易、商品、营销等核心链路包括在双11场景下都有广泛使用,服务于阿里内部上千个核心应用,每天消息量达上千亿条,MQ由阿里巴巴集团中间件技术部自主研发,是原汁原味的阿里集团中间件技术精华之沉淀。

Kafka

Kafka是Apache下的一个子项目,是一个高性能跨语言分布式发布/订阅消息队列系统。具有以下特性:快速持久化,可以在O(1)的系统开销下进行消息持久化;高吞吐,在一台普通的服务器上既可以达到10W/s的吞吐速率;完全的分布式系统,Broker、Producer、Consumer都原生自动支持分布式,自动实现负载均衡.

Kafka的用户中包括LinkedIn, Yahoo, Twitter, Uber, PayPal, Airbnb, Tumblr等, 被用于日志收集, 离线分析, 实时分析, 消息管道等, 详情见 Powerd By Kafka

Kafka官方提供了Java版本的客户端API, Kafka社区产生了多种语言的客户端, 包括PHP, Python, Go, C/C++, Ruby, NodeJS等, 详情见 Kafka 客户端列表

Kafka Broker较为轻量, 不保存consumer的消费进度, 由consumer自己控制。 因此使用起来非常灵活, 可以针对不同场景定制不同的消费服务.

  • Exactly Once: 消费且仅消费一次
  • 回溯数据, 进行重复消费

目前Kafka的管理界面不友好, 官方只给了命令行工具. 通过命令行工具能简单地查看和操作Topic. Yahoo开源了自己的Kafka Web管理界面 Kafka-Manager, 但不支持最新的0.9.0版本的部分功能.

Kafka, RabbitMQ, MNS, ONS对比

– Kafka RabbitMQ MNS ONS
所属社区/公司 Apache Mozilla Public License Alibaba Alibaba
成熟度 成熟 成熟 成熟 比较成熟,公测中
特点 充分考虑消息堆积因素,认为 consumer 不一定处于 alive 状态;考虑各个角色的分布式; 为追求吞吐量设计;被多家公司和多个开源项目使用 由于Erlang语言的并发能力,性能很好, 支持多种协议,重量级系统 消息服务API采用HTTP RESTful标准,接入方便,跨网络能力强 高性能, 支持数据海量堆积, 支持主动推送
授权方式 开源 开源 商业 商业,有对应的开源项目RocketMQ
开发语言 Scala&Java Erlang Java Java
客户端支持语言 官方支持Java, 开源社区有多语言版本, 如PHP, Python, Go, C/C++, Ruby, NodeJS等编程语言, 详见 Kafka 客户端列表 官方支持Erlang, Java, Ruby等, 社区产出多种语言API,详见RabbitMQ客户端&开发工具 Java, C++, Python, C#, PHP, Node.js(非官方), Golang(非官方) Java, C/C++, C#, PHP
协议支持 自有协议,社区封装了HTTP协议支持 多协议支持:AMQP,XMPP, SMTP, STOMP HTTP ONS私有协议
消息批量操作 支持 不支持 支持 不支持
消息推拉模式 Pull 多协议, Pull/Push均有支持 Pull Pull, Push
保证消息至少消费一次 默认保证 保证 在消息有效期内,确保消息至少能被成功消费一次。 不保证(消费失败16次后丢弃)
消息回溯 支持 消费完即删除, 不支持回溯 消费完即删除, 不支持回溯 支持
HA 支持replica机制, leader宕掉后, 备份自动顶替, 并重新选举leader(基于Zookeeper) master/slave模式, master提供服务, slave仅作备份 – –
数据可靠性 上周的测试中, 使用Kafka作为消息中间件, 数据可靠, 并且有replica机制, 有容错容灾能力 可以保证数据不丢, 有slave用作备份 数据三重备份, 可靠性达10个9 (官方数据) 99.99% (官方数据)
QPS 性能卓越, 详见下文Linkedin团队的测试 性能优秀, 详见下文Linkedin团队的测试 默认4000 默认5000
持久化能力 磁盘文件, 只要磁盘容量够, 可以做到无限消息堆积 内存、文件,支持数据堆积,但数据堆积反过来影响生产速率 消息持久化默认有期限, 支持海量堆积 ONS消息默认保留三天,支持海量堆积
是否有序 多Client保证有序 若想有序,只能使用一个Client 不保证有序 不保证有序
事务 不支持, 但可以通过Low Level API保证仅消费一次 不支持 不支持 支持
集群 支持 支持 支持 支持
负载均衡 支持 支持 支持 支持
管理界面 官方只提供了命令行版, Yahoo开源自己的Kafka Web管理界面Kafka-Manager 较好 好 好
部署方式 独立 独立 Aliyun提供服务 Aliyun提供服务,可以独立部署

总结

  1. 事务支持方面,ONS/RocketMQ较为优秀,但是不支持消息批量操作, 不保证消息至少被消费一次.
  2. Kafka提供完全分布式架构, 并有replica机制, 拥有较高的可用性和可靠性, 理论上支持消息无限堆积, 支持批量操作, 消费者采用Pull方式获取消息, 消息有序, 通过控制能够保证所有消息被消费且仅被消费一次. 但是官方提供的运维工具不友好,开源社区的运维工具支持的版本一般落后于最新版本的Kafka.
  3. 目前使用的MNS服务,拥有HTTP REST API, 使用简单, 数据可靠性高, 但是不保证消息有序,不能回溯数据.
  4. RabbitMQ为重量级消息系统, 支持多协议(很多协议是目前业务用不到的), 但是不支持回溯数据, master挂掉之后, 需要手动从slave恢复, 可用性略逊一筹.

附: LinkedIn团队对Kafka, RabbitMQ, ActiveMQ的性能研究

生产者测试

LinkedIn团队在所有系统中配置代理,异步将消息刷入其持久化库。对每个系统,运行一个生产者,总共发布1000万条消息,每条消息200字节。Kafka生产者以1和50批量方式发送消息。ActiveMQ和RabbitMQ似乎没有简单的办法来批量发送消息,LinkedIn假定它的批量值为1。结果如下图所示:

Kafka生产者测试

消费者测试

为了做消费者测试,LinkedIn使用一个消费者获取总共1000万条消息。LinkedIn让所有系统每次拉请求都预获取大约相同数量的数据,最多1000条消息或者200KB。对ActiveMQ和RabbitMQ,LinkedIn设置消费者确认模型为自动。结果如下图所示

Kafka消费者测试

参考文档:

  • MNS官方文档
  • ONS官方文档
  • RabbitMQ Java Client API Guide
  • Kafka基准测试
  • LinkedIn是如何优化Kafka的
  • Apache Kafka 官方性能报告
  • Kafka剖析(一):Kafka背景及架构介绍
  • Apache Kafka:下一代分布式消息系统
  • RocketMQ与Kafka对比(18项差异)
  • Kafka, RabbitMQ对比

使用Nginx做内网域名转发

发表于 2015-03-25   |   分类于 sumary   |  

下面这个例子是通过具有公网IP的网关上的反向代理,将域名demo.wenotng.me的请求转发到内网机器192.168.1.100上,这样就可以在公网上通过域名demo.wentong.me访问内网的机器了.

1) 第一步,添加一条A记录(Adress), demo.wentong.me 指向网关的公网IP.

2) 添加Nginx配置:

网关服务器nginx配置:

# /etc/nginx/sites-enable/demo
server {
listen 80;
server_name demo.wentong.me;

access_log /var/log/nginx/demo.access.log;

location / {
proxy_redirect off;
proxy_set_header Host $host;
proxy_set_header User-Agent $http_user_agent;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_pass http://192.168.1.100;
}
}
阅读全文 »

Nodejs笔记 -- 模块和包

发表于 2015-03-05   |   分类于 cheatsheet   |  

基本概念

包是实现了某些功能模块的集合,它将摸个独立的功能封装起来, 用于发布、更新、依赖管理和版本控制.

模块和文件是一一对应的, 一个Node.js文件就是一个模块(类似于python的模块),这个文件可能是JavaScript代码 (*.js), JSON(*.json)或者编译过的C/C++扩展(*.node).

创建模块

创建模块非常简单,因为一个文件就是一个模块, 我们需要关注的是如何在其他文件中获取这个模块.

Node.js 提供了exports和require两个对象,其中exports是模块公开的接口, require用于从外部获取一个模块的接口,即获取外部模块的exports对象

阅读全文 »

ECMAScript引用类型简要笔记

发表于 2015-01-20   |   分类于 cheatsheet   |  

1. Object

  1. 可以通过对象字面量的方式定义
  2. 通过方括号或者.号进行访问

2. Array

  1. 数组内部数据类型可以不同,大小可以动态调整
  2. 使用方括号表示法或者构造函数创建
  3. length属性记录数组项数, 它不是只读的,可以通过设置该值,从数组末尾添加或者移除项
  4. toString()方法:调用数组每一项的toString()方法, 默认以逗号连接, 构成一个字符串. join()方法,可以使用指定的分隔符来构建这个字符串.
  5. 栈方法: push()&pop() 对数组尾部进行操作
  6. 队列方法: push()&shift() vs pop()&unshift()
  7. 排序: sort()&reverse,对源数组进行排序并返回. sort()默认调用每项的toString()方法,将转型后的字符串排序,也可以接受一个函数作为比较方法. reverse()反转数组
  8. concat()方法,用于连接数组, 该方法不影响被调用者. 返回连接后的数组.
  9. slice():切片操作,希望接受两个参数,不改变原始数组, 返回切片结果数组
  10. splice(): 铰接 splice(start, num, new1, new2, ...), 从start位置开始删除num项, 并插入后面的参数new1, new2 …
  11. indexOf()&lastIndexOf()从数组前/后(方向)查找第一个参数的位置, 返回第一个找到的位置,否则返回-1, 可以指定搜索起点,作为第二个参数.
  12. 迭代方法
    • every(): 每一项返回ture,则返回true
    • filter(): 返回true的项组成数组作为返回值
    • forEach(): 没有返回值
    • map(): 返回每次调用结果组成的数组
    • some(): 对任一项返回true, 则返回true
阅读全文 »

ECMAScript 数据类型

发表于 2015-01-18   |   分类于 cheatsheet   |  

数据类型

ECMAScript 中有5种简单数据类型(也称基本数据类型): Undefined、Null、Boolean、Number和String, 还有一种复杂数据类型——Object, Object本质上是由一组无序的键值对组成的。 ECMAScript 不支持任何类型创建自定义类型的机制,而所有值都将是上述6种数据类型之一。

typeof 操作符

typeof操作符用于检测给定变量的数据类型, 该操作符可能返回下列某个字符串:

  • "undefined" —— 如果这个值未定义;
  • "boolean" —— 如果这个值是布尔值;
  • "string" —— 如果这个值是字符串
  • "number" —— 如果这个值是数值
  • "object" —— 如果这个值是对象或null
  • "function" —— 如果这个值是函数
阅读全文 »

Markdown 使用教程

发表于 2014-06-03   |   分类于 cheatsheet   |  

Markdown简介

wiki释义:

Markdown 是一种轻量级标记语言,创始人为约翰·格鲁伯(John Gruber)和亚伦·斯沃茨(Aaron Swartz)。
它允许人们“使用易读易写的纯文本格式编写文档,然后转换成有效的XHTML(或者HTML)文档”。
这种语言吸收了很多在电子邮件中已有的纯文本标记的特性。

Markdown的优点包括但不限于以下几点:

  • 纯文本,所以兼容性极强,可以用所有文本编辑器打开。
  • 让你专注于文字而不是排版。
  • 格式转换方便,Markdown 的文本你可以轻松转换为 html、电子书等。
  • Markdown 的标记语法有极好的可读性。

Markdown语法

Markdown语法简洁明了、容易学习,下面一一介绍。

基本符号

  • *, - , +: 这3个符号的效果都一样, 被称为Markdown符号
  • 空白行 : 表示另起一个段落,对应HTML中的 <p> 元素
  • ` 表示 内联代码, tab是用来标记代码段,分别对应HTML的 <code> , <pre> 元素

段落换行

  • 单个回车会被解释为空格
  • 一个空白行(即两个回车)变成单一段落<p>
  • 连续3个Markdown符号,然后回车,表示<hr>横线
  • 连续多个空格会被解释为单个空格
阅读全文 »

使用Terminator增强你的终端

发表于 2014-05-30   |   分类于 cheatsheet   |  

简介

在linux下编程怎能缺少一个功能强大的终端,这里向大家介绍一款广受好评的终端——Terminator(终结者).

Terminator 是CrunchBang的默认终端,该终端基于 GNOME terminal。Terminator最大的特点就是可以在一个窗口中打开多个终端,
可以自由的将一个终端区域横向或纵向分割建立新终端,通过鼠标拉伸调整每个终端的大小,对需要同时使用多个终端的用户非常方便。


安装

Ubuntu

sudo apt-get install terminator

Fedora

sudo yum install terminator
阅读全文 »

在linux中安装/卸载字体

发表于 2014-05-30   |   分类于 cheatsheet   |  

问题由来

在安装Terminator
后,终端字体不够美观,于是就琢磨着安装新的字体来替代默认选项,下面以安装苹果的Monaco等宽字体为例,
来介绍在linux中安装字体的方法。

安装

安装字体可以使用图形化的字体查看器,也可以通过命令在命令行中安装。


一. Ubuntu中利用字体查看器安装

首先到这里下载Monaco字体, 然后在Ubuntu系统中,双击下载得到的Monaco.ttf文件,即用系统自带的字体查看器打开了该文件,点击面板上的安装按钮,即可完成安装,这种方法安装后,字体文件存放在~/.local/share/fonts目录下。

这种方法的优点是安装前能预览字体效果,安装过程简单便捷,动几下鼠标就完成了安装,非常适合在图形界面下安装; 缺点是安装借助图形化工具,无法通过自动化脚本安装。

阅读全文 »
fangwentong

fangwentong

9 日志
3 分类
9 标签
RSS
GitHub Twitter Weibo

友情链接

豪神的博客
© 2014 - 2016 fangwentong
由 Hexo 强力驱动
主题 - NexT.Pisces