2018年9月17日

Phoenix_3_Ecto

Ecto 是 RDB 的 wrapper。他有個稱為 changesets 的功能,可保存所有對 database 的修改內容,他會封裝在寫入 RDB 前的 receiving external data, casting 及 validating 這些程序。

設定 Ecto

修改 /lib/rumbl/repo.ex

defmodule Rumbl.Repo do
    use Ecto.Repo, otp_app: :rumbl
end

修改 /lib/rumbl.ex,enable Ecto supervisor

defmodule Rumbl do
  use Application

  # See http://elixir-lang.org/docs/stable/elixir/Application.html
  # for more information on OTP Applications
  def start(_type, _args) do
    import Supervisor.Spec

    # Define workers and child supervisors to be supervised
    children = [
      # Start the Ecto repository
      supervisor(Rumbl.Repo, []),
      # Start the endpoint when the application starts
      supervisor(Rumbl.Endpoint, []),
      # Start your own worker by calling: Rumbl.Worker.start_link(arg1, arg2, arg3)
      # worker(Rumbl.Worker, [arg1, arg2, arg3]),
    ]

    # See http://elixir-lang.org/docs/stable/elixir/Supervisor.html
    # for other strategies and supported options
    opts = [strategy: :one_for_one, name: Rumbl.Supervisor]
    Supervisor.start_link(children, opts)
  end

  # Tell Phoenix to update the endpoint configuration
  # whenever the application is updated.
  def config_change(changed, _new, removed) do
    Rumbl.Endpoint.config_change(changed, removed)
    :ok
  end
end

設定 DB /config/dev.exs

# Configure your database
config :rumbl, Rumbl.Repo,
  adapter: Ecto.Adapters.MySQL,
  username: "root",
  password: "password",
  database: "rumbl",
  hostname: "localhost",
  pool_size: 10

產生資料庫

$ mix ecto.create

DB Schema and Migration

修改 /web/models/user.ex 的內容

defmodule Rumbl.User do
    use Rumbl.Web, :model
    schema "users" do
        field :name, :string
        field :username, :string
        field :password, :string, virtual: true
        field :password_hash, :string
        timestamps
    end
end

DSL 是用 Elixir macro 實作的,schema 及 field 讓我們指定 DB table 及 Elixir struc,每個 field 會關聯到 DB table 的一個欄位,以及 User struct 的 field。

另外有增加一個 virtual 欄位,這是 password 的 hash 前結果,virtual field 不需要對應到 DB table 的欄位,因為 DB 只要紀錄 hash 後的密碼,Ecto 會自動定義 Elixir struct,所以可以直接使用 %Rumbl.User{}。

在 /web/web.ex 裡面 model 定義為

  def model do
    quote do
      use Ecto.Schema

      import Ecto
      import Ecto.Changeset
      # 增加  , only: [from: 1, from: 2]
      import Ecto.Query, only: [from: 1, from: 2]
    end
  end

現在我們需要用 "migrations" 的功能,將 DB schema 的定義反應到實際的 DB 裡面,migration 會修改資料庫以符合 application 的需要。

$ mix ecto.gen.migration create_user
* creating priv/repo/migrations
* creating priv/repo/migrations/20170903125816_create_user.exs

打開 priv/repo/migrations/20170903125816createuser.exs ,填入 change 的 function 內容(create table: users)

defmodule Rumbl.Repo.Migrations.CreateUser do
  use Ecto.Migration

  def change do
    create table(:users) do
      add :name, :string
      add :username, :string, null: false
      add :password_hash, :string
      timestamps()
    end
    create unique_index(:users, [:username])
  end
end

Ecto.Migration API 提供了 create, remove, and change database tables, fields, and indexes 這些 functions。

$ mix ecto.migrate
[info] == Running Rumbl.Repo.Migrations.CreateUser.change/0 forward
[info] create table users
[info] create index users_username_index
[info] == Migrated in 0.0s

要注意,目前是針對 dev 環境的處理,如果要換別的環境,就要設定 MIX_ENV 這個環境變數。

利用 Repository 新增 data

利用 IEX 產生測試資料

$ iex -S mix
iex(1)> alias Rumbl.Repo
Rumbl.Repo
iex(2)> alias Rumbl.User
Rumbl.User


iex(3)> Repo.insert(%User{name: "Jose", username: "josie", password_hash: "<3<3elixir"})
[debug] QUERY OK db=2.7ms
INSERT INTO `users` (`name`,`password_hash`,`username`,`inserted_at`,`updated_at`) VALUES (?,?,?,?,?) ["Jose", "<3<3elixir", "josie", {{2017, 9, 3}, {13, 9, 55, 939011}}, {{2017, 9, 3}, {13, 9, 55, 941294}}]
{:ok,
 %Rumbl.User{__meta__: #Ecto.Schema.Metadata<:loaded, "users">, id: 1,
  inserted_at: ~N[2017-09-03 13:09:55.939011], name: "Jose", password: nil,
  password_hash: "<3<3elixir", updated_at: ~N[2017-09-03 13:09:55.941294],
  username: "josie"}}
  
  
iex(4)> Repo.insert(%User{name: "Bruce", username: "bruce", password_hash: "7langs"})
[debug] QUERY OK db=1.8ms
INSERT INTO `users` (`name`,`password_hash`,`username`,`inserted_at`,`updated_at`) VALUES (?,?,?,?,?) ["Bruce", "7langs", "bruce", {{2017, 9, 3}, {13, 10, 3, 333839}}, {{2017, 9, 3}, {13, 10, 3, 333851}}]
{:ok,
 %Rumbl.User{__meta__: #Ecto.Schema.Metadata<:loaded, "users">, id: 2,
  inserted_at: ~N[2017-09-03 13:10:03.333839], name: "Bruce", password: nil,
  password_hash: "7langs", updated_at: ~N[2017-09-03 13:10:03.333851],
  username: "bruce"}}
  
  
iex(5)> Repo.insert(%User{name: "Chris", username: "chris", password_hash: "phoenix"})
[debug] QUERY OK db=182.8ms
INSERT INTO `users` (`name`,`password_hash`,`username`,`inserted_at`,`updated_at`) VALUES (?,?,?,?,?) ["Chris", "phoenix", "chris", {{2017, 9, 3}, {13, 10, 13, 487813}}, {{2017, 9, 3}, {13, 10, 13, 487825}}]
{:ok,
 %Rumbl.User{__meta__: #Ecto.Schema.Metadata<:loaded, "users">, id: 3,
  inserted_at: ~N[2017-09-03 13:10:13.487813], name: "Chris", password: nil,
  password_hash: "phoenix", updated_at: ~N[2017-09-03 13:10:13.487825],
  username: "chris"}}
  
  
iex(6)> Repo.all(User)
[debug] QUERY OK source="users" db=2.6ms
SELECT u0.`id`, u0.`name`, u0.`username`, u0.`password_hash`, u0.`inserted_at`, u0.`updated_at` FROM `users` AS u0 []
[%Rumbl.User{__meta__: #Ecto.Schema.Metadata<:loaded, "users">, id: 1,
  inserted_at: ~N[2017-09-03 13:09:55.000000], name: "Jose", password: nil,
  password_hash: "<3<3elixir", updated_at: ~N[2017-09-03 13:09:55.000000],
  username: "josie"},
 %Rumbl.User{__meta__: #Ecto.Schema.Metadata<:loaded, "users">, id: 2,
  inserted_at: ~N[2017-09-03 13:10:03.000000], name: "Bruce", password: nil,
  password_hash: "7langs", updated_at: ~N[2017-09-03 13:10:03.000000],
  username: "bruce"},
 %Rumbl.User{__meta__: #Ecto.Schema.Metadata<:loaded, "users">, id: 3,
  inserted_at: ~N[2017-09-03 13:10:13.000000], name: "Chris", password: nil,
  password_hash: "phoenix", updated_at: ~N[2017-09-03 13:10:13.000000],
  username: "chris"}]
  
  
iex(7)> Repo.get(User, 1)
[debug] QUERY OK source="users" db=1.9ms
SELECT u0.`id`, u0.`name`, u0.`username`, u0.`password_hash`, u0.`inserted_at`, u0.`updated_at` FROM `users` AS u0 WHERE (u0.`id` = ?) [1]
%Rumbl.User{__meta__: #Ecto.Schema.Metadata<:loaded, "users">, id: 1,
 inserted_at: ~N[2017-09-03 13:09:55.000000], name: "Jose", password: nil,
 password_hash: "<3<3elixir", updated_at: ~N[2017-09-03 13:09:55.000000],
 username: "josie"}

從 mix phoenix.server 的 console log 也能發現,查詢的語法已經改用 SQL 連接 DB,而不是 memory DB。

$ mix phoenix.server

[info] GET /users
[debug] Processing by Rumbl.UserController.index/2
  Parameters: %{}
  Pipelines: [:browser]
[debug] QUERY OK source="users" db=2.2ms decode=4.6ms
SELECT u0.`id`, u0.`name`, u0.`username`, u0.`password_hash`, u0.`inserted_at`, u0.`updated_at` FROM `users` AS u0 []
[info] Sent 200 in 82ms

Building Forms

Phoenix form builder

修改 /web/controllers/user_controller.ex,增加 new 新增 User 的 function

defmodule Rumbl.UserController do
  use Rumbl.Web, :controller

  # 取得所有 users
  def index(conn, _params) do
    users = Repo.all(Rumbl.User)
    render conn, "index.html", users: users
  end

  # 顯示某個 id 的 user
  def show(conn, %{"id" => id}) do
    user = Repo.get(Rumbl.User, id)
    render conn, "show.html", user: user
  end

  alias Rumbl.User
  # 利用 changeset 功能新增 user
  def new(conn, _params) do
    changeset = User.changeset(%User{})
    render conn, "new.html", changeset: changeset
  end
end

修改 /web/models/user.ex

defmodule Rumbl.User do
  use Rumbl.Web, :model
  schema "users" do
    field :name, :string
    field :username, :string
    field :password, :string, virtual: true
    field :password_hash, :string
    timestamps()
  end

  # 參數: a User struct and parameters
  # User struct 傳入 cast,限制 name 及 username 都是必要欄位
  # 並將 requied & optional values 轉成 schema types
  # 接下來傳入 validate_length,檢查 username 資料長度

  # 如果沒有任何 parameters,必須傳入 empty map  %{}
  def changeset(model, params \\ :empty) do
    model
    |> cast(params, ~w(name username), [])
    |> validate_length(:username, min: 1, max: 20)
  end
end

注意 User.changeset 的部分,他會接收一個 struct 及 controller 的參數,回傳 Ecto.Changeset。

Changeset 可讓 Ecto 管理 record changes, cast parameters, perform validations。可使用 changeset 製作 customized strategy 處理各種資料修改的功能,例如新增使用者或更新資訊。

注意: 在舊版 Ecto 要用 :empty 當作 empty map,但在 Ecto 2.0 後,要改寫為 %{}

ref: Empty atom in Ecto changeset

接下來修改 /web/router.ex 的 scope

  scope "/", Rumbl do
    pipe_through :browser # Use the default browser stack

    get "/", PageController, :index
    resources "/users", UserController, only: [:index, :show, :new, :create]
  end

resources 是一個簡化的一組 REST action implementations,包含 create, read, update, delete

resources "/users", UserController

等同以下這些網址的定義

get "/users", UserController, :index
get "/users/new", UserController, :new
get "/users/:id", UserController, :show

get "/users/:id/edit", UserController, :edit

post "/users", UserController, :create
patch "/users/:id", UserController, :update
put "/users/:id", UserController, :update
delete "/users/:id", UserController, :delete

剛剛的定義用 only: 限制只使用某幾個 route 定義

resources "/users", UserController, only: [:index, :show, :new, :create]

可在 console 直接用這個指令查看整個 application 的 routes

$ mix phoenix.routes
page_path  GET   /           Rumbl.PageController :index
user_path  GET   /users      Rumbl.UserController :index
user_path  GET   /users/new  Rumbl.UserController :new
user_path  GET   /users/:id  Rumbl.UserController :show
user_path  POST  /users      Rumbl.UserController :create

新增網頁

<h1>New User</h1>

<%= form_for @changeset, user_path(@conn, :create), fn f -> %>
  <div class="form-group">
    <%= text_input f, :name, placeholder: "Name", class: "form-control" %>
  </div>
  <div class="form-group">
    <%= text_input f, :username, placeholder: "Username", class: "form-control" %>
  </div>
  <div class="form-group">
    <%= password_input f, :password, placeholder: "Password", class: "form-control" %>
  </div>
  <%= submit "Create User", class: "btn btn-primary" %>
<% end %>

http://localhost:4000/users/new

<h1>New User</h1>

<form accept-charset="UTF-8" action="/users" method="post"><input name="_csrf_token" type="hidden" value="VQAbE0ACaEdYJglWOgIbMioeICosAAAA60MX/k8+nSByckvzFyogeg=="><input name="_utf8" type="hidden" value="✓"><div class="form-group">
<input class="form-control" id="user_name" name="user[name]" placeholder="Name" type="text"></div>
<div class="form-group">
<input class="form-control" id="user_username" name="user[username]" placeholder="Username" type="text"></div>
<div class="form-group">
<input class="form-control" id="user_password" name="user[password]" placeholder="Password" type="password"></div>
<button class="btn btn-primary" type="submit">Create User</button></form>

