2014年7月29日

Java 8 Stream Api簡介

上篇介紹完Lambda之後,接著要來介紹 JAVA 8新增的 Stream API。

Stream介紹

Stream並不是一個儲存容器,也就是說,他並不是像List、Set、Map那種存放實際資料的容器,他是將資料來源從來源取得stream之後,透過管線(pipleline)操作,來取得你所需要的結果。

舉例來說:

今天我想知道公司的vip會員到底為公司貢獻了多少的營業額。首先,我先拿到了一百萬筆會員的資料儲存容器,我取得其容器的stream,先做初步的資料filter,只留會員資格為vip資料,之後將filter完的結果透過管線交給接下來的操作。

下一個操作執行map的動作,將資料內真正關心的資料指出來,在這邊我就可以指定,我只關心他們每個人累積消費金額為多少,map完之後在交給下一個操作處理,

最後一個操作執行reduce,將所需要的結果整理好,也就是說我可以透過reduce將所有vip的總消費金額加總起來,得知vip為公司貢獻了多少營業額。

Stream管線操作架構

有了基本的Stream能做什麼的概念之後,接著來看看Stream管線操作的架構。

整個管線操作有分成兩個部分,intermediate與terminal,其中intermediate可以有0~n個,比如說filter和map,他們就是屬於intermediate,這類的操作並不會立即生效,只會回傳新的stream回來,他們屬於lazy operation。

而terminal只能有一個,當遇到terminal operation時,則會立即生效,他們是屬於eager operation。當stream執行完terminal operation之後,則此管線已被consumed了,已經不能在被使用了,如果需要在做任何的管線操作,則必須從資料來源在產生一個新的stream,建立先的管線,才能執行管線操作。

以下為管線操作架構的整理:

  • source => Collection, array, generator function, I/O channel

  • intermediate=>Stream.filter, Stream.map

  • terminal operation=>Stream.forEach, Stream reduce.

Stream Api Sample

接下來舉一些例子,來說明怎麼使用Stream Api。

原始資料,List內放0~5的整數資料

List<Integer> listSource = new ArrayList<Integer>();
Collections.addAll(listSource, 0, 1, 2, 3, 4, 5);

挑出大於2的元素

可以透過filter的操作來實現,filter的官方文件寫他需要傳入Predicate介面。而在上一篇提到, Predicate為輸入參數類型為T,輸出一個布林值。回傳為true,則代表此元素要留著,回傳為false則該元素會被過濾,透過Lambda的語法來實作Predicate,讓整個程式變的更簡潔易讀,如下:

List<Integer> listGreaterThen3 = listSource.stream()
    .filter((t) -> (t > 2) ? true : false)
    .collect(Collectors.toList());

// listSource = [0, 1, 2, 3, 4, 5]
// listGreaterThen2 = [3, 4, 5]

挑出第一個大於2的元素

一樣夠過filter來實現,差別在於上面的例子把filter之後得到的stream透過collect method,將結果收集到另一個List內,這邊的例子是使用findFirst method,把在此stream裡第一個大於3的元素取回。這邊需要注意的是,如果filter的條件,無法過濾出任何元素,則在使用get method時,會丟出java.util.NoSuchElementException。

Integer firstGreaterThen2 = listSource.stream().filter((t) -> (t > 2) ? true : false)
        .findFirst().get();

// listSource = [0, 1, 2, 3, 4, 5]
// firstGreaterThen2 = 3

取所有大於2元素的,將每個被挑出來的元素乘以2之後,加總

先透過filter取得大於2的元素,之後透過mapToInt method,將這些元素皆乘以2,之後透過reduce方法來取得總和。下面總共有三種reduce的方式,第一種是利用IntStream.sum()來加總,第二種則是透過reduce method,自己寫加總演算法,第三種則是透過Method References,使用Interger的sum method來達成。

int sum1 = listSource.stream().filter((t) -> (t > 2) ? true : false)
        .mapToInt((t) -> t * 2).sum();

int sum2 = listSource.stream().filter((t) -> (t > 2) ? true : false)
        .mapToInt((t) -> t * 2).reduce(0, (i, t) -> i + t);

int sum3 = listSource.stream().filter((t) -> (t > 2) ? true : false)
        .mapToInt((t) -> t * 2).reduce(0, Integer::sum);

// listSource = [0, 1, 2, 3, 4, 5]
// after filter and map = [6, 8 ,10]
// sum = 24

取所有大於2元素的,將每個被挑出來的元素乘以2之後,取最大值

先透過filter取得大於2的元素,之後透過mapToInt method,將這些元素皆乘以2,之後透過reduce方法來取得stream裡面最大值為何。下面總共有三種reduce的方式,第一種是利用IntStream.max()來求最大值,第二種則是透過reduce method,自己寫求最大值算法,第三種則是透過Method References,使用Interger的max method來達成。這邊需要注意的是,如果找不出最大的元素,則會丟出java.util.NoSuchElementException。

int max1 = listSource.stream().filter((t) -> (t > 2) ? true : false)
        .mapToInt((t) -> t * 2).max().getAsInt();

int max2 = listSource.stream().filter((t) -> (t > 2) ? true : false)
        .mapToInt((t) -> t * 2).reduce(0, (a, b) -> (a > b) ? a : b);

int max3 = listSource.stream().filter((t) -> (t > 2) ? true : false)
        .mapToInt((t) -> t * 2).reduce(0, Integer::max);

// listSource = [0, 1, 2, 3, 4, 5]
// after filter and map = [6, 8 ,10]
// max = 10

Stream資料來源被修改

最後來看一個Stream的一個例子:

List<String> list = new ArrayList<String>();
Collections.addAll(list, "one", "two");
Stream<String> stream = list.stream();

list.add("three");
List<String> listColl = stream.collect(Collectors.toList());
// [one, two, three]

這例子內,原本有個ArrayList,裡面放著"one"、"two"兩個字串。首先我們先得到了一個此ArrayList的stream並存放在變數內,之後我們先把新的字串"three"直接加進ArrayList內,在從先前存放的stream變數收集元素放到新的List內,結果最後收集到的結果為"one"、"two"、"three"。

