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

2014/12/15

如何使用 snmp4j 進行查詢

SNMP 的原理跟概念 分為 Agent 與 Client 兩端需要處理,如果不是提供機器與服務的,就不需要做 Agent 這個 Server,一般最基本的 SNMP 程式設計是撰寫 SNMP Client,Client 又有兩個部份 (1) SNMP GET, Walk (2) Trap Receiver,以下以 snmp4j 套件提供第一個部份 SNMP GET, Walk 的範例。

SNMP GET

給予 server ip,這邊刻意將程式區分為 connect, snmpget, close 三個部份,因為 UDP 本身是 connection less 的一種網路連線,未來在使用時,只要連結起來,應該就能持續發送 SNMP GET,並取得結果。

snmpGet 有兩種 method,第一種就單純地只接受一個 oid 參數,第二種,是接受一個 oid List,可一次查詢多個 oid 的結果。

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Vector;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.snmp4j.CommunityTarget;
import org.snmp4j.PDU;
import org.snmp4j.Snmp;
import org.snmp4j.event.ResponseEvent;
import org.snmp4j.mp.SnmpConstants;
import org.snmp4j.smi.GenericAddress;
import org.snmp4j.smi.Integer32;
import org.snmp4j.smi.Null;
import org.snmp4j.smi.OID;
import org.snmp4j.smi.OctetString;
import org.snmp4j.smi.Variable;
import org.snmp4j.smi.VariableBinding;
import org.snmp4j.transport.DefaultUdpTransportMapping;

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

    private static int version = SnmpConstants.version1;
    private static String protocol = "udp";

    private static CommunityTarget target = null;
    private static DefaultUdpTransportMapping udpTransportMapping = null;
    private static Snmp snmp = null;

    public static void main(String[] args) {

        String ip = "192.168.1.8";
        String community = "public";
        int port = 161;

        Variable var = null;
        SnmpGet tester = new SnmpGet();
        // tester.snmpGet(ip, port, community, oidval);
        try {
            tester.connect(ip, port, community);
            // System Uptime(系統運行時間) 的 oid, 最前面的 . 可以忽略不寫
            String oid = ".1.3.6.1.2.1.1.3.0";
            var = tester.snmpGet(oid);
            logger.info(oid + " = " + var);

            Thread.sleep(3 * 1000);
            logger.info("");
            oid = ".1.3.6.1.2.1.1.1.0";
            var = tester.snmpGet(oid);
            logger.info(oid + " = " + var);

            Thread.sleep(3 * 1000);
            logger.info("");
            logger.info("SNMP GetList");
            List<String> oidList = new ArrayList<String>();
            oidList.add(".1.3.6.1.2.1.1.1.0");
            oidList.add(".1.3.6.1.2.1.1.3.0");
            oidList.add(".1.3.6.1.2.1.1.5.0");

            List<VariableBinding> vblist = tester.snmpGet(oidList);
            for (VariableBinding vb : vblist) {
                logger.info("oid:" + vb.getOid() + ", var=" + vb.getVariable());
            }
        } catch (Exception e) {
            logger.error("Error:", e);
        } finally {
            tester.close();
        }
    }

    public void connect(String ip, int port, String community) throws Exception {
        String address = protocol + ":" + ip + "/" + port;
        // CommunityTarget target = SnmpUtil.createCommunityTarget(address,
        // community, version, 2 * 1000L, 3);

        target = new CommunityTarget();
        target.setCommunity(new OctetString(community));
        target.setAddress(GenericAddress.parse(address));
        target.setVersion(version);
        target.setTimeout(2 * 1000L); // milliseconds
        target.setRetries(3); // retry 3次

        try {
            udpTransportMapping = new DefaultUdpTransportMapping();
            // 這裡一定要呼叫 listen, 才能收到結果
            udpTransportMapping.listen();
            snmp = new Snmp(udpTransportMapping);

        } catch (Exception e) {
            logger.error("Error", e);
            throw e;
        }
    }

    public void close() {
        if (snmp != null) {
            try {
                snmp.close();
            } catch (IOException ex1) {
                snmp = null;
            }
        }
        if (udpTransportMapping != null) {
            try {
                udpTransportMapping.close();
            } catch (IOException ex2) {
                udpTransportMapping = null;
            }
        }
    }

    public Variable snmpGet(String oid) throws Exception {
        try {
            PDU pdu = new PDU();
            // pdu.add(new VariableBinding(new OID(new
            // int[]{1,3,6,1,2,1,1,2})));
            pdu.add(new VariableBinding(new OID(oid)));
            pdu.setType(PDU.GET);

            // 以同步的方式發送 snmp get, 會等待target 設定的 timeout 時間結束後
            // 就會以 Request time out 的方式 return 回來
            ResponseEvent response = snmp.send(pdu, target);
            // logger.debug("PeerAddress:" + response.getPeerAddress());
            PDU responsePdu = response.getResponse();

            if (responsePdu == null) {
                logger.debug("Request time out");
            } else {
                Vector<?> vbVect = responsePdu.getVariableBindings();
                logger.debug("vb size:" + vbVect.size());
                if (vbVect.size() == 0) {
                    logger.debug(" pdu vb size is 0 ");
                } else {
                    Object obj = vbVect.firstElement();
                    VariableBinding vb = (VariableBinding) obj;
                    // logger.debug(vb.getOid() + " = " + vb.getVariable());

                    // logger.info("success finish snmp get the oid!");
                    return vb.getVariable();
                }
            }

        } catch (Exception e) {
            logger.error("Error", e);
            throw e;
        }
        return null;
    }

    public List<VariableBinding> snmpGet(List<String> oidList) throws Exception {
        try {
            PDU pdu = new PDU();
            pdu.setType(PDU.GET);
            for (String oid : oidList) {
                pdu.add(new VariableBinding(new OID(oid)));
            }

            // 以同步的方式發送 snmp get, 會等待target 設定的 timeout 時間結束後
            // 就會以 Request time out 的方式 return 回來
            ResponseEvent response = snmp.send(pdu, target);
            // logger.debug("PeerAddress:" + response.getPeerAddress());
            PDU responsePdu = response.getResponse();

            if (responsePdu == null) {
                logger.debug("Request time out");
            } else {
                logger.debug(" response pdu vb size is " + responsePdu.size());
                List<VariableBinding> datalist = new ArrayList<VariableBinding>();
                for (int i = 0; i < responsePdu.size(); i++) {
                    VariableBinding vb = responsePdu.get(i);
                    // logger.debug(vb.getOid() + "=" + vb.getVariable());
                    datalist.add(vb);
                }
                return datalist;
            }

        } catch (Exception e) {
            logger.error("Error", e);
            throw e;
        }
        return null;
    }
}

執行結果

2014-12-04 17:19:11,055 [main] DEBUG SnmpGet 145
 vb size:1
2014-12-04 17:19:11,083 [main] INFO  SnmpGet 50
 .1.3.6.1.2.1.1.3.0 = 1 day, 1:13:23.16
2014-12-04 17:19:14,084 [main] INFO  SnmpGet 53

2014-12-04 17:19:14,085 [main] DEBUG SnmpGet 145
 vb size:1
2014-12-04 17:19:14,086 [main] INFO  SnmpGet 56
 .1.3.6.1.2.1.1.1.0 = Linux server.maxkit.com.tw 2.6.32-431.el6.i686 #1 SMP Fri Nov 22 00:26:36 UTC 2013 i686

SNMP walk

這是使用 GETNext 的方式,當有下一個OID 時,就自動往下抓取,直到沒有資料為止。

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Vector;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.snmp4j.CommunityTarget;
import org.snmp4j.PDU;
import org.snmp4j.Snmp;
import org.snmp4j.event.ResponseEvent;
import org.snmp4j.mp.SnmpConstants;
import org.snmp4j.smi.GenericAddress;
import org.snmp4j.smi.Integer32;
import org.snmp4j.smi.Null;
import org.snmp4j.smi.OID;
import org.snmp4j.smi.OctetString;
import org.snmp4j.smi.Variable;
import org.snmp4j.smi.VariableBinding;
import org.snmp4j.transport.DefaultUdpTransportMapping;

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

    private static int version = SnmpConstants.version1;
    private static String protocol = "udp";

    private static CommunityTarget target = null;
    private static DefaultUdpTransportMapping udpTransportMapping = null;
    private static Snmp snmp = null;

    public static void main(String[] args) {

        String ip = "192.168.1.8";
        String community = "public";
        int port = 161;

        Variable var = null;
        SnmpGet tester = new SnmpGet();
        // tester.snmpGet(ip, port, community, oidval);
        try {
            tester.connect(ip, port, community);
            logger.info("");
            logger.info("SNMP walk");

            vblist = tester.snmpWalk("1.3.6.1.2.1.1");
            for (VariableBinding vb : vblist) {
                logger.info("oid:" + vb.getOid() + ", var=" + vb.getVariable());
            }
        } catch (Exception e) {
            logger.error("Error:", e);
        } finally {
            tester.close();
        }
    }

    public void connect(String ip, int port, String community) throws Exception {
        String address = protocol + ":" + ip + "/" + port;
        // CommunityTarget target = SnmpUtil.createCommunityTarget(address,
        // community, version, 2 * 1000L, 3);

        target = new CommunityTarget();
        target.setCommunity(new OctetString(community));
        target.setAddress(GenericAddress.parse(address));
        target.setVersion(version);
        target.setTimeout(2 * 1000L); // milliseconds
        target.setRetries(3); // retry 3次

        try {
            udpTransportMapping = new DefaultUdpTransportMapping();
            // 這裡一定要呼叫 listen, 才能收到結果
            udpTransportMapping.listen();
            snmp = new Snmp(udpTransportMapping);

        } catch (Exception e) {
            logger.error("Error", e);
            throw e;
        }
    }

    public void close() {
        if (snmp != null) {
            try {
                snmp.close();
            } catch (IOException ex1) {
                snmp = null;
            }
        }
        if (udpTransportMapping != null) {
            try {
                udpTransportMapping.close();
            } catch (IOException ex2) {
                udpTransportMapping = null;
            }
        }
    }

    /**
     * 1)responsePDU == null<br>
     * 2)responsePDU.getErrorStatus() != 0<br>
     * 3)responsePDU.get(0).getOid() == null<br>
     * 4)responsePDU.get(0).getOid().size() < targetOID.size()<br>
     * 5)targetOID.leftMostCompare(targetOID.size(),responsePDU.get(0).getOid())
     * !=0<br>
     * 6)Null.isExceptionSyntax(responsePDU.get(0).getVariable().getSyntax())<br>
     * 7)responsePDU.get(0).getOid().compareTo(targetOID) <= 0<br>
     */
    public List<VariableBinding> snmpWalk(String targetOid) {
        OID targetOID = new OID(targetOid);

        PDU requestPDU = new PDU();
        requestPDU.setType(PDU.GETNEXT);
        requestPDU.add(new VariableBinding(targetOID));

        try {
            List<VariableBinding> vblist = new ArrayList<VariableBinding>();
            boolean finished = false;
            while (!finished) {
                VariableBinding vb = null;
                ResponseEvent response = snmp.send(requestPDU, target);
                PDU responsePDU = response.getResponse();

                if (null == responsePDU) {
                    logger.debug("responsePDU == null");
                    finished = true;
                    break;
                } else {
                    vb = responsePDU.get(0);
                }
                // check finish
                finished = checkWalkFinished(targetOID, responsePDU, vb);
                if (!finished) {
                    // logger.debug("vb:" + vb.toString());
                    vblist.add(vb);
                    // Set up the variable binding for the next entry.
                    requestPDU.setRequestID(new Integer32(0));
                    requestPDU.set(0, vb);
                }
            }
            // logger.debug("success finish snmp walk!");
            return vblist;
        } catch (Exception e) {
            logger.error("Error: ", e);
        }
        return null;
    }

    /**
     * check snmp walk finish
     * 
     * @param resquestPDU
     * @param targetOID
     * @param responsePDU
     * @param vb
     * @return
     */
    private boolean checkWalkFinished(OID targetOID, PDU responsePDU,
            VariableBinding vb) {
        boolean finished = false;
        if (responsePDU.getErrorStatus() != 0) {
            logger.debug("responsePDU.getErrorStatus() != 0 ");
            logger.debug(responsePDU.getErrorStatusText());
            finished = true;
        } else if (vb.getOid() == null) {
            logger.debug("vb.getOid() == null");
            finished = true;
        } else if (vb.getOid().size() < targetOID.size()) {
            logger.debug("vb.getOid().size() < targetOID.size()");
            finished = true;
        } else if (targetOID.leftMostCompare(targetOID.size(), vb.getOid()) != 0) {
            logger.debug("targetOID.leftMostCompare() != 0");
            finished = true;
        } else if (Null.isExceptionSyntax(vb.getVariable().getSyntax())) {
            logger.debug("Null.isExceptionSyntax(vb.getVariable().getSyntax())");
            finished = true;
        } else if (vb.getOid().compareTo(targetOID) <= 0) {
            logger.debug("Variable received is not "
                    + "lexicographic successor of requested " + "one:");
            logger.debug(vb.toString() + " <= " + targetOID);
            finished = true;
        }
        return finished;

    }
}