另外有個 protocol: Phoenix.HTML.FormData,可分離畫面以及後端實作,Ecto.Changeset 也實作了這個 Protocol,並把 Table 的 data strcuture 直接轉換到 Phoenix form。

Creating Resources

  def create(conn, %{"user" => user_params}) do
    changeset = User.changeset(%User{}, user_params)
    # Repo.insert 的結果有兩種,正確時,會 redirect 到 users index 首頁
    # 錯誤時,會停留在 new.html 的畫面
    case Repo.insert(changeset) do
      {:ok, user} ->
        conn
        |> put_flash(:info, "#{user.name} created!")
        |> redirect(to: user_path(conn, :index))
      {:error, changeset} ->
        render(conn, "new.html", changeset: changeset)
    end
  end

修改 /web/templates/user/new.html.eex ,加上 error message

<h1>New User</h1>

<%= form_for @changeset, user_path(@conn, :create), fn f -> %>
  <%= if @changeset.action do %>
    <div class="alert alert-danger">
      <p>Oops, something went wrong! Please check the errors below.</p>
    </div>
  <% end %>

  <div class="form-group">
    <%= text_input f, :name, placeholder: "Name", class: "form-control" %>
    <%= error_tag f, :name %>
  </div>
  <div class="form-group">
    <%= text_input f, :username, placeholder: "Username", class: "form-control" %>
    <%= error_tag f, :username %>
  </div>
  <div class="form-group">
    <%= password_input f, :password, placeholder: "Password", class: "form-control" %>
    <%= error_tag f, :password %>
  </div>
  <%= submit "Create User", class: "btn btn-primary" %>
<% end %>

error_tag helper 定義在 web/views/error_helpers.ex 可顯示錯誤訊息。


$ iex -S mix

iex(1)> changeset = Rumbl.User.changeset(%Rumbl.User{username: "eric"})
#Ecto.Changeset<action: nil, changes: %{}, errors: [], data: #Rumbl.User<>,
 valid?: true>

iex(2)> import Ecto.Changeset
Ecto.Changeset

iex(3)> changeset = put_change(changeset, :username, "eric")
#Ecto.Changeset<action: nil, changes: %{}, errors: [], data: #Rumbl.User<>,
 valid?: true>
iex(4)> changeset.changes
%{}

iex(5)> changeset = put_change(changeset, :username, "ericname")
#Ecto.Changeset<action: nil, changes: %{username: "ericname"}, errors: [],
 data: #Rumbl.User<>, valid?: true>
iex(6)> changeset.changes
%{username: "ericname"}
iex(7)>  get_change(changeset, :username)
"ericname"

Ecto 使用 changesets 儲存 persistence 前後異動的資料。

References

Programming Phoenix

2018年9月10日

Phoenix_2_Controller_View_Template

首先 create 新的 project rumbl

$ mix phoenix.new rumbl --database mysql

修改 config/dev.exs 的 DB 設定,然後 create database

$ mix ecto.create

啟動 server

mix phoenix.server

或是

iex -S mix phoenix.server

網址在 http://localhost:4000

調整首頁 /web/templates/page/index.html.eex

<div class="jumbotron">
  <h2>Welcome to Rumbl.io</h2>
  <p class="lead">Rumble out loud.</p>
</div>

Create User

系統通常會以 User 開發為起點。首先定義 Rumbl.User module,包含欄位 id, name, username, password

/web/models/user.ex

defmodule Rumbl.User do
  defstruct [:id, :name, :username, :password]
end

為什麼要使用 struct 而不是 map 定義 User model?

使用 map,在撰寫 model 定義,並不會在編譯時發現有打錯字的狀況,另外也不能為所有欄位都填上預設值。

在 project folder 直接測試

$ iex -S mix
Generated rumbl app

# 可以使用 User
iex(1)> alias Rumbl.User
Rumbl.User

# 以 Map 定義 user
iex(2)> user = %{usernmae: "jose", password: "elixir"}
%{password: "elixir", usernmae: "jose"}

# 因為剛剛定義 user map 時,把 username 打錯了
iex(3)> user.username
** (KeyError) key :username not found in: %{password: "elixir", usernmae: "jose"}

# 如果用 User struct 就不會發生這個問題,還將其他欄位都填上預設值 nil
iex(3)> jose = %User{name: "Jose"}
%Rumbl.User{id: nil, name: "Jose", password: nil, username: nil}
iex(4)> jose.name
"Jose"

# 編譯時就會檢查欄位名稱
iex(5)>  chris = %User{nmae: "chris"}
** (KeyError) key :nmae not found in: %Rumbl.User{id: nil, name: nil, password: nil, username: nil} ....

# struct 就跟 map 一樣,差別只在於 struct 是一個擁有 __struct__ key 的 map
iex(5)> jose.__struct__
Rumbl.User

Working with Repositories

Repository 是一組儲存資料的 API,可快速建立測試資料的 data interface,待完成 view, template 後,就可以替換為完整的 database-backend repository。換句話說,可以將 data model 跟 database 完全分離。

修改 /lib/rumbl/repo.ex

defmodule Rumbl.Repo do
  #  use Ecto.Repo, otp_app: :rumbl

  # 所有 User 的資料
  def all(Rumbl.User) do
    [
      %Rumbl.User{id: "1", name: "Jose", username: "josie", password: "elixir"},
      %Rumbl.User{id: "2", name: "Bruce", username: "bruce", password: "pass"},
      %Rumbl.User{id: "3", name: "Chris", username: "chris", password: "phx"}
    ]
  end

  def all(_module), do: []

  # get user by id
  def get(module, id) do
    Enum.find all(module), fn map -> map.id == id end
  end

  # get user by a custom attribute
  def get_by(module, params) do
    Enum.find all(module), fn map ->
      Enum.all?(params, fn {key, val} -> Map.get(map, key) == val end)
    end
  end

end

將 /lib/rumbl.ex,關掉跟 Ecto 相關的 supervisor

#      supervisor(Rumbl.Repo, []),

檢查 User 的測試資料庫

$ iex -S mix
iex(1)> alias Rumbl.User
Rumbl.User
iex(2)> alias Rumbl.Repo
Rumbl.Repo

iex(3)> Repo.all User
[%Rumbl.User{id: "1", name: "Jose", password: "elixir", username: "josie"},
 %Rumbl.User{id: "2", name: "Bruce", password: "pass", username: "bruce"},
 %Rumbl.User{id: "3", name: "Chris", password: "phx", username: "chris"}]
 
iex(4)> Repo.all Rumbl.Other
[]

iex(5)> Repo.get User, "1"
%Rumbl.User{id: "1", name: "Jose", password: "elixir", username: "josie"}

iex(6)> Repo.get_by User, name: "Bruce"
%Rumbl.User{id: "2", name: "Bruce", password: "pass", username: "bruce"}

撰寫 Controller & View

修改 /web/router.ex,將 /users 及 /users/:id 對應到 UserController 的 :index 及 :show 兩個 functions

  scope "/", Rumbl do
    pipe_through :browser # Use the default browser stack

    get "/users", UserController, :index
    get "/users/:id", UserController, :show
    get "/", PageController, :index
  end

新增 /web/controllers/user_controller.ex

defmodule Rumbl.UserController do
  use Rumbl.Web, :controller

  def index(conn, _params) do
    users = Repo.all(Rumbl.User)
    render conn, "index.html", users: users
  end

end

/web/views/user_view.ex

defmodule Rumbl.UserView do
  use Rumbl.Web, :view

  alias Rumbl.User
  def first_name(%User{name: name}) do
    name
    |> String.split(" ")
    |> Enum.at(0)
  end

end

/web/templates/user/index.html.eex

<h1>Listing Users</h1>
<table class="table">
    <%= for user <- @users do %>
    <tr>
        <td><b><%= first_name(user) %></b> (<%= user.id %>)</td>
        <td><%= link "View", to: user_path(@conn, :show, user.id) %></td>
    </tr>
    <% end %>
</table>

因為剛剛有修改 /lib/rumbl/Repo.ex,因為 lib 目錄更新程式後不會自動 reload,記得要重新啟動 phoenix.server,修改後的程式才會有作用。

mix phoenix.server

Using Helpers

link function 封裝了很多有用的 functions,可用來處理很多 HTML structures。

link 的第二個參數是 keyword list

iex(1)> Phoenix.HTML.Link.link("Home", to: "/")
{:safe, [60, "a", [[32, "href", 61, 34, "/", 34]], 62, "Home", 60, 47, "a", 62]}

iex(2)> Phoenix.HTML.Link.link("Delete", to: "/", method: "delete")
{:safe,
 [60, "a",
  [[32, "data-csrf", 61, 34,
    "DSMgIAMOCzQqQTczRR0/ElEUFDIJJgAA8KZynKSYp9CZ0/MYaNCwDA==", 34],
   [32, "data-method", 61, 34, "delete", 34], [32, "data-to", 61, 34, "/", 34],
   [32, "href", 61, 34, "#", 34], [32, "rel", 61, 34, "nofollow", 34]], 62,
  "Delete", 60, 47, "a", 62]}

HTML helper 會放在每個 view 的最上層,Phoenix.HTML 負責處理 HTML functions in views

/web/web.ex 裡面 view 的區塊內容為

  def view do
    quote do
      use Phoenix.View, root: "web/templates"

      # Import convenience functions from controllers
      import Phoenix.Controller, only: [get_csrf_token: 0, get_flash: 2, view_module: 1]

      # Use all HTML functionality (forms, tags, etc)
      use Phoenix.HTML

      import Rumbl.Router.Helpers
      import Rumbl.ErrorHelpers
      import Rumbl.Gettext
    end
  end

ref: Phoenix.HTML Helpers for working with HTML strings and templates.

不能把自己做的 function 直接寫在 web.ex,要用 import 的方式處理

Showing a User

剛剛在 /web/router.ex 有定義這個 url route

get "/users/:id", UserController, :show

修改 /web/controllers/user_controller.ex,增加 show function,注意參數的部分為 %{"id" => id},這是從網址來的

defmodule Rumbl.UserController do
  use Rumbl.Web, :controller

  def index(conn, _params) do
    users = Repo.all(Rumbl.User)
    render conn, "index.html", users: users
  end

  def show(conn, %{"id" => id}) do
    user = Repo.get(Rumbl.User, id)
    render conn, "show.html", user: user
  end
end

新增 /web/templates/user/show.html.eex

<h1>Showing User (<%= @user.id %>)</h1>
<b><%= first_name(@user) %></b> (<%= @user.id %>)'s username is <b><%= @user.username %></b>

http://localhost:4000/users/1


Naming Conventions

當 Phoenix 在 controller 要 render templates 時,他會使用 controler module 名稱 Rumbl.UserController 參考到 view module 的名稱為 Rumbl.UserView,template 目錄在 /web/templates/user/。

在後面可知道如何 customize 這些 conventions


Nesting Templates,共用 template

新增 /web/templates/user/user.html.eex

<b><%= first_name(@user) %></b> (<%= @user.id %>)'s username is <b><%= @user.username %></b>

修改 /web/templates/user/show.html.eex

<h1>Showing User (<%= @user.id %>)</h1>
<%= render "user.html", user: @user %>

修改 /web/templates/user/index.html.eex

<h1>Listing Users</h1>
<table class="table">
    <%= for user <- @users do %>
    <tr>
        <td><%= render "user.html", user: user %></td>
        <td><%= link "View", to: user_path(@conn, :show, user.id) %></td>
    </tr>
    <% end %>
</table>

<%= render "user.html", user: user %> 這就是共用的部分

iex -S mix 測試 view

$ iex -S mix
iex(1)> user = Rumbl.Repo get Rumbl.User, "1"
** (SyntaxError) iex:1: syntax error before: get

iex(1)> user = Rumbl.Repo.get Rumbl.User, "1"
%Rumbl.User{id: "1", name: "Jose", password: "elixir", username: "josie"}
iex(2)> view = Rumbl.UserView.render("user.html", user: user)
{:safe,
 [[[[[[["" | "<b>"] | "Jose"] | "</b> ("] | "1"] | ")'s username is <b>"] |
   "josie"] | "</b>"]}
iex(3)> Phoenix.HTML.safe_to_string(view)
"<b>Jose</b> (1)'s username is <b>josie</b>"

每個 template 最後都會變成 render(template_name, assigns),因此 rendering template 就是 template name 及 function 的 pattern matching。

我們可以跳過整個 template 機制,自訂自己的 render clause,這也是 Rumbl.ErrorView 提供的自訂錯誤頁面的方式。

/web/views/error_view.ex

defmodule Rumbl.ErrorView do
  use Rumbl.Web, :view

  def render("404.html", _assigns) do
    "Page not found"
  end

  def render("500.html", _assigns) do
    "Internal server error"
  end

  # In case no render clause matches or no
  # template is found, let's render it as 500
  def template_not_found(_template, assigns) do
    render "500.html", assigns
  end
end

Phoenix.View module 也提供了 render view 的 functions,包含 a function to render 及 轉換 rendered template 為 string的 function。

Phoenix.View 會呼叫 view 的 render functions。

iex(5)> user = Rumbl.Repo.get Rumbl.User, "1"
%Rumbl.User{id: "1", name: "Jose", password: "elixir", username: "josie"}


