2017年10月23日

Thrift

Apache Thrift 提供了一個產生不同語言程式碼的 compiler engine,可在不同程式語言(C++, Java, Python, PHP, Ruby, Erlang, Perl, Haskell, C#, Cocoa, JavaScript, Node.js, Smalltalk, OCaml and Delphi)環境之間,透過 Thrift 以 RPC 方式傳輸資料。

安裝 thrift compiler tool

thrift compiler tool 是產生 thrift 程式碼的工具。

在 macos,可透過 macport 安裝,在 thrift Portfile 中查看目前支援的語言是 java, c#, C, haskell, php, erlang,還不是很清楚為什麼不支援 go, python, ruby 等語言。

sudo port install thrift +java +erlang
$ thrift --version
Thrift version 0.10.0

在 CentOS 可直接編譯

cd thrift-0.10.0
./configure
make
sudo make install

.thrift 文件

在安裝 thrift compiler 後,要撰寫一份 .thrift 定義文件,該檔案是一份以 thrift types 及 Services 定義的interface definition,其中 Services 就是 server 要實作的功能,並提供給 client 呼叫,然後才能用 compiler 產生某個對應程式語言的程式碼,使用方式如下。

thrift --gen <language> <Thrift filename>

Thrift Types

Base Types

一般程式語言都提供的最基本的資料類型

  • bool: A boolean value (true or false)
  • byte: An 8-bit signed integer
  • i16: A 16-bit signed integer
  • i32: A 32-bit signed integer
  • i64: A 64-bit signed integer
  • double: A 64-bit floating point number
  • string: A text string encoded using UTF-8 encoding

Special Types

  • binary: a sequence of unencoded bytes

    這是上述 string 的特殊格式,可用在跟 java 系統之間有更好的資料收送,未來可能會提升為 base type

Structs

定義 common object,這等同於 OOP 中的 class,但沒有繼承的機制,前面的數字 1: 是要讓 compiler 可針對不同版本 IDL 識別,可填上 default value,也可以設定為 optional。

ex:

struct User {  
    1: i16 gender = 1,  
    2: string username,  
    3: string password,  
    4: i32 id  
}

Containers

有三種 container types

  • list: 有順序的元素集合 An ordered list of elements,會對應到 STL vector, Java ArrayList, native arrays in scripting languages

  • set: 沒有順序的元素集合 An unordered set of unique elements,會對應到 an STL set, Java HashSet, set in Python, etc.

    Note: PHP 不支援 sets,會以 List 方式處理。

  • map: 唯一的 key 對應 value 的集合 A map of strictly unique keys to values,會對應到 an STL map, Java HashMap, PHP associative array, Python/Ruby dictionary, etc.

    如果有初始值時,可以在不同的程式語言中,以自訂的 code generator 替代為 custom data type

    為了提供最佳的相容性,key 的 data type 最好使用 Basic Type

Exceptions

Exception 等同於 structs,是繼承自不同程式語言的 native exception base class

exception NotFoundException{  
    1:i16 errorType,  
    2:string message  
}

Services

以 Thrift type 定義 services,其作用就像是 OOP 裡面的 interface,Thrift compiler 可以自動產生實作 client, server 的 stubs codes,service 之間有提供繼承的機制。

service 包含了一組 functions,每一個都有一個參數 list 及 return type。

return type 可以使用 void,也就是不回傳資料,但實際上,server 還是會回傳一個 response 給 client,用來告訴 client 已經把所有工作都做完了。

可以在 function 上增加一個 oneway modifier,這代表 clietn 不會等待 server 的 response,這表示只能保證 client 會呼叫 server 的 function,但不能保證 server 會依照呼叫的順序執行該 function。

service <name> {  
  <returntype> <name> (<arguments>)[throws (<exceptions>)]
}
service UserService{  
  void saveUser(1:User user),  
  User get(1:i32 id) throws (1:NotFoundException nfe),
  oneway void zip()
}
service Calculator extends shared.SharedService 

其他

  • include

    通過include引用其他的thrift文件,默認在當前路徑下尋找,也可以在相對路徑下尋找,需要通過編譯參數 -I 來設置

  • namespace 與 java 的 package 作用一樣

namespace java thrift.sa
namespace python thrift.sa
  • 常數
const i32 INT32CONSTANT = 9853
const map<string,string> MAPCONSTANT = {'hello':'world', 'goodnight':'moon'}
  • enum
enum Operation {  
  ADD = 1,  
  SUBTRACT = 2,  
  MULTIPLY = 3,  
  DIVIDE = 4  
}

Java 支援的傳輸格式, 方式

  • 支援的傳輸格式

    1. TBinaryProtocol 二進制格式.
    2. TCompactProtocol 壓縮格式
    3. TJSONProtocol JSON格式
    4. TSimpleJSONProtocol 提供 JSON 只寫協議, 生成的文件很容易通過腳本語言解析
    5. TDebugProtocol 使用易懂的可讀的文本格式,以便於debug
  • 支援的數據傳輸方式

    1. TSocket 阻塞式 socket server
    2. TFramedTransport 以frame為單位進行傳輸,非阻塞式服務中使用
    3. TFileTransport 以文件形式進行傳輸。
    4. TMemoryTransport 將 memory 用於I/O,java 實作時內部實際使用了簡單的ByteArrayOutputStream
    5. TZlibTransport – 使用zlib進行壓縮, 與其他傳輸方式聯合使用,目前無java實現
  • 支援的服務模型

    1. TSimpleServer 簡單的單線程服務模型,常用於測試
    2. TThreadPoolServer 多線程服務模型,使用標準的阻塞式IO。
    3. TNonblockingServer 多線程服務模型,使用非阻塞式IO(需使用TFramedTransport數據傳輸方式)

Example - bookservice.thrift

先寫 bookservice.thrift,然後以 thrift compiler 產生 java 及 erlang 的 code

namespace java tw.com.maxkit.test

struct Book_info{
    1: i32 book_id;
    2: string book_name;
    3: string book_author;
    4: double book_price;
    5: string book_publisher
}

service BookSender{
    void ping(),

    i32 add(
        1:i32 num1, 2:i32 num2
    ),

    bool sender(
        1: list<Book_info> books
    );

    oneway void sender2(
        1: list<Book_info> books
    );
}

產生 java 及 erlang 的 code

thrift --gen erl bookservice.thrift
thrift --gen java bookservice.thrift
gen-erl/
    book_sender_thrift.erl
    book_sender_thrift.hrl
    bookservice_constants.hrl
    bookservice_types.erl
    bookservice_types.hrl
gen-java/tw/com/maxkit/test/
    Book_info.java
    BookSender.java

Java

BookServiceHandler 實作 Server Side 的四個 function

import tw.com.maxkit.test.*;


public class BookServiceHandler implements BookSender.Iface {

    public BookServiceHandler() {
    }

    public void ping() {
        System.out.println("ping()");
    }

    public int add(int n1, int n2) {
        System.out.println("add(" + n1 + "," + n2 + ")");
        return n1 + n2;
    }

    public boolean sender(java.util.List<Book_info> books) throws org.apache.thrift.TException {

        System.out.println("Sender get books");
        for(Book_info b: books) {
            System.out.println("get book "+b.book_id);
        }
        return true;
    }

    public void sender2(java.util.List<Book_info> books) throws org.apache.thrift.TException {
        System.out.println("Sender2 get books");
        for(Book_info b: books) {
            System.out.println("get book "+b.book_id);
        }
    }

}

BookServer

import org.apache.thrift.server.TServer;
import org.apache.thrift.server.TServer.Args;
import org.apache.thrift.server.TSimpleServer;
import org.apache.thrift.transport.TSSLTransportFactory;
import org.apache.thrift.transport.TServerSocket;
import org.apache.thrift.transport.TServerTransport;
import org.apache.thrift.transport.TSSLTransportFactory.TSSLTransportParameters;

import tw.com.maxkit.test.*;

public class BookServer {

    public static BookServiceHandler handler;

    public static BookSender.Processor processor;

    public static void main(String [] args) {
        try {
            handler = new BookServiceHandler();
            processor = new BookSender.Processor(handler);

            Runnable simple = new Runnable() {
                public void run() {
                    simple(processor);
                }
            };
            Runnable secure = new Runnable() {
                public void run() {
                    secure(processor);
                }
            };

            new Thread(simple).start();
            new Thread(secure).start();
        } catch (Exception x) {
            x.printStackTrace();
        }
    }

    public static void simple(BookSender.Processor processor) {
        try {
            TServerTransport serverTransport = new TServerSocket(9090);
            TServer server = new TSimpleServer(new Args(serverTransport).processor(processor));

            // Use this for a multithreaded server
            // TServer server = new TThreadPoolServer(new TThreadPoolServer.Args(serverTransport).processor(processor));

            System.out.println("Starting the simple server...");
            server.serve();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public static void secure(BookSender.Processor processor) {
        try {
            TSSLTransportParameters params = new TSSLTransportParameters();
            // The Keystore contains the private key
            params.setKeyStore("keystore", "max168kit", null, "PKCS12");

            TServerTransport serverTransport = TSSLTransportFactory.getServerSocket(9091, 0, null, params);
            TServer server = new TSimpleServer(new Args(serverTransport).processor(processor));

            // Use this for a multi threaded server
            // TServer server = new TThreadPoolServer(new TThreadPoolServer.Args(serverTransport).processor(processor));

            System.out.println("Starting the secure server...");
            server.serve();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

BookClient

import org.apache.thrift.TException;
import org.apache.thrift.transport.TSSLTransportFactory;
import org.apache.thrift.transport.TTransport;
import org.apache.thrift.transport.TSocket;
import org.apache.thrift.transport.TSSLTransportFactory.TSSLTransportParameters;
import org.apache.thrift.protocol.TBinaryProtocol;
import org.apache.thrift.protocol.TProtocol;
import tw.com.maxkit.test.BookSender;
import tw.com.maxkit.test.Book_info;

import java.util.ArrayList;

public class BookClient {
    public static void main(String[] args) {

        String servertype="simple";
        if (args.length == 1) {
            //System.out.println("Please enter 'simple' or 'secure'");
            //System.exit(0);
            servertype=args[0];
        }

        try {
            TTransport transport;
            if (servertype.contains("simple")) {
                transport = new TSocket("localhost", 9090);
                transport.open();
            } else {
                TSSLTransportParameters params = new TSSLTransportParameters();
                params.setTrustStore("keystore", "max168kit", null, "PKCS12");
                transport = TSSLTransportFactory.getClientSocket("localhost", 9091, 0, params);
            }

            TProtocol protocol = new TBinaryProtocol(transport);
            BookSender.Client client = new BookSender.Client(protocol);

            perform(client);

            transport.close();
        } catch (TException x) {
            x.printStackTrace();
        }
    }

    private static void perform(BookSender.Client client) throws TException {
        client.ping();
        System.out.println("ping()");

        int sum = client.add(1, 1);
        System.out.println("1+1=" + sum);

        Book_info b1 = new Book_info(1, "name1", "author1", 11.1, "publisher1");

        Book_info b2 = new Book_info(2, "name2", "author2", 22.2, "publisher2");

        ArrayList list = new ArrayList();
        list.add(b1);
        list.add(b2);

        boolean result = client.sender(list);
        System.out.println("sender1 result="+result);

        client.sender2(list);
        System.out.println("sender2 done");

    }
}

Erlang

book_server.erl

-module(book_server).
-include("book_sender_thrift.hrl").

%% API
-export([start/0, handle_function/2, ping/0, add/2, sender/1, sender2/1, stop/1]).

debug(Data)->
  io:format("Debug info:~s~n",[Data]).
debug(Format, Data) ->
  error_logger:info_msg(Format, Data).

ping() ->
  debug("ping()",[]),
  ok.

add(N1, N2) ->
  debug("add(~p,~p)",[N1,N2]),
  N1+N2.

sender(_L1) ->
  true.

sender2(_L1) ->
  ok.

start()->
  start(9090).

start(Port)->
  Handler = ?MODULE,
  debug("1",[]),
  Res=thrift_socket_server:start([{handler, Handler},
    {service, book_sender_thrift},
    {port, Port},
    {name, book_server}]),
  debug("2 ~p ~n",[Res]),
  Res.

stop(Server)->
  thrift_socket_server:stop(Server).

%%handle_function(Function, Args) when is_atom(Function), is_tuple(Args) ->
%%  case Function of
%%    ping ->
%%      {reply, ping()};
%%    add ->
%%      {reply, add(tuple_to_list(Args))};
%%    % add function here
%%    _ ->
%%      error
%%  end.
handle_function(Function, Args) when is_atom(Function), is_tuple(Args) ->
  debug("handle_function ~n",[]),
  case apply(?MODULE, Function, tuple_to_list(Args)) of
    ok -> ok;
    Reply -> {reply, Reply}
  end.

book_client.erl

-module(book_client).

%% API
-export([]).

%% API
-export([ping/0]).

-spec ping() -> ok.

ping() ->
  io:format("call thrift server\n"),
%%  {ok, Port} = application:get_env(larzio, agent_port),
%%  {ok, Ip} = application:get_env(larzio, agent_server),
  Port = 9090,
  Ip = "localhost",
  {ok, Client0} = thrift_client_util:new(Ip, Port, book_sender_thrift, []),
  {Client1, {ok, ok}} = thrift_client:call(Client0, ping, []),
  {Client1, {ok, AddResult}} = thrift_client:call(Client0, add, [1, 2]),
  io:format("add result ~p ~n", [AddResult]),
  {_Client8, ok} = thrift_client:close(Client1),
  io:format("call thrift done~n"),
  ok.

Reference

thrift 教程

初探Thrift客戶端異步模式

[Android] apache thrift 入門與android上的實作

Apache Thrift 官方JAVA教程

erlang+thrift配合開發

erlang server.erl

2017年10月16日

thrift, protobuf, avro 比較

以往在跨不同程式語言的系統之間,會使用 SOAP(Web Service), XML 或是 JSON,但這幾個方案目前目前大都是用在網頁上。thrift, protobuf, avro 是三種跨語言通信方案,在不同程式語言之間,資料必須要以有效率的方式,序列化後,再進行傳輸,到另一端反序列化。另外 thrift 及 avro 還將傳輸機制內建在函式庫中,可在不同程式語言環境之間,進行 RPC 遠端呼叫。

thrift, protobuf, avro 簡述

Google Protocol Buffers 是一種序列化與結構化數據的一種機制,具有跨平台、解析速度快、序列化數據體積小、擴展性高、使用簡單的特點。

Apache thrift 是由 Facebook 主導開發的一個跨平台、支持多語言的,通過定義 IDL 文件,自動生成 RPC 客戶端與服務端通信代碼的工具,可產生在 C++, Java, Python, PHP, Ruby, Erlang, Perl, Haskell, C#, Cocoa, JavaScript, Node.js, Smalltalk, and OCaml 這些語言之間互相傳遞資料的使用程式碼。其中一方是 Server,另一端是 client。

Apache Avro 是一個二進制的數據序列化系統。實際上 Avro 除了序列化之外,也提供了 RPC 功能。 Avro 是屬於 Hadoop的一個子項目,由 Hadoop 的 創始人 Doug Cutting 開始開發,用於支援大量數據交換的應用,依賴 Schema 來實現資料結構定義,Schema 是以JSON 對象來表示, Avro 也被作為一種 RPC 框架來使用。客戶端希望同服務器端交互時,就需要交換雙方通信的協議,它類似於模式,需要雙方來定義,在 Avro中被稱為Message。

thrift avro protobuf 的優缺點比較

一般認定,protobuf 因為不包含 RPC 的部分,如果單純要對資料進行傳輸前的序列化或是接收後的反序列化,不管是序列化速度跟資料大小,都是最佳選擇。

如果要在不同的程式語言之間進行 RPC 呼叫,那麼 avro 的性能及效率,會是比 thrift 還好的選擇,但因為 avro 官方支援的程式語言比較少一點,如果以涵蓋的程式語言範圍來看,就要選用 thrift

thrift

優點

支持非常多的語言 (C++, Java, Python, PHP, Ruby, Erlang, Perl, Haskell, C#, Cocoa, JavaScript, Node.js, Smalltalk, and OCaml)
thrift文件生成目標代碼,簡單易用
消息定義文件支持註釋
數據結構與傳輸實作分離,支援多種消息格式
包含完整的客戶端/服務端stack,可快速實現RPC
支援 同步 和 異步 兩種通信方式

缺點

不支援動態特性

avro

優點

binary 消息,性能好/效率高
使用JSON描述模式(schema)
schema 和 資料 統一存儲,消息自描述,不需要生成stub代碼(支持生成IDL)
RPC調用在握手階段交換模式定義
包含完整的客戶端/服務端 stack,可快速實現RPC
支援 同步 和 異步 通信
支援 動態 消息
模式定義允許定義數據的排序(序列化時會遵循這個順序)
提供了基於Jetty內核的服務基於Netty的服務

缺點

只支援 Avro 自己的序列化格式
語言綁定(python, java, C, C++, C#) 不如Thrift豐富

protobuf

優點

binary 消息,性能好/效率高(空間和時間效率都很不錯)
proto 文件生成目標代碼,簡單易用
序列化反序列化直接對應程序中的數據類別,不需要解析後再進行映射(XML,JSON都是這種方式)
支援向前相容(新加字段採用默認值)和向後相容(忽略新加字段),簡化升級
支援多種語言(類似IDL文件)

缺點

官方只支援 C++, JAVA 和 Python 語言
binary 可讀性差
binary 不具有自描述特性
不具備動態特性(可以通過動態定義生成消息類型或者動態編譯支持)
只有序列化和反序列化技術,沒有RPC功能

References

跨語言通信方案比較——thrift、protobuf和avro

protobuf和thrift對比

三種通用應用層協議protobuf、thrift、avro對比,完爆xml,json,http

Thrift、protocolbuffer、avro這幾種序列化之間的比較

使用Apache Avro

2017年10月2日

macport 如何安裝舊版軟體

如果在 macport 直接以 sudo port upgrade outdated 更新軟體,都會直接更新到最新版本,但有時候為了軟體的相容性問題,還是需要安裝舊版的軟體,以下記錄安裝舊版軟體的過程。

以 erlang 為例,目前 macport 最新為 20.0 版,如要安裝 erlang 19.3 版,要用以下程序處理。

由於 macport 的權限問題,必須在 /tmp 執行下列的程序。

cd /tmp
mkdir port
cd port

git clone --single-branch https://github.com/macports/macports-ports.git
cd macports-ports

因為 erlang 19.3 版,在 erlang Portfile history 可找到該 Portfile History,在 19.3 版的地方,點擊 <>,可查看該 commit 的網址https://github.com/macports/macports-ports/blob/e80897a5cc8f3583eac1bff12a62db6dc8ce4f99/lang/erlang/Portfile

因此我們用 git 指令切換到該 commit

git checkout e80897a5cc8f3583eac1bff12a62db6dc8ce4f99

安裝 19.3 版 erlang

cd lang/erlang

sudo port install +hipe+ssl+wxwidgets

以指令查詢目前 active 的 erlang 版本

sudo port installed | grep erlang

結果為

  erlang @19.3_0+hipe+ssl+wxwidgets (active)
  erlang @20.0_0+hipe+ssl+wxwidgets

如果版本錯誤,可用以下指令切換至 19.3

sudo port activate erlang @19.3

更新 macport 軟體可用以下的 script,在 uninstall inactive 時,可排除 erlang,避免舊版 erlang 被移除。

port_upgrade.sh

#!/bin/bash
echo "upgrade port..."
echo "!port selftupdate"
sudo port selfupdate

echo "!port upgrade outdated"
sudo port upgrade outdated

echo "!port installed inactive"
# list inactive installed packages
sudo port installed inactive

echo "!port uninstall inactive"
# uninstall inactive installed packages
#sudo port uninstall inactive
sudo port uninstall inactive and not erlang

References

InstallingOlderPort

2017年9月25日

Optional in swift

在物件導向程式語言中,如果一開始沒有定義一個變數本身儲存的資料,通常會用 nil(null) 來先定義變數,然後在後面要使用到這個變數時,進行物件的初始化。

在沒有 Optional 的狀況下,程式必須要在使用該變數時,先檢查這個變數倒底初始化了沒,如果沒有初始化,還得跳過使用這個變數的程式區塊,或是直接在這裡初始化,古早以前的 Java 程式就常常遇到這樣的問題,在使用到沒有初始化的變數時,發生 Null Pointer Exception,而造成程式 crash。

Optional 的宣告

swift 的 Optional 是以 enum 的方式實作,Optional 所代表的意義為 "there is a value, and it equals x" 或是 "there isn’t a value at all"

Optional 的定義宣告如下,可得知有兩種case: 有值 Some 及 無值 None

public enum Optional<Wrapped> : _Reflectable, NilLiteralConvertible {
    case None
    case Some(Wrapped)
}

Optional 就像是一個物件的包裝盒/包裹,裡面裝了 Some 或是 None 兩種東西,Some 就是盒子裡裝了其他類型的物件 instance,None 就是盒子裡面沒有東西。

在 swift 要可以用 Optional.some 或是 Optional.none 包裝物件 instance。

let box = Optional.some("iphone")
let box2:Optional<String> = Optional.some("iphone")
let box3:Optional<String> = Optional.none

print("box3 is \(box3)")

? 是 swift 裡面針對 Optioanl 的 syntax sugar,在宣告時,型別後面加上?,就可以宣告該變數為 Optional

var newbox:String?
newbox = nil
print("newbox is \(newbox)")

let newbox2:String? = "iphone"
let newbox3:String? = nil

使用 Optional 裡面的物件

Optional 是個包裝盒,要使用 optional 變數必須要用下面幾種方式,將盒子拆開

  • 使用 !

要使用 Optional 可用下面 unwrap 函數取得裡面的物件,但在 box 為 nil 時,會發生 crash 的狀況

func unwrap<String>(box:Optional<String>) -> String {
    switch box {
    case .none:
        fatalError("it is Nil !!!")
    case let .some(boxval):
        return boxval
    }
}

let tempbox1 = unwrap(box: "iphone")
//let tempbox2 = unwrap(box: nil)

在 swift 可用 ! 這個 syntax sugar 語法,其功能就跟 unwrap 一樣,遇到 nil 時,程式會 crash。

let tempbox3 = newbox2!
//let tempbox4 = newbox3!
  • if

先用 if 判斷一下 Optional 裡面有沒有東西,如果有就打開使用,如果沒有就不處理

if newbox2 != nil {
    print ("newbox2 is \(newbox2!)")
}

if newbox3 != nil {
    print ("newbox3 is \(newbox3!)")
}
  • if let

可簡化第二種方式的實作,直接將盒子裡的物件指定給另一個變數

if let pbox2 = newbox2 {
    print("pbox2 is = \(newbox2!)")
} else {
    print("pbox2 is nil")
}

if let pbox3 = newbox3 {
    print("pbox3 is = \(newbox3!)")
} else {
    print("pbox3 is nil")
}
  • Assigned Value if nil

用另一個變數來儲存物件 instance,但在打開 Optional 時,如果發現是 nil,就將新的變數初始化為空白的字串

let pbox5: String
if let pbox4 = newbox2 {
    pbox5 = pbox4
} else {
    pbox5 = ""
}

Optional Chaining ?.

Optioanl Chaining ?. 是 swift 的另一個 syntax sugar

[object]?.[property]?.[property | method]?.[method]
class Person {
    var residence: Residence?
}

class Residence {
    var numberOfRooms = 1
}

直接用 ! 打開 john 時,會造成程式 crash,但改用 ?.,可避免這樣的問題。

let john = Person()
//let roomCount = john.residence!.numberOfRooms
let roomCount = john.residence?.numberOfRooms

使用 ?. 可在打開 optional 時,進行 nil 的檢查及保護,搭配 if let 使用,就可以避免因為 nil 而造成程式 crash。

if let roomCount2 = john.residence?.numberOfRooms {
    print("John's residence has \(roomCount2) room(s).")
} else {
    print("Unable to retrieve the number of rooms.")
}

john.residence = Residence()

if let roomCount3 = john.residence?.numberOfRooms {
    print("John's residence has \(roomCount3) room(s).")
} else {
    print("Unable to retrieve the number of rooms.")
}

References

初學Swift:愛恨交織的 Optional

Swift的問號與驚嘆號:可有可無的 Optional

Swift Optional 變數的處理方法

[Swift] 第二章 - 可選(Optional)

2017年9月18日

Guice

Guice 是 google 推出的一個輕量級依賴注入框架,解決Java中的 Dependency Injection 依賴注入問題,這個功能就像是 Spring 的 DI IoC。但因為 Spring 的框架 scope 龐大,如果只是想要一個單純的 DI library,那麼 Guice 是一個很好的選擇。

JSR 330 DI

在 Spring 誕生後,Google 也提供了另一個 DI 的實作 Guice,後來在 2009 年,定義了 JSR 330 DI 的規範,隨後 Spring 與 Guice 也都支援了 JSR 330。

javax.inject.* 提供了依賴注入的定義類別,但沒有限制依賴配置方式,
依賴配置方式取決於注入器的實作,injector 可以有多種配置設定的方式,可以基於XML、annotation、DSL(Domain-specific language),甚至是Java代碼,在 injector 實作的部分,可以採用反射、代碼生成技術等等,不受限制。


@Inject

可在constructor、field、method上使用,也可以在static 的非 final 的field、method上使用。使用該註解標註的constructor、field、method訪問修飾符 (private、package- private、protected、public 中任意一種) 不受限制。Injector在進行注入時,要按照constructors、fiedls、methods的順序進行。

對被標註 @Inject 的constructor的要求:

  • 在滿足上述說明的情況下,可以有其他的依賴作為方法的參數,別的要求倒沒有什麼。

對被標註 @Inject 的field的要求:

  • 不能是final

對被標註 @Inject 的method的要求:

  • 方法不能是abstract
  • 可以有其他的依賴作為該方法的參數

  • 當一個方法標註了 @Inject 並覆寫了其他標註了 @Inject 的方法時,對於每一個實例的每一次注入請求,該方法只會被注入一次。

  • 當一個方法沒有標註 @Inject 並覆寫了其他標註了 @Inject 的方法時,該方法不會被注入。


@Qualifier

用於標記限定器 annotation,用來指定採用哪個 class

假設 class A 有兩個 subclass A1,A2。B 依賴了A,那麼DI容器在為 B 的實例注入 A 時到底該注入 A1 或 A2 呢?

class B {
    @Inject
    A a;
}

解決方式是在 A1 及 A2 分別寫上 Qualifier 標記

@A_1
public class A1{
}

@A_2
public class A2{
}

在 class B 中指定 A1 或是 A2

class B{
    @Inject
    @A_1
     A a;
}

也可以用 @Named 進行標記

@Named("A1")
Public class A1{
}

@Named("A2")
Public class A2{
}

class B{
    @Inject
    @Named("A1")
    A a;
}

@Scope @Singleton

@Scope 用在 class 上,用來告訴 injector,為該 class 建立多少個 instances。

@Singleton 就是指產生一個 instance。

Guice example in scala sbt project

在 build.sbt 中加上 guice library

libraryDependencies += "com.google.inject" % "guice" % "4.1.0"

定義兩個 Service 介面,分別有 UserServiceImpl 及 LogServiceImpl 實作。

trait UserService {
  def process(): Unit
}

class UserServiceImpl extends UserService {
  override def process(): Unit = {
    System.out.println("UserServiceImpl in process")
  }
}

trait LogService {
  def log(msg: String): Unit
}

class LogServiceImpl extends LogService {

  override def log(msg: String): Unit = {
    System.out.println("log message:" + msg)
  }
}

定義 Application 介面,在 MyApp 實作的 constructor 中,引用了 UserService 及 LogService,將來由 guice 動態指定 UserServiceImpl 及 LogServiceImpl 實作。

import javax.inject.Inject

trait Application {
  def work(): Unit
}

class MyApp @Inject()(val userService: UserService, val logService: LogService) extends Application {
  override def work(): Unit ={
    userService.process()
    logService.log("MyApp is working")
  }
}

Guice 的 Module 定義,必須要 extends AbstractModule,在 configure 中,設定 class 實作的 Denpendency 關係。

import com.google.inject.AbstractModule

class AppModule extends AbstractModule {
  override protected def configure(): Unit = {
    bind(classOf[UserService]).to(classOf[UserServiceImpl])
    bind(classOf[LogService]).to(classOf[LogServiceImpl])

    bind(classOf[Application]).to(classOf[MyApp])
  }
}

scala 主程式,以 Guice.createInjector(new AppModule) 產生 injector,藉由 inject 取得 Application 的 instance,然後就能呼叫 work。

import com.google.inject.Guice

object Main {
  def main(args: Array[String]) {
    println("Main")

    val injector = Guice.createInjector(new AppModule)

    val myApp = injector.getInstance(classOf[Application])

    myApp.work()
  }
}

執行結果

Main
UserServiceImpl in process
log message:MyApp is working

如果要限制 MyApp 為 Singleton,可以在 MyApp 上加上 @Singleton

import javax.inject.{Inject, Singleton}

trait Application {
  def work(): Unit
}

@Singleton
class MyApp @Inject()(val userService: UserService, val logService: LogService) extends Application {
  override def work(): Unit ={
    userService.process()
    logService.log("MyApp is working")
  }
}

也可在 Module 中設定 class dependency 的地方,加上 .in(classOf[Singleton]) 的限制

bind(classOf[Application]).to(classOf[MyApp]).in(classOf[Singleton])

或是寫成 asEagerSingleton,在程式啟動時,就馬上產生 MyApp

bind(classOf[Application]).to(classOf[MyApp]).asEagerSingleton

Spring vs Guice

關於選擇Spring還是Google-Guice的一些想法

SpringComparison

以往的 Spring 是使用 xml 的方式定義 java bean,一般會認為 Guice 處理速度比 Spring 快,但可能只在啟動的時候有差異,因為 spring 需要讀取 xml 設定檔,而 guice 完全都是用程式碼處理的。

Guice 是由 Google 的 AdWords 專案誕生的,他不像是 Spring 整合了許多不同的 Java EE Framework,只是單純且專注在處理 Dependency Injection 的問題。在官方 Spring Comparison 文件中提到一個例子,他是由 Spring 轉換到 Guice,發現大約有 3/4 的程式碼是不需要的,用 Guice 寫的 module 程式碼短,且容易閱讀。

Guice 不支援以設定檔的方式設定 DI,完全是以 annotations 及 generics 程式碼的方式處理,因此可以達成動態 DI 的功能。

References

Guice簡明教程

Guice 快速入門

Guice Getting Started

Google Guice的動機

Java 依賴注入標準(JSR-330)簡介

JSR330 DI

2017年9月11日

scala play from 2.5 to 2.6

scala play framework 專案如果要由 2.5 升級到 2.6,必須調整一些設定項目,另外 action composition 部分的程式也有更新的寫法,Lightbend activator 在 2017/5/24 已經退役,現在都要直接使用 sbt 編譯及封裝專案。

build.sbt

scala play 2.6 相關的 library 版本都要更新,scala 也要由 2.11 版改為 2.12,以下是 build.sbt

name := """project"""
organization := "tw.com.maxkit"

version := "0.1.0"

lazy val root = (project in file(".")).enablePlugins(PlayScala, JavaServerAppPackaging)

scalaVersion := "2.12.2"

scalacOptions ++= Seq("-encoding", "UTF-8")

libraryDependencies += guice

// Adds additional packages into Twirl
//TwirlKeys.templateImports += "tw.com.maxkit.controllers._"

// Adds additional packages into conf/routes
// play.sbt.routes.RoutesKeys.routesImport += "tw.com.maxkit.binders._"

libraryDependencies ++= Seq(
  ws,
  filters,
  "com.typesafe.play" %% "play-slick" % "3.0.0",
  "com.typesafe.play" %% "play-slick-evolutions" % "3.0.0",

  // slick
  "com.typesafe.slick" %% "slick" % "3.2.1",
  "org.slf4j" % "slf4j-nop" % "1.6.4",
  "com.typesafe.slick" %% "slick-hikaricp" % "3.2.1",

  // 讓 slick 支援 Timestamp 轉換 slick-joda-mapper https://github.com/tototoshi/slick-joda-mapper
  "com.github.tototoshi" %% "slick-joda-mapper" % "2.3.0",
  "joda-time" % "joda-time" % "2.7",
  "org.joda" % "joda-convert" % "1.7",

  // akka remoteing
  "com.typesafe.akka" % "akka-remote_2.12" % "2.5.3",

  // smtp email plugin
  // https://github.com/playframework/play-mailer
  "com.typesafe.play" %% "play-mailer" % "6.0.0",

  // mariadb java client
  //"mysql" % "mysql-connector-java" % "5.1.36",
  "org.mariadb.jdbc" % "mariadb-java-client" % "2.0.3",

  // 使用 FileUtils
  "commons-io" % "commons-io" % "2.5",

  // 使用 Base64
  "commons-codec" % "commons-codec" % "1.10",

  // object pool
  "commons-pool" % "commons-pool" % "1.6",

  // CLI parser library scopt https://github.com/scopt/scopt
  "com.github.scopt" % "scopt_2.12" % "3.6.0",

  // redis for Play https://github.com/KarelCemus/play-redis
  play.sbt.PlayImport.cacheApi,
  // include play-redis library
  "com.github.karelcemus" %% "play-redis" % "1.5.1",

  // Test Framework
  "org.scalatestplus.play" %% "scalatestplus-play" % "3.1.0" % Test,
  specs2 % Test
)

resolvers ++= Seq(
  "Typesafe repository" at "https://repo.typesafe.com/typesafe/releases/",
  "Typesafe Maven Repository" at "http://repo.typesafe.com/typesafe/maven-releases/",
  "Typesafe ivy" at "http://dl.bintray.com/typesafe/ivy-releases",
  "scalaz-bintray" at "https://dl.bintray.com/scalaz/releases"
)

fork in run := true

// production settings
maintainer := "service <service@maxkit.com.tw>"
packageSummary := "project"
packageDescription := """"""

project/plugins.sbt

resolvers += "Typesafe repository" at "http://repo.typesafe.com/typesafe/releases/"

addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.6.2")

project/build.properties

sbt.version=0.13.15

Action Composition

Actions Composition 2.6 官方文件

如果要在 controller 裡面每一個 method 都進行的登入的驗證,可以用 ActionComposition 的方式實作,但 2.6 版的 ActionComposition 已經調整為以下的做法。

首先獨立實作一個 AuthAction.scala

package controllers.admin

import java.sql.Timestamp
import java.util.Date
import javax.inject.Inject

import model.Usr
import play.api.{Environment, Logger, Mode}
import play.api.cache.redis.CacheApi
import play.api.mvc._
import utils.ServerConstants

import scala.concurrent.{ExecutionContext, Future}

class AuthRequest[A](val usr: Option[Usr], request: Request[A]) extends WrappedRequest[A](request)

class AuthAction @Inject()(cache: CacheApi,
                           env: Environment,
                           val parser: BodyParsers.Default)
                          (implicit val executionContext: ExecutionContext)
  extends ActionBuilder[AuthRequest, AnyContent] with ActionTransformer[Request, AuthRequest] {
  val cacheTimeout = ServerConstants.cacheTimeout

  def transform[A](request: Request[A]) = Future.successful {
    //    val now: Timestamp = new Timestamp(new Date().getTime)
    //    val usr: Usr = new Usr(0, "", "", "", now, "", now, "")
    //
    //    new AuthRequest(Some(usr), request)

    (request.session.get("key").flatMap { key =>
      cache.get[Usr](key)
    } map { usr =>

      // 需要再設定一次 cache,否則會發生 cache timeout
      cache.set(request.session.get("key").get, usr, cacheTimeout)
      new AuthRequest(Some(usr), request)

    }).orElse {

      env.mode match {
        case Mode.Dev | Mode.Test => {
          Logger.info("Mode.Dev don't check admin login status")

          val now: Timestamp = new Timestamp(new Date().getTime)
          val usr: Usr = new Usr(0, "", "devusr", "", now, "", now, "")
          Some( new AuthRequest(Some(usr), request) )
        }
        case Mode.Prod => {
          Some( new AuthRequest(None, request) )
        }
      }

    }.get
  }
}

在 controller 中,要用 injection 的方式將 AuthAction 引用進來。

@Singleton
class MyController @Inject()(
                               authAction: AuthAction,
                               actorSystem: ActorSystem, env: Environment,
                               implicit val executionContext: ExecutionContext,
                               cc: ControllerComponents) extends AbstractController(cc) {
    def listCdrs = authAction.async { request: Request[AnyContent] =>

        val body: AnyContent = request.body
        val formdata: Option[Map[String, Seq[String]]] = body.asFormUrlEncoded
        ........
    }
}

移除 Play.current

在 scala play 2.5 就已經不能用 Play.current,這裡記錄怎麼利用 injector 直接產生 instance。

在一般的 scala class 直接使用 database 的 model,已經不能用以下這種寫法,Play.current、DatabaseConfigProvider.get 都已經是 deprecated method。

val dbConfig = DatabaseConfigProvider.get[JdbcProfile](Play.current)

首先建立一個新的 GlobalContext

package modules

import play.api.inject.Injector
import javax.inject.{Inject,Singleton}

@Singleton
class GlobalContext @Inject()(playInjector: Injector) {
  GlobalContext.injectorRef = playInjector
}

object GlobalContext {
  private var injectorRef: Injector = _

  def injector: Injector = injectorRef
}

在自訂的 Guice Module 中,產生 GlobalContext

bind(classOf[GlobalContext]).asEagerSingleton()

然後就能直接使用 injector 產生 database Model

 val npRepo = GlobalContext.injector.instanceOf[NpRepo]

ref: How to access Play Framework 2.4 guice Injector in application?

Lightbend activator 在 2017/5/24 終止

因為 LIGHTBEND ACTIVATOR TEMPLATES 發布,未來已經不會再用 activator 進行 project template 的管理,要求大家改用 giter8 templates

以往用 activator 產生新的 project 的指令,都要用 sbt 取代。

根據 giter8 template 產生新的 project

sbt new playframework/play-scala-seed.g8

在過程中,要填寫 project name, organization 等資料

This template generates a Play Scala project

name [play-scala-seed]: projectname
organization [com.example]: tw.com.maxkit
scalatestplusplay_version [3.1.1]:
play_version [2.6.2]:

在 poject 中,以往使用 activator 的指令,都要改成 sbt

編譯

sbt compile

啟動 server

sbt run

封裝整個 project

sbt clean update compile stage dist

giter8

Giter8 是一個基於發佈在 Github 或任何 git 上的template來生成文件或目錄的命令行工具,它是以 Scala 實作並由 sbt launcher 運行。

除了一個官方的 giter8 project templates 集散地 之外,我們可以自己建立自己的 project template 並以 git 形式存放及分享在 git server 中。

References

Giter8 gitbook

Giter8

使用Scalatra創建Scala WEB工程

action-composition 2.5

2017年9月4日

monorepos

Monorepo 是一種管理企業代碼的方式,在這種方式下會摒棄原先一個 module 一個 git repo 的方式,而是把所有的 modules 都放在一個 repo 內來管理。單體倉庫 monorepos 是一個包含了多個獨立 project 的代碼倉庫,一個代碼 repository 包含一個單體倉庫。

目前有 Babel, React, Angular, Ember, Meteor 等等專案都使用了這個專案管理方式。

multirepos

當一個軟體專案隨著功能跟開發人員的擴增,漸漸地會發生共用程式碼的問題,一個大型的專案會拆分出多個小型 repos,每個 repo 代表了一個單獨的離散想法。

但也因為多個平行專案的發展,會發生一些問題:

  1. 架構孤島: 因為在一個整合專案中,拆分了功能並分配給不同的開發團隊處理及發展,每個團隊在不同的精進道路上,使用了不同的 library,好處是工程師可以根據自由選擇適當的 library,缺點是越來越多的 library,表示開發人員必須花更多時間學習不同的架構。

  2. 依賴地獄(Dependency Hell): 某個程式的修正,會影響到多少專案,在專案整合建構時,會需要很多時間找出整合的問題並提出修正。

  3. 建構耗時: 傳統的循序建構方式,會需要數十分鐘的時間,因應 monorepo 提供的新 build tool,可以平行建構加速建構的過程,也有更快的 incremental build 模式。

build tool

在 monorepo 中會搭配使用一個適當的 build tool,原因當然是為了在這樣的環境下,縮短建構的時間。

沒有萬靈丹

在一個專案建置初期,會因為開發維護的工程師人數不多,傾向於建置一個單一的專案進行開發,隨著應用本身的演進,公司的業務成長,會慢慢增加這個專案開發的參與人員數量,甚至會成長到有一個或多個開發 team。

在多人分工開發的環境下,就會面臨切割專案,抽取共用的函式庫的過程,一般的直覺,就是讓不同的專案模組有各自獨立負責的人員/團隊,也就是有各自獨立的版本演進,最後再整合在一起。

但如果分工是用垂直方式分工,以功能的方式分工,就有可能會造成程式碼的衝突,或是撰寫出來的程式運作的邏輯不同,或是使用了不同的輔助函式庫的問題。

這情況又有 monorepos 這樣的解決方案,想要解決這種問題。

應該說不管什麼樣的方案,都會有相對應的優點及缺點,那就要看每一個使用的情境來決定,該用什麼方式處理,讓優點多一點,缺點少一點,也沒有必要換來換去,畢竟轉換作業方式,加上適應的過程,也需要花上不少時間。

《三國演義》的第一句話就說:「話說天下大勢,分久必合,合久必分。」整體的局勢就是在分分合合中,不斷地來來往往反覆進行。

References

單體代碼倉庫:Uber的Android代碼倉庫演化史

Monorepos in Git

monorepo 新浪潮 | introduce lerna

多包存儲庫管理工具 Lerna

語意化版本 2.0.0

2017年8月28日

Firebase Cloud Messaging FCM

GCM 已經被 Firebase Cloud Messaging FCM 取代,以下記錄測試 FCM 的過程。

註冊 Firebase

firebase 的頁面登入 google 帳號,登入後點擊右上方的 "Console" 即可進入控制台,第一件事是建立Firebase 專案,進入主控台後,請按下「CREATE NEW PROJECT」建立一個新專案,專案名稱填 alarm,國家/地區 填台灣。

接下來建立一個 android 應用程式,套件名稱的部分,要跟 Android APP 一樣,我們填 tw.com.maxkit.alarm

建立 Android project

用 Android Studio 建立一個新的 project: Alarm,package name 為 tw.com.maxkit.alarm。

將剛剛新增的 firebase android 應用程式裡面下載的 "google-services.json" 這個檔案,放到 Alarm/app 這個目錄裡面。

修改以下的設定檔

  • Alarm/build.gradle

增加一行 classpath 'com.google.gms:google-services:3.1.0'

buildscript {
    repositories {
        jcenter()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:2.3.2'

        classpath 'com.google.gms:google-services:3.1.0'
    }
}

allprojects {
    repositories {
        jcenter()
    }
}

task clean(type: Delete) {
    delete rootProject.buildDir
}
  • Alarm/app/build.gradle

增加這三個部分

compile 'com.google.firebase:firebase-messaging:10.2.6'
compile 'com.firebase:firebase-jobdispatcher:0.5.2'

apply plugin: 'com.google.gms.google-services'

完整的內容

apply plugin: 'com.android.application'

android {
    compileSdkVersion 24
    buildToolsVersion "25.0.3"
    defaultConfig {
        applicationId "tw.com.maxkit.alarm"
        minSdkVersion 14
        targetSdkVersion 24
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
}

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
        exclude group: 'com.android.support', module: 'support-annotations'
    })
    compile 'com.android.support:appcompat-v7:24.2.1'
    compile 'com.android.support.constraint:constraint-layout:1.0.2'
    testCompile 'junit:junit:4.12'

    compile 'com.google.firebase:firebase-messaging:10.2.6'
    compile 'com.firebase:firebase-jobdispatcher:0.5.2'
}

apply plugin: 'com.google.gms.google-services'
  • Alarm/app/src/main/AndroidManifest.xml

增加兩個 meta-data,以及 MyFirebaseMessagingService 這個 service

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="tw.com.maxkit.alarm">

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <!-- 收到通知時, 到狀態列要顯示的 icon -->
        <meta-data
            android:name="com.google.firebase.messaging.default_notification_icon"
            android:resource="@drawable/ic_stat_announcement" />
        <!-- 收到通知的背景色 -->
        <meta-data
            android:name="com.google.firebase.messaging.default_notification_color"
            android:resource="@color/black" />

        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>

        <service
            android:name=".MyFirebaseMessagingService">
            <intent-filter>
                <action android:name="com.google.firebase.MESSAGING_EVENT"/>
            </intent-filter>
        </service>
    </application>

</manifest>

這裡用到了自訂的 color 跟 icon

Alarm/app/src/main/res/values/colors.xml

要增加 black

<color name="black">#00000000</color>
  • MainActivity.java

處理 notification 的部分,由系統列點擊 notification 會進入 MainActivity,在這裡取得該 notification 的資訊。

package tw.com.maxkit.alarm;

import android.content.Intent;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;

import com.google.firebase.messaging.FirebaseMessaging;

public class MainActivity extends AppCompatActivity {

    private static final String TAG = "MainActivity";

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        //這一行是要註冊 alarms 這個 topic,如果不需要,就把這行刪除
        FirebaseMessaging.getInstance().subscribeToTopic("alarms");

        Log.d(TAG, "onCreate ..");

        Intent intent = getIntent();
        String msg = intent.getStringExtra("msg");

        if (msg!=null)
            Log.d(TAG, "msg:"+msg);

        if (getIntent().getExtras() != null) {
            for (String key : getIntent().getExtras().keySet()) {
                Object value = getIntent().getExtras().get(key);
                Log.d(TAG, "Key: " + key + " Value: " + value);
            }
        }
    }

}
  • MyFirebaseMessagingService.java

處理訊息通知的 code,主要是 onMessageReceived

package tw.com.maxkit.alarm;

import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.media.RingtoneManager;
import android.net.Uri;
import android.support.v4.app.NotificationCompat;
import android.util.Log;
import com.google.firebase.messaging.FirebaseMessagingService;
import com.google.firebase.messaging.RemoteMessage;

public class MyFirebaseMessagingService extends FirebaseMessagingService{

    private static final String TAG = "MyFirebaseMsgService";

    @Override
    public void onMessageReceived(RemoteMessage remoteMessage) {
        Log.d(TAG, "onMessageReceived:"+remoteMessage+", from:"+remoteMessage.getFrom()+", data="+remoteMessage.getData());

        if (remoteMessage.getData().size() > 0) {
            Log.d(TAG, "Message data notifytitle: "+ remoteMessage.getData().get("notifytitle"));
            Log.d(TAG, "Message data notifybody: "+ remoteMessage.getData().get("notifybody"));
            Log.d(TAG, "Message data payload: " + remoteMessage.getData());
        }

        if (remoteMessage.getNotification() != null) {
            Log.d(TAG, "Message Notification title: "+ remoteMessage.getData().get("title"));
            Log.d(TAG, "Message Notification Body: " + remoteMessage.getNotification().getBody());
        }

        //Display notification in notification bar
        sendNotification(remoteMessage.getData().get("notifytitle"), remoteMessage.getData().get("notifybody"));
    }

    private void sendNotification(String notifytitle, String notifybody) {
        Intent intent = new Intent(this, MainActivity.class);

        intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
        PendingIntent pendingIntent = PendingIntent.getActivity(this, 0 /* Request code */, intent,
                PendingIntent.FLAG_ONE_SHOT);

        Uri defaultSoundUri=
                RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION);
        NotificationCompat.Builder notificationBuilder = new NotificationCompat.Builder(this)
                .setSmallIcon(R.drawable.ic_stat_access_alarms)
                .setContentTitle( notifytitle )
                .setContentText( notifybody )
                //.setAutoCancel(true)
                .setSound(defaultSoundUri)
                .setContentIntent(pendingIntent);

        NotificationManager notificationManager =
                (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
        notificationManager.notify(0 /* ID of notification */, notificationBuilder.build());
    }
}

用 Firebase 網頁測試

在左側點擊 Notifications 的部分,填寫以下的訊息,就會以 notification 的方式,收到訊息。意思就是說,當 APP 在背景時,會直接在系統列裡面收到 notification。

用 curl 測試

根據 Firebase Cloud Messaging HTTP Protocol 的說明,可以直接用 http 的方式發送訊息。

在專案設定 -> Cloud Messaging 的地方,可以找到 Authorization Key 伺服器金鑰

以下是 curl 的測試,"Authorization: key=" 後面要換成剛剛取得的 伺服器金鑰。

data 的部分可以參考 Firebase Cloud Messaging HTTP Protocol 的說明,以下我們設定了 to alarms 這個 topic,另外只傳送了 data 區塊,在 android APP 不管是前景或背景,都會在 onMessageReceived 收到訊息,我們可在那邊發送 local notification 把資料顯示在系統列上。

curl -X POST -H "Content-Type: application/json" \
    -H "Authorization: key=yourkey" \
    https://fcm.googleapis.com/fcm/send \
    -d '{"to":"/topics/alarms", "priority":"high", "data":{"notifytitle":"測試title", "notifybody":"測試body", "訊息 key": "訊息 Topic!", "key2": "value2"}}'

也可以加上 notification 的區塊,這部分就會跟在 Firebase 頁面測試的結果一樣,Android APP 會直接收到 notification。

curl -X POST -H "Content-Type: application/json" \
    -H "Authorization: key= yourkey" \
    https://fcm.googleapis.com/fcm/send \
    -d '{"to":"/topics/alarms", "priority":"high", "notification":{"title" : "title","icon" : "new","body" : "訊息 body"}, "data":{"訊息 key": "訊息 Topic!", "key2": "value2"}}'

References

Firebase 心得(Cloud Messaging)

在Android中使用2016新版Firebase加快開發過程(一)

Firebase雲端訊息-發送測試通知至Android APP中

Firebase Cloud Messaging (FCM) 入門到進階應用(1) --- 開發環境建立

【Android Studio】從 GCM 移植到 FCM

Android: 利用Firebase實作使用者間的產品分享機制

Send Messages to Multiple Devices

Firebase push Notification using curl command — Devoid Backend


for ios

Push Notification教學:如何使用Firebase在iOS實現推播功能

[教學] 實作Google Firebase的Notification 使用Objective-C

2017年8月21日

記錄用 CentOS 7 安裝 Redmine 3.3.3

因機器搬遷,需要把舊的 server 的資料,移到新機器上。

用 docker 測試

docker run -d -p 10022:22 -p 10080:80 centosssh /usr/sbin/sshd -D

如果遇到 Failed to get D-Bus connection: Operation not permitted 的問題,就改用這個方式啟動。因為需要 ssh 及 web 的 port,啟動時先對應好。

ref [原创] 解决 CentOS7 容器 Failed to get D-Bus connection: Operation not permitted

docker run -d -p 10022:22 -p 10080:80 -e "container=docker" --privileged=true -v /sys/fs/cgroup:/sys/fs/cgroup --name centos7test centosssh /usr/sbin/init

docker exec -it centos7test /bin/bash

安裝Apache、MariaDB、PHP

yum install -y httpd php mariadb mariadb-server mariadb-devel systemd which wget

設定MariaDB的DB為utf8

vi /etc/my.cnf.d/server.cnf

[mysqld]
character-set-server=utf8

vi /etc/my.cnf.d/client.cnf

[client]
default-character-set=utf8

啟動 MariaDB

systemctl start mariadb
mysql_secure_installation

建立 redmine 資料庫及帳號

mysql -u root -p

CREATE DATABASE redmine CHARACTER SET utf8;
CREATE USER 'redmine'@'localhost' IDENTIFIED BY 'password';
GRANT ALL PRIVILEGES ON redmine.* TO 'redmine'@'localhost';
flush privileges;
exit;

修改 MariaDB 密碼

mysqladmin -u root password 'password'

安裝 rvm

安裝RVM (Ruby管理工具)、Ruby、Rubygem

# Install Key
gpg --keyserver hkp://keys.gnupg.net --recv-keys 409B6B1796C275462A1703113804BB82D39DC0E3
#Install RVM
\curl -sSL https://get.rvm.io | bash -s stable

測試過 ruby 2.4.0 有些問題,所以安裝 ruby 限制在 2.2.4 版。

source /etc/profile.d/rvm.sh
rvm requirements
rvm install ruby 2.2.4
#rvm install rubygem
#gem install rails --no-rdoc --no-ri

不使用 yum 安裝 ruby (現在是 2.0 版)

#yum list ruby 
yum install -y gcc libxml2-devel

# 包含ruby/gem/libyaml
#yum install -y ruby ruby-devel

gem install bundler

gem install rake --no-document
gem i nokogiri --no-document -v='1.6.8'
gem i mime-types --no-document

# ruby 2 與 rails 5 不相容
gem install rails --no-document -v='4.2.7'

gem install rbpdf --no-document
gem install rbpdf-font --no-document

安裝passenger

yum install -y libcurl-devel httpd-devel apr-devel apr-util-devel

gem install passenger
passenger-install-apache2-module

安裝完成後,會出現module passenger的設定檔文字

vi /etc/httpd/conf.d/passenger.conf

LoadModule passenger_module /usr/local/rvm/gems/ruby-2.2.4/gems/passenger-5.1.4/buildout/apache2/mod_passenger.so
<IfModule mod_passenger.c>
     PassengerRoot /usr/local/rvm/gems/ruby-2.2.4/gems/passenger-5.1.4
     PassengerDefaultRuby /usr/local/rvm/gems/ruby-2.2.4/wrappers/ruby
</IfModule>

設定 redmine httpd virtual host

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

RailsEnv production
RailsBaseURI /redmine

<Directory /home/redmine/redmine-3.3.3/public>
  Options FollowSymlinks
  AllowOverride none
  Require all granted
</Directory>

restart httpd

systemctl restart httpd

安裝 redmine

cd /home

mkdir redmine
cd redmine

wget http://www.redmine.org/releases/redmine-3.3.3.tar.gz

tar -xf redmine-3.3.3.tar.gz

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

chown -R apache:apache redmine-3.3.3

設定 mysql

cd redmine-3.3.3/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
cd ..

bundle install --without development test rmagick

### 這個是生成redmine的什麼token,不生成的話瀏覽器會連接不上的,每次生成,以前的cookie內容就會失效
bundle exec rake generate_secret_token

如果有舊的 DB 要先 restore redmine db

mysql -uroot -p redmine < redmine_noplugins.sql

升級 DB schema

RAILS_ENV=production bundle exec rake db:migrate

有舊DB 就不需要 loaddefaultdata

# 生成數據庫對象

RAILS_ENV=production REDMINE_LANG=zh bundle exec rake redmine:load_default_data

可用 webrick 內建 web server 測試,也可以跳過不做

bundle exec rails server webrick -e production
wget http://localhost:3000
  > 測試安裝

為以後apache服務器對應(redmine/public目錄)做準備

cd public

cp htaccess.fcgi.example htaccess.fcgi
cp dispatch.fcgi.example dispatch.fcgi

設定 email

cd /home/redmine/redmine-3.3.3/config
cp configuration.yml.example configuration.yml
vi configuration.yml

修改 configuration.yml 前面的 email_delivery,要注意不能修改縮排的格式。

  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"

references

Installing Redmine

CentOS 7 安裝 Redmine

Centos 7安裝 redmine 3.X

最小化安装centos7.3 redmine3.3.3 passenger


Upgrading redmine

Redmine 2.6.3 to 3.0.1 upgrade

redmine_bak to git repository

2017年8月14日

以 docker 安裝一個可以遠端 ssh 登入的 centos 7 image

以下紀錄如何產生一個基本的 docker image,安裝了 openssh-server 可以用 ssh 遠端登入。

設定 docker image 以及 openssh-server

docker run -it --name c1 centos:latest /bin/bash

安裝一些基本工具,以及 openssh-server

#yum provides ifconfig

yum install -y net-tools telnet iptables sudo initscripts
yum install -y passwd openssl openssh-server

測試 sshd

/usr/sbin/sshd -D
Could not load host key: /etc/ssh/ssh_host_rsa_key
Could not load host key: /etc/ssh/ssh_host_ecdsa_key
Could not load host key: /etc/ssh/ssh_host_ed25519_key

缺少了一些 key

ssh-keygen -t rsa -f /etc/ssh/ssh_host_rsa_key
#直接 enter 即可

ssh-keygen -t dsa -f /etc/ssh/ssh_host_dsa_key
#直接 enter 即可

ssh-keygen -t ecdsa -f /etc/ssh/ssh_host_ecdsa_key -N ""

ssh-keygen -t ed25519 -f /etc/ssh/ssh_host_ed25519_key -N ""

修改 UsePAM 設定

vi /etc/ssh/sshd_config
# UsePAM yes 改成 UsePAM no
UsePAM no

再測試看看 sshd

/usr/sbin/sshd -D

修改 root 密碼

passwd root

離開 docker

exit

以 docker ps -l 找到剛剛那個 container 的 id

$ docker ps -l
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS                     PORTS               NAMES
107fb9c3fc0d        centos:latest       "/bin/bash"         7 minutes ago       Exited (0) 2 seconds ago                       c1

將 container 存成另一個新的 image

docker commit 107fb9c3fc0d centosssh

以新的 image 啟動另一個 docker instance

docker run -d -p 10022:22 centosssh /usr/sbin/sshd -D

現在可以直接 ssh 登入新的 docker machine

ssh root@localhost -p 10022

如果遇到 Failed to get D-Bus connection: Operation not permitted 的問題:ref [原创] 解决 CentOS7 容器 Failed to get D-Bus connection: Operation not permitted

docker run -d -p 10022:22 -e "container=docker" --privileged=true -v /sys/fs/cgroup:/sys/fs/cgroup --name centos7test centosssh /usr/sbin/init

docker exec -it centos7test /bin/bash

gitolite 測試

在新的 docker 機器上安裝 gitolite 測試

yum install -y autoconf git

useradd git
passwd git

產生管理員的 key

ssh-keygen

Generating public/private rsa key pair.
Enter file in which to save the key (/root/.ssh/id_rsa):
Created directory '/root/.ssh'.
Enter passphrase (empty for no passphrase):
Enter same passphrase again:
Your identification has been saved in /root/.ssh/id_rsa.
Your public key has been saved in /root/.ssh/id_rsa.pub.
The key fingerprint is:
01:93:46:03:17:6e:e2:06:ec:d6:07:db:2e:13:a3:92 root@1f01b0c5ad69
The key's randomart image is:
+--[ RSA 2048]----+
|    .oBo         |
| .   oo+         |
|  o o.o .        |
| . + *   .       |
|  o B o S        |
| o o =           |
|E . o .          |
| .   o           |
|                 |
+-----------------+
cp /root/.ssh/id_rsa.pub /home/git/admin.pub

以 scp 遠端測試 key

sshpass -p "password" scp -p -P 10022 git@localhost:/home/git/admin.pub .

在本機上安裝 gitolite

su - git

mkdir ~/bin

git clone git://github.com/sitaramc/gitolite

gitolite/install -ln ~/bin

把 admin.pub 放入 gitolite

gitolite setup -pk admin.pub

Initialized empty Git repository in /home/git/repositories/gitolite-admin.git/
Initialized empty Git repository in /home/git/repositories/testing.git/
WARNING: /home/git/.ssh missing; creating a new one
    (this is normal on a brand new install)
WARNING: /home/git/.ssh/authorized_keys missing; creating a new one
    (this is normal on a brand new install)

回到 root 身份

exit

以 git clone gitolite-admin 進行 local git 測試

mkdir test
cd test
git config --global user.email "charley@maxkit.com.tw"
git config --global user.name "charley"

git clone ssh://git@localhost/gitolite-admin

現在就可以利用 gitolite-admin 進行 git 帳號及 repo 維護

放入新的 user key: test.pub 放到 keydir 目錄中

git add keydir/test.pub

修改 conf/gitolite.conf

repo gitolite-admin
    RW+     =   admin
    RW+     =   test

repo testing
    RW+     =   admin
    RW+     =   test

將新的 test 增加到 gitolite-admin 裡面

git add keydir/test.pub
git add conf/gitolite.conf
git commit -m 'add test key'
git push origin master

也可以用遠端的方式存取 git

git clone ssh://git@localhost:10022/gitolite-admin

How to install Gitolite in CentOS 7

Linux 使用 Gitolite 架設 Git Server

使用Gitolite搭建Git服務器

gitolite basic administration

References

centos7中安裝一個可以ssh登陸的docker容器

Docker安裝SSH【Ubuntu、CentOS】

2017年8月7日

python tornado websocket server and client

tornado 是一個用Python語言寫成的Web服務器兼Web應用框架,以下記錄如何用 tornado framework 撰寫 websocket Echo Server & Client。

安裝 tornado

在 debian 安裝 python library

wget https://bootstrap.pypa.io/get-pip.py
sudo python get-pip.py

sudo pip install tornado

在 mac 安裝 tornado

sudo port install py27-tornado

Echo Server

# -*- coding: utf-8 -*-

import datetime
import sys
import tornado.httpserver
import tornado.websocket
import tornado.ioloop
import tornado.web

class WSHandler(tornado.websocket.WebSocketHandler):
    clients = []

    def check_origin(self, origin):
        return True

    def open(self):
        print "New client connected"
        #self.write_message("You are connected")
        WSHandler.clients.append(self)

    def on_message(self, message):
        self.write_message(message)

    def on_close(self):
        print "Client disconnected"
        WSHandler.clients.remove(self)

    @classmethod
    def write_to_clients(cls):
        print "Writing to clients"
        for client in cls.clients:
            client.write_message("Hi there!")

application = tornado.web.Application([
    (r"/", WSHandler),
])

if __name__ == "__main__":
    try:
        http_server = tornado.httpserver.HTTPServer(application)
        http_server.listen(9000)
        main_loop = tornado.ioloop.IOLoop.instance()

        # Schedule event (5 seconds from now)
        #main_loop.add_timeout(datetime.timedelta(seconds=5), WSHandler.write_to_clients)

        # background update every x seconds
        # 固定每 5 秒鐘就呼叫一次 WSHandler.write_to_clients 廣播訊息
        task = tornado.ioloop.PeriodicCallback(
                WSHandler.write_to_clients,
                5 * 1000)
        task.start()

        # Start main loop
        #main_loop.start()
        main_loop.make_current()
    except KeyboardInterrupt:
        #print("KeyboardInterrupt")
        sys.exit()

EchoServer in aother Thread

將 Server 放在另一個 Thread 啟動,保留 main thread 用在其他的用途上。

# -*- coding: utf-8 -*-

import datetime
import sys
import tornado.httpserver
import tornado.websocket
import tornado.ioloop
import tornado.web
import os
from threading import Thread

class WSHandler(tornado.websocket.WebSocketHandler):
    clients = []

    def check_origin(self, origin):
        return True

    def open(self):
        print "New client connected"
        #self.write_message("You are connected")
        WSHandler.clients.append(self)

    def on_message(self, message):
        self.write_message(message)

    def on_close(self):
        print "Client disconnected"
        WSHandler.clients.remove(self)

    @classmethod
    def write_to_clients(cls):
        print "Writing to clients"
        for client in cls.clients:
            client.write_message("Hi there!")

class WebThread(Thread):
    def __init__(self):
        Thread.__init__(self, name='WebThread')

    def run(self):
        curdir = os.path.dirname(os.path.realpath(__file__))

        application = tornado.web.Application([
            (r"/", WSHandler),
        ])

        http_server = tornado.httpserver.HTTPServer(application)
        http_server.listen(9000)
        main_loop = tornado.ioloop.IOLoop.instance()

        # Schedule event (5 seconds from now)
        #main_loop.add_timeout(datetime.timedelta(seconds=5), WSHandler.write_to_clients)

        # background update every x seconds
        # 固定每 5 秒鐘就呼叫一次 WSHandler.write_to_clients 廣播訊息
        task = tornado.ioloop.PeriodicCallback(
                WSHandler.write_to_clients,
                5 * 1000)
        task.start()

        main_loop.start()


if __name__ == "__main__":
    try:
        webThread = WebThread()
        webThread.daemon = True
        webThread.start()

        while True:
            pass

    except KeyboardInterrupt:
        #print("KeyboardInterrupt")
        sys.exit()

EchoClient.html


<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.12.1/jquery.min.js"></script>
<script>

$(document).ready(function(){
    var socket = new WebSocket('ws://127.0.0.1:9000/');

    socket.onopen = function(event){
        socket.send('Hi');
    }

    socket.onmessage = function(event){
        console.log(event.data);
    };

    $(window).unload(function(event){
        socket.close();
    });
});

</script>

Echo Client with tornado framework

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import sys

from tornado.ioloop import IOLoop, PeriodicCallback
from tornado import gen
from tornado.websocket import websocket_connect


class Client(object):
    def __init__(self, url, timeout):
        self.url = url
        self.timeout = timeout
        self.ioloop = IOLoop.instance()
        self.ws = None
        self.connect()

        # 每 20 秒發送一次 ping
        PeriodicCallback(self.keep_alive, 20000, io_loop=self.ioloop).start()

        self.ioloop.start()

    @gen.coroutine
    def connect(self):
        print "trying to connect"
        try:
            self.ws = yield websocket_connect(self.url)
        except Exception, e:
            print "connection error"
        else:
            print "connected"
            self.run()

    @gen.coroutine
    def run(self):
        while True:
            msg = yield self.ws.read_message()
            if msg is None:
                print "connection closed"
                self.ws = None
                break
            else:
                print msg

    def keep_alive(self):
        if self.ws is None:
            self.connect()
        else:
            self.ws.write_message("ping")

if __name__ == "__main__":
    try:
        client = Client("ws://localhost:9000", 5)
    except KeyboardInterrupt:
        #print("KeyboardInterrupt")
        sys.exit()

References

SIMPLE WEB SOCKET CLIENT IMPLEMENTATION USING TORNADO FRAMEWORK.

tornado-websocket-client-example/client.py

2017年7月31日

用 socket 將 OpenCV 影像傳送到遠端 client

camera 影像以 socket 傳送到 client 的測試,目前是用 socket,將來還要改成用 websocket 處理,用以接受多個 client 連線的問題。

numpy

numpy 可以快速地將 bytearray 及 martix 進行轉換,透過這邊的程式碼,我們可以了解到,圖片就是一個二維陣列的矩陣,矩陣中每一個點,代表圖片中的一個像素點,而 OpenCV 是以 BGR 的形式儲存像素點的資料。

#!/usr/bin/python
# coding=utf-8

import cv2
import numpy
import os
import time

# 亂數產生 120000 個 bytes, 轉換為 numpy array
randomByteArray = bytearray(os.urandom(120000))
flatNumpyArray = numpy.array(randomByteArray)

# reshape 成 300x400 的矩陣並存成 gray scale 圖片
grayImage = flatNumpyArray.reshape(300,400)
cv2.imwrite('RandomGray.png', grayImage)

# reshape 成 400x100 的矩陣並存成 BGR 圖片
bgrImage = flatNumpyArray.reshape(100,400, 3)
cv2.imwrite('RandomBGR.png', bgrImage)

camera 影像以 socket 傳送到 client

  • 版本1

server.py 等待 client.py 連接,server 接收 client 的 camera 資料

server.py

import socket
import cv2
import numpy

def recvall(sock, count):
    buf = b''
    while count:
        newbuf = sock.recv(count)
        if not newbuf: return None
        buf += newbuf
        count -= len(newbuf)
    return buf

TCP_IP = "192.168.1.152"
TCP_PORT = 8002
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.bind((TCP_IP, TCP_PORT))
s.listen(True)
conn, addr = s.accept()
while 1:
    length = recvall(conn,16)
    stringData = recvall(conn, int(length))
    data = numpy.fromstring(stringData, dtype='uint8')
    decimg=cv2.imdecode(data,1)
    cv2.imshow('SERVER',decimg)
    cv2.waitKey(30)

s.close()
cv2.destroyAllWindows()

client.py

import socket
import cv2
import numpy

TCP_IP = "192.168.1.152"
TCP_PORT = 8002

sock = socket.socket()
capture = cv2.VideoCapture(0)
ret, frame = capture.read()
sock.connect((TCP_IP, TCP_PORT))
encode_param=[int(cv2.IMWRITE_JPEG_QUALITY),90]
while ret:
    result, imgencode = cv2.imencode('.jpg', frame, encode_param)
    data = numpy.array(imgencode)
    stringData = data.tostring()
    sock.send( str(len(stringData)).ljust(16));
    sock.send( stringData );

    ret, frame = capture.read()
    decimg=cv2.imdecode(data,1)
    cv2.imshow('CLIENT',decimg)
    cv2.waitKey(30)

sock.close()
cv2.destroyAllWindows()
  • 版本2

server.py 等待 client.py 連接,client 接收 server 的 camera 資料,顯示在畫面上

server2.py

import socket
import cv2
import numpy

capture = cv2.VideoCapture(0)

TCP_IP = "192.168.1.159"
TCP_PORT = 8002
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.bind((TCP_IP, TCP_PORT))
s.listen(True)

conn, addr = s.accept()

ret, frame = capture.read()
encode_param=[int(cv2.IMWRITE_JPEG_QUALITY),90]

while ret:
    result, imgencode = cv2.imencode('.jpg', frame, encode_param)
    data = numpy.array(imgencode)
    stringData = data.tostring()

    conn.send( str(len(stringData)).ljust(16));
    conn.send( stringData );

    ret, frame = capture.read()
    decimg=cv2.imdecode(data,1)
    cv2.imshow('SERVER2',decimg)
    cv2.waitKey(30)

conn.close()
cv2.destroyAllWindows()

client2.py

import socket
import cv2
import numpy

def recvall(sock, count):
    buf = b''
    while count:
        newbuf = sock.recv(count)
        if not newbuf: return None
        buf += newbuf
        count -= len(newbuf)
    return buf

TCP_IP = "192.168.1.159"
TCP_PORT = 8002

sock = socket.socket()
sock.connect((TCP_IP, TCP_PORT))

while 1:
    length = recvall(sock,16)
    stringData = recvall(sock, int(length))
    data = numpy.fromstring(stringData, dtype='uint8')
    decimg=cv2.imdecode(data,1)
    cv2.imshow('CLIENT2',decimg)
    cv2.waitKey(1)

sock.close()
cv2.destroyAllWindows()

改用 wxPython 作為 GUI

wxWidget 是一個開放原始碼且跨平台的物件工具集(widget toolkit),可用來建立基本的圖形使用者介面,先前測試時,都是以 cv2.imshow 進行畫面 preview,未來為了要製作更複雜的 GUI,所以先測試將畫面改為利用 wxPython 處理。

# mac 上要安裝 py27-wxpython
sudo port install py27-wxpython-3.0
# -*- coding: utf-8 -*-
import wx
import cv2
import time

class TestOpenCV ( wx.Frame ):
    windowWidth = 500
    windowHeight = 320

    def __init__( self, parent=None ):
        wx.Frame.__init__ ( self, parent, id = wx.ID_ANY, title = u"單一視訊畫面", pos = wx.DefaultPosition, size = wx.Size( self.windowWidth, self.windowHeight), style = wx.DEFAULT_FRAME_STYLE|wx.TAB_TRAVERSAL )

        self.SetSizeHintsSz( wx.DefaultSize, wx.DefaultSize )

        bSizer1 = wx.BoxSizer( wx.VERTICAL )

        self.stbmp1 = wx.StaticBitmap( self, wx.ID_ANY, wx.NullBitmap, wx.DefaultPosition, wx.DefaultSize, 0 )
        bSizer1.Add( self.stbmp1, 1, wx.ALL|wx.EXPAND, 5 )
##        self.stbmp1.SetBitmap(wx.Bitmap( u"../image/heats1-f1-02_gray_pressed.png", wx.BITMAP_TYPE_ANY ))

        self.SetSizer( bSizer1 )
        self.Layout()

        self.Centre( wx.BOTH )

    def __del__( self ):
        pass

    def scale_bitmap(self, bitmap, width, height):
        image = wx.ImageFromBitmap(bitmap)
        image = image.Scale(width, height, wx.IMAGE_QUALITY_NORMAL)
        newimg = wx.BitmapFromImage(image)
        return newimg

    def updateImage(self, bitmap):
        # 縮小圖片符合視窗的大小
        newbitmap = self.scale_bitmap(bitmap, self.windowWidth-10, self.windowHeight-30)
        self.stbmp1.SetBitmap(newbitmap)

class App(wx.App):
    """Application class."""

    def OnInit(self):
        self.frame = TestOpenCV()
        self.frame.Show()
        self.SetTopWindow(self.frame)
        self.run()
        return True

    def rot90(self, img, angle):
        if(angle == 270 or angle == -90):
            img = cv2.transpose(img)
            img = cv2.flip(img, 0)  # transpose+flip(0)=CCW
        elif (angle == 180 or angle == -180):
            img = cv2.flip(img, -1)  # transpose+flip(-1)=180
        elif (angle == 90 or angle == -270):
            img = cv2.transpose(img)
            img = cv2.flip(img, 1)  # transpose+flip(1)=CW
        elif (angle == 360 or angle == 0 or angle == -360):
            pass
        else :
            raise Exception("Unknown rotation angle({})".format(angle))
        return img

    def run(self):

        cap = cv2.VideoCapture(0);

        while True:
            ret, frame = cap.read()

            if ret == True:
                # 畫面旋轉 90度
                srcBGR = self.rot90(frame, -90)

                # wxPython 只能處理 RGB 的圖片,要從 BGR 轉 RGB
                srcRGB = cv2.cvtColor(srcBGR, cv2.COLOR_BGR2RGB)

                #print dst.shape  w=720, h=1280
                w, h = srcRGB.shape[:2]

                #dst = cv2.resize(srcRGB, (h/2,w/2), interpolation = cv2.INTER_AREA)
                #wxImage = wx.ImageFromBuffer(h/2, w/2, dst)
                wxImage = wx.ImageFromBuffer(h, w, srcRGB)
                bitmap = wx.BitmapFromImage(wxImage)

                # 更新 視窗上的圖片
                self.frame.updateImage(bitmap)

                #cv2.imshow('frame', dst)
                #if cv2.waitKey(30) & 0xFF == ord('q'):
                #    break

                # sleep 30ms
                time.sleep(0.03)

            else:
                break

        cap.release()
        cv2.destroyAllWindows()


def main():
    app = App()
    app.MainLoop()

if __name__ == '__main__':
    main()

2017年7月24日

安裝 OpenCV 3

OpenCV Open Source Computer Vision Library 是一個跨平台的電腦視覺庫,由 Intel 發起並參與開發,由於 Intel 為了推動需要更高速運算的應用,增加硬體的銷售,因此發展了這個機器視覺的運算函式庫,以BSD授權條款授權發行,可以在商業和研究領域中免費使用。OpenCV可用於開發 real time 的圖像處理、電腦視覺以及特徵識別程式。

OpenCV 雖然是以 C++ 寫成,但同時提供了 python 及 java 的 bindings,也因為 python,我們可以用更短的程式碼就完成一些基本的視覺應用,以下紀錄如何在 RPi 3 以及 MacOS 中安裝 OpenCV。

Raspberry Pi 3

RPi 的 camera 是使用 CSI 介面,參考 Raspberry Pi相機模組開箱文 將 RPi 的 camera 裝好。

以 raspi-config 指令 enable camera,然後 reboot。

sudo raspi-config

vcgencmd 是用來查詢一些系統參數的指令,可以用 vcgencmd 測試 camera 狀態。

$ vcgencmd get_camera
supported=1 detected=1
vcgencmd version
vcgencmd get_mem arm
vcgencmd get_mem gpu

安裝 OpenCV 3,必須先更新 RPi,安裝一些基本的工具及 library

sudo apt-get update
sudo apt-get upgrade
sudo apt-get -y install build-essential cmake pkg-config
sudo apt-get -y install cmake-curses-gui htop swig

# load various image file formats from disk
sudo apt-get -y install libjpeg-dev libtiff5-dev libjasper-dev libpng12-dev

#sudo apt-get -y install build-essential cmake cmake-curses-gui pkg-config libpng12-0 libpng12-dev libpng++-dev libpng3 libpnglite-dev zlib1g-dbg zlib1g zlib1g-dev pngtools libtiff5-dev libtiff5 libtiffxx0c2 libtiff-tools libeigen3-dev

#sudo apt-get -y install libjpeg8 libjpeg8-dev libjpeg8-dbg libjpeg-progs swig libv4l-0 libv4l-dev python-numpy

# read various video file formats from disk
sudo apt-get -y install libavcodec-dev libavformat-dev libswscale-dev libv4l-dev
sudo apt-get -y install libxvidcore-dev libx264-dev

# for module: highgui
sudo apt-get -y install libgtk2.0-dev

# matrix operations
sudo apt-get -y install libatlas-base-dev gfortran

# python
sudo apt-get -y install python2.7-dev python3-dev python-numpy python3-numpy

sudo apt-get -y install doxygen

如果需要 tesseract,不安裝也沒關係,可以直接用 pytesseract:

sudo apt-get install -y tesseract-ocr libtesseract-dev libleptonica-dev

安裝 python library

wget https://bootstrap.pypa.io/get-pip.py
sudo python get-pip.py

# for numerical processing
sudo pip install numpy

下載並安裝 OpenCV 3.1

wget -O opencv-3.1.0.zip https://github.com/Itseez/opencv/archive/3.1.0.zip

wget -O opencv_contrib-3.1.0.zip https://github.com/opencv/opencv_contrib/archive/3.1.0.zip

unzip opencv-3.1.0.zip
unzip opencv_contrib-3.1.0.zip

因為 Opencv + Tesseract 編譯會發生問題,所以要把 OpenCV 偵測 Tesseract library 的部分關閉,未來直接用 pytesseract 就可以做 OCR,不用透過 OpenCV 呼叫 tesseract。

#修改 /opt/opencv_contrib-3.1.0/modules/text/FindTesseract.cmake

#增加最後一行
set(Tesseract_FOUND 0)

編譯 opencv

cd opencv-3.1.0/
mkdir build
cd build

# setup build with cmake 或是以 ccmake ../ 用介面設定 build
cmake -D CMAKE_BUILD_TYPE=RELEASE \
    -D CMAKE_INSTALL_PREFIX=/usr/local \
    -D INSTALL_C_EXAMPLES=OFF \
    -D INSTALL_PYTHON_EXAMPLES=ON \
    -D OPENCV_EXTRA_MODULES_PATH=~/opencv/opencv_contrib-3.1.0/modules \
    -D BUILD_EXAMPLES=ON ..

如果不直接用 cmake,也可以用 ccmake gui 設定 cmake 的選項

ccmake ../

# 按 c 產生全新設定,決定細項設定後,修改完後,按 c 設定新的選項,然後再按下 g 即可產生編譯用的設定檔案。

如果 cmake 時會出現這個錯誤

CMake Error at samples/gpu/CMakeLists.txt:100 (list):
  list sub-command REMOVE_ITEM requires list to be present.

參考這裡的解法,要將 INSTALLCEXAMPLES=OFF 設定為 OFF。

# 這個步驟會要做很久,大概是 80 分鐘,如果一直發生問題,就改用 make 去掉 -j4,但會需要 3~4 hrs
# 如果 make -j4 出現 error,可以再用 make -j4 或是改用 make -j2 多試幾次看看,如果錯誤沒有出現在同一個地方,,可以這樣繼續編譯
make -j2

也可以用這樣的方式重複 10 次 make

for i in {1..10}; do make -j2; done

或是用 shell script

#!/bin/bash
for i in {1..10}
do
    make -j2
    if [ $? -ne 0 ]; then
        echo "Try again";
    else
        break;
    fi
done
sudo make install
sudo ldconfig

在 python 的 dist-packages 目錄中,看到 cv2.so 就成功了。

ls -l /usr/local/lib/python2.7/dist-packages/

以 python 測試 cv2

$ python
>>> import cv2
>>> cv2.__version__
'3.1.0'

測試拍照及錄影

raspistill -o t.jpg
raspivid -o t.h264

要讓 OpenCV 使用 camera 必須安裝V4L2套件,在 RPi 必須要先載入 video driver for cv video capture method,camera 的程式才會有作用。

可以直接編譯

cd ~/opencv/
wget http://linuxtv.org/downloads/v4l-utils/v4l-utils-1.6.2.tar.bz2
tar xfvj v4l-utils-1.6.2.tar.bz2

sudo apt-get -y install autoconf gettext libtool libjpeg-dev

cd v4l-utils-1.6.2
autoreconf -vfi
./configure
make
sudo make install

或是用這樣的方式安裝

# 增加sources.list
$ sudo vim /etc/apt/sources.list
於sources.list中寫入以下資訊
deb http://www.linux-projects.org/listing/uv4l_repo/raspbian/ wheezy main

# 加入GPG key
$ sudo wget http://www.linux-projects.org/listing/uv4l_repo/lrkey.asc ~/
$ sudo apt-key add ./lrkey.asc

# 再更新一次系統
$ sudo apt-get update && sudo apt-get upgrade

# 安裝V4L2套件
$ sudo apt-get install uv4l uv4l-raspicam
sudo modprobe bcm2835-v4l2

在modules文件中寫入以下資訊,下次開機就會自動載入這個 module

$ sudo vim /etc/modules
bcm2835-v4l2

測試 /dev/video0

v4l2-ctl --list-ctrls --device /dev/video0

Mac OS El Caption

用類似 RPi 的方式編譯,因為 OpenCV 3.2 會遇到 freetype 編譯錯誤,還不知道怎麼解決,目前只能使用 OpenCV 3.1。另外 python 整合的部分也沒有裝好。所以就不用這樣的方式,改用 macport 直接安裝 opencv +python27。

#安裝 macport, xcode

sudo xcodebuild -license
sudo port install cmake +gui

sudo port install libgphoto2
sudo port install jpeg libpng tiff openexr
sudo port install eigen tbb eigen3
sudo port install py27-numpy

unzip opencv-3.1.0.zip
unzip opencv_contrib-3.1.0.zip

cd opencv-3.1.0
mkdir build
cd build

cmake -D CMAKE_BUILD_TYPE=Release \
    -D CMAKE_INSTALL_PREFIX=/usr/local \
    -D OPENCV_EXTRA_MODULES_PATH=~/project/opencv/opencv_contrib-3.1.0/modules \
    -D BUILD_opencv_python2=ON \
    -D BUILD_opencv_python3=OFF \
    -D INSTALL_PYTHON_EXAMPLES=ON \
    -D INSTALL_C_EXAMPLES=OFF \
    -D BUILD_EXAMPLES=ON \
    -D WITH_EIGEN=OFF \
    -D PYTHON2_PACKAGES_PATH=/opt/local/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/site-packages \
    -D PYTHON2_LIBRARY=/opt/local/Library/Frameworks/Python.framework/Versions/2.7/bin \
    -D PYTHON2_INCLUDE_DIR=/opt/local/Library/Frameworks/Python.framework/Versions/2.7/include/python2.7/ \
    ..

make -j4

sudo make install

改用 macport 直接安裝的方式

# 安裝 xcode
xcode-select -install

sudo port selfupdate

sudo port install py27-tkinter
sudo port install cmake +gui
sudo port install py27-scipy

# 設定 python
sudo port install python_select
sudo port slect python python27

# 編譯 opencv
sudo port install opencv +python27 +openni

可以在編譯 opencv 的 Porfile 中查看一些資料,目前是用 3.1.0 版,也已經包含了 opencv_contrib modules,如果真的需要調整編譯的過程,可以設定 macport 的 local repository,複製這個 Portfile,修改編譯的過程。

/opt/local/var/macports/sources/rsync.macports.org/release/tarballs/ports/graphics/opencv/Portfile

測試 1: 打開 1.jpg 顯示在視窗畫面中

test.cpp: 打開 1.jpg 顯示在視窗畫面中

#include <opencv2/opencv.hpp>
using namespace cv;

int main()
{
        Mat img=imread("1.jpg");
        imshow("result",img);
        // 6s後視窗自動關閉
        waitKey(6000);
}

編譯與執行

# 在 mac 編譯
g++ -ggdb `pkg-config --cflags --libs opencv` -stdlib=libc++ test.cpp -o test

# 在 RPi 編譯
g++ -ggdb `pkg-config --cflags --libs opencv` test.cpp -o test

./test

一樣的程式碼,改用 python 寫:test.py

#!/usr/bin/python
# coding=utf-8

import cv2
import time

img = cv2.imread('1.jpg');
cv2.imshow("result",img);
cv2.waitKey()

直接用 python 就可以執行,在 Mac 及 RPi 都一樣。

python test.py

測試 2: 載入圖片, 並用 MORPH_RECT 腐蝕操作

用圖片中暗色的部分,腐蝕高亮的部分。

test2.cpp

#include <opencv2/highgui/highgui.hpp>
#include <opencv2/imgproc/imgproc.hpp>

using namespace cv;

int main(   )
{
    //載入原圖
    Mat srcImage = imread("1.jpg");
    //顯示原圖
    imshow("source", srcImage);
    //進行腐蝕操作
    Mat element = getStructuringElement(MORPH_RECT, Size(15, 15));
    Mat dstImage;
    erode(srcImage, dstImage, element);
    //顯示效果圖
    imshow("result", dstImage);
    waitKey(0);

    return 0;
}

編譯+執行

# 在 mac 編譯
g++ -ggdb `pkg-config --cflags --libs opencv` -stdlib=libc++  test2.cpp -o test2

# 在 RPi 編譯
g++ -ggdb `pkg-config --cflags --libs opencv` test2.cpp -o test2

./test2

test2.py

#!/usr/bin/python
# coding=utf-8

import cv2
import time

# 中文字型
from matplotlib.font_manager import FontProperties
font = FontProperties(fname=r"SimSun.ttc", size=14)

img = cv2.imread('1.jpg');
# grayscale image
#img = cv2.imread('1.jpg', 0);
#img = cv2.imread('1.jpg', cv2.IMREAD_GRAYSCALE)

kernel = cv2.getStructuringElement(cv2.MORPH_RECT,(5,5))
#腐蝕圖像  
eroded = cv2.erode(img,kernel)

#顯示腐蝕後的圖片
cv2.imshow("result", eroded);
cv2.waitKey(0)
cv2.destroyAllWindows()

編譯+執行

python test2.py

測試 3: blur 影像模糊

test3.cpp

#include "opencv2/highgui/highgui.hpp"
#include "opencv2/imgproc/imgproc.hpp"
using namespace cv;

int main( )
{
    Mat srcImage=imread("1.jpg");
    imshow( "source", srcImage );

    Mat dstImage;
    blur( srcImage, dstImage, Size(7, 7));

    imshow( "result" ,dstImage );

    waitKey( 0 );
}

編譯+執行

# 在 mac 編譯
g++ -ggdb `pkg-config --cflags --libs opencv` -stdlib=libc++ test3.cpp -o test3
./test3

# 在 RPi 編譯
g++ -ggdb `pkg-config --cflags --libs opencv` test3.cpp -o test3

./test3

tes3.py

#!/usr/bin/python
# coding=utf-8

import cv2
import time

img = cv2.imread('1.jpg');

blur = cv2.blur(img,(7,7))

cv2.imshow("result", blur);
cv2.waitKey(0)
cv2.destroyAllWindows()

測試 4: canny 邊緣檢測

test4.cpp

#include <opencv2/opencv.hpp>
#include<opencv2/imgproc/imgproc.hpp>
using namespace cv;

int main( )
{
    Mat srcImage = imread("1.jpg");
    imshow("source", srcImage);     //顯示原始圖
    Mat dstImage,edge,grayImage;    //參數定義

    //建立與src同類別型和大小的矩陣(dst)
    dstImage.create( srcImage.size(), srcImage.type() );

    //將原圖像轉換為灰度圖像
    //此句程式碼的OpenCV2版為:
    //cvtColor( srcImage, grayImage, CV_BGR2GRAY );
    //此句程式碼的OpenCV3版為:
    cvtColor( srcImage, grayImage, COLOR_BGR2GRAY );

    // 使用 3x3核心來降噪
    blur( grayImage, edge, Size(3,3) );

    // 執行Canny算子
    Canny( edge, edge, 3, 9,3 );

    imshow("result", edge);

    waitKey(0);

    return 0;
}

編譯+執行

# 在 mac 編譯
g++ -ggdb `pkg-config --cflags --libs opencv` -stdlib=libc++ test4.cpp -o test4
./test4

# 在 RPi 編譯
g++ -ggdb `pkg-config --cflags --libs opencv` test4.cpp -o test4

./test4

test4.py

#!/usr/bin/python
# coding=utf-8

import cv2
import time

# grayscale image
img = cv2.imread('1.jpg', 0);

# 用高斯平滑處理原圖像降噪
img = cv2.GaussianBlur(img,(3,3),0)
# Canny只能處理 grayscale image, 指定最大和最小閾值,其中apertureSize默認為3
canny = cv2.Canny(img, 3, 9)

cv2.imshow("result", canny);
cv2.waitKey(0)
cv2.destroyAllWindows()

test5: 播放 avi 影片

test5.cpp

#include <opencv2/opencv.hpp>
using namespace cv;

//-----------------------------------【main( )函數】--------------------------------------------
//      描述:控制臺應用程式的入口函數,我們的程式從這里開始
//-------------------------------------------------------------------------------------------------
int main( )
{
    VideoCapture capture("1.avi");

    while(1)
    {
        Mat frame;//定義一個Mat變數, 儲存現在的 frame
        capture>>frame;  //讀取現在的 frame
        if (!frame.empty()) {
            imshow("result",frame);
        } else {
            break;
        }
        waitKey(30);  // delay 30ms
    }
    return 0;
}

編譯+執行

# 在 mac 編譯
g++ -ggdb `pkg-config --cflags --libs opencv` -stdlib=libc++ test5.cpp -o test5
./test5

# 在 RPi 編譯
g++ -ggdb `pkg-config --cflags --libs opencv` test5.cpp -o test5

./test5

test5.py

#!/usr/bin/python
# coding=utf-8

import cv2
import time

cap = cv2.VideoCapture('1.avi');

while True:
    ret, frame = cap.read()

    if ret == True:
        ## grayscale avi
        #gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
        #cv2.imshow('frame', gray)

        ## normal avi
        cv2.imshow('frame', frame)

        if cv2.waitKey(30) & 0xFF == ord('q'):
            break

    else:
        break

cap.release()
cv2.destroyAllWindows()

test6: 使用 camera

test6.cpp,只有將 VideoCapture capture(0); 換成 camera index 就可以了。

#include <opencv2/opencv.hpp>
using namespace cv;

int main( )
{
    VideoCapture capture(0);
    while(1)
    {
        Mat frame, dst;
        capture>>frame;

        if (!frame.empty()) {
            imshow("result", frame);
        } else {
            break;
        }
        waitKey(30);  //delay 30ms
    }
    return 0;
}

編譯+執行

# 在 mac 編譯
g++ -ggdb `pkg-config --cflags --libs opencv` -stdlib=libc++ test6.cpp -o test6
./test6

# 在 RPi 編譯
g++ -ggdb `pkg-config --cflags --libs opencv` test6.cpp -o test6

./test6

test6.py

#!/usr/bin/python
# coding=utf-8

import cv2
import time

cap = cv2.VideoCapture(0);

while True:
    ret, frame = cap.read()

    if ret == True:
        ## grayscale avi
        #gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
        #cv2.imshow('frame', gray)

        ## normal avi
        cv2.imshow('frame', frame)

        if cv2.waitKey(30) & 0xFF == ord('q'):
            break

    else:
        break

cap.release()
cv2.destroyAllWindows()

test7: 使用 camera 加上畫面旋轉

上面的 test6 在 mac 的 camera 角度不對,畫面必須逆時針旋轉一下,才回變成正面。

#include <opencv2/opencv.hpp>
using namespace cv;

void rotate_90n(cv::Mat const &src, cv::Mat &dst, int angle)
{
     CV_Assert(angle % 90 == 0 && angle <= 360 && angle >= -360);
     if(angle == 270 || angle == -90){
        // Rotate clockwise 270 degrees
        cv::transpose(src, dst);
        cv::flip(dst, dst, 0);
    }else if(angle == 180 || angle == -180){
        // Rotate clockwise 180 degrees
        cv::flip(src, dst, -1);
    }else if(angle == 90 || angle == -270){
        // Rotate clockwise 90 degrees
        cv::transpose(src, dst);
        cv::flip(dst, dst, 1);
    }else if(angle == 360 || angle == 0 || angle == -360){
        if(src.data != dst.data){
            src.copyTo(dst);
        }
    }
}

int main( )
{
    VideoCapture capture(0);
    while(1)
    {
        Mat frame, dst;
        capture>>frame;

        if (!frame.empty()) {

            rotate_90n(frame, dst, -90);
            imshow("result", dst);
        } else {
            break;
        }
        waitKey(30);  //delay 30ms
    }
    return 0;
}

編譯+執行

# 在 mac 編譯
g++ -ggdb `pkg-config --cflags --libs opencv` -stdlib=libc++ test7.cpp -o test7
./test7

# 在 RPi 編譯
g++ -ggdb `pkg-config --cflags --libs opencv` test7.cpp -o test7

./test7

test7.py

#!/usr/bin/python
# coding=utf-8

import cv2
import time

def rot90(img, angle):
    if(angle == 270 or angle == -90):
        img = cv2.transpose(img)
        img = cv2.flip(img, 0)  # transpose+flip(0)=CCW
    elif (angle == 180 or angle == -180):
        img = cv2.flip(img, -1)  # transpose+flip(-1)=180
    elif (angle == 90 or angle == -270):
        img = cv2.transpose(img)
        img = cv2.flip(img, 1)  # transpose+flip(1)=CW
    elif (angle == 360 or angle == 0 or angle == -360):
        pass
    else :
        raise Exception("Unknown rotation angle({})".format(angle))
    return img

cap = cv2.VideoCapture(0);

while True:
    ret, frame = cap.read()

    if ret == True:
        ## grayscale avi
        #gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
        #cv2.imshow('frame', gray)

        ## normal avi
        dst = rot90(frame, -90)
        cv2.imshow('frame', dst)

        if cv2.waitKey(30) & 0xFF == ord('q'):
            break

    else:
        break

cap.release()
cv2.destroyAllWindows()

References

Install OpenCV-Python in Windows

Computer Vision with Raspberry Pi and the Camera Pi module

使用 vcgencmd 指令查看 Raspberry Pi 的 CPU 溫度、運行速度與電壓等資訊

在 Raspberry Pi 上面安裝 OpenCV 函式庫

安裝 OPENCV 紀錄

Install guide: Raspberry Pi 3 + Raspbian Jessie + OpenCV 3

[翻译]Python 2.7 和 Python 3+ 的OpenCV 3.0 安装教程

[Raspberry Pi] 解決 Raspberry Pi 找不到 /dev/video0

OpenCV on Raspberry Pi - Using Java(6)- 使用 OpenCV 拍攝照片(Camera Module)


Installing OpenCV in Mac OSx tutorial

macOS: Install OpenCV 3 and Python 2.7

Mac下安装OpenCV3.0—包含opencv_contrib模块

Undefined freetype symbols when building openCV 3.2.0


How to compile OpenCV sample code ?

【OpenCV】安裝在Mac及XCode筆記

VideoCapture.open(0) won't recognize pi cam

Rotate image by 90, 180 or 270 degrees