2014年3月31日

Hibernate ResultTransformer

在開發報表功能時我們常會遇到一張報表所要顯示的資料是來自多個table,例如我們要開發一個訂單查詢的報表,其中資料來源是來自下列2個table

訂單table,欄位如下

orderid(訂單編號), customerid(會員id), orderdate(訂單日期)

會員table,欄位如下

customerid(會員id), name(會員姓名), phone(會員電話), email(會員email)

假設報表要顯示的欄位有orderid(訂單編號), name(會員姓名), phone(會員電話), email(會員email),通常我們會寫下列的程式碼來取得資料

1.先定義Helper Class, 此class並不會對映到一個實體的table, 僅方更讓我們將查詢結果建立成一個一個的物件

public class OrderReport {

    private String orderid;
    private String name;
    private String phone;
    private String email;

    public String getOrderid() {
        return customerid;
    }

    public void setOrderid(String orderid) {
        this.customerid = customerid;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getPhone() {
        return phone;
    }

    public void setPhone(String phone) {
        this.phone = phone;
    }

    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }

}

2.接著寫hibernate code

List<OrderReport> list = new ArrayList<OrderReport>();

String sql = "select o.orderid, c.name, c.phone, c.email from orderform o, customer c where o.customerid = c.customerid";

SQLQuery query = session.createSQLQuery(sql);
List<Object[]> list = query.list();
for (int i = 0; i < list.size(); i++) {
    Object[] m = list.get(i);
    String orderid = m[0].toString();
    String name = m[1].toString();
    String phone = m[2].toString();
    String email = m[3].toString();

    OrderReport report = new OrderReport();
    report.setOrderid(orderid);
    report.setName(name);
    report.setPhone(phone);
    report.setEmail(email);

    list.add(report);   
}

//..other code ...

Hibernate有提供ResultTransformer可以自動的將查詢結果轉成指定的類別讓程式碼更簡短,如下

String sql = "select o.orderid, c.name, c.phone, c.email from orderform o, customer c where o.customerid = c.customerid";

SQLQuery query = session.createSQLQuery(sql);
query.addScalar("orderid", StandardBasicTypes.STRING);
query.addScalar("name", StandardBasicTypes.STRING);
query.addScalar("phone", StandardBasicTypes.STRING);
query.addScalar("email", StandardBasicTypes.STRING);

//Transformers
query.setResultTransformer(Transformers.aliasToBean(OrderReport.class));

List<OrderReport> list = query.list();

//..other code ...

2014年3月27日

在iOS應用程式中使用推播通知(Push Notification)功能 - Provider以Java-apns實作

前言

開發iOS應用程式時,我們常常需要連上自己的平台或伺服器建立連線,取得資料。然而,當應用程式尚未開啟,或是已經被退到背景時,此時就無法讓使用者即時看到自己的伺服器送來的新資料了。
推播通知(Apple Push Notification Service, APNs)是當iOS應用程式不在前景時,使自己的平台或伺服器可以發送通知給iOS設備的一項技術。

以下是發送一則推播通知的流程示意圖:
Provider是iOS應用程式開發者自己所擁有的平台或伺服器,APNs Server則是Apple官方的推播通知服務伺服器。 Provider將推播通知的相關資訊依照規定的格式送給APNs Server,APNs Server即可依此資訊將通知傳送給指定的iOS設備。
因此,Provider需要準備以下幾項資料:
  1. APNs SSL Certificate(APNs SSL憑證檔):由於Provider發送推播通知訊息給APNs Server處理時需要進行SSL連線,進行此連線需要準備憑證檔。
  2. Device Token:識別iOS設備的唯一ID,表示需要送給哪一台iOS設備。

Provider事前準備

在App ID設定中啟用推播通知,並建立APNs SSL憑證檔

當某個iOS應用程式需要使用推播通知時,需要先在其App ID設定中啟用此功能。
首先,登入iOS Dev Center, 點選App IDs, 展開該iOS應用程式,進行編輯。
點選編輯後,找到Push Notification項目,點選建立憑證檔(Create Certificate)。
APNs SSL憑證檔分為兩種:開發階段用的Development SSL Certificate與上架之後用的Production SSL Certificate。試此時你的情況與需求使用。
建立APNs SSL憑證檔,需要上傳CSR檔案(CertificateSigningRequest.certSigningRequest)。
完成後,點選下載即可。
下載憑證檔號,點擊兩下憑證檔,開啟鑰匙圈存取(keychain acess),右鍵點選憑證檔,選擇輸出



