2015/1/6

Concurrency in Java

關於 Java Concurrency 議題,Java 並發編程的藝術 將所有相關的文章集結成了一本迷你書。在實作 java server side 程式時,一定會面臨到的問題就是 thread 之間的記憶體資料共享,另外還有在面對耗時的工作時,必須要同時執行的工作,也就是非同步呼叫。

synchronized vs volatile

volatile 是一種輕量級的同步機制,使用這種變數,就可以保證不會發生 context switching 或是 thread 切換的動作,但 volatile 的限制是,當變數的新值是依據舊值產生的時候,就不能使用 volatile。volatile 保證了共享變數的 visibility,當一個 thread 修改此共享變數時,另一個 thread 就能夠讀到這個修改後的值,簡單地說,就是 JVM 保證所有 thread 看到這個變數的值,都是一致的。

java 的 atomic operations 如下

  1. all assignments of primitive data types except for long and double
  2. all assignments of references
  3. all operations of java.concurrent.Atomic* classes
  4. all assignments of volatile longs and doubles

long foo = 87654321L;
並不是 thread-safe 的 operation,因為 java 會分成兩個步驟,先寫入 32bits 再寫入後 32bits,所以要改成

volatile long foo = 87654321L;

volatile 並不能完全取代 synchronied,甚至我們要注意,不能誤用 volatile,要不然就很容易出錯。一般基本判斷要不要使用 volatile 的條件,就是對一個變數讀取的次數遠高於寫入的次數。

可以使用 volatile 的情境如下:

狀態標籤

在另一個 thread 中呼叫 shutdown 用以中止 doWork 的無窮迴圈,這樣的寫法可保障 doWork 不回被惡意強制中斷。

volatile boolean shutdownRequested;
...
public void shutdown() { shutdownRequested = true; }

public void doWork() { 
    while (!shutdownRequested) { 
        // do stuff
    }
}

one-time safe publication

建立 singleton 物件,可使用 volatile 搭配 Double-checked locking 處理。

public class SingletonVolatile {

    private static volatile SingletonVolatile _instance;

    public static SingletonVolatile getInstance() {
        if (_instance == null) {
            synchronized (SingletonVolatile.class) {
                if (_instance == null)
                    _instance = new SingletonVolatile();
            }
        }

        return _instance;
    }

    public static void main(String[] args) {
        System.out.println(SingletonVolatile.getInstance());
        System.out.println(SingletonVolatile.getInstance());
        System.out.println(SingletonVolatile.getInstance());
    }
}

independent observation

定期 「發佈」 觀察結果供程序內部使用,一個後台thread可能會每隔幾秒讀取一次感應器,並更新包含當前文檔的 volatile 變量。然後,其他thread可以讀取這個變量,從而隨時能夠看到最新的溫度值。

以下是身份驗證機制記憶最近一次登錄的用戶的名字的範例。

public class UserManager {
    public volatile String lastUser;

    public boolean authenticate(String user, String password) {
        boolean valid = passwordIsValid(user, password);
        if (valid) {
            User u = new User();
            activeUsers.add(u);
            lastUser = user;
        }
        return valid;
    }
}

volatile bean

JavaBean 的所有數據成員都是 volatile 類型的,並且 getter 和 setter 方法必須非常普通 —— 除了獲取或設置相應的屬性外,不能包含任何邏輯,對於物件引用的成員,引用的對象必須是有效不可變的。

@ThreadSafe
public class Person {
    private volatile String firstName;
    private volatile String lastName;
    private volatile int age;

    public String getFirstName() { return firstName; }
    public String getLastName() { return lastName; }
    public int getAge() { return age; }

    public void setFirstName(String firstName) { 
        this.firstName = firstName;
    }

    public void setLastName(String lastName) { 
        this.lastName = lastName;
    }

    public void setAge(int age) { 
        this.age = age;
    }
}

開銷較低的讀-寫鎖策略

如果讀操作遠遠超過寫操作,可以結合使用內部鎖和 volatile 變量來減少公共代碼路徑的開銷。

@ThreadSafe
public class CheesyCounter {
    // Employs the cheap read-write lock trick
    // All mutative operations MUST be done with the 'this' lock held
    @GuardedBy("this") private volatile int value;

    public int getValue() { return value; }

    public synchronized int increment() {
        return value++;
    }
}

CAS(compare and swap)

獨佔鎖是一種悲觀鎖,synchronized 是一種獨佔鎖,它只有在確保其它線程不會造成干擾的情況下執行。樂觀鎖假設沒有衝突而去完成某項操作,如果因為衝突失敗就重試,直到成功為止。

實現 lock-fre 的 non-blocking 演算法,最有名的方式就是使用 CAS (compare and swap),CAS是個樂觀鎖技術,當有多個threads嘗試同時更新同一個變數時,只有其中一個 thread 能成功更新變數的值,而其它threads都會被告知更新失敗,然後再重試一次,失敗的threads並不會被暫停執行的程序。

CAS內部有3個內部變數,(1) 記憶體實際值V (2) 目標變數的舊預期值A (3) 目標變數要被修改的新值B,當 A==V時,才能將 V 修改為 B,否則什麼都不做。

ConcurrentHashMap