iex(6)> Phoenix.View.render(Rumbl.UserView, "user.html", user: user)
{:safe,
 [[[[[[["" | "<b>"] | "Jose"] | "</b> ("] | "1"] | ")'s username is <b>"] |
   "josie"] | "</b>"]}


iex(7)> Phoenix.View.render_to_string(Rumbl.UserView, "user.html", user: user)
"<b>Jose</b> (1)'s username is <b>josie</b>"

Layouts

在 controller 呼叫 render 時,controller 會先 render the layout view,然後再依照 predefined markup 去 render the actual template

因為 layout 也是 view with templates,每個 template 會收到一些特定的變數,

查看 /web/templates/layout/app.html.eex 的內容有一段 <%= render @view_module, @view_template, assigns %>1

layouts 就是 HTML templates that embed an action’s HTML

  <body>
    <div class="container">
      <header class="header">
        <nav role="navigation">
          <ul class="nav nav-pills pull-right">
            <li><a href="http://www.phoenixframework.org/docs">Get Started</a></li>
          </ul>
        </nav>
        <span class="logo"></span>
      </header>

      <p class="alert alert-info" role="alert"><%= get_flash(@conn, :info) %></p>
      <p class="alert alert-danger" role="alert"><%= get_flash(@conn, :error) %></p>

      <main role="main">
        <%= render @view_module, @view_template, assigns %>
      </main>

    </div> <!-- /container -->
    <script src="<%= static_path(@conn, "/js/app.js") %>"></script>
  </body>

References

Programming Phoenix

Using MySQL with the Phoenix Framework

2018年9月3日

Phoenix_1_Installation

Phoenix 是一個 Productive. Reliable. Fast. 的 web framework,以 elixir 實作,運作在 erlang VM 上。

installation

  • erlang vm

  • elixir

  • hex: elixir's package manager

    mix local.hex
  • MySQL/PostgreSQL

  • Node.js for Assets

    必須要 v5.3.0 以上

    node --version
  • Phoenix

    $ mix archive.install https://github.com/phoenixframework/archives/raw/master/phoenix_new.ez
  • mix phoenix.new

    mix 會增加 phoenix.new 的選項

    $ mix -h
    ...
    mix phoenix.new       # Creates a new Phoenix v1.2.5 application

create hello project,預設 DB 是使用 PostgreSQL,我們改用 MySQL

$ mix phoenix.new hello --database mysql

* creating hello/config/config.exs
* creating hello/config/dev.exs
* creating hello/config/prod.exs
* creating hello/config/prod.secret.exs
* creating hello/config/test.exs
* creating hello/lib/hello.ex
* creating hello/lib/hello/endpoint.ex
* creating hello/test/views/error_view_test.exs
* creating hello/test/support/conn_case.ex
* creating hello/test/support/channel_case.ex
* creating hello/test/test_helper.exs
* creating hello/web/channels/user_socket.ex
* creating hello/web/router.ex
* creating hello/web/views/error_view.ex
* creating hello/web/web.ex
* creating hello/mix.exs
* creating hello/README.md
* creating hello/web/gettext.ex
* creating hello/priv/gettext/errors.pot
* creating hello/priv/gettext/en/LC_MESSAGES/errors.po
* creating hello/web/views/error_helpers.ex
* creating hello/lib/hello/repo.ex
* creating hello/test/support/model_case.ex
* creating hello/priv/repo/seeds.exs
* creating hello/.gitignore
* creating hello/brunch-config.js
* creating hello/package.json
* creating hello/web/static/css/app.css
* creating hello/web/static/css/phoenix.css
* creating hello/web/static/js/app.js
* creating hello/web/static/js/socket.js
* creating hello/web/static/assets/robots.txt
* creating hello/web/static/assets/images/phoenix.png
* creating hello/web/static/assets/favicon.ico
* creating hello/test/controllers/page_controller_test.exs
* creating hello/test/views/layout_view_test.exs
* creating hello/test/views/page_view_test.exs
* creating hello/web/controllers/page_controller.ex
* creating hello/web/templates/layout/app.html.eex
* creating hello/web/templates/page/index.html.eex
* creating hello/web/views/layout_view.ex
* creating hello/web/views/page_view.ex

Fetch and install dependencies? [Yn] y
* running mix deps.get
* running npm install && node node_modules/brunch/bin/brunch build

We are all set! Run your Phoenix application:

    $ cd hello
    $ mix phoenix.server

You can also run your app inside IEx (Interactive Elixir) as:

    $ iex -S mix phoenix.server

Before moving on, configure your database in config/dev.exs and run:

    $ mix ecto.create

設定 DB 連線資訊

config/dev.exs

config :hello, Hello.Repo,
  adapter: Ecto.Adapters.MySQL,
  username: "root",
  password: "password",
  database: "hello",
  hostname: "localhost",
  pool_size: 10

執行 ecto.create

$ mix ecto.create

啟動 phoenix

$ mix phoenix.server
[info] Running Hello.Endpoint with Cowboy using http://localhost:4000
16:42:29 - info: compiled 6 files into 2 files, copied 3 in 1.1 sec

或是用 IEx (Interactive Elixir) 啟動

$ iex -S mix phoenix.server

網址在 http://localhost:4000

Simple tutorial

在 /web/router.ex 裡面有一段 scope 的定義

  scope "/", Hello do
    pipe_through :browser # Use the default browser stack

    get "/", PageController, :index
  end

所有以 ./ 開頭的 route 都會符合這個規則,pipe_through :browser macro 負責處理一般 browser 的 requests。

增加 /hello 的網址,指定給另一個 Controller

    get "/hello", HelloController, :world
    get "/", PageController, :index

http://localhost:4000/hello 會出現錯誤訊息

UndefinedFunctionError at GET /hello
function Hello.HelloController.init/1 is undefined (module Hello.HelloController is not available)

新增 /web/controllers/hello/hello_controller.ex

defmodule Hello.HelloController do
  @moduledoc false

  use Hello.Web, :controller
  def world(conn, _params) do
    render conn, "world.html"
  end

end

再瀏覽一次 http://localhost:4000/hello 會出現不同的錯誤訊息

UndefinedFunctionError at GET /hello
function Hello.HelloView.render/2 is undefined (module Hello.HelloView is not available)

新增 /web/views/hello_view.ex

defmodule Hello.HelloView do
  use Hello.Web, :view
end

新增 /web/templates/hello/world.html.eex

<h1>From template: Hello world!</h1>

不需要重新啟動 server,reload 網頁就可以看到結果


修改 web/router.ex,把網址當作變數

get "/hello/:name", HelloController, :world

修改 hello_controller.ex

defmodule Hello.HelloController do
  @moduledoc false

  use Hello.Web, :controller
  # external 參數定義為 "name" => name,但內部卻是用atom,也就是 name: name,這是因為 atom table 不會被 GC。
  def world(conn, %{"name" => name}) do
    render conn, "world.html", name: name
  end

end

修改 world.html.eex

<h1>Hello <%= String.capitalize @name %>!</h1>

瀏覽網址 http://localhost:4000/hello/test


lib 跟 web 資料夾的用途

supervision trees 及 long-running processes 要放在 lib

web-related code 包含 models, views, templates, and controllers 這些放在 web

當 code reloading 功能打開時,web 資料夾裡面的程式在被修改後,會自動 reload,但是 lib 資料夾的程式並不會自動 reload,因此 lib 很適合放 long-running services,例如 Phoenix’s PubSub system, the database connection pool 或是自訂的 supervised processes。

.exs 是 Elixir scripts,不會編譯為 .beam files,因此 mix 設定檔 mix.exs 是用 script 而不是 .ex

Phoenix 支援master configuration 加上其他不同環境的設定檔,所以可看到主設定檔 config.exs 裡面有一段

import_config "#{Mix.env}.exs"

就是說明,這是透過 MIX_ENV 環境變數,判斷 prod/dev/test 三種環境,會嵌入不同環境的設定檔

dev.exs
prod.exs
test.exs

另外 production 環境有一個獨立另外嵌入的設定檔 prod.secret.exs,這是用來儲存 production 環境需要被保護的一些密碼,獨立的檔案可保持設定不會進入 git server。

Endpoint

config.exs 包含了 loggin, endpoint 的設定

Endpoint 是 web server 處理 connection 的介面,設定檔裡面只有一個 Hello.Endpoint

# Configures the endpoint
config :hello, Hello.Endpoint,
  url: [host: "localhost"],
  root: Path.dirname(__DIR__),
  secret_key_base: "g8c9YZ5dYGeA.....XkLz5",
  render_errors: [accepts: ~w(html json)],
  pubsub: [name: Hello.PubSub,
           adapter: Phoenix.PubSub.PG2]

而 /lib/hello/endpoint.ex 中 Hello.Endpoint 裡面包含了這些 chaing of functions 及 plugs

defmodule Hello.Endpoint do
    use Phoenix.Endpoint, otp_app: :hello
    
    plug Plug.Static, ...
    plug Plug.RequestId
    plug Plug.Logger
    plug Plug.Parsers, ...
    plug Plug.MethodOverride
    plug Plug.Head
    plug Plug.Session, ...
    plug Hello.Router
end

這在內部其實是用 pipeline 做的,Endpoints 就是對每一個 request 執行 chain of functions。

connection
    |> Plug.Static.call
    |> Plug.RequestId.call
    |> Plug.Logger.call
    |> Plug.Parsers.call
    |> Plug.MethodOverride.call
    |> Plug.Head.call
    |> Plug.Session.call
    |> Hello.Router.call

Endpoint 也是一個 plug,尤其他 plugs 組合而成。application 是以 series of plugs 組成,由 endpoint開始,以 controller 結束,最後一個 plug 是 controller,也就是 web/router.ex 定義的 controller。

connection
    |> endpoint
    |> plug
    |> plug
    ...
    |> router
    |> HelloController

通常 application 只需要一個 endpoint,但也沒有限制只能有一個,如果要支援 http 80 及 https 443 或是 特別的 8080 for admin,就可以增加 endpoints

Router Flow

web/router.ex 是由兩個部分組成的: pipelines 及 route table

defmodule Hello.Router do
  use Hello.Web, :router

  pipeline :browser do
    plug :accepts, ["html"]
    plug :fetch_session
    plug :fetch_flash
    plug :protect_from_forgery
    plug :put_secure_browser_headers
  end

  pipeline :api do
    plug :accepts, ["json"]
  end

  scope "/", Hello do
    pipe_through :browser # Use the default browser stack

    get "/", PageController, :index
  end

  # Other scopes may use custom stacks.
  # scope "/api", Hello do
  #   pipe_through :api
  # end
end

有時會需要針對不同的網址,提供不同的 set of tasks/transformations,例如 :browser 是針對 HTML,有提供取得 session 的方法,並有一個稱為 flash 的 user message system,這裡也提供 security service,例如 request forgery protection。

第二個 pipeline 為 :api,是針對 JSON 的 API,只會處理 JSON requests,如果像要轉換為只處理 XML,就要修改這裡的 plug。

hello application 只使用了 :browser,是用 pipe_through :browser 指定的。

一般的 Phoenix application 是這樣組成的

connection
|> endpoint
|> router
|> pipeline
|> controller
  • endpoint: functions for every request
  • connection: 會經過 named pipeline,針對幾種主要類型的 request提供一般化的 functions
  • controller: 會呼叫 data model 並透過 view template 產生頁面

Controllers, Views, and Templates

web 目錄為

└── web
    ├── channels
    ├── controllers
    │ ├── page_controller.ex
    │ └── hello_controller.ex
    ├── models
    ├── static
    ├── templates
    │ ├── hello
    │ │ └── world.html.eex
    │ ├── layout
    │ │ └── app.html.eex
    │ ├── page
    │ │ └── index.html.eex
    ├── views
    │ ├── error_view.ex
    │ ├── layout_view.ex
    │ ├── page_view.ex
    │ └── hello_view.ex
    ├── router.ex
    └── web.ex

最底層為 router.ex 及 web.ex,web.ex 定義了整個 application structure。

在後面會提到 channel 的部分。

  • Erlang VM 會提供 application scalability
  • endpoint 能過濾 static request,並 parse request 然後呼叫 router
  • browser pipeline 會處理 "Accept" header,取得 session,也能防止 Cross-Site Request Forgery(CSRF) 的攻擊

hello application 相關的檔案

connection                      # Plug.Conn
    |> endpoint                 # lib/hello/endpoint.ex
    |> browser                  # web/router.ex
    |> HelloController.world    # web/controllers/hello_controller.ex
    |> HelloView.render(        # web/views/hello_view.ex
        "world.html")           # web/templates/hello/world.html.eex

References

Programming Phoenix

Using MySQL with the Phoenix Framework

2018年8月27日

微型加密演算法 Tiny Encryption Algorithm (TEA)

Tiny Encryption Algorithm (TEA) 微型加密演算法是劍橋大學電腦實驗室的 David Wheeler 與 Roger Needham 在 1994年於 Fast Software Encryption workshop 發表的加密演算法。因為該演算法速度快,很容易實作,也很容易設計電路,因此常常被使用。

這個演算法在內地的很有名氣,主要因為騰訊 QQ 大量使用了這個演算法,進行資料加密的工作。

tea.c