至此,Provider需要的APNs SSL憑證檔已完成。
注意:編輯完App ID後,該App的Profitioning file需要重新產生。

取得Device Token

取得Device Token需要透過iOS設備來進行。
iOS設備使用UIApplication的registerForRemoteNotificationTypes方法, 來註冊使用推播通知, 並取得Device Token,範例如下:
        
    //注意registerForRemoteNotificationType方法在iOS 8之後已經被宣告為deprecated了
    //因此需要針對不同版本的iOS來執行不同的程式

    UIApplication* application = [UIApplication sharedApplication];
    //若iOS為較新的版本(支援registerUserNotificationSettings方法)
    if([application respondsToSelector:@selector(registerUserNotificationSettings:)]){
        UIUserNotificationType userNotificationTypes = (UIUserNotificationTypeAlert | UIUserNotificationTypeBadge | UIUserNotificationTypeSound);
        UIUserNotificationSettings *settings = [UIUserNotificationSettings settingsForTypes:userNotificationTypes categories:nil];
        [application registerUserNotificationSettings:settings];
        [application registerForRemoteNotifications];
    }
    //若iOS為較舊的版本
    else {
        [[UIApplication sharedApplication] registerForRemoteNotificationTypes:(UIRemoteNotificationTypeAlert| UIRemoteNotificationTypeBadge |  UIRemoteNotificationTypeSound)];
    }
此方法接收一個UIRemoteNotificationType參數,用以設定是否接收Badge, 使用提示聲音。
而在AppDelegate類別(專案中實作UIApplicationDelegate的類別)中,加入下列兩個方法,用以處理註冊成功或失敗
- (void)application:(UIApplication *)app  didRegisterForRemoteNotificationsWithDeviceToken: (NSData *)deviceToken
{
    //將Device Token由NSData轉換為字串     
    const unsigned *tokenBytes = [deviceToken bytes];
    NSString *iOSDeviceToken = 
    [NSString stringWithFormat:@"%08x%08x%08x%08x%08x%08x%08x%08x", 
        ntohl(tokenBytes[0]), ntohl(tokenBytes[1]), ntohl(tokenBytes[2]),
        ntohl(tokenBytes[3]), ntohl(tokenBytes[4]), ntohl(tokenBytes[5]),
        ntohl(tokenBytes[6]), ntohl(tokenBytes[7])];

    //將Device Token傳給Provider...
}

- (void)application:(UIApplication *)app didFailToRegisterForRemoteNotificationsWithError: (NSError *)err {
    //錯誤處理...
}
iOS設備取得Device Token後,iOS應用程式開發者再以任何方式(如:Web Service)將Device Token傳至Provider即可。

由Provider發送推播通知的相關資訊給APNs Server

先前提及,Provider需要將推播通知的相關資訊依照規定的格式送給APNs Server,此相關資訊稱為Notification Payload,其格式為JSON。範例如下:
{
    "aps": {
        "alert": "Alert Message",
        "badge": 3,
        "sound": "alarm"
    },
    "custom_key": "custom_value"
}
若使用開發中的憑證檔(Development SSL Certificate),則Payload要發給測試用的APSs Server,位址是:gateway.sandbox.push.apple.com,port為2195。
若使用上架用的憑證檔(production SSL Certificate),則Payload要發給正式用的APSs Server,位址是:gateway.push.apple.com,port為2195。
而如今,網路上也已經有眾多第三方程式實作Provider的範例可供參考。本文將以Java-apns為例。

使用Java-apns套件實作Provider

java-apns是以Java寫成的Provider套件,只要是可以安裝Java Virtual Machine的環境下都可以執行。
建立ApnsService物件,此時需要提供p12檔案的路徑,以及先前儲存p12檔案時,所使用的密碼。
然後,如果使用Development SSL憑證檔,請使用withSandboxDestination方法; 如果使用Production SSL憑證檔,請使用或是withProductionDestination方法。 如下範例:
String certPath = "<p12 File Path>";
String pwd = "<p12 File password>";
ApnsService service = APNS.newService()
    .withCert(certPath, KokolaConstants.getApnsPwd())
    .withSandboxDestination()   // 或是withProductionDestination(), for 上架用
    .build();