ConcurrentHashMap 是 HashMap 的 multithread 版本,只要會有多個 thread 會同時存取的資料結構,就必須使用 ConcurrentHashMap。至於早期 Java 版本使用的 HashTable,由於內部是以 synchronized 的方式來實作,再面對大量儲存資料的狀況下,其使用效率就會遠低於比 ConcurrentHashMap。

ConcurrentHashMap 是以 Segment 與 HashEntry 組成,Segment 就類似於 HashMap,因此我們可以同時存取對多個 Segment,但到了 Segment 這一層,就會是 synchronzied 的呼叫,所以只要 hash 過後的資料,落入了不同的 Segment,這樣才能保障 ConcurrentHashMap 可以發揮它的功能。

ConcurrentHashMap 的 get method 把所有分享的變數都設定為 volatile,在處理過程中不需要加鎖,除非讀到的值是空的才會加鎖重讀,volatile 可保證只能被單一 thread 修改,且修改後也不會讀到舊的資料,重要的是可讓多個 thread 同時讀取。

put method 為了 thread-safe 必須加鎖處理。size method 裡面是以累加 Segment 裡面的 volatile 變數 count 實作的,由於 count 的變化機率很小,累加過程多半沒有發生 count 的變化,size 會先嘗試兩次不加鎖累加。但如果發生了,就會再進行加鎖累加。

count 的變化判斷是以 modCount 變數進行,在 put, remove, clean 之前,都會先將 modCount 加 1。

ThreadPool

通常在使用 J2EE Server 連結 DB 時,會利用 DB Connection Pool 當作中間層,這可以減少 DB Connection 建立與銷毀所消耗掉的資源。

至於 JVM 的 thread 也一樣,可以使用 ThreadPool 來進行 Thread 的 reuse 與 管理,不僅可減少建立與銷毀 thread 所消耗掉的資源,也可以縮短任務處理的時間。

thread 跟 db connection 都屬於 JVM 的外部資源,這些資源都有使用的上限,以 pool 的方式進行使用管理,而不是無限制地建立與使用,可以保護 server 並提高穩定度。

建立 ThreadPool

new ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveTime, milliseconds,runnableTaskQueue, handler);

相關的參數

  1. corePoolSize(pool的基本大小)
    ThreadPool 會持續建立 thread,直到 thread 的數量達到 corePoolSize。如果呼叫 prestartAllCoreThreads method,ThreadPool 就會提前建立所有基本線程。

  2. runnableTaskQueue
    儲存等待執行的任務的blocking queue

    2.1 ArrayBlockingQueue:以 Array 實作,FIFO(先進先出)

    2.2 LinkedBlockingQueue:以 LinkedList 實作,FIFO (先進先出),throughput比ArrayBlockingQueue好。Executors.newFixedThreadPool() 就是使用這個queue。

    2.3 SynchronousQueue:enqueue必須等到另一個線程呼叫移除才能執行,否則enqueue就會一直處於阻塞狀態,throughput比LinkedBlockingQueue高,Executors.newCachedThreadPool使用了這個queue。

    2.4 PriorityBlockingQueue

  3. maximumPoolSize(pool最大大小)
    ThreadPool允許建立的最大線程數。

  4. ThreadFactory
    用於建立thread的factory

  5. RejectedExecutionHandler(飽和策略)
    當 Queue 與 ThreadPool 都滿了,必須採取一種策略處理提交的新任務。以下是JDK1.5提供的四種策略。
    5.1 AbortPolicy:預設值,直接拋出異常。
    5.2 CallerRunsPolicy:使用呼叫端所在線程來執行任務
    5.3 DiscardOldestPolicy:丟棄 queue 裡最近的一個任務,並執行當前任務
    5.4 DiscardPolicy:不處理,直接丟棄
    5.5 自訂 RejectedExecutionHandler

  6. keepAliveTime
    thread 空閒後,保持存活的時間。如果任務很多,並且每個任務執行的時間比較短,可以調大這個時間,提高線程的利用率。

  7. TimeUnit
    天(DAYS),小時(HOURS),分鐘(MINUTES),毫秒(MILLISECONDS),微秒(MICROSECONDS, 千分之一毫秒)和毫微秒(NANOSECONDS, 千分之一微秒)

提交處理任務的方式有兩種

  1. execute方法沒有返回值,所以無法判斷任務是否被線程池執行成功

    threadsPool.execute(new Runnable() {
             @Override
             public void run() {
             }
         });
    
  2. 使用 submit 來提交任務,它會返回一個future,我們可以通過這個future來判斷任務是否執行成功。future.get()會 blocking 住,直到任務完成,如果使用 get(long timeout, TimeUnit unit) 則會阻塞一段時間後立即返回。

    Future<Object> future = executor.submit(harReturnValuetask);
    try {
      Object s = future.get();
    } catch (InterruptedException e) {
     // 處理中斷異常
    } catch (ExecutionException e) {
     // 處理無法執行任務異常
    } finally {
     // 關閉線程池
     executor.shutdown();
    }
    

當提交一個新任務到 ThreadPool 處理流程如下:

  1. 判斷基本線程池是否已滿?沒滿,建立一個工作線程來執行任務。滿了,則進入下個流程。
  2. 判斷工作隊列是否已滿?沒滿,則將新提交的任務儲存在工作隊列裡。滿了,則進入下個流程。
  3. 判斷整個線程池是否已滿?沒滿,則建立一個新的工作線程來執行任務,滿了,則交給 RejectedExecutionHandler 飽和策略來處理這個任務。

