Google Code Prettify

2015年10月21日 星期三

JGit - getting started

目前使用 Java 要操作 git,似乎只有 JGit 這組 API,從這篇開始,簡要的說明 JGit 的使用方法。在 JGit 中,最重要的兩個類別是 Repository 及 Git,Repository 類別可以用來存取 Git Repository (Git 容器) 中的屬性,Git 類別則提供了 Git 相關的指令。接下來舉一些實例:
  • 創建 Repository、Git
Repository repo = new FileRepositoryBuilder()
   .setGitDir(new File("D:/BitBucket/VersionControl/.git"))
   .build();
        
Git git = new Git(repo);
我的電腦中有一個目錄 D:/BitBucket/VersionControl,這是一個 git repository,在 BitBucket 這個網站上,有相對應的的一個 git bare repository,在創建 Repository 物件時,路徑指到本機的路徑,至於怎麼存取遠端的 bare repository 內的資料稍後說明。不管是 Repository 類別或 Git 類別,使用完都要呼叫 close() method,以關閉 I/O 連線。
  • 取得 remote name
如上圖所示,我的 git repository 設定了兩個 remote repository,要如何取得名稱呢? 如下:
Set<String> remoteNames = repo.getRemoteNames();




  •  取得本機 branch
除了預設的 master 外,我又建立了一個 TEST branch,以下是取得 branch 參考物件的方法。
ListBranchCommand branchCmd = git.branchList();
List<Ref> branch = branchCmd.call();
使用 Git 類別時,只要想想 git 有提供那些指令,大至上就可以找到相對的 method,上面的程式會取得 branch 的參考物件 (Ref),如果用 toString() 把它輸出,會如下:
Ref[refs/heads/TEST=e5dc4bc8a01cca3c74a3554f87857d468815b58b]
Ref[refs/heads/master=977a0b74fe3e49c166119d473f2415663e158260]
輸出的格式為 Ref[name=objectId],這表示如果我們要操作 branch,就可以透過這個方式取得 name 或 objectId 帶入其它 method 來操作 branch。
  • 取得遠端 branch
CredentialsProvider cp = new UsernamePasswordCredentialsProvider("username", "password");
Collection<Ref> lsRemote = git.lsRemote()
    .setCredentialsProvider(cp)
    .call();
        
for(Ref remote:lsRemote) {
    String objName = remote.getName();
            
    Ref localRef = repo.getRef(objName);
    System.out.println("local ref = " + localRef.toString());
            
    Ref remoteRef = remote.getTarget();
    System.out.println("remote ref = " + remoteRef.toString());
}
要取得遠端的物件,當然要認證,這裡採用帳號、密碼登入的方式,輸出會如下:
local ref = SymbolicRef[HEAD -> refs/heads/TEST=e5dc4bc8a01cca3c74a3554f87857d468815b58b]
remote ref = Ref[HEAD=6c248c9ae67bdf1fc65e3d5cc8110a5c017aba53]
local ref = Ref[refs/heads/master=977a0b74fe3e49c166119d473f2415663e158260]
remote ref = Ref[refs/heads/master=6c248c9ae67bdf1fc65e3d5cc8110a5c017aba53]
local ref = Ref[refs/heads/TEST=e5dc4bc8a01cca3c74a3554f87857d468815b58b]
remote ref = Ref[refs/heads/TEST=b6c238f6a641b4272dc0728a9db0ddd93690ce64]
可以看到,local 和 remote 的 object id 不同,對照直接下 git ls-remote 的輸出,就可以發現,上面的 remote ref 與 ls-remote 的輸出是一樣的。

2015年10月17日 星期六

Producer-Consumer Pattern



實務上,曾經遇過如上的例子,說明如下:

  1. 資料源每天在規定的時間(約數小時)快速的產生資料,寫到檔案裡,在該時段裡,約會產生近兩億筆的資料,每筆資料中會有不重複的序號。
  2. 有兩支程式會由檔案讀出資料,第一支是讀出資料後,寫入資料庫,第二支程式則是寫到 MQSeries,不管是寫到資料庫或 MQSeries,當然是為了下游的其它系統的需要。(方框中的兩支程式)
