本文依照 知識共享許可協議(署名-非商業性使用-禁止演繹) 發布。
感謝@嚴禁扯淡 的修改建議。
2017-2-9
:更新異步協同部分。
先放兩個鏈接:
源文檔
Github repository.
話說用Java這么多年,沒給社區做過什么貢獻。這次趁使用Vert.x3的機會,簡單翻譯了核心包的手冊。
Vert.x3的手冊簡潔明了,過一遍即可輕松入門。所以諸君若是看到什么無法理解的,必定是我的譯文有問題(嘿嘿,水平低,見諒)。
部分名詞對照表:
- handler:事件處理器
- event loop:事件循環(線程)
- verticle:Vert.x的專有名詞。指代Vert.x中基本的功能單元,一個Vert.x應用應該是由一組verticles構成的。
- worker:顧名思義,干活的(線程)。對應文檔中有worker thread pool,worker verticle(Vert.x里的work verticle與標準版verticle區別較大)。
- event bus:事件總線
Vert.x核心包提供如下的功能:
- TCP客戶端與服務器
- HTTP客戶端與服務器(包含Websocket支持)
- 事件總線(Event bus)
- 共享數據-局部的map和集群下的分布式map
- 定時或延遲的處理
- 部署、卸載verticle
- 數據報文套接字(datagram socket)
- DNS客戶端
- 文件系統存取
- 高可用性
- 集群
核心包提供的功能是相當底層的。這意味著沒有諸如數據庫存取、認證、高級web功能此類的組件,你可以在Vert.x ext(擴展包)找到以上這些。
Vert.x 的核心小且輕量,諸位可以各取所需。它可以整個的嵌入你現有的應用當中,不需要為了使用Vert.x而以特別的方式組織你的應用。
你可以在任何Vert.x支持的語言中使用核心包。但是有一點要提一下,我們不會迫使你在Javascript或者Ruby里使用為Java準備的API;畢竟不同的語言有不同的約定和慣用法,強迫Ruby開發者使用Java的慣用法確實比較古怪。相反的,我們為每一種語言都生成了等價于核心Java API的慣用法(idiomatic)。
現在開始,我們將使用核心包(core)指代Vert.x core。
如果你使用Maven或Gradle,把下列幾行加入項目描述符的依賴配置即可使用核心包的API:
- Maven (in your
pom.xml
):
<dependency>
<groupId>io.vertx</groupId>
<artifactId>vertx-core</artifactId>
<version>3.2.1</version>
</dependency>
- Gradle (in your build.gradle file):
compile io.vertx:vertx-core:3.2.1
下面讓我們看看核心包里的各種特性。
開始使用Vert.x
注意:這里所述大部分都是Java適用的,語言相關的部分需要以某種方式進行轉換。
在Vert.x里,如果沒有Vertx對象,那幾乎什么都做不了!
Vertex對象是Vert.x的控制中心,許多功能都通過它實現。包括創建客戶端和服務器、獲取事件總線(event bus)的引用、設置定時器,等等。
那么,如何獲取它的實例呢?
如果你在程序中嵌入Vert.x,那么可以像下面這樣創建實例:
Vertx vertx = Vertx.vertx();
當你使用Verticle時
注意:絕大部分應用其實只需要一個Vert.x實例;當然,你也可以創建多個實例。例如,兩個總線需要隔離時或客戶端服務器需分組時。
創建Vert.x實例時指定可選項(option)
創建Vert.x實例時,如果缺省選項不合適,你也可以設定一些值:
Vertx vertx = Vertx.vertx(new VertxOptions().setWorkerPoolSize(40));
VertxOptions對象有很多設置項,你可以配置集群(clustering)、高可用性(high availability),worker 線程池的大小(pool sizes)等等。詳細的內容請參見Javadoc。
創建集群模式(clustered)的Vert.x對象
如果你在使用Vert.x的集群模式(更多細節請參考下面的event bus一節,關于集群下的event bus),記得創建Vert.x對象也是異步的。
為了把集群里不同的vertx實例組織在一起,通常需要花一點時間(可能是幾秒鐘)。在這段時間里,為了不阻塞調用線程(the calling thread),結果會以異步的方式返回。
Are you fluent?(fluent狂人,別走)
你可能已經注意到,在前面的例子中,我們使用了流式(fluent)的API。
流式API是指多個方法可以用鏈式的方式一起調用。例如:
request.response().putHeader("Content-Type", "text/plain").write("some text").end();
這在Vert.x的API里是很普遍的模式,你要試著習慣它。 :)
鏈式調用允許你更簡潔的編寫代碼。當然,如果你不喜歡這種方式,這也不是必須的。你可以愉快地忽略這些,然后像下面這樣寫:
HttpServerResponse response = request.response();
response.putHeader("Content-Type", "text/plain");
response.write("some text");
response.end();
不要調用我們,我們會調用你(Don’t call us, we’ll call you.)
大部分Vert.x API 都是事件驅動的。這意味著如果你對Vert.x里發生的某事感興趣,Vert.x會以向你發送事件(events)的方式通知你。
例如下面的事件:
- 定時器被觸發
- socket收到了一些數據
- 一些數據已經從磁盤上被讀取
- 某個異常產生了
- HTTP服務器接受了一個請求
通過提供handlers,你可以處理這些事件。例如定義一個定時器事件:
vertx.setPeriodic(1000, id -> {
// This handler will get called every second
System.out.println("timer fired!");
});
或者接受一個HTTP請求:
server.requestHandler(request -> {
// This handler will be called every time an HTTP request is received at the server
request.response().end("hello world!");
});
如果觸發了某個事件,Vert.x將會異步地(asynchronously)調用它(the handler)。
這里我們發現了Vert.x的如下重要概念。
不要阻塞我!(Don't block me!)
除了極少的例外(即某些以‘Sync’結尾的文件系統操作),Vert.x里沒有API會阻塞調用線程。
如果結果可以即刻獲得,它會立刻被返回。否則,通常你需要提供一個處理器,以便稍后接收事件。
沒有API會阻塞線程意味著:用少量的線程,就可以處理大量的并發。
傳統的阻塞API可能會在哪些地方發生呢:
- 從socket讀取數據
- 寫數據到磁盤
- 發消息給某個接收者,然后等待回應
- 。。很多其他狀況
在上面這些案例中,你的線程在等待一個結果時不能做其他任何事,這樣是很低效的。
這也意味著,如果你想使用阻塞API處理大量并發,你將需要大量的線程來防止你的應用卡住。
線程在內存(例如:棧)和上下文切換方面的開銷不容忽視。
以很多現代的應用所需求的并發級別,阻塞的方式根本實現不了。
Reactor and Multi-Reactor(反應器和多路反應器?)
前面我們提到了Vert.x的API是事件驅動的,當handlers可用的時候,Vert.x會向它們傳遞事件。
絕大多數情況下,Vert.x通過一個叫event loop的線程調用你的handlers。
event loop可以在事件到達時持續不斷地將其分發給不同的handler,因為Vert.x和你的應用中不會有什么是阻塞的。
同樣,因為沒什么是阻塞的,所以event loop具有在短時間內分發巨量事件的潛力。例如,單個event loop可以極迅速地處理數千的HTTP請求。
我們稱之為Reactor模式。
你之前可能已經聽說過它--nodejs就實現了這種模式。
標準的Reactor實現里,有一個單獨的event loop(single event loop)線程,它會在所有事件到達時持續不斷地將其分發給所有的handler。
單一線程的困擾在于,在任意時刻,它只能在一個cpu核心上運行。所以如果你希望你的單線程Reactor應用(或者Nodejs應用)能夠運用上多核服務器的擴展能力( scale over your multi-core server ),你需要啟動多個進程并管理好它們。
Vert.x的工作方式不同于此,每個vertx實例會維護數個event loop(several event loops)。缺省情況下,我們基于機器上可用的核心數來確定這個數字,當然這個也可以設置。
不像Nodejs,這意味著單個Vert.x進程可以利用到服務器的擴展。
為了與單線程的reactor模式區分開,我們稱之為Multi-Reactor模式。
注意:雖然一個Vert.x實例會維護多個event loop,但任何特定的handler都絕不會被并發地執行,在絕大多數情況下(worker verticle除外),它都會被某個固定的event loop(exact same event loop)調用。
黃金準則:不要阻塞Event Loop(Don’t Block the Event Loop)
我們已經了解了,Vert.x的API是非阻塞的,不會阻塞event loop;但是,如果你在handler中自己(yourself)阻塞了event loop,那么上面的其實都沒啥用。。
如果你這么干了,那么event loop被阻塞的時候它啥都干不了。再如果你阻塞了Vert.x實例里所有的event loop,那你的應用將會陷入完全停滯的狀態!
所以千萬別這么干!我們已經警告過你了哈(You have been warned)。
阻塞的例子包括:
- Thread.sleep()
- 等待一個鎖
- 等待一個同步鎖或監視器(例如同步塊(synchronized section))
- 做一個耗時的數據庫操作并等待結果
- 做一個復雜的計算,耗費大量的時間
- 在循環中(Spinning in a loop)
如果上面任何一步掛住了event loop,讓它花了大量時間( significant amount of time),那么你只能安心等待程序執行。
那么,什么樣是大量時間( significant amount of time)呢?
這個時間,實際上取決于你的應用對并發量的需求。
如果你有單一的event loop并且想每秒處理一萬個http請求,那么很明顯,處理每個請求的時間不能超過0.1毫秒,所以阻塞不能超過這個時間。
這里面的計算不難,作為練習,我們將之留給讀者(The maths is not hard and shall be left as an exercise for the reader)。
如果你的應用沒有響應了,這可能是event loop被阻塞的信號。為了幫助診斷這樣的問題,Vert.x會在檢測到某個event loop一段時間后還未返回時自動打印警告日志。如果在你的日志中看見這樣的警告,那你可得調查調查了。
Thread vertx-eventloop-thread-3 has been blocked for 20458 ms
為了精確定位阻塞發生在何處,Vert.x也會提供堆棧跟蹤消息。
如果你想關閉這些警告或改變設置,可以在創建Vertx對象前,去VertxOptions對象里設置。
執行阻塞式代碼
完美的世界里,不會有戰爭,也不會有饑餓。所有的API都會以異步的方式寫成,小兔和小羊羔會手牽手地穿過陽光明媚的綠草地。
但是,現實世界不是這樣的。。(你有關注最近的新聞嗎?)
(迷之聲:這篇文檔生成時發生了啥??)
事實上,不算其他大多數庫,單單在JVM的子系統里就有同步的API和很多可能造成阻塞的方法。一個好的例子是JDBC,它天然就是同步的;無論怎么使勁,Vert.x也不會魔法,沒辦法撒一點魔力粉就讓它變成異步的。
我們不會整夜不睡的去重寫所有(現存的組件)使它們成為異步的,所以我們需要提供一種方式,以使在Vert.x應用里可以安全的使用“傳統的”阻塞API。
就像前面討論的,為了不妨礙event loop干其他有益的活,不能從它這直接調用阻塞操作。所以你該怎么辦呢?
指定待執行的阻塞代碼和一個結果處理器(result handler),然后調用executeBlocking,當阻塞代碼執行完畢的時候,handler將會以異步的方式被回調(to be called back asynchronous )。
vertx.executeBlocking(future -> {
// Call some blocking API that takes a significant amount of time to return
String result = someAPI.blockingMethod("hello");
future.complete(result);
}, res -> {
System.out.println("The result is: " + res.result());
});
缺省情況下,如果executeBlocking 在同一上下文環境中(例如同一個verticle)被多次調用,那么不同的executeBlocking 會被順序地執行(即一個接一個)。
如果你不在意executeBlocking 執行的順序,那么你可以將ordered參數設置為false。這種情況下,worker pool有可能會并行地執行executeBlocking。
另一種執行阻塞代碼的方法是在worker verticle中干這事。
worker verticle總是由worker pool里的線程來執行。
異步協同
多個異步結果的協同可以由Vert.x的futures來實現。
CompositeFuture.all接受數個future參數(到6為止)并返回一個future;當所有的future都成功了,就返回成功(succeeded)的future,否則返回失敗(failed)的future:
Future<HttpServer> httpServerFuture = Future.future();
httpServer.listen(httpServerFuture.completer());
Future<NetServer> netServerFuture = Future.future();
netServer.listen(netServerFuture.completer());
CompositeFuture.all(httpServerFuture, netServerFuture).setHandler(ar -> {
if (ar.succeeded()) {
// All server started
} else {
// At least one server failed
}
});
由completer返回的handler會完成這個future。
CompositeFuture.any接受數個future參數(到6為止)并返回一個future;只要有一個future成功了,那返回的future也成功(succeeded),否則就失敗(failed):
Future<String> future1 = Future.future();
Future<String> future2 = Future.future();
CompositeFuture.any(future1, future2).setHandler(ar -> {
if (ar.succeeded()) {
// At least one is succeeded
} else {
// All failed
}
});
2017-2-9 更新
新版CompositeFuture 的API 中增加了與all
類似的系列方法:join 。
看文檔的說明,all
和 join
的區別在于:
如果參數列表中的某個Future 失敗了,那么
all
不會繼續等,這個CompositeFuture
將被標記為失敗并完成;而join
會繼續等待直到所有參數完成(不管成功與否)。
compose可以用來鏈式調用future:
FileSystem fs = vertx.fileSystem();
Future<Void> fut1 = Future.future();
Future<Void> fut2 = Future.future();
fs.createFile("/foo", fut1.completer());
fut1.compose(v -> {
fs.writeFile("/foo", Buffer.buffer(), fut2.completer());
}, fut2);
fut2.compose(v -> {
fs.move("/foo", "/bar", startFuture.completer());
}, startFuture);
Verticles
Vert.x有一個簡單的、可擴展的,類似actor的部署方式(actor-like deployment )和開箱即用的并發模型,這方面可以節省下你親自動手的時間精力。
這個模型是完全可選的,如果你不想,Vert.x并不會強迫你以這種方式創建自己的應用。。
這個模型并未嚴格地實現actor模型,但確實與其有相似之處,尤其在并發性、擴展,部署方面。
為了使用這個模型,你需要將代碼寫成verticles的集合。
verticles是由Vert.x部署和運行的代碼塊。verticles可以由任何Vert.x支持的語言寫成,并且單獨的應用可以包含多種語言寫就的verticles。
你可以把verticle看成有點像Actor Model里的actor。
一個典型的應用由同一時間運行在同一Vert.x實例里的很多verticle實例組成的。不同的verticle實例之間通過在event bus上向彼此發送消息來通信。
編寫verticle
verticle類必須實現Verticle接口。
你可以直接實現這個接口,但通常有個更簡單的辦法,就是繼承下面這個抽象類:AbstractVerticle。
舉個例子:
public class MyVerticle extends AbstractVerticle {
// Called when verticle is deployed
public void start() {
}
// Optional - called when verticle is undeployed
public void stop() {
}
}
通常你需要像上面的例子一樣,重載start方法。
Vert.x部署verticle時,會調用其start方法。當該start方法完成時,就認為該verticle已啟動。
你也可以選擇重載stop方法,當verticle被卸載(undeployed)時會調用這個方法。同樣,stop方法完成時,會認為verticle已被終止。
異步verticle的啟動和終止
有時候你可能想在verticle啟動時做點耗時的事,除非完成了,否則不應該認定verticle已成功部署。比如你可能想在start方法里部署其他的verticles。
你不能在start方法里阻塞地等待其他verticles部署完成,這會打破我們的黃金準則。
那該怎么做呢?
合適的途徑是實現異步的start方法。這個版本的start方法有一個future參數,這個方法返回時verticle并不會被認定已經部署完成。
等干完所有活之后(例如啟動其他的verticles)你就可以調用future對象的complete(或者fail)方法;這是一個信號,標記你這里已經都完成了。
下面有個例子:
public class MyVerticle extends AbstractVerticle {
public void start(Future<Void> startFuture) {
// Now deploy some other verticle:
vertx.deployVerticle("com.foo.OtherVerticle", res -> {
if (res.succeeded()) {
startFuture.complete();
} else {
startFuture.fail();
}
});
}
}
類似的,stop方法也有一個異步的版本。如果你做清理工作要花點時間,就可以用它。
public class MyVerticle extends AbstractVerticle {
public void start() {
// Do something
}
public void stop(Future<Void> stopFuture) {
obj.doSomethingThatTakesTime(res -> {
if (res.succeeded()) {
stopFuture.complete();
} else {
stopFuture.fail();
}
});
}
}
提示:并不需要在stop方法中手動卸載某個verticle的子verticles(child verticles),因為Vert.x會在父verticle被卸載時自動卸載它們。
Verticle的類型
有三種不同類型的verticle。
標準verticle(Standard Verticles)
這是最平常并有用的版本,它們會一直由同一個event loop線程執行。下一節里我們會更詳細地討論這個。
Worker Verticles
這一類由worker pool里的線程運行。絕不會有超過一個線程并發地執行單一實例。
多線程版(Multi-threaded) worker verticles
這些還是由worker pool里的線程運行,不過單一實例可以被多個線程并發執行。
標準verticle
標準verticle被創建的時候,它們會被指定給一個event loop線程,然后event loop會調用其start方法。當你從event loop調用任意核心API上可以接受handler的方法時,Vert.x保證那些handlers會由同樣的event loop執行。
這意味著我們可以保證一個verticle實例里的所有代碼都會在同一個event loop上執行(只要你不自己創建線程并調用它!)。
這同樣意味著可以像單線程應用那樣來寫所有代碼,至于多線程并發和擴展的問題交給Vert.x就可以了。不需要有同步和易變性(volatile)的困擾,這樣你也可以避免‘傳統的’手寫多線程應用中普遍會碰到的狀況,譬如競爭條件(race conditions)和死鎖(deadlock)。
Worker Verticles
worker verticle和標準verticle挺像的,不同點在于worker verticle由Vert.x的worker 線程池中的線程執行,而標準verticle由event loop執行。
worker verticle是為調用阻塞代碼而設計的。它們不會阻塞任意的event loop。
如果你不想用worker verticle來執行阻塞代碼,那也可以通過直接運行內聯阻塞代碼的方式(就是前文所述的executeBlocking)。
如果你想將某個verticle作為worker verticle部署,可以通過調用setWorker方法。
DeploymentOptions options = new DeploymentOptions().setWorker(true);
vertx.deployVerticle("com.mycompany.MyOrderProcessorVerticle", options);
worker verticle實例絕不會被多個線程并發執行,但可以被不同線程在不同時候執行。
多線程版worker verticle
一個多線程版worker verticle就像普通的worker verticle一般,但它可以被不同線程并發執行。
警告:多線程版worker 線程是個高級特性,絕大多數應用對此并無需求。為了這些verticle的并發執行,你需要使用標準的Java多線程編程技能,小心地使verticles保持一致的狀態。
以編程的方式部署verticle
你可以使用deployVerticle系列方法中的一個來部署verticle,只需要知道verticle的名稱或者你自己創建一個verticle實例丟過去。
注意:部署verticle實例的方式是Java專有的。
Verticle myVerticle = new MyVerticle();
vertx.deployVerticle(myVerticle);
你也可以通過指定verticle的名稱來部署。
verticle的實例化需要用到特定的VerticleFactory,verticle的名稱就是用來查詢這個特定的工廠類。
不同的語言有不同的工廠類,用來初始化verticle。原因多種多樣,比如運行時從Maven加載服務或者獲取verticles。
這樣你可以部署任意以Vert.x支持的語言寫就的verticle。
下面是一個部署不同種verticle的例子:
vertx.deployVerticle("com.mycompany.MyOrderProcessorVerticle");
// Deploy a JavaScript verticle
vertx.deployVerticle("verticles/myverticle.js");
// Deploy a Ruby verticle verticle
vertx.deployVerticle("verticles/my_verticle.rb");
從verticle的名稱映射到verticle factory的規則
當使用名稱部署verticle時,名稱的作用是選出實際中用來實例化這個verticle的verticle factory。
verticle的名稱可以有一個前綴:前綴是個字符串,后面緊跟著一個冒號;如果前綴存在將被用于查詢對應的factory。
即:
js:foo.js // Use the JavaScript verticle factory
groovy:com.mycompany.SomeGroovyCompiledVerticle // Use the Groovy verticle factory
service:com.mycompany:myorderservice // Uses the service verticle factory
如果沒有前綴,Vert.x會尋找后綴來查詢factory。
即:
foo.js // Will also use the JavaScript verticle factory
SomeScript.groovy // Will use the Groovy verticle factory
如果前后綴都不存在,那么Vert.x會假定這是一個完全限定類名(FQCN)的Java verticle,并試著循此實例化。
Verticle Factories如何定位呢?
絕大多數verticle factories都是從類路徑(classpath)中加載的,在Vert.x啟動時注冊。
同樣地,如果你希望用編程的方式注冊、注銷verticle factories,那么有registerVerticleFactory和unregisterVerticleFactory可用。
等待部署完成
verticle的部署是異步進行的,可能完成的時候對部署方法的調用都已經返回一陣子了。
如果你想在部署完成時收到通知,可以在部署時指定一個完成處理器(completion handler):
vertx.deployVerticle("com.mycompany.MyOrderProcessorVerticle", res -> {
if (res.succeeded()) {
System.out.println("Deployment id is: " + res.result());
} else {
System.out.println("Deployment failed!");
}
});
如果部署成功,此handler會收到一個字符串,這里面包含了部署的ID。
后面在你卸載這次部署的verticle時,會用到這個ID。
卸載部署的verticle
可以使用undeploy來卸載已部署的verticle。
卸載本身也是異步的。所以如果你想在完成的時候收到通知,處理方法同部署的時候:
vertx.undeploy(deploymentID, res -> {
if (res.succeeded()) {
System.out.println("Undeployed ok");
} else {
System.out.println("Undeploy failed!");
}
});
指定verticle實例的數量
用verticle的名稱部署時,可以指定verticle實例的數量:
DeploymentOptions options = new DeploymentOptions().setInstances(16);
vertx.deployVerticle("com.mycompany.MyOrderProcessorVerticle", options);
這個特性在擴展到多核cpu上時很有幫助。比如你要部署一個web服務器的verticle,并且你的機器上有多個核心;為了這多個核心能充分發揮自己的光和熱,你可以部署上多個實例。
給verticle傳遞配置參數
部署時可以將配置以JSON的形式傳遞給verticle:
JsonObject config = new JsonObject().put("name", "tim").put("directory", "/blah");
DeploymentOptions options = new DeploymentOptions().setConfig(config);
vertx.deployVerticle("com.mycompany.MyOrderProcessorVerticle", options);
之后配置信息將可通過Context對象使用,或者直接使用config方法。
返回的配置是一個JSON對象,所以你可以像下面這樣取數據:
System.out.println("Configuration: " + config().getString("name"));
在verticle中訪問環境變量
環境變量和系統屬性可以用Java API訪問:
System.getProperty("prop");
System.getenv("HOME");
verticle隔離組(Verticle Isolation groups)
缺省情況下,Vert.x有一個扁平的類路徑(flat classpath),部署vertilce時,Vert.x會使用當前的類加載器(classloader)--而不是創建一個新的。多數情況下,這都是最簡單、清晰、穩健的做法。
然而,有時候你可能想把某些verticle的部署與其他的隔離開來。
譬如,你想在同一個Vert.x實例中部署同一個verticle的不同版本,他倆還有著相同的類名;又或者你的兩個不同verticle分別用到了同一個類庫的不同版本。
使用隔離組時,你需要提供待隔離類的名稱列表。方法setIsolatedClasses可以搞定這個事。傳入的名稱可以是類似com.mycompany.myproject.engine.MyClass這樣的完全限定類名;還可以是類似com.mycompany.myproject.這樣帶通配符的,這會匹配到包com.mycompany.myproject*里的任意類和任意子包。
請注意唯有匹配到的類才會被隔離,其他的類仍然由當前的類加載器載入。
如果想從非主類路徑中加載類和資源,那你可以用setExtraClasspath方法提供額外的類路徑條目。
警告:使用這個特性要小心。類加載器們也可能帶來一堆bug,使你的排錯工作變得困難(譯注:大家知道bug在英文里有蟲子和計算機程序錯誤的意思;所以此處前面說蟲子,后面說除錯困難)。
這兒有個利用隔離組來隔離一個verticle的部署的例子。
DeploymentOptions options = new DeploymentOptions().setIsolationGroup("mygroup");
options.setIsolatedClasses(Arrays.asList("com.mycompany.myverticle.*",
"com.mycompany.somepkg.SomeClass", "org.somelibrary.*"));
vertx.deployVerticle("com.mycompany.myverticle.VerticleClass", options);
高可用性
部署verticle時可以打開高可用性(HA),在這樣的上下文環境里,若某個Vert.x實例上的某個vertilce意外地掛掉,集群里的另一個Vert.x實例將會重新部署這個verticle。
以高可用性運行verticle時,只需要在命令行后面加上** -ha **:
vertx run my-verticle.js -ha
打開高可用性時,無需添加** -cluster **。
更多關于高可用性和配置的細節可以在下面的高可用性和故障轉移(High Availability and Fail-Over)一節中找到。
從命令行運行verticles
將依賴添加到Vert.x核心包里,就能以正常方式在你的maven或gradle項目中直接使用Vert.x。
不過你也可以直接從命令行運行verticle。
為了達成這個目的,你要下載并安裝好Vert.x的發布包,并將安裝目錄下的** bin 目錄添加到 PATH 環境變量,同樣要確保Java8 的JDK在 PATH **里。
注意:為了動態編譯Java代碼,JDK是必要的(言下之意,只裝JRE是不夠的)。
一切就緒,現在可以用** vertx run **運行verticle了。下面有幾個例子:
# Run a JavaScript verticle
vertx run my_verticle.js
# Run a Ruby verticle
vertx run a_n_other_verticle.rb
# Run a Groovy script verticle, clustered
vertx run FooVerticle.groovy -cluster
至于Java verticle,甚至不需要編譯你就可以直接運行它!
vertx run SomeJavaSourceFile.java
Vert.x會在運行前動態地編譯它。這點在快速建立原型和演示時特別有用。不需要先設置Maven或Gradle就能開始了。
有關在命令行執行vertx時各種其他可用的選項的所有信息,只需要在命令行輸入vertx即可獲得。
退出Vert.x
Vert.x實例維護的進程不是守護進程(daemon threads),所以它們會阻止JVM退出。
如果你以嵌入的方式使用Vert.x,工作完成的時候,你可以調用close方法關閉它。
這樣做會關閉所有內部線程池、其他的資源,并允許JVM退出。
上下文對象(The Context object)
Vert.x給handler提供事件、或者調用verticle的start/stop方法時,其執行狀況都是與一個Context(上下文)聯系在一起的。通常,這個context是一個與特定的event loop線程綁定的event-loop context。所以與此context相關的執行動作都發生在同一確定的event loop線程上。至于worker verticle和運行內聯的阻塞代碼時,會有一個worker context與之關聯,這些動作都由worker 線程池里的線程運行。
利用getOrCreateContext方法,可以獲得上下文環境:
Context context = vertx.getOrCreateContext();
如果當前線程已經存在一個context與之關聯,它會重用這個context對象。否則會創建context的一個新實例。可以像下面這樣測試下取到的context的類型:
Context context = vertx.getOrCreateContext();
if (context.isEventLoopContext()) {
System.out.println("Context attached to Event Loop");
} else if (context.isWorkerContext()) {
System.out.println("Context attached to Worker Thread");
} else if (context.isMultiThreadedWorkerContext()) {
System.out.println("Context attached to Worker Thread - multi threaded worker");
} else if (! Context.isOnVertxThread()) {
System.out.println("Context not attached to a thread managed by vert.x");
}
在你拿到一個context對象后,可以在此context里異步地運行代碼。換句話說,你提交的任務最終會運行在同樣的context里:
vertx.getOrCreateContext().runOnContext( (v) -> {
System.out.println("This will be executed asynchronously in the same context");
});
當有數個handler運行在同一context里時,它們可能會希望共享一些數據。context對象提供了存取共享在上下文里數據的方法。例如,你要傳遞數據過去做點事,可以用runOnContext方法:
final Context context = vertx.getOrCreateContext();
context.put("data", "hello");
context.runOnContext((v) -> {
String hello = context.get("data");
});
context對象也允許你通過config方法訪問verticle的配置信息。去看看** 給verticle傳遞配置 **一節吧,你將獲得關于此項配置的更多細節。
定期、延時執行
在Vert.x里,延時或定期執行是很普遍的。
在標準verticle里,你不能以使線程休眠的方式引入延遲;這樣干會阻塞event loop線程。
取而代之的是Vert.x定時器,定時器分為一次性(one-shot)和周期性(periodic)的。下面我們會分別討論。
一次性定時器
一段確定的延時過后,一次性定時器將調用事件handler,度量衡是毫秒。
設置一個觸發一次的定時器用到setTimer方法,它有兩個參數:延時和一個handler。
long timerID = vertx.setTimer(1000, id -> {
System.out.println("And one second later this is printed");
});
System.out.println("First this is printed");
返回值是定時器的ID(long類型),它具有唯一屬性。之后你可以用這個ID來取消定時器。這個handler也會收到定時器的ID。
周期性定時器
類似的,利用setPeriodic方法可以設置一個定期觸發的定時器。
初始的延遲值就是周期間隔。
返回值與一次性定時器一樣,此處不再贅述。
定時器的事件handler的參數也與一次性定時器一致:
記住,定時器會定期觸發。如果你的定期處理需要耗費大量時間,你的定時器事件可能會連續運行甚至糟糕到堆積在一起。
在這種情況下,你應該考慮轉而使用setTimer。一旦你的處理完成了,你可以再設置下一個定時器。
long timerID = vertx.setPeriodic(1000, id -> {
System.out.println("And every second this is printed");
});
System.out.println("First this is printed");
取消定時器
像下面這樣,調用cancelTimer方法,指定定時器ID,即可取消周期定時器。
verticle里的自動清理(Automatic clean-up in verticles)
如果你是在verticle內部創建的定時器,那么verticle被卸載時,這些定時器將被自動關閉。
事件總線(The Event Bus)
event bus是Vert.x的神經系統。
每個Vert.x實例都擁有單獨的一個event bus實例,你可以通過eventBus方法得到它。
應用的不同部分,不管是否在同一個Vert.x實例里,即使是不同語言編寫的,都可以通過event bus彼此交流。
甚至瀏覽器里運行的的客戶端JavaScript也可以通過同一個event bus相互通信。
event bus在多個服務器和多個瀏覽器間形成了一個分布式的點對點消息系統。
event bus支持發布/訂閱(publish/subscribe)、點對點、請求-響應(request-response)這三種消息模式。
event bus的API很簡單,主要包括注冊handlers,注銷handlers,發送和發布消息。
基本概念
尋址(Addressing)
消息通過event bus發送到某個地址(address)。
Vert.x沒有花哨的令人困擾的尋址方案。Vert.x里地址就是字符串。任意字符串都有效。不過使用某種命名策略還是很明智的,例如使用分隔符限定命名空間。
這里是一些有效的地址:europe.news.feed1, acme.games.pacman, sausages, and X。
處理器(Handlers)
消息由handlers接收,所以你需要把handler注冊到地址上。
同一個地址可以注冊多個不同的handler。
某個handler也可以被注冊在多個不同的地址上。
發布/訂閱消息(Publish / subscribe messaging)
event bus支持發布(publishing)消息。
消息被發布到某個地址,這意味著把消息分發到注冊在此地址上的所有handlers。
這就是我們很熟悉的發布/訂閱模式。
點對點和請求-響應(Point to point and Request-Response messaging)
event bus也支持點對點消息。
當消息被發送到某個地址,Vert.x會把消息路由給注冊在此地址上的某個handler。
如此此地址上注冊了超過一個handler,Vert.x將會通過一個不嚴格的輪詢算法(non-strict round-robin algorithm)從中選擇一個。
在點對點的消息機制中,發消息時可以選擇指定一個應答handler(reply handler)。
當有接收者收到消息并處理后,接收者可以選擇是否答復此消息。如果選擇答復,上述的reply handler將被調用。
發送者收到消息回應后,同樣可以做出回應。這可以無限地重復下去,并允許兩個不同的verticle間形成對話。
這種通用的消息模式稱為請求-響應模式。
盡力分發(Best-effort delivery)
Vert.x會盡最大的努力分發消息,絕不會有意丟棄某些消息。這被稱為盡力分發。
然而,在event bus部分或全部失效的情況下,消息還是有可能丟失。
如果你的應用很關心這一點,那么編碼時應該注意使你的handler是冪等的(be idempotent),并且發送方應該在恢復后嘗試重新發送消息。
消息的類型
任何的基本類型/簡單類型,字符串或者buffers都可以被當成消息發送出去。
但是Vert.x里通常使用JSON格式的消息。
在Vert.x支持的語言里,創建、讀取、解析JSON都很容易,所以它就成了Vert.x上的通用語(lingua franca)。
當然了,并不是必須使用JSON。
event bus是很靈活的,支持在其上發送任意專有的對象。你只需為此定義一個編解碼器(codec)。
The Event Bus API
下面讓我們來看看API。
獲取event bus對象
可以像下面這樣拿到event bus對象的引用:
EventBus eb = vertx.eventBus();
每個Vert.x實例有唯一的event bus實例。
注冊handlers
注冊handler最簡單的方法是用consumer方法。看例子:
EventBus eb = vertx.eventBus();
eb.consumer("news.uk.sport", message -> {
System.out.println("I have received a message: " + message.body());
});
當你的handler收到一條消息時,handler會被調用,而消息(message)會作為參數傳遞過去。
consumer方法會返回一個MessageConsumer實例。
這個對象可以用來注銷handler,或將handler當作流(stream)來使用。
或者你也可以用consumer方法得到一個未設置handler的MessageConsumer 對象,隨后再設置handler:
EventBus eb = vertx.eventBus();
MessageConsumer<String> consumer = eb.consumer("news.uk.sport");
consumer.handler(message -> {
System.out.println("I have received a message: " + message.body());
});
當在一個集群event bus上注冊了handler時,完成在集群上每個節點的注冊需要花點時間。
如果你想在完成時得到通知,你可以在MessageConsumer 對象上注冊一個completion handler。
consumer.completionHandler(res -> {
if (res.succeeded()) {
System.out.println("The handler registration has reached all nodes");
} else {
System.out.println("Registration failed!");
}
});
注銷handlers
注銷handler可以通過調用unregister方法完成。
在集群event bus做這件事時,同樣要花點時間等其傳播到各個節點,所以你也可以通過unregister方法得到通知。
consumer.unregister(res -> {
if (res.succeeded()) {
System.out.println("The handler un-registration has reached all nodes");
} else {
System.out.println("Un-registration failed!");
}
});
發布消息
發布一個消息只需簡單地調用publish方法,指定要發往的地址。
eventBus.publish("news.uk.sport", "Yay! Someone kicked a ball");
這個消息會被分發到注冊在地址 news.uk.sport 上的所有handlers。
發送消息
發送消息的結果是注冊在此地址上的handler只有一個會收到消息。這是點對點的消息模式。
你可以用send方法發送消息:
eventBus.send("news.uk.sport", "Yay! Someone kicked a ball");
設置消息頭(Setting headers on messages)
event bus 上傳輸的消息也可以包含一些消息頭(headers)。
可以在發送/發布消息時指定一個DeliveryOptions對象來做這件事:
DeliveryOptions options = new DeliveryOptions();
options.addHeader("some-header", "some-value");
eventBus.send("news.uk.sport", "Yay! Someone kicked a ball", options);
消息的順序
Vert.x會將消息以發送者送出的順序分發給handler。
消息對象
你在handler里收到的消息是一個Message對象。
消息的body就是發送過來的對象。
消息頭可以通過headers方法得到。
消息/發送回應的知識點(Acknowledging messages / sending replies)
使用send方法時event bus會嘗試把消息發送到注冊在event bus上的MessageConsumer對象。
某些情況下,發送方可能想知道消息已經被接收并處理了。
為了讓發放方了解消息已被處理,consumer可以通過調用reply方法給予回應。
如果這么做了,那么發送方將會收到一個回應,并且回應handler將被調用。看下面這個例子:
接收方:
MessageConsumer<String> consumer = eventBus.consumer("news.uk.sport");
consumer.handler(message -> {
System.out.println("I have received a message: " + message.body());
message.reply("how interesting!");
});
發送方:
eventBus.send("news.uk.sport", "Yay! Someone kicked a ball across a patch of grass", ar -> {
if (ar.succeeded()) {
System.out.println("Received reply: " + ar.result().body());
}
});
回應可以帶一個消息體,你可以在其中放置一些有用的信息。
“處理”實際上是由應用程序定義的,其中會發生什么完全依賴于consumer做了什么;Vert.x的event bus并不知道也不關心這些。
例如:
- 一個簡單的消息consumer,它實現了返回當天時間的服務;可以確認回應的消息體中包含了這個時間。
- 一個實現了持久化隊列的消息consumer,如果消息被成功地持久化在存儲中可以確認是true,反之則為false。
- 一個處理訂單的消息consumer,當訂單被成功處理時,它可以從數據庫里被刪掉,這時候可以確認是true。
指定超時時間的發送(Sending with timeouts)
在發送一個帶回應handler的消息時,可以在DeliveryOptions對象里指定超時的時間。
如果在這段時間里沒有收到回應,回應handler將被調用,并以失敗結束。
缺省的超時時間是30秒。
發送失敗(Send Failures)
消息發放也可能因為其他原因失敗,包括:
- 消息發送到的地址沒有handler可用。
- 接收方顯示地調用fail方法返回了失敗的信息。
所有的情況下回應的handler都會被調用,并返回特定的錯誤。
消息編解碼器(Message Codecs)
只要你定義并注冊了相關的message codec,就可以在event bus上發送任意的對象。
當發送/發布消息時,你需要在DeliveryOptions對象上指定codec的名稱:
eventBus.registerCodec(myCodec);
DeliveryOptions options = new DeliveryOptions().setCodecName(myCodec.name());
eventBus.send("orders", new MyPOJO(), options);
如果你想一直使用同一個codec,可以將它注冊成缺省的codec,這樣后面就不用每次發消息時再專門指定:
eventBus.registerDefaultCodec(MyPOJO.class, myCodec);
eventBus.send("orders", new MyPOJO());
unregisterCodec方法可以用來注銷codec。
消息codec并不總是對同樣的類型進行編碼、解碼。例如,你可以寫一個codec用來發送MyPOJO類,而當消息送達handler時,可以是MyOtherPOJO類。
集群event bus
event bus并不只是存在于單一的Vert.x實例中。將多個Vert.x實例組成集群后,可以形成一個單獨的,分布式的event bus。
以編程實現集群
如果你以編碼的方式創建了Vert.x實例,將Vert.x實例配置成集群式的即可得到集群event bus;
VertxOptions options = new VertxOptions();
Vertx.clusteredVertx(options, res -> {
if (res.succeeded()) {
Vertx vertx = res.result();
EventBus eventBus = vertx.eventBus();
System.out.println("We now have a clustered event bus: " + eventBus);
} else {
System.out.println("Failed: " + res.cause());
}
});
當然你應該確保classpath中有一個ClusterManager的實現,例如缺省的HazelcastClusterManager。
命令行里實現集群
你可以在命令行里運行集群vertx:
vertx run my-verticle.js -cluster
verticle的自動清理
如果你在verticle內部注冊了event bus handlers,當這些verticle被卸載時handlers將被自動注銷。
JSON
與其他語言不同,Java對JSON并沒有提供頭等的支持;所以我們提供了兩個類,使得JSON的處理容易些。
JSON 對象
JsonObject類用來表示JSON對象。
JSON對象基本可以認為是個map,鍵是字符串,而值可以是JSON支持的類型中的一種(字符串、數字、布爾型)。
JSON對象也支持null 值。
創建JSON對象
默認的構造器可以創建一個空的JSON對象。
你也可以從JSON格式的字符串創建一個JSON對象:
String jsonString = "{\"foo\":\"bar\"}";
JsonObject object = new JsonObject(jsonString);
向JSON對象中添加條目
put方法可以用于往JSON對象中添加條目。
put方法支持流式API:
JsonObject object = new JsonObject();
object.put("foo", "bar").put("num", 123).put("mybool", true);
從JSON對象中取值
可以使用類似getXXX方法從JSON對象中取值,例如:
String val = jsonObject.getString("some-key");
int intVal = jsonObject.getInteger("some-other-key");
將JSON對象編碼為字符串
encode方法用來將JSON對象轉換為字符串。
JSON 數組
JsonArray類用來表示JSON 數組。
一個JSON 數組是一些值(字符串、數字或者布爾型)組成的序列。
JSON 數組也可以包括null 。
創建JSON 數組
默認的構造器可以創建一個空的JSON 數組。
可以從JSON格式的字符串創建JSON 數組:
String jsonString = "[\"foo\",\"bar\"]";
JsonArray array = new JsonArray(jsonString);
往JSON數組中添加元素
add方法:
JsonArray array = new JsonArray();
array.add("foo").add(123).add(false);
從JSON 數組中取值
類似下面這樣:
String val = array.getString(0);
Integer intVal = array.getInteger(1);
Boolean boolVal = array.getBoolean(2);
將JSON 數組轉換為字符串
使用encode即可。
Buffers
Vert.x中大量使用buffers傳輸數據。
buffer是一個可以讀寫的字節序列,超出其容量時,它會自動擴展。可以將其看成一個智能的字節數組。
創建buffers
可以使用靜態方法Buffer.buffer創建buffers。
buffer可以由字符串或字節數組初始化,當然,空的buffer也是允許的。
這里有些例子。空buffer:
Buffer buff = Buffer.buffer();
字符串初始化的buffer,這里的字符串將使用UTF-8編碼成buffer。
Buffer buff = Buffer.buffer("some string");
或者你可以指定編碼:
Buffer buff = Buffer.buffer("some string", "UTF-16");
從字節數組創建:
byte[] bytes = new byte[] {1, 3, 5};
Buffer buff = Buffer.buffer(bytes);
如果你知道將會有多少數據待寫入,可以在創建buffer時指定buffer的尺寸。這樣buffer創建時就會分配這么多內存,這在效率上要優過邊寫入邊擴容。
注意,這樣創建的buffer仍然是空的(empty)。創建時并不會有0填充于其中。
Buffer buff = Buffer.buffer(10000);
寫buffer
寫入buffer有兩種方式:附加(appending)、隨機存取(random access)。這兩種方式下,buffer都會自動擴容。不會產生**IndexOutOfBoundsException **異常。
Appending to a Buffer
往buffer上附加信息,可以使用**appendXXX **系列方法。有適合各種類型的append方法。
append系列方法的返回值就是buffer本身,所以適用鏈式寫法:
Buffer buff = Buffer.buffer();
buff.appendInt(123).appendString("hello\n");
socket.write(buff);
Random access buffer writes
你也可以通過一系列**setXXX **方法在指定的索引處寫入數據。set系列方法的第一個參數都是索引值。
buffer會自動擴容的。
Buffer buff = Buffer.buffer();
buff.setInt(1000, 123);
buff.setString(0, "hello");
讀buffer
**getXXX **系列方法用來從buffer中讀取數據。get系列方法的第一個參數也是指示從哪開始讀的索引值。
Buffer buff = Buffer.buffer();
for (int i = 0; i < buff.length(); i += 4) {
System.out.println("int value at " + i + " is " + buff.getInt(i));
}
使用無符號數
可以使用**getUnsignedXXX、appendUnsignedXXX、setUnsignedXXX **系列方法讀寫buffer。當你在為網絡協議實現編解碼器時,如果想將帶寬消耗優化到極致,這個特性能幫上忙。
下面這個例子里,使用一個字節在指定位置寫入200:
Buffer buff = Buffer.buffer(128);
int pos = 15;
buff.setUnsignedByte(pos, (short) 200);
System.out.println(buff.getUnsignedByte(pos));
控制臺將顯示‘200’。
buffer的長度
length方法可以獲得buffer的長度。buffer的長度是最大的索引值+1。
復制buffer
使用copy方法。
將buffer分片(slicing buffers)
slice方法用來將buffer分片,切分出來的新buffer與原buffer共享緩存區。
buffer重用
在buffer被寫入socket或類似地方后,它就不能再被使用了。