發送一個簡單的通知:
String payload = APNS.newPayload()
    .alertBody("Hello")
    .badge(badge).sound ("alert.wav").build();
service.push(id, payload);
更詳細的內容可以參考Notification Payload,Java-apns幾乎都有實作對應的Method可以使用。

結語

推播通知是個在行動裝置上廣為使用的基本功能,尤其在背景執行程式限制重重的iOS環境下,善用推播通知更是重要。 在iOS下實作推播通知,只要對APNs Server發送正確格式的資料(Notification Payload)即可,因此網路上不難找到各種程式語言/平台實作的Provider,本篇文章僅以Java作為範例,供大家參考。

Reference:

2014年3月26日

IOS 將 console 寫成 file


在 app 測試時,常會出現一些無法重現的問題。這時工程師會習慣查看 console,但總不能把手機一直接著電腦查看 console吧!若等到問題發生在馬上接電腦查看,也可能因為 log 過多被洗版而失去重要資訊。此時,若能將 console 寫成 file,來協助工程師 debug,對工程師來說將會輕鬆許多,也可讓一些無法重現的 bug 留下線索!

重新定義 NSLog


#define NSLog(FORMAT, ...) NSLog(@"%s %d %@",__FILE__, __LINE__, [NSString stringWithFormat:FORMAT, ##__VA_ARGS__]);NSLog
//印出範例:/ full package name / full class name  行數  訊息

write file


/**
 * 將 NSLog 寫成 file,存在 doc 下 (請用 iTools 開啟)
 */
- (void) redirectConsoleLogToDocumentFolder

{
    NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory,
                                                         NSUserDomainMask, YES);
    NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
    [dateFormatter setDateFormat:@"yyyy-MM-dd"];
    NSDate* date = [[NSDate alloc] init];
    NSString *fileName = [NSString stringWithFormat:@"%@%@", [dateFormatter stringFromDate:date], @".log" ];  //檔名為 yyyy-MM-dd.log
    NSString *documentsDirectory = [paths objectAtIndex:0];
    NSString *logPath = [documentsDirectory stringByAppendingPathComponent:fileName];//@"console.log"
    freopen([logPath fileSystemRepresentation],"a+",stderr);  //印出 NSLog,a代表讀取文字檔案將附加資料在檔案最後
    freopen([logPath fileSystemRepresentation],"a+",stdout);  //印出 print,a代表讀取文字檔案將附加資料在檔案最後
}
在 AppDelegate 的 didFinishLaunchingWithOptions 中呼叫

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions{
 [self redirectConsoleLogToDocumentFolder];  //呼叫寫入 log method
 //... other code ...
 return YES;
}
※ freopen 參數可參考 C 語言標準函數庫分類導覽 - stdio.h freopen()
※ 注意!若您將 console 寫入 file,那在 xcode 中,可能會看不到 console

執行結果

  • 執行前

2014-03-26 19:39:40.698 kokola[1847:60b] data.updateEmo:0
2014-03-26 19:39:40.703 kokola[1847:60b] loadStartupData OK
2014-03-26 19:39:41.026 kokola[1847:60b] responseString={"contact":{"genTime":"2014-03-26 19:39:39","grouplist":[],"userlist":[],"uglist":[]},"resultcode":"200","resultdesc":"Success."}
2014-03-26 19:39:41.050 kokola[1847:60b] 寫入公司通訊錄最後更新時間至UserDefaults:2014-03-26 19:39:39
2014-03-26 19:39:56.743 kokola[1847:60b] [ContactViewController]viewWillDisappear
2014-03-26 19:39:56.778 kokola[1847:60b] -[ChatHistoryViewController viewWillAppear:]
2014-03-26 19:39:58.721 kokola[1847:60b] didSelectRowAtIndexPath
2014-03-26 19:39:58.779 kokola[1847:60b] setSendBtn;
2014-03-26 19:39:59.025 kokola[1847:60b] [MainViewController]viewWillDisappear
2014-03-26 19:39:59.026 kokola[1847:60b] [ChatHistoryViewController]viewWillDisappear
2014-03-26 19:39:59.030 kokola[1847:60b] genChats
2014-03-26 19:39:59.045 kokola[1847:60b] unreadMkeys count:0
2014-03-26 19:39:59.070 kokola[1847:60b] setAllRead:4
  • 執行後