還記得上面的部分有說明,Stream有分成intermediate operation與terminal operation,而其中前者為lazy,後者為eager,上面例子裡,collect()屬於terminal operation,也就是說,由於ArrayList的修改是在collect()之前執行的,而stream是在遇到terminal operation才會真正去執行程式,因此上面程式裡在執行collect()時,原本的ArrayList變成了"one"、"two"、"three",因此最後stream收集到的結果,也是"one"、"two"、"three"。

參考

Package java.util.stream

2014年7月28日

使用PHP上傳檔案及檔案大小限制設定

使用PHP上傳檔案是個再基礎不過的功能了,而且相當常用。 此外,在php.ini設定檔中,還可以使用upload_max_filesize以及post_max_size兩個參數來設定上傳檔案的大小限制。

上傳檔案簡介

以下先簡單介紹如何使用PHP上傳檔案:
首先,當然是寫個簡單的上傳用網頁:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title></title>
</head>

<body>
    <form action="upload.php" method="post" enctype="multipart/form-data">  
    <input type="file" name="file" id="file" /> 
    <input type="submit" name="submit" value="submit" />
    </form>
</body>
</html>
而PHP程式則如下:
<?php
  if ($_FILES["file"]["error"] == UPLOAD_ERR_OK) {

    $html = "Upload(name): " . $_FILES["file"]["name"] . "<br />";
    $html .= "Type: " . $_FILES["file"]["type"] . "<br />";
    $html .= "Size: " . ($_FILES["file"]["size"] / 1024) . " Kb<br />";
    $html .= "Stored in: " . $_FILES["file"]["tmp_name"] . "<br />";

    //將上傳成功的檔案放到指定路徑下
    $moveRes = move_uploaded_file($_FILES["file"]["tmp_name"],
    "/path_for_uploaded_file");

    $html .= "Uploaded file is moved to /path_for_uploaded_file". "<br />";
    echo $html;
  }
  else {
    echo "Error: " . $_FILES["file"]["error"] . "<br />";
  }

?>
程式第一行即檢查檔案是否上傳成功。 UPLOAD_ERR_OK表示上傳成功,而其他錯誤代碼可以參考:http://php.net/manual/en/features.file-upload.errors.php

在php.ini限制上傳檔案的大小

在php.ini設定可以限制上傳檔案的大小: 以下範例設定檔案上傳大小最大不得超過120M。
; Maximum allowed size for uploaded files.
upload_max_filesize = 120M
設定完畢之後,重開httpd伺服器即可生效。
然而,還有另一個地方需要注意的,那就是設定HTTP POST資料量的大小。
; Maximum size of POST data that PHP will accept.
post_max_size = 120M
upload_max_filesizepost_max_size有什麼不同呢? upload_max_filesize是限制一個檔案的大小,而post_max_size是限制一次HTTP POST資料量的大小。
因此,一般來說post_max_size至少要大於等於upload_max_filesize;否則,上傳檔案大小的限制會小於預期。
筆者第一次上傳檔案時,只改了upload_max_filesize(改為120M),卻不知道post_max_size預設只有8M,因此一直遇到「檔案小於120M卻無法正確上傳」的狀況。最後是看了PHP log, 發現PHP警告POST資料超過8M, 上網查詢了一下資料才得以解決。
運用upload_max_filesizepost_max_size兩個參數,還可以設定一些特殊的情況,像是要限制上傳的單一檔案大小10M,不過最多可以一次上傳10個檔案的話,可以這樣設定:
; Maximum allowed size for uploaded files.
upload_max_filesize = 10M

;...

; Maximum size of POST data that PHP will accept.
post_max_size = 100M
即設定post_max_size為100M,而upload_max_filesize為10M就可以了,非常簡單卻實用。

erlang - rebar

本文接續上一篇對 rebar 的簡介,說明有關取得相依套件, 建構 release package 以及程式不關機直接升級的問題。

Templates

如果要使用自己的 template,就把 mytemplate.template 放到 templates 目錄中,並執行以下指令

rebar create template=mytemplate

rebar 提供了幾個內建的 templates

Template Variables Command Alias Command
simplesrv srvid rebar create template=simplesrv X
simplenode nodeid rebar create template=simplenode rebar create-node
simplemod modid rebar create template=simplemod X
simplelib libid rebar create template=simplelib rebar create-lib
simplefsm fsmid rebar create template=simplefsm X
simpleapp appid rebar create template=simpleapp rebar create-app
ctsuite testmod rebar create template=ctsuite X
basicnif module rebar create template=basicnif X

管理發行版本

rebar 利用 reltool.config 建立執行的節點。

如果一個專案中,包含了多個 OTP applicaitons,我們可以建立一個 app 目錄,並把 otp application 移到 app 目錄中。

mkdir apps
cd apps

mkdir myapp
cd myapp

rebar create-app appid=myapp

cd ../../

編輯檔案 rebar.config

{sub_dirs, ["apps/myapp", "rel"]}.

然後就可以編譯專案

rebar compile

建立 release 目錄與檔案

mkdir rel
cd rel
rebar create-node

修改 reltool.config

line 4
        {lib_dirs, ["../apps"]},

line 12 mynode 改為 myapp
        [
         kernel,
         stdlib,
         sasl,
         myapp
        ]},

line 27 改為
        {app, myapp, [{mod_cond, app}, {incl_cond, include}, {lib_dir, ".."}]}

產生 build release

cd ..
rebar -v generate

使用 rel/mynode/bin/mynode 啟動與停止節點

> rel/mynode/bin/mynode
Usage: mynode {start|start_boot <file>|foreground|stop|restart|reboot|ping|console|getpid|console_clean|console_boot <file>|attach|remote_console|upgrade}

啟動 app 後,進入 console 互動
> rel/mynode/bin/mynode console
(mynode@127.0.0.1)1> application:which_applications().
[{sasl,"SASL  CXC 138 11","2.3.4"},
 {myapp,[],"1"},
 {stdlib,"ERTS  CXC 138 10","1.19.4"},
 {kernel,"ERTS  CXC 138 10","2.16.4"}]


在背景啟動或停止 app
> rel/mynode/bin/mynode start
> rel/mynode/bin/mynode stop