原本找到的文件,提供了一個 C 語言的版本。因為 TEA 是針對 byte array 進行加解密,實作時要注意,是使用哪一種 CPU,如果是 Intel CPU,都是 Little Endian 的 byte order。

#include <stdio.h>
#include <stdint.h>

void encipher(unsigned long *const v,unsigned long *const w,
   const unsigned long *const k)
{
   register unsigned long y=v[0], z=v[1], sum=0;
   register unsigned long delta= 0x9E3779B9;
   register unsigned long a=k[0], b=k[1], c=k[2], d=k[3];
   register unsigned long n=32;

   while(n-->0)
      {
      sum += delta;
      y += (z << 4)+a ^ z+sum ^ (z >> 5)+b;
      z += (y << 4)+c ^ y+sum ^ (y >> 5)+d;
      }

   w[0]=y; w[1]=z;
}

void decipher(unsigned long *const v,unsigned long *const w,
   const unsigned long *const k)
{
   register unsigned long y=v[0], z=v[1];
   register unsigned long delta=0x9E3779B9;
   register unsigned long sum= delta * 32;
   register unsigned long a=k[0],b=k[1], c=k[2], d=k[3];
   register unsigned long n=32;

   /* sum = delta<<5, in general sum = delta * n */

   while(n-->0)
      {
      z -= (y << 4)+c ^ y+sum ^ (y >> 5)+d;
      y -= (z << 4)+a ^ z+sum ^ (z >> 5)+b;
      sum -= delta;
      }

   w[0]=y; w[1]=z;
}


#define htonl(A) ((((unsigned long)(A) & 0xff000000) >> 24) | \
    (((unsigned long)(A) & 0x00ff0000) >> 8) | \
    (((unsigned long)(A) & 0x0000ff00) << 8) | \
    (((unsigned long)(A) & 0x000000ff) << 24))

#define ntohl htonl
#define ENCRYPTED_VALUE_POSITION_IN_CONN_REQ 16

void printlong(unsigned long l) {
    unsigned long nl = ntohl(l);
    printf("%lX ", nl);
}

int main() {
    unsigned long u_auth_key[4] = {0x11223344, 0x55667788, 0x99AABBCC, 0xDDEEFF00};
    char random_num[8] = {0x72, 0x13, 0x8F, 0x90, 0x57, 0x68, 0x29, 0xaa };

    unsigned long encrypted_num[2] = {0};
    unsigned long * p_rand_num = (unsigned long *) random_num;
    char xnl_conn_req[24] = {0};
    // 由 big endiness 轉為 little
    *p_rand_num = ntohl(*p_rand_num);
    *( p_rand_num + 1) = ntohl(*(p_rand_num + 1));

    encipher(p_rand_num, encrypted_num, u_auth_key);
    // 由 little 轉為 big
    encrypted_num[0] = htonl(encrypted_num[0]);
    encrypted_num[1] = htonl(encrypted_num[1]);

    // printf("%lx %lx\n", encrypted_num[0], encrypted_num[1]);
    printlong(encrypted_num[0]);
    printlong(encrypted_num[1]);

}

但是編譯後發現,加密後的結果是錯誤的。原因在於 unsigned long 這個資料型別,因為該資料型別在 64位元 CPU 的機器上,資料長度超過 32 bits。解決方式是在編譯時,要加上 -m32 這個參數。

$ gcc -m32 tea.c
$ ./a.out
C5E65710 A9CA5E6F

tea2.c

TEA Wiki 也提供了一個 C 語言的加解密版本,可以注意到他使用了 uint32_t 這個資料型別,uint32_t 在 UNIX 系統上,是固定為 32-bit unsigned interger,不管 CPU 是 32bits 或是 64bits 都一樣,所以使用這個資料型別會比 long 來得好。

tea2.c

#include <stdio.h>
#include <stdint.h>

void encrypt(uint32_t* v, uint32_t key[4], uint32_t delta) {
  uint32_t v0=v[0], v1=v[1], sum=0, i;             // set up
  // uint32_t delta=0x12345678;                    // a key schedule constant
  for (i=0; i < 32; i++) {
    sum += delta;
    v0 += ((v1<<4) + key[0]) ^ (v1 + sum) ^ ((v1>>5) + key[1]);
    v1 += ((v0<<4) + key[2]) ^ (v0 + sum) ^ ((v0>>5) + key[3]);
  }
  v[0]=v0; v[1]=v1;
}

void decrypt (uint32_t* v, uint32_t key[4], uint32_t delta) {
    uint32_t v0=v[0], v1=v[1], i;  // set up
    // uint32_t sum=0x468ACF00;
    // uint32_t delta=0x12345678;  // a key schedule constant
    uint32_t sum = delta * 32;  // delta << 5

    // printf("%X %X %X\n", v0, v1, sum);
    for (i=0; i<32; i++) {
      v1 -= ((v0<<4) + key[2]) ^ (v0 + sum) ^ ((v0>>5) + key[3]);
      v0 -= ((v1<<4) + key[0]) ^ (v1 + sum) ^ ((v1>>5) + key[1]);
      sum -= delta;
      // printf("%X %X %X\n", v0, v1, sum);
    }
    v[0]=v0; v[1]=v1;
}

int main()
{
    uint32_t v[] = {0x72138F90, 0x576829AA};

    uint32_t key[4]={0x11223344, 0x55667788, 0x99AABBCC, 0xDDEEFF00};
    uint32_t delta=0x9E3779B9;

    printf("Original Values: ");
    printf("%X %X\n", v[0], v[1]);

    encrypt(v, key, delta);

    printf("Encrypted      : ");
    printf("%X %X\n", v[0], v[1]);

    decrypt(v, key, delta);
    printf("Decrypted      : ");
    printf("%X %X\n", v[0], v[1]);
    return 0;
}

執行結果如下

$ gcc tea2.c
$ ./a.out
Original Values: 72138F90 576829AA
Encrypted      : C5E65710 A9CA5E6F
Decrypted      : 72138F90 576829AA

tea.py

另外如果是 python,可以參考 C 語言的寫法,直接用 ctypes 進行開發。

tea.py

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

import sys
from ctypes import *

def encipher(v, k):
    y = c_uint32(v[0])
    z = c_uint32(v[1])
    sum = c_uint32(0)
    delta = c_uint32(0x9E3779B9).value
    n = 32
    w = [0,0]

    while(n>0):
        sum.value += delta
        y.value += ( z.value << 4 ) + k[0] ^ z.value + sum.value ^ ( z.value >> 5 ) + k[1]
        z.value += ( y.value << 4 ) + k[2] ^ y.value + sum.value ^ ( y.value >> 5 ) + k[3]
        n -= 1

    w[0] = y.value
    w[1] = z.value
    return w

def decipher(v, k):
    y = c_uint32(v[0])
    z = c_uint32(v[1])
    delta = c_uint32(0x9E3779B9).value
    sum = c_uint32(delta * 32)
    n = 32
    w = [0,0]

    while(n>0):
        z.value -= ( y.value << 4 ) + k[2] ^ y.value + sum.value ^ ( y.value >> 5 ) + k[3]
        y.value -= ( z.value << 4 ) + k[0] ^ z.value + sum.value ^ ( z.value >> 5 ) + k[1]
        sum.value -= delta
        n -= 1

    w[0] = y.value
    w[1] = z.value
    return w

if __name__ == "__main__":
    key = [0x11223344, 0x55667788, 0x99AABBCC, 0xDDEEFF00]
    v = [0x72138F90, 0x576829AA]

    enc = encipher(v,key)
    print "Original Values: " + ' '.join([format(i, 'x').upper() for i in v])

    print "Encrypted:       " + ' '.join([format(i, 'x').upper() for i in enc])

    dec = decipher(enc,key)
    print "Decrypted:       " + ' '.join([format(i, 'x').upper() for i in dec])

執行結果

$ python tea.py
Original Values: 72138F90 576829AA
Encrypted:       C5E65710 A9CA5E6F
Decrypted:       72138F90 576829AA

TEA.java

這是 Java 語言的版本

public class TEA {

    public static int[] encrypt(int[] block, int[] key) {
        int i = block[0];
        int j = block[1];
        int sum = 0;
        int delta = 0x9E3779B9;

        for (int k = 0; k < 32; ++k) {
            sum += delta;
            i += (j << 4 & 0xfffffff0) + key[0] ^ j + sum ^ (j >> 5 & 0x7ffffff) + key[1];
            j += (i << 4 & 0xfffffff0) + key[2] ^ i + sum ^ (i >> 5 & 0x7ffffff) + key[3];
        }

        block[0] = i;
        block[1] = j;

        return block;
    }

    public static int[] decrypt(int[] block, int[] key) {
        int i = block[0];
        int j = block[1];
        int delta = 0x9E3779B9;
        int sum = delta*32;   // sum=468ACF00
        // System.out.println("sum="+byteToUnsignedHex(sum));

        // System.out.println("i, j, sum="+byteToUnsignedHex(i)+", "+byteToUnsignedHex(j)+", "+byteToUnsignedHex(sum));
        for (int k = 0; k < 32; ++k) {
            j -= (i << 4 & 0xfffffff0) + key[2] ^ i + sum ^ (i >> 5 & 0x7ffffff) + key[3];
            i -= (j << 4 & 0xfffffff0) + key[0] ^ j + sum ^ (j >> 5 & 0x7ffffff) + key[1];
            sum -= delta;

            // System.out.println("i, j, sum="+byteToUnsignedHex(i)+", "+byteToUnsignedHex(j)+", "+byteToUnsignedHex(sum));
        }

        block[0] = i;
        block[1] = j;

        return block;
    }


    public static String byteToUnsignedHex(int i) {
        String hex = Integer.toHexString(i).toUpperCase();
        while(hex.length() < 8){
            hex = "0" + hex;
        }
        return hex;
    }

    public static String intArrToHex(int[] arr) {
        StringBuilder builder = new StringBuilder(arr.length * 8 + arr.length-1);
        for (int b : arr) {
            builder.append(byteToUnsignedHex(b));
            builder.append(" ");
        }
        return builder.toString();
    }

    public static void main(String[] args){
        int[] key = new int[]{0x11223344, 0x55667788, 0x99AABBCC, 0xDDEEFF00};
        int[] data = new int[]{0x72138F90, 0x576829AA};
        System.out.println("Original Values: "+intArrToHex(data));

        int[] enc = TEA.encrypt(data, key);
        System.out.println("Encrypted      : "+intArrToHex(enc));

        int[] dec = TEA.decrypt(enc, key);
        System.out.println("Decrypted      : "+intArrToHex(dec));
    }
}

執行結果

$ javac TEA.java
$ java TEA
Original Values: 72138F90 576829AA
Encrypted      : C5E65710 A9CA5E6F
Decrypted      : 72138F90 576829AA

References

Tiny Encryption Algorithm

Java/J2ME implementation of the Tiny Encryption Algorithm

TEA、XTEA、XXTEA加密解密算法

介紹XXTEA加密算法

XXTEA encryption arithmetic library

TEA和QQTEA

TEA加密算法java版

一個長整數各自表述 (in 64-bit system)

Tiny-Encryption-Algorithm

TEA算法在QQ中的應用

2018年8月20日

Lua Support in Wireshark

wireshark 是網路封包解析工具,但如果是新的或是自己開發的 protocol,就必須要自行開發封包解析的 plugin,讓 wireshark 也能看得懂新的協定的封包。

wireshark 是以 C 開發的,所以可以用 C 語言開發 plugin,但還有另一個比較快速的方式,就是使用 lua script 開發,Lua 在 wireshark 可支援撰寫 dissectors, post-dissectors and taps。

dissector 就是封包解析的 plugin,雖然可以用 lua 快速開發 dissector,但是執行速度還是比 C 語言開發的 plugin 慢。不過如果要快速完成 dissector 開發,lua 還是一個好的選擇,畢竟不需要花時間 compile, debug C dissector。

post-dissectors 是在每一個 dissector 執行後,可以運作的 plugin,可用來增加 dissection tree,建立自訂的 filtering mechanism。

taps 是針對被 dissected 的封包,用來收集資訊的 plugin。

installation

由 wireshark 官網下載安裝套件安裝後,在 About Wireshark 選單中,可查閱 wireshark 的資訊。如果在中間有看到 "with Lua 5.2.4" 的字串,就表示這個 wireshark 有支援 lua。

另外,要查閱 Folders 頁籤,要注意 Personal Lua Plugins, Global Lua Plugins, Global Configuration 的路徑

Global Configuration: /Applications/Wireshark.app/Contents/Resources/share/wireshark

Personal Lua Plugins: /Users/charley/.local/lib/wireshark/plugins

Global Lua Plugins: /Applications/Wireshark.app/Contents/PlugIns/wireshark

安裝 plugin

有三種方式

  1. 放在 Personal Lua Plugins 目錄中
  2. 修改 init.lua,以 dofile() 載入 lua script
  3. 在啟動 wireshark 的 command line 中,加上 "-X lua_script:pcap_file.lua" 參數

如果已經做好了 plugin,可以直接放在 Personal Lua Plugins 目錄中,複製進去後,就可以在 About Wireshark -> Plugins 頁籤,看到那些 lua script。

第二種方式,先找到 init.lua 的存放位置,在這台機器上是放在 Global Configuration 目錄中。

編輯 init.lua,可看到最後面是 dofile(DATA_DIR.."console.lua")