這裡要說明的是方框中的那兩支程式,這兩支程式的架構肯定會很像,因為都是讀檔後輸出,差別只在於輸出的地方不同。實務上看到的程式架構是,主程式只有一個類別,在該類別中讀取資料後輸出,再讀取下一筆,再輸出,也就是循序的,如下圖:


從 I/O 的效率看,讀檔案的速度遠遠大於寫入資料庫的速度,就算讀與寫沒有分開成兩個執行緒,影響應該是很有限。回到第一個圖,這整套系統有兩支這樣的程式,另一支是寫到 MQSeries,所以,又有一支如圖二架構的程式,差別只在輸出的那部份是寫到 MQSeries。這樣的寫法有兩個明顯的問題:
  1. 程式碼重複。
  2. 輸入與輸出的部份相耦合,可重用性(reuse)不高。
要改善這個架構,可以套用 Producer-Consumer Pattern (生產者-消費者 範式),這個 pattern 在多執行緒程式中是很常見的,以第二張圖的架構來說, 輸入的部份會成為 producer,輸出的部份成為 consumer,中間加上一個佇列為中介,即可以將這兩部份解耦,如下圖:


套用 pattern 後的程式,輸入與輸出的程式,要面對的都是佇列,而不需要在意另一端是如何實作,實際上資料源是什麼? 或實際要輸出的儲存體是什麼? 也就是說,輸出的部份要寫兩個類別,一個是輸出到資料庫的,一個是輸出到 MQSeries 的,其它部份都不需要改變,只要依需要,換用適當的輸出類別。這個寫法有以下優點:
  1. 輸入、輸出解耦,可重用性高。
  2. 解耦後,各自的邏輯變簡單,程式的可讀性提高。
  3. 輸入、輸出分開在不同執行緒,效率提高。




現在來看一下程式怎麼寫?

 1 package idv.steven.patterns;
 2 
 3 import java.nio.ByteBuffer;
 4 import java.util.Vector;
 5 import java.util.concurrent.TimeUnit;
 6 import java.util.concurrent.locks.Condition;
 7 import java.util.concurrent.locks.ReentrantLock;
 8 
 9 public class Queue {
10     private Vector<ByteBuffer> pool = new Vector<ByteBuffer>();
11     private Vector<ByteBuffer> queue = new Vector<ByteBuffer>();
12     
13     private ReentrantLock lock = new ReentrantLock();
14     private Condition waitQueue = lock.newCondition();
15     
16     private int size = 0;
17     private int capacity = 0;
18     
19     public Queue(int size, int capacity) {
20         this.size = size;
21         this.capacity = capacity;
22         
23         for(int i=0; i<size; i++) {
24             pool.add(ByteBuffer.allocate(capacity));
25         }
26     }
27     
28     public ByteBuffer alloc() {
29         ByteBuffer buf = null;
30         
31         try {
32             buf = pool.remove(0);
33         }
34         catch (ArrayIndexOutOfBoundsException e) {
35             
36         }
37         
38         return buf;
39     }
40     
41     /**
42      * 取得的 ByteBuffer 使用完一定要記得呼叫此 method,否則資源會越來越少。
43      * @param buf
44      */
45     public void release(ByteBuffer buf) {
46         if (buf.capacity() != capacity) {
47             buf = ByteBuffer.allocate(capacity);
48         }
49         else {
50             buf.clear();
51         }
52         
53         pool.add(buf);
54     }
55     
56     public ByteBuffer take() {
57         ByteBuffer buf = null;
58         
59         try {
60             buf = queue.remove(0);
61         }
62         catch (ArrayIndexOutOfBoundsException e) {
63             
64         }
65         
66         if (buf == null) {
67             try {
68                 lock.lock();
69                 if (waitQueue.await(10, TimeUnit.SECONDS)) {
70                     buf = queue.remove(0);
71                 }
72             } catch (InterruptedException | ArrayIndexOutOfBoundsException e) {
73             }
74             finally {
75                 lock.unlock();
76             }
77         }
78         
79         return buf;
80     }
81     
82     public void put(ByteBuffer buf) {
83         try {
84             lock.lock();
85             queue.add(buf);
86             waitQueue.signal();
87         }
88         finally {
89             lock.unlock();
90         }
91     }
92 }

