4行代码
上古时代的Java程序员如果想写一个HTTP服务,需要按下面的步骤操作:
- 编写Servlet, 实现
doXXX()
方法 - 打成war包
- 部署Tomcat
- 将war包copy到指定目录下进行"部署"
- 访问8080
一顿操作猛如虎,旁边的小弟小妹对你佩服的五体投地。
我可能只是想说一句Hello World而已,需要这么装X么?
到了中古时代,事情变的更麻烦了。虽然不需要直接写servlet, 但为了正确配置好"春天"这个框架,我们要写一箩筐XML文件,一不小心写错Tomcat就会一堆Error。由此就诞生了"面向XML编程"的段子。
到了近代,我们终于可以直接在main()
方法里写代码了,但是之前还需要引入一堆xxx-starter
和spring boot的打包插件,这样才能做到打出的jar包可以执行运行。不过本质还是Servlet没变,只是框架帮你隐藏了而已,感觉还是少了那么点意思。
来到当下,Vert.x来拯救我们了!来看看一个最简单的HTTP Server长啥样:
Vertx vertx = Vertx.vertx(); // (1)
vertx.createHttpServer() // (2)
.requestHandler(req -> req.response().end("it works!")) // (3)
.listen(8080); // (4)
只需要4行代码!而且,你不需要继承xxx-parent
, 更不需要xxx-starter
, 你只需要在pom.xml中添加一个依赖就搞定了:
io.vertx
vertx-web
3.5.4
长久以来,很多人都一直在指责Java"又臭又长", 开发速度慢,运行时笨重,Tomcat更重,IDE难用(主要指Eclipse)等槽点,当然,他们说的都没错,错就错在Servlet。Servlet在当时(2001年)的编程环境下确实起到了Web开发大一统的作用,但慢慢的很多躁动的程序员们就会不满这种呆板笨重编程方式。好了扯远了,我们先来看看上面的几行代码都干了什么:
(1): 构造一个Vertx对象,这里暂时没什么好说的
(2): 创建了一个HttpServer对象
(3): 注册一个请求处理器,这里我们无论什么请求,只要来了就返回一个"it works!"字符串
(4): 让Server在8080端口上监听
是不是非常的直观?
我知道,做为一个有上进心的开发者,我们怎么能不关心你这几行代码能抗多少请求呢?业务代码写在哪?错误处理?先不要着急,以后会讲到的,这里只是想告诉大家,用Java写Http Server,其实还有更优雅的方式。
Verticle
Verticle是Vert.x里最重要的概念。之前我们在准备篇《C10K问题与Reactor模式》中讲过什么是Reactor, 这里的Verticle其实就是Reactor的一个实现,即事件循环。 只不过,Vert.x有一个"部署"的概念,每一个Verticle需要先进行部署,且Vert.x保证一个Verticle在整个Vertx生命周期内一定只由同一条线程来执行,这样就避免了线程安全问题。等等,这说的好像跟Netty里的EventLoop
有点像?对,事实是,Vert.x底层就是Netty, Verticle直接就是使用Netty的EventLoop
执行的。可能有人会问,为什么不直接用Netty? 哦,如果想用Netty的话,可以先去看看官网的Hello World需要多少行代码吧。
下面我们来看一下一个比较"正常"的Vert.x HTTP Server应该怎么写:
VertxOptions options = new VertxOptions();
options.setEventLoopPoolSize(8);
Vertx vertx = Vertx.vertx(options); // (1)
DeploymentOptions depOps = new DeploymentOptions();
depOps.setInstances(8);
vertx.deployVerticle(HttpVerticle.class, depOps, ar -> { // (2)
if (ar.succeeded()) {
System.out.println("done deployment");
} else {
System.out.println(ar.cause());
}
});
(1): 我们在创建Vertx对象时设置了一个参数对象,把事件循环线程数设为8
默认值为 CPU核心数 * 2, 我用的是物理4核的macbook pro, 这里如果不设置会是16,因为有超线程技术加持。
(2): 这块可能看起来比较复杂,其实非常简单。前面说过Verticle需要部署,那么这就是编程式部署的代码了。调用deployVerticle()
方法,第一参数是我们想要部署的Verticle对象本身(下面会给出代码); 第二个参数是部署参数,这里我们设置部署8个Verticle; 第三个参数用来注册一个回调方式,部署完成或失败时Vert.x会使用NIO线程调用此方法。
在Vert.x里会经常看到这种异步回调的方式,类似于Node.js, 前期需要适应一下。
接下来看看主解HttpVerticle
长什么样:
public static class HttpVerticle extends AbstractVerticle { // (0)
@Override
public void start(Future startFut) throws Exception {
vertx.createHttpServer()
.requestHandler(req -> req.response().end("it works!"))
.listen(8080, result -> { // (1)
if (!result.succeeded()) { // (2)
System.out.println("failed to start server, msg = " + result.cause());
startFut.fail(result.cause()); // (3)
}
});
}
}
对,基本上就是之前我们的4行代码。
(0): 我们需要继承AbstractVerticle
来编写自己的Verticle
(1): 监听8080端口也需要注册一个回调方法,这样才能知晓是否监听成功。
(2): 判断是否发生了错误
(3): 如果出错,调用方法参数中传过来的Future
对象的fail()
方法,作用是通知Vertx这个Verticle启动失败了,这样上面deployVerticle()
中注册的回调里的失败逻辑才会被调用。
这里有朋友肯定会想,EventLoopPoolSize
跟verticle的Instances
到底应该设置多少合适呢?前面说了,一个Verticle总是由同一条NIO线程执行,如果NIO线程数与verticle数相同,那么Vertx会保证每一个verticle都会有一条专有线程执行;如果NIO > verticle, 那么会有NIO线程空闲; 如果 NIO < verticle, 那么会出现一个NIO线程负责执行多个verticle的情况。这里要注意的是,第三种情况也不会有线程安全问题,因为一个verticle还是总是由同一条线程执行的。
着急的程序员可能已经不满足于上面这种Hello World玩具代码了,他们想尽快把自己的业务代码塞进去。如果想发挥异步编程的全部实力,那么业务代码必须也要"异步"起来,即所有的阻塞调用,都要通过注册事件-->回调的方式执行,万万不可阻塞NIO线程。说白了,就是争取让NIO线程一直在干需要CPU发力的活,一刻也不能因为等待I/O而停下来。应该怎么办呢,下期再说。