測試結果

2014-12-04 17:19:17,087 [main] INFO  SnmpGet 60
 SNMP GetList
2014-12-04 17:19:17,091 [main] DEBUG SnmpGet 182
  response pdu vb size is 3
2014-12-04 17:19:17,091 [main] INFO  SnmpGet 68
 oid:1.3.6.1.2.1.1.1.0, var=Linux server.maxkit.com.tw 2.6.32-431.el6.i686 #1 SMP Fri Nov 22 00:26:36 UTC 2013 i686
2014-12-04 17:19:17,092 [main] INFO  SnmpGet 68
 oid:1.3.6.1.2.1.1.3.0, var=1 day, 1:13:29.25
2014-12-04 17:19:17,092 [main] INFO  SnmpGet 68
 oid:1.3.6.1.2.1.1.5.0, var=server.maxkit.com.tw
2014-12-04 17:19:20,092 [main] INFO  SnmpGet 72

2014-12-04 17:19:20,093 [main] INFO  SnmpGet 73
 SNMP walk
2014-12-04 17:19:20,130 [main] DEBUG SnmpGet 276
 targetOID.leftMostCompare() != 0
2014-12-04 17:19:20,130 [main] INFO  SnmpGet 77
 oid:1.3.6.1.2.1.1.1.0, var=Linux server.maxkit.com.tw 2.6.32-431.el6.i686 #1 SMP Fri Nov 22 00:26:36 UTC 2013 i686
2014-12-04 17:19:20,130 [main] INFO  SnmpGet 77
 oid:1.3.6.1.2.1.1.2.0, var=1.3.6.1.4.1.8072.3.2.10
2014-12-04 17:19:20,131 [main] INFO  SnmpGet 77
 oid:1.3.6.1.2.1.1.3.0, var=1 day, 1:13:32.26
2014-12-04 17:19:20,131 [main] INFO  SnmpGet 77
 oid:1.3.6.1.2.1.1.4.0, var=Root <root@localhost> (configure /etc/snmp/snmp.local.conf)
2014-12-04 17:19:20,131 [main] INFO  SnmpGet 77
 oid:1.3.6.1.2.1.1.5.0, var=server.maxkit.com.tw
2014-12-04 17:19:20,132 [main] INFO  SnmpGet 77
 oid:1.3.6.1.2.1.1.6.0, var=Unknown (edit /etc/snmp/snmpd.conf)
2014-12-04 17:19:20,132 [main] INFO  SnmpGet 77
 oid:1.3.6.1.2.1.1.8.0, var=0:00:00.01
2014-12-04 17:19:20,132 [main] INFO  SnmpGet 77
 oid:1.3.6.1.2.1.1.9.1.2.1, var=1.3.6.1.6.3.11.2.3.1.1
2014-12-04 17:19:20,132 [main] INFO  SnmpGet 77
 oid:1.3.6.1.2.1.1.9.1.2.2, var=1.3.6.1.6.3.15.2.1.1
2014-12-04 17:19:20,133 [main] INFO  SnmpGet 77
 oid:1.3.6.1.2.1.1.9.1.2.3, var=1.3.6.1.6.3.10.3.1.1
2014-12-04 17:19:20,133 [main] INFO  SnmpGet 77
 oid:1.3.6.1.2.1.1.9.1.2.4, var=1.3.6.1.6.3.1
2014-12-04 17:19:20,133 [main] INFO  SnmpGet 77
 oid:1.3.6.1.2.1.1.9.1.2.5, var=1.3.6.1.2.1.49
2014-12-04 17:19:20,133 [main] INFO  SnmpGet 77
 oid:1.3.6.1.2.1.1.9.1.2.6, var=1.3.6.1.2.1.4
2014-12-04 17:19:20,133 [main] INFO  SnmpGet 77
 oid:1.3.6.1.2.1.1.9.1.2.7, var=1.3.6.1.2.1.50
2014-12-04 17:19:20,133 [main] INFO  SnmpGet 77
 oid:1.3.6.1.2.1.1.9.1.2.8, var=1.3.6.1.6.3.16.2.2.1
2014-12-04 17:19:20,133 [main] INFO  SnmpGet 77
 oid:1.3.6.1.2.1.1.9.1.3.1, var=The MIB for Message Processing and Dispatching.
2014-12-04 17:19:20,134 [main] INFO  SnmpGet 77
 oid:1.3.6.1.2.1.1.9.1.3.2, var=The MIB for Message Processing and Dispatching.
2014-12-04 17:19:20,134 [main] INFO  SnmpGet 77
 oid:1.3.6.1.2.1.1.9.1.3.3, var=The SNMP Management Architecture MIB.
2014-12-04 17:19:20,134 [main] INFO  SnmpGet 77
 oid:1.3.6.1.2.1.1.9.1.3.4, var=The MIB module for SNMPv2 entities
2014-12-04 17:19:20,134 [main] INFO  SnmpGet 77
 oid:1.3.6.1.2.1.1.9.1.3.5, var=The MIB module for managing TCP implementations
2014-12-04 17:19:20,134 [main] INFO  SnmpGet 77
 oid:1.3.6.1.2.1.1.9.1.3.6, var=The MIB module for managing IP and ICMP implementations
2014-12-04 17:19:20,134 [main] INFO  SnmpGet 77
 oid:1.3.6.1.2.1.1.9.1.3.7, var=The MIB module for managing UDP implementations
2014-12-04 17:19:20,134 [main] INFO  SnmpGet 77
 oid:1.3.6.1.2.1.1.9.1.3.8, var=View-based Access Control Model for SNMP.
2014-12-04 17:19:20,135 [main] INFO  SnmpGet 77
 oid:1.3.6.1.2.1.1.9.1.4.1, var=0:00:00.01
2014-12-04 17:19:20,136 [main] INFO  SnmpGet 77
 oid:1.3.6.1.2.1.1.9.1.4.2, var=0:00:00.01
2014-12-04 17:19:20,137 [main] INFO  SnmpGet 77
 oid:1.3.6.1.2.1.1.9.1.4.3, var=0:00:00.01
2014-12-04 17:19:20,138 [main] INFO  SnmpGet 77
 oid:1.3.6.1.2.1.1.9.1.4.4, var=0:00:00.01
2014-12-04 17:19:20,139 [main] INFO  SnmpGet 77
 oid:1.3.6.1.2.1.1.9.1.4.5, var=0:00:00.01
2014-12-04 17:19:20,143 [main] INFO  SnmpGet 77
 oid:1.3.6.1.2.1.1.9.1.4.6, var=0:00:00.01
2014-12-04 17:19:20,143 [main] INFO  SnmpGet 77
 oid:1.3.6.1.2.1.1.9.1.4.7, var=0:00:00.01
2014-12-04 17:19:20,144 [main] INFO  SnmpGet 77
 oid:1.3.6.1.2.1.1.9.1.4.8, var=0:00:00.01

SNMP Asynchronous Get

如果在程式中,無法在發送 snmp request 之後,直接等待結果,例如在 UI 界面上發送 request,但 UI 無法被 blocking 以等待 response 或 timeout 的狀況下,我們就必須要使用非同步 SNMP Get。

程式中主要是利用 ThreadPool,以 multithread 的方式,在背景中發送 request,並以 event listener 作為 call back function。

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.snmp4j.CommunityTarget;
import org.snmp4j.MessageDispatcherImpl;
import org.snmp4j.PDU;
import org.snmp4j.Snmp;
import org.snmp4j.event.ResponseEvent;
import org.snmp4j.event.ResponseListener;
import org.snmp4j.mp.MPv1;
import org.snmp4j.mp.MPv2c;
import org.snmp4j.mp.MPv3;
import org.snmp4j.mp.SnmpConstants;
import org.snmp4j.smi.GenericAddress;
import org.snmp4j.smi.OID;
import org.snmp4j.smi.OctetString;
import org.snmp4j.smi.VariableBinding;
import org.snmp4j.transport.DefaultUdpTransportMapping;
import org.snmp4j.util.MultiThreadedMessageDispatcher;
import org.snmp4j.util.ThreadPool;
import org.snmp4j.util.WorkerPool;

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

    private static int version = SnmpConstants.version1;
    private static String protocol = "udp";

    private static CommunityTarget target = null;
    private static DefaultUdpTransportMapping udpTransportMapping = null;
    private static Snmp snmp = null;
    private WorkerPool threadPool = null;

    public static void main(String[] args) {

        String ip = "192.168.1.8";
        String community = "public";
        int port = 161;

        SnmpClientAsync tester = new SnmpClientAsync();
        try {
            tester.connect(ip, port, community);
            logger.info("");
            logger.info("SNMP GetList");
            List<String> oidList = new ArrayList<String>();
            oidList.add(".1.3.6.1.2.1.1.1.0");
            oidList.add(".1.3.6.1.2.1.1.3.0");
            oidList.add(".1.3.6.1.2.1.1.5.0");

            tester.snmpGet(oidList);

            // 非同步,必須等待一段處理時間
            Thread.sleep(3*1000);
        } catch (Exception e) {
            logger.error("Error:", e);
        } finally {
            tester.close();
        }
    }

    public void connect(String ip, int port, String community) throws Exception {
        String address = protocol + ":" + ip + "/" + port;

        target = new CommunityTarget();
        target.setCommunity(new OctetString(community));
        target.setAddress(GenericAddress.parse(address));
        target.setVersion(version);
        target.setTimeout(2 * 1000L); // milliseconds
        target.setRetries(3); // retry 3次

        try {
            threadPool = ThreadPool.create(ip + "SNMPWorkPool", 2);
            MultiThreadedMessageDispatcher dispatcher = new MultiThreadedMessageDispatcher(
                    threadPool, new MessageDispatcherImpl());

            udpTransportMapping = new DefaultUdpTransportMapping();

            snmp = new Snmp(dispatcher, udpTransportMapping);
            snmp.getMessageDispatcher().addMessageProcessingModel(new MPv1());
            snmp.getMessageDispatcher().addMessageProcessingModel(new MPv2c());
            snmp.getMessageDispatcher().addMessageProcessingModel(new MPv3());
            snmp.listen();
        } catch (Exception e) {
            logger.error("Error", e);
            throw e;
        }
    }

    public void close() {
        if (snmp != null) {
            try {
                snmp.close();
            } catch (IOException ex1) {
                snmp = null;
            }
        }
        if (udpTransportMapping != null) {
            try {
                udpTransportMapping.close();
            } catch (IOException ex2) {
                udpTransportMapping = null;
            }
        }
        if (threadPool != null) {
            threadPool.stop();
            threadPool = null;
        }
    }

    public void snmpGet(List<String> oidList) throws Exception {
        try {
            PDU pdu = new PDU();
            pdu.setType(PDU.GET);
            for (String oid : oidList) {
                pdu.add(new VariableBinding(new OID(oid)));
            }

            snmp.send(pdu, target, null, listener);
        } catch (Exception e) {
            logger.error("Error", e);
            throw e;
        }
    }

    public void printResponse(List<VariableBinding> datalist) {
        for (VariableBinding vb : datalist) {
            logger.info("oid:" + vb.getOid() + ", var=" + vb.getVariable());
        }
    }

    ResponseListener listener = new ResponseListener() {
        public void onResponse(ResponseEvent event) {
            ((Snmp) event.getSource()).cancel(event.getRequest(), this);
            PDU response = event.getResponse();
            PDU request = event.getRequest();
            //logger.debug("[request]:" + request);

            if (response == null) {
                logger.debug("[ERROR]: response is null");
            } else if (response.getErrorStatus() != 0) {
                logger.debug("[ERROR]: response status"
                        + response.getErrorStatus() + " Text:"
                        + response.getErrorStatusText());
            } else {
                logger.debug("Received response Success!!!");
                List<VariableBinding> datalist = new ArrayList<VariableBinding>();
                for (int i = 0; i < response.size(); i++) {
                    VariableBinding vb = response.get(i);
                    logger.debug(vb.toString());
                    datalist.add(vb);
                }
                printResponse(datalist);
            }
        }
    };

}

測試結果

2014-12-08 10:18:07,548 [main] INFO  SnmpClientAsync 49

2014-12-08 10:18:07,581 [main] INFO  SnmpClientAsync 50
 SNMP GetList
2014-12-08 10:18:07,607 [192.168.1.8SNMPWorkPool.0] DEBUG SnmpClientAsync 151
 Received response Success!!!
2014-12-08 10:18:07,607 [192.168.1.8SNMPWorkPool.0] DEBUG SnmpClientAsync 155
 1.3.6.1.2.1.1.1.0 = Linux koko.maxkit.com.tw 2.6.32-431.el6.i686 #1 SMP Fri Nov 22 00:26:36 UTC 2013 i686