使用 ThreadPool 之前必須先分析任務特性,可以從以下幾個角度來進行分析:

  1. 任務的性質:CPU密集型任務,IO密集型任務和混合型任務。
    CPU密集型任務,儘可能小的線程,如配置N(cpu)+1個線程的線程池
    IO密集型任務則由於線程並不是一直在執行任務,則配置儘可能多的線程,如2*N(cpu)
    混合型的任務,如果可以拆分,則將其拆成一個CPU密集型任務和一個IO密集型任務
    Runtime.getRuntime().availableProcessors() 可取得當前的CPU個數
  2. 任務的優先級:高,中和低。
    優先級不同的任務可以使用優先級隊列PriorityBlockingQueue來處理。它可以讓優先級高的任務先得到執行,但如果一直有優先級高的任務,那麼優先級低的任務可能永遠會不能被執行。
  3. 任務的執行時間:長,中和短。
    執行時間不同的任務可以交給不同規模的線程池來處理,也可以使用優先級隊列,讓執行時間短的任務先執行。
  4. 任務的依賴性:是否依賴其他系統資源,如DB連接。
    因為發送SQL後需要等待DB返回結果,如果等待的時間越長CPU空閒時間就越長,因此thread 數量應該設置越大,這樣才能更好的利用CPU。

監控 ThreadPool

有一些屬性在監控 ThreadPool 的時候可以使用

  1. taskCount:需要執行的任務數量
  2. completedTaskCount:在執行過程中已完成的任務數量
  3. largestPoolSize:曾經建立過的最大線程數量
  4. getPoolSize:Thread數量如果線程池不銷毀的話,池裡的線程不會自動銷毀,所以這個大小只增不+ getActiveCount:獲取活動的線程數。

可以繼承 ThreadPool 並覆寫 beforeExecute,afterExecute 和 terminated方法,可以在任務執行前,執行後和線程池關閉前,監控任務的平均執行時間,最大執行時間和最小執行時間等。這幾個方法在線程池裡是空的方法。

protected void beforeExecute(Thread t, Runnable r) { }

Reference

聊聊並發系列文章

非阻塞同步算法與CAS(Compare and Swap)無鎖算法

JDK, Jetty, Tomcat 線程池的實現算法分析
Java 理論與實踐: 正確使用 Volatile 變量

2014/12/31

REPLAY 重播 - Ken Grimwood

REPLAY 是作者 Ken Grimwood 在 1987年 發表的作品,商周出版的中文版,初版是在 2009年,我是最近才買的初版二刷,等於是我在看了一本 27年前就發表的經典作品。

時間悖論

首先得再複習一次時間悖論,由 時間悖論 wiki 的說明,時間悖論通常又稱為祖父悖論,如果有一個人經過時間旅行,回到過去,殺害了自己的祖父,那麼就不會有父親,也不會有他,既然這個人不存在,怎麼能回去殺害自己的祖父呢?

因此搭配著時光旅行,通常會使用「平行宇宙」的概念,來解決時光旅行所產生的時間悖論問題,換句話說,就是當有一個人回到了過去,這時候就會因為多了他這個人,而產生了一個新的平行宇宙,這樣解釋,就解決了時間悖論的問題。

我們可以看一下這個影片的說明,會有更清楚的概念:




影片中提到的重點,就是已經發生的事情,是無法改變的,當一個人回到了過去,就是到了一個平行宇宙,殺掉了爺爺,也只是在那個平行宇宙中發生的事情,而且在那個平行宇宙中,這個人是從來都沒有出生的。

REPLAY

REPLAY情節的高明之處,就是不使用平行宇宙這個老掉牙的解決方式,男女主角傑夫跟潘密拉,都在1888年10月18日死亡,而死亡後,卻突然發現自己又在一個地方醒過來了,原來他們都回到了過去,但是在同一個身體裡面。

男女主角都有著小時候的容貌,但卻有著前幾次生命的所有記憶,換句話說,因為是同一個形體,身為人這樣的物理存在,在三度空間中,是完全沒有改變的,我們還是無法在四度空間中自由旅行的,而只是在第四度空間變換了刻度。

男女主角因應著前世的記憶,以預測的賭博方式賺錢,但他們必須永遠在這樣的時間循環中,不斷地重來,他們曾經墮落過,也曾經運用這些記憶,解救過很多人。男女主角在無數次的重逢與相愛中不斷地循環。

當他們發現重生的時間離死亡時間越來越近的時候,任何事情都無所謂了,因為他們都終將死亡,不斷地死亡。就在死亡跟重生時間一直重疊的狀況下,時間往前走了,走到了下一秒鐘。

實際上,這個時空的所有事物都沒有改變,改變的是這兩位重生者大量的互動與記憶。

超時空攔截 Predestination

同樣的時空旅行,超時空攔截這部電影,選擇的方式,是把時空悖論的理念玩到極致,主角 Jane 是個棄嬰,女性外表,卻有著男性的體能,因此被選擇進入了時間特派員,就像是個時空警察。

故事一開始的爆炸後,有一大段時間,是一個男生在 Bar 裡面跟酒保對話,看起來越看越奇怪,這跟爆炸案根本沒有關係。這個男生 John 講述了他由女變男的一段驚人的過去。