# 備份 init.lua
cp /Applications/Wireshark.app/Contents/Resources/share/wireshark/init.lua /Applications/Wireshark.app/Contents/Resources/share/wireshark/init.lua.bak

vi /Applications/Wireshark.app/Contents/Resources/share/wireshark/init.lua

.....
dofile(DATA_DIR.."console.lua")
--dofile(DATA_DIR.."dtd_gen.lua")

要注意 init.lua 的最前面 disable_lua 必須要是 false

disable_lua = false

if disable_lua then
    return
end

在 init.lua 最後面加上

LUA_SCRIPT_PATH="/Users/charley/.wireshark/lua/"
dofile(LUA_SCRIPT_PATH.."hello.lua")

編輯 hello.lua vi /Users/charley/.wireshark/lua/hello.lua

-- hello.lua
print("hello world!")

以 command line wireshark 測試

$ tshark -x lua_script:hello.lua
hello world!
Capturing on 'FireWire'

References

使用 wireshark 的 lua script

Lua編寫Wireshark插件實戰

Lua

Chapter 10. Lua Support in Wireshark

2018年8月13日

如何編譯 RPi kernel

以下紀錄如何編譯 Raspberry Pi Kernel

查詢 RPi 上安裝的 kernel 版本號碼

$ uname -a
Linux raspberrypi 4.14.34-v7+ #1110 SMP Mon Apr 16 15:18:51 BST 2018 armv7l GNU/Linux

找一台 x86 linux 電腦,以 cross-compile 方式重新編譯 RPi kernel,雖然可以直接在 RPi 編譯,但速度會很慢,因此還是選擇用 cross-compile 方式編譯。

RPi kernel 存放在 git repository 裡面

cd
mkdir raspberry
cd raspberry

確認有安裝 git, 並更新一些套件(需要安裝nss 相關和更新curl)

yum update git
yum update nss nss-util nspr
yum update curl

下載 kernel source

git clone https://github.com/raspberrypi/linux.git

下載 cross-compilers tools

git clone https://github.com/raspberrypi/tools

使用 tools/arm-bcm2708/gcc-linaro-arm-linux-gnueabihf-raspbian,設定環境變數 CCPREFIX

export CCPREFIX=/root/raspberry/tools/arm-bcm2708/gcc-linaro-arm-linux-gnueabihf-raspbian/bin/arm-linux-gnueabihf-

${CCPREFIX}gcc -v

在 GitHub 上面的 kernel 有許多不同的 branch,請選擇你想要使用的 branch 來使用。

cd linux
git branch -a

目前版本為

* rpi-4.14.y
  remotes/origin/HEAD -> origin/rpi-4.14.y

由 RPi 取得目前的 kernel 設定,因為 RPi 上沒有 config.gz,用以下指令產生 /proc/config.gz

sudo modprobe configs

將 /proc/config.gz 複製到要編譯 kernel 的機器上的 /root/raspberry/linux 目錄中

cd /root/raspberry/linux
scp pi@192.168.1.15:/proc/config.gz . 
gunzip -c config.gz > .config

用以下指令產生新的 kernel configuration,如果遇到新的功能,就會問你要不要支援

ARCH=arm CROSS_COMPILE=${CCPREFIX} make oldconfig

reset .config 裡面的 debug symbols,Enable the ‘DEBUGINFO’ option and don’t enable the DEBUGINFO_REDUCED option.

grep -v DEBUG_INFO < .config > newconfig 
mv newconfig .config 
ARCH=arm CROSS_COMPILE=${CCPREFIX} make oldconfig

以 menuconfig 微調設定,調整 kernel modules support。

make -j4 ARCH=arm CROSS_COMPILE=${CCPREFIX} menuconfig

編譯 kernel,如果是 multi-core cpu,可加上 -j<amount of cores> 加速編譯

ARCH=arm CROSS_COMPILE=${CCPREFIX} make

如果編譯過程中,有發生 compile error,打開錯誤的 source code,在發生錯誤的 function 前面加上

#pragma GCC optimize("-g0")

編譯完成後,必須檢查 symbols,用以下指令,查看 initutsns.name.release 的值

${CCPREFIX}gdb vmlinux 
print init_uts_ns.name.release

確認 kernel 有包含 symbols 後,將 RPi 需要的 modules 放在 modules 目錄

ARCH=arm CROSS_COMPILE=${CCPREFIX} INSTALL_MOD_PATH=../modules make modules_install

建立 uncompressed kernel image,並放到 RPi 的 tmp 目錄中

cd <raspberry pi downloads>/tools/mkimage 
./imagetool-uncompressed.py ../../linux/arch/arm/boot/zImage
scp kernel.img pi@raspberrypi:/tmp

將 modules 目錄,壓縮後,傳送到 RPi 的 tmp 目錄中

cd <raspberry pi downloads>/tools/modules 
tar czf modules.tgz * 
scp modules.tgz pi@raspberrypi:/tmp

最後 ssh 到 RPi,安裝新的 kernel 及 modules

cd / 
sudo mv /tmp/kernel.img /boot/ 
sudo tar xzf /tmp/modules.tgz 
rm /tmp/modules.tgz

reboot RPi

sudo shutdown -r now

以 uname 檢查是否使用了新版的 kernel

uname -r

如果要 debug kernel ,可參考 Preparing Raspberry PI for JTAG Debugging 的說明

References

https://sysprogs.com/VisualKernel/tutorials/raspberry/buildkernel/

https://blog.gtwang.org/iot/raspberry-pi-compile-linux-kernel/

https://www.raspberrypi.com.tw/528/compiling-kernel-for-raspberry-pi/

https://coldnew.github.io/f5873b3f/

2018年8月6日

Architect 軟體架構師該做什麼事情?

架構 architecture 這個名詞是源於建築學,就是指建築物在設計上,是如何利用內部的支撐物相互結合,而製作出一個穩固的建築物的方法。而架構師 architect 則是為滿足某種架構設計目標,而進行整體建物的構思與設計的角色。在軟件系統工程領域,借用了建築學的 architecture 以及 architect 這兩個名詞,將軟體系統架構設計者,也稱為 architect。

Architect 這個角色跟 Project Manager 最根本的不同點,是 Project Manager 偏向管理面,比較重視專案的進度、成本、資源等管理項目,而 Architect 雖然也會進行管理工作,但只針對技術面,實作面,負責掌握所有開發者的做事方式及進度,有點像是 Technology Project Manager 的角色。

梓人傳

唐代的柳宗元,撰寫了一篇文章「梓人傳」,非常貼切地描寫了一位房屋建築師的工作寫照,文章的第一段,就明確地讓梓人自述他平常的工作內容跟收入:「吾善度材。視棟宇之制,高深圓方短長之宜,吾指使而群工役焉。舍我,眾莫能就一宇。故食於官府,吾受祿三倍;作於私家,吾收其宜大半焉。」語譯為「我擅長計算材料,看房子的規格,知道高深圓方短長所需要的木材。我指揮工人們去工作。沒有我,工人們就沒有人能蓋好一幢房子。如果我在政府機關裏做事,我領別人三倍的薪水。在私人住宅裡工作,我收的工錢也比別人多一大半。」

作者還是覺得這個人很奇怪的,為什麼不是拿著刀鋸斧斤,只會用尺規墨斗,領到的錢還比其他人多。甚至舉了例子,梓人家裡的椅子壞了,還要找工人來修理。後來作者實際到了梓人工作的地方,觀察了很久,發現他指揮大家做事,掌握進度,調度工人。最後完工後,還寫上他的名字以示負責。

軟體工程感覺上跟建築業很像,也需要一個 architect 出面,進行指揮調度的工作。不過有一點最大的差異,建築業的 architect 只需要進行設計,還能掌握成品的品質,就算他從來沒拿過鋸子,不拿工具,也可以完成工作。而軟體開發比較不一樣,沒有真正實作過一個軟體系統的經驗,是沒辦法直接用眼睛看,就看出軟體開發的奧秘,所以基本上,不大可能會出現一個從來沒有寫過程式的 architect。

架構師該做什麼?

接下來由不同的文章,來看看軟體架構師應該要做什麼,要會什麼?

抽象、分層、分治和演化思維

優秀架構師必須掌握的架構思維

架構的本質是管理複雜性,抽象、分層、分治和演化思維是架構師應對和管理複雜性的四種最基本武器。

抽象:架構師先要在大腦中形成抽象概念,然後是子模塊分解,然後是依次實現子模塊,最後將子模塊拼裝組合起來,形成最後系統。

分層:把整個系統劃分成若干個層次,每一層專註解決某個領域的問題,並向上提供服務。有些層次是縱向的,它貫穿所有其它層次,稱為共享層。分層也可以認為是抽象的一種方式,將系統抽象分解成若干層次化的模塊。

分治:分而治之(divide and combine或者split and merge) 把大問題分解成若干個子問題,如果子問題還無法直接解決,則繼續分解成子子問題,直到可以直接解決的程度,這個是分解(divide)的過程;然後將子子問題的解組合拼裝成子問題的解,再將子問題的解組合拼裝成原問題的解,這個是組合(combine)的過程。

架構既是設計出來的,同時也是演化出來的,對於互聯網系統,基本上可以說是三分設計,七分演化,而且是在設計中演化,在演化中設計,一個不斷迭代的過程。從單塊架構開始,隨著架構師對業務域理解的不斷深入,也隨著業務和團隊規模的不斷擴大,漸進式地把單塊架構拆分成微服務架構的思路,這就是演化式架構的思維。

廣度、高度、深度、寬度

如何成為一個架構師?

廣度指的是架構師應該對所在領域的主流技術體系有一個全面清晰的認識,每一種技術不需要很深入的瞭解,但必須知道每種技術的3W:1,Why:每種技術的由來,為什麼會出現這種技術,這個技術是用來解決什麼問題的?2,What:每種技術是什麼?技術的基本組成部分是什麼?3,Which:解決同一問題的相同技術各自的優缺點是什麼,更適合哪種場景?比如,ORM框架(Hibernate與IBatis),MVC框架(Struts與SpringMVC),大數據技術(Hadoop與Spark)它們各自的優缺點是什麼,只有清晰認識同一類型技術的優缺點,才能在技術選型時能夠使用更加合理的技術。

高度指的是架構師應具備對客觀事物的拔高能力,能夠從紛繁雜亂的信息中建立秩序,也就是我們一般所說的抽象能力。

深度指的是架構師能對主流技術有較為深入的理解,可以不瞭解源代碼,但對主流技術的原理,運作機理有一個基本的理解。

寬度指的是架構師能夠熟知當前的技術前沿和熱點,能夠使用新的技術解決問題。比如,微服務、大數據、雲計算、人工智能等。

廣度決定了系統架構技術選型的合理性 高度決定了系統架構設計的合理性 深度決定了系統架構的優化能力 寬度決定了系統架構的領先性,不至於三五年被淘汰

確認需求、系統分解、技術選型、制定技術規格

如何成為一個架構師?

確認需求:架構師要懂得用戶需求,理解用戶真正想要什麼,這使得架構師必須要和分析人員不斷溝通,反覆確認需求規格說明書,以此來保證他精準清楚用戶需求。

系統分解:在架構師認可需求規格說明書後,架構師已明確用戶需求是是什麼,這時候便看架構師的分解能力了。一般分為縱向分解和橫向分解,縱向分解是將整個系統分層,從而將整體系統分解成下一級的子系統與組件。橫向分解是在系統分解成不同的邏輯層或服務後,對邏輯層進行分塊,確定層與層之間的關係。

技術選型:在系統分解後,架構師會最終形成軟件整體架構,接下來,架構師的職責是技術選型。「前端到底用瘦客戶端還是富客戶端呢?數據庫是用MySQL還是MSSQL又或是Oracle呢?」在瞭解用戶需求後,分解完系統後,技術選型是非常重要的環節,提出各個方向,再進行評估。架構師在技術選型階段會提供參考信息給專案經理,專案經理再從預算、進度、人力、資源等各方面情況來權衡,最終確認。

制定技術規格:架構師在項目開發過程中是「靈魂人物」,並且要具備協調組織能力和懂得人員分工。在制定技術規格說明階段,架構師要協調起所有的開發人員,架構師通常會用技術規格說明書與開發人員保持溝通,讓開發人員能從各個視角去觀測、理解他們負責的模塊或者子系統,確保開發人員能夠按照架構意圖實現各項功能。

設計能力、技術實力、溝通能力

如何成為一個架構師?

設計能力:擅長整合分析架構是過程,並非結果。架構是架構師洞察內在結構、原則、規律與邏輯的過程,架構師要做到清晰理解系統,以及簡潔描述,這是分析整合的能力。一個架構師必須具備極強的分析能力,要做到根據產品宗旨和目標,分析清楚產品定位以及產品業務,再整合利用現有的技術領域,找出最佳方案,實現產品概念。

技術實力:實現產品規劃架構師首先要將代碼寫的清晰易懂,要能夠實現功能,做到沒有Bug,這要求架構師必須具備至少熟練掌握一門語言。這是最重要的,每一名出色的架構師,必定是一位優秀程序員。架構師並不是純粹的管理崗位,對那些愛寫各式文檔、畫流程圖、脫離代碼、只說不做、高高在上的架構師,程序員們通常會稱他們為——PPT 架構師。不懂編程的架構師的職業生涯必定是短暫的,無論如何都不可本末倒置,要想實現自己的職業規劃,不能荒廢自己本身的技能,技術是架構師賴以生存的最基本能力。