2014-12-08 10:18:07,610 [192.168.1.8SNMPWorkPool.0] DEBUG SnmpClientAsync 155
 1.3.6.1.2.1.1.3.0 = 4 days, 18:12:20.49
2014-12-08 10:18:07,611 [192.168.1.8SNMPWorkPool.0] DEBUG SnmpClientAsync 155
 1.3.6.1.2.1.1.5.0 = koko.maxkit.com.tw
2014-12-08 10:18:07,611 [192.168.1.8SNMPWorkPool.0] INFO  SnmpClientAsync 133
 oid:1.3.6.1.2.1.1.1.0, var=Linux koko.maxkit.com.tw 2.6.32-431.el6.i686 #1 SMP Fri Nov 22 00:26:36 UTC 2013 i686
2014-12-08 10:18:07,611 [192.168.1.8SNMPWorkPool.0] INFO  SnmpClientAsync 133
 oid:1.3.6.1.2.1.1.3.0, var=4 days, 18:12:20.49
2014-12-08 10:18:07,612 [192.168.1.8SNMPWorkPool.0] INFO  SnmpClientAsync 133
 oid:1.3.6.1.2.1.1.5.0, var=koko.maxkit.com.tw

Reference

Java實現snmp的get和walk代碼示例

Introduction to snmp4j

2014/12/13

國姓爺的寶藏 - 蘇上豪

會知道蘇上豪這位醫師作家,是因為這篇借刀醫人的開膛史文章的介紹,開膛史這本書買了但還沒有看,先看了這本國姓爺的寶藏,這本書是以國姓爺鄭成功為引子,藉由國姓爺後代尋找當時留下寶藏的過程,介紹了台灣各地的特殊小吃,以及一些特別的景點。

如果想要像電影「國家寶藏」那樣有刺激的過程,可能會有點小失望,整本小說最有趣的地方,是在描述尋寶故事起源的序曲中,如果能用多一點的篇幅描述這一段就好了。

平常看的書,大部分都是由外語翻譯來的小說,中文作者的小說並不多,雖然像前一段所說的那樣,故事的高低起伏並不大,但讀起來還算是輕鬆愉快,不會因為人物變多,而有看不懂的感覺,書本前面的人物關係表幫了大忙,稍微翻一下,就可以回想起來故事的大綱。

如果尋寶的那一段,要更刺激,我覺得可以設計讓鄭成功的直系與旁系後代兩邊互相對抗爭奪寶藏,這樣應該會比一個鄭家後代委託尋寶的單線劇情還來得有趣。

或許可以讓這兩個後代,各成立一個組織,或甚至讓他們承襲鄭家流傳下來的武術,發展出兩套不同的武功,這樣就可以讓對抗的過程更戲劇化。

單就小說本身,雖然我的評語是平淡無奇,但仍然還算是一本容易閱讀的小說,建議讀者可以把這本書當作一個小品,因為這還算是很通順易懂的一個作品。就我來說,一位醫師也能寫出這樣的小說,一方面是欽佩,一方面是驚奇,我只能寫寫讀者不多的 blog,沒辦法寫出一個像樣的小說故事出來。

2014/12/12

秦始皇:一場歷史的思辨之旅 - 呂世浩

台灣大學在Coursera發布的課程中,受到最多學生迴響的,有一個是葉丙成老師的機率,另一個就是呂世浩老師的秦始皇coursera 中國古代歷史與人物--秦始皇,其中機率結合了線上遊戲的體驗,由學生分組出題的方式,在最近得到了創新教學冠軍。臺大電機線上遊戲學習PaGamO,奪全球創新教學冠軍!

這本書是呂世浩老師藉由秦始皇這個主題,告訴大家我們長久以來學習歷史的方式,讓我們忘記了一個最重要的事,學習的目的不是只有背誦下來的知識,而是要去思考,如何運用這些知識,成為知識的主人。

學習歷史的重點,不在背誦與記憶,最重要的是學習做人處事的方法,面對事情的時候,決策不會只有一種,也沒有標準答案,我們應該在了解歷史事件之後,反思當時的情況,並以設身處地的方式,假設自己在那個時空下,會做出什麼決定。用這個方式去認識與了解其他人,就能對其他人理解多一些。

如果沒有買書,也沒有時間去 MOOC 上課,可以先看一下這個演講影片:




這篇文章:《秦始皇:一場歷史的思辨之旅》教我的七件事,也有把一些重要的句子節錄出來。

根據呂老師說明「思辨」的方法與建議,我也對這一本書提出了一些我認為該再進一步往下思考的觀點。

唧唧 是歎息聲,不是織布機

閱讀這本書的第一個課程,就是先知道自己被機械式教育的毒害有多深,「唧唧復唧唧,木蘭當戶織。」 唧唧在課本裡面的解釋是織布機的聲音,幾乎所有人都會這樣回答,可是下一句「不聞機杼聲,惟聞女歎息」,很明確地告訴了讀者,沒有織布機的聲音阿!唧唧是嘆息聲。

是啊,為什麼我們從來都沒有懷疑過這個事情,我們該怪罪國文老師嗎?不對,問題在於,我們的教育模式,太過於強調標準答案,認為只有標準答案才能「正確」地將人分出高下。

呂老師在影片中也提到,為什麼我們沒有一門課,是教大家該如何選擇人生的伴侶,因為這一件事對我們一生的影響,遠比學習其他知識重要很多。

「背誦」是所有學問的基礎

雖然作者告訴我們,要以思辨的方法閱讀歷史,但思辨的背後基礎,還是要「背誦」,這是所有學問的基礎,我們的問題是,在課業與升學的壓力下,忘記了一些事情,忘記了我們該更深入認識與了解古人,用呂老師提醒的方式,將知識更進一步內化到心中。

幾乎所有的學問都是這樣,會先經過一段背誦的時期,再因為思辨與了解,會讓我們自然而然地把這些知識記憶起來,我們欠缺的,是再進一步地理解、運用與思考,而這些學問也會自然而然地進入長期記憶,我們也會慢慢地變成這個領域的專家。

經驗累積所帶來的副作用

我們常常會以自己的經驗去分析與判斷,寫程式更是如此,最直覺的解決方案,往往是自己最熟悉的方法,但這種模式並不一定永遠適用,有時就是會遇到所有的經驗與方法都不可行的狀況,一個完全沒有經驗的人,可能會提出一個有經驗的人永遠想不出來的方法。

這時候就會發生天降奇兵的狀況,老兵也得要認老,慢慢地就會發生「老兵不死,只是凋零」的窘境。

歷史就像是一個古人的經驗,在我們以理性分析的方法,認識古人的時候,就像是已經吸取了古人的經驗,在我們遇到問題時,如果腦袋裡一下子有好幾個古人跳出來,告訴你該怎麼處理,這時候,我們到底該怎麼辦呢。

就算是完全沒有讀過書的人,遇到事情的時候,也會得到周圍所有人的經驗與建議,在這樣的狀況下,該相信誰呢?

或許在這樣的時候,也只能反璞歸真,回頭相信自己的直覺。真的想太多,一直無法做下決定的人,可能就會在眾多意見與自己的想法之間繞圈圈,進入一種無窮迴圈式的思考模式。

其實不管什麼決定,帶來的結果與影響都是不可以且無法預先得知的,我們總是要踏出那一步,才知道最後的結果。做出決定後,就要放輕鬆,然後再做好準備,面對下一個遇到的人生課題。

當呂老師一直以這樣理性的方式分析與了解歷史人物,相信他的腦袋裡,同時會有很多個歷史人物會在遇到事情的時候跳出來給他建議,我很好奇在他遇到自己人生的分歧點時,他會如何面對與處理自己的人生呢?

能下決定的只有你自己

在自己、朋友、家人,再加上歷史人物的經驗之間,最終的決定還是在自己手上,就算是再怎麼困難的抉擇,每一件事情都會有最終決定的期限,等到倒數時間終止的那一刻,還是得做出你自己的決定。

2014/12/8

改變世界的九大演算法 - John MacCormick

本書所介紹的九大演算法是:搜尋引擎的索引(search engine indexing)、網頁排序(page rank)、公鑰加密(public-key cryptography)、錯誤更正碼(error-correcting codes)、模式辨識(pattern recognition,如手寫辨識、聲音辨識、人臉辨識等等)、資料壓縮(data compression)、資料庫(databases)、數位簽章(digital signature)。

寫程式的人很多,做IT工作的也很多,但並不是每一個人都能了解這些常用演算法背後的精神與原理,我也跟一般 IT 工程師一樣,沒有專業到完全了解這些演算法的來歷,沒有辦法把每一個演算法講出來,還能讓 dummy user 聽得懂。

本書的推薦序也講得很明白,要撰寫甚至出版這本書,本身就有很高的風險,科普書的禁忌是:出現的算式越多,賣得越差,所以這本書的目標讀者該是給像我們這樣的 IT 工程師閱讀,但如果是剛畢業沒多久的工程師們,應該都還有印象,在演算法課程時鴨子聽雷的窘樣。

這似乎也多少註定了書本的命運,銷售量肯定不會很高。但我還是強烈建議,應該花時間去把這本書看一看,這可讓我們對關聯式資料庫、Google搜尋等等項目,有更深一層的認識。專業工作者的一項最重要的功能,就是在遇到一些技術上要實作的問題時,能提出一套解決方案,而這本書的知識,就是你備而不用的專業背景知識。

接下來,我只討論「公鑰加密」這個章節,其他的部份,就留待讀者自己去看書了。「公鑰加密」這章的副標題是:用明信片寄送祕密,這個比喻非常貼切,因為網路上的資料,對所有網路節點來說,都是明白且清楚的,把資料放在網路上傳送,也就等同於我們寫了一張明信片投遞出去,所有經手這張明信片的人都能看到,上面寫了什麼。

最直覺的解決方式,就是收送雙方預先協商出一個密碼,但這樣會有另一個問題,當面對另一個不認識的人,我們就沒辦法預先協商密碼了。

作者用顏料混色法,來解釋應該要怎麼處理

  1. A 與 B 各自選擇一個「個人色」
  2. A 與 B 其中一位,公開宣佈一個不同的新顏色為「公共色」
  3. A 與 B 各自將公共色與個人色混合,製造出一個「公共個人混色」
  4. A 取得 B 的「公共個人混色」,再加入自己的「個人色」。同時間 B 取得 A 的「公共個人混色」,再加入自己的「個人色」。結果兩個人製造出完全一樣的三色混色結果。

於是這最終的三色混漆,就成了 A 與 B 的共同祕密混漆。由於 C 不知道 A 與 B 的「個人色」,因此就無法破解「公共個人混色」

電腦不是混色,而是將顏色改為數字,混色改為乘法。最有明的公鑰加密方法是 RSA。了解這個章節搭配上第九章數位簽章的內容,就可以知道,我們怎麼在公開的 Internet 環境中,製作一個在任意的 A 與 B 兩地之間,安全地傳送資料的計算環境。

在我們學習網路程式設計時,通常會連帶學到,區域網路的廣播特性,還有網路監聽側錄 sniffer 程式的使用,因為撰寫網路程式就是在跟封包資料奮戰,我們常常得一個一個 byte 去觀察收送兩端傳送資料的正確性,才知道自己寫的程式到底對或錯。

sniffer 技巧如果用在 hacker 行為上,只要在網路上經過的節點上放置 sniffer 程式,就可以錄製到所有的封包資料,雖然一般使用者沒辦法想像網路的傳輸協定,但都能理解到,有個監聽者在監聽資料的狀況下,沒有資料加密機制,就等於拿一張明信片在上面寫字,告訴大家我在講什麼。

還記得以前的宿舍網路,當時還是用同軸電纜,網路一段一段加上 repeater,當時也是 BBS 流行的時代,所以只要在其中有一台電腦裝上 sniffer,就可以看到大家在 BBS 上跟妹妹 talk 的肉麻對話。

雖然現在的集線器已經都是 switch 而不是 hub,switch 不會將某個 port 的資料轉送到其他所有 port 上,但是 hacker 還是可以透過 ARP 的方式,製造出 man-in-the-middle 的環境,就能取得特定 IP (range) 的進出封包,資料加密機制在現今有缺陷的網路協定上,是不可或缺的技術。

博客來:改變世界的九大演算法:讓今日電腦無所不能的最強概念

2014/12/1

Google 模式 - Eric Schmidt & Jonathan Rosenberg

這本書是 Eric Schmidt 與 Jonathan Rosenberg 所撰寫,內容是說明 Google 的企業文化,Larry Page 在推薦序中指出,Google 人就該有自主思考的力量,每個人都該大膽思考,探尋可能,解決問題,尋找答案。

