2018/03/26

Elixir 1 - Basic Data Types

安裝 elixir

Installing Elixir 記錄了如何在不同 OS 安裝 elixir 的方法。

我是透過 macport 安裝 elixir

sudo port install elixir

elixir 是用 Erlang/OTP 20 開發的,所以要注意是不是已經有安裝了 erlang @20

$ port installed |grep erlang
  erlang @20.1_0+hipe+ssl+wxwidgets (active)

CentOS 7 上安装 Elixir 文章提到可以從 elixir 的 github 下載 precompiled zip,解壓縮後就可以用了。

CentOS 7 上 Elixir 開發之環境配置

interactive shell: iex

iex 是可以直接 evaluate elixir expression 的互動 shell, h 是 help, i 可查看資料的資訊

$ iex
Erlang/OTP 20 [erts-9.0] [source] [64-bit] [smp:4:4] [ds:4:4:10] [async-threads:10] [hipe] [kernel-poll:false]

Interactive Elixir (1.5.1) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)> h

iex(2)> h(v/0)
Returns the value of the nth expression in the history.

iex(3)> h(IO)
Functions handling input/output (IO).

iex(4)> > i 123
Term
  123
Data type
  Integer
Reference modules
  Integer
Implemented protocols
  IEx.Info, Inspect, List.Chars, String.Chars

離開 iex 的方式有兩種

  1. Ctrl-C 兩次
  2. Ctrl-G 然後輸入 q 及 Retuen

Customize iex: 在 IEx.configure 可查看 iex 所有可調整設定的選項

iex(1)> h IEx.configure

把設定的相關程式放在 ~/.iex.exs 就可以使用, ex:

IEx.configure(colors: [ eval_result: [:red, :bright] ])
IEx.configure(inspect: [limit: 10])

編譯與執行

hello.exs

IO.puts "Hello, World!"
$ elixir hello.exs
Hello, World!

或是在 iex 裡面執行

iex(1)> c "hello.exs"
Hello, World!
[]

Pattern Matching

elixir 將 = 視為 pattern matching 的 operator

iex(4)> list = [1, 2, [ 3, 4, 5 ] ]
[1, 2, [3, 4, 5]]
iex(5)> [a, b, c ] = list
[1, 2, [3, 4, 5]]
iex(6)> c
[3, 4, 5]
iex(7)> [a, 1, c ] = list
** (MatchError) no match of right hand side value: [1, 2, [3, 4, 5]]

_ 是 ignore 的意思,這跟 erlang 一樣

iex(7)> [a, _, c ] = list
[1, 2, [3, 4, 5]]

Variable 可在不同的 expression 重新被 assgin 新的 value,但如果要強制使用舊的 value,可使用 ^ pin operator

iex(1)> a=1
1
iex(2)> a=2
2
iex(3)> ^a=1
** (MatchError) no match of right hand side value: 1

Immutable Data

在 mutable data 的程式語言中,count 傳入 function 後,可能會在該 function 內改變數值,但 Immutable Data 強調 count 數值不變的特性。

count = 99
do_something_with(count)
print(count)

elixir 會複製一份 list1 的資料,加到 4 後面,然後指定給 list2

iex(3)> list1 = [ 3, 2, 1 ]
[3, 2, 1]
iex(4)> list2 = [ 4 | list1 ]
[4, 3, 2, 1]

用不到的變數,會自動以 Garbage Collection 的方式回收掉。因為 elixir 會有很多 processes,每個 process 都有自己的 heap,每個 heap 都是各自獨立的。

Elixir Data Types

Built-in Types

  • Value types:
    • Arbitrary-sized integers
    • Floating-point numbers
    • Atoms
    • Ranges
    • Regular expressions
  • System types:
    • PIDs and ports
    • References
  • Collection types:
    • Tuples
    • Lists
    • Maps
    • Binaries
  • Functions

elixir 的檔案有兩種 extensions

  • ex: for compiled code
  • exs: for interpreted code

如果要寫 scripts 或是 test,要使用 .exs,其他狀況就用 .ex


  • Integers: 可寫成

  1. decimal: 123
  2. hexadecimal: 0xcafe
  3. octal: 0o777
  4. binary: 0b10110

  • Floating-point numbers

  1. 1.0
  2. 0.2456
  3. 0.314159e1
  4. 314159.0e-5

  • Atoms

erlang 的 atom 為小寫字母,變數為大寫,但在 elixir 是以 : 前置符號代表 atom

  1. :fred
  2. :is_binary?
  3. :var@2
  4. :<>
  5. :===
  6. :"func/3"
  7. :"long john silver"

  • Ranges: start..end

ex: 1..5

  • Regular expressions: ~r{regexp} 或是 ~r{regexp}opts,regular expression 是使用 PCRE 格式