2014-03-26 19:33:49.917 kokola[1830:60b] /Users/james/sourcetree/kokola/kokola/business/impl/StartupBusinessImpl.m 124 data.updateEmo:0
2014-03-26 19:33:49.926 kokola[1830:60b] /Users/james/sourcetree/kokola/kokola/util/LongPollingClient.m 348 loadStartupData OK
2014-03-26 19:33:49.986 kokola[1830:60b] /Users/james/sourcetree/kokola/kokola/util/HttpUtil.m 290 responseString={"contact":{"genTime":"2014-03-26 19:33:49","grouplist":[],"userlist":[],"uglist":[]},"resultcode":"200","resultdesc":"Success."}
2014-03-26 19:33:50.012 kokola[1830:60b] /Users/james/sourcetree/kokola/kokola/business/impl/OrgContactBusinessImpl.m 189 寫入公司通訊錄最後更新時間至UserDefaults:2014-03-26 19:33:49
2014-03-26 19:33:50.173 kokola[1830:60b] Could not load the "btn_topbar1_pressed.png" image referenced from a nib in the bundle with identifier "com.maxkit.kokola"
2014-03-26 19:33:50.251 kokola[1830:60b] /Users/james/sourcetree/kokola/kokola/view/ContactViewController.m 178 [ContactViewController]viewWillAppear
2014-03-26 19:33:50.313 kokola[1830:60b] /Users/james/sourcetree/kokola/kokola/view/LoginViewController.m 410 app.longPollingClient=
2014-03-26 19:33:51.921 kokola[1830:60b] /Users/james/sourcetree/kokola/kokola/view/ContactViewController.m 207 [ContactViewController]viewWillDisappear
2014-03-26 19:33:51.956 kokola[1830:60b] /Users/james/sourcetree/kokola/kokola/view/ChatHistoryViewController.m 86 -[ChatHistoryViewController viewWillAppear:]

References

slove UIImagePickerController use on ios 7 crashes


Received memory warning 問題

  • 在 IOS6 中使用 UIImagePickerController 可正常運行,但在 IOS7 / iPhone 4S 中拍照時,卻出現

2014-03-25 16:45:39.990 CameraTest[3442:60b] Received memory warning.
  • 在拍照時,內存使用會較平常高出許多,所以在較低階的設備 ( pad min,iPad2 ) 執行時,有時甚至會導致 app crash

Slove

在 didReceiveMemoryWarning 中將佔用的資源釋放

- (void)didReceiveMemoryWarning
{
    [self.imagePicker dismissViewControllerAnimated:YES completion:^{}];  //釋放 UIImagePickerController
    [super didReceiveMemoryWarning];
}

Snapshotting a view that has not been rendered results... 問題

  • IOS7 再調用 UIImagePickerController 時,若 device 為橫向則會出現以下訊息

2014-03-25 16:47:48.824 CameraTest[3442:60b] Snapshotting a view that has not been rendered results in an empty snapshot. Ensure your view has been rendered at least once before snapshotting or snapshot after screen updates.

Slove

  • 在 Apple Developer 中,可看到 The UIImagePickerController class supports portrait mode only. 故在橫向時,會出現如上訊息
  • 目前尚未找到有效的解決辦法,因為系統在開啟相機時會自動判斷 device 方向,且 device 方向是唯讀不可修改
  • 經過測試,上述訊息並不會影響 app 的運行,更不會導致 crash

References

2014年3月25日

erlang - distributed programming

分散式程式

為什麼要開發分散式程式

  1. 效能:將程式不同的部份,指派給不同的電腦運算
  2. 可靠度:容錯
  3. 規模:服務更多的使用者
  4. 本質分散的應用:網路遊戲或聊天這些天生就分散的應用

分散式模型

  1. 分散式 erlang:以erlang 的 process 為基礎,寫出來的程式,不同 process 運作的程式,可以將 process 分配到不同機器節點上,程式不需要修改。
  2. 以 socket 為基礎的分散式應用:分散式erlang必須要在信賴的區域網路內運作,在非信賴的公用網路上,必須要以 TCP/IP 網路為基礎,撰寫網路分散式程式