這個佇列程式在初始化時先產生足夠的 ByteBuffer 備用,這樣效率會比較好,因為這個系統會產生大量的資料,一秒鐘就有超過百筆,如果每次都用 ByteBuffer.allocate(...) 產生,會造成效率上的瓶頸。

在 take() method 中,如果第一次從佇列中取資料,結果佇列沒資料回傳 null,就等 10 秒再取一次。在寫 multi-thread 程式時,盡量不要用 sleep 來等其它 thread,這樣很沒有效率,應該用 await,當其它 thread 完成正等待的事情後 (這裡就是等 producer 產生資料放入佇列後),該 thread 呼叫 signal 就可以繼續往下執行,程式第 86 行就是當有資料放入佇列後,會呼叫 signal。

 1 package idv.steven.patterns;
 2 
 3 import java.nio.ByteBuffer;
 4 
 5 public class Consumer implements Runnable {
 6     private String name = "";
 7     private Queue queue = null;
 8     
 9     public Consumer(String name, Queue queue) {
10         this.name = name;
11         this.queue = queue;
12     }
13 
14     @Override
15     public void run() {
16         ByteBuffer buf = null;
17         
18         int i = 1;
19         while ((buf = queue.take()) != null) {
20             byte[] dst = new byte[buf.limit()];
21             buf.get(dst, 0, buf.limit());
22             String s = new String(dst);
23             System.out.println(name + "(" + (i++) + "):" + s);
24             queue.release(buf);
25         }
26     }
27 }

Consumer (消費者) 程式收到資料將它輸出到 console,真正的程式當然要輸出到資料庫或 MQSeries,那會複雜的多,這裡為了說明簡化了。

1 package idv.steven.patterns;
 2 
 3 import java.nio.ByteBuffer;
 4 import java.util.Random;
 5 
 6 public class Producer implements Runnable {
 7     private Queue queue = null;
 8     
 9     @Override
10     public void run() {
11         queue = new Queue(100, 1024);
12         
13         Thread tC1 = createConsumer("c1");
14         Thread tC2 = createConsumer("c2");
15         Thread tC3 = createConsumer("c3");
16         
17         Random rand = new Random();
18         
19         for(int i=0; i<10; i++) {
20             ByteBuffer buf = queue.alloc();
21             if (buf != null) {
22                 buf.put(String.format("%04d", rand.nextInt(10000)).getBytes());
23                 buf.flip();
24                 queue.put(buf);
25             }
26         }
27         
28         try {
29             tC1.join();
30             tC2.join();
31             tC3.join();
32         } catch (InterruptedException e) {
33         }
34     }
35     
36     private Thread createConsumer(String name) {
37         Consumer c = new Consumer(name, queue);
38         Thread tConsumer = new Thread(c);
39         tConsumer.start();
40         
41         return tConsumer;
42     }
43 
44     public static void main(String[] args) {
45         Producer producer = new Producer();
46         Thread tProducer = new Thread(producer);
47         tProducer.start();
48     }
49 }

這個程式是一個 Producer (生產者) 有三個 Consumer (消費者),實際上的應用會依需要而定,所以,Producer-Consumer pattern 會有很多的變形。這裡為了說明方便,由 producer 用亂數產生十個資料,放入佇列中,由 consumer 取出並輸出到 console,佇列中的資料會由那一個 consumer 取得,會依系統的排程決定。輸出的結果如下,當然每次執行會有點差異。
c1(1):2368
c3(1):0728
c2(1):5240
c3(2):3292
c1(2):7614
c3(3):7558
c2(2):8970
c3(4):2345
c1(3):1866
c2(3):5144
佇列也可以更複雜,不同的資料還會有優先權,或是其它各項規則,也可能佇列改成堆壘,這都可視需要而變更。