iex(1)> Regex.run ~r{[aeiou]}, "caterpillar"
["a"]
iex(2)> Regex.scan ~r{[aeiou]}, "caterpillar"
[["a"], ["e"], ["i"], ["a"]]
iex(3)> Regex.split ~r{[aeiou]}, "caterpillar"
["c", "t", "rp", "ll", "r"]
iex(4)> Regex.replace ~r{[aeiou]}, "caterpillar", "*"
"c*t*rp*ll*r"
Opt Meaning
f 強制該 pattern 要出現在多行資料的第一行
g 支援命名的 groups
i case insensitive
m 如果有多行文字, ^ and $ 分別代表每一行文字的開始與結束。\A and \z 是這些文字的開頭與結束
s 可使用 . 代表任意字元
U 通常 * + 是 greedy,就是越多字元越好,但如果加上 U 就變成 ungreedy
u 可使用 unicode-specific patterns 例如 \p
x 可使用 extended mode—ignore whitespace and comments (# to end of line)

  • PID Ports

PID: a reference to a local or remote process Port: a reference to an external resource,例如 C 的 library

  • References

make_ref 會產生 a globally unique reference


  • Tuples

tuple: an ordered collection of values

  1. { 1, 2 }
  2. { :ok, 42, "next" }
  3. { :error, :enoent }

通常 :ok 代表 noerror

iex(1)> {status, file} = File.open("hello.exs")
{:ok, #PID<0.86.0>}
iex(2)> {status, file} = File.open("hello1.exs")
{:error, :enoent}
  • Lists
iex(3)> [ 1, 2, 3 ] ++ [ 4, 5, 6 ]
[1, 2, 3, 4, 5, 6]
iex(4)> [1, 2, 3, 4] -- [2, 4]
[1, 3]
iex(5)> 1 in [1,2,3,4]
true
iex(6)> "wombat" in [1, 2, 3, 4]
false

Keyword Lists: lists of key/value pairs,elixir 會轉換為 a list of two-value tuples:

[ name: "Dave", city: "Dallas", likes: "Programming" ]
[ {:name, "Dave"}, {:city, "Dallas"}, {:likes, "Programming"} ]

elixir 可省略 []

DB.save record, [ {:use_transaction, true}, {:logging, "HIGH"} ]
# 可寫成
DB.save record, use_transaction: true, logging: "HIGH"

以下為 list 與 tuple 的差異

iex(11)> [1, fred: 1, dave: 2]
[1, {:fred, 1}, {:dave, 2}]
iex(12)> {1, fred: 1, dave: 2}
{1, [fred: 1, dave: 2]}
  • Map
%{ key => value, key2 => value2 }

當 key 為 atoms,可使用 . 直接取到該 value

iex(13)> states = %{ "AL" => "Alabama", "WI" => "Wisconsin" }
%{"AL" => "Alabama", "WI" => "Wisconsin"}
iex(14)> colors = %{ :red => 0xff0000, :green => 0x00ff00, :blue => 0x0000ff }
%{blue: 255, green: 65280, red: 16711680}
iex(15)> %{ "one" => 1, :two => 2, {1,1,1} => 3 }
%{:two => 2, {1, 1, 1} => 3, "one" => 1}
iex(16)> colors.red
16711680
iex(17)> colors[:red]
16711680

同時存在 Map 以及 keyword list 的原因:Map 的 key 必須是唯一的,但 keyword lists 的 key 可以重複

  • Binary

可直接指定單一個 byte 內 不同寬度 bits 的數值

iex(1)> bin = << 1, 2 >>
<<1, 2>>
iex(2)> byte_size bin
2
iex(3)> bin = <<3 :: size(2), 5 :: size(4), 1 :: size(2)>>
<<213>>
iex(4)> :io.format("~-8.2b~n", :binary.bin_to_list(bin))
11010101
:ok
iex(5)> byte_size bin
1
  • Dates and Times

Date 是 ISO-8601 格式

iex(6)> d1 = Date.new(2016, 12, 25)
{:ok, ~D[2016-12-25]}
iex(7)> {:ok, d1} = Date.new(2016, 12, 25)
{:ok, ~D[2016-12-25]}
iex(8)> d2 = ~D[2016-12-25]
~D[2016-12-25]
iex(9)> d1 == d2
true
iex(10)> inspect d1, structs: false
"%{__struct__: Date, calendar: Calendar.ISO, day: 25, month: 12, year: 2016}"
iex(15)> {:ok, t1} = Time.new(12, 34, 56)
{:ok, ~T[12:34:56]}
iex(16)> t2 = ~T[12:34:56]
~T[12:34:56]
iex(17)> t1==t2
true
iex(18)> inspect t2, structs: false
"%{__struct__: Time, calendar: Calendar.ISO, hour: 12, microsecond: {0, 0}, minute: 34, second: 56}"
iex(19)> inspect t1, structs: false
"{:ok, %{__struct__: Time, calendar: Calendar.ISO, hour: 12, microsecond: {0, 0}, minute: 34, second: 56}}"
iex(20)> t3 = ~T[12:34:56.78]
~T[12:34:56.78]

其他規則

elixir 的 identifier 可用大小寫 ASCII characters, digits 及 _,可用 ? 當結尾

module, record, protocol, and behavior names 是以大寫 letter 開頭

source code 使用 UTF-8 encoding

註解以 # 開頭

Boolean operations 有三種 true, false, and nil. nil is treated as false in Boolean contexts.


Operators

  • Comparison

    a === b # strict equality (so 1 === 1.0 is false)
    a !== b # strict inequality (so 1 !== 1.0 is true)
    a == b # value equality (so 1 == 1.0 is true)
    a != b # value inequality (so 1 != 1.0 is false)
    a > b # normal comparison
    a >= b # :
    a < b # :
    a <= b # :
  • Boolean

    a or b # true if a is true, otherwise b
    a and b # false if a is false, otherwise b
    not a # false if a is true, true otherwise
  • Relaxed Boolean

    a || b # a if a is truthy, otherwise b
    a && b # b if a is truthy, otherwise a
    !a # false if a is truthy, otherwise true
  • Arithmetic operators

    + - * / div rem
  • Join operators

    binary1 <> binary2 # concatenates two binaries
    list1 ++ list2 # concatenates two lists
    list1 -- list2 # removes elements of list2 from a copy of list 1
  • in operator

    a in enum # tests if a is included in enum

Variable Scope: function body

with Expression

content = "Now is the time"
lp = with {:ok, file} = File.open("/etc/passwd"),
        content = IO.read(file, :all),
        :ok = File.close(file),
        [_, uid, gid] = Regex.run(~r/_lp:.*?:(\d+):(\d+)/, content) do
        "Group: #{gid}, User: #{uid}"
    end
IO.puts lp # => Group: 26, User: 26
IO.puts content

References

Programming Elixir

2018/03/19

Elixir

Elixir 是一個基於 Erlang Beam VM 的 functioncal concurrent programming language。Elixir 以 Erlang為基礎,支持分佈式、高容錯、實時應用程式的開發,也可通過巨集實現 meta programming 對其進行擴展,並通過 Protocol 支援多態。

以下是 Elixir 的一些特性

  • 2013年誕生
  • 基於Erlang虛擬機(BEAM)
  • 語法類似 Ruby,因為 Elixir 的創造者 José Valim(巴西人) 他是 Ruby on Rails 的核心團隊成員!有不少 Ruby 的開發者跑來學。(ref: An Interview with Elixir Creator José Valim)
  • 由 Elixir code 直接編譯為 Beam VM 的 binary code
  • 動態強型別語言 (變數型別不會在運算中自動轉型) ,類似 Ruby、Python 這類動態強型別語言
  • 可直接呼叫 erlang 開發的 module
  • 基於巨集的 meta programming 能力,語言的抽象語法樹
  • 基於 Protocol 的多態實現
  • 以 Message 互相傳遞資料
  • 支援 Unicode
  • 開發 Web 搭配 Phoenix Framework

erlang 有著平行處理的平台優勢,但是作為一個古老的語言,其語法會讓人難以接受。

Elixir解決了 erlang 的問題,有自己的程序包管理系統、Macro、易於使用的構建工具和Unicode處理機制,就像是一個新時代的語言,掛上了原本就性能卓越的引擎。

一開始接觸 elixir,通常會介紹 Pipe Operator

如果在 erlang 要針對某個 function 的回傳值放入另一個 function 當作參數,會寫成以下這樣。

list_to_atom(binary_to_list(capitalize_binary(list_to_binary(atom_to_list(X))))).

當然習慣了以後,為了避免老眼昏花的問題,通常會在中間加上幾個變數,把一行的語法改成 2~3 行,讓程式可讀性更好。

不過 elixir 借用了 unix 的 pipe 概念,用了 |> 這樣的 operator,所以上面的例子就會變成這樣

X |> atom_to_list |> list_to_binary |> capitalize_binary |> binary_to_list |> binary_to_atom

就這樣一個轉變,增加了程式可讀性

Sublime Text for Elixir

可使用 Sublime Text 加上 Plugin 支援 Elixir 語法

先安裝 Package Control

Command+Shift+P Package Control: Install Packages

安裝

  • ApplySyntax: 自動判斷文件類型
  • SublimeCodeIntel: 支援多種語言 Autocomplete
  • SublimeLinter: 支援多種語言 Linter
  • GitGutter: 側欄顯示 git diff
  • ExlixirSublime: 支援 Elixir 語法 Code completion and linter
  • SublimeLinter-contrib-elixirc: 支援 elixir linter

另外獨立安裝 Elixir-tmbundle

cd ~/Library/Application Support/Sublime Text 3/Packages
git clone git://github.com/elixir-lang/elixir-tmbundle Elixir

IDEA for Elixir

適合使用的 Plugin

  • intellij-elixir
  • AceJump: 移動 cursor
  • .ginore: 內建多種語言 .gitignore

References

Elixir:可能成為下一代Web開發語言

Lessons about the Elixir programming language, inspired by Twitter’s Scala School

5分鐘快速認識 Elixir 程式語言

Why the Elixir language has great potential

elixir libs

phoenix web framework

Unix 哲學:Elixir 將會替代 Go

Elixir 一個月深入使用感想

Elixir語言特性簡介

2018/03/12

Chaos Engineering

Chaos Engineering 是一個在分散式系統中進行實驗測試的準則,透過這樣的方法,可提升系統在正式環境中處理災難性異常的能力,對系統的穩定性更有信心。

大型的分散式系統已經改變了傳統的軟體工程方法,現在的網路服務目標都是以快速彈性的開發方式提供新的服務,對於開發人員來說,面對這樣複雜的服務系統,在將軟體更新到正式環境之前,到底有多大的信心,可以在更新後,不將系統弄壞。

在三十年前,Jim Gray 提出要提高可靠性 availability 的方法,就是使用驗證過的軟體跟硬體,然後就可以不用再管他。但現今對於提供 Internet Service 的公司來說,持續改變,增加新功能的網路服務,不能再用這樣的方法。

Netflix 在 2012/7/20 發佈了 Chaos Monkey 專案,它的作用是可以隨機刪除在正式環境中運作的 VM instance and container,原因是:要避免失敗最好的方法,就是平常就要經常失敗。這有點像是不定時的災難演習,讓工程師跟開發人員,平常就能習慣災難,這樣自然而然就能建造出一個穩定的系統,不會在意外真正發生時,人仰馬翻。

Chaos Monkey 只會在平日週一到週五 9:00~15:00 運作,Netflix 在應付一般的異常狀況,一兩個 VM 斷線並不會導致系統失效,當然如果真的發生問題,也能因為該問題而受惠,因為這樣的問題可以影響系統設計,並解決問題。

隨著這些網路服務大廠針對「正式環境的破壞」實驗越來越熟悉,他們認為這樣的破壞概念,應該要更正式地成為一個新興的服務及產業,也就是 "Chaos Enginerring",簡單地說,Choas Engineering 就是在分散式系統上的實驗工程,用來建立網路服務正式環境的災難處理能力。這些實驗包含硬體故障、客戶端服務量突然暴增、設定參數異常等等,這也是 PRINCIPLES OF CHAOS ENGINEERING 所要說明的內容。

CHAOS IN PRACTICE

Chaos Engineering 就是要簡化系統弱點實驗的程序。實驗要遵循以下四個步驟

  1. 定義"穩定狀態"的系統測量值,用來表示系統是否正常運作的量測數值
  2. 假設該穩定狀態會持續發生在實驗組及對照組
  3. 找出現實世界發生的災難事件,例如 server crash、硬體故障、網路異常等等
  4. 嘗試反證,無法找出實驗組及對照組的"穩定狀態"之間的差異。換句話說,就是嘗試不讓實驗組及對照組的"穩定狀態"之間的量測數據不同。

如果 "穩定狀態" 的數據越穩定,就表示系統讓人更放心。如果有未經測試的弱點,那就有改善的目標。

ADVANCED PRINCIPLES

要運用 Choas Engineering,還要注意以下的使用原則

  1. 面對 Steady State Behavior 提出假設

    要注意系統輸出的可量測數值,而不是系統的內部屬性。可透過 proxy 量測一段時間內的數據,系統的 throughput, error rate, latency percentiles 都是 steady state behavior 可觀察的 metrics。Chaos 可幫助我們證明系統穩定可靠,而不是去了解系統如何運作。

  2. 真實世界的意外事件

    Chaos variables 就是真實世界可能發生的事件,有可能是 server crash,軟體回應異常,網路流量異常上升,任何會影響 "steady state" 的事件,都是 Chaos experiment 的實驗變因。

  3. 在正式環境中進行實驗

    系統的行為會根據運作環境及網路流量而不同,為確保 Chaos experiment 是針對正式環境,強烈建議要用真正正式環境的 traffic 進行實驗。

  4. 自動化持續的實驗

    讓 chaos engineering 自動並持續在正式環境中運作

  5. 限制影響的範圍

    在正式環境實驗有可能會造成某些客戶使用的異常,但這些異常都只會造逞短暫的不便。

Netflix

Netflix 以 SPS: (stream) starts per second 作為量測系統健康狀態的目標,因為他們長久以來的經驗,已經能從 SPS 的統計報表中看出系統是不是有發生異常。

Netflix 使用以下這些實驗項目

  1. 停止 VM instance
  2. 在服務之間的請求中增加 latency
  3. 讓服務之間請求故障
  4. 讓內部服務故障
  5. 讓整個Amazon區域失效

另一個觀測值是每秒新帳號註冊數,電子商務網站可使用每秒完成的購買次數,廣告投放服務可使用每秒瀏覽的廣告數。

References

Chaos Engineering

Chaos工程

Netflix新放出來的開源工具Chaos Monkey

2018/03/05

Apache HTTP Client 4.5 Connection & State Management

以下是 Apache HTTPClient API 連線管理,以 cookie 處理 state management,另外有個 Fluent API,簡化了 HttpClient 的操作介面這三個部分的 tutorial。

Connection Management

因為多次建立 HTTP Connection 會產生過多不必要的 Overdead,需要提供 connection management 機制,re-use connection 執行多個 requests。HttpClient 支援 connection persistence 機制。

HttpClient 可經過多個 hops 的 routes,建立 connection,或是直接連到 target host,區分三種不同的 route: plain, tunneled and layered。

  1. plain routes 是直接連接目標 host,或是透過唯一一個 proxy 建立連線

  2. tunneled routes: 連接到一個 proxy,並透過多個 proxies 的 tunnel 連接到 host

  3. layered routes: 在既有的 connection 上以 layering a procotol 建立連線,這種方式能用在 tunnel 或是沒有透過 proxy 的直接連線。

Route Computation

RouteInfo interface 儲存既定路由(一或多個 steps/hops)的資訊。HttpRoute 實作了 RouteInfo,是 immutable 不能被修改,HttpTracker 是 mutable RouteInfo 實作,在 HttpClient 內部被使用,用來追蹤到目標 host 的 hops,HttpTracker 會在到達下一個 hop 時被更新。HttpRouteDirector 是用來計算 route 的下一個步驟的 helper class。

HttpRoutePlanner 是在 context 計算 route 的 interface,目前有兩個 implementations:

  1. SystemDefaultRoutePlanner 利用 java.net.ProxySelector 實作的,預設會取得 JVM 的 proxy 設定(由 system properties 或是 browser 取得)。

  2. DefaultProxyRoutePlanner 不透過 system property 或 browser 取得 proxy 設定,他會固定使用某一個 default proxy。


HTTPS 是透過 SSL/TLS protocol 在上面疊加 HTTP transport security。HTTP transport 也就是透過 layered over SSL/TLS connection。

Connection managers

HTTP connection 一次只能讓一個 thread 使用,HttpClient 透過實作了 HttpClientConnectionManager 介面的 HTTP connetion manager 管理 HTTP connection。Connection manager 作為產生新 HTTP connection 的 factory,管理 persistent connection 的 life cycle,確保一次只有一個 thread 使用該 connection。

如果一個http connection被釋放或者被consumer關閉了,底層的connection 將與 proxy 分離,重新交給 manager,即使服務使用者仍然持有對代理實例的引用,但是它不再能夠執行任何 I/O 操作或者改變實際連接的狀態。

public class Test {
    public static void main(String[] args) {
        HttpClientContext context = HttpClientContext.create();
        HttpClientConnectionManager connMrg = new BasicHttpClientConnectionManager();
        HttpRoute route = new HttpRoute(new HttpHost("kokola.maxkit.com.tw", 80));
        // Request new connection. This can be a long process
        ConnectionRequest connRequest = connMrg.requestConnection(route, null);
        try {
            // 等待 10 sec,看有沒有建立 connection
            HttpClientConnection conn = connRequest.get(10, TimeUnit.SECONDS);
            try {
                if (!conn.isOpen()) {
                    // 依照 rout info 建立連線
                    connMrg.connect(conn, route, 1000, context);
                    // mark it as route complete
                    connMrg.routeComplete(conn, route, context);
                }
                // Do useful things with the connection.
            } finally {
                connMrg.releaseConnection(conn, null, 1, TimeUnit.MINUTES);
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

BasicHttpClientConnectionManager 是一次只維護一個 connection 的 connection mananger,但還是要注意,一次只能有一個 thread 使用,BasicHttpClientConnectionManager 會在 connection 被關閉後,以相同的 route 再建立一次連線,如果 route 跟既有的 connection request 不同,就會產生 java.lang.IllegalStateException。

Pooling connection manager

PoolingHttpClientConnectionManager 針對每個 route 建立有限數量的連線,connection 是根據 route 建立 pool。

PoolingHttpClientConnectionManager cm = new PoolingHttpClientConnectionManager();
// Increase max total connection to 200
cm.setMaxTotal(200);
// Increase default max connection per route to 20
cm.setDefaultMaxPerRoute(20);
// Increase max connections for localhost:80 to 50
HttpHost localhost = new HttpHost("locahost", 80);
cm.setMaxPerRoute(new HttpRoute(localhost), 50);
CloseableHttpClient httpClient = HttpClients.custom()
        .setConnectionManager(cm)
        .build();

在不需要使用 HttpClient instance 時,需要注意要關閉 connection manager,確保所有 connetion 的資源都被釋放。

CloseableHttpClient httpClient = <...>
httpClient.close();

Multithreaded request execution

使用 PoolingHttpClientConnectionManager 後,HttpClient 可同時在多個 threads 執行多個 requests。如果 PoolingHttpClientConnectionManager 已經沒有多餘的 connection,就會 blocking connection 要求,設定 http.conn-manager.timeout 就可以確保不會無限期等待 connection request,而是丟出 ConnectionPoolTimeoutException。

雖然 HttpClient 是 thread-safe,可在多個 threads 之間共享,但建議每一個 thread 要維護自己的 HttpContext。

public class Test {

    public static void main(String[] args) {
        PoolingHttpClientConnectionManager cm=new PoolingHttpClientConnectionManager();
        //設置最大連接數不超過200
        cm.setMaxTotal(200);
        //每個路由默認的連接數20
        cm.setDefaultMaxPerRoute(20);
        CloseableHttpClient httpclient=HttpClients.custom()
                .setConnectionManager(cm)
                .build();
        String[] urisToGet= {
                "http://www.domain1.com/",
                "http://www.domain2.com/",
                "http://www.domain3.com/",
                "http://www.domain4.com/"
        };
        GetThread[] threads=new GetThread[urisToGet.length];
        for(int i=0;i<threads.length;i++) {
            HttpGet httpGet=new HttpGet(urisToGet[i]);
            threads[i]=new GetThread(httpclient,httpGet);
        }

        // start the threads
        for (int j = 0; j < threads.length; j++) {
            threads[j].start();
        }

        // join the threads
        try {
            for (int j = 0; j < threads.length; j++) {
                threads[j].join();
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    static class GetThread extends Thread {

        private final CloseableHttpClient httpClient;
        private final HttpContext context;
        private final HttpGet httpget;

        public GetThread(CloseableHttpClient httpClient, HttpGet httpget) {
            this.httpClient = httpClient;
            this.context = HttpClientContext.create();
            this.httpget = httpget;
        }

        @Override
        public void run() {
            try {
                CloseableHttpResponse response = httpClient.execute(
                        httpget, context);
                try {
                    HttpEntity entity = response.getEntity();
                } finally {
                    response.close();
                }
            } catch (ClientProtocolException ex) {
                // Handle protocol errors
            } catch (IOException ex) {
                // Handle I/O errors
            }
        }

    }
}

eviction policy

傳統 blocking I/O model 的問題是無法在 blocked I/O 操作時,針對 I/O event 進行回應,當 connection 回到 manager,會保持 alive,但無法監控 socket status,如果 connection 被 server 關閉,client side 無法偵測到 connection state 已經改變了。

HttpClient 透過檢測 connection 是否 stale 的方式來解決這個問題,但 stale connection check 並不是 100% reliable。另一個方式就是用專屬的 monitor thread 監控 evict connections,他會定時呼叫 ClientConnectionManager#closeExpiredConnections(),關閉 expired connections,並evict closed connections from pool (呼叫 ClientConnectionManager#closeIdleConnections())。

import org.apache.http.conn.HttpClientConnectionManager;

import java.util.concurrent.TimeUnit;

public class IdleConnectionMonitorThread extends Thread {

    private final HttpClientConnectionManager connMgr;
    private volatile boolean shutdown;

    public IdleConnectionMonitorThread(HttpClientConnectionManager connMgr) {
        super();
        this.connMgr = connMgr;
    }

    @Override
    public void run() {
        try {
            while (!shutdown) {
                synchronized (this) {
                    wait(5000);
                    // Close expired connections
                    connMgr.closeExpiredConnections();
                    // Optionally, close connections
                    // that have been idle longer than 30 sec
                    connMgr.closeIdleConnections(30, TimeUnit.SECONDS);
                }
            }
        } catch (InterruptedException ex) {
            // terminate
        }
    }

    public void shutdown() {
        shutdown = true;
        synchronized (this) {
            notifyAll();
        }
    }

}

connection keep alive strategy

HTTP spec 沒有規定 connection 要保持 alive 多久,有些 Server 使用非標準的 Keep-Alive header,一般 http server 都會設定一段時間沒有活動的 http connection 會被刪除,但不會通知 client side。

public class Test {
    public static void main(String[] args) {
        ConnectionKeepAliveStrategy myStrategy = new ConnectionKeepAliveStrategy() {

            public long getKeepAliveDuration(HttpResponse response, HttpContext context) {
                // Honor 'keep-alive' header
                HeaderElementIterator it = new BasicHeaderElementIterator(
                        response.headerIterator(HTTP.CONN_KEEP_ALIVE));
                while (it.hasNext()) {
                    HeaderElement he = it.nextElement();
                    String param = he.getName();
                    String value = he.getValue();
                    if (value != null && param.equalsIgnoreCase("timeout")) {
                        try {
                            return Long.parseLong(value) * 1000;
                        } catch (NumberFormatException ignore) {
                        }
                    }
                }
                HttpHost target = (HttpHost) context.getAttribute(
                        HttpClientContext.HTTP_TARGET_HOST);
                if ("www.server.com".equalsIgnoreCase(target.getHostName())) {
                    // Keep alive for 5 seconds only
                    return 5 * 1000;
                } else {
                    // otherwise keep alive for 30 seconds
                    return 30 * 1000;
                }
            }

        };
        CloseableHttpClient client = HttpClients.custom()
                .setKeepAliveStrategy(myStrategy)
                .build();
    }
}

connection socket factory

Http connection 內部使用 java.net.Socket 物件處理資料傳輸,透過 ConnectionSocketFactory interface 建立, 初始化, 連接 socket。可讓 HttpClient 提供建立 socket 的功能,預設是使用 PlainConnectionSocketFactory 建立 plain sockets。
 產生 socket 跟建立 conection 是兩件事,socket 可在 blocked connection operation 時被關閉。

public class Test {
    public static void main(String[] args) {
        HttpClientContext clientContext = HttpClientContext.create();
        PlainConnectionSocketFactory sf = PlainConnectionSocketFactory.getSocketFactory();

        try {
            Socket socket = sf.createSocket(clientContext);
            int timeout = 1000; //ms
            HttpHost target = new HttpHost("localhost");
            InetSocketAddress remoteAddress = new InetSocketAddress(
                    InetAddress.getByAddress(new byte[] {127,0,0,1}), 80);
            sf.connectSocket(timeout, socket, target, remoteAddress, null, clientContext);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

LayeredConnectionSocketFactory 是 ConnectionSocketFactory 的 extension,可在既有的 plain sokcet 建立 layered socket。socket layering 主要是用來透過 proxies 建立 secure sockets。
 HttpClient 提供 SSLSocketFactory 實作了 SSL/TLS layering。

Hostname Verification: HttpClient 可自訂要不要檢查儲存在 X.509 certificate 的 hostname,有兩種 javax.net.ssl.HostnameVerifier implementations。

  1. DefaultHostnameVerifier 相容於 RFC2818,hostname 必須符合 certificate 中的 alterntive names,如果 CN 中沒有指定 alternative name,則必須在 CN 中填寫 *
  2. NoopHostnameVerifier
 不使用 hostname verification
public HttpClient httpClient() {

    try {
        ConnectionSocketFactory plainsf = PlainConnectionSocketFactory.getSocketFactory();
        SSLContext sslContext = SSLContexts.custom()
                .loadTrustMaterial(null, new TrustSelfSignedStrategy())
                .build();
        LayeredConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(sslContext, NoopHostnameVerifier.INSTANCE);
        Registry<ConnectionSocketFactory> r = RegistryBuilder.<ConnectionSocketFactory>create()
                .register("http", plainsf)
                .register("https", sslsf)
                .build();

        HttpClientConnectionManager cm = new PoolingHttpClientConnectionManager(r);

        RequestConfig requestConfig = RequestConfig
                .custom()
                .setSocketTimeout(30000)
                .setConnectTimeout(30000).build();

        return HttpClients.custom().setConnectionManager(cm)
                .setDefaultRequestConfig(requestConfig).build();
    } catch (Exception e) {
        throw new RuntimeException("Cannot build HttpClient using self signed certificate", e);
    }
}

proxy configuration

可自訂 proxy host

HttpHost proxy = new HttpHost("someproxy", 8080);
DefaultProxyRoutePlanner routePlanner = new DefaultProxyRoutePlanner(proxy);
CloseableHttpClient httpclient = HttpClients.custom()
    .setRoutePlanner(routePlanner)
    .build();

可使用 JRE 標準的 proxy 設定

SystemDefaultRoutePlanner routePlanner = new SystemDefaultRoutePlanner(
ProxySelector.getDefault());
CloseableHttpClient httpclient = HttpClients.custom()
    .setRoutePlanner(routePlanner)
    .build();

也可以實作 RoutePlanner,建立 route

        HttpRoutePlanner routePlanner = new HttpRoutePlanner() {
            public HttpRoute determineRoute(
                    HttpHost target,
                    HttpRequest request,
                    HttpContext context) throws HttpException {
                return new HttpRoute(target, null, new HttpHost("someproxy", 8080),
                        "https".equalsIgnoreCase(target.getSchemeName()));
            }
        };
        CloseableHttpClient httpclient = HttpClients.custom()
                .setRoutePlanner(routePlanner)
                .build();

HTTP State Management

HTTP 本身是 stateless, request/response 的 protocol,沒有 session 的概念,後來Netscape 提出了 cookie 的概念,並送交標準化。

HttpClient 使用 Cookie interface 處理 cookie token。通常 cookie 包含了多個 name/value pair,有 domain 限制,另外有個 path 決定該 cookie 可使用的 urls,還有 maxmimum period of time。

BasicClientCookie cookie = new BasicClientCookie("name", "value");
// Set effective domain and path attributes
cookie.setDomain(".mycompany.com");
cookie.setPath("/");
// Set attributes exactly as sent by the server
cookie.setAttribute(ClientCookie.PATH_ATTR, "/");
cookie.setAttribute(ClientCookie.DOMAIN_ATTR, ".mycompany.com");

HttpClient 提供多個 CookieSpec,建議使用 Standard or Standard strict policy。

  1. Standard strict RFC 6265, section 4

  2. Standard 比 RFC 6265, section 4 的限制寬鬆一些,可相容於大部分的 servers

  3. Netscape draft (obsolete)

  4. RFC 2965 (obsolete)

  5. RFC 2109 (obsolete)

  6. Browser compatibility (obsolete)

  7. Default RFC 2965, RFC 2109, or Nescape draft 相容的規格,將會被 Standard 取代

  8. Ignore cookies

RequestConfig globalConfig = RequestConfig.custom()
        .setCookieSpec(CookieSpecs.DEFAULT)
        .build();
CloseableHttpClient httpclient = HttpClients.custom()
        .setDefaultRequestConfig(globalConfig)
        .build();
RequestConfig localConfig = RequestConfig.copy(globalConfig)
        .setCookieSpec(CookieSpecs.STANDARD_STRICT)
        .build();
HttpGet httpGet = new HttpGet("/");
httpGet.setConfig(localConfig);

如果需要用自訂的 cookie policy,就要實作 CookieSpec interface。

HttpClient 需要實作 CookieStore interface 的 persistent cookie store,預設是 BasicCookieStore,內部以ArrayList 實作。

// Create a local instance of cookie store
CookieStore cookieStore = new BasicCookieStore();
// Populate cookies if needed
BasicClientCookie cookie = new BasicClientCookie("name", "value");
cookie.setDomain(".mycompany.com");
cookie.setPath("/");
cookieStore.addCookie(cookie);
// Set the store
CloseableHttpClient httpclient = HttpClients.custom()
        .setDefaultCookieStore(cookieStore)
        .build();

Fluent API

自 HttpClient 4.2 開始,提供了新的 fluent interface 的 API,Fluent API 簡化了 HttpClient,也不需要處理連接管理、資源釋放等繁雜的操作。

使用 Fluent API 必須在 build.sbt 增加 fluent-hc

  "org.apache.httpcomponents" % "httpclient" % "4.5.4",
  "org.apache.httpcomponents" % "fluent-hc" % "4.5.4",
import java.io.IOException;
import java.net.URI;
import java.util.LinkedList;
import java.util.Queue;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

import org.apache.http.*;
import org.apache.http.client.*;
import org.apache.http.client.config.CookieSpecs;

import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.fluent.*;
import org.apache.http.client.utils.URIBuilder;
import org.apache.http.concurrent.FutureCallback;
import org.apache.http.entity.ContentType;
import org.apache.http.impl.client.BasicCookieStore;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.impl.cookie.BasicClientCookie;
import org.apache.http.util.EntityUtils;
import org.json.JSONObject;

/**
 * Easy to use facade API
 *
 * Fluent API 是共用的一個HttpClient實例(Executor.CLIENT),
 * 使用了 PoolingHttpClientConnectionManager,
 * MaxPerRoute: 100, MaxTotal: 200.
 */
public class FluentExample {
    public static void main(String[] args) throws Exception {
        // 使用URIBuilder來構造複雜的url
        URI uri = new URIBuilder().setScheme("http")
                .setHost("www.bing.com")
                .setPath("/dict/")
                .addParameter("a", "中文test123")
                .addParameter("b", "")
                .build();

        testFluentGet(uri.toString());
        testFluentPost("http://www.maxkit.com.tw/temp/test/json.html");
        testFluentWithContext();
        testFluentJsonResponse();

        // 多線程並發模式, 平均1.3毫秒發一個請求(共發10個)
        testFluentConcurrent("http://www.maxkit.com.tw/temp/test/json.html", 10);
    }

    private static void testFluentGet(String url) {
        try {
            String result = Request.Get(url)
                    .userAgent("Test")
                    .addHeader(HttpHeaders.ACCEPT, "a") // HttpHeaders包含很多常用的http header
                    .addHeader("AA", "BB")
                    .connectTimeout(1000)
                    .socketTimeout(1000)
                    .execute().returnContent().asString();
            System.out.println(result);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private static void testFluentPost(String url) {
        try {
            String result = Request.Post(url)
                    .version(HttpVersion.HTTP_1_1)
                    .useExpectContinue()
                    .addHeader("X-Custom-header", "stuff")
                    .bodyForm(Form.form().add("a", "abc123")
                            .add("b", "中文abc123").build(), Consts.UTF_8)
                    // 或者傳入自定義類型的body
                    // ContentType包含很多常用的content-type
                    // .bodyString("Important stuff 中文abc123", ContentType.DEFAULT_TEXT)
                    .execute().returnContent().asString();
            System.out.println(result);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private static void testFluentJsonResponse() {
        try {
            JSONObject result = Request.Get("http://www.maxkit.com.tw/temp/test/json.html")
                    .execute().handleResponse(new JsonResponseHandler());
            System.out.println(result.toString(4));
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 由於Fluent API默認是共用的一個HttpClient實例, 因此HTTP的session狀態本身就會被控制住.
     *
     * 如果想獲得更多的自定義選項, 可以使用Executor來控制.
     * 例如預先設置一個cookie, 保持多個請求的cookie是一致的, 這樣服務器就能夠識別出這些HTTP來自同一個用戶
     **/
    private static void testFluentWithContext() {
        // To maintain client-side state (cookies, authentication) between requests,
        // the fluent Executor helps by keeping a cookie store and setting up other types of authentication:
        CookieStore cookieStore = new BasicCookieStore();
        BasicClientCookie cookie = new BasicClientCookie("a", "b");
        // 必須設置domain, 請求會根據訪問的域名自動在請求header中添加屬於該域名的cookie(瀏覽器默認行為)
        cookie.setDomain(".bing.com");
        cookieStore.addCookie(cookie);


        RequestConfig config = RequestConfig.custom().setCookieSpec(CookieSpecs.DEFAULT).build();

        // 預先設置請求中需要包含的cookie
        // 更多的自定義可以使用
        // HttpClient httpClient = HttpClientBuilder.create().setMaxConnTotal(20).setMaxConnPerRoute(20);
        // Executor.newInstance(httpClient);
        Executor executor = Executor.newInstance(HttpClients.custom().setDefaultRequestConfig(config).build())
                .use(cookieStore);

        try {
            // 發送2個一樣的請求, 注意查看請求中cookie的情況
            Request request1 = Request.Get("http://www.maxkit.com.tw/temp/test/json.html");
            Request request2 = Request.Get("http://www.maxkit.com.tw/temp/test/json.html");

            String result1 = executor.execute(request1).returnContent().asString();
            System.out.println(result1);
            // 發送了第一個請求過後, executor會自動將response中的set-cookie補充的客戶端的cookie中去(這就是一般瀏覽器的行為)
            String result2 = executor.execute(request2).returnContent().asString();
            System.out.println(result2);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private static void testFluentConcurrent(String url, int count) throws InterruptedException {
        // Creates a thread pool that creates new threads as needed,
        // but will reuse previously constructed threads when they are available.
        // If no existing thread is available, a new thread will be created and added to the pool.
        // These pools will typically improve the performance of programs that
        // execute many short-lived asynchronous tasks.
        // Threads that have not been used for sixty seconds are terminated and
        // removed from the cache. Thus, a pool that remains idle for long
        // enough will not consume any resources.
        ExecutorService threadpool = Executors.newCachedThreadPool();
        // 如果不傳入ExecutorService線程池, 則直接採用多線程模式
        // Async async = Async.newInstance().use(threadpool);
        Async async = Async.newInstance();

        // 增大連接數量, 預防出現連接不夠用的情況
        int connMaxTotal = count * 2;
        // 自定義httpclient, 主要是設置連接池
        // MaxPerRoute: 每個路由(可以看作是每個URL)默認最多可佔用多少個連接
        // connMaxTotal: 連接池最大多少個連接
        HttpClient hc = HttpClients.custom().setMaxConnPerRoute(connMaxTotal).setMaxConnTotal(connMaxTotal).build();
        async.use(Executor.newInstance(hc));

        Request[] requests = new Request[count];
        for (int i = 0; i < count; i++) {
            requests[i] = Request.Get(url + "?_=" + i);
        }

        Queue<Future<Content>> queue = new LinkedList<Future<Content>>();
        // Execute requests asynchronously
        for (final Request request : requests) {
            Future<Content> future = async.execute(request, new FutureCallback<Content>() {
                public void failed(final Exception ex) {
                    System.out.println(ex.getMessage() + ": " + request);
                }
                public void completed(final Content content) {
                    System.out.println("Request completed: " + request);
                }
                public void cancelled() {
                }
            });
            queue.add(future);
        }

        while (!queue.isEmpty()) {
            Future<Content> future = queue.remove();
            try {
                future.get();
            } catch (ExecutionException ex) {
                ex.printStackTrace(System.err);
            }
        }
        threadpool.shutdown();
    }


}
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.StatusLine;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.HttpResponseException;
import org.apache.http.client.ResponseHandler;
import org.apache.http.entity.ContentType;
import org.apache.http.util.EntityUtils;
import org.json.JSONObject;

import java.io.IOException;

public class JsonResponseHandler implements ResponseHandler<JSONObject> {
    @Override
    public JSONObject handleResponse(HttpResponse response) throws ClientProtocolException, IOException {
        final StatusLine statusLine = response.getStatusLine();
        final HttpEntity entity = response.getEntity();
        if (statusLine.getStatusCode() >= HttpStatus.SC_MULTIPLE_CHOICES) {
            throw new HttpResponseException(statusLine.getStatusCode(),
                    statusLine.getReasonPhrase());
        }
        if (entity != null) {
            return new JSONObject(EntityUtils.toString(entity, ContentType.getOrDefault(entity).getCharset()));
        }
        return null;
    }
}

References

HTTPClient4.5.2學習筆記(二):連接管理(Connection management)