更特別的是,Jane 邂逅了一個特別的男性,還跟這個人生下了一個女嬰,在 John 跟著酒保這個時間特工回到過去後,才發現那個特別的男生人就是 John自己,自己跟自己生了一個小女孩。

這小女孩在育嬰室就被盜走,後來慢慢的才知道,原來這酒保也是 John,他在解除爆炸案的過程中毀容並重建了容貌,這個時間特工為了完成最後一個任務,回到了紐約,發現爆炸案也是另一個自己引發的。

跳脫平行宇宙的劇情

Replay 跟 Predestination 同樣都是時空旅行的題材,但以完全相反的兩種方式闡述劇情,不過他們同樣都跳脫了平行宇宙的解法,兩個故事,都只使用了一個時間軸。

如果可以的話,建議大家把兩個故事都找來看看,對比看看,會覺得非常有趣,而且佩服這兩個作品的創作者。

References

超時空攔截/前目的地Predestination——時空悖論的極端演繹
超時空攔截 wiki

REPLAY 讀後感
我讀 肯恩.格林伍德的《REPLAY重播》

2014/12/29

話務量 電話流量單位 erlang

erlang 是話務量的單位,簡單地說,是表示每小時連續的總通話長度數量,我們可根據此話務量的數值,作為設計電話通訊網路流量的規模參考數值。

由來

在 1946 年,CCITT(International Telegraph and Telephone Consultative Committee) 也就是現在的 ITU-T (International Telecommunication Union-Telecommunication Standardization Sector) 將電話話務流量命名為 erlang,命名來自丹麥數學家及統計學家 Agner Krarup Erlang。

A.K. Erlang 是在 1878 年出生於丹麥,是電信流量理論的專家,在 1909年發表了第一個著作: The Theory of Probabilities and Telephone Conversations。1929年過世後,到了1940年代,erlang 這個電信流量計算公式被大家廣為接受。

erlang 計算範例

如果一條電話線被佔用 1 小時,話務量就是 1 erlang。
如果一條電話線被佔用 0.5 小時,話務量是 0.5 erlang。

假設有一組使用者,在一個小時內產生了30通電話,平均每一通電話持續了 5 分鐘,那麼 erlang 值可以按照以下步驟計算:

  1. 一小時內的電話流量總時間 = 通話數量 (BHCA) X 平均每一通電話的通話時間 (Holding Time, hrs)
  2. 通話數量為 30
  3. 平均每一通電話的通話時間為 5/60 (hrs)
  4. 30 * 5 / 60 = 2.5

因此在這個範例中,erlang 值就是 2.5。

Traffic Models

當取得 erlang 數值後,就可以套用到話務模型中,用以計算電話系統必須要提供幾條外撥線路給這一群使用者使用。

目前有三種話務模型,其中以 Erlang B 最為常被使用

  1. Erlang B
  2. Extended Erlang B
  3. Erlang C

Erlang B 有三個參數

  1. BHT: Busy Hour Traffic (in Erlangs)
    這就是剛剛計算出來的 erlang 數值,要以此電話系統最忙碌的那一個小時內的資料,計算出此 erlang 數值。
  2. Blocking
    這是因為外撥線路不足,發生無法外撥電話的錯誤通話數,如果是0.01,就表示每 100 通電話,會發生 1 次無法外撥電話的錯誤。
  3. Lines
    就是我們需要知道的計算結果,在這一組電話系統中,要提供幾條外撥線路的數量。

Erlang B 線上計算頁面 可計算出結果,如果是以 BHT = 2.5,Blocking = 0.01 去計算,可得到 Lines 結果為 7 。

Trunking 中繼

Trunking 中繼是兩點間的一條傳輸通道,通常兩點都是電話交換中心。Trunking 在以前是利用實體線路將兩點連接起來,隨著科技發展,中繼的概念不僅應用於無線通信中,基於網際網路的電信交換機的trunk也可以稱為一種中繼。

Erlang Traffic Model 中計算得到的結果 Lines,就是用在連接兩個電話交換中心之間的 trunking 能夠支援的同時通話線路數量。

參考資料

  1. wiki: Erlang (unit)
  2. What is erlang
  3. 話務量
  4. 什麼是Erlang B公式,什麼是Erlang C公式?

2014/12/28

平面國 - Edwin Abbott Abbott

平面國是一本以二維與三維空間為立論概念的小說,故事的主角是個正方形,生活在一個二度空間的平面上。如果你認為,這是本在討論生硬的數學理論的科普故事,那就大錯特錯了,小說的內容不僅涵蓋了幾何數學,還有哲學的對話討論,階級社會制度的殘酷。這是一本非常值得去閱讀與討論的書,也該列入青少年必讀的書單中。

沒有高度的平面幾何

在一個平面上,是只有長度跟寬度的,即使我們用筆,在紙上畫出任何的形狀,在物理的領域中,畫出的線段,全部都有著高度,而且會因為畫筆的材質,而產生不同的效果,例如:使用炭筆,高度就會比用原子筆畫出來的高一些,還有可能會發生線段上的高度忽高忽矮的狀況。

然而在數學的領域中,平面上的幾何圖形,全部都是沒有高度的,或許更精確的說法是,這高度非常地矮,矮到非常接近 0 ,但又不等於 0 ,因為等於 0 就等同於沒有這條線了。