Google 的文化也不是在公司開門的第一天就建立起來了,我們該有開放的態度,慢慢地去調整與建立一個屬於我們自己的企業文化。接下來,我只把一些我想再討論的議題 highlight 出來,並嘗試進行一些討論,其中會夾雜著我的個人意見。

持續賺錢的方法

大家都知道 Google 最賺錢的業務就是 Adwords,也因為這個能夠持續獲利的金雞母,讓 Google 可以貫徹自己的企業理念,可以不斷賠錢去實驗一大堆高風險的新專案。

但在這樣思考的同時,我們也該想到,如果 Google 天生沒有擁抱創新的原生泥漿,也可能不會誕生一個 Adwords。有點雞生蛋或是蛋生雞的問題。

但很明確的是,Google 在 2002 年以前都還是用最重要的 100個專案的表單來進行資源分配與管理,也就是說,在公司發展的初期,受限於當時的時空背景,Google 還是得用傳統的方式,集中發展的資源在最高決策者認定最有發展潛力的專案上。

一直等到穩定獲利的時空下,才來談 70/20/10 的資源分配法則,這都是為了讓公司規模在快速膨脹下,還能夠保有企業內部創業的原生文化才提出的口號與決定,20% 的資源放在有初步成果的新產品,10% 投入在全新的專案。

10%的資源投資很適當,還有一個道理:創造力熱愛資源受限(creativity loves constraints)。真正有效的想法,反而在受限資源的條件下,讓人可以快速地切入重點。

別聽河馬的意見

河馬也就是公司裡面最高薪資的人的意見,但是決策品質基本上跟薪資高低無關,只要是一個有說服力的理論,就能勝出。

Adword 發展初期遇到了「河馬」布林以及蘇利哈.拉瑪斯兩個點子的對抗,而爭辯的最後,布林的方式被放棄了,原因是布林了解討論中提出的資料,如果換成不了解的河馬,就可能會以職位強壓進行他自己提出的策略。

決策的討論要以資料為基礎進行討論,收集資料成了最重要的前提,如果是大家都沒有做過的事情,沒有解決過的議題,很明顯地就會落入不容易找到佐證資料的狀況。

如果是全新的想法,就可能要做 prototype 來實驗,例如 Google Books 的想法,一開始是用簡單的掃描機制去計算,掃描每一本書所需要耗費的時間,以此推論整個圖書館需要處理多久。

適應改變的能力

人才招募是最重要的事務,找對了人,就能在環境中得到相乘的效果,至於什麼樣的人才,對公司來說是最佳的人才,不同的公司可能有不同的答案,有些公司需要即時的人力,他考慮的只要有基本的程式技能就好了,不需要員工想太多,反正案子進來了,配合客戶把客戶照顧好,就沒有問題了。

在前一次進行面試時,應試者問了我一個問題,你認為我欠缺了什麼樣的能力,應該補足什麼樣的技能?我想了一下,給他的答案是,要擁有「適應改變的能力」。

我給他的理由是,在發展產品的過程中,有很多功能是在公司內部發想,然後進行開發,接下來推送到客戶端,根據回應進行產品功能修正。

什麼樣的功能對產品來說是好的?如果是 Google,可能就是用 20% 的時間先去開發一個 prototype,接下來在內部推動,並取得其他人的回饋意見,最終決策者會判定這樣的修正是不是可以被接受。

20% 的時間事實上是 120%

在沒有看過這本書以前,我們已經聽過 Google 人可以使用 20% 的時間自己進行專案不需要老闆的同意,但事實上這 20% 的 Free Time,還是有一些遊戲規則在背後的。

首先是 20% 的時間不代表每一星期都可以使用星期五作為 Free Time,而是在不影響現有工作的前提下,才能進行自己的專案,止於這 20% 的時間,並不一定就是在公司上班時間的 20%,而是要包含自己的時間。

換句話說,公司鼓勵內部創業,但自己的事情要先做好,而且內部創業,有想法之後,必須自己找到認同你的想法的人加入你的專案,取得別人的認同是首要條件。

至於時間,有些人是利用一個暑假,或是週六週日的時間,沒有人規定可以放掉手邊的工作,直接進行自己的專案。也就是說,20% 的 Free Time,在發展初期可能是 120%,一直要等到有初步成果開花結果下,公司才認定可以分配資源繼續進行下去。

使用 email 的方法

在處理對應客戶的問題的習慣下,我常常會做以下的事情,接收到問題,嘗試去解決並尋找答案,在一天內進行回應,我非常認同,每一個人做事都該盡可能地在很短的時間內,進行回應,就算是告訴客戶,你已經在了解中,但還不知道怎麼解決,這也是一種良性的回應。經常清除 email 收件匣這個建議,就跟快速回應是一樣的,因為要快速回應,我們就不應該常在收件匣裡面看到有還沒有處理過的 email。

讓要求事項很容易後續追蹤這個建議,讓我想到,gmail 的基本功能,其實就是圍繞著這個想法,收到郵件時,首先是過濾器,可輕鬆地設定標籤,決定優先順序,並排除掉 promo 或是垃圾信。接下來是閱讀與處理,如果閱讀後,認定需要追蹤處理,則可以歸類到 todo 的項目中。至於 Draft 也像是一種 todo,還沒有寫完的回信,都是必須要盡快處理的事項。

被挑戰的準備

正如同前面提到的適應改變的能力,我常常在等待我的意見被挑戰與否定,原因是一方面我不認為自己永遠是對的,如果只因為常常主導意見的角色,而壓抑了其他人的聲音,這並不是個健康的互動方式。

我承認有些時候,會因為時程的壓力,我必須進行強勢的時間安排與規格的主導,但在這樣的會議過程中,如果有不一樣的聲音,而且又有絕佳的理由與論點,這是非常有幫助的。如果我可以在還沒有實現的一開始,就馬上調整方法與作法,相對來說就節省了很多成本的浪費。

所以我必須告訴自己說,要有被挑戰的準備,而相對來說,也要做好準備挑戰其他人。

擁抱改變

我認定自己在專案管理上,一直以來常常在進行動態調控,因為我並沒有辦法在產品發展的初期,就知道會遇到什麼困難,因此我必須不斷地根據開發的狀況,遇到的問題與解決的狀況,動態調控人力的分配與資源,甚至有時候還需要更深入去了解問題,並在沒有在該專案上寫過任何一行 code的狀況下,嘗試去想像 programmer 可能會怎麼去寫這一段的程式邏輯,而因為這樣的邏輯缺陷,而造成某些問題。

我認為自己該「擁抱改變」,甚至該擁抱不間斷地改變,這改變並非一夕之間,但確實每天都在發生。企業生命體也該如此,每天持續地做出更適當的決定,在能持續生存的前提下,持續鍛鍊出更健康的公司體質。

如果你的出發點是個人的利益得失,那每天就得帶著鋼盔與龜殼做事,期待不會被子彈掃射,如果出發點是公司的成敗,那麼就該擇善固執,認為是對的事情,就得去做,並嘗試證明你的意見是對的。

2014/11/19 「關於把Google模式用在台灣,我想說的是...」
(轉)從書中Google的實際故事了解google獨樹一幟的管理哲學
《Google模式》:Eric Schmidt 教你 Google 人怎麼使用電子郵件
《Google 模式》:在網路時代找工作如同衝浪,讓技術洞察者為你指引明燈
Google人才招募九大守則,不想解雇員工,一開始就別錄取他
Google模式(讀後心得)
偷學《Google 模式》!學會 70/20/10 法則,建立說 Yes 的企業文化
《Google模式》談人才 -- 招募是最重要的事務!

2014/11/24

再用 list comprehension 解魔方陣

小朋友的學校又給了一個特殊的魔方陣題目,erlang 的 list comprehension 解魔方陣真的很快,把每一種可能發生的狀況慢慢列出來,就可以得出結果了。

題目:
五芒星共十個節點,填入 1 ~ 10 的數字,外側有五個小三角形,每一個小三角形的三個數字和,如果是 13,請列出十個節點的填法。再把總和改為14,15,16,17,18,19,20,分別找出可能的填寫方法。



如果用筆慢慢計算,必須要這樣想,五個三角形總和都是 13,13*5=45,因為 1~10 十個數字的總和是 55,但中間的 A,B,C,D,E 各被多加了一次,所以 A+B+C+D+E+55=45,因此 A+B+C+D+E = -10,此題無解。

至於 14,就是 A+B+C+D+E+55=14*5,所以 A+B+C+D+E=15,再慢慢列舉 A,B,C,D,E 進而找出 F,G,H,I,J。

用 erlang 可以這樣寫 matrix.erl

-module(matrix).
-export([resolve/1]).

resolve(SUM) ->
    statistics(runtime),
    statistics(wall_clock),
    K = lists:seq(1,10),
    L=[{A,B,C,D,E,F,G,H,I,J}||
     A <- K,
     B <- K--[A],
     F <- K--[A,B],
     A+B+F == SUM,

     C <- K--[A,B,F],
     G <- K--[A,B,F,C],
     B+C+G == SUM,

     D <- K--[A,B,F,C,G],
     H <- K--[A,B,F,C,G,D],
     C+D+H == SUM,

     E <- K--[A,B,F,C,G,D,H],
     I <- K--[A,B,F,C,G,D,H,E],
     D+E+I == SUM,

     J <- K--[A,B,F,C,G,D,H,E,I],
     A+E+J == SUM
    ],
    {_, Time1} = statistics(runtime),
    {_, Time2} = statistics(wall_clock),
    io:format("runtime=~p wall_clock=~p ~n", [Time1, Time2]),
    L.

編譯後,再到 erl 執行。

1> matrix:resolve(13).
runtime=0 wall_clock=0
[]
2> matrix:resolve(14).
runtime=15 wall_clock=15
[{1,3,5,2,4,10,6,7,8,9},
 {1,4,2,5,3,9,8,7,6,10},
 {2,4,1,3,5,8,9,10,6,7},
 {2,5,3,1,4,7,6,10,9,8},
 {3,1,4,2,5,10,9,8,7,6},
 {3,5,2,4,1,6,7,8,9,10},
 {4,1,3,5,2,9,10,6,7,8},
 {4,2,5,3,1,8,7,6,10,9},
 {5,2,4,1,3,7,8,9,10,6},
 {5,3,1,4,2,6,10,9,8,7}]

我們把一個答案填到五芒星中。



目前的缺點是 A,B,C,D,E 重複的 set,並沒有排除掉相同的答案,還沒想到要怎麼處理。

結果蠻奇怪的,13, 15, 18, 20 沒有解,14,16,17,19 有解,沒有什麼特別的規則。

2014/11/17

kamailio installation step by step

先前已經測試過 opensips,雖然可以使用,但還是遇到一些問題,例如網頁使用者界面的套件一直沒辦法運作地很順利,我們試著改成版本更新速度比較快的 kamailio,接下來就是安裝的過程。

準備工作

安裝 kamailio 之前,必須把 CentOS 基本的套件裝好,通常我們會把開發者工具、kernel 的開發套件都裝上去,還會裝上 EPEL、rpmforge 這兩個 package repository。

因為 kamailio 的 dialplan 需要,所以必須要安裝 pcre,因為 rtpengine 的需要,所以要安裝 xmlrpc-c-devel iptables-devel。

yum -y install pcre pcre-devel libpcap libpcap-devel libunistring libunistring-devel xmlrpc-c-devel iptables-devel

安裝 kamailio

kamailio 才剛在 2014/10/16 發布 kamailio 4.2.0 版,我們可以到 kamailio download page 下載 kamailio-4.2.0_src.tar.gz。

把原始程式碼放在 /usr/local/src 資料夾中。

cd /usr/local/src
tar zxvf kamailio-4.2.0_src.tar.gz

cd kamailio-4.2.0
make cfg

修改 modules.lst 一行資料

vi modules.lst

include_modules= db_mysql websocket tls dialplan

編譯並安裝 kamailio

make all
make install

現在安裝完成的 kamailio

設定檔在 /usr/local/etc/kamailio
執行擋在 /usr/local/sbin

這裡面有4個執行檔

kamailio - Kamailio SIP server
kamdbctl - script to create and manage the Databases
kamctl - script to manage and control Kamailio SIP server
sercmd - CLI - command line tool to interface with Kamailio SIP server

modules在 /usr/local/lib64/kamailio/modules/
文件在 /usr/local/share/doc/kamailio/
man page 在 /usr/local/share/man/man5/ 以及 /usr/local/share/man/man8/

產生 mysql database

編輯 /usr/local/etc/kamailio/kamctlrc

vi /usr/local/etc/kamailio/kamctlrc
把這一行設定的註解移掉
DBENGINE=MYSQL

修改 DB 預設的密碼

DBRWPW="dbpassword"
DBROPW="dbpassword"

執行

/usr/local/sbin/kamdbctl create

結果會產生兩個 mysql users,預設密碼的部份剛剛有改過了,應該會變成 dbpassword。