開發的步驟

  1. 在單一erlang 節點中,撰寫並測試程式
  2. 在同一台電腦,兩個 erlang 節點中,測試程式
  3. 在兩台電腦的 erlang 節點中,測試程式
  4. 兩台電腦在不同 domain,不同區域網路上,測試程式

最後一個步驟是很重要的,在遠端網路的環境中運作程式,就需要一併確認防火牆是不是有開放連線。

Sample: key-value cache server

在單一erlang 節點中,撰寫並測試程式

server 的程式界面規格

  1. @spec kvs:start() -> true
    啟動 server,註冊名稱為 kvs
  2. @spec kvs:store(Key, Value) -> true
    儲存 Key - Value
  3. @spec kvs:lookup(Key) -> {ok, Value} | undefined
    尋找 key

因為這只是一個簡單的範例,所以是用 process dictionary 做出來的,使用 process dictionary 要注意以下條件:盡量不要使用 process dictionary,可能會導致一些 bug,難以除錯,只有一個地方可以使用,就是拿來儲存「只寫入一次」的變數,如果一個 key 得到的一個值僅此一次,且不會再變動,那麼就可以放入 process dictionary。

-module(kvs).
-export([start/0, store/2, lookup/1]).

start() -> register(kvs, spawn(fun() -> loop() end)).

store(Key, Value) -> rpc({store, Key, Value}).

lookup(Key) -> rpc({lookup, Key}).

rpc(Q) ->
    kvs ! {self(), Q},
    receive
        {kvs, Reply} ->
            Reply
    end.

loop() ->
    receive
        {From, {store, Key, Value}} ->
            put(Key, {ok, Value}),
            From ! {kvs, true},
            loop();
        {From, {lookup, Key}} ->
            From ! {kvs, get(Key)},
            loop()
    end.

測試

1> kvs:start().
true
2> kvs:store({location, joe}, "Island").
true
3> kvs:store(weather, raining).
true
4> kvs:lookup(weather).
{ok,raining}
5> kvs:lookup({location, joe}).
{ok,"Island"}
6> kvs:lookup({location, mary}).
undefined
7> kvs:store(weather, sunny).
true
8> kvs:lookup(weather).
{ok,sunny}

在同一台電腦,兩個 erlang 節點中,測試程式

以 erl -sname kvs_server 啟動 kvs_server 節點

>erl -sname kvs_server
Eshell V5.10.4  (abort with ^G)
(kvs_server@yaoclNB)1> kvs:start().
true

以 erl -sname kvs_client 啟動 kvs_client 節點,然後用 rpc:call(kvs_server@yaoclNB, kvs, store, [weather, fine]) 進行遠端呼叫。

>erl -sname kvs_client
Eshell V5.10.4  (abort with ^G)
(kvs_client@yaoclNB)1> rpc:call(kvs_server@yaoclNB, kvs, store, [weather, fine]).
true
(kvs_client@yaoclNB)2> rpc:call(kvs_server@yaoclNB, kvs, lookup, [weather]).
{ok,fine}

在 kvs_server 節點,kvs:lookup 確實可以查詢出,遠端呼叫填寫進去的 weather - fine 的資料。

(kvs_server@yaoclNB)2> kvs:lookup(weather).
{ok,fine}

在 LAN 的兩台電腦的 erlang 節點中,測試程式

以 erl -name kvs_server -setcookie abc 啟動 kvs_server 節點,因為要遠端使用,所以是以 -name 要求 erlang 以長的網域名稱建立此 erlang 節點。-setcookie 是 erlang 節點之間的安全檢查,當此 cookie 值不同時,這兩個 erlang 節點就無法互相進行遠端呼叫。

[root@git temp]# erl -name kvs_server -setcookie abc
Erlang R16B03 (erts-5.10.4) [source] [smp:2:2] [async-threads:10] [hipe] [kernel-poll:false]

Eshell V5.10.4  (abort with ^G)
(kvs_server@git.maxkit.com.tw)1>

如果執行上面的指令後產生以下這個錯誤訊息。

{error_logger,{{2014,1,27},{15,3,21}},"Can't set long node name!\nPlease check your configuration\n",[]}