身處在三維空間之中,我們只知道在紙上對數學題目作答,但有沒有想過,我們隨手在紙上畫的三角形、正方形或其他形狀,把自己想像成其中一份子的時候,在這個平面上生活,會是什麼光景。

作者在書本的第一個部份,就是透過一個正方形的視角,來告訴讀者,生活在平面上會是什麼樣的感覺,看到的、聽到的、觸摸到的、聞到的,究竟會是什麼。

該怎麼想像呢?現在先讓自己在紙上畫出一個正三角形,就像是下圖最上面那個三角形,接下來把自己的眼睛從紙張的正上方慢慢地往下移,移動的時候,還是持續看著自己畫的三角形,我們會發現這三角形已經慢慢地縮小,直到眼睛跟桌面同高的時候,我們只能看到一條直線。



生活在平面上,為了要識別男女、階級,必須發展一些感官的方法。包含了視覺、聽覺、觸覺這些方式,而這都必須讓自己設身處地想像自己生活在平面上,才能得到的解決方式,藉由介紹平面國的過程,讀者也慢慢地學會換個角度去觀察事物,換成正方形這個主角的角度去觀察平面國。

由遺傳主導的階級制度

平面國的女性,天生就是直線,而且女性永遠不會改變形狀,至於男性則是從最低階的等腰三角形,往上一層是正三角形、然後是正方形、正五邊形等等,形狀並不是直接遺傳下來的,卻是間接慢慢地由正三角形經過數個世代的演化,變成四邊形,男性的形狀會隨著時間而增加邊的數量。

當邊的數量增加到非常多的時候,就會越來越接近圓形,平面國的圓形因為數量稀少,具有非常崇高的地位,就像是原住民的祭師一樣唯一的存在。

平面國國民天生就會有這樣邊數增加的過程,社會就因為這種生理的現象,自然而然地就形成階級,即使在故事的中間,曾經發生過色彩革命,爭權的結果,最後也再次因為圓形主教卑劣的手段,將叛亂份子一網打盡,全部都處死了。

當社會地位會因為世襲而繼承時,這社會就會充滿了不公平,因為每一個新生兒一出生,除了繼承了父母的面貌與體型,更繼承了父母的社會地位,而且自動往上增加一個層次。

當階級有了世襲的因素,這社會就產生了不公平,國民們也只能被動接受,對這樣的生活無可奈何,也只能慢慢地期待,自己的後代會漸漸地增加邊的數量,晉升高級知識分子的行列。

男尊女卑的社會制度

平面國的女性是直線,完完全全的直線,連形狀都沒有,根本稱不上是在平面上存在的圖形,女性天生的不同,也沒有任何進化的可能性,造就了純粹男尊女卑的社會制度。女性是不能接受教育的,單純只有育養後代的功能。

又因為直線的正面就跟所有男性的形狀,看起來完全一樣,當女性轉了90度,就會變成一個點,而尖銳的直線,是可以把其他形狀刺死的,因此這個社會就得搭配許多規定,例如女性必須隨時發出聲音,必須永遠以直線這個方向面對家人。

為了平面國社會的永續發展,女性也只好在不得已的情況下,要接受這樣的社會規範與支配。人類的性別雖然天生會有一些基本的差別,但幸好不像是平面國一樣,差異那麼大,我們的社會,絕對能接受各種性別平權的制度。

將自己的手腳伸入平面,伸入直線

球體把自己伸入到平面之中,這對他來說是非常簡單的事情,一進去之後,會一直讓正方形覺得自己就是圓形主教。而當他從二度空間,觀察線段國之後,可以再進一步進入到第 0 維空間,線段國國王的生活限制更多了,沒辦法跟自己的同胞交換位置,只能用大叫的方式跟國民溝通。

比上不足、比下有餘,正方形覺得線段國王非常愚蠢,而球體也覺得當個正方形狠愚蠢,正方形無法想像的第三維度,同時球體也無法想像的第四維度。這可不是簡單地把高度拉長就好了。

啟發正方形的球體,也無法想像什麼是四度甚至更高維度的空間

身在三維空間的球體,輕易地就能把正方形提出平面國,在 24 小時內,就讓正方形了解了第三維度,也就是高度實際存在的世界。

直線的人民,無法了解第二維度,平面國的人民無法了解第三維度,都得等到自己被拉出生活的維度之後,才恍然大悟,發現了新的維度存在的事實。

但球體也沒想到,他也會被正方形挑戰,因為正方形領悟到了,或許除了第 0、1、2、3 這些維度,還有著第四維度,這是身處於立體空間的人民無法理解的存在。甚至還有第五維度,第六維度等等。

被挑戰的球體,似乎也從來沒有想過第四維度的存在,唯一的方式,也只能嚇斥正方形,別再講那些危言聳聽的話。

這件事告訴我們,我們不會永遠正確,也永遠要放大心胸,站在真理的一方,只要是合理的推論,就會有存在的可能性。

身為多維空間的先知,只能老死在監獄中

這個問題有因果關係的先決條件,我們現在是以承認有多維空間的方式,來討論這個事情的話,那麼正方形身為多維空間的先知,只能老死在監獄中,的確是一件非常可惜的事情,這就像是我們以我們現在的知識,去探討哥白尼那個時代,哥白尼提出了地球繞著太陽旋轉這個理論,跟當時的教會對立,我們會認為哥白尼是個先知,而先知的下場是抑鬱而終。