kamailio 預設密碼 kamailiorw
    有 'kamailio' database 完整權限
kamailioro 預設密碼 kamailioro
    有 'kamailio' database read-only 權限

雖然 已經改過 /usr/local/etc/kamailio/kamctlrc 的密碼,需要再一次覆寫資料庫的密碼。

mysql -u root -p
use mysql;
UPDATE user SET Password=PASSWORD("dbpassword") WHERE User='kamailio';
UPDATE user SET Password=PASSWORD("dbpassword") WHERE User='kamailioro';
flush privileges;

製作啟動服務的 script

先把 kamailio 核心的設定檔改好。

cp /usr/local/src/kamailio-4.2.0/pkg/kamailio/centos/6/kamailio.init /etc/init.d/kamailio
mkdir -p /etc/kamailio
cp /usr/local/etc/kamailio/kamailio.cfg /etc/kamailio/

修改 DB 的設定

vi /etc/kamailio/kamailio.cfg

在檔案前面增加三行
#!define WITH_MYSQL
#!define WITH_AUTH
#!define WITH_USRLOCDB
修改 DBURL 密碼
#!ifndef DBURL
#!define DBURL "mysql://kamailio:max168kit@localhost/kamailio"
#!endif

以 kamailio 的 init script sample 把啟動服務的 script 做好。

cp /usr/local/src/kamailio-4.2.0/pkg/kamailio/centos/6/kamailio.sysconfig /etc/sysconfig/kamailio

chmod 755 /etc/init.d/kamailio

修改 script 內容

vi /etc/init.d/kamailio
修改這2行
KAM=/usr/local/sbin/kamailio
RUN_KAMAILIO=yes


# 最後面增加 -f $KAMCFG 
OPTIONS="-P $PID_FILE -m $SHM_MEMORY -M $PKG_MEMORY -u $USER -g $GROUP $EXTRA_OPTIONS -f $KAMCFG "

修改執行 kamailio 的 user 權限

mkdir -p /var/run/kamailio
adduser --system --shell "/sbin/nologin" --home /var/run/kamailio kamailio
chown kamailio:kamailio /var/run/kamailio

設定 SIP Domain 變數, 有二種方式

1.
export SIP_DOMAIN=192.168.1.24
2.
vi /root/.kamctlrc
SIP_DOMAIN=192.168.1.24

執行 script 時有一些錯誤。

which: no greadlink in (/usr/lib64/qt-3.3/bin:/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin:/root/bin:/)

修改第 19 行可以解決這個問題

vi /usr/local/sbin/kamctl

which greadlink > /dev/null 2>&1

使用獨立的log檔案

檢查 kamailio.cfg 設定

vi /etc/kamailio/kamailio.cfg
debug=3 #此值控制日誌輸出的詳細程度,3為普通,4為詳細(會產生很多日誌)。
log_stderror=no #設置為no表示將日誌輸出到文件,否則輸出到控制台(應該是以前台方式啟動opensips服務時才有用)。
log_facility=LOG_LOCAL0 #應該是用於在syslog服務的配置文件裡區分opensips產生的日誌(見下面"使用獨立的log文件")。
fork=yes #設置為yes表示在後台啟動opensips服務,設置為no表示在前台啟動。

kamailio 使用syslog服務,在沒有作任何設定的狀況下,log 會進入/var/log/message這個檔案,如果希望使用獨立的log檔案,可以這樣設定

touch /var/log/kamailio.log
vi /etc/rsyslog.conf
增加一行
local0.* /var/log/kamailio.log

/etc/init.d/rsyslog restart

一併把 logrotate 設定好

vi /etc/logrotate.d/kamailio.logrotate
/var/log/kamailio.log {
   missingok
   rotate 5
   daily
   create 0640 root root
}

安裝 網頁界面 siremis

把 siremis 準備好

cd /usr/local/src
tar zxvf siremis-4.1.0.tgz
mv siremis-4.1.0 /var/www/html/siremis

產生 apache conf file

cd /var/www/html/siremis
make apache-conf

依照內容建議,編寫 apache httpd config for siremis

vi /etc/httpd/conf.d/siremis.conf
Alias /siremis "/var/www/html/siremis/siremis"
<Directory "/var/www/html/siremis/siremis">
    Options Indexes FollowSymLinks MultiViews
    AllowOverride All
    Order allow,deny
    Allow from all
    <FilesMatch "\.xml$">
        Order deny,allow
        Deny from all
    </FilesMatch>
    <FilesMatch "\.inc$">
        Order deny,allow
        Deny from all
    </FilesMatch>
</Directory>

修改資料夾權限,重新啟動 httpd

make prepare
chown -R apache:apache /var/www/html/siremis

service httpd restart

建立 siremis DB

mysqladmin create siremis -p

mysql -uroot -p
GRANT ALL PRIVILEGES ON siremis.* TO siremis@localhost IDENTIFIED BY 'dbpassword';

連上 siremis 網頁,一開始會是一個設定的 wizard,我們必須把資料庫的密碼,改成剛剛修改後的 dbpassword。

http://localhost/siremis/

在產生 DB 的畫面最下面,把這三個項目打勾

Import Default Data 打勾
Update sip database 打勾
Replace DB Config 打勾

最後只要看到 Installation Completed 的畫面,就完成安裝程序了。

2014/11/10

在 CentOS 6 安裝 redmine 專案管理工具

原本公司內是使用 bugzilla 做專案的 task/issue tracking 工具,但是 bugzilla 卻沒有 gantt chart 的功能。我們需要的是一個可以取代 bugzilla 的專案管理工具,這要分兩個部份來看,programmer 必須要能使用 Eclipse Mylyn 直接連結、查看、處理跟自己相關的 task/issue,專案管理者要能看到 gantt chart,再加上一些其他的管理工具,survey 後就選擇跟 trac 相近的 redmine。

雖然 redmine 在安裝上並不像 php 專案那麼簡單,但因為內建提供了 gantt chart,網頁使用者界面又比較簡潔,因此就試著安裝 redmine,接下來進行試用,最後就是把 bugzilla 退役。

準備工作

安裝 redmine 之前,必須把 CentOS 基本的套件裝好,通常我們會把開發者工具、kernel 的開發套件都裝上去,還會裝上 EPEL、rpmforge 這兩個 package repository。

redmine 的官方網頁目前提供了兩個版本 2.5.3 以及 2.6.0,我先把兩個版本的原始程式碼取回來。

安裝 redmine

首先把 Ruby on Rails 相關的套件裝好。

yum -y install ruby ruby-devel ImageMagick ImageMagick-devel rubygem-rake

wget http://download.opensuse.org/repositories/home:csbuild:centosextra/CentOS_CentOS-6/home:csbuild:centosextra.repo
mv home:csbuild:centosextra.repo /etc/yum.repos.d/

yum -y install rubygem-bundler

在 MariaDB(或是 MySQL)建立 redmine DB 跟使用者資料。

mysql --user=root --password=dbpassword
create database redmine character set utf8;
create user 'redmine'@'localhost' identified by 'dbpassword';
grant all privileges on redmine.* to 'redmine'@'localhost';
quit;

解壓縮 redmine 原始程式碼,一開始我們是先試著安裝 2.6.0 版,但因為到後面一直遇到 email 的設定問題,嘗試多個解決方案的過程中,又使用 2.5.3 版重新安裝了一次,基本上 2.6.0 以及 2.5.3 的安裝過程沒有什麼很大的差異。

tar zxvf redmine-2.6.0.tar.gz
mv redmine-2.6.0 /var/www/redmine

cd /var/www/redmine/config
cp database.yml.example database.yml

設定資料庫使用者名稱、密碼。

vi database.yml

把
production:
  adapter: mysql2
  database: redmine
  host: localhost
  username: root
  password: ""
  encoding: utf8

改為

production:
  adapter: mysql2
  database: redmine
  host: localhost
  username: redmine
  password: "dbpassword"
  encoding: utf8

調整 Gemfile 的相依性套件,增加 mongrel, dispatcher

vi /var/www/redmine/Gemfile

在
gem "rbpdf", "~> 1.18.1"
這一行的後面增加兩行

gem 'mongrel', '>= 1.2.0.pre2'
gem 'dispatcher'

安裝 ROR 相關套件,在鍵入 bundle install 這一個步驟的指令之後,console 的畫面會停住,看起來很像是當掉的狀態,而且會停著大約10~20分鐘,搜尋解法後,大家回應只說這可能是 https://rubygems.org/ 的網路問題,所以這個步驟就只能等,沒有別的解決方法。

cd /var/www/redmine
bundle install --without development test

安裝 redmine 資料庫

rake generate_secret_token
RAILS_ENV=production rake db:migrate
# 載入預設資料
RAILS_ENV=production rake redmine:load_default_data

# 輸入 zh-TW

如果先裝了 2.6.0 再重裝 2.5.3,在上面這個步驟,就會遇到 rake 升級後的 版本錯誤,這時候就改用下面這些指令

bundle exec rake generate_secret_token
RAILS_ENV=production bundle exec rake db:migrate
RAILS_ENV=production bundle exec rake redmine:load_default_data

調整 redmine 資料夾

mkdir -p tmp tmp/pdf public/plugin_assets
chown -R apache:apache files log tmp public/plugin_assets
chown -R apache:apache /var/www/redmine
chmod -R 755 files log tmp public/plugin_assets

基本上這樣就裝好了,可以直接用 webrick 啟動 redmine。

ruby script/rails server webrick -e production

redmine 網頁會在

http://localhost:3000/

跟 Apache 整合

因為要用Apache當作我的Web Server,所以要透過 Phusion passenger 做和 Ruby on Rail的處理。

首先要安裝Phusion passenger

yum -y install make zlib-devel ruby-devel rubygems ruby-libs apr-devel apr-util-devel httpd-devel mod_dav_svn subversion subversion-ruby automake autoconf curl-devel darcs hg bzr

cd /var/www/redmine/
gem install passenger
passenger-install-apache2-module

畫面看起來很奇怪時, 就鍵入 ! ,因為預設就選擇了 ruby python, 所以就直接 enter。

在/etc/httpd/conf.d/加上redmine.conf,並編輯redmine.conf加入以下的設定。

vi /etc/httpd/conf.d/redmine.conf

LoadModule passenger_module /usr/lib/ruby/gems/1.8/gems/passenger-4.0.53/buildout/apache2/mod_passenger.so
<IfModule mod_passenger.c>
    PassengerRoot /usr/lib/ruby/gems/1.8/gems/passenger-4.0.53
    PassengerDefaultRuby /usr/bin/ruby
</IfModule>

RailsBaseURI /redmine
<Directory /var/www/redmine/public>
    # This relaxes Apache security settings.
    AllowOverride all
    Options -MultiViews
</Directory>

在DocumentRoot的路徑下加上redmine的symbolic link

ln -s /var/www/redmine/public /var/www/html/redmine

這個設定過程很重要,因為 redmine 官方只提供了以 virtual host 的方式跟 apache 整合,但我們希望用 http://localhost/redmine/ 的網址方式,連結 redmine,努力兩天後,得到上面的解決方案。

設定 email

首先取得設定檔

cd /var/www/redmine/config
cp configuration.yml.example configuration.yml

因為我們是使用 gmail 帳號,同時也使用了 google apps 代管公司的 email,在設定 email 的時候,會遇到很多狀況,基本上再努力兩天,得到下面的解決方案。

修改 Gemfile.lock 的 mail 版本號碼, 由 2.5.4 改為 2.5.3

vi /var/www/redmine/Gemfile.lock

修改第 7 行
    mail (~> 2.5.3)

修改第 50 行
    mail (2.5.3)

修改後要執行

bundle install --without development test

如果是使用 gmail 帳號,要把 POP3 的功能打開,然後填寫設定

vi /var/www/redmine/config/configuration.yml
    delivery_method: :smtp
    smtp_settings:
       enable_starttls_auto: true
       address: "smtp.gmail.com"
       port: 587
       domain: "smtp.gmail.com"
       authentication: :login
       user_name: "maxkit@gmail.com"
       password: "youremailpassword"

如果是使用 google apps 代管 email 帳號,要把 POP3 的功能打開,然後填寫設定

vi /var/www/redmine/config/configuration.yml
  email_delivery:
    delivery_method: :smtp
    smtp_settings:
      enable_starttls_auto: true
      address: "smtp.gmail.com"
      port: 587
      domain: "maxkit.com.tw"
      authentication: :login
      user_name: "maxkit@maxkit.com.tw"
      password: "youremailpassword"

讓 eclipse mylyn 可以使用 redmine

Redmine官網的HowToMylyn裡提到Redmine的V2.x以後需要使用Redmine-Mylyn Connector。但是這個只是服務端的plugin。客戶端的Eclipse plugin有好幾個git repo的clone。目前最活躍的是這兩個。

服務端的Redmine plugin的版本庫地址: https://github.com/danmunn/redmine_mylyn_connector