增加 deps

專案通常會使用一些第三方的專業套件,我們必須要先修改 rebar.config。
必須增加 deps tuple 如下
{deps, [Dependency1, dependency2, ...]}

每一個 Dependency1 內容如下
{App, VsnRegex, Source}

Source 指定該 library 的來源,有以下幾種選擇。

  1. {hg, Url, Rev}
    mercury repository
  2. {git, Url}
  3. {git, Url, {branch, Branch}}
  4. {git, Url, ""} 等同於 {git, Url, {branch, "HEAD"}}
  5. {git, Url, {tag, Tag}}
  6. {git, Url, Rev}
  7. {bzr, Url, Rev}
    bazaar repository

以下這個例子,使用了 cowboy。

{deps, [
    {cowboy, "",
        {git, "git://github.com/extend/cowboy.git",{branch, "master"}}}
        ]}.
{sub_dirs, ["apps/myapp", "rel"]}.

我們可以在 command line 用以下指令,從 git 取得 cowboy 相關的 libraries,包含了 cowlib, ranch與cowboy,程式碼都會 clone 到 deps。

rebar get-deps
rebar update-deps

如果專案 app 裡的程式碼使用了 cowboy,編譯時,也必須把 deps 相關的 libs 包含進去。

修改 reltool.config

line 4 增加 ../deps
        {lib_dirs, ["../apps", "../deps"]},

line 12 mynode 改為 myapp
        [
         kernel,
         stdlib,
         sasl,
         myapp
        ]},

line 27
        {app, mynode, [{incl_cond, include}]},
        {app, cowboy, [{incl_cond, include}]}

Makefile

可以做個簡單的 Makefile,簡化重複輸入指令的麻煩。

all: compile

deps:
    rebar get-deps
    rebar update-deps

compile:
    rebar compile

clean:
    rebar clean

test: compile
    rebar eunit skip_deps=true

release: compile
    rebar -v generate
    mkdir -p ./rel/mynode/priv
    cp -r ./apps/myapp/priv/ ./rel/mynode/priv/

.PHONY: all deps compile

Upgrades

把 apps/myapp/src/myapp.app.src and rel/reltool.conf 兩個檔案的版本號碼 vsn 都從 1 改為 2

在 rebar 有個測試的 dummy project 可用來驗證 OTP 程式線上直接升級的程序。

在取得 dummy project source code 時,一開始是設定為 0.1 版,所以先編譯然後就啟動 dummy server。

rebar compile
rebar generate
mv rel/dummy rel/dummy_0.1
rebar clean

啟動 dummy server

cd rel/dummy_0.1
bin/dummy console

(dummy@127.0.0.1)1> dummy_server:get_state().
0
(dummy@127.0.0.1)2> dummy_server:set_state(123).
{ok,123}
(dummy@127.0.0.1)3> dummy_server:get_state().
123

在另一個 terminal,進行 0.2 版的編譯

先將版本號碼從 0.1 版改為 0.2

vi apps/dummy/src/dummy.app.src
vi rel/reltool.config

編譯並封裝 0.2 版

rebar compile
rebar generate

rebar generate-appups previous_release=dummy_0.1
rebar generate-upgrade previous_release=dummy_0.1

這一行是一個測試 dummy_0.2.tar.gz 的壓縮檔
tar -zvtf rel/dummy_0.2.tar.gz

將 0.1 版 升級到 0.2 版

mv rel/dummy_0.2.tar.gz rel/dummy_0.1/releases/

回到剛剛 0.1 版的 console
(dummy@127.0.0.1)6> release_handler:unpack_release("dummy_0.2").
{ok,"0.2"}
(dummy@127.0.0.1)7> release_handler:install_release("0.2").
{ok,"0.1",[]}
(dummy@127.0.0.1)8> release_handler:make_permanent("0.2").
ok
(dummy@127.0.0.1)9> release_handler:which_releases().
[{"dummy","0.2",
  ["kernel-2.16.4","stdlib-1.19.4","sasl-2.3.4","dummy-0.2",
   "asn1-2.0.4","compiler-4.9.4","crypto-3.2","et-1.4.4.5",
   "gs-1.5.15.2","inets-5.9.8","mnesia-4.11",
   "observer-1.3.1.2","public_key-0.21","runtime_tools-1.8.13",
   "ssl-5.3.3","tools-2.6.13","webtool-0.8.9.2","wx-1.1.2"],
  permanent},
 {"dummy","0.1",
  ["kernel-2.16.4","stdlib-1.19.4","sasl-2.3.4","dummy-0.1",
   "asn1-2.0.4","compiler-4.9.4","crypto-3.2","et-1.4.4.5",
   "gs-1.5.15.2","inets-5.9.8","mnesia-4.11",
   "observer-1.3.1.2","public_key-0.21","runtime_tools-1.8.13",
   "ssl-5.3.3","tools-2.6.13","webtool-0.8.9.2","wx-1.1.2"],
  old}]
(dummy@127.0.0.1)10> dummy_server:get_state().
123

References

[How To]使用rebar構建erlang 項目
Rebar:Erlang構建工具

Netty 介紹

如果我們想要開發client-server應用程式,大部份的programmer會使用較熟悉的http協定來開發web service,以慣用java的programmer來說最常用的http container就是tomcat。但隨著project需求變化,我們會發現這一類的協定或實作有時不易擴展(scale)的很好,我們會想要高度客製化來滿足我們的特殊需求。

這時我們可能會需要Netty,以下是取自Netty官網對於Netty的介紹

The Netty project is an effort to provide an asynchronous event-driven network application framework and tooling for the rapid development of maintainable high-performance · high-scalability protocol servers and clients.

也就是說Netty是一個framework,一個socket框架,並非僅是web server如tomcat,我們可以拿Netty來開發http, ftp, snmp一類的協定, 或者是自定的協定。

以下介紹使用Netty來實作簡單的Echo Server。從名稱可知此範例的server接收到來自clent的訊息後就回應相同的訊息回去給client。 執行時server會listen一個port。client可使用telnet來連此server的port,連上後client便可開始送訊息,server收到後也會回應相同的訊息回去。