但如果轉換到當時那個年代,一直以來接受的教育都是地球為中心的理論,再怎麼去想,也沒辦法一下子就接受哥白尼的理論。

事實上,我們所該做到的,是接受與了解所有的意見與可能性,再透過對話與討論,去釐清每一個人的論點與意見,而不是像老師打手心一樣的制約方法,在其他人提出論點時,就拿出棒子先敲一下,然後就忽略他。

一個有效的溝通循環,要從理解與對話開始,如果沒有辦法引起對話與共鳴,就只會是單方向的宣導與強迫,這種方式,是沒有辦法讓所有人有發自內心的接受某些制度與作法。

結論

這本書的內容並不算太多,雖然未來有無限的可能,未來或許也沒那麼大機會,能再看到類似題材的作品,能將數學完完全全融合到一本小說的作品。

這是非常值得去閱讀與討論的小說,建議大家去看看。

2014/12/22

如何使用 snmp4j 處理 trap

以下以 snmp4j 套件提供處理 trap 的範例。

multithread trap receiver

trap receiver 必須在本機建立一個 UDP Port 162 的 server,一直等待 client 端發送 UDP trap,在處理這樣的 server時,multithread 是必要的。

程式範例

import java.io.IOException;
import java.net.UnknownHostException;
import java.util.Iterator;
import java.util.Vector;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.snmp4j.CommandResponder;
import org.snmp4j.CommandResponderEvent;
import org.snmp4j.MessageDispatcherImpl;
import org.snmp4j.Snmp;
import org.snmp4j.TransportMapping;
import org.snmp4j.mp.MPv1;
import org.snmp4j.mp.MPv2c;
import org.snmp4j.mp.MPv3;
import org.snmp4j.security.AuthMD5;
import org.snmp4j.security.PrivDES;
import org.snmp4j.security.SecurityModels;
import org.snmp4j.security.SecurityProtocols;
import org.snmp4j.security.USM;
import org.snmp4j.security.UsmUser;
import org.snmp4j.smi.Address;
import org.snmp4j.smi.GenericAddress;
import org.snmp4j.smi.OctetString;
import org.snmp4j.smi.TcpAddress;
import org.snmp4j.smi.UdpAddress;
import org.snmp4j.smi.VariableBinding;
import org.snmp4j.transport.DefaultTcpTransportMapping;
import org.snmp4j.transport.DefaultUdpTransportMapping;
import org.snmp4j.util.MultiThreadedMessageDispatcher;
import org.snmp4j.util.ThreadPool;

public class SNMPTrapReceiver implements CommandResponder {
    public static Logger logger = LoggerFactory
            .getLogger(SNMPTrapReceiver.class.getName());

    private MultiThreadedMessageDispatcher dispatcher;
    private Snmp snmp = null;
    private Address listenAddress;
    private ThreadPool threadPool;
    private int n = 0;
    private long start = -1;

    public SNMPTrapReceiver() {
    }

    public static void main(String[] args) {
        new SNMPTrapReceiver().run();
    }

    private void run() {
        try {
            init();
            snmp.addCommandResponder(this);
        } catch (Exception ex) {
            ex.printStackTrace();
        }
    }

    private void init() throws UnknownHostException, IOException {
        threadPool = ThreadPool.create("Trap", 10);
        dispatcher = new MultiThreadedMessageDispatcher(threadPool,
                new MessageDispatcherImpl());
        listenAddress = GenericAddress.parse(System.getProperty(
                "snmp4j.listenAddress", "udp:0.0.0.0/162"));
        TransportMapping<?> transport;
        if (listenAddress instanceof UdpAddress) {
            transport = new DefaultUdpTransportMapping(
                    (UdpAddress) listenAddress);
        } else {
            transport = new DefaultTcpTransportMapping(
                    (TcpAddress) listenAddress);
        }
        USM usm = new USM(SecurityProtocols.getInstance(), new OctetString(
                MPv3.createLocalEngineID()), 0);
        usm.setEngineDiscoveryEnabled(true);

        snmp = new Snmp(dispatcher, transport);
        snmp.getMessageDispatcher().addMessageProcessingModel(new MPv1());
        snmp.getMessageDispatcher().addMessageProcessingModel(new MPv2c());
        snmp.getMessageDispatcher().addMessageProcessingModel(new MPv3(usm));
        SecurityModels.getInstance().addSecurityModel(usm);
        snmp.getUSM().addUser(
                new OctetString("MD5DES"),
                new UsmUser(new OctetString("MD5DES"), AuthMD5.ID,
                        new OctetString("UserName"), PrivDES.ID,
                        new OctetString("PasswordUser")));
        snmp.getUSM().addUser(new OctetString("MD5DES"),
                new UsmUser(new OctetString("MD5DES"), null, null, null, null));

        snmp.listen();
    }

    public void processPdu(CommandResponderEvent event) {
        if (start < 0) {
            start = System.currentTimeMillis() - 1;
        }
        n++;
        if ((n % 100 == 1)) {
            logger.info("Processed "
                    + (n / (double) (System.currentTimeMillis() - start))
                    * 1000 + "/s, total=" + n);
        }

        StringBuffer msg = new StringBuffer();
        msg.append(event.toString());
        Vector<? extends VariableBinding> varBinds = event.getPDU()
                .getVariableBindings();
        if (varBinds != null && !varBinds.isEmpty()) {
            Iterator<? extends VariableBinding> varIter = varBinds.iterator();
            while (varIter.hasNext()) {
                VariableBinding var = varIter.next();
                msg.append(var.toString()).append(";");
            }
        }
        logger.info("Message Received: " + msg.toString());
    }
}

