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月26日

Bash Shell 漏洞 Shell Shock

繼上次 OpenSSL Heartbleed 漏洞之後,Bash Shell 又中獎,出現 Shell Shock 漏洞。

我們習慣使用的 CentOS 已經提供了 bash patch,直接再安裝一次 bash,就會自動更新 bash 套件。

yum -y install bash

Shell Shock 要用以下的指令檢測

env x='() { :;}; echo vulnerable' bash -c "echo this is a test"

我們實際在更新與未更新的機器上測試。
沒有更新 bash 的機器結果如下

# env x='() { :;}; echo vulnerable' bash -c "echo this is a test"
vulnerable
this is a test

如果已經更新了 bash,結果如下

# env x='() { :;}; echo vulnerable' bash -c "echo this is a test"
bash: warning: x: ignoring function definition attempt
bash: 錯誤,輸入的函數定義為 `x'
this is a test

Bash Shell 漏洞威脅不小於 Heartbleed!Unix-Like 作業系統請儘速更新

2014年9月24日

Docker 簡介

緣由

最近在看InfoQ的新聞時,常常看到一個詞不斷出現,由於出現的頻率實在是太高了,讓我對它產生好奇心,到底是在做什麼的?怎麼會如此火熱?花了點時間去找尋相關資料,並試了一下其功能,真是特別的東西!

那個詞也就是本篇要說的主題,Docker

Docker簡介

Docker其實就是新一代的VM,只是他更加簡潔,並且更加方便。之前在使用的VM,比如說很常用VMWare,要使用VMWare來執行一個Linux作業系統,比如說要執行CentOS好了,首先必須準備一個ios檔或是安裝光碟,之後掛載它,執行完整的作業系統安裝流程,安裝結束之後才能開始執行,整個過程可能需要幾十分至幾小時之久。

而Docker就不一樣了,Docker要執行一個CentOS的作業系統,要做的步驟只有幾項,首先先下載CentOS的Image檔(CentOS官方有釋出),接著使用Docker Container執行它,就可以執行CentOS了,整個過程可能不到10分鐘吧,其中大半的時間還是花在下載Image檔上面(大約200MB),非常的神奇。

Image與Container

在開始使用之前,要先介紹兩個Docker常看到的名詞,Image和Container,這邊不討論他的實作原理與技術,而是要說明他們之間的相對關係。

Image為初始環境,而Container為執行Image時的實例。比如說今天想透過Docker用CentOS執行J2EE的服務,則先下載CentOS官方釋出的CentOS Image,下載完後要執行時,Docker會用一個Container來執行此CentOS image。你可以在Container內做任何事情,比如說安裝Tomcat、JDK、Web Application等等之類的,但是一切的行為,都只會在這個Container內,它不會影響到原本的Image。

接著你需要一個單獨安裝MySQL的CentOS服務器,則你可以在將Image透過另一個Container執行,接著在上面安裝MySQL,就完成了一個CentOS上的MySQL服務。這樣一來你就會擁有兩個Container,其中一個為J2EE的伺服器,建立在CentOS上,另一個為MySQL的伺服器,也建立在CentOS上。

下圖出自於官方對Image說明時所用到的圖片,其中可以看到Debian為Image,而Container在其上方,為執行image時的實例,由此圖可以了解他們倆之間的關係。

安裝Docker

由於本人是使用MacBook Air,因此只試了在Mac上安裝的流程。在Mac上安裝Docker非常的簡單,只要安裝Boot2Docker - OS X Installer即可。安裝完在應用程式內點開它,就會看到它開啓command line,這時他會先啟動docker deamon,我們只要等他跑完,即可在本機上使用docker了。

可以參考官方文件Installing Docker on Mac OS X來安裝,基本上就是下一步下一步一直執行即可安裝完成。其他作業系統的使用者可以參考Docker Installation選擇自己所使用的作業系統,來參考如何安裝。

開始玩Docker

在Mac的應用程式裡,點開boot2docker後,它會自己去啟動Docker deamon,等它跑完之後,就可以開始下指令與docker互動。

抓取CentOS image

Docker官方有提供一個Docker Hub網站,該網站上面存放著各種不同的開發人員與公司所提供的image檔,而我們可以透過指令,很輕鬆的從Docker Hub上抓取你想要的Image檔,在這邊要執行的是抓取CentOS官方提供的Image檔。

指令輸入:
    bash-3.2$ docker pull centos

會抓取:
    REPOSITORY          TAG                 IMAGE ID            CREATED             VIRTUAL SIZE
    centos              centos5             5a1ebaa356ff        9 days ago          484 MB
    centos              centos7             70214e5d0a90        13 days ago         224 MB
    centos              latest              70214e5d0a90        13 days ago         224 MB
    centos              centos6             68eb857ffb51        13 days ago         212.7 MB    

也可以只抓某個TAG,例如我只想抓取CentOS 6,則我可以下此指令:
    docker pull centos:centos6

使用Container執行CentOS Image

抓取完畢之後,接著就是用Container執行image,只要透過run的指令,即可執行。

指令輸入:
    bash-3.2$ docker run -t -i centos:centos6 /bin/bash

則畫面會變成:
    bash-4.1#

就這樣,此時你已經成功開啟了一個CentOS 6的VM,並開啟bash了。 可以透過以下指令來驗證:

指令輸入:
    bash-4.1# cat /etc/issue
印出:
    Linux version 3.16.1-tinycore64 (root@5d533df6cbc4) (gcc version 4.7.2(Debian 4.7.2-5) ) #1 SMP Fri Aug 22 06:40:10 UTC 2014
    bash-4.1# cat /etc/issue
    CentOS release 6.5 (Final)
    Kernel \r on an \m

Docker官方線上教學網站

如果你還在觀望Docker,對它有點興趣但又不想安裝到自己的電腦上的話,你可以先透過Docker Online Tutorial - Try It!這個官方提供的線上教學網站,實際操作Docker一遍,裡面會教學一些基本的操作,如搜尋Docker Hub上面的Image,用Container執行Image,還有本篇沒提到的Commit與Push指令,花些玩過一遍就會對Docker有些基本的了解。

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

2014年9月18日

erlang的loop

本篇介紹如何使用erlang來達到java, javascript的for的寫法

首先先寫幾行簡單的程式碼,接著再示範如何重覆的執行這些程式碼, 程式碼如下。

connect()->
    {ok,S} = gen_tcp:connect({127,0,0,1},2222,[{packet,2}]), (1)
    Result = gen_tcp:send(S,<<"hello">>), (2)
    Result.

(1)使用tcp連線到server的2222 port

(2)送hello字串到server

如果我們要將connect()執行100次的話該如何寫? 由於erlang沒有內建的for可以使用,所以我們只能用遞迴(recursive)的寫法,如下

run()->
    T = 100,          
    connect(T),       
    ok.

connect(T)->          
    if
        T>0->         
            {ok,S} = gen_tcp:connect({127,0,0,1},2222,[{packet,2}]), (1)
            gen_tcp:send(S,<<"hello">>),  (2)
            connect(T-1); 
        true->
            ok
    end.

先改寫connect function,在該function增加一個參數叫T(執行次數),如果T>0的話就執行(1)連線至tcp server (2)送hello字串至server,接著將T-1後繼續呼叫connect function,如果T=0的話就結束,不再遞迴呼叫

增加run function,在該function定義T來儲存要執行100次,接著就使用使參數來呼叫connect function。

上述寫法會有一個缺點,connection function的邏輯應該專注在執行連線、送資料部份,但現在裡面多加了與邏輯無關的程式流程控制在裡面,我們來看下列改良的寫法

run()->
    R = for(1, 100, fun connect/1),         (1)
    io:format("R=~p~n", [R]),
    ok.

connect(I)->
    {ok,S} = gen_tcp:connect({127,0,0,1},2222,[{packet,2}]),
    Result = gen_tcp:send(S,<<"hello">>),
    {I,Result}.

for(Max, Max, F) -> [F(Max)];               (2)
for(I, Max, F) -> [F(I)|for(I+1, Max, F)].  (3)

此寫法增加了for/3 function來控制程式要執行幾次,connect function僅專注在連線、送資料。

首先來看(2)(3) for function的部份,該function有3個參數,第1個參數是起始值,第2個參數是結束值,第3個參數是所要執行的function,也就是connection/1

接著我們來看(1) for(1, 100, fun connect/1),這有點像我們一般習慣的for寫法,也就是描述要執行connect/1 100次。

首先會先呼叫到for(I, Max, F),所以會先執行F(I)也就是connect(1),接著再把I+1後呼叫for(I+1, Max, F)也就是for(2, 100, F)。最終執行到for(100,100,F)。

整個執行過程會像下列...最終會回傳一個list存著每個connection function的回傳值 [connect(1), connection(2), connection(3).....connect(100)]

另外erlang有提供foreach的function,該function適用時機在List值已知,將再List裡的值一個一個傳給function去執行,範例如下

test_foreach()->
    lists:foreach(fun connect2/1, ["hello", "how", "are", "you"]).

connect2(Text)->
    {ok,S} = gen_tcp:connect({127,0,0,1},2222,[{packet,2}]),
    gen_tcp:send(S,term_to_binary(Text)).

上述的例子總共會執行4次connect2,代入的connection2參數依次為"hello", "how", "are", "you"

小結

在java裡用到遞迴的機會不多,或許可以用其它的寫法躲過遞迴,但踏入erlang後遞迴是閃不掉的,還是得花時間適應。

2014年9月15日

在工作團隊中,我們都該期待自己可以成為做事快、狠、準的空條承太郎嗎?

荒木飛呂彥的作品「JoJo的奇妙冒險」中,第三部JoJo 星塵鬥士 在最近這幾個月的時間裡,動畫版本持續的播映當中,這部作品受人矚目的原因,當然是主角「空條承太郎」,以及「替身使者」的出現。也因為JoJo這第三部的作品,奠定了JoJo奇妙冒險在歷史上的地位。

先看一下OP片頭動畫,看到承太郎快速的白金之星,反差的慢動作鏡頭時,就會開始熱血起來。

這也讓人想起,Matrix 的 bullet time 經典畫面,用高速攝影的方式,讓大家看清楚動作的細節。

星塵鬥士的團隊成員是由五個人組成

  1. 紫色隱者:喬瑟夫.喬斯達
  2. 白金之星:空條承太郎
  3. 綠色法皇:花京院典明
  4. 紅色魔術師:穆罕默德.阿布德爾
  5. 銀色戰車:J.P .波魯那雷夫

這五個人個性、能力都不同,在團隊中,自然就得扮演著不同的角色,負責處理不同的事務,如果讓星塵鬥士的團隊成員,進到一個軟體公司,接手技術開發的工作,這個團隊在遇到不同的專案開發事務時,也得要選擇最合適的人,去處理最適合的工作,這樣才能達到適才適所,降低搞砸事情的機率。

對於一個技術人員來說,從進入開發團隊一開始,可能就一直在想,我該好好地加強自己的技術能力,成為最強的替身使者白金之星,畢竟技術就是一切,但實際上,白金之星只有一個,這並不表示其他人就是沒有用處的,因為不管是紫色隱者、綠色法皇、紅色魔術師、銀色戰車都是團隊的必要成員,每一個人都有最適合要扮演的角色。

產品前期銷售要給誰處理?

喬瑟夫.喬斯達是團隊中年紀最大,替身功能最弱的成員,他是承太郎的爺爺、荷莉·喬斯達的父親,手裡掌握著史比特·瓦根財團的資源。

在遇到需要與其他路人交涉的情況時,通常都是由喬瑟夫.喬斯達負責協調,有時候也會介紹當地的特殊人文習慣,前期銷售需要一位略懂技術的人員,在基本的技術能力輔助下,進行業務活動,作為業務人員,也需要有足夠的社會經驗,喬瑟夫.喬斯達理所當然得要承擔前期銷售的工作。

研發前期雛型實作要給誰處理?

空條承太郎的替身白金之星擁有速度、準確度、破壞力,簡單來說做事情總是快、狠、準,甚至還透過學習,學會了凍結時間,可說是最強的替身使者。

承太郎最適合處理雛型實作的工作,因為他在如此強大的能力條件下,可用各種他所知道的方式,以最快的方式達成結果,完成雛型,做出一個可以展示的成品,在這個階段裡,成品不需要太多包裝與實際使用的考量,只要能展示出使用的概念就算是完成這項工作了。

專案測試要給誰處理?

穆罕默德.阿布德爾外表看起來,年紀是次於喬瑟夫.喬斯達的成員,除了個性上是團隊成員中最沉穩的這個優點之外,阿布德爾還有占星的能力,知道每一個替身使者代表的塔羅牌。

測試工作,基本上就跟算命沒什麼兩樣,算命師要先有塔羅占星的基本知識,然後根據牌面,計算(猜)出每個人的個性運勢,如果可以用命盤算一算就知道專案的 bug 會出現在哪裡,對團隊來說,等於幫了一個大忙。

客製專案要給誰處理?

穆罕默德.阿布德爾跟J.P .波魯那雷夫都算是不錯的選擇,阿布德爾的沉穩跟波魯那雷夫的輕佻,是天平的兩端,在決定分派客製專案時,可以根據客戶承辦的特性,決定要給誰處理。

比較正派,中規中矩的就交給阿布德爾,比較喜歡玩樂的,個性活潑的,就交給波魯那雷夫,客製專案基本上就只有一個目的,就是滿足客戶的專案需求,盡一切的可能,讓專案順利結案,結案的方式,會因為承辦的不同,而有不同的要求與處置,配合客戶的個性指定不同的人處理,才是最佳的選擇。

創新研發專案要給誰處理?

花京院典明在團隊中屬於智囊團的角色,花京院是最能運用思考的方式,得到處理方案的角色。團隊曾經在沙漠中遭遇到一個小嬰兒(死神13)的追擊,團隊中有些人甚至在事件解決後,還完全不曉得小嬰兒就是替身使者,由此可見花京院是最能靠腦力搭配自己替身能力解決問題的成員。

創新研發專案是在許多的未知中,需要多方的嘗試,才能完成的專案,負責的人得盡一切的努力,找到技術門檻的解決方式,這也不是隨便什麼人都能夠解決的事情,指定了錯誤的人負責這種專案,很有可能花了時間跟金錢,還是沒辦法得到什麼成果。

結語

星塵鬥士的五人團隊是個防禦型團隊,一邊解決來犯的敵人,一邊前進,其實本質跟躲藏在公司深處的研發團隊有些不一樣。這篇文章的用意,只是要提醒大家,基於同一個目的(打倒 DIO)而形成一個團隊,團隊成員為獨立個體,每一個人都有自己的個性、特殊專長(替身),當團隊面對不同任務(替身使者)時,必須各自發揮自己的能力,才能解決各式各樣不同的問題,如果五個人都是白金之星,團隊或許已經因為內鬨互毆而解散了。

每個人都該了解自己,才能為自己在團隊中定位,確認自己的價值,但或者應該反過來說,老闆必須要了解團隊成員中每一個人,才能為每一個人在團隊中定位,確認每一個人都能適才適所,並對團隊產生價值,五隻手指頭握起來,才是一個拳頭。

最後看一下片尾曲,這是英文老歌 Walk Like An Egyptian

2014年9月10日

erlang Mnesia replication測試

本篇簡介紹如何使用Mnesia的RAM replicates功能。使用erlang啟動數個node並將之相連接,這些node可以在單台機器上執行,也可以是在不同機器上執行。假設對某一個node裡的mnesia插入資料時,其它的node裡的mnesia也會一併插入此資料。這樣一來如果某一個node毀掉後其它node還能保留著完整資料以達fault-tolerant。

本篇示範在同一台機器上執行2個node,步驟如下

1.建立目錄給mnesia儲存檔案。在disk裡先建立2個資料夾分別給node1, node2使用。例如 /Users/james/Develop/erlang-test-node/node1 /Users/james/Develop/erlang-test-node/node2

2.啟動node1(注意啟動erlang時的路徑)

cd /Users/james/Develop/erlang-test-node/node1
erl -sname node1

Erlang R16B02 (erts-5.10.3) [source] [64-bit] [smp:4:4] [async-threads:10] [kernel-poll:false]

Eshell V5.10.3  (abort with ^G)
(node1@jamestekiMacBook-Pro)1>

3.啟動node2(注意啟動erlang時的路徑)

cd /Users/james/Develop/erlang-test-node/node2
erl -sname node2

Erlang R16B02 (erts-5.10.3) [source] [64-bit] [smp:4:4] [async-threads:10] [kernel-poll:false]

Eshell V5.10.3  (abort with ^G)
(node2@jamestekiMacBook-Pro)1>

4.將node1, node2相連

(node1@jamestekiMacBook-Pro)2> net_adm:ping('node2@jamestekiMacBook-Pro'). (1)
pong
(node1@jamestekiMacBook-Pro)3> nodes(). (2)
['node2@jamestekiMacBook-Pro']

(1)node1執行net_adm:ping/1指令來ping node2。收到回傳pong就代表連上了

(2)列出與自已相連的node,可以從回傳值看到已連接'node2@jamestekiMacBook-Pro'

到node2查看是否已與node1相連

(node2@jamestekiMacBook-Pro)1> nodes().
['node1@jamestekiMacBook-Pro']

5.準備測試用程式碼。將test_mnesia.erl分別copy至node1, node2所執行的當前目錄,即/Users/james/Develop/erlang-test-node/node1、/Users/james/Develop/erlang-test-node/node2

程式碼的重點會在於create_schema跟create_table指令的參數要指定nodes

例如 do_this_once()裡的 mnesia:create_schema([node(),'node2@jamestekiMacBook-Pro']), 其中node()指的是當前的node,'node2@jamestekiMacBook-Pro'指的是node2

本例子的do_this_once()只要在node1執行一次即可,node2不需執行。

test_mnesia.erl

-module(test_mnesia).
-import(lists, [foreach/2]).
-compile(export_all).

-include_lib("stdlib/include/qlc.hrl").

-record(shop, {item, quantity, cost}).
-record(cost, {name, price}).
-record(design, {id, plan}).

do_this_once() ->
    mnesia:create_schema([node(),'node2@jamestekiMacBook-Pro']),
    mnesia:start(),
    mnesia:create_table(shop,   [{ram_copies, [node(), 'node2@jamestekiMacBook-Pro']},{attributes, record_info(fields, shop)}]),
    mnesia:create_table(cost,   [{ram_copies, [node(), 'node2@jamestekiMacBook-Pro']},{attributes, record_info(fields, cost)}]),
    mnesia:create_table(design, [{ram_copies, [node(), 'node2@jamestekiMacBook-Pro']},{attributes, record_info(fields, design)}]),
    mnesia:stop().

start() ->
    mnesia:start(),
    mnesia:wait_for_tables([shop,cost,design], 20000).

reset_tables() ->
    mnesia:clear_table(shop),
    mnesia:clear_table(cost),
    F = fun() ->
        foreach(fun mnesia:write/1, example_tables())
    end,
    mnesia:transaction(F).

example_tables() ->
    [%% The shop table
     {shop, apple,   20,   2.3},
     {shop, orange,  100,  3.8},
     {shop, pear,    200,  3.6},
     {shop, banana,  420,  4.5},
     {shop, potato,  2456, 1.2},
     %% The cost table
     {cost, apple,   1.5},
     {cost, orange,  2.4},
     {cost, pear,    2.2},
     {cost, banana,  1.5},
     {cost, potato,  0.6}
    ].

demo(select_shop) ->
    do(qlc:q([X || X <- mnesia:table(shop)]));

add_shop_item(Name, Quantity, Cost) ->
    Row = #shop{item=Name, quantity=Quantity, cost=Cost},
    F = fun() ->
        mnesia:write(Row)
    end,
    mnesia:transaction(F).

do(Q) ->
    F = fun() -> qlc:e(Q) end,
    {atomic, Val} = mnesia:transaction(F),
    Val.

6.編譯並執行

在node1執行資料庫初始化與replicates

(node1@jamestekiMacBook-Pro)3> c('test_mnesia').  (1)
{ok,test_mnesia}
(node1@jamestekiMacBook-Pro)4> test_mnesia:do_this_once(). (2)
stopped
(node1@jamestekiMacBook-Pro)5>
=INFO REPORT==== 10-Sep-2014::15:37:39 ===
    application: mnesia
    exited: stopped
    type: temporary

(node1@jamestekiMacBook-Pro)5>test_mnesia:start(). (3)
ok
(node1@jamestekiMacBook-Pro)6> test_mnesia:reset_tables(). (4)
{atomic,ok}
(node1@jamestekiMacBook-Pro)7> test_mnesia:demo(select_shop). (5)
[{shop,potato,2456,1.2},
 {shop,apple,20,2.3},
 {shop,orange,100,3.8},
 {shop,pear,200,3.6},
 {shop,banana,420,4.5}]

(1)編譯test_mnesia

(2)此function會先初始化db,接著建立3個table,當執行完後可以發現2個node裡的當前執行路徑下已自動建立下列資料夾Mnesia.node1@jamestekiMacBook-Pro or Mnesia.node2@jamestekiMacBook-Pro

(3)start mnesia(因為do_this_once()裡最後有再將mnesia做stop)

(4)清除table資料,並插入一些測試資料

(5)將shop table的資料印出,可以看出shop table已有一些資料

7.至node2查詢shop table是否也有跟node1相同的資料

(node2@jamestekiMacBook-Pro)2> c('test_mnesia'). (1)
{ok,test_mnesia}
(node2@jamestekiMacBook-Pro)3> test_mnesia:start(). (2)
ok
(node2@jamestekiMacBook-Pro)4> test_mnesia:demo(select_shop). (3)
[{shop,potato,2456,1.2},
 {shop,apple,20,2.3},
 {shop,orange,100,3.8},
 {shop,pear,200,3.6},
 {shop,banana,420,4.5}]

(1)在node2編譯test_mnesia

(2)start mnesia(註:不需要再初始化db了,因為在node1已經執行了會將table、資料同步過來)

(3)查詢table資料, 由結果可知資料已經有replicates

8.將node1關掉(直接把console關掉),並在node2插入資料

(node2@jamestekiMacBook-Pro)5> test_mnesia:add_shop_item(iphone6, 10, 20). (1)
{atomic,ok}
(node2@jamestekiMacBook-Pro)6> test_mnesia:demo(select_shop). (2)
[{shop,iphone6,10,20},    
 {shop,potato,2456,1.2},
 {shop,apple,20,2.3},
 {shop,orange,100,3.8},
 {shop,pear,200,3.6},
 {shop,banana,420,4.5}]

(1)執行插入一筆記錄 ex:iphone6

(2)查詢看是否有插入成功

9.將node1重新開啟並做連結,最後查看mnesia是否有做replicates

jamestekiMacBook-Pro:~ james$ cd /Users/james/Develop/erlang-test-node/node1
jamestekiMacBook-Pro:node1 james$ erl -sname node1Erlang R16B02 (erts-5.10.3) [source] [64-bit] [smp:4:4] [async-threads:10] [kernel-poll:false]

Eshell V5.10.3  (abort with ^G)
(node1@jamestekiMacBook-Pro)1> net_adm:ping('node2@jamestekiMacBook-Pro'). (1)
pong
(node1@jamestekiMacBook-Pro)2> nodes(). (2)
['node2@jamestekiMacBook-Pro']
(node1@jamestekiMacBook-Pro)3> test_mnesia:start(). (3)
ok
(node1@jamestekiMacBook-Pro)4> test_mnesia:demo(select_shop). (4)
[{shop,iphone6,10,20},
 {shop,potato,2456,1.2},
 {shop,apple,20,2.3},
 {shop,orange,100,3.8},
 {shop,pear,200,3.6},
 {shop,banana,420,4.5}]

(1) node1啟動後跟node2做連結

(2) 確認是否已經連上node2

(3) start mnesia

(4) 確認資料是否已經replicates

小結

本篇示範在同一台機器上啟動2個node來做replicates,如果要在不同機器上要做replicates的話需要再設定cookie才能work,有興趣的人請再參考相關資料。

參考資料

Programming Erlang, Second Edition.

2014年9月9日

初探trackingjs(一) - 臉部辨識

trackingjs是用JavaScript實作的計算機視覺(computer vision)函式庫。 trackingjs可以即時對瀏覽器中的圖片進行臉部辨識、顏色偵測...等處理。

本文直接以一個簡單的臉部辨識功能為範例。
首先至trackingjs的官方網站下載trackingjs,並解壓縮。
進行臉部辨識,需要使用以下幾個js檔案:
/build/tracking-min.js
/build/data/face-min.js
/build/data/eye-min.js
/build/data/mouth-min.js
建立tracker物件,並定義需要偵測的物件為:'face', 'eye', 'mouth'
var tracker = new tracking.ObjectTracker(['face', 'eye', 'mouth']);
tracker.setStepSize(1.7);
接著,註冊一個callback函式,用以在偵測到物件時呼叫:
tracker.on('track', function(event) {
    event.data.forEach(function(rect) {
      //以下為偵測到的物件的位置及大小
      //console.log(rect.x, rect.y, rect.width, rect.height);
    });
});
然後,再輸入要trace的圖片物件。
有兩種方法可以取得圖片:
1.直接取得HTML DOM元件上的圖片
var img = document.getElementById('img');
2.由JavaScript建立圖片物件,並給定src以載入圖片
var img2 = new Image();
img2.src = "assets/faces.jpg"; 
最後,可以開始trace圖片了:
tracking.track(img, tracker);
然而,此處要特別注意的是,在執行tracking.track之前,請務必確認圖片已經載入(尤其是在JavaScript執行時才建立的圖片物件)。否則tracking會告訴你圖片寬度為0,無法偵測。
因此,可以善用img物件的onload函式,確保圖片已經載入,再執行track。
img2.onload = function() {
    tracking.track(img2, tracker);
}
以下是一個完整範例:
<!doctype html>
<html>
<head>
  <meta charset="utf-8">
  <title>tracking.js - face hello world</title>
  <script src="../build/tracking-min.js"></script>
  <script src="../build/data/face-min.js"></script>
  <script src="../build/data/eye-min.js"></script>
  <script src="../build/data/mouth-min.js"></script>
  <style>
  .rect {
    border: 2px solid red;
    left: -1000px;
    position: absolute;
    top: -1000px;
  }
  #panel {
    width:954px;height:407px;background-image:url('assets/faces.png');
  }
  </style>
</head>
<body>
<div id="panel" class="demo-container"></div>
<script>
    window.onload = function() {    
      var tracker = new tracking.ObjectTracker(['face', 'eye', 'mouth']);
      tracker.setStepSize(1.7);

      var img = new Image();
      img.src = "assets/faces.png";  
      img.onload = function() {
          tracking.track(img, tracker);
      };      

      tracker.on('track', function(event) {
        console.log(event);
        event.data.forEach(function(rect) {
          window.plot(rect.x, rect.y, rect.width, rect.height);
        });
      });

      var panel = document.getElementById('panel')
      window.plot = function(x, y, w, h) {
        var rect = document.createElement('div');
        document.querySelector('#panel').appendChild(rect);
        rect.classList.add('rect');
        rect.style.width = w + 'px';
        rect.style.height = h + 'px';
        rect.style.left = (panel.offsetLeft + x) + 'px';
        rect.style.top = (panel.offsetTop + y) + 'px';
      };
    };
  </script>
</body>
</html>
原圖:



 執行結果:

opensips - connect to gateway

如果整個 SIP 環境要跟傳統電話交換機連接,必須要有 voice gateway 處理這個問題,而 opensips 要做的,就是提供 SIP 界面跟其他 voice gateway 整合。

我們設想一個情境,有兩個 edge 端分別用 asterisk 當作 voice gateway,這兩個端點之間,可以直接互相設定 sip trunk。但最好的方式,是在中間增加一個 opensips,充當 SIP Proxy Server 的功能,只針對受話號碼的 prefix 形式來判斷,要將電話話務轉送給那一個 gateway 處理。

在中間的 SIP Proxy Server 單純地只處理 SIP 的封包, 最重要的就是 INVITE,在轉送話務後,讓兩個端點直接對送 RTP 資料。

osipsconfig

先前我們使用 opensips 承擔了 registration server 的工作,但在這個情境中,並不需要註冊的功能,所以我們先使用 osipsconfig 產生新的設定檔。

我們選擇了 Trunking Script,並只勾選 USE_DIALPLAN, USE_DIALOG 這兩個項目。

為了要對 script 除錯,我們可以使用 xlog() 這個函數,當 opensips 收到 SIP 封包,就會執行 routing script,並將 xlog 的資訊列印到 log file 裡面。

xlog 的第一個參數可有有無,可填上以下的 log level,將來就能直接在 opensips.cfg 的 debug=3 裡面決定 log 的資訊等級。

L_ALERT - log level -3
L_CRIT - log level -2
L_ERR - log level -1
L_WARN - log level 1
L_NOTICE - log level 2
L_INFO - log level 3
L_DBG - log level 4

獨立的 log file

opensips使用syslog服務,預設安裝的情況下,log內容會寫入 /var/log/message 這個文件,如果希望使用獨立的log文件,可用以下的指令設定。

touch /var/log/opensips.log
vi /etc/rsyslog.conf –> 增加一行:local0.* /var/log/opensips.log
/etc/init.d/rsyslog restart

針對 opensips.log 檔案,我們再用 logrotate 避免 log file 太大。

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

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 負責。

osipsconfig - Trunking Script

我們只單純需要 opensips 扮演 SIP Proxy 的角色,因此就先在 osipsconfig 的 Trunking Script,只勾選 USE_DIALPLAN, USE_DIALOG 兩個選項,然後就 Generate Trunking Script 產生設定檔,檔名的形式為 opensips_trunking_2014-5-26_10:6:51.cfg,中間的日期及時分秒就是產生檔案的時間。

把該設定檔替換為 opensips.cfg,然後就能再進行下一步的設定修改,設定檔分為以下三個部份。

global configuration

要修改 listen=udp:192.168.1.24:5060,並增加一行 db_default_url="mysql://opensips:opensipsrw@localhost/opensips" 。

####### 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"

module configuration

修改 module path
mpath="/usr/lib64/opensips/modules/"

然後注意 db_url 的密碼的地方,根據自己的設定修改密碼
modparam("drouting", "db_url",
"mysql://opensips:opensipsrw@localhost/opensips") # CUSTOMIZE ME

####### 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

####  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 configuration

把 if ( check_source_address("1","$avp(trunk_attrs)") ) { ... } 這個部份加上 # 註解掉。

把 if ( !isflagset(IS_TRUNK) ) { ... } 這個部份加上 # 註解掉。

在 record_routing 的前面,增加一段 URI 的檢查

    # 增加 URI 的檢查
    if ( uri == myself ) {
        if(is_method("INVITE") && !has_totag() && uri=~"sip:.*") {
            route(home);
        }
    }

增加subroute: route[home],就是以 prefix 號碼來決定要不要 rewritehostport,如果機器的 port 不是預設的 5060,而是 5070,就改成 192.168.1.17:5070。

# 增加 route[home]
route[home] {
    if (uri=~"^sip:2[0-9]{3}@") {
        # uri 開頭為 2,共 4 碼
        rewritehostport("192.168.1.17");
    } else if(uri=~"^sip:[1][0-9].*") {
        # uri 開頭為 1
        rewritehostport("192.168.1.5");
    }
    route(RELAY);
}

完整的 script 內容如下

####### 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

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

    # CANCEL processing
    if (is_method("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;
    }

    # 增加 URI 的檢查
    if ( uri == myself ) {
        if(is_method("INVITE") && !has_totag() && uri=~"sip:.*") {
            route(home);
        }
    }

    # 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("1") ) {
        send_reply("404","No Route found");
        exit;
    }

    t_on_failure("GW_FAILOVER");

    route(RELAY);
}


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

# 增加 route[home]
route[home] {
    if (uri=~"^sip:2[0-9]{3}@") {
        # uri 開頭為 2,共 3 碼
        rewritehostport("192.168.1.17");
    } else if(uri=~"^sip:[1][0-9].*") {
        # uri 開頭為 1
        rewritehostport("192.168.1.5");
    }
    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.cfg,並直接將 routing 的條件寫在設定檔中,接下來應該要嘗試使用 dynamic routing module。

2014年9月1日

opensips 簡介 2/2

接續上一篇,這裡要看 opensips.cfg 裡面的 routing 區塊該怎麼寫。如何處理 routing 是 opensips 裡面最難懂的部份。

Routing Basics

Routing requests and replies

opsnsips script 裡面處理的 routing 機制,通常是用來處理 inter-domain calls,我們可用 DNS server 來尋找 destination address,而 intra-domain calls 則是使用 user location table 來處理 routing。replies 是利用 request 裡面的 VIA header。對 statefule routing 來說,transaction 是使用 VIA 裡面的 branch 參數來做對應。

範例:sip proxy server 為 192.168.1.201:5060,Peer 1000 為 192.168.1.159:39132,Peer 1001 為 192.168.1.159:5060。

在 VIA header 裡面就有足夠的資訊,可以將 reply 送回去,另一個重要的參數是 branch,這是用來在 stateful mode識別 transaction 的資訊,received 與 rport 用在 RFC 3581 處理 NAT traversal。

  1. Peer 1000 -> INVITE -> Proxy
    From 192.168.1.159:39132 -> 192.168.1.201:5060
     INVITE sip:1001@192.168.1.201 SIP/2.0.
     Via: SIP/2.0/UDP 192.168.1.159:39132;branch=z9hG4bK-d87543-f467f33a206c333a-1--d87543-;rport.
     ...
  2. Proxy -> INVITE -> Peer 1001
    From 192.168.1.201:5060 -> 192.168.1.159:5060
     INVITE sip:1001@192.168.1.159:5060;rinstance=a1d5fa7ecfde6278;transport=UDP SIP/2.0.
     Via: SIP/2.0/UDP 192.168.1.201;branch=z9hG4bKf5b7.34401122.0.
     Via: SIP/2.0/UDP 192.168.1.159:39132;received=192.168.1.159;branch=z9hG4bK-d87543-f467f33a206c333a-1--d87543-;rport=39132.
     ...
  3. Peer 1001 -> 200 OK -> Proxy
    From 192.168.1.159:5060 -> 192.168.1.201:5060
     SIP/2.0 200 OK.
     Via: SIP/2.0/UDP 192.168.1.201;branch=z9hG4bKf5b7.34401122.0.
     Via: SIP/2.0/UDP 192.168.1.159:39132;received=192.168.1.159;branch=z9hG4bK-d87543-f467f33a206c333a-1--d87543-;rport=39132.
     ...
  4. Proxy -> 200 OK -> Peer 1000
    From 192.168.1.201:5060 -> 192.168.1.159:39132
     SIP/2.0 200 OK.
     Via: SIP/2.0/UDP 192.168.1.159:39132;received=192.168.1.159;branch=z9hG4bK-d87543-f467f33a206c333a-1--d87543-;rport=39132.
     ...
Initial and sequential requests

要區分 initial 與 sequential requests 的差異,這兩種 request 的 routing logic 不同。

initial requests: routed based on discovery mechanism,通常是 location table 或 DNS,initial request 會紀錄相關的 SIP proxy hops。

initial request 的 TO header 裡面不會有 TAG parameter。

根據 caller, callee 的不同,使用的 routing mechanism 可能是下列幾項中的一項:enum, aliases, dns, user location 或其他方式。

  1. Peer 1000 -> INVITE -> Proxy
    From 192.168.1.159:39132 -> 192.168.1.201:5060
     INVITE sip:1001@192.168.1.201 SIP/2.0.
     Contact: <sip:1000@192.168.1.159:39132>.
     ...
  2. Proxy -> INVITE -> Peer 1001
    From 192.168.1.201:5060 -> 192.168.1.159:5060
     INVITE sip:1001@192.168.1.159:5060;rinstance=a1d5fa7ecfde6278;transport=UDP SIP/2.0.
     Record-Route: <sip:192.168.1.201;lr>.
     Contact: <sip:1000@192.168.1.159:39132>.
     ...
  3. Peer 1001 -> 200 OK -> Proxy
    From 192.168.1.159:5060 -> 192.168.1.201:5060
     SIP/2.0 200 OK.
     Record-Route: <sip:192.168.1.201;lr>.
     Contact: <sip:1001@192.168.1.159:5060;rinstance=a1d5fa7ecfde6278;transport=UDP>.
  4. Proxy -> 200 OK -> Peer 1001
    From 192.168.1.201:5060 -> 192.168.1.159:39132
     SIP/2.0 200 OK.
     Record-Route: <sip:192.168.1.201;lr>.
     Contact: <sip:1001@192.168.1.159:5060;rinstance=a1d5fa7ecfde6278;transport=UDP;nat=yes>.

sequential requests: routed based on initial requests 上的資訊,收集到的 routes 資訊稱為 route set,在 script 中,可使用 loose_route() 函數來利用 route set 處理 routing。

可利用 TO header 裡面的 TAG 參數來區分 initial 與 sequential requests。

sequential requests 是利用 Route header 與 URI 來做 routing,換句話說,就是 UAC 收到由 Record-Route 與 Contact headers 產生的 route set。

當 client 發現了 route set,就會 mirror Contact header 的 request URI 還有 Route header 的 Record-Route。對Proxy server 來說,使用 loose_route function 重新利用 location table 或 DNS 尋找 destination 這樣處理速度會比較快。

  1. Peer 1000 -> ACK -> Proxy
    From 192.168.1.159:39132 -> 192.168.1.201:5060
     ACK sip:1001@192.168.1.159:5060;rinstance=a1d5fa7ecfde6278;transport=UDP;nat=yes SIP/2.0.
     Route: <sip:192.168.1.201;lr>.
     ...
  2. Proxy -> ACK -> Peer 1001
    From 192.168.1.201:5060 -> 192.168.1.159:5060
     ACK sip:1001@192.168.1.159:5060;rinstance=a1d5fa7ecfde6278;transport=UDP;nat=yes SIP/2.0.
Sample route script

整個 routing script 是包括在 route{ } 區塊裡面。

第一個部份是檢查是不是超過 10 個 SIP Proxy hops。mf_process_maxfwd_header這個 function 是由 maxfwd.so 這個 module 提供(script 的 modules 區塊必須要 loadmodule "maxfwd.so"),可避免 SIP message loops。

sl_send_reply 是由 stateless (sl.so) 提供,用來傳送 stateless request 給 SIP client,這表示 opensips 並不會等待 message ack。

exit 是告訴 opensips 結束 request processing。

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

第二個部份是處理 TO header。這表示這是一個 sequential request,通常會用 loose_route 提供 routing,如果遇到 BYE, CANCEL 就會 forward 此訊息。

有 To 但是沒有 ;lr 將會被認為是 error message 而被丟棄。

如果遇到 ACK 不符合任何一個 transaction,也會直接被丟棄。

    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;
    }

第三部份是處理 CANCEL,我們不需要自己處理 routing,因為 CANCEL 是屬於某個 INVITE transaction,所以就只要 t_check_trans(),然後直接 t_relay()。

接下來獨立的 t_check_trans() 是為了要檢查是否屬於某個 INVITE transaction,這個檢查可以在 request restransmission 時,停止繼續執行 script。

    # CANCEL processing
    if (is_method("CANCEL"))
    {
        if (t_check_trans())
            t_relay();
        exit;
    }

    t_check_trans();

這個部份是處理 non-register requests。

    if ( !(is_method("REGISTER")  ) ) {

        if (from_uri==myself)

        {

            # authenticate if from local subscriber
            # authenticate all initial non-REGISTER request that pretend to be
            # generated by local subscriber (domain from FROM URI is local)
            if (!proxy_authorize("", "subscriber")) {
                proxy_challenge("", "0");
                exit;
            }
            if (!db_check_from()) {
                sl_send_reply("403","Forbidden auth ID");
                exit;
            }

            consume_credentials();
            # caller authenticated

        } else {
            # if caller is not local, then called number must be local

            if (!uri==myself) {
                send_reply("403","Rely forbidden");
                exit;
            }
        }

    }

這個部份是處理沒有 TO header 但卻有 Route 的 request,如果發現這種封包,除了 ACK 之外,就直接丟棄。

    # 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;
    }

如果 request 目標 server 不是自己,就用 record_route() 紀錄 routes。

    # record routing
    if (!is_method("REGISTER|MESSAGE"))
        record_route();

把 INVITE request 貼上 ACC_DO 要處理 accounting 的標籤。

    # account only INVITEs
    if (is_method("INVITE")) {

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

        setflag(ACC_DO); # do accounting
    }

處理不是由自己服務的 domain 的 request,這裡預設是以 open relay 的方式運作。

將 request forward 到其他 proxy 的處理過程,將會在後續章節裡面討論。

    ## replace with following line if multi-domain support is used
    ##if (!is_uri_host_local())
    if (!uri==myself) {
        append_hf("P-hint: outbound\r\n"); 
        # if you have some interdomain connections via TLS
        ##if($rd=="tls_domain1.net") {
        ## t_relay("tls:domain1.net");
        ## exit;
        ##} else if($rd=="tls_domain2.net") {
        ## t_relay("tls:domain2.net");
        ## exit;
        ##}
        route(relay);
    }

可以自己決定要不要提供 presence 功能,改成 route(2) 就能提供 presense agent 的功能。

    ## uncomment this if you want to enable presence server
    ## and comment the next 'if' block
    ## NOTE: uncomment also the definition of route[presence] from below
    ##if( is_method("PUBLISH|SUBSCRIBE"))
    ## route(2);
    if (is_method("PUBLISH|SUBSCRIBE"))
    {
        sl_send_reply("503", "Service Unavailable");
        exit;
    }

如果是 REGISTER request,就可用 www_authorize, db_check_to 進行使用者驗證,驗證通過後,就儲存 AOR 至 location table。

    if (is_method("REGISTER"))
    {

        # authenticate the REGISTER requests
        if (!www_authorize("", "subscriber"))
        {
            www_challenge("", "0");
            exit;
        }

        if (!db_check_to()) 
        {
            sl_send_reply("403","Forbidden auth ID");
            exit;
        }

        if (   0 ) setflag(TCP_PERSISTENT);

        if (!save("location"))
            sl_reply_error();

        exit;
    }

丟棄沒有完整 URI 的 requests。
alias_db_lookup 可查詢 alias,例如 1000@mydomain.com 會跟 boss@mydomain.com 一樣。

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

    # apply DB based aliases (uncomment to enable)
    ##alias_db_lookup("dbaliases");

在 localtion db 尋找 AOR(address of record),參數 m 是條件過濾。不存在時,就回傳 404 Not Found。進一步搜尋 URI,不存在時,就回傳 420 Bad Entension。

    # do lookup with method filtering
    if (!lookup("location","m")) {
        if (!db_does_uri_exist()) {
            send_reply("420","Bad Extension");
            exit;
        }

        t_newtran();
        t_reply("404", "Not Found");
        exit;
    }

到最後,就進行 relay 的 subroute。

    # when routing via usrloc, log the missed calls also
    setflag(ACC_MISSED);
    route(relay);

這個部份是 relay subroute。t_relay 可基於 request URI 將 forward request statefully,這是由 TM (tm.so) module 提供,負責發送 request 與處理 resneds, responses。如果 t_relay 沒有成功送到 destination,就會回傳 error。

route[relay] {
    # for INVITEs enable some additional helper routes
    if (is_method("INVITE")) {
        t_on_branch("per_branch_ops");
        t_on_reply("handle_nat");
        t_on_failure("missed_call");
    }

    if (!t_relay()) {
        send_reply("500","Internal Error");
    };
    exit;
}

這是 presence agent 的範例。

# Presence route
/* uncomment the whole following route for enabling presence
NOTE: do not forget to enable the call of this route from the main
route */
##route[presence]
##{
##    if (!t_newtran())
##    {
##        send_reply("500","Internal Error");
##        exit;
##    };
##
##    if(is_method("PUBLISH"))
##    {
##        handle_publish();
##        t_release();
##    }
##    else
##    if( is_method("SUBSCRIBE"))
##    {
##        handle_subscribe();
##        t_release();
##    }
##
##    exit;
##}

branch_route, onreply_route, failure_route, local_route 最後是這四個 subroutes。

branch_route 是由 TM module 提供,在 route 裡面呼叫 t_on_branch[1],就可以進入 branch_route[1]。

failure_route 是由 TM module 提供,也就是 failed transaction routing block,當 transaction 在任一個 branch 收到 >=300 的 reply 時,就會進入 failure_route。

onreply_route: reply routing block,這會在收到任一個 reply 時執行,他會處理 reply 的訊息,預設會發送 reply 給 caller side。

local_route: 當 TM 內部產生一個新的 SIP request (internal UAC request) 時就會 trigger local_route。

branch_route[per_branch_ops] {
    xlog("new branch at $ru\n");
}

onreply_route[handle_nat] {

    xlog("incoming reply\n");
}


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

    # uncomment the following lines if you want to block client 
    # redirect based on 3xx replies.
    ##if (t_check_status("3[0-9][0-9]")) {
    ##t_reply("404","Not found");
    ##    exit;
    ##}
}

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

        acc_db_request("200 Dialog Timeout", "acc");

    }
}