2015年9月8日 星期二

NIO.2: UDP 網路程式設計 (multicast)

同樣是採用 UDP 通訊協定,multicast 和前一篇 (UDP 網路程式設計 (request / response)) 點對點的傳播方式有什麼不同?
  1. multicast 是多點傳播,在 IPv4 時有廣播和群播,到了 IPv6 只有群播。群播即事先加入群組的消費者,就可以收到生產者廣播出來的資料。
  2. multicast 的參數中有個 TTL 值,這個值介於 1~255 之間,當封包每通過一個 router,就會被減 1 (有些 router 會減 2 或更多),當值為 0 時即停止傳播。
UDP 雖然是不保證送達的通訊協定,但是少了確認的封包,速度比 TCP 快約 3 倍,當一個生產者要同時送給非常多消費者時,TCP 很快就會拖垮伺服器的效能,UDP 則可以在消費者判斷漏收了資料時,才要求生產者重送,大量減少網路流量,又能保證送達,當然,程式會複雜許多。

在 NIO.2 中,UDP 程式都是使用 DatagramChannel 這個類別,回顧前一篇的類別圖會發現,點對點傳播時沒有用到 MulticastChannel 介面所定義的 method,從名稱就知道,這是給 multicast 使用的。

在開始說明 multicast 程式前,請先執行如下 oracle 附的程式:
import java.io.*;
import java.net.*;
import java.util.*;
import static java.lang.System.out;

public class ListNets {

    public static void main(String args[]) throws SocketException {
        Enumeration<NetworkInterface> nets = NetworkInterface.getNetworkInterfaces();
        for (NetworkInterface netint : Collections.list(nets))
            displayInterfaceInformation(netint);
    }

    static void displayInterfaceInformation(NetworkInterface netint) throws SocketException {
        out.printf("Display name: %s\n", netint.getDisplayName());
        out.printf("Name: %s\n", netint.getName());
        Enumeration<InetAddress> inetAddresses = netint.getInetAddresses();
        for (InetAddress inetAddress : Collections.list(inetAddresses)) {
            out.printf("InetAddress: %s\n", inetAddress);
        }
        out.printf("\n");
     }
}
這個程式會列出電腦上所有的網卡及其名稱,這個非常重要,因為 multicast 程式需指明要從那一張網卡收、送資料,接下來看看程式,底下的程式 server 會送出 10 次的系統時間,client 收到後將它輸出到 console,先看輸出結果,應該會類似如下:




  • Server
 1 package idv.steven.udp;
 2 
 3 import java.io.IOException;
 4 import java.net.InetAddress;
 5 import java.net.InetSocketAddress;
 6 import java.net.NetworkInterface;
 7 import java.net.StandardProtocolFamily;
 8 import java.net.StandardSocketOptions;
 9 import java.nio.ByteBuffer;