這是發送 trap 的範例,有區分 snmp v1, v2c, v3 三種協定。

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.snmp4j.CommandResponder;
import org.snmp4j.CommandResponderEvent;
import org.snmp4j.CommunityTarget;
import org.snmp4j.PDU;
import org.snmp4j.ScopedPDU;
import org.snmp4j.Snmp;
import org.snmp4j.TransportMapping;
import org.snmp4j.UserTarget;
import org.snmp4j.mp.MPv3;
import org.snmp4j.mp.SnmpConstants;
import org.snmp4j.security.SecurityLevel;
import org.snmp4j.security.SecurityModels;
import org.snmp4j.security.SecurityProtocols;
import org.snmp4j.security.USM;
import org.snmp4j.security.UsmUser;
import org.snmp4j.smi.Address;
import org.snmp4j.smi.GenericAddress;
import org.snmp4j.smi.IpAddress;
import org.snmp4j.smi.OID;
import org.snmp4j.smi.OctetString;
import org.snmp4j.smi.UdpAddress;
import org.snmp4j.smi.VariableBinding;
import org.snmp4j.transport.DefaultUdpTransportMapping;
import org.snmp4j.util.DefaultPDUFactory;

public class SNMPTrapGeneratorClient {
    public static Logger logger = LoggerFactory
            .getLogger(SNMPTrapGeneratorClient.class.getName());

    private static final String community = "public";
    private static final String trapOid = ".1.3.6.1.2.1.1.6";
    private static final String ipAddress = "127.0.0.1";
    private static final int port = 162;

    public static void main(String args[]) {
        sendSnmpV1V2Trap(SnmpConstants.version1);
        sendSnmpV1V2Trap(SnmpConstants.version2c);
        sendSnmpV3Trap();
    }

    /**
     * This methods sends the V1/V2 trap
     * 
     * @param version
     */
    private static void sendSnmpV1V2Trap(int version) {
        // send trap
        sendV1orV2Trap(version, community, ipAddress, port);
    }

    private static PDU createPdu(int snmpVersion) {
        PDU pdu = DefaultPDUFactory.createPDU(snmpVersion);
        if (snmpVersion == SnmpConstants.version1) {
            pdu.setType(PDU.V1TRAP);
        } else {
            pdu.setType(PDU.TRAP);
        }
        pdu.add(new VariableBinding(SnmpConstants.sysUpTime));
        pdu.add(new VariableBinding(SnmpConstants.snmpTrapOID, new OID(trapOid)));
        pdu.add(new VariableBinding(SnmpConstants.snmpTrapAddress,
                new IpAddress(ipAddress)));
        pdu.add(new VariableBinding(new OID(trapOid), new OctetString("Major")));
        return pdu;
    }

    private static void sendV1orV2Trap(int snmpVersion, String community,
            String ipAddress, int port) {
        try {
            // create v1/v2 PDU
            PDU snmpPDU = createPdu(snmpVersion);

            // Create Transport Mapping
            TransportMapping<?> transport = new DefaultUdpTransportMapping();
            transport.listen();

            // Create Target
            CommunityTarget comtarget = new CommunityTarget();
            comtarget.setCommunity(new OctetString(community));
            comtarget.setVersion(snmpVersion);
            comtarget.setAddress(new UdpAddress(ipAddress + "/" + port));
            comtarget.setRetries(2);
            comtarget.setTimeout(5000);

            // Send the PDU
            Snmp snmp = new Snmp(transport);
            snmp.send(snmpPDU, comtarget);
            logger.info("Sent Trap to (IP:Port)=> " + ipAddress + ":" + port);
            snmp.close();
        } catch (Exception e) {
            logger.error("Error: ", e);
        }
    }

    /**
     * Sends the v3 trap
     */
    private static void sendSnmpV3Trap() {
        try {
            Address targetAddress = GenericAddress.parse("udp:" + ipAddress
                    + "/" + port);
            TransportMapping<?> transport = new DefaultUdpTransportMapping();
            Snmp snmp = new Snmp(transport);
            USM usm = new USM(SecurityProtocols.getInstance(), new OctetString(
                    MPv3.createLocalEngineID()), 0);
            SecurityModels.getInstance().addSecurityModel(usm);
            transport.listen();

            snmp.getUSM().addUser(
                    new OctetString("MD5DES"),
                    new UsmUser(new OctetString("MD5DES"), null, null, null,
                            null));

            // Create Target
            UserTarget target = new UserTarget();
            target.setAddress(targetAddress);
            target.setRetries(1);
            target.setTimeout(11500);
            target.setVersion(SnmpConstants.version3);
            target.setSecurityLevel(SecurityLevel.NOAUTH_NOPRIV);
            target.setSecurityName(new OctetString("MD5DES"));

            // Create PDU for V3
            ScopedPDU pdu = new ScopedPDU();
            pdu.setType(ScopedPDU.NOTIFICATION);
            pdu.add(new VariableBinding(SnmpConstants.sysUpTime));
            pdu.add(new VariableBinding(SnmpConstants.snmpTrapOID,
                    SnmpConstants.linkDown));
            pdu.add(new VariableBinding(new OID(trapOid), new OctetString(
                    "Major")));

            // Send the PDU
            snmp.send(pdu, target);
            logger.info("Sending Trap to (IP:Port)=> " + ipAddress + ":"
                    + port);
            snmp.addCommandResponder(new CommandResponder() {
                public void processPdu(CommandResponderEvent arg0) {
                    logger.info(arg0);
                }
            });
            snmp.close();
        } catch (Exception e) {
            logger.error("Error: ", e);
        }
    }
}