這時候,就要去檢查機器的 hostname,此 hostname 必須要包含網域資料,這樣才能正確以長的網域名稱建立此 erlang 節點。

以 CentOS 來說,必須要修改 /etc/sysconfig/network 檔案,並以 hostname 指令設定。

>vi /etc/sysconfig/network
NETWORKING=yes
HOSTNAME=git.maxkit.com.tw
GATEWAY=192.168.1.1
>hostname git.maxkit.com.tw

完整的測試過程如下

kvs_server 的部份

[root@git temp]# erl -name kvs_server -setcookie abc
Erlang R16B03 (erts-5.10.4) [source] [smp:2:2] [async-threads:10] [hipe] [kernel-poll:false]

Eshell V5.10.4  (abort with ^G)
(kvs_server@git.maxkit.com.tw)1> kvs:start().
true

kvs_client 的部份

[root@koko temp]# erl -name kvs_client -setcookie abc
Erlang R16B03 (erts-5.10.4) [source] [smp:2:2] [async-threads:10] [hipe] [kernel-poll:false]

Eshell V5.10.4  (abort with ^G)
(kvs_client@koko.maxkit.com.tw)1> rpc:call('kvs_server@git.maxkit.com.tw', kvs, store, [weather, fine]).
true
(kvs_client@koko.maxkit.com.tw)2> rpc:call('kvs_server@git.maxkit.com.tw', kvs, lookup, [weather]).
{ok,fine}

-setcookie 是 erlang 節點之間的安全檢查,當此 cookie 值不同時,這兩個 erlang 節點就無法互相進行遠端呼叫。

kvs_server 的 cookie 為 abc

[root@git temp]# erl -name kvs_server -setcookie abc
Erlang R16B03 (erts-5.10.4) [source] [smp:2:2] [async-threads:10] [hipe] [kernel-poll:false]

Eshell V5.10.4  (abort with ^G)
(kvs_server@git.maxkit.com.tw)1> kvs:start().
true
(kvs_server@git.maxkit.com.tw)2>
=ERROR REPORT==== 27-Jan-2014::15:16:20 ===
** Connection attempt from disallowed node 'kvs_client@koko.maxkit.com.tw' **

kvs_client 的 cookie 為 def,在遠端呼叫 rpc:call 時,就會得到 {badrpc,nodedown} 的錯誤訊息,且 kvs_server 會出現 dissallowed 連線的錯誤訊息。

[root@koko temp]# erl -name kvs_client -setcookie def
Erlang R16B03 (erts-5.10.4) [source] [smp:2:2] [async-threads:10] [hipe] [kernel-poll:false]

Eshell V5.10.4  (abort with ^G)
(kvs_client@koko.maxkit.com.tw)1> rpc:call('kvs_server@git.maxkit.com.tw', kvs, store, [weather, fine]).
{badrpc,nodedown}

在兩個遠端遠端機器中,進行 erlang 分散式運算,有下列步驟。

  1. 啟動 erlang 時要使用 -name,如果是單一機器,則使用 -sname 短機器名稱就可以了。
  2. 確認兩個 erlang nodes 有相同的 cookie,使用 -setcookie。erl 預設會讀取 $HOME/.erlang_cookie 檔案,作為預設的 cookie value,因此在單一機器上,運作兩個 nodes,就不需要加上 -setcookie,直接修改 .erlang_cookie 檔案內容,也可以讓兩個 erlang nodes 環境 cookie 一樣。
  3. 確認 fully qualified hostname 可以被 DNS 解析,如果不能修改 DNS entries,則直接修改機器上的 hosts 檔案,CentOS 為 /etc/hosts,Windows 則在 c:\windows\system32\hosts
  4. 確認兩個系統有相同的程式碼版本,有幾種方式可達到此要求
    (a) 自行複製 kvs.erl
    (b) NFS 共享的磁碟
    (c) 設定程式碼伺服器:erl_prim_loader 模組
    (d) 使用 nl(Mod) 命令,這會在所有連結的電腦上載入 Mod 模組,但前提是要先用 net_admin:ping(Node) 的方式,將 erlang 節點連結在一起

兩台電腦在不同 domain,不同區域網路上,測試程式