首先直接撰寫handler部份,handler負責處理Netty產生的I/O event. 此範例的重點就是將收到的訊息再回傳回去。

import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;

public class EchoServerHandler extends ChannelInboundHandlerAdapter {//(1)

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) {//(2)

        // echo
        ctx.write(msg); // (3)
        ctx.flush(); // (4)

    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { 
        // Close the connection when an exception is raised.
        cause.printStackTrace();
        ctx.close();
    }
}
  1. handler需extend ChannelInboundHandlerAdapter
  2. override channelRead method,當server收到訊息後此method會被呼叫
  3. ChannelHandlerContext有提供許多的method來觸發一些io event,在這個範例中使用write來回傳資料至client
  4. ctx.write()被呼叫時並未立即將資料送至client,僅是先buffer起來,當ctx.flush()被呼叫時才是送到client

主要的處理邏輯寫完了,接下來就是寫main來啟動server並使用EchoServerHandler

import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;

public class EchoServer {

    private int port;

    public EchoServer(int port) {
        this.port = port;
    }

    public void run() throws Exception {
        EventLoopGroup bossGroup = new NioEventLoopGroup(); // (1)
        EventLoopGroup workerGroup = new NioEventLoopGroup();
        try {
            ServerBootstrap b = new ServerBootstrap(); // (2)
            b.group(bossGroup, workerGroup)
                    .channel(NioServerSocketChannel.class) // (3)
                    .childHandler(new ChannelInitializer<SocketChannel>() { // (4)
                                @Override
                                public void initChannel(SocketChannel ch)
                                        throws Exception {
                                    ch.pipeline().addLast(
                                            new EchoServerHandler());
                                }
                            }).option(ChannelOption.SO_BACKLOG, 128) 
                    .childOption(ChannelOption.SO_KEEPALIVE, true);

            // Bind and start to accept incoming connections.
            ChannelFuture f = b.bind(port).sync(); // (5)

            // Wait until the server socket is closed.
            // In this example, this does not happen, but you can do that to
            // gracefully
            // shut down your server.
            f.channel().closeFuture().sync();
        } finally {
            workerGroup.shutdownGracefully();
            bossGroup.shutdownGracefully();
        }
    }

    public static void main(String[] args) throws Exception {
        int port;
        if (args.length > 0) {
            port = Integer.parseInt(args[0]);
        } else {
            port = 8080;
        }
        new EchoServer(port).run();
    }
}
  1. NioEventLoopGroup is a multithreaded event loop that handles I/O operation.
  2. ServerBootstrap 是個helper class用來設定server相關的參數.
  3. 此範例指定NioServerSocketChannel來處理進來的連線
  4. 指定channel要使用剛撰寫的的EchoServerHandler
  5. bind指定的port並啟動server

測試

首先先將server run起來,(直接用java run EchoServer main())。接著開終端機來下指令telnet localhost 8080,等連上後就可以開始送訊息了!畫面如下,其中(1)(3)是向erver送出的訊息,(2)(4)是server回應的訊息

jamestekiMacBook-Pro:~ james$ telnet localhost 8080
Trying ::1...
Connected to localhost.
Escape character is '^]'.
hello  (1)
hello  (2)
world  (3)
world  (4)

Summary

在study Netty過程中常會看到Jetty, Tomcat, Mina。這邊做個簡單的比較。Jetty是http容器,和Tomcat是同样的概念,但是具體實現不一樣。 Netty是socket框架,和MINA是相同的概念,但是具體實現不一樣。 據說Mina跟Netty是來自同一個作者,但Mina好像較少在更新了比較沒有Netty來的活躍。

參考來源 http://netty.io/wiki/user-guide-for-4.x.html#wiki-h3-15

2014年7月25日

CentOS 6.x 網卡Mac Address 變更導至網卡未被啟用無法啟動

Linux 網卡Mac Address 變更導至網卡未被啟用無法啟動

你的電腦工作環境是什麼系統呢?應該也如同大部份的人不是Windows 環境就是 MAC 環境,至於在自己的電腦上想試著玩玩其他的OS (作業系統),最方便最省錢的方法也就是使用VM這種軟體工具,坊間的VM tools其實也不少,而我所採用的VMWare。

前陣子在VM上安裝了一個Linux系統,有一次電腦當掉強制把VM給終止,隔天開啟動電腦時,已經不能用網路連結此系統,看來是VM的網路裝置跑掉了,mac address 也不一樣了試著障礙排除吧!

(1) 在我安裝的Linux環境下,試著查查看網路設定是否跑掉。

# ifconfig - a






用這個指令來查系統上所有網卡設備,發現原先的eth0 變成eth2,而eth1 變成了eth3。

(2) 懷疑VM的網路裝置跑掉了,關閉VM看一下設定值是不是有異常,遺憾的是...沒看出什麼異動。



(3) 再重新啟動系統,再利用指令查找網卡的統計訊息


# cat /proc/net/dev


這裡可以發現網卡代號真的不是原來的eth0 跟 eth1

(4) 編輯 /etc/udev/rules.d/70-persistent-net.rules


網路卡設備的內容會記錄在 /etc/udev/rules.d/70-persistent-net.rules
# vi /etc/udev/rules.d/70-persistent-net.rules


發現除了之前的eth0 eth1 之外還多出了兩個 eth2 跟 eth3
eth2 & eth3 網卡的mac address才是正確的,應該將eth0的address 改為eth2的address,應該將eth3的address 改為eth2的address
再把eth2 & eth3 的設定刪除



