Storm入門指南第二章 入門
本章我們將會建立一個Storm工程和我們的第一個Storm topology。
提示:下述假設你已經安裝JRE1.6或者更高階版本。推薦使用Oracle提供的JRE:http://www.java.com/downloads/.
操作模式
在開始建立專案之前,瞭解Storm的操作模式(operation modes)是很重要的。Storm有兩種執行方式:
本地模式
在本地模式下,Storm topologies 執行在本地機器的一個JVM中。因為本地模式下檢視所有topology元件共同工作最為簡單,所以這種模式被用於開發、測試和除錯。例如,我們可以調整引數,這使得我們可以看到我們的topology在不同的Storm配置環境中是如何執行的。為了以本地模式執行topologies,我們需要下載Storm的開發依賴包,這是我們開發、測試topologies所需的所有東西。當我們建立自己的第一個Storm工程的時候我們很快就可以看到是怎麼回事了。
提示:本地模式下執行一個topology同Storm叢集中執行類似。然而,確保所有元件執行緒安全非常重要,因為當它們被部署到遠端模式時,它們可能執行在不同的JVM或者不同的物理機器上,此時,它們之間不能直接交流或者共享記憶體。
在本章的所有示例中,我們都以本地模式執行。
遠端模式
在遠端模式下,我們將topology提交到Storm叢集中,Storm叢集由許多程式組成,這些程式通常執行在不同的機器上。遠端模式下不顯示除錯資訊,這也是它被認為是產品模式的原因。然而,在一臺機器上建立一個Storm叢集也是可能的,並且在部署至產品前這樣做還是一個好方法,它可以確保將來在一個成熟的產品環境中執行topology不會出現任何問題。
譯者的話:所謂產品環境/模式,指的是程式碼比較成熟,可以當成產品釋出了,與開發環境相對。
在第六章中可以瞭解到更多關於遠端模式的內容,我會在附錄B中展示如何安裝一個叢集。
Hello World Storm
在這個專案中,我們會建立一個簡單的topology來統計單詞個數,我們可以將它看成是Storm topologies中的“Hello World”。然而,它又是一個非常強大的topology,因為它幾乎可以擴充套件到無限大小,並且經過小小的修改,我們甚至可以使用它建立一個統計系統。例如,我們可以修改本專案來找到Twitter上的熱門話題。
為了建立這個topology,我們將使用一個spout來負責從檔案中讀取單詞,第一個bolt來標準化單詞,第二個bolt去統計單詞個數,如圖2-1所示:
你可以在https://github.com/storm-book/examples-ch02-getting_started/zipball/master下載本例原始碼的ZIP檔案。
譯者的話:本站有備份:http://www.flyne.org/example/storm/storm-book-examples-ch02-getting_started-8e42636.zip
提示:如果你使用git(一個分散式的版本控制和原始碼管理工具),則可以執行命令:git clone git@github.com:storm-book/examplesch02-getting_started.git進入你想要下載的原始碼所在的目錄。
檢查Java安裝
搭建環境的第一步就是檢查正在執行的Java版本。執行java -version命令,我們可以看到類似如下資訊:
java -version
java version “1.6.0_26″
Java(TM) SE Runtime Environment(build 1.6.0_26-b03)
Java HotSpot(TM) Server VM (build 20.1-b02,mixed mode)
如果沒有,檢查下你的Java安裝。(見http://www.java.com/download/.)
建立專案
首先,建立一個資料夾,用於存放這個應用(就像對於任何Java應用一樣),該資料夾包含了整個專案的原始碼。
接著我們需要下載Storm的依賴包——新增到本應用classpath的jar包集合。可以通過下面兩種方式完成:
- 下載依賴包,解壓,並將它們加入classpath路徑
- 使用Apache Maven
提示:Maven是一個軟體專案管理工具,可以用於管理一個專案開發週期中的多個方面(從從依賴包到釋出構建過程),在本書中我們會廣泛使用Maven。可以使用mvn命令檢查maven是否安裝,如果未安裝,可以從http://maven.apache.org/download.html下載。
下一步我們需要新建一個pom.xml檔案(pom:project object model,專案的物件模型)去定義專案的結構,該檔案描述了依賴包、封裝、原始碼等等。這裡我們將使用由nathanmarz構建的依賴包和Maven庫,這些依賴包可以在https://github.com/nathanmarz/storm/wiki/Maven找到。
提示:Storm的Maven依賴包引用了在本地模式下執行Storm所需的所有函式庫。
使用這些依賴包,我們可以寫一個包含執行topology基本的必要元件的pom.xml檔案:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
|
< modelVersion >4.0.0</ modelVersion > < groupId >storm.book</ groupId > < artifactId >Getting-Started</ artifactId > < version >0.0.1-SNAPSHOT</ version > < build > < plugins > < plugin > < groupId >org.apache.maven.plugins</ groupId > < artifactId >maven-compiler-plugin</ artifactId > < version >2.3.2</ version > < configuration > < source >1.6</ source > < target >1.6</ target > < compilerVersion >1.6</ compilerVersion > </ configuration > </ plugin > </ plugins > </ build > < repositories > <!--
Repository where we can found the storm dependencies --> < repository > < id >clojars.org</ id > </ repository > </ repositories > < dependencies > <!--
Storm Dependency --> < dependency > < groupId >storm</ groupId > < artifactId >storm</ artifactId > < version >0.6.0</ version > </ dependency > </ dependencies > </ project > |
前幾行指定了專案名稱、版本;然後我們新增了一個編譯器外掛,該外掛告訴Maven我們的程式碼應該用Java1.6編譯;接著我們定義庫(repository)(Maven支援同一個專案的多個庫),clojars是Storm依賴包所在的庫,Maven會自動下載本地模式執行Storm需要的所有子依賴包。
本專案的目錄結構如下,它是一個典型的Maven Java專案。
java目錄下的資料夾包含了我們的原始碼,並且我們會將我們的單詞檔案放到resources資料夾中來處理。
建立第一個topology
為建立我們第一個topology,我們要建立執行本例(統計單詞個數)的所有的類。本階段例子中的有些部分不清楚很正常,我們將在接下來的幾個章節中進一步解釋它們。
Spout(WordReader類)
WordReader類實現了IRichSpout介面,該類負責讀取檔案並將每一行傳送到一個bolt中去。
提示:spout傳送一個定義欄位(field)的列表,這種架構允許你有多種bolt讀取相同的spout流,然後這些bolt可以定義欄位(field)供其他bolt消費。
例2-1包含WordReader類的完整程式碼(後面會對程式碼中的每個部分進行分析)
例2-1.src/main/java/spouts/WordReader.java
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
|
package
spouts; import
java.io.BufferedReader; import
java.io.FileNotFoundException; import
java.io.FileReader; import
java.util.Map; import
backtype.storm.spout.SpoutOutputCollector; import
backtype.storm.task.TopologyContext; import
backtype.storm.topology.IRichSpout; import
backtype.storm.topology.OutputFieldsDeclarer; import
backtype.storm.tuple.Fields; import
backtype.storm.tuple.Values; public
class
WordReader implements
IRichSpout{ private
SpoutOutputCollector collector; private
FileReader fileReader; private
boolean
completed= false ; private
TopologyContext context; public
boolean
isDistributed(){ return
false ;} public
void
ack(Object msgId) { System.out.println( "OK:" +msgId); } public
void
close(){} public
void
fail(Object msgId) { System.out.println( "FAIL:" +msgId); } /** *
該方法用於讀取檔案併傳送檔案中的每一行 */ public
void
nextTuple() { /** *
The nextuple it is called forever, so if we have beenreaded the file *
we will wait and then return */ if (completed){ try
{ Thread.sleep( 1000 ); }
catch (InterruptedException
e) { //Do
nothing } return ; } String
str; //Open
the reader BufferedReader
reader = new
BufferedReader(fileReader); try { //Read
all lines while ((str=reader.readLine())!= null ){ /** *
By each line emmit a new value with the line as a their */ this .collector.emit( new
Values(str),str); } } catch (Exception
e){ throw
new
RuntimeException( "Errorreading
tuple" ,e); } finally { completed
= true ; } } /** *
We will create the file and get the collector object */ public
void
open(Map conf,TopologyContext context,SpoutOutputCollector collector) { try
{ this .context=context; this .fileReader= new
FileReader(conf.get( "wordsFile" ).toString()); }
catch (FileNotFoundException
e) { throw
new
RuntimeException( "Errorreading
file[" +conf.get( "wordFile" )+ "]" ); } this .collector=collector; } /** *
宣告輸出欄位“line” */ public
void
declareOutputFields(OutputFieldsDeclarer declarer) { declarer.declare( new
Fields( "line" )); } } |
在任何spout中呼叫的第一個方法都是open()方法,該方法接收3個引數:
- TopologyContext:它包含了所有的topology資料
- conf物件:在topology定義的時候被建立
- SpoutOutputCollector:該類的例項可以讓我們傳送將被bolts處理的資料。
下面的程式碼塊是open()方法的實現:
1
2
3
4
5
6
7
8
9
|
public
void
open(Map conf,TopologyContext context,SpoutOutputCollector collector) { try
{ this .context=context; this .fileReader= new
FileReader(conf.get( "wordsFile" ).toString()); }
catch (FileNotFoundException
e) { throw
new
RuntimeException( "Errorreading
file[" +conf.get( "wordFile" )+ "]" ); } this .collector=collector; } |
在open()方法中,我們也建立了reader,它負責讀檔案。接著,我們需要實現nextTuple()方法,在該方法中傳送要被bolt處理的值(values)。在我們的例子中,這個方法讀檔案並且每行傳送一個值。
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
public
void
nextTuple() { if (completed){ try
{ Thread.sleep( 1000 ); }
catch (InterruptedException
e) { //Do
nothing } return ; } String
str; //Open
the reader BufferedReader
reader = new
BufferedReader(fileReader); try { //Read
all lines while ((str=reader.readLine())!= null ){ /** *
By each line emmit a new value with the line as a their */ this .collector.emit( new
Values(str),str); } } catch (Exception
e){ throw
new
RuntimeException( "Errorreading
tuple" ,e); } finally { completed
= true ; } } |
提示:Values類是ArrayList的一個實現,將列表中的元素傳遞到構造方法中。
nextTuple()方法被週期性地呼叫(和ack()、fail()方法相同的迴圈),當沒有工作要做時,nextTuple()方法必須釋放對執行緒的控制,以便其他的方法有機會被呼叫。因此必須在nextTuple()第一行檢查處理是否完成,如果已經完成,在返回前至少應該休眠1秒來降低處理器的負載,如果還有工作要做,則將檔案中的每一行讀取為一個值併傳送出去。
提示:元組(tuple)是一個值的命名列表,它可以是任何型別的Java物件(只要這個物件是可以序列化的)。預設情況下,Storm可以序列化的常用型別有strings、byte arrays、ArrayList、HashMap和HashSet。
Bolt(WordNormalizer&WordCounter類)
上面我們設計了一個spout來讀取檔案,並且每讀取一行傳送一個元組(tuple)。現在,我們需要建立兩個bolt處理這些元組(見圖2-1)。這些bolt實現了IRichBolt介面。
在bolt中,最重要的方法是execute()方法,每當bolt收到一個元組,該方法就會被呼叫一次,對於每個收到的元組,該bolt處理完之後又會傳送幾個bolt。
提示:一個spout或bolt可以傳送多個tuple,當nextTuple()或execute()方法被呼叫時,它們可以傳送0、1或者多個元組。在第五章中你將會了解到更多。
第一個bolt,WordNormalizer,負責接收每一行,並且將行標準化——它將行分解為一個個的單詞後轉化成小寫,並且消除單詞前後的空格。
首先,我們需要宣告bolt的輸出引數:
1
2
3
|
public
void
declareOutputFields(OutputFieldsDeclarer declarer) { declarer.declare( new
Fields( "word" )); } |
這兒,我們宣告bolt傳送一個命名為“word”的欄位。
接著,我們實現execute方法,輸入的tuple將會在這個方法中被處理:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
|
public
void
execute(Tuple input) { String
sentence = input.getString( 0 ); String[]words=
sentence.split( "
" ); for (String
word:words){ word
=word.trim(); if (!word.isEmpty()){ word
=word.toLowerCase(); //Emit
the word List
a = new
ArrayList(); a.add(input); collector.emit(a, new
Values(word)); } } //
Acknowledge the tuple collector.ack(input); } |
第一行讀取元組中的值,可以按照位置或者欄位命名讀取。值被處理後使用collector物件傳送出去。當每個元組被處理完之後,就會呼叫collector的ack()方法,表明該tuple成功地被處理。如果tuple不能被處理,則應該呼叫collector的fail()方法。
例2-2包含這個類的完整程式碼。
例2-2.src/main/java/bolts/WordNormalizer.java
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
|
package
bolts; import
java.util.ArrayList; import
java.util.List; import
java.util.Map; import
backtype.storm.task.OutputCollector; import
backtype.storm.task.TopologyContext; import
backtype.storm.topology.IRichBolt; import
backtype.storm.topology.OutputFieldsDeclarer; import
backtype.storm.tuple.Fields; import
backtype.storm.tuple.Tuple; import
backtype.storm.tuple.Values; public
class
WordNormalizer implements
IRichBolt{ private
OutputCollector collector; public
void
cleanup(){} /** *
The bolt will receive the line from the *
words file and process it to Normalize this line * *
The normalize will be put the words in lower case *
and split the line to get all words in this */ public
void
execute(Tuple input) { String
sentence = input.getString( 0 ); String[]words=
sentence.split( "
" ); for (String
word:words){ word
=word.trim(); if (!word.isEmpty()){ word
=word.toLowerCase(); //Emit
the word List
a = new
ArrayList(); a.add(input); collector.emit(a, new
Values(word)); } } //
Acknowledge the tuple collector.ack(input); } public
void
prepare(Map stormConf,TopologyContext context,OutputCollector collector) { this .collector=collector; } /** *
The bolt will only emit the field "word" */ public
void
declareOutputFields(OutputFieldsDeclarer declarer) { declarer.declare( new
Fields( "word" )); } } |
提示:在這個類中,每呼叫一次execute()方法,會傳送多個元組。例如,當execute()方法收到“This is the Storm book”這個句子時,該方法會傳送5個新元組。
第二個bolt,WordCounter,負責統計每個單詞個數。當topology結束時(cleanup()方法被呼叫時),顯示每個單詞的個數。
提示:第二個bolt中什麼也不傳送,本例中,將資料新增到一個map物件中,但是現實生活中,bolt可以將資料儲存到一個資料庫中。
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
|
package
bolts; import
java.util.HashMap; import
java.util.Map; import
backtype.storm.task.OutputCollector; import
backtype.storm.task.TopologyContext; import
backtype.storm.topology.IRichBolt; import
backtype.storm.topology.OutputFieldsDeclarer; import
backtype.storm.tuple.Tuple; public
class
WordCounter implements
IRichBolt{ Integer
id; String
name; Map<String,Integer>counters; private
OutputCollector collector; /** *
At the end of the spout (when the cluster is shutdown *
We will show the word counters */ @Override public
void
cleanup(){ System.out.println( "--
Word Counter [" +name+ "-" +id+ "]--" ); for (Map.Entry<String,Integer>entry:
counters.entrySet()){ System.out.println(entry.getKey()+ ":
" +entry.getValue()); } } /** *
On each word We will count */ @Override public
void
execute(Tuple input) { String
str =input.getString( 0 ); /** *
If the word dosn't exist in the map we will create *
this, if not We will add 1 */ if (!counters.containsKey(str)){ counters.put(str, 1 ); } else { Integer
c =counters.get(str) + 1 ; counters.put(str,c); } //Set
the tuple as Acknowledge collector.ack(input); } /** *
On create */ @Override public
void
prepare(Map stormConf,TopologyContext context,OutputCollector collector) { this .counters=newHashMap<String,Integer>(); this .collector=collector; this .name=context.getThisComponentId(); this .id=context.getThisTaskId(); } @Override public
void
declareOutputFields(OutputFieldsDeclarer declarer) {} } |
execute()方法使用一個對映(Map型別)採集單詞並統計這些單詞個數。當topology結束的時候,cleanup()方法被呼叫並且列印出counter對映。(這僅僅是個例子,通常情況下,當topology關閉時,你應該使用cleanup()方法關閉活動連結和其他資源。)
主類
在主類中,你將建立topology和一個LocalCluster物件,LocalCluster物件使你可以在本地測試和除錯topology。LocalCluster結合Config物件允許你嘗試不同的叢集配置。例如,如果不慎使用一個全域性變數或者類變數,當配置不同數量的worker測試topology的時候,你將會發現這個錯誤。(關於config物件在第三章會有更多介紹)
提示:所有的topology結點應該可以在程式間沒有資料共享的情形下獨立執行(也就是說沒有全域性或者類變數),因為當topology執行在一個真實的叢集上時,這些程式可能執行在不同的機器上。
你將使用TopologyBuilder建立topology,TopologyBuilder會告訴Storm怎麼安排節點順序、它們怎麼交換資料。
1
2
3
4
|
TopologyBuilder
builder = new
TopologyBuilder(); builder.setSpout( "word-reader" , new
WordReader()); builder.setBolt( "word-normalizer" , new
WordNormalizer()).shuffleGrouping( "word-reader" ); builder.setBolt( "word-counter" , new
WordCounter(), 2 ).fieldsGrouping( "word-normalizer" , new
Fields( "word" )); |
本例中spout和bolt之間使用隨機分組(shuffleGrouping)連線,這種分組型別告訴Storm以隨機分佈的方式從源節點往目標節點傳送訊息。
接著,建立一個包含topology配置資訊的Config物件,該配置資訊在執行時會與叢集配置資訊合併,並且通過prepare()方法傳送到所有節點。
1
2
3
|
Config
conf = new
Config(); conf.put( "wordsFile" ,args[ 0 ]); conf.setDebug( false ); |
將wordFile屬性設定為將要被spout讀取的檔名稱(檔名在args引數中傳入),並將debug屬性設定為true,因為你在開發過程中,當debug為true時,Storm會列印節點間交換的所有訊息和其他除錯資料,這些資訊有助於理解topology是如何執行的。
前面提到,你將使用LocalCluster來執行topology。在一個產品環境中,topology會持續執行,但是在本例中,你僅需執行topology幾秒鐘就能看到結果。
1
2
3
4
|
LocalCluster
cluster = new
LocalCluster(); cluster.submitTopology( "Getting-Started-Toplogie" ,conf,builder.createTopology()); Thread.sleep( 1000 ); cluster.shutdown(); |
使用createTopology和submitTopology建立、執行topology,睡眠兩秒(topology執行在不同的執行緒中),然後通過關閉叢集來停止topology。
例2-3將上面程式碼拼湊到一起。
例2-3.src/main/java/TopologyMain.java
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
|
import
spouts.WordReader; import
bolts.WordCounter; import
bolts.WordNormalizer; import
backtype.storm.Config; import
backtype.storm.LocalCluster; import
backtype.storm.topology.TopologyBuilder; import
backtype.storm.tuple.Fields; public
class
TopologyMain{ public
static
void
main(String[]args) throws
InterruptedException{ //Topology
definition TopologyBuilder
builder = new
TopologyBuilder(); builder.setSpout( "word-reader" , new
WordReader()); builder.setBolt( "word-normalizer" , new
WordNormalizer()).shuffleGrouping( "word-reader" ); builder.setBolt( "word-counter" , new
WordCounter(), 2 ).fieldsGrouping( "word-normalizer" , new
Fields( "word" )); //Configuration Config
conf = new
Config(); conf.put( "wordsFile" ,args[ 0 ]); conf.setDebug( false ); //Topology
run conf.put(Config.TOPOLOGY_MAX_SPOUT_PENDING, 1 ); LocalCluster
cluster = new
LocalCluster(); cluster.submitTopology( "Getting-Started-Toplogie" ,conf,builder.createTopology()); Thread.sleep( 1000 ); cluster.shutdown(); } } |
執行本專案
現在開始準備執行第一個topology!如果你新建一個文字檔案(src/main/resources/words.txt)並且每行一個單詞,則可以通過如下命令執行這個topology:
mvn exec:java -Dexec.mainClass=”TopologyMain” -Dexec.args=”src/main/resources/words.txt”
例如,如果你使用如下words.txt檔案:
Storm
test
are
great
is
an
Storm
simple
application
but
very
powerful
really
Storm
is
great
在日誌中,你將會看到類似如下資訊:
is: 2
application: 1
but: 1
great: 1
test: 1
simple: 1
Storm: 3
really: 1
are: 1
great: 1
an: 1
powerful: 1
very: 1
在本例中,你只使用了每個結點的一個單一例項,假如此時有一個非常大的日誌檔案怎麼去統計每個單詞的個數?此時可以很方便地改系統中節點數量來並行工作,如建立WordCounter的兩個例項:
1
|
builder.setBolt( "word-counter" , new
WordCounter(), 2 ).shuffleGrouping( "word-normalizer" ); |
重新執行這個程式,你將看到:
– Word Counter [word-counter-2] –
application: 1
is: 1
great: 1
are: 1
powerful: 1
Storm: 3
– Word Counter [word-counter-3] –
really: 1
is: 1
but: 1
great: 1
test: 1
simple: 1
an: 1
very: 1
太棒了!改變並行度,so easy(當然,在實際生活中,每個例項執行在不同的機器中)。但仔細一看似乎還有點問題:“is”和“great”這兩個單詞在每個WordCounter例項中都被計算了一次。Why?當使用隨機分組(shuffleGrouping)時,Storm以隨機分佈的方式向每個bolt例項傳送每條訊息。在這個例子中,將相同的單詞傳送到同一個WordCounter例項是更理想的。為了實現這個,你可以將shuffleGrounping(“word-normalizer”)改成fieldsGrouping(“word-normalizer”,new Fields(“word”))。嘗試一下並重新執行本程式來確認結果。後面的章節你將看到更多關於分組和訊息流的內容。
總結
本章我們討論了Storm的本地操作模式和遠端操作模式的不同,以及用Storm開發的強大和簡便。同時也學到了更多關於Storm的基本概念,我們將在接下來的章節深入解釋這些概念。
- 本文固定連結: http://www.flyne.org/article/42
- 轉載請註明: 東風化宇 2014年03月10日 於 Flyne 發表
相關文章
- 搞定storm-入門ORM
- Storm入門之附錄CORM
- storm的很好的入門文件ORM
- Storm入門指南第一章 基礎知識ORM
- Storm入門指南第三章 拓撲結構ORM
- Zookeeper入門指南
- CPack 入門指南
- Docker 入門指南Docker
- numpy入門指南
- EOS 入門指南
- Vue 入門指南Vue
- RabbitMQ入門指南MQ
- Nginx入門指南Nginx
- Vagrant 入門指南
- React 入門指南React
- Flask 入門指南Flask
- gulp入門指南
- OSWorkFlow入門指南
- CouchDB 入門指南
- RxJava入門指南RxJava
- ODA入門指南
- MySQL 入門指南MySql
- Markdown入門指南
- KNIME快速入門指南
- Markdown快速入門指南
- CodeMirror入門指南
- GitHub Actions 入門指南Github
- RequireJS入門指南UIJS
- Vuex新手入門指南Vue
- Airflow使用入門指南AI
- 自學機器學習入門指南機器學習
- Maven入門指南(一)Maven
- Bash快速入門指南
- mongoDB 入門指南、示例MongoDB
- 資訊保安入門指南
- 《Gulp 入門指南》- 前言
- Firebug入門指南
- sap入門--操作指南