溝通能力:能夠橫向溝通架構師必須參與項目開發全過程,包括確認需求、系統分解、架構設計、技術選型、制定技術規格說明、系統實現、集成測試和部署各階段,在這一系列過程中,架構師會與各部門溝通交流。一個產品會有多部門合作,架構師在其中的溝通極為重要,直接影響產品進度與質量。架構師不僅要與開發人員溝通,也要和項目經理、分析人員甚至用戶溝通,來實現產品的各種可能性。所以,對於架構師來講,不僅有技術方面的要求,還有能夠橫向溝通的要求。

什麼是架構?架構師的職責是什麼?

設計能力:擅長整合分析 技術實力:實現產品規劃 溝通能力:能夠橫向溝通

References

四個架構設計案例分析及其背後的架構師思維

{程序員邀稿} 從軟體架構師(Architect)的觀點來看軟體開發流程

淺談技術型專案經理和管理型專案經理

2018年7月30日

chatbot 不能笨得像是問券調查,也不能太聰明像是真人

使用者已經厭煩不斷地安裝手機 APP,安裝了 APP 卻又很少打開來使用,自 2016 年 4 月 Facebook、Line 陸續發表聊天機器人 API 後,情況似乎有點改變,整個行銷活動,轉向了 chatbot,使用者在每天持續打開FB或是Line的狀況下,透過 chatbot,讓所有品牌跟廠商連接到了使用者那端。

過了 2017 到現在 2018 兩年,chatbot 的戰場可說是百家爭鳴,基本的核心 IM 平台已經很難有新進的服務,也沒有什麼改變,為了搶食 IM 平台上跟終端使用者聯繫的最後一哩路,圍繞在 IM API 上提供的服務及工具。

觀察一個 chatbot 的成功案例

AsiaYo 如何在一週內打造破萬使用者的 Chatbot!這篇文章中,說明了AisaYo 如何透過聊天機器人連貫起使用者之旅程體驗。他們以 UX 和商業的考量去定義 AsiaYo Chatbot 所應該賦予的功能,希望能協助旅客在訂房的前、中、後做決策及輔助。AsiaYo chatbot 經過了兩個版本的歷程。他們認為在旅遊訂房產業適用的設計的重點是:提醒與無縫式對話情境。也就是情境式的對話體驗,以及引導式的交談結構。

過程中的重點是在使用者瀏覽 AisaYo 旅遊資訊時,透過 cookie 收集資料,並在使用者瀏覽途中,彈出一則模擬 Facebook 對話視窗。如使用者同意接受對談,就將對話內容轉至 chatbot,並繼續以聊天機器人進行對話。且在使用者二次造訪 AsiaYo 網站時,便不再傳送訊息。

儘管 AsiaYo 利用模擬對話提示使用者,但仍然有少數消費者和 Chatbot 互動的過程中仍會察覺到自己被追蹤,並且對於追蹤的技術感到疑惑,造成使用者誤會侵犯隱私,加上對話的形式也會接續所追蹤的內容,所以少數消費者收到通知時會一頭霧水,此時的使用者體驗反而會降低。

在這個開發的過程中,可發現兩個重點,首先 chatbot 必須要夠聰明,不能太笨,一問一答當中,都是用固定的話術套路,交談的文字語氣,會影響使用者的感受,但是如果 chatbot 太聰明了,讓人覺得被騙了,原來不是真人,這時候反而會產生反效果,所以在 chatbot 進行前,需要適當地揭露,接下來是由專屬的機器人協助處理。

Google Duplex

最近最熱門的影片【超狂影片展示】Google AI 助理新把戲:幫忙打電話到餐廳訂位,等等你確定這不是真人?,展示了 Google Duplex 為基礎的 Assistant 功能,雖然只是代理「客戶」訂位,聲音幾與人類無異,不但能與店員來回對話、協調時間,還能在句子中適切加入「嗯…」、「呃哼」、「哦」等語助詞,成功在對方毫無察覺情況下完成預約。

然而這樣的技術產生了反效果,也導致 Google 必須強調他們將為Duplex加入自我揭露的功能,確保Duplex系統「適當表明身份」,Duplex太逼真嚇壞人,Google將讓AI語音助理表明「我是機器人」,Duplex 的表現嚇壞不少人,也引起批評聲浪。有人認為AI和人類聲音應該要加以區隔,就像刻意在無色無味的瓦斯中加入臭味,合成聲音應該就聽起來是合成的,任何以假亂真的事物將破壞信任,而當沒有信任就會引發失序。

同級生

有足夠年紀的人,應該還記得這個「同級生」這個戀愛遊戲

1992年發行的「同級生」確立了戀愛遊戲的基本形式。在典型的戀愛遊戲裡,玩者操縱一個被女性角色包圍的男性主角,透過交談選單選擇的形式,與女性角色交談,以增加該角色對主角的「好感度」。

當遊戲結束時會根據「好感度」和遊戲內發生的特定「事件」來決定遊戲的結局。這種遊戲通常會有多種結局,而玩家會重複多玩很多次,目的就是想要看到不同的結局。

有發現關鍵字「交談」跟「選單」嗎?戀愛遊戲就像是一個超級龐大的 chatbot,不過遊戲內操作的目標比 chatbot 簡單,就是記錄男主角跟其他每一個女性角色的好感度跟事件。

如果 chatbot 的使用者把 chatbot 當作女性角色在攻略,心理上就會有一玩再玩的衝動,但如果只是把 chatbot 當作是一個存取資訊的方式跟窗口,chatbot 表現出來的講話方式,跟笨拙的選單功能,不夠聰明的話,就會有惹惱使用者的風險。

基本上就是這個事件:誤交損友:微軟的聊天機器人是怎麼被計畫性「攻陷」帶壞的?發生的原因,因為微軟設定,Tay 是一名 19 歲的機器少女,使用者計畫性地不斷地以話術攻陷 Tay,導致一天內這個 chatbot 就下架了。

人工智慧畢竟還是人工

史丹佛大學人工智慧實驗室主任,同時也是Google雲端人工智慧既機器學習首席科學家李飛飛最喜歡的說法是:「機器很快、很精準,也很笨;人類很慢、不精準,但聰慧(Brilliant)。」。換句話說,人工智慧這麼聰明的原因,是因為實作人工智慧的工程師夠聰明,能夠讓這麼笨的機器,進行動態的思考與處理,但仍然是「有範圍」的聰明,因為再怎麼開發,也沒辦法做到什麼都會的人工智慧。

這樣說好像有點過分,畢竟「術業有專攻」,也沒有一個真人能夠樣樣精通。就像是自動駕駛的問題,我們會要求自動駕駛必須要「零」風險,然而自動駕駛如果是代替一個真人開車,真人是不可能完全迴避車禍風險的,但要怎麼要求人工智慧自動駕駛要零風險?從另一個角度想,既然這項新科技,是為了協助人類,避免做出失敗的決策,如果將 AI 視為機器設備的一部分,而不是取代人工,那麼必須要求零風險才能上路,否則只要 AI 出現異常,就算是機器故障,因此不應該貿然上路。

智能音箱是另一個必須要真人互動的 AI 科技,在 Amazon Echo 打開了低價智能音箱的市場後,各家科技大廠就先後推出智能音箱產品,搶進智慧家庭的一環。在 當智能音箱跌破百元,它們看上去更像玩具了 這篇文章中提到,音箱最重要的還是音質,再搭配 AI 晶片,而晶片的優劣決定了音箱到底夠不夠智能。

因為 AI 晶片不夠聰明的關係,聽不懂用戶在說什麼,或誤解用戶的意思,不少智能音箱被用戶怒噴為智障,是目前該領域非常嚴重的一個問題。很明顯的,智能音箱現在還沒有太過於聰明的問題,因為使用者在使用時,明確地知道自己是在跟機器對話。

chatbot 因為要跟其他人類互動,太像真人的科技反而會造成恐慌,而自動駕駛完全是機器跟資料處理的世界,這項科技必須要非常聰明,到完全零風險,才會被市場接受。

該怎麼做

Chatbot 的拐點之年這篇文章中,提供了 Facebook 跟營銷商會面並對 Messenger 做相關表述時,列出了三種 chatbot 類型

  1. 真人:僱傭人類職員回答入站信息
  2. 以樹形檢索為基礎的決策機器人:它們有固定的、自動的對話路徑
  3. 人工智慧機器人:能夠使用自然語言以及計算機技術進行更為廣泛的對話。

而他們建議最成功的 chatbot 應該介於第二種和第三種之間。更實際的做法是,透過一家科技公司的協助,能針對每一個問題,找到25種正常人對一個問題的反應的對話腳本,這樣的交談分析結果,讓 chabot 有了生命,也更能跟客戶進行長時間的交談,當然為了避免 Tay 那樣的問題,chatbot 所能處理的事情有範圍限制,他會嘗試讓客戶的對話回到能夠服務的主題上。

在 IM 中的 Menu,有點像是走回瀏覽器的老路,不過這個方式,確實引導了對話的進程。雖然純文字自然語言的交談互動,讓使用者覺得親切,但更多的狀況,是純文字的打字過程,增加了使用的難度,選單的確讓整個交談的過程變得更順暢,但相對地讓「交談」這個事情變得無趣。

使用菜單結構或傳統的用戶介面結構,幾乎都是認輸的表現,如果走向了某種極端,讓 Chatbot 變成Messenger平台上一個小小的Web視圖,這就很難讓用戶感到愉悅或者吸引用戶來使用了。

References

你做的聊天機器人像個人工智障?六個實戰指南幫你的機器客服找回智商

持續的互動需要持續理解背景信息 EQ 跟 IQ 同樣重要

打造跨平臺企業級Chatbot,Google Bot引擎Dialogflow正式版來了

如何用聊天機器人創造 150 倍的自然觸及|Mr.Reply 教學

必須先設定 訊息腳本,再來則是設定 觸發訊息腳本的規則,最後才是設定 貼文留言自動回覆的條件。

留言回覆的訊息千萬不能只有一種,因為會有非常高的機率被 Facebook 封鎖留言功能,建議至少設定 3 種以上的留言回覆,讓 Mr.Reply 自動任意回覆即可。

2018年7月23日

RTSP/RTP vs RTMP vs WebRTC vs HLS vs SIP/RTP

多媒體資料包含了語音及影像,因為原始資料不同,壓縮格式也有差異,在一個影音資料中,通常包含了這兩種獨立的媒體,如果遇到多國語言的狀況,也可能會看到一個影片中夾帶了多個語音。當多媒體資料放到網路上,為了達到一邊下載,一邊觀看的功能,我們需要 streaming media 的網路協定,幫助我們將影音資料壓縮後,搭配協定傳送到收視端,透過媒體播放器解壓縮,並播放出來。

串流 streaming 的意思就是將影音資料壓縮後,切割成多個區塊的網路資料,分開但連續地發送給客戶端,客戶端不需要一次將整個多媒體資料下載完成後,才能播放影片,而是透過即時下載的分塊資料,先暫存在 buffer 中,只要客戶端有了幾秒鐘的影音 buffer,就可以一邊繼續下載,一邊播放影片。

骨灰級的使用者,可能有聽過 MMS(Microsoft Media Server),或是 RM(Real Media) 這兩種串流多媒體的標準,但現在幾乎都已經消失了,取而代之的是 RTSP, RTMP, WebRTC, HLS 及 SIP 這些協定。

RTSP

RTSP(Real Time Streaming Protocol) 是用來控制遠端多媒體的播放、錄製、暫停的控制協定,有點像是遠端遙控錄放影機的感覺,我們在網路的遠端,發送 RTSP 的控制指令,告訴 Server 我們想要看哪一個影片,找到後,就開始播放影片,也可以暫停。RTSP 協定中,看不到影音資料的內容,因為真正的影音資料是透過另一個協定 RTP(Real-time Transport Protocol) 發送的,在 RTSP 中,Server 會以 SDP(Session Description Protocol) 的形式,將 RTP 影音的資訊,包含 UDP Port,影音的壓縮格式等等資訊,告訴 client 端。

SIP

跟 RTSP 功能比較相近的是 SIP(Session Initiation Protocol),SIP 也是一種多媒體的控制協定,RTSP 像是個網路影音播放器,但 SIP 卻是一種網路電話,SIP 本身也只負責處理通訊的對談建立以及掛斷的處理,真正的影音多媒體資料,也同樣是由 SIP 以 SDP 的方式描述 RTP 影音資料的資訊,透過 RTP 將影音資料傳送到另一端,因為 SIP 是建立雙向對談的協定,因此 RTP 影音會是雙向的串流。

同樣的 RTP 的影像以及語音是分開在不同的 UDP Port,這個 Port 是透過 SDP 在通訊建立時,即時雙向建立起來的。因為電話這種應用必須要一邊講一邊聽,這是最常見的一種串流媒體的應用。

SIP 跟 RTSP 的基本差異在於,SIP 是雙向影音,而 RTSP 是單向點播,雖然說是這樣,但 RTSP 還是可以做到雙向視訊通話,不過那已經不是該協定的應用本意。目前 RTSP 最常見的應用場景是網路攝影機。