另一個解決方法:當我們使用備份功能將系統複製到新的硬體上之後,會因為新網卡的 MAC address 與 70-persistent-net.rules 內的 MAC address 不相同,導致網路卡未被啟用、網路無法連線。 此時,只需要刪除 70-persistent-net.rules ,再重開機,讓系統重新產生 70-persistent-net.rules。(未測試過,資料來源 - http://blog.roodo.com/rocksaying/archives/11777065.html )

(5) 編輯 /etc/sysconfig/network-scripts/ifcfg-eth0


# vi /etc/sysconfig/network-scripts/ifcfg-eth0



將"HWADDR="參數修改成正確的 MAC Address,或刪除此參數,若沒有這個參數應該在上一個步驟重新開機後會自動啟動網卡。

(6) 修改完成後,啟動網卡。

# ifup eth0


似乎無法順利啟動
試著將網路重新啟動

# /etc/init.d/network retstart
無法重新啟動網路

(7) 用最後一招 重新啟動server


# reboot

檢查一下網路設定

# ifconfig -a




解決的方法不只一種,請多方嘗試找到最佳方法,會更省時省力。

2014年7月24日

AVAudioPlayer 在靜音模式時關閉音效


使用過 apple 手持裝置的人應該都知道,apple 無論是 pad 或是 iphone 甚至 touch,都會有一個 " 響鈴 / 靜音 " 的切換鍵 ( switch )。當您不希望被任何聲音干擾時,可將它切換成靜音模式,這樣任何 app 就不應該在發出聲音。

使用 AVAudioPlayer 播放聲音,將手機 " 響鈴 / 靜音 " 按鍵切換至 " 靜音 " 卻還有聲音

  1. 問題:當 我們在 app 中使用 AVAudioPlayer 播放聲音時,一般來說會設定 AudioSession 為AVAudioSessionCategoryPlayback。這時若將手機上的 " 響鈴 / 靜音 " 切換到靜音時,AVAudioPlayer 依然能播放聲音,並沒有達到預期 " 靜音 " 的效果。
    從 apple 官方文件可看出,若設置 AudioSession 為 AVAudioSessionCategoryPlayback 時,無論您的手機是否切換靜音模式,或者 app 是否退到背景,都會繼續撥放音效!
  2. 解決辦法:將 AudioSession 改為 AVAudioSessionCategoryAmbient
    ※ 注意,若設定為 AVAudioSessionCategoryAmbient,則 app 一退到背景,無論是否切換靜音模式 app 都無法播放聲音!但這不會影響其他 app 音效(其他 app 音效不會停止)
  3. 實作方式:(在播放音效前呼叫)

- (void)useAmbient
{
    AVAudioSession *session = [AVAudioSession sharedInstance];
    NSError *error;
    //設定如果手機的響鈴/靜音switch切到靜音,或音量鍵切到靜音,則完全關閉app所有音效,app退背景也無法播放音效,但不會影響其他app音效(其他app音效不會停止)
    [session setCategory:AVAudioSessionCategoryAmbient error:&error];
    if(error!=nil){
        NSLog(@"set AudioSession AVAudioSessionCategoryAmbient error: %@", [error localizedDescription]);
    }
}

References

2014年7月22日

IOS UIScrollView 的使用


有時我們會希望能在畫面上顯示較多的訊息,這些訊息可能會超過螢幕大小。這時就需要用到 scrollview 了。scrollview 可以在有限的畫面上顯示較多的資料,例如個人資料,設定畫面等等。

contentsize

contentsize 顧名思義就是 scrollview 所包含的內容大小,也可以說是可滑動範圍。IOS UIScrollView 不像 Android 的 scrollview 一樣,雖然他一樣會自動偵測內容大小,但必須每個元件都要有固定寬 / 高,否則就必須手動設定。
通常我們會先將 scrollview 所包含的元件建立完,之後再設置 contentsize,這樣在滑動的時候才能看到完整資料。若設置太小,下面的資料可能就會有部分資料無法顯示,若設置太大,則會在下方出現大量空白區域。
上圖為 contentsize 設定太小的結果,下方資料無法顯示
上圖為 contentsize 設定太大的結果,下方留下大片空白區域

contentsize not work

若您的畫面是使用 storyboard 設計,可能會出現在 controller 中不管怎麼設定 contentsize,它都還是 storyboard 中的設定,即使已經在 viewDidAppear 中設定,卻還是沒用的情況。(猜測可能系統在執行完 viewDidAppear 後,才載入 storyboard 中的一些設定,導致這種情況,但官方沒有確切的說明 storyboard 在顯示的流程...),這時建議改在 viewDidLayoutSubviews 中做設定

//失敗
- (void)viewDidAppear:(BOOL)animated{
    [super viewDidAppear:YES];
    if(self.reloadData.intValue == 1){
        if(cgroupList.count > 0){
            [editCgroupListDelegate reloadDataWithTableView:self.tv_group list:cgroupList];
            float groupListHeight = [self setListHight:self.tv_group delegate:editCgroupListDelegate listCount:[cgroupList count]];
            NSLog(@"updateGroupListUI profileview height:%f", height_profileview+groupListHeight);

     //設定 scrollview contentSize
            self.scview_profile.contentSize = CGSizeMake(self.view_profile.frame.size.width, height_profileview+groupListHeight+height_phoneList);//(phoneCellHeight*phoneList.count)

            NSLog(@"tableview phoneList height:%f", height_phoneList);
            self.tv_group.hidden = NO;
        }else{
            self.tv_group.hidden = YES;
        }
    }
}


//成功,但會進入兩次,可以加些 flag 去過濾
- (void)viewDidLayoutSubviews{
    [super viewDidLayoutSubviews];
    if(self.reloadData.intValue == 1){
        if(cgroupList.count > 0){
            [editCgroupListDelegate reloadDataWithTableView:self.tv_group list:cgroupList];
            float groupListHeight = [self setListHight:self.tv_group delegate:editCgroupListDelegate listCount:[cgroupList count]];
            NSLog(@"updateGroupListUI profileview height:%f", height_profileview+groupListHeight);
            
            //設定 scrollview contentSize
            self.scview_profile.contentSize = CGSizeMake(self.view_profile.frame.size.width, height_profileview+groupListHeight+height_phoneList);//(phoneCellHeight*phoneList.count)
            
            NSLog(@"tableview phoneList height:%f", height_phoneList);
            self.tv_group.hidden = NO;
        }else{
            self.tv_group.hidden = YES;
        }
    }
}

viewDidLayoutSubviews 執行順序


//進入
viewWillAppear
viewDidLayoutSubviews
viewDidAppear
viewDidLayoutSubviews

//換頁
viewWillDisappear
viewDidLayoutSubviews
viewDidDisappear

scrollview 的排版

如下圖,一般來說我們會再 scrollview 中放置一個 view,當作 contentview,在將要顯示的元件放在 contentview 中。這種排版的好處是,在設置 contentsize 的時候,可以直接抓 contentview 的寬 / 高。

上圖圈起來的部分,是 contentview 的高度

上圖圈起來的部分,是 contentview 的高度
※ 若 contentview 中所有元件有固定的寬高,那甚至可以不用設置 scrollview 的 contentsize,系統會自行計算 contentsize 的寬 / 高

scrollview 中元件有註冊 event,在畫面上也看的到元件,卻永遠點不到?

當您遇到上述問題時,就該檢查一下您 scrollview 中元件的排版了。
※ 注意!只有在 contentview 中的元件,才能觸發 event
解決辦法:將您的 contentview 上色,執行並查看剛剛的元件是否包含在 contentview 中
上圖中,下方白色區塊,因為元件不在 contentview 中,所以無法觸發 event
上圖中,所有元件都在 contentview 中,所以可以正常觸發 event

References

2014年7月21日

erlang - otp application release tool

OTP application 最後發布時,需要一個工具幫我們分析 application 的相關 dependency libraries,發布正確的 OTP application。

目前有看到三個 release 工具: reltool, rebar, relx,雖然 reltool 是 OTP 官方提供的封裝工具,前面的部份先簡述 reltool 與 relx,relx 是因為 survey 了 cowboy 的關係,而去了解,功能跟 rebar 類似,但提供了更高階的封裝簡化工具,這個工具是 erlware 這個組織開發提供的。rebar 提供了比 reltool 高階的封裝方式,我們就以 rebar 為主,因為 rebar 的文件說明比 relx 還清楚。

reltool

reltool提供 GUI 與 CLI 兩種界面

reltool user guide
reltool doc
reltool使用指南
reltool使用入門

erlang.mk and relx

建構 OTP releases 一直以來都是個很麻煩的工作,除了 reltool, rebar 之外 cowboy 作者提供了另一個建立 application 的方式。

建立 OTP release 需要兩個步驟:

  1. compile source
    erlang.mk 用來處理第一個步驟,erlang.mk 是 GNU Makefile 的一個 include file,它可以用來 build project, 取得 building dependencies,產生文件,做 dialyzer 靜態分析。
  2. create a release
    relx 是個 release creation tool,它是一個執行檔。用來封裝OTP application,裡面包含了Erlang runtime, a boot script to start the node and all its applications, configuration files

最簡單的 Makefile,就只需要一個 PORJECT name 並 include erlang.mk

PROJECT = my_project

include erlang.mk

DEPS 定義本專案相關的 dependencies,條列出來後,在下面以 dep_cowboy 的方式定義該 library 的 repository URL 以及 commit number, tag 或 branch。 .PHONY 表示有兩個 build target,預設是 release。 release 最後會使用 relx 將 project 建構在 rel 目錄中。

PROJECT = ninenines

DEPS = cowboy erlydtl
dep_cowboy = https://github.com/extend/cowboy.git 0.8.5
dep_erlydtl = https://github.com/evanmiller/erlydtl.git 4d0dc8fb

.PHONY: release clean-release

release: clean-release all projects
    relx -o rel/$(PROJECT)

clean-release: clean-projects
    rm -rf rel/$(PROJECT)

include erlang.mk

這是 relx.config 檔,第一行定義 release name: ninenines,版本號碼1,裡面包含一個 application: ninenines,extended_start_script 告訴 relx 要建立一個可啟動 application 的 script,下一行 sys.config 代表可指定 erlang vm 的起始參數,

{release, {ninenines, "1"}, [ninenines]}.

{extended_start_script, true}.
{sys_config, "rel/sys.config"}.

{overlay, [
    {mkdir, "log"},
    {copy, "rel/vm.args",
        "releases/\{\{release_name\}\}-\{\{release_version\}\}/vm.args"}
]}.

參考文件 Build Erlang releases with erlang.mk and relx

rebar

編譯與安裝

  1. 下載 rebar source code
    git clone https://github.com/basho/rebar.git

  2. 編譯
    cd rebar
    ./bootstrap

    產生出 rebar 執行檔後,可以將 rebar 複製到任何地方,都可以使用。我們可以將 rebar 複製到 /usr/local/bin 或是 ~/bin 目錄,放在隨時可使用到的 PATH 裡面,就完成 rebar 的安裝了。

  3. 直接下載 rebar
    因為 rebar 是完全由 erlang 撰寫的,而且有整合成一個獨立的 escript,我們也可以直接下載編譯好的 rebar http://cloud.github.com/downloads/basho/rebar/rebar ,然後就能使用了。

  4. windows
    把在 linux 環境編譯出來的 rebar 複製到 windows 環境,把 rebar.cmd 與 rebar 放在 windows 的 PATH 路徑中可以存取到的地方,就可以直接使用 rebar 了。

測試專案

  1. mkdir myapp
    建立專案目錄

  2. rebar create-app appid=myapp
    產生專案的檔案骨架
    完成後會看到一個 src 目錄,裡面有三個檔案:myapp.app.src、myapp_app.erl、myapp_sup.erl

  3. rebar compile
    編譯 myall project,完成後會產生 ebin 目錄,也會得到一個 OTP 專案檔 myapp.app

  4. rebar clean
    清除編譯結果

命令列的參數

在開發過程中,最常用的功能有

  1. 編譯
  2. 單元測試和覆蓋分析
  3. 靜態分析(通過Dialyzer和Xref)
  4. 生成文檔
  5. 依賴管理

Rebar commands 提供了指令與參數列表

常見指令 description
compile 編譯
eunit 執行 eunit
doc 使用 edoc 產生文件
clean 清除所有生成的資料
一般指令 description
check_deps 檢查 rebar.config 的 dep libs
create 建立專案,必須要給予 template 的設定
create-app 根據 simpleapp.template 建立 app 專案
create-lib 根據 simplelib.template 建立 OTP library
create-node 根據 simplenode.template 建立一個 prototypical OTP embedded system
ct 執行 common_test suites
delete-deps 刪除依賴的專案原始碼
escriptize 以 ebin 目錄的 beam files 產生 escript 的執行檔
generate 使用 reltool 建立一個embedded system
generate-upgrade 產生 upgrade package
get-deps 根據 rebar.config 取得依賴的專案原始碼
list-deps 依賴的專案列表
update-deps 更新依賴的專案原始碼
xref 使用 xref 分析依賴

compile 支援的 source code 格式

source file destination file description
src/*.erl ebin/*.beam erlang source
src/*.app.src ebin/*.app otp application
c_src/*.c priv/.so port driver的c語言源代碼或者NIF共享鏈接庫
mibs/*.mib priv/mibs/*.bin SNMP 的 MIB 檔案
src/*.xrl src/*.erl leex 產生的檔案
src/*.yrl src/*.erl Yecc 產生的檔案
asn1/*.asn1 src/*.erl ASN-1文件
templates/*.dtl ebin/*_dtl.beam ErlyDTL模板文件 (需要額外安裝 ErlyDTL)
src/*.lfe ebin/*.beam LFE source code (需要額外安裝LFE)
src/*.peg ebin/*.beam Neotoma PEG 語法 source code (需要額外安裝Neotoma)
src/*.proto ebin/_pb.beam, include/_pb.hrl Protocol Buffers 參數(需要額外安裝protobuffs)

rebar.config 設定的選項

命令 設定的選項 description
compile erl_first_files 需要提前編譯的erlang源文件(例如behavior模塊)
compile erl_opts 編譯器支援的其他設定,請參閱文件
compile mib_first_files 需要提前編譯的mib文件列表 (例如, mib 文件中import部分的引用的RFC文件
compile src_dirs 列出其他包含erlang源文件的目錄
compile erlydtl_opts erlydtl 更多的支援的設定 ErlyDTL Templates
clean clean_files 需要在clean步驟刪除的文件列表,列出那些需要clean指令刪除的其他模塊的文件
doc edoc_opts edoc 支援的指令
eunit eunit_opts eunit支援的指令
eunit cover_enabled 開啟erlang的覆蓋率分析
eunit eunit_compile_opts Eunit編譯時用到的其他的選項
analyze dialyzer_opts 指定Dialyzer PLT 文件
build_plt dialyzer_opts 指定Dialyzer PLT 文件
check_plt dialyzer_opts 指定 Dialyzer PLT 文件
get-deps, delete-deps base_dir 為deps_dir 指定一個候選的目錄
get-deps, delete-deps deps_dir 設定存儲依賴檔的資料夾
get-deps, delete-deps deps 依賴的列表
generate target_dir 目標資料夾
generate overlay_vars Overlay variables file
xref xref_warnings 打開xref的警告
xref xref_checks Xref模塊中analyze/3支持的選項

目錄結構

rebar 遵循 OTP 的建議,包含了下列的資料夾:

  1. src
  2. ebin
  3. priv
  4. include
  5. test: eunit test source code
  6. c_src: C 語言寫的 Port Driver

Testing

rebar 支援 eunitcommon test 兩種 testing frameworks。

首先撰寫一些 eunit 測試的程式碼,在 apps/myapp/src/myapp_app.erl 的 -export 跟 程式的最後面,增加兩段 -ifdef(TEST) 到 -endif 的測試區塊。

-module(myapp_app).
-behaviour(application).

%% Application callbacks
-export([start/2, stop/1]).

-ifdef(TEST).
-include_lib("eunit/include/eunit.hrl").
-endif.

%% ===================================================================
%% Application callbacks
%% ===================================================================

start(_StartType, _StartArgs) ->
    myapp_sup:start_link().

stop(_State) ->
    ok.

-ifdef(TEST).

simple_test() ->
    ok = application:start(myapp),
    ?assertNot(undefined == whereis(myapp_sup)).

-endif.

因為直接執行 rebar eunit,連帶會對 deps 進行 eunit 的測試,所以增加 skip_deps 參數,只針對我們自己寫的程式進行測試。

rebar compile eunit skip_deps=true

如果要檢查 code coverage 的統計資料,我們必須在 rebar.config 增加以下設定

{cover_enabled, true}.

再執行一次 rebar compile eunit skip_deps=true ,就可以看到 code coverage analysis 結果寫在 apps/myapp/.eunit/index.html 檔案裡面。

其它

有關 deps, 建構 release package 以及升級的問題,就放在下一篇文章裡面說明。

2014年7月7日

erlang - cowboy - file upload

因為 cowboy 在目前 git master 版本跟最新的 0.9 release 兩個版本,在實作 http multi-part request 的處理上,給了兩個不相容的處理方式,而且 cowboy 又在 1.0 開發的過程中,作者也沒有時間給出很多範例以及參考的資料,再加上網路上搜尋到的解決方案,幾乎都是舊版的 cowboy 支援的方式,因此,我們試著研究把 multi-part http request 的範例程式寫出來。

我們參考了 cowboy multipart 這一份唯一的官方文件,另外在 cowboy source code 裡面,有一個 http_multipart 測試程式,由這兩個資料,我們可以組合出一個可以運作的 multi-part http request 的範例程式。

project

這個範例專案已經放在 github 了,可以直接由clone 這個 project cowboy_fileupload source code in github

project settings

這個範例遵循 cowboy 範例的作法,使用了 erlang.mk 以及 relx 這兩個工具。

專案要寫 Makefile 與 relx.config 兩個設定檔,Makefile 要將 cowboy master branch 設定為 dependent library,接下來在 make 時,才會自動下載這些 libraries。

  1. Makefile

     PROJECT = upload
    
     DEPS = cowboy
     dep_cowboy = pkg://cowboy master
    
     include erlang.mk
  2. relx.config

     {release, {upload_example, "1"}, [upload]}.
     {extended_start_script, true}.

static html index.html

靜態網頁要放在 priv 的目錄中,index.html 裡面寫了三個 html form,以下只列出最多的第三個 form,這些 form 的 action 都設定為 /upload 這個網址,cowboy 的 routing 要設定 /upload 的網址,由 multipart 的程式碼處理。

這個 form 有兩個 text 欄位,另外還有兩個 file 的欄位,而且是一個 text 與一個 file 間隔的順序,接下來我們作的 cowboy mutipart handler 必須要能根據欄位資料的型態,自動判斷是不是 text 或是檔案,而有不同的對應處理方式。

<form id="uploadForm3" action="/upload" method="POST" enctype="multipart/form-data">
    <h1>Upload Form 3</h1>
    description 3.1: <input type="text" id="desc3_1" name="desc3_1" /><br/>
    file 3.1: <input type="file" name="file3_1" /><br/>
    description 3.2: <input type="text" id="desc3_2" name="desc3_2"><br>
    file 3.2: <input type="file" name="file3_2" /><br/>
    <button type="submit">Submit</button>
</form>

source codes

  1. upload.app.src
    relx 工具會自動根據這個檔案,產生 OTP upload.app 設定檔,兩個檔案的差異只有 modules 欄位。upload.app.src填寫為 [] 空的 list,而 upload.app 自動由 relx 把相關的 modules 填寫上去了。

     {modules, [upload_app, upload_sup, upload_handler]},
  2. upload_sup.erl
    這是 OTP 的 supervisor 程式,基本上內容就跟其他 cowboy samples 一樣。

  3. upload_app.erl
    重點是 cowboy 的 routing 部份,/ 指定為靜態檔案,路徑在 upload 的 priv 路徑裡面的 index.html。

    而 /upload 路徑指派由 upload_handler 處理。

     Dispatch = cowboy_router:compile([
             {'_', [
                 {"/", cowboy_static, {priv_file, upload, "index.html"}},
                 {"/upload", upload_handler, []}
             ]}
         ]),
         {ok, _} = cowboy:start_http(http, 100, [{port, 8000}], [
             {env, [{dispatch, Dispatch}]}
         ]),
  4. uplaod_handler.erl

    這個檔案是 multipart 程式的處理重點,首先我們定義這個 handler 的 behaviour。

     -behaviour(cowboy_http_handler).

    接下來是實作三個 callback functions

     init/3, handle/2, terminate/3

    重點是 handle,acc_multipart 是處理 multipart的遞迴程式,最後取得的結果 Result,是所有 multipart 資料的 header 與 body 的 list,但因為有些欄位是檔案,我們沒有必要把檔案內容放到 body 裡面傳回來這裡,所以在 acc_multipart 裡面有特別把檔案的 body 改寫為固定的文字內容 filecontent。

     handle(Req, State) ->
    
         {Result, Req2} = acc_multipart(Req, []),
         io:format( "Result= ~p~n", [Result] ),
         {ok, Req3} = cowboy_req:reply(200, [
             {<<"content-type">>, <<"text/plain; charset=UTF-8">>}
         ], <<"OK">>, Req2),
         %%writeToFile(term_to_binary(Result)),
         {ok, Req3, State}.

    這裡把測試時取得的 Result 資料記錄下來。

     %% Result= [{[{<<"content-disposition">>,<<"form-data; name=\"desc3_1\"">>}],
     %%          <<"desc1">>},
     %%         {[{<<"content-type">>,<<"text/plain">>},
     %%           {<<"content-disposition">>,
     %%            <<"form-data; name=\"file3_1\"; filename=\"userlist1.txt\"">>}],
     %%         <<"filecontent\r\n">>},
     %%         {[{<<"content-disposition">>,<<"form-data; name=\"desc3_2\"">>}],
     %%          <<"desc2">>},
     %%         {[{<<"content-type">>,<<"text/plain">>},
     %%           {<<"content-disposition">>,
     %%            <<"form-data; name=\"file3_2\"; filename=\"userlist2.txt\"">>}],
     %%          <<"filecontent\r\n">>}]

    參考 cowboy multipart 裡面 Reading a multipart message 這一段的內容,我們可以用 cow_multipart:form_data 回傳的資料內容的不同,直接將 text 與 file 兩個區分開來。file 的部份可直接取得 content type: CType 與檔名 Filename。

    因為要把檔案寫入磁碟中,但當檔案超過 8MB 的時候,cowboy 不能一次把所有資料都傳給 stream_file 處理,因此搭配 stream_file 的檔案寫入的處理,我們把檔案開啟 file:open 跟關閉檔案 file:close 分別寫在 stream_file 的前面與後面。

     acc_multipart(Req, Acc) ->
         case cowboy_req:part(Req) of
             {ok, Headers, Req2} ->
                 [Req4, Body] = case cow_multipart:form_data(Headers) of
                     {data, _FieldName} ->
                         {ok, MyBody, Req3} = cowboy_req:part_body(Req2),
                         [Req3, MyBody];
                     {file, _FieldName, Filename, CType, _CTransferEncoding} ->
                         io:format("stream_file filename=~p content_type=~p~n", [Filename, CType]),
                         {ok, IoDevice} = file:open( Filename, [raw, write, binary]),
                         Req5=stream_file(Req2, IoDevice),
                         file:close(IoDevice),
                         [Req5, <<"skip printing file content">>]
                     end,
                 acc_multipart(Req4, [{Headers, Body}|Acc]);
             {done, Req2} ->
                 {lists:reverse(Acc), Req2}
         end.

    參考 cowboy multipart 裡面 Skipping unwanted parts 這一段的內容,我們知道 cowboy 在還沒取得所有上傳檔案的資料時,cowboy_req:part_body 就會先回傳給呼叫端,並用 more 為信號告訴 client 還需要再呼叫一次,取得檔案後面的資料。

     stream_file(Req, IoDevice) ->
         case cowboy_req:part_body(Req) of
             {ok, Body, Req2} ->
                 io:format("part_body ok~n", []),
                 file:write(IoDevice, Body),
                 Req2;
             {more, Body, Req2} ->
                 io:format("part_body more~n", []),
                 file:write(IoDevice, Body),
                 stream_file(Req2, IoDevice)
         end.

編譯與測試

  1. 編譯

     > make

    它會自動取得所有需要的 libraries 包含了 cowboy, cowlib, ranch 還有封裝工具 relx。

  2. 啟動

     > _rel/bin/upload_example console
  3. 測試

    瀏覽網頁 http://localhost:8000/