10 import java.nio.channels.DatagramChannel;
11 import java.util.Date;
12 
13 public class MulticastServer {
14     public static void main(String[] args) {
15         final int DEFAULT_PORT = 5555;
16         final String GROUP = "225.5.6.6";
17         ByteBuffer datetime;
18         
19         // 開啟一個 IPv4 的資料封包通道
20         try (DatagramChannel datagramChannel = DatagramChannel.open(StandardProtocolFamily.INET)) {
21             // 檢查是否成功開啟
22             if (datagramChannel.isOpen()) {
23                 // 取得名稱為 eth3 的網卡
24                 NetworkInterface networkInterface = NetworkInterface.getByName("eth3");
25                 // 設定 multicast 要使用的網卡
26                 datagramChannel.setOption(StandardSocketOptions.IP_MULTICAST_IF, networkInterface);
27                 datagramChannel.setOption(StandardSocketOptions.SO_REUSEADDR, true);
28                 // 將 DatagramChannel 繫結到指定的埠號,要注意,這時還不需要指定 IP 群組。
29                 datagramChannel.bind(new InetSocketAddress(DEFAULT_PORT));
30                 
31                 for(int i=0; i<10; i++) {
32                     // 每 10 秒送出一筆資料
33                     try {
34                         Thread.sleep(10000); // 睡 10 秒
35                     } catch (InterruptedException ex) {}
36                     
37                     datetime = ByteBuffer.wrap(new Date().toString().getBytes());
38                     // 送出時才指定要送到那個 IP 群組,這表示 bind 後,
39                     // 還是可以看資料特性,送到不同 IP 群組。
40                     datagramChannel.send(datetime, new
41                             InetSocketAddress(InetAddress.getByName(GROUP), DEFAULT_PORT));
42                     datetime.flip();
43                 }
44             } else {
45                 System.out.println("通道開啟失敗");
46             }
47         } catch (IOException ex) {
48             ex.printStackTrace();
49         }
50     }
51 }
  • Client
 1 package idv.steven.udp;
 2 
 3 import java.io.IOException;
 4 import java.net.InetAddress;
 5 import java.net.InetSocketAddress;
 6 import java.net.NetworkInterface;
 7 import java.net.StandardProtocolFamily;
 8 import java.net.StandardSocketOptions;
 9 import java.nio.ByteBuffer;
10 import java.nio.CharBuffer;
11 import java.nio.channels.DatagramChannel;
12 import java.nio.channels.MembershipKey;
13 import java.nio.charset.Charset;
14 import java.nio.charset.CharsetDecoder;
15 
16 public class MulticastClient {
17 
18     public static void main(String[] args) {
19         final int DEFAULT_PORT = 5555;
20         final int MAX_PACKET_SIZE = 65507;
21         final String GROUP = "225.5.6.6";
22         
23         CharBuffer charBuffer = null;
24         Charset charset = Charset.defaultCharset();
25         CharsetDecoder decoder = charset.newDecoder();
26         ByteBuffer datetime = ByteBuffer.allocateDirect(MAX_PACKET_SIZE);
27         
28         try (DatagramChannel datagramChannel = DatagramChannel.open(StandardProtocolFamily.INET)) {
29             // 要傾聽的 IP 群組
30             InetAddress group = InetAddress.getByName(GROUP);
31             // 檢查是否為合法的多址傳播群組
32             if (group.isMulticastAddress()) {
33                 // 檢查是否成功開啟通道
34                 if (datagramChannel.isOpen()) {
35                     // 設定要傾聽的網卡
36                     NetworkInterface networkInterface = NetworkInterface.getByName("eth3");
37                     datagramChannel.setOption(StandardSocketOptions.SO_REUSEADDR, true);
38                     // 將 DatagramChannel 繫結到指定的埠號,要注意,這時還不需要指定 IP 群組。
39                     datagramChannel.bind(new InetSocketAddress(DEFAULT_PORT));
40                     // 加入要傾聽的群組
41                     MembershipKey key = datagramChannel.join(group, networkInterface);
42                     
43                     while (true) {
44                         if (key.isValid()) {
45                             // 開啟等待 server 端送來的封包
46                             datagramChannel.receive(datetime);
47                             datetime.flip();
48                             charBuffer = decoder.decode(datetime);
49                             System.out.println(charBuffer.toString());
50                             datetime.clear();
51                         } else {
52                             break;
53                         }
54                     }
55                 } else {
56                     System.out.println("通道無法開啟");
57                 }
58             } else {
59                 System.out.println("這個 IP 不是合法的多址傳播 IP");
60             }
61         } catch (IOException ex) {
62             ex.printStackTrace();
63         }
64     }
65 }