RTMP

RTMP(Real Time Messaging Protocol) 是由 adobe flash player 引領的串流媒體協定,因為 flash player 在過去的網頁曾經霸佔了很長一段黃金時代,只要裝了 flash player,不僅可以看串流影片,還可以存取電腦的麥克風及 webcam,還能玩一堆網頁遊戲。

不過 flash player 的時代已經走入歷史,但在裡面應用的 RTMP 串流協定還持續存活著,原因在於網路直播,因為 RTMP 有低延遲的特性,適合用在網路影音直播中,目前的原生 APP 網路影音直播都是採用 RMTP 協定,除非要轉到網頁瀏覽器播放,才會轉換到 HTML5 或是 HLS 的協定。

RTMP 跟 RTSP 與 SIP 不同的地方在於,RTMP 將媒體控制指令跟多媒體資料放在同一個協定中,標準是使用 TCP Port 1935,而不像是 RTP 一樣是使用 UDP,每一種媒體資料使用一個 UDP Port。

而且 RTMP 裡面還有著網路頻寬偵測的能力,因應著不同的網路速度,可以動態調整影音資料內容的解析度,以低解析度的影片應付低頻寬的網路環境。

WebRTC

WebRTC(Web Real-Time Communication) 是 google 開放的標準,目前已經提交成為 W3C 標準,專門支援在網頁瀏覽器中進行影音對談的 API。

不同於 Flash Player 的 plugin 機制,WebRTC 需要瀏覽器原生的支援,也就是內建於瀏覽器的影音對談 API,雖然 WebRTC 已經解除了 flash player 的窘境,不需要安裝 plugin,但這個協定/API 本身的能力跟等級,跟 RTMP 還是有落差。

如果要做少量的視訊會議對談,用 WebRTC 是可以做到的,但如果要做大量的 APP 影音直播,瞬間有上千或上萬個人在觀看的直播影音,目前還是要走 RTMP 搭配 HLS 的解決方案。

HLS

HLS (HTTP Live Streaming) 是 Apple 提出的基於 HTTP 延伸的串流媒體傳輸協定,在媒體播放過程中,允許客戶端因應網路速度動態調整不同解析度的媒體資料,在串流媒體一開始,客戶端會下載 m3u8 playlist,用來取得可以使用的媒體資料流。

HLS 跟 RTP 的差異在於 HLS 可穿過所有允許 HTTP 通過的 firewall 或 proxy,非常適合搭配 CDN 發布媒體串流。因此網路 APP 影音直播,才會以 RTMP 發布,在雲端轉換為 HLS,再提供給大量客戶端觀看這樣的機制。

ONVIF

ONVIF (Open Network Video Interface Forum) 是 Axis、Bosch Security System 及 Sony 在 2008 成立的標準論壇,目的在於讓不同品牌的網路影音設備能夠有共通的標準,能夠互通,幫助硬體生產及網路開發商能夠透過標準整合出各種不同的網路影音監視系統。

ONVIF 有五種 profile

  • Profile S for encompasses video streaming 網路監視系統
  • Profile G for video storage 視訊儲存及重播
  • Profile C for access control 門禁控制
  • Profile Q for Out-of-the-box interoperability 開箱即用,更簡便的操作介面
  • Profile A for Physical Access Control System (PACS) 跟 Profile C 很類似,但 Profile C 用於基本的門禁控制,Profile A 則是擴充,有比較複雜的控制邏輯

以往在網路攝影機的市場,大多是以 RTSP/RTP 的方式提供影音,但現在已經有支援 RTMP 的網路攝影機出現了。

References

即時串流通信協定 RTSP

直播終端技術比較:Native vs H5 vs WebRTC vs 小程序

網絡視頻監控:ONVIF標準協議6個常見問題

什麼是Onvif協議,誰開啟了Onvif時代?

ONVIF -- Profiles (S,G,C,Q,A)

Which protocol is best for a video live streaming from a server to an Android: RTSP, RTMP, HTTP or something else?

Streaming 通訊協定 RTP RTCP RTSP RTMP HLS 介紹

Streaming 通訊協定 RTP RTCP RTSP RTMP HLS 介紹

RTMP vs RTSP/RTP: Which to choose for an interactive livestream?

RTSP協議詳解

[RTSP]rtsp和sip的區別和聯繫

SDP (Session Description Protocol) 閱讀心得

可以用WebRTC來做視頻直播嗎?

2018年7月2日

Green Process, Green thread, Native Process

在某個作業系統中,要達到 multitasking 的能力,必須要由 OS 提供建立 thread 或是 process 分別處理不同工作的 library,thread 跟 process 的最大差異是,thread 是在一個 process 裡面運作,多個 thread 之間可以共享資料。

而 Green Thread 是 Java VM 裡面的特殊用語,當時 Java VM 是利用 libthread.so 發展支援多工的專案,該專案名稱為 "Green",所以就稱為 Green Thread。

至於有人寫說 Erlang VM 的多工是採用了 Green Process,這就像是借用了 Green Thread 跟 Native Process 的概念,因為 Erlang 的 Process 之間不能共享資料,而又跟 Java 一樣本身是 VM,所以就借用了 "Green",稱為 Green Process。

有個比 Green Process 更一般化的名詞為 Light-weight process,他會以單一個 kernel thread 實作,運作在 user space,且會共享 memory address space。

Green threads wiki 中明確的下了定義,只要是取代 OS 原生的機制,透過 runtime library 或是 VM,進行 thread 的 scheduling 處理,這種 thread 就稱為 Green Thread。Green Thread 會在 user space 而不是 kernel space 運作,讓多工服務不需要 native thread 的支援。

但現今的 Java VM 其實也已經放棄了 Green Thread,而改回使用 native thread。原因在於運作的速度,在抽象化 thread 多工模型時,耗費了太多精力,再加上新的多核心 CPU,如果要完全運用所有CPU核心的運算能力,也就是 SMP,透過 OS 的 thread 機制會是最簡單且快速的,因此現在的 VM 都是直接映射到 OS 的 thread,再搭配 Thread Pool 的做法,可減少 Thread 建立跟銷毀所消耗的資源。


References

What other systems beside Erlang are based on “Green Processes”?

What's the difference between “green threads” and Erlang's processes?

What is the difference between multicore programming in Erlang and other language?

Erlang調度器的一些細節以及它重要的原因(譯文)

Why not Green Threads?

Java的Green threads是Coroutine嗎?

2018年6月25日

清理 mac 的儲存空間

一直是習慣用 df 指令查看 disk 使用量,最近發現 disk 可用空間慢慢地不足,就開始清理一些不常用的文件跟軟體。

首先可在左上角的蘋果圖示中,點擊 "關於這台 Mac",然後可以看 "儲存空間" 這個項目,但是卻發現這裡的可用空間跟 df 查詢到的結果差異很大。

接著點擊右邊的 "管理",可看到 "文件"、"GarageBand" 等等項目的資訊,"GarageBand" 可清除 2G 的樂器音檔,文件區可查看佔用空間比較大的檔案資料,自己可以選擇要清除哪些檔案。

但做了這些動作後,df 的資訊還是沒有什麼改變。原因是在 Time Machine 的 Local Snapshots。

在 command line 用 sudo tmutil listlocalsnapshots / 指令,可查看目前 local 硬碟中,包含的 localsnapshot,因為最近頻繁地刪除檔案,導致 localsnapshot 產生了很多快照。

$ sudo tmutil listlocalsnapshots /
com.apple.TimeMachine.2018-06-19-105146
com.apple.TimeMachine.2018-06-19-114843
com.apple.TimeMachine.2018-06-19-125049
com.apple.TimeMachine.2018-06-19-154914
com.apple.TimeMachine.2018-06-19-164914
com.apple.TimeMachine.2018-06-19-175221
com.apple.TimeMachine.2018-06-19-230612
com.apple.TimeMachine.2018-06-20-000523
com.apple.TimeMachine.2018-06-20-093756
com.apple.TimeMachine.2018-06-20-104818

確認這些資料快照沒有回復的需求後,可以下指令逐個刪除 localsnapshot。

sudo tmutil deletelocalsnapshots 2018-06-20-000523

最後比較一下原本的 df 資訊,跟刪除所有 localsnapshots 以後的資訊差異。

$ df -H
Filesystem      Size   Used  Avail Capacity  iused               ifree %iused  Mounted on
/dev/disk1s1    480G   410G    68G    86%  2387622 9223372036852388185    0%   /
devfs           343k   343k     0B   100%     1160                   0  100%   /dev
/dev/disk1s4    480G   1.1G    68G     2%        1 9223372036854775806    0%   /private/var/vm
map -hosts        0B     0B     0B   100%        0                   0  100%   /net
map auto_home     0B     0B     0B   100%        0                   0  100%   /home
$ df -H
Filesystem      Size   Used  Avail Capacity  iused               ifree %iused  Mounted on
/dev/disk1s1    480G   286G   192G    60%  2387620 9223372036852388187    0%   /
devfs           343k   343k     0B   100%     1160                   0  100%   /dev
/dev/disk1s4    480G   1.1G   192G     1%        1 9223372036854775806    0%   /private/var/vm
map -hosts        0B     0B     0B   100%        0                   0  100%   /net
map auto_home     0B     0B     0B   100%        0                   0  100%   /home

References

Mac顯示「系統」佔用太多硬碟空間?試試用這些方法把它清乾淨

【macOS 技巧】如何從 macOS Sierra 內置清理工具掀出塞爆空間的兇手!?

2018年6月11日

tmux

習慣使用 linux terminal 遠端處理 server 工作的人,有時會遇到一個問題,就是在遠端 terminal 處理過程中,有時會遇到一些程式處理很久,或是需要同時查看 log,系統 loading 的狀況,這時,就需要再對同一台機器打開另一個 terminal,導致 terminal 的頁籤越來越多。更麻煩的是有時候遇到網路異常斷線,所有 terminal 的連線中斷了,就必須要重新一個一個再連接 server。

tmux 是一個 terminal multiplexer,可讓使用者以單一terminal,連接多個 terminal sessions或是windows。換句話說,就不需要再連接多個 terminal tab。另外,更重要的功能是,tmux 內建了一個 terminal server,即使 terminal 斷線,只要 tmux server 還存活,任何時候再重連,都可以取回剛剛工作中的 terminal sessions/windows,繼續工作。

安裝

在 centos 安裝 tmux

yum install tmux

在 macos 安裝 tmux

sudo port install tmux

tmux 指令

只要在 terminal 執行 tmux,就會啟動 tmux server,另外還有一些常用的指令

# 啟動新的 terminal session
tmux new -s sessionanme
tmux new -s sessionanme -n windowname

# 列出所有 tmux sessions
tmux ls

# a/at/attach session
tmux at -t sessionname
tmux a #

# kill session
tmux kill-session -t sessioname

# 當 session 內所有shell都結束,該 session 就會中止
exit

進入 tmux 後,terminal 下方就會出現一條綠色的 status bar,很明確的顯示目前正在 tmux 工作 session 中。

tmux 專有名詞的概念

  • tmux server

    啟動 tmux 會產生一個 server,負責管理所有 sessions

  • session

    一個 terminal 可以有多個 sessions,通常一個 project 會使用一個 session。

  • window

    一個 session 可以有多個 window,每一個 window 會佔滿整個 terminal 畫面,可以開多個 window,讓某些 window 在背景運作。

  • pane

    每一個 windows 可切割多個區塊,每一個區塊就是一個 pane。通常會將 window 水平或垂直切割,增加多個 pane。

控制指令

進入 tmux 後,跟平常一樣,會在多個 shell 中切換執行工作,如果需要對 tmux 下指令,要用 Ctrl-b 功能鍵啟動。

因為 Ctrl-b 會有點難按,大部分都會改成其他的 function key,在 ~/.tmux.conf 設定檔中,可增加這些設定,將 Ctrl-b 改為 Ctrl-a

set -g prefix C-a
unbind C-b
bind C-a send-prefix

另外可在設定檔中加上快速鍵,以下設定,可以在 Ctrl-a 後,直接按 | ,就會水平方向增加一個 shell pane。

unbind %
bind | split-window -h
bind - split-window -v

啟用滑鼠,可用滑鼠修改 pane 的大小,捲動視窗

set -g mouse on

  • session 處理的指令 大部分都是在原本的 terminal 中,不是 tmux 的 function
tmux ls
tmux attach -t 0
tmux kill-session -t 0

Ctrl-a (由 Ctrl-b 改為 Ctrl-a) 後的 fuction

Ctrl-a 後的 function 功能
d detach session
s list session
:new new session
$ 為 session 命名
  • window 指令
Ctrl-a 後的 function 功能
c 產生新 window
& 關閉目前的 window
p 切換到上一個 window
n 切換到下一個 window
w list windows
f find window
  • pane 指令
Ctrl-a 後的 function 功能
% (改為 |) 水平分割新的 pane
" (改為 -) 垂直分割新的 pane
方向鍵 切換到其他 panes
x 關閉目前的 pane
o 交換 pane
空白鍵 切換 layout
q 顯示每個 pane 的編號,再按編號,可切換到該 pane
{ 跟上一個 pane 交換位置
} 跟下一個 pane 交換位置
z 切換 pane 最大/最小化

~/.tmux.conf 設定的內容