基本上就跟在 LAN 的兩台電腦的 erlang 節點中一樣,但因為區域網路之間各有防火牆保護,要進行分散式 erlang 就要採取下列步驟。

  1. 網路要開放 TCP 與 UDP Port 4369,因為 epmd(erlang port mapper daemon) 會使用到這兩個 port
  2. 選擇一個 port 或是一個範圍的 port,進行分散式 erlang,確保這些 port 是開放的。如果這個 port 範圍是 Min 與 Max,就用以下指令啟動 erlang node
    >erl -name ... -setcookie ... -kernel inet_dist_listen_min Min inet_dist_listen_max Max
    如果只有開放一個 Port,就讓上面指令中的 Min=Max 即可。

分散式應用的相關函數

erlang node是一個有自己的位址空間,自己的行程集合,完整的vm的系統。

每個節點都有一個單一的 cookie,在一組 erlang node 之間,cookie 必須要一樣。互連且有相同 cookie 的節點,集合起來就是一個 erlang cluster。

分散式應用的相關BIF如下

  1. @spec spawn(Node, Fun) -> Pid
    在 Node 節點上生成 process
  2. @spec spawn(Node, Mod, Fun, ArgList) -> Pid
    在 Node 節點上生成 process
  3. @spec spawn_link(Node, Fun) -> Pid
  4. @spec spawn_link(Node, Mod, Fun, ArgList) -> Pid
  5. @spec disconnect_node(Node) -> bool() | ignored
    強迫節點斷線
  6. @spec monitor_node(Node, Flag) -> true
    當 Flag 為 true,會打開監控功能,如果 Node 加入或離開「連接的 erlang 節點」集合時,負責估算此 BIF 的行程,將會收到 {nodeup, Node} 與 {nodedown, Node} 訊息
  7. @spec node() -> Node
    本地節點的名稱,如果不是分散式節點,會傳回 nonode@nohost
  8. @spec node(Arg) -> Node
    Arg 為 PID、Ref或Port,會傳回 Arg 所在的節點
  9. @spec nodes() -> [Node]
    傳出連結的所有其他節點的 list
  10. @spec is_alive() -> bool()
    如果是系統的一部分,就傳出 true
  11. {RegName, Node}!Msg
    會送出 Msg 到 Node 節點註冊的行程 RegName

分散式應用相關的函式庫

  1. rpc
    call(Node, Mod, Fun, ArgList) -> Result | {badrpc, Reason}
  2. global
    提供註冊分散式系統名稱與鎖的函數

遠端生成的範例

-module(dist_demo).
-export([rpc/4, start/1]).

start(Node) ->
    spawn(Node, fun() -> loop() end).

rpc(ServerPid, M, F, A) ->
    ServerPid ! {rpc, self(), M, F, A},
    receive
        {SenderPid, Response} ->
            Response
    end.

loop() ->
    receive
        {rpc, SenderPid, M, F, A} ->
            SenderPid ! {self(), (catch apply(M, F, A))},
            loop()
    end.

測試
啟動 server 節點

[root@git temp]# erl -name server -setcookie abc
Erlang R16B03 (erts-5.10.4) [source] [smp:2:2] [async-threads:10] [hipe] [kernel-poll:false]

Eshell V5.10.4  (abort with ^G)
(server@git.maxkit.com.tw)1>

啟動 client 節點

[root@koko temp]# erl -name client -setcookie abc
Erlang R16B03 (erts-5.10.4) [source] [smp:2:2] [async-threads:10] [hipe] [kernel-poll:false]

Eshell V5.10.4  (abort with ^G)
(client@koko.maxkit.com.tw)1>

在 client 節點呼叫遠端生成程式,產生 server 節點的 process,並投過訊息傳遞,遠端估算 erlang:node(),然後取得結果。

(client@koko.maxkit.com.tw)1> Pid = dist_demo:start('server@git.maxkit.com.tw').
<6016.43.0>
(client@koko.maxkit.com.tw)2> dist_demo:rpc(Pid, erlang, node, []).
'server@git.maxkit.com.tw'
(client@koko.maxkit.com.tw)3>

設定 erlang cookie 的方法

  1. 將相同的 cookie 值儲存到 $HOME/.erlang.cookie
  2. 啟動 erl 時,以 -setcookie 指定
  3. erlang:set_cookie(node(), C)

以 socket 為基礎的分散式應用