執行結果

trap 發送端,很單純地就直接把 trap 送出去

2014-12-08 10:43:51,195 [main] INFO  SNMPTrapGeneratorClient 91
 Sent Trap to (IP:Port)=> 127.0.0.1:162
2014-12-08 10:43:51,204 [main] INFO  SNMPTrapGeneratorClient 91
 Sent Trap to (IP:Port)=> 127.0.0.1:162
2014-12-08 10:43:51,224 [main] INFO  SNMPTrapGeneratorClient 137
 Sending Trap to (IP:Port)=> 127.0.0.1:162

trap 接收端

2014-12-08 10:43:51,279 [Trap.1] INFO  SNMPTrapReceiver 102
 Processed 1000.0/s, total=2
2014-12-08 10:43:51,279 [Trap.0] INFO  SNMPTrapReceiver 118
 Message Received: CommandResponderEvent[securityModel=1, securityLevel=1, maxSizeResponsePDU=65535, pduHandle=PduHandle[0], stateReference=StateReference[msgID=0,pduHandle=PduHandle[0],securityEngineID=null,securityModel=null,securityName=public,securityLevel=1,contextEngineID=null,contextName=null,retryMsgIDs=null], pdu=V1TRAP[reqestID=0,timestamp=0:00:00.00,enterprise=0.0,genericTrap=0,specificTrap=0, VBS[1.3.6.1.2.1.1.3.0 = Null; 1.3.6.1.6.3.1.1.4.1.0 = 1.3.6.1.2.1.1.6; 1.3.6.1.6.3.18.1.3.0 = 127.0.0.1; 1.3.6.1.2.1.1.6 = Major]], messageProcessingModel=0, securityName=public, processed=false, peerAddress=127.0.0.1/51136, transportMapping=org.snmp4j.transport.DefaultUdpTransportMapping@13fdb89, tmStateReference=null]1.3.6.1.2.1.1.3.0 = Null;1.3.6.1.6.3.1.1.4.1.0 = 1.3.6.1.2.1.1.6;1.3.6.1.6.3.18.1.3.0 = 127.0.0.1;1.3.6.1.2.1.1.6 = Major;
2014-12-08 10:43:51,286 [Trap.1] INFO  SNMPTrapReceiver 118
 Message Received: CommandResponderEvent[securityModel=2, securityLevel=1, maxSizeResponsePDU=65535, pduHandle=PduHandle[1338161657], stateReference=StateReference[msgID=0,pduHandle=PduHandle[1338161657],securityEngineID=null,securityModel=null,securityName=public,securityLevel=1,contextEngineID=null,contextName=null,retryMsgIDs=null], pdu=TRAP[requestID=1338161657, errorStatus=Success(0), errorIndex=0, VBS[1.3.6.1.2.1.1.3.0 = Null; 1.3.6.1.6.3.1.1.4.1.0 = 1.3.6.1.2.1.1.6; 1.3.6.1.6.3.18.1.3.0 = 127.0.0.1; 1.3.6.1.2.1.1.6 = Major]], messageProcessingModel=1, securityName=public, processed=false, peerAddress=127.0.0.1/51138, transportMapping=org.snmp4j.transport.DefaultUdpTransportMapping@13fdb89, tmStateReference=null]1.3.6.1.2.1.1.3.0 = Null;1.3.6.1.6.3.1.1.4.1.0 = 1.3.6.1.2.1.1.6;1.3.6.1.6.3.18.1.3.0 = 127.0.0.1;1.3.6.1.2.1.1.6 = Major;
2014-12-08 10:43:51,312 [Trap.2] INFO SNMPTrapReceiver 118
 Message Received: CommandResponderEvent[securityModel=3, securityLevel=1, maxSizeResponsePDU=65428, pduHandle=PduHandle[1828414888], stateReference=null, pdu=TRAP[{contextEngineID=80:00:13:70:01:c0:a8:01:39:e2:ca:22:3e, contextName=}, requestID=1828414888, errorStatus=0, errorIndex=0, VBS[1.3.6.1.2.1.1.3.0 = Null; 1.3.6.1.6.3.1.1.4.1.0 = 1.3.6.1.6.3.1.1.5.3; 1.3.6.1.2.1.1.6 = Major]], messageProcessingModel=3, securityName=MD5DES, processed=false, peerAddress=127.0.0.1/51139, transportMapping=org.snmp4j.transport.DefaultUdpTransportMapping@13fdb89, tmStateReference=null]1.3.6.1.2.1.1.3.0 = Null;1.3.6.1.6.3.1.1.4.1.0 = 1.3.6.1.6.3.1.1.5.3;1.3.6.1.2.1.1.6 = Major;

Reference

Trap Receiver