set -g default-terminal "screen-256color"
set -g display-time 3000
set -g escape-time 0
set -g history-limit 65535
set -g base-index 1
set -g pane-base-index 1

# Ctrl-b -> Ctrl-a
set -g prefix C-a
unbind C-b
bind C-a send-prefix

# enable mouse
set -g mouse on

# split window
unbind %
bind | split-window -h
bind - split-window -v

# <prefix> or Ctrl- or <prefix> Ctrl-
#   k --- previous pane
# h   l --- previous window / next window
#   j --- next pane

# navigate windows
unbind-key l
bind-key h select-window -t :-
bind-key C-h select-window -t :-
bind-key -n C-h select-window -t :-
bind-key l select-window -t :+
bind-key C-l select-window -t :+
bind-key -n C-l select-window -t :+

# navigate panes
bind-key j select-pane -t :.-
bind-key C-j select-pane -t :.-
bind-key -n C-j select-pane -t :.-
bind-key k select-pane -t :.+
bind-key C-k select-pane -t :.+
bind-key -n C-k select-pane -t :.+

References

終端機 session 管理神器 — tmux

終端機必備的多工良伴:tmux

tmux ,不只是 terminal multiplexer

Tutorial — tmux Basics

tmux cheatsheet

Tmux 快捷鍵 & 速查表

使用 tmux 與 tmuxifier 打造 Console 開發環境(比 screen 更棒)

2018年6月4日

資料科學可以回答的問題

資料科學會使用 Machine Learning 的演算法,這些演算法的使用方式,都是這三個步驟:讀取資料,轉譯,提供答案,但在選擇演算法之前,要先知道這些演算法能回答什麼問題,要問對問題,才能找到答案。

以下這些是可以回答的問題

  1. 這是A,還是B? Is this A or B? (two-class classification)
  2. 這是A、B、C 還是 D? Is this A or B or C or D? (multi-class classification)
  3. 有沒有奇怪的地方? Is this Weird? (anomaly detection)
  4. 這有多少/有幾個? How Much/How Many? (regression)
  5. 用迴歸演算法解決多元分類問題 Multi-Class Classification as Regression
  6. 用迴歸演算法解決二元分類問題 Two-Class Classification as Regression
  7. 資料是由什麼組成的?怎麼分類? How is this Data Organized? (unsupervised learning, clustering)
  8. 接下來該怎麼做? What Should I Do Now? (reinforcement learning)

這是A,還是B? Is this A or B? (two-class classification)

二元分類 two-class classification,用來解決只有兩種答案的問題,例如:

  • 這位客戶會不會續約?
  • 這張照片是貓還是狗?
  • 這位顧客會不會點最上面的連結?
  • 如果再開一千英里,這個輪胎會不會爆胎?
  • 五元折價券或是打七五折,哪一個促銷手段能吸引更多顧客?

這是A、B、C 還是 D? Is this A or B or C or D? (multi-class classification)

多元分類 multi-class classification,用來解決有多種答案的問題,例如:

  • 這是哪種動物的圖片?
  • 這是哪種飛機的雷達訊號?
  • 這篇新聞屬於哪一個主題?
  • 這則 twitter 隱含了哪一種情緒?
  • 這則錄音裡的講者是誰?

有沒有奇怪的地方? Is this Weird? (anomaly detection)

異常偵測 anomaly detection,用來辨別不正常的資料,當分析的情況發生率很低,導致樣本數也很少的時候,異常偵測就顯得特別有用。感覺上跟 二元分類 two-class classification 很像,差別在於二元分類的原始資料中,就包含了兩種答案,但是異常偵測則不一定。

例如:

  • 是不是信用卡盜刷
  • 壓力大小有任何異狀嗎?
  • 這則網路訊息正常嗎?
  • 這些消費記錄跟這位使用者過去的行為落差很大嗎?
  • 這些用電量在這個季節和時間算是正常的嗎?

這有多少/有幾個? How Much/How Many? (regression)

當解決的問題涉及數字而非分類時,這一類的演算法就稱為迴歸(regression),例如:

  • 下週二的氣溫為何?
  • 第四季在葡萄牙的銷售量會有多少?
  • 三十分鐘後,我的風力發電廠會有多少千瓦(kW)的需求?
  • 我下週會獲得多少新追蹤者?
  • 每一千個使用這種軸承的產品裡,有多少個能被使用超過一萬小時?

用迴歸演算法解決多元分類問題 Multi-Class Classification as Regression

有些看起來很像多元分類的問題,但更適合用迴歸解決。例如

  • 讀者對哪則新聞最感興趣

    乍看之下是個分類問題,但如果將問題換成「對讀者來說,每則新聞的有趣程度為何」並為每則新聞評分,接下來就只需要選出最高分的新聞。這類問題通常和排名或對比有關。

  • 我的車隊中,哪台廂型車最需要保養

    可以換成「我的車隊裡,每台廂型車需要保養的程度為何」

  • 哪 5% 的顧客隔年會跳槽到對手公司

    可以換成「每名顧客明年跳槽到對手公司的機率為何」。

用迴歸演算法解決二元分類問題 Two-Class Classification as Regression

二元分類問題也可以換成迴歸問題,這類問題也通常以「有多少可能性」、「有多少比例」開頭。例如:

  • 這位使用者有多大機率會點我的廣告?
  • 這台拉霸機有多少比例的回合會給獎金?
  • 這名員工有多大機率會造成內部安全的威脅?
  • 今天有多少比例的航班會準時抵達?

二元分類、多元分類、異狀偵測和迴歸等四種演算法之間都很相近,它們都是監督式學習(supervised learning)下的演算法。共通之處,在於建模時都用了一組包含回答的資料(這個過程稱作訓練,training),並被用來分類或預測一組不包含回答的資料(這個過程稱作評分,scoring)。

資料是由什麼組成的?怎麼分類? How is this Data Organized? (unsupervised learning, clustering)

這是非監督和強化式學習(unsupervised and reinforcement learning)的演算法。

判斷資料分類的方法有很多,其中一類是聚類法(clustering),包括資料群集(chunking)、分組(grouping)、聚束(bunching)、分段(segmentation)等等。聚類法所分析的資料不包含任何用來引導分群、說明分群意義和數量的數字或名字。聚類法的基礎是衡量資料之間的距離或相似度,也就是距離度量(distance metric)。距離度量可以是任何可測量的數據。

  • 哪些消費者對農產品有相似的品味?
  • 哪些觀眾喜歡同一類電影?
  • 哪些型號的印表機有類似的故障問題?
  • 這間變電所在每週的哪幾天有類似的電力需求?
  • 用什麼方法可以自然地將這些文件分成五類?

另一類演算法稱作降維法(dimensionality reduction)。降維是另一種簡化資料的方法,它可以讓資料的溝通變得更容易、處理變得更快、而且存取變得更簡單。降維的運作原理是創造出一套簡化資料的方法。等第積分平均(GPA)是一個很簡單的例子。

  • 哪幾組飛機引擎偵測器的數據呈同向(和反向)變化?
  • 成功的 CEO 有哪些共通的領導力特質?
  • 全美的油價起伏有哪些相似的特徵?
  • 這些文件裡有哪幾組詞彙常常同時出現?(它們和哪些主題有關?)

接下來該怎麼做? What Should I Do Now? (reinforcement learning)

第三類演算法和行動有關,即強化學習(reinforcement learning)演算法。這些演算法和監督式和非監督式都不太一樣。

比方說,迴歸演算法雖然可以用來預測明天的最高溫為華氏 98 度,但它不能用來判斷該做什麼;另一方面,強化學習演算法就可以用來判斷該採取的行動,例如趁天氣還沒變熱的時候,先開辦公大樓內上半層的冷氣。

強化學習演算法很適合用於需要在無人監督情況下、完成許多簡單決策自動化系統,例如電梯、電熱器、冷氣和照明系統。由於強化學習最初被開發的目的是用來操縱機器人,任何自動物件也能使用這套演算法,像是偵查用無人機或掃地機器人。強化學習的問題總是和該採取什麼行動有關,雖然最後往往還是機器在處理這些問題。

  • 我該把廣告放在網頁何處,才能讓讀者最有機會點到它?
  • 我該把溫度調高或調低一點,還是維持現狀?
  • 我該再掃一次客廳還是繼續充電?
  • 我現在該買入多少股?
  • 看到黃燈時,我該保持當前速度、煞車還是加速?

References

What Types of Questions Can Data Science Answer?

五種可以用機器學習回答的問題

Which Algorithm Family Can Answer My Question?

2018年5月28日

CQRS: Command Query Responsibility Separation

CQS (Command Query Separation) 是由 Bertrand Meyer (Eiffel 語言的爸爸) 在 1988 於 "Object Oriented Software Construction" 這本書中提出的軟體架構概念,所有的 computing method 只會分成兩類,一種是執行某個 action 的 command,另一種是呼叫查詢並取得回傳資料,但不應該同時做兩種工作。換句話說,發問時,不能在該 method 裡面修改答案。

CQRS (Command Query Responsibility Separation) 應用 CQS 的概念,進一步將 Query 及 Command 物件分離,分別處理取得資料及修改資料的工作。

CQS 遇到的問題,會是 re-entrant 及 multi-thread,就是當一件工作處理到一半,被新的工作中斷這樣的問題,也就是 thread-safe 的問題,也就造成實作複雜度的問題。

然而這樣的問題,搭配著 Event Sourcing 的方法,將某個 APP state 的變更,收集成一個 sequence of events,以這種方式處理 command action,就能解決 thread-safe 的問題。

不過我們還是會遇到,command action 執行的速度跟 query 的時機點的問題,如果修改資料的動作在 query 以前還沒有完成,那麼前端就會查詢到舊狀態的資料,但資料還是會達到最終一致性,而不會有強一致性。

CQRS 的優點:

  1. Command 及 Query 分工明確,可分別進行效能調整及最佳化
  2. 將業務上的命令和查詢的職責分離能夠提高系統的性能、可擴展性和安全性
  3. 企業邏輯簡單清楚,能夠從事件歷程看到系統中的那些行為或者操作導致了系統的狀態變化。
  4. 將開發的邏輯概念,從數據驅動 (Data-Driven) 轉到任務驅動 (Task-Driven) 以及事件驅動 (Event-Driven)

在以下狀況,可以考慮使用CQRS模式:

  1. 當在業務邏輯層有很多操作需要相同的實體或者對象進行操作的時候。CQRS使得我們可以對讀和寫定義不同的實體和方法,從而可以減少或者避免對某一方面的更改造成衝突
  2. 用在特定任務的用戶互動系統,通常系統會引導用戶通過一系列複雜的步驟和操作,通常會需要一些複雜的領域模型。寫入資料的部分有很多和業務邏輯相關的命令操作,輸入驗證,業務邏輯驗證來保證數據的一致性。讀取資料沒有業務邏輯,僅僅是回傳 DTO。讀與寫的資料只需要達到最終一致性。
  3. 適用於一些需要對查詢性能和寫入性能分開進行優化的系統,尤其是讀/寫比非常高的系統。例如在很多系統中讀取資料的使用次數遠大於寫入資料。
  4. 對於系統在將來會隨著時間不斷演化,有可能會包含不同版本的模型,或者業務規則經常變化的系統
  5. 需要和其他系統整合,特別是需要和事件歷程 Event Sourcing 進行整合的系統,這樣子系統的臨時異常不會影響整個系統的其他部分。

在以下狀況,不適合使用CQRS:

  1. 領域模型或者業務邏輯比較簡單,這種情況下使用CQRS會把系統弄得太複雜
  2. 對於簡單的,CRUD模式的用戶介面以及與之相關的數據訪問操作已經足夠的話,都只是一個簡單的對數據進行增刪改查,沒有必要使用CQRS
  3. 不適合在整個系統中全部都使用 CQRS,在特定模組中CQRS可能比較有用

以下是一些查詢到的 CQRS 架構圖,從圖片可以看到跟傳統的 CRUD Data-Driven 架構的差異。

這種架構區分了 Business 及 Query model 的 DataBase,也可以想成將資料寫入了 Business Database,而前端使用者不會觸及該資料庫,是比較強調資料安全性的方式,但不能保證 Query Model 的 DB 裡面的資料一定會跟 Business Model DB 的資料一樣。

這種架構比較接近一個一般性的系統,資料庫是單一的,且可以在 Event Handler 中確認並檢查 Database 及 Analysis Database 的資料一致性。

這種架構圖比較強調資料流的過程,但基本上架構跟上面那個很接近,不過在 Comamnd 的部分,可注意到 Command 沒有 Reply DTO,也可以說,只要 Comamnd 有送進 Command Handler,就視為一個成功執行的 Command。

這裡強調 Command Bus 是以非同步的方式,送進 Command Handler,非同步可強化系統效能的表現,但如果要用同步的方式也可以,要等到 Command Event 送進 Event Database 後,才回應給前端確認該命令已經完成。而 Query 是用同步的方式,發送 Query 並等待要回傳的 ViewModel。

References

CQRS - Martin Fowler

CQRS 介紹

CQRS 命令查詢職責分離模式介紹

從CQS到CQRS

From CQS to CQRS

深度長文:我對CQRS/EventSourcing架構的思考

DDD CQRS架構和傳統架構的優缺點比較

Introduction to Domain Driven Design, CQRS and Event Sourcing