客戶端的Eclipse plugin的版本庫地址: https://github.com/ljader/redmine-mylyn-plugin

安裝過程如下

cd /var/www/redmine/plugins
git clone git://github.com/danmunn/redmine_mylyn_connector.git
cd ..
bundle install --without development test

Eclipse 的部份則是先下載 plugin: net.sf.redmine_mylyn.p2repository-0.4.0-SNAPSHOT.zip

接下來在 Eclipse -> Help -> Add New Software -> Add -> Archive ,選取 net.sf.redmine_mylyn.p2repository-0.4.0-SNAPSHOT.zip 後就可以安裝 plugin,剩下的部份,就跟一般設定 mylyn repository 一樣了。

結語

雖然 redmine 的界面簡潔,但要安裝 remine 會遇到不少困難,而這些問題,在官方網站卻找不到明確的幫助,反而得要自己慢慢地 google 搜尋,然後測試每一個人說的到底對或錯,才能解決這些奇怪的問題。

2014/11/3

scala: 不在迴圈中使用變數

通常在撰寫迴圈時,最直覺的寫法,就是帶入變數,隨時在迴圈中檢查變數的值,也可以利用變數控制,是否要跳出迴圈,但 functional style programming 的重點,除了希望 programmer 能用更易讀的方式撰寫程式,同時希望程式的效能,可以得到顯著的提昇,因此消滅不必要的迴圈與變數,成了 functional programming 的另一項重要任務,而 java programmer 要學習的,是忘記那些 OO Design Patterns,讓程式碼更精簡。

Java 的定位是商用語言,以往的歷史也證實,在 Server Side 的運算環境中,採用 Java J2EE solution 是個很好的選擇,但也因為是 Server Side 的語言,java programmer 比較不在意使用了多少變數,消耗了多少記憶體,著眼點通常放在要把功能做好,模組切割要合理,系統要穩定,只要選個好一些的 Server 多一些記體體就可以運作了。

Function Programming 讓我重新回到撰寫 C 語言程式碼的心情,C 語言追求卓越的速度,在意自己使用了多少記憶體,有時候甚至還要動用 assembly,相容於 Java 的 Scala 是個雙面人,我們該沿用 Java 帶來的物件導向分析習慣,將系統模組化,接下來在實作時,思考如何撰寫高效率的 FP 程式。

if expression

  var filename = "default.txt"
  if (!args.isEmpty)
    filename = args(0)

如果換個寫法,就可以不需要使用 var,程式也比較短,使用 val 也比較接近 functional style,要使用 variable 之前,要先想一下是不是可以 改寫成 expression,另外要盡可能使用 val,可讓程式更容易 refactor。

  val filename =
    if (!args.isEmpty) args(0)
    else "default.txt"

while loops

unit value 寫成 (),() 的存在,就是跟 java void 不同的地方,因為 greet() 會回傳 Unit, 所以 greet() 就會等於 ()。

  scala> def greet() { println("hi") }
  greet: ()Unit

  scala> greet() == ()
  hi
  res0: Boolean = true

以 java 的習慣,可能會這樣寫

  var line = ""
  while ((line = readLine()) != "")
    println("Read: "+ line)

但因為 line = readLine() 的結果是 () Unit,而 Unit 一定不會等於 "",所以就會永遠是 true

  var line = ""
  do {
    line = readLine()
    println("Read: "+ line)
  } while (line != "")

使用 recursion 取代 while

如果要計算 g.c.d 可以這樣寫

  def gcdLoop(x: Long, y: Long): Long = {
    var a = x
    var b = y
    while (a != 0) {
      val temp = a
      a = b % a
      b = temp
    }
    b
  }

改用 functional style: recursion 撰寫 gcd, 同時可以省去很多不需要的vars。

  def gcd(x: Long, y: Long): Long =
    if (y == 0) x else gcd(y, x % y)

generator

  val filesHere = (new java.io.File(".")).listFiles

  for (file <- filesHere)
    println(file)

file <- filesHere 的語法稱為 generator

如果要對一堆檔案重複進行某個運算,也許我們會很直覺地這樣寫

  for (i <- 0 to filesHere.length - 1)
    println(filesHere(i))

這種寫法的缺點是需要產生一個變數 i,因此這種寫法,在 scala 是不常見的,我們必須修改成,直接對 file collection 做 iteration,而 iteration 可能有下面四種問題:filtering、nested iteration、mid-stream variable binding、producing a new collection。

filtering

取得檔名是 .scala 結尾的 檔案

  val filesHere = (new java.io.File(".")).listFiles
  for (file <- filesHere if file.getName.endsWith(".scala"))
    println(file)

也可以使用兩個以上的 filter

  for (
    file <- filesHere
    if file.isFile
    if file.getName.endsWith(".scala")
  ) println(file)

nested iteration

在 for 裡面,也可以同時使用多個 generator

  def fileLines(file: java.io.File) = 
    scala.io.Source.fromFile(file).getLines().toList

  def grep(pattern: String) =
    for (
      file <- filesHere
      if file.getName.endsWith(".scala");
      line <- fileLines(file)
      if line.trim.matches(pattern) 
    ) println(file +": "+ line.trim)

  grep(".*gcd.*")

mid-stream variable binding

上面的程式碼,重複執行了 line.trim,如果想要只運算一次,就要產生一個變數 trimmed = line.trim,trimmed 用了兩次, 一次在 if, 一次在 println。

  def grep(pattern: String) =
    for {
      file <- filesHere
      if file.getName.endsWith(".scala")
      line <- fileLines(file)
      trimmed = line.trim
      if trimmed.matches(pattern)  
    } println(file +": "+ trimmed)

  grep(".*gcd.*")

producing a new collection

用 yield, 可以把過濾後的結果 array 紀錄起來,結果會得到 Array[File], 因為 filesHere 是 array, file 是 File。yield 必須放在 for 的外面,語法為 for clauses yield body。

  def scalaFiles =
    for {
      file <- filesHere
      if file.getName.endsWith(".scala")
    } yield file

try - catch - finally

在 finally 中關閉 file, 確保檔案一定有被關閉,scala 可以在 finally 裡面 return value, 但會把結果覆蓋掉。

  val file = new FileReader("input.txt")
  try {
    // Use the file
  } finally {
    file.close()
  }

match expression

類似 java 的 switch,預設是 _ ,每個 case 裡面不需要寫 break,不只可以用 int, enum, 也可以用 string,可直接將 match 結果,儲存到變數中。

  val firstArg = if (!args.isEmpty) args(0) else ""

  val friend =
    firstArg match {
      case "salt" => "pepper"
      case "chips" => "salsa"
      case "eggs" => "bacon"
      case _ => "huh?"
    }

  println(friend)

no break and continue

因為 scala 沒有 break 跟 continue,最簡單的方式,就是把 continue 換成 if,把 break 換成 boolean 變數。

  var i = 0
  var foundIt = false

  while (i < args.length && !foundIt) {
    if (!args(i).startsWith("-")) {
      if (args(i).endsWith(".scala"))
        foundIt = true
    }
    i = i + 1
  }

更好的寫法是 recursive 呼叫 seachFrom

  def searchFrom(i: Int): Int =
    if (i >= args.length) -1
    else if (args(i).startsWith("-")) searchFrom(i + 1) 
    else if (args(i).endsWith(".scala")) i
    else searchFrom(i + 1)

  val i = searchFrom(0)

Variable scope

scala 的 scoping rule 跟 java 大部分都一樣
只有一點不同,scala 可在 nested scopes 中定義同樣的 variable name。

  val a = 1;
  {
    val a = 2 // Compiles just fine
    println(a)
  }
  println(a)

但是在 scala interpreter 中,看起來雖然像是同一個 scope,但實際上是不同的,因此在 interpreter 中,變數可重複定義。

  scala> val a = 1
  a: Int = 1

  scala> val a = 2
  a: Int = 2

  scala> println(a)
  2

Reference

Programming In Scala by Martin Odersky, Lex Spoon, and Bill Venners

2014/10/27

functional object in scala

通常認為 functional object 的特性,就是使用了 immutable object 的特性。

immutable vs mutable object

immutable objects 比 mutables obejcts 多了幾個優點, 但有一個缺點。

優點是

  1. immutable objects 比 mutable objects 容易理解,因為他們不會隨著時間改變。

  2. 可任意傳送 immutable objects,但如果是 mutable table,則可能需要複製一份,以防止被其他程式碼修改。

  3. 因為不能修改 immutable object 的內容,所以兩個 threads 不能同時存取一個 immutable object。這在 multi-thread 環境會很好用。

  4. immutable objects 可安全地用在 hash table key 裡面,才不會發生這種情況:當 mutable object 用在 HashSet,下次使用 HashSet 時,物件可能就不見了。

缺點是

  1. immutable objects 需要較大的一塊記憶體空間,進行物件複製。

製作一個 Rational Number Immutable Object

// primary constructor,需要兩個 整數,分別是分子與分母
class Rational(n: Int, d: Int) {

    // scala 會直接將 class body 裡面,不屬於任何 field/method 的程式碼
    // 直接放到 primary constructor

    // scala 限制更多, 只能讓 primary constructor,
    // 在第一行呼叫 superclass constructor

    // require 語法是 preconditions
    // 因為有理數的分母不能為 0,必須在建立物件時,加上欄位檢查
    require(d != 0)

    // 66/42 可約分成  11/7
    // 需要計算 gcd(66,42)   greatest common divisor
    private val g = gcd(n.abs, d.abs)

    // 外部可直接使用 r.numer, r.denom 兩個欄位的數值
    val numer = n / g
    val denom = d / g

    // auxiliary constructors
    // 5/1  原本要寫 Rational(5,1)   可省略成  Rational(5)
    // 任何 auxiliary constructor 都可以在第一行 呼叫其他 constructor
    def this(n: Int) = this(n, 1)

    // 有理數的加法,為了保持 immutable object 的特性
    // 運算後,回傳新的 Rational object
    // 定義 operator method
    def +(that: Rational): Rational =
        new Rational(
            numer * that.denom + that.numer * denom,
            denom * that.denom)

    def +(i: Int): Rational =
        new Rational(numer + i * denom, denom)

    def -(that: Rational): Rational =
        new Rational(
            numer * that.denom - that.numer * denom,
            denom * that.denom)

    def -(i: Int): Rational =
        new Rational(numer - i * denom, denom)

    def *(that: Rational): Rational =
        new Rational(numer * that.numer, denom * that.denom)

    // Rational 要處理 r*2 的運算,必須要對 * 作 overloaded
    def *(i: Int): Rational =
        new Rational(numer * i, denom)

    def /(that: Rational): Rational =
        new Rational(numer * that.denom, denom * that.numer)

    def /(i: Int): Rational =
        new Rational(numer, denom * i)

    // 列印物件時,會自動呼叫 toString,否則預設列印出物件的 reference 位址
    // 這裡是覆寫 override 上層的 toString
    override def toString = numer + "/" + denom

    // gcd 最大公因數 greatest common divisor
    private def gcd(a: Int, b: Int): Int =
        if (b == 0) a else gcd(b, a % b)
}

identifier in scala