分散式 erlang 適合寫「彼此信任」的叢集應用,不適合開放式的「不信任」環境。當我們有所有機器的控制權,我們可以用分散式 erlang 統一控管所有的機器。

當不同人管理不同機器,想控制哪些程式可以在他們的機器執行時,就要使用受限制版本的 spawn。

lib_chan

lib_chan 是作者 Joe Armstrong 提供的一個 module,可讓使用者手動控制那個行程可以在這個機器上生成。

lib_chan 需要自己編譯,請到 書本的網頁 取得原始程式碼,需要的程式有 lib_chan, lib_chan_auth, lib_chan_cs, lib_chan_mm, lib_md5。

  1. @spec start_server() -> true
    在本地主機上,啟動一個伺服器,該伺服器的行為由 $HOME/.erlang/lib_chan.conf 決定
  2. @spec start_server(Conf) -> true
    在本地主機上,啟動一個伺服器,該伺服器的行為由 Conf 決定
    configuration file 內容包含下列兩項
    (a) {port, NNNN}:對 port number: NNNN 進行監聽
    (b) {service, S, password, P, mfa, SomeMod, SomeFunc, SomeArgsS}:定義一個服務 S,密碼為 P,如果服務開始,就會以 SomeMod:SomeFunc(MM, ArgsC, SomeArgsS) 建立行程,處理來自客戶端的訊息。MM 是一個 proxy 行程的 PID,此代理行程用來傳送訊息給客戶端,參數 ArgsC 是來自客戶端的連線呼叫

  3. @spec connect(Host, Port, S, P, ArgsC) -> {ok, Pid} | {error, Why}
    對主機 Host 開啟 Port,啟動密碼為 P 的伺服器 S,如果密碼正確,會傳回 {ok, Pid},此 Pid 為「負責送訊息到伺服器」的代理行程PID

當客戶端呼叫 connect/5,會產生兩個 proxy process,一個在客戶端,一個在伺服器端。proxy process 負責處理 TCP 封包資料轉換,攔截控制行程的離開訊號,關閉 socket。

範例

configuration file: conf

%% conf
{port, 1234}.
{service, nameServer, password, "ABXy45", mfa, mod_name_server, start_me_up, notUsed}.

當客戶端呼叫
connect(Host, 1234, nameServer, "ABXy45", nil)
伺服器就會生成
mod_name_server:start_me_up(MM, nil, notUsed)
其中 MM 是 proxy process 的 PID,使用它來對客戶端溝通

%% mod_name_server.erl
-module(mod_name_server).
-export([start_me_up/3]).

start_me_up(MM, _ArgsC, _ArgS) ->
    loop(MM).

loop(MM) ->
    receive
        {chan, MM, {store, K, V}} ->
            kvs:store(K, V),
            loop(MM);
        {chan, MM, {lookup, K}} ->
            MM ! {send, kvs:lookup(K)},
            loop(MM);
        {chan_closed, MM} ->
            true
    end.

mod_name_server 遵循下面的協定:

  1. 如果客戶端送出訊息 {send, X} 給伺服器,它會出現在 mod_name_server,訊息格式類似 {chan, MM, X} (MM 是伺服器 proxy process 的 PID)
  2. 如果客戶端終結或用來溝通的 socket 關閉,伺服器會收到 {chan_closed, MM} 的訊息
  3. 如果伺服器想送訊息 X 到客戶端,它會呼叫 MM!{send, X}
  4. 如果伺服器想手動關閉連線,可執行 MM!close

此協定為 middle-man protocol,客戶端與伺服器端都要遵守

測試

1> kvs:start().
true
2> lib_chan:start_server().
lib_chan starting:"lib_chan.conf"
ConfigData=[{port,1234},
            {service,nameServer,password,"ABXy45",mfa,mod_name_server,start_me_up,notUsed}
true

在另一個 erl

1> {ok, Pid} = lib_chan:connect("localhost", 1234, nameServer, "ABXy45", "").
{ok,<0.41.0>}
2> lib_chan:cast(Pid, {store, joe, "writing a book"}).
{send,{store,joe,"writing a book"}}
3> lib_chan:rpc(Pid, {lookup, joe}).
{ok,"writing a book"}
4> lib_chan:rpc(Pid, {lookup, jim}).
undefined

參考

Erlang and OTP in Action
Programming Erlang: Software for a Concurrent World