有四種

  1. alphanumeric identifier
    或 a letter 開始, 後面可以是 letters/digits/,$ 也算是一個 character,但這是保留給 scala compiler 使用,建議不要使用 $ , 以免跟 compiler 產生的 id 有衝突。

  2. operator identifier
    包含 1~多 個 operator characters (ex: + : ? ~ # )
    ex: + ++ ::: <?> :->

  3. mixed ifentifier
    包含 a alphanumberic identifier, 後面是 , 然後是 operator identifier。
    ex: unary
    + myvar_=

  4. literal identifier
    用 (...) 包含在裡面的任意 string
    ex: x <clinit> yield
    yield 是 java Thread class 的 static method,但在 scala 不能寫 Thread.yeild(),因為 yield 是 scala 的保留字,所以要改寫成 Thread.yield()。

implicit conversion

上面的 Rational 可支援 r2,但是無法支援 2r 的計算,為了解決這個問題,scala 讓我們建立一個 implicit conversion,自動轉換 integer 為 rational number。

implicit conversion 定義有 scope 的限制,如果要在 scala interpreter 裡面使用,則必須在 interpreter 裡面執行 implicit conversion 的定義。如果把 implicit method 定義放在 Rational class 裡面,則對 inpterpreter 來說,是沒有作用的。

implicit def intToRational(x: Int) = new Rational(x)

測試

scala> val r = new Rational(2,3)
r: Rational = 2/3

scala> 2 * r
res0: Rational = 4/3

雖然 implicit conversion 很好用,但可能會造成程式可讀性降低。

scala 的程式特性是簡潔,但在簡化程式碼的時候,同時要考慮到 readable 與 understandable 的要求,才不至於寫出難以維護的程式碼。

2014/10/20

迷你書 阿里巴巴的技術力量

因應阿里巴巴在美國上市的重大消息,InfoQ 將一些對阿里巴巴工程師的專訪,集結成一本迷你書 阿里巴巴上市背後的技術力量,我們可以從這些互動的 FAQ 訪問中,了解到這個 公司的文化與態度。

阿里巴巴的目標,是要幫助大陸的中小企業,讓他們也能很快地使用網路平台進行銷售與交易,平台使用的關鍵問題在使用者,而且是關於賣方與買方雙方面的問題。

甚至連技術深度導向的核心業務優化團隊,同樣也要面對使用者,開發者必須要將自己的優化成果推廣出去,系統夠好也要有人使用,除非你開發的東西已經超過目前全人類的想像,已經開發到了未來。

在政治集權的市場中,建立互信金流機制

在阿里巴巴上市現場,記者提出對 trust 的疑問,阿里巴巴如何說服美國投資者,面對中共政權,如何讓投資者相信,這是一個可以被信任的企業。



馬雲說明,阿里巴巴為了提供一個讓中小企業可以使用的 ecosystem,必須跟中共政權對話,建立互信基礎,這是必要的,政府跟企業必須合作,才能在非常糟糕的信用卡市場中,完成一個網路交易平台。 Every trust takes time to build.

架構師的歷程

要成為一個架構師,更重要的學習是來自於實踐,所謂一個行業的專家指的並不是他能力有多強,是指他碰到過了這個行業裡面所有的問題,同時他解決了,他就能成為專家。

架構師不僅僅要了解技術,同時要進行業務分析,更全面地了解問題與方法,才能更容易去掌握問題的全貌。

淘寶的架構變革

  1. 2003/5 ~ 2004/5 LAMP
  2. 2004/2 ~ 2008/3 weblogic -> JBoss,開發了 TFS, iSearch, TDBM, CDN
  3. 2007/10 ~ 2009/11 系統走向產品化、服務化,支援大型團隊的並行開發,逐步模組化、中心化、可快速擴充、提昇可用性。非核心資料庫由 Oracle 移至 MySQL,建立訊息系統與服務框架,淘寶開放平台(TOP)上線
  4. 2009/8 ~ now 逐步提供系統自動化,減少操作失誤機率

資料庫的演進

阿里巴巴一開始是使用 Oracle,發展至今,已經慢慢地藉由拆分的方法,移轉到 MySQL,至於 NoSQL 則是依照業務的需求而使用,因為阿里巴巴的核心業務比較複雜,並不適合直接改用 NoSQL,而是在適當的業務場景中,採用 NoSQL。

當資料庫性能不足時,就會考慮使用 Cache,目前有集中式與分散式兩種 Cache 解決方案,也有使用 memory cache。數據資料由集中式演進為分散式,可跨多個 IDC 進行容錯備份,數據資料異地同步。

拆分資料庫有專責單位,最重要的任務是,如何在拆分的過程中,達成不間斷服務,平順地過渡到新架構上。第一次拆分最大的 table,花了兩年的時間,現在進步到2~3個月。

企業訊息系統

訊息系統對阿里巴巴來說,是非常重要的,但是阿里巴巴沒有直接採用既有的訊息系統,反而選擇以 KafKa 的概念,重新實作一個,最重要的原因是「系統維護」,因為阿里巴巴是使用 Java 開發語言,而 KafKa 是用 Scala 開發的。

訊息系統最重要的是要維護資料的一致性,目前的規模是每天處理百億個訊息,系統 Loading 在 4~5 左右。

前端性能優化的重點

  1. 減少 http request
  2. 減少 redirect
  3. preload 資源
  4. 盡量減少 cookie 的大小
  5. delay loading
  6. asynchronous ajax,減少 DOM 節點數,加快render與 first byte時間
  7. CDN 加速
  8. delay render
  9. pre-resolve DNS
  10. js 不放在 header,避免 blocking concurrent render tasks

系統必須根據網站的特點進行優化,每個網站的轉換率、流量與使用者的特性不同,必須根據實際的數據,判斷優化是否對轉換率及流量是有幫助的,並不是無止境的優化。

JVM 團隊

核心系統開發部一開始,是在集團內部尋找需求,優化後,得到十倍的效能提昇,漸漸地累積出優化成果,內部其他團隊就會自動找開發部協助解決效能問題。

優化的方式,並不是要求應用程式修改程式碼,而是直接針對 JVM 進行修改與調整。但如果是計算密集的應用,就要用調整演算法的方式處理。

因為有優化 JVM 的經驗的工程師不多,因此這個單位大多都是由應屆畢業生進行專業培養。

這個單位的 KPI 不單純只看優化的結果,重要的是要對優化結果,找到適當的應用,把成果推廣出去,但有時候會因為各種原因,而用不上你修改後的東西。

Open Source

雖然阿里巴巴將自身開發的一些軟體以 Open Source 的方式發布出來,但對於阿里雲飛天平台,企業認為這是阿里巴巴的核心業務與價值,因此沒有開源。

開源對阿里巴巴來說,並不是一個核心企業價值,阿里巴巴是個需要賺錢的公司,這些開源專案,都是因為跟某些原始專案的團隊搭配上的問題,才刻意 fork 並調整的專案,而這些修改,都是因應阿里巴巴公司規模成長業務需要而去做的。

2014/10/19

洞 - 路易斯.薩其爾

洞是一本很奇特的兒童文學小說,故事的大綱,可以到 洞 wiki page 查閱,奇特的地方是,小朋友跟我讀過後的感受有些不同,這是一部1999年得獎的作品,我相信作品會得獎,一定有它特別的道理。

小朋友讀完後,認為這是一部故事情節很刺激的小說,主角史丹利.葉納慈特殊的倒楣家族歷史,雖然故事的前半段,有點不知所云,主角被意外送入了綠湖營,但情節從最後一個洞那裡開始,就狠緊張。

我還特別問了他一個問題,那些洋蔥跟水是從哪裡來的,他很快就回答出來,是山姆種的,還說了「一吻奪命」凱蒂.巴婁跟山姆的關係,重要的是,他了解故事情節有些部份是回顧過去發生過的事情,這個回答確定他是看得懂這個故事的。

在我剛讀完的時候,我的第一個直覺是,這麼特殊的情節,有情殺案、偷竊、集中營,為什麼這樣的故事會得獎。唯一的亮點,應該址有史丹利跟零蛋兩個人發展出來的友情,最後互相幫助並存活下來的過程,這個部份也是整個故事中,比較正面的情節,其他部份都是在描寫黑暗面。

故事的脈絡,是簡單而且直接的,沒有什麼隱藏的內情,我想這也是小朋友能看得懂的一個重要的原因。簡單的脈絡中,包含了綠湖營裡面的團體生活,少年之間如何互相鬥爭,藉以獲取最大的生存利基。另外是凱蒂.巴婁跟山姆之間發生的故事,還有左若尼夫人的詛咒,這三個部份巧妙地融合在故事當中。

書本最後的折頁中,另外介紹了三本得獎的作品,其中第一本讓我驚訝了一下,因為書名是「嗑藥」,很明顯就是一個在討論青少年在青春期過程中,為了種種原因誤入歧途,選擇用藥品麻醉自己,最後發現自己該換一個方式與角度看世界的故事,而這樣的故事也可以取得兒童文學獎,我得考慮一下,才能想清楚該不該買來給小朋友看。

2014/10/6

Classes and Objects in Scala

Scala 特地將 object 從 class 中取出來,讓 object 直接作為 singleton object 的保留字,原本在 Java 中需要透過 Singleton Design Pattern 才能達成的工作,Scala 內建了 singleton object,我們不需要再去了解 Design Pattern,只需要專注在開發 object 內容的工作上。

static method

在 object 中定義可以使用的 method,實際上用起來就像是在 Java 呼叫 static method 一樣。通常我們會在撰寫 Utility 程式中,用到 singleton 與 static method 的想法去實做,畢竟使用公用程式還要先產生物件,會覺得非常麻煩。

object StringUtil {
    def trimHead(s:String) = s.dropWhile(_ == ' ')

    def main(args: Array[String]):Unit = {
        println(StringUtil.trimHead(args(0)))
    }
}

測試

>scala testscala.StringUtil "   test  22 "
test  22

companion object

如果在同一個 scala 檔案中,包含了同樣名字的 class 與 object 兩個定義,在 scala 就稱為 companion object。

class StringUtil {
    val s = "     I am a string in class StringUtil"
}

object StringUtil {
    def trimHead(s: String) = s.dropWhile(_ == ' ')
    def getClassString(): String = {
        val su = new StringUtil()
        trimHead(su.s)
    }

    def main(args: Array[String]): Unit = {
        println(StringUtil.trimHead(args(0)))
        println(StringUtil.getClassString())
    }
}

如果使用 StringUtil.xxx ,實際上是呼叫 singleton object StringUtil,如果是 val su = new StringUtil(),實際上是產生了 StringUtil class 的 instance。

因此 su 是沒辦法呼叫 trimHead 這個定義在 object 的 method,而 StringUtil.s 也同樣是沒有辦法使用的。

仔細看一下 companion object,其實就跟定義一個 Java class,裡面寫上幾個 static method/field 完全一樣,scala 明確地用 object 與 class 兩個關鍵字,將 class/object 內部的靜態與一般區塊分開。

對於programmer來說,有了更簡潔的語法,更簡短的程式碼,又不需要了解 Singleton Design Pattern,整體來說,有什麼道理,不該繼續往 scala 前進呢?

Scala IDE in Eclipse 4.4 Luna

另外再提供一個資訊,目前 Scala IDE 正式版只能在 Eclipse 4.3 Kepler 上運作,如果是使用 Eclipse 4.4 Luna,則我們得要改用開發版的 Scala IDE。

雖然Scala IDE 4.0.0 Milestone 3在網頁上說可以用在 Eclipse Luna,不過實際上測試後是不行的,必須要使用Scala IDE Lithium (4.0), Nightly版本。

2014/9/29

Scala vs Groovy

當全世界都在走向 functional programming 的時候,身為一個傳統的 OOP Java Programmer 也必須要面對現實,不僅僅是 JDK 本身,慢慢地加入了 FP 的元素,還有其他跟 Java 相容的新語言,可以讓我們選擇如何踏入 FP 的世界。

Java 的優勢,是將近二十年來累積而成的龐大函式庫,如果說要以跟 Java 相容為前題,跨足進入 FP 的世界,我們可以選擇的是 GroovyScala,至於 Jython 或是 JRuby,都只能算是另一個語言的 Porting,就不需要去考慮使用他們。

在面對技術趨勢的選擇時,我們可以使用 Google Trend: Groovy vs Scala 觀察搜尋的趨勢,Groovy 跟 Scala 比較起來,還是 Scala 比較吸睛,重點還是在 Scala 天生支援的 Actor 功能。

以往 Java 一直都是在 Server Side 佔據著重要的地位,當我們撰寫 Server 程式時,最重要的就是要解決多人共用的運算環境,這是 Scala 從 erlang 借來的 concurrent 運算的 Actor 所要作的事情,由於 mailbox 並行運算在 erlang 已經被驗證過是個絕佳的實做方式,我們也更容易接受,選擇 Scala 作為 Java 的下一步,會是比較好的決定。

在 google 搜尋 groovy vs scala的時候,可以看到很多人的爭執。

  1. Scala? Groovy? Why Java is the right programming language for 2014 and beyond
  2. What are the key differences between Scala and Groovy?
  3. To Scala or Groovy? Which is better for a 'mathematical' approach?
  4. Java.next(): Groovy vs. Scala

在討論時,首先被提出來的就是 static type/dynamic type 的差異,再來是談到 learning curve 的差別,通常認為學習 scala 需要花多一些時間。

如果不深入談技術本質上的差異,我們再來看看 Java 爸爸 James Gosling 的代言

During a meeting in the Community Corner (java.net booth) with James Gosling, a participant asked an interesting question:

"Which Programming Language would you use now on top of JVM, except Java?".

The answer was surprisingly fast and very clear: - Scala.

再來看看 Groovy 的爸爸 James Strachan,在 blog 文章 Scala as the long term replacement for java/javac? 所說的話:

I can honestly say if someone had shown me the Programming in Scala book by by Martin Odersky, Lex Spoon & Bill Venners back in 2003 I'd probably have never created Groovy.

聽爸爸的話準沒錯!

2014/9/22

opensips - dr(dynamic routing)

上一次是將 routing 訊息直接填寫到 opensips.cfg 設定檔,但有另一個方式可以達到這個功能,就是使用 dynamic routing,因為 dynamic routing 是將設定好的 routing 資訊寫到 DB 裡面,再以 MI command(dr_reload) 在不停止 opensips 的狀況下,重新載入 routing 資訊。

senario

asterisk: 192.168.1.5
asterisk: 192.168.1.17
opensips: 192.168.1.24

兩個 asterisk 分別設定 siptrunk 指定 host 為 192.168.1.24,另外再以電話號碼 prefix 來區分,prefix 為 2 的分機是由 192.168.1.17 負責,prefix 為 1 是由 192.168.1.5 負責。

DB tables

跟 dynamic routing 有關的 3 個 DB tables

  1. dr_groups
    從 opensips.conf 中呼叫 dr 的 table
  2. dr_gateways
    route endpoints,也就是要填 asterisk 的 ips
  3. dr_rules
    存放 inbound DID 或 default routes 的 rules

填寫 table 資料

dr_groups

在這裡我們不管 inbound 的 username 與 domain,所以設定為 * ,最重要的是 groupid 0 這個資訊,這個 groupid 在後面用來設定 dynamic routes。

mysql> use opensips;
mysql> INSERT INTO dr_groups(username,domain,groupid,description) VALUES(".*",".*","0","INBOUND");

dr_gateway

address 的地方填寫 gateway ip:port,strip 欄位決定要去除電話號碼的幾位,probe_mode 這個欄位設定為 2 代表我們要 enable probe,使用一個 active gw 的列表。

mysql> INSERT INTO dr_gateways(type,gwid,address,strip,probe_mode,description) VALUES("0","1","192.168.1.5:5060","1","2","asterisk 5");

mysql> INSERT INTO dr_gateways(type,gwid,address,strip,probe_mode,description) VALUES("0","2","192.168.1.17:5060","0","2","asterisk 17");

dr_rules

dr_rules 設定撥號規則,groupid 填 0 就是剛剛填寫的 dr_groups "INBOUND",prefix 1 是電話號碼的前置碼,也可以填上完整的 DID 號碼。
gwlist 欄位是參考到 dr_gateways 的 gwid 資料

mysql> INSERT INTO dr_rules(groupid,prefix,priority,gwlist,description) VALUES("0","1","0","1","My Number");
mysql> INSERT INTO dr_rules(groupid,prefix,priority,gwlist,description) VALUES("0","2","0","2","My Number 2");

修改 opensips.conf

modules 裡面要加上

loadmodule "drouting.so"
loadmodule "db_mysql.so"

modparam("drouting", "db_url", "mysql://opensips:opensipsrw@localhost/opensips")
modparam("drouting", "probing_interval", 60)
modparam("drouting", "probing_from", "sip:probe@URI")
modparam("drouting", "probing_method", "OPTIONS")
modparam("drouting", "probing_reply_codes", "501, 403, 404")
modparam("drouting", "use_domain", 1)

在 #### INITIAL REQUESTS 這一行的後面,加上封包判斷,INVITE/CANCEL 時,就呼叫 route[gw]。
do_routing("0") 就是呼叫 groupid 為 0 的路由

if (method == "INVITE") {
    setflag(1);
    record_route();
    xlog("INBOUND CALL,$dd,$ru,$ci,$fn,$fu");
    route(gw);
    exit;
} else if ( is_method("CANCEL") ) {
    xlog("!!CANCEL\n");
    setflag(ACC_DO);
    setflag(ACC_FAILED);

    xlog("CANCEL CALL,$dd,$ru,$ci,$fn,$fu");
    route(gw);
    exit;
}

route[gw] {
    if (!do_routing("0")) {
        xlog("do_routing: No rules matching the URI\n");
        send_reply("503","No rules matching the URI");
        exit;
    }

    if (is_method("INVITE")) {
        t_on_failure("GW_FAILOVER");
    }
    route(RELAY);
}

最後處理找不到正確路由的狀況,這部份就保留原本設定檔的內容就可以了。

failure_route[GW_FAILOVER] {
    if (t_was_cancelled()) {
        exit;
    }

    # detect failure and redirect to next available GW
    if (t_check_status("(408)|([56][0-9][0-9])")) {
        xlog("Failed GW $rd detected \n");

        if ( use_next_gw() ) {
            t_on_failure("GW_FAILOVER");
            t_relay();
            exit;
        }

        send_reply("500","All GW are down");
    }
}

opensips.cfg

完整的 opensips.cfg

####### Global Parameters #########

debug=3
log_stderror=no
log_facility=LOG_LOCAL0

fork=yes
children=4

/* uncomment the following lines to enable debugging */
#debug=6
#fork=no
#log_stderror=yes

/* uncomment the next line to enable the auto temporary blacklisting of 
   not available destinations (default disabled) */
#disable_dns_blacklist=no

/* uncomment the next line to enable IPv6 lookup after IPv4 dns 
   lookup failures (default disabled) */
#dns_try_ipv6=yes

/* comment the next line to enable the auto discovery of local aliases
   based on revers DNS on IPs */
auto_aliases=no


listen=udp:192.168.1.24:5060   # CUSTOMIZE ME


disable_tcp=yes

disable_tls=yes

db_default_url="mysql://opensips:opensipsrw@localhost/opensips"


####### Modules Section ########

#set module path
mpath="/usr/lib64/opensips/modules/"



#### SIGNALING module
loadmodule "signaling.so"

#### StateLess module
loadmodule "sl.so"

#### Transaction Module
loadmodule "tm.so"
modparam("tm", "fr_timer", 5)
modparam("tm", "fr_inv_timer", 30)
modparam("tm", "restart_fr_on_each_reply", 0)
modparam("tm", "onreply_avp_mode", 1)

#### Record Route Module
loadmodule "rr.so"
/* do not append from tag to the RR (no need for this script) */
modparam("rr", "append_fromtag", 0)

#### MAX ForWarD module
loadmodule "maxfwd.so"

#### SIP MSG OPerations module
loadmodule "sipmsgops.so"

#### FIFO Management Interface
loadmodule "mi_fifo.so"
modparam("mi_fifo", "fifo_name", "/tmp/opensips_fifo")
modparam("mi_fifo", "fifo_mode", 0666)

#### URI module
loadmodule "uri.so"
modparam("uri", "use_uri_table", 0)

#### MYSQL module
loadmodule "db_mysql.so"

#### AVPOPS module
loadmodule "avpops.so"

####  DYNAMIC ROUTING module
loadmodule "drouting.so"
modparam("drouting", "db_url",
    "mysql://opensips:opensipsrw@localhost/opensips") # CUSTOMIZE ME
modparam("drouting", "probing_interval", 60)
modparam("drouting", "probing_from", "sip:probe@URI")
modparam("drouting", "probing_method", "OPTIONS")
modparam("drouting", "probing_reply_codes", "501, 403, 404")
modparam("drouting", "use_domain", 1)


####  PERMISSIONS module
loadmodule "permissions.so"
modparam("permissions", "db_url",
    "mysql://opensips:opensipsrw@localhost/opensips") # CUSTOMIZE ME

#### ACCounting module
loadmodule "acc.so"
/* what special events should be accounted ? */
modparam("acc", "early_media", 0)
modparam("acc", "report_cancels", 0)
/* by default we do not adjust the direct of the sequential requests.
   if you enable this parameter, be sure the enable "append_fromtag"
   in "rr" module */
modparam("acc", "detect_direction", 0)
modparam("acc", "failed_transaction_flag", "ACC_FAILED")
/* account triggers (flags) */
modparam("acc", "log_flag", "ACC_DO")
modparam("acc", "log_missed_flag", "ACC_MISSED")


#### DIALOG module
loadmodule "dialog.so"
modparam("dialog", "dlg_match_mode", 1)
modparam("dialog", "default_timeout", 21600)  # 6 hours timeout
modparam("dialog", "db_mode", 2)
modparam("dialog", "db_url",
    "mysql://opensips:opensipsrw@localhost/opensips") # CUSTOMIZE ME



####  DIALPLAN module
loadmodule "dialplan.so"
modparam("dialplan", "db_url",
    "mysql://opensips:opensipsrw@localhost/opensips") # CUSTOMIZE ME




####### Routing Logic ########

# main request routing logic

route{
    if (!mf_process_maxfwd_header("10")) {
        sl_send_reply("483","Too Many Hops");
        exit;
    }

    if ( check_source_address("1","$avp(trunk_attrs)") ) {
        # request comes from trunks
        setflag(IS_TRUNK);
    } else if ( is_from_gw() ) {
        # request comes from GWs
    } else {
        send_reply("403","Forbidden");
        exit;
    }

    if (has_totag()) {
        # sequential request withing a dialog should
        # take the path determined by record-routing
        if (loose_route()) {
            # validate the sequential request against dialog
            if ( $DLG_status!=NULL && !validate_dialog() ) {
                xlog("In-Dialog $rm from $si (callid=$ci) is not valid according to dialog\n");
                ## exit;
            }

            if (is_method("BYE")) {
                setflag(ACC_DO); # do accounting ...
                setflag(ACC_FAILED); # ... even if the transaction fails
            } else if (is_method("INVITE")) {
                # even if in most of the cases is useless, do RR for
                # re-INVITEs alos, as some buggy clients do change route set
                # during the dialog.
                record_route();
            }

            # route it out to whatever destination was set by loose_route()
            # in $du (destination URI).
            route(RELAY);
        } else {
            if ( is_method("ACK") ) {
                if ( t_check_trans() ) {
                    # non loose-route, but stateful ACK; must be an ACK after 
                    # a 487 or e.g. 404 from upstream server
                    t_relay();
                    exit;
                } else {
                    # ACK without matching transaction ->
                    # ignore and discard
                    exit;
                }
            }
            sl_send_reply("404","Not here");
        }
        exit;
    }

    #### INITIAL REQUESTS
    xlog("initial requests\n");
if ( is_method("INVITE") ) {
    xlog("!!INVITE\n");
    setflag(1);
    record_route();
    xlog("INBOUND CALL,$dd,$ru,$ci,$fn,$fu");
    route(gw);
    exit;
} else if ( is_method("CANCEL") ) {
    xlog("!!CANCEL\n");
    setflag(ACC_DO);
    setflag(ACC_FAILED);

    xlog("CANCEL CALL,$dd,$ru,$ci,$fn,$fu");
    route(gw);
    exit;
}

    if ( !isflagset(IS_TRUNK) ) {
        ## accept new calls only from trunks
        send_reply("403","Not from trunk");
        exit;
    }

    # CANCEL processing
    if (is_method("CANCEL")) {
        xlog("CANCEL");
        if (t_check_trans())
            t_relay();
        exit;
    } else if (!is_method("INVITE")) {
        send_reply("405","Method Not Allowed");
        exit;
    }

    if ($rU==NULL) {
        # request with no Username in RURI
        sl_send_reply("484","Address Incomplete");
        exit;
    }

    t_check_trans();

    # preloaded route checking
    if (loose_route()) {
        xlog("L_ERR",
        "Attempt to route with preloaded Route's [$fu/$tu/$ru/$ci]");
        if (!is_method("ACK"))
            sl_send_reply("403","Preload Route denied");
        exit;
    }


    # record routing
    record_route();

    setflag(ACC_DO); # do accounting


    # create dialog with timeout
    if ( !create_dialog("B") ) {
        send_reply("500","Internal Server Error");
        exit;
    }



    # apply transformations from dialplan table
    dp_translate("0","$rU/$rU");

    # route calls based on prefix
    if ( !do_routing("0") ) {
        send_reply("404","No Route found");
        exit;
    }

    t_on_failure("GW_FAILOVER");

    route(RELAY);
}


route[RELAY] {
    if (!t_relay()) {
        sl_reply_error();
    };
    exit;
}


route[gw] {
    if (!do_routing("0")) {
        xlog("do_routing: No rules matching the URI\n");
        send_reply("503","No rules matching the URI");
        exit;
    }

    if (is_method("INVITE")) {
        t_on_failure("GW_FAILOVER");
    }
    route(RELAY);
}

failure_route[GW_FAILOVER] {
    if (t_was_cancelled()) {
        exit;
    }

    # detect failure and redirect to next available GW
    if (t_check_status("(408)|([56][0-9][0-9])")) {
        xlog("Failed GW $rd detected \n");

        if ( use_next_gw() ) {
            t_on_failure("GW_FAILOVER");
            t_relay();
            exit;
        }

        send_reply("500","All GW are down");
    }
}


local_route {
    if (is_method("BYE") && $DLG_dir=="UPSTREAM") {

        acc_log_request("200 Dialog Timeout");

    }
}

啟動 opensips

用這個指令,重新啟動 opensips

service opensips restart

如果 opensips 已經啟動了,但修改了 DB table 裡面的 routing 資訊,就可以用這個指令讓 opensips 重新由 DB 載入 routing 資訊。

opensipsctl fifo dr_reload

測試

在 192.168.1.5 的分機,撥打 2000 到 opensips (192.168.1.24) 時,opensips 會自動將電話轉送到 192.168.1.17 的號碼 2000

在 192.168.1.17 的分機,撥打 1106 時,opensips 會自動將第一碼 1 去掉,電話轉送到 192.168.1.5 的號碼 106。

參考網頁

OpenSIPS Dynamic Routing