2019年8月19日 星期一

用 Flutter 幾行程式搞定一個 app

如果熟悉 Javascript 與 HTML 語法, 透過 Flutter 平台, 用內建的 flutter_webview_plugin 幾行程式就能完成一個 android app, 甚至 iOS app 也應該很容易, Flutter 是用 dart 語言所開發的跨平台程式架構, 而 dart 語法跟 Javascript 長的很像, 輕鬆就能上手:
1. 使用 vscode 開啟 Flutter 新專案取名為 webview
2. 在目錄結構新增一個新目錄(document root)例如 www, 把 *.html, *.js, *.css ...等等所要用到的檔案複製到該目錄, 假設 html 主檔案是 index.html
3. 修改 pubspec.yaml 檔案, 加入 flutter_webview_plugin 並啟用 flutter_assets, 把它取名為 www/
4. 修改主程式 main.dart, 不到 10 行程式碼, 讓 WebviewScaffold 去載入 html 主檔案 index.html, 一個 app 就啟動了.
5. 就這樣, 連線到手機開啟開發者模式, 允許 debug 上傳 app, 把 app 上載到手機上執行.
p.s.
1. 修改的 pubspec.yaml 內容:
   name: webview
   description: A new Flutter project.
   version: 1.0.0+1
   environment:
        sdk: "> =2.1.0 <3.0.0"

   dependencies:
        flutter:
            sdk: flutter
        cupertino_icons: ^0.1.2
        flutter_webview_plugin: ^0.3.0+2

   dev_dependencies:
        flutter_test:
             sdk: flutter

   flutter:
       uses-material-design: true
       assets:
             -  www/


2. 修改 main.dart 的內容, 開啟 lib/main.dart 主程式, 以下內容全數複製貼過去:
import 'package:flutter/material.dart';
import 'package:flutter_webview_plugin/flutter_webview_plugin.dart';
void main() {  runApp(MyApp()); }
class MyApp  extends StatelessWidget {
  static String assets = "file:///android_asset/flutter_assets/www";
  @override build(BuildContext context) => MaterialApp( theme:ThemeData.dark(),
    routes: {
       '/': (_) => WebviewScaffold(url:"$
assets/index.html", withZoom: true, withLocalStorage: true, withJavascript: true, allowFileURLs: true)
    }
  );

}

3. 修改 launch.json 內容, 在 vscode 拉下 Debug-> Start Debugging 或是用快速鍵 Ctrl-F5 上傳到手機上, 如果想將 app 改成 release 版, 順便減肥, 要拉下 Debug-> Open Configurations 修改 launch.json 檔案, 添加 flutterMode:
{
    "configurations": [
        {
            "name": "Flutter",
            "request": "launch",
            "type": "dart",
            "flutterMode": "release"
        }
    ]
}
4. 如果發生 WeView 找不到檔案 index.html, 可能是 Fullter 將 assets 包成 app 後所在目錄位置出錯, 可以用解壓縮程式 zip, 打開 webview/build/app/outputs/apk/release/app-release.apk, 看看到底是把 assets 目錄放在那個目錄底下, 藉此去修正程式類型 MyApp 中所定義的靜態字串 static String assets = "pathTo/www" 就能解決.

2019年8月18日 星期日

Dart 數列及資料流產生器, async/await/Future

參考 https://www.youtube.com/watch?v=TF-TBsgIErY
在 dart 世界裡, 透過 sync* 關鍵字標註一個函式讓它可以用 yield 暫停後回傳一筆資料(實際上是包裝成一個 iterable 物件直接回傳,因此不會 block 程式),讓後續用 forEach 依序讀取, 若將 yield 放進一個 for 迴圈, 就能產生一個數列:
        IntRange(int start, int end) sync* {for(int i=start; i<=end; i++)  yield i;}//一個整數數列
        void main() => IntRange(1,10).forEach(print); // 實際上是一個  iterable物件
或是搭配 yield * 直接呼叫遞迴函式(recursive funtion), 讓它看似沒有 loop 的序列產生器:
     IntRange(int start, int end) sync* { // 打包成 Iterable 類型的物件
         if (start <= end) { // 結束遞迴的條件
              yield start;      // 產生初始值
              yield * IntRange(start+1, end); // 事件(event loop)註冊, 下次繼續用遞迴函式產生
         }
     }
     void main() => IntRange(1,10).forEach(print); // 實際上是一個  iterable物件
當來到非同步世界後, 同樣可以用 yield 產生序列, 但要改用 async* (非同步產生器)標註函式, async * 所產生的是序列會包裝成資料流 stream, 而非 iterable:
     IntStrean(int start, int end) async* {for(int i=start; i<=end; i++)  yield i;}//一串整數流
     void main() => IntStrean(1,10).forEach(print);//實際上是一個 Stream<int> 物件
若是 yieild Future 物件的話, 也就是非同步串, 實際上 Future 是一個抽象類型(abstract class), 它並非當下產生資料, 而是當完成後 then( )或是等待他完成 await 才現出物件的原型:
     futureStream(int start, int end)  async * {  for(int i=start; i <=end; i++) yield     Future.value(i); } // 實際是回傳 Stream<Future>
     void main()  =>  futureStream(1,10).forEach( (f) => f.then(print));// f 實際上是 Future 物件, 用 then( ) 現出物件原型

用 yield await 看似等待完成後才回傳, 實際只是註冊到 event loop , 順便將物件打包成相對應之類型,他並不會 block 程式的運行:
     intStream(int start, int end)  async* { // 打包成 Stream 類型的物件
         for(int i=start; i <=end; i++) yield await Future.value(i); // 註冊至 event loop, 用 await 現出原型
     }
     void main() => intStream(1,10).forEach(print);//實際上是一個 Stream<int>物件
當然也能搭配 yield * 呼叫遞迴函式(recursive funtion)形成一個看似不用 loop 的資料流產生器,實際上也是註冊至 event loop, 待後續再執行:
     intStream(int start, int end) async* {
         if (start <= end) { // 結束遞迴的條件
              yield await Future.value(start);  // 註冊事件, 用 await 等待物件現出原型
              yield * intStream(start+1, end); // 只註冊至 event loop 續用遞迴函式, 無需等待
         }
     }
     void main() => intStream(1,10).forEach(print);//實際上是一個 Stream<int>物件
dart 程式庫的 Future 物件可類比成 Javascript 的 promise 物件, 而將 Future.value(值)類比成 promise 的 resolv(非同步傳回值), 他們運作邏輯是相同的, 同樣的,我們也可以用簡單邏輯來理解 async/await 的運作方式,  async 其實只是把將來要回傳(return)的值一起打包成 Future/promise 物件, 而 await 負責註冊事件並把後續程式碼(位在相應的 async 函式碼)打包放入 then( ) 的 callback 函式內,後續當物件現出原形時, 呼叫這個 callback  方法完成後續動作, then 是 Future/promise 裏面一個註冊過的從屬函式(簡稱方法), 一旦條件滿足(物件現出原形)才會執行. 對於 sync * 產生器而言, 打包的是 iterable 物件, iterable 物件是一個在迴圈中可以一個一個取出使用的一串數列, 而 async * 所打包的是 Stream 類型的物件, Stream 則是一個可以一個一個取出來的非同步事件(a sequence of events), forEach 只是其中一個取出物件的方法, 透過註冊事件與物件方法間接或直接讀取物件的原型, 而不會 block 程式的運行,如此而已. Dart 的 async/await 語法跟 Javascript 語法長的很像:
     asyncFunction( ) async =>  3; // async 標註會將函式傳回值打包成 Future 類型的物件
     void main( ) async =>  print(await asyncFunction( )); // await 只能在 async 用, 他讓物件現出原型
或是同步程式的寫法,用 Future 的 then( ) 方法現出物件原型後再處理:
     void main( )          =>  asyncFunction( ).then(print);
同步程式也可以打包成 Future 物件, 後續沿用 then 方法取得物件原型:
     syncFunction( )   => Future.value(3);  // Future.value 是立刻賦值, promise resolve
     void main( )         => syncFunction( ).then(print); // then 其實是一個 Future 物件裏面一個方法

2019年7月28日 星期日

理解 Kotlin 的 companion object (變身) vs instance of class (化身) 之不同

在 Kotlin 中可以將類型與類型建構式分別指定給變數, 到底有何不同, 看以下例子:
class AAA { // 類型 AAA,  預設的建構式就是 AAA( )
    var b = 3 // 這是待化身的變數宣告, 透過建構式 AAA( ) 化身成物件成員(instance member)
    companion object {// 這是協同物件, 也就是 AAA 的變身, 唯一實體共享物件(static object)
        var a  = 5  // 需透過 AAA 存取成員
        var b = 10 // 名字同化身的成員, 直接變身成物件成員, 必需透過 AAA 存取成員
    }
}
fun main(args: Array < String > ) {
    val c = AAA    // c 是 AAA 的變身(companion object), 但不是化身(instance of class)
    val i  = AAA( ) // i 才是類型的化身, 透過類型建構式 ( ) 才能化身成物件(instance of class)
    println(c.a)    // a 是 AAA 共享成員
    println(c.b)    // b 也是 AAA 成員之一
    println(i@AAA.a) // i 要透過變身標籤 @AAA 才能存取共享成員 a
    println(i@AAA.b) // i 要透過變身標籤 @AAA 才能存取共享成員 b
    // println(i.a)  //  錯誤的用法: a 實際並非 i 的成員
    // println(i.b)  //  錯誤的用法: b 實際並非 i 的成員
    println(i.b)     //  這裡的 b 是透過建構式生成的, 並非變身(companion object)裡面的 b
執行結果:
5
10
5
10
3
變身與化身中文意義是相同的, 如果把英文的 companion object(協同物件)翻譯成"變身", 而將 instance of class (類型物件)翻譯成"化身"時, 兩者本質上都是物件, 就要區分它們的不同, 也許可以這樣理解: 一旦宣告了協同物件(簡稱變身), 類型名稱可以變身(ailias of companion object)成物件去存取共享成員, 而類型物件(instance of class)必需透過建構式去化身, 兩者物件的生成方式不同在於: 變身在是在編譯階段(compile phase)生成的靜態物件(static object),從程式開始執行到結束生命都一直維係著不會消失, 但化身是在執行階段(runtime)才生成的區域物件(scope object), 生命周期受到控制, 一旦離開所在的生命區域(scope)便消失殆盡. 變身主要目的是維繫共享資源, 實際上只有一份變身物件存在類型內,  所有類型的化身並沒有變身這個物件!如果真要在化身物件方便取用,除了用 @標籤外, 也可以用 set() 及 get() 刻意連結起來,只不過要注意的是, 因為是共享的(static object),若在多工環境下也許要用 semaphore 來加以鎖定.
class AAA { // 類型 AAA,  預設化身的建構式就是 AAA( )
    companion object {// 這是協同物件, 也就是 AAA 的變身, 唯一實體共享物件(static object)
          var a  = 5  //  AAA 變身的成員
          var b = 10 // 若名稱與化身的成員相同時, 必需透過 AAA 存取成員
    }
    var b: Int  get( )         {  return AAA.b } // 符號讀取連結, 讓化身間接讀取變身成員 b
                     set(v: Int) {  AAA.b = v      } // 符號寫入連結, 讓化身間接寫入變身成員 b
    fun a( ) { // 當化身成員名稱與變身的成員不同時, 化身可以取用到變身的成員只是因為內定(default)符號聯結的關係, 並不表示他就是化身的成員
         return a  // 等同取用 AAA.a , 只是因為 kotlin 內定會將符號 a 聯結到變身成員 AAA.a
    }
}
fun main(args: Array < String > ) {
    val c = AAA    // c 是 AAA 的變身(companion object), 但不是化身(instance of class)
    val i  = AAA( ) // i 是類型的化身, 透過類型建構式 ( ) 化身成物件(instance of class)
    println(c.a)    // a 是 AAA 共享成員
    println(c.b)    // b 也是 AAA 成員之一
    println(i.a())  // 呼叫函式 a( )間接讀取 AAA.a
    println(i.b)    // 已經刻意將 b 做了符號讀取連結, 看似直接(實際是間接)讀取
    i.b = 3           // 已經刻意將 b 做了符號寫入連結, 看似直接(實際是間接)寫入
    println(c.b)    // 因為 b 是共享成員, 所以變身的成員也跟著改變
     
}
結果:
5
10
5
10
3

2019年7月26日 星期五

Kotlin 的 promise

若要用 Kotlin 寫 Javascript (Kotlin to JS), 使用 promise 要先 import kotlin.js.Promise,  接著宣告資料型態, 一般用法像是:
    import kotlin.js.Promise
    import kotlin.browser.*
    fun main() {   
           lateinit var resolve: (Int)->Unit
           val promise = Promise < Int > { Yes, _ ->
                resolve = Yes
           }
           promise.then { result ->    // only execute when promise fullfill
                println("I got $result")
           }
           window.setTimeout( {resolve(0)}, 2000)  // resolve result sometime or somewhere
    }
例如應用在按鍵事件驅動的不定時的異步化(async function)程式上相當實用.
promise 的原理很簡單, 只要沒呼叫  resolve( ) 去解析, 就會維持在暫停(suspend)狀態, 一旦執行過 resolve( ) 後就變成滿足(fullfill)狀態. 後續當呼叫 then { } 區塊時, 若還是在暫停狀態下,沒意外(exception)的話,就維持該狀態,但要將該區塊(指標變數)附加(累積)到 promise 物件裏面,一旦呼叫了 resolve(result), resolve 除了會解析結果外, 同時還會就將結果傳遞給之前所累積的 then { } 區塊去執行. 也就是說只有在滿足狀態當下, promise 的 then { } 區塊才會執行. 有個應用情境是:將 promise.then { } 區塊放在函式內執行, 再用像是 window.setTimeout 或是 window.window.requestAnimationFrame 定時啟動該函式, 重複啟動函式時, 若 promise 一直維持在暫停的狀態, 就會造成 then 區塊不停的累積, 一旦狀態滿足時, 就有可能造成區塊重複執行的後遺症, 例如:
     import kotlin.js.Promise
     import kotlin.browser.*
     fun main() {   
           lateinit var resolve: (Int)->Unit
           val promise = Promise < Int > { Yes, _ ->
                resolve = Yes
           }
           var count = 0
           fun render( ) {
                 val capture = count
                 promise.then { result ->  // dupcate then if not fullfill
                       println("fullfill:  result=${result} capture=${capture}")
                 }
                 if( count ++ < 10 ) window.requestAnimationFrame( {render() } )
           }
           window.requestAnimationFrame({render() })
           // promise not resolve
           window.setTimeout( {resolve(0)}, 2000)  // resolve result sometime or somewhere
     }
執行結果:
fullfill:  result=0 capture=0
fullfill:  result=0 capture=1
fullfill:  result=0 capture=2
fullfill:  result=0 capture=3
fullfill:  result=0 capture=4
fullfill:  result=0 capture=5
fullfill:  result=0 capture=6
fullfill:  result=0 capture=7
fullfill:  result=0 capture=8
fullfill:  result=0 capture=9
fullfill:  result=0 capture=10
為了用 kotlin 實現 async/await 的邏輯.  運用 promise 物件, 基本上就可以成為 async function, await 只能放在 async function 裏面, 直到 promise 完成解析(滿足狀態)才能進行後續動作, 因此特別用高階函式及 skip re-entry 技巧, 將 promise 包進新的類型, 並把他的 then 區塊打包放進高階函式當成 callback 函式去執行, 就能解決重複執行的困擾:
     import kotlin.js.Promise
     import kotlin.browser.*
     class myAsync {             
               var once = 0
               lateinit var resolve:(Int)-> Unit
               val promise = Promise < Int > { Yes, _ ->
                      resolve = Yes
               }
               fun bwait(callback: (Int) -> Unit ) { //  high order function with callback function
                    promise.then { result ->
                         if (once ++ == 0)  callback(result ) // 只執行第一次的 then  區塊
                    }
               }
               fun await(callback: (Int) -> Unit ) { //  high order function with callback function
                    once ++
                    promise.then { result ->
                         if (-- once == 0)  callback(result ) // 只執行最後一次的 then 區塊
                    }
               }
     }
     fun main() {
           val async = myAsync( )
           var count = 0
           fun render( ) {
                  val capture = count
                  async.await { result ->  // no duplicate then if fullfill
                       println("fullfill:  result=${result} capture=${capture}")
                 }
                 if( count ++ < 10 ) window.requestAnimationFrame( {render() } )
           }
           window.requestAnimationFrame( { render() } )         
           window.setTimeout( {async.resolve(0)}, 2000) // resolve result sometime or somewhere
     }
若是用 async.await  等待執行的結果:
         fullfill:  result=0   capture=10
若是用 async.bwait  等待執行的結果:
         fullfill:  result=0   capture=0


2019年7月14日 星期日

for Kotlin

一直以來寫 c 或 Javascript 時習慣用:
               for (i = 0; i < 10; i++)  // ...
Kotlinfor 迴圈語法只能用 for(variable in Range) 來實現,像上述例子就可用單端式範圍遞增數列運算子 until 來實現:
               for (i in 0 until 10)     // until 是單端式範圍, 包含 0, 但不含 10
或是改成雙端式範圍遞增數列運算子 .. 加以實現:
               for(i in 0 .. 9 ) // .. 是雙端式範圍, 0 與 9 皆包含在內
累進數(等差級數步階值)大於 1, 就要加上 step,  像上式用 c 或 Javascipt寫的步階改為 2:
               for (i = 0; i < 10; i+=2)  // ...
在 Kotlin 就直接加上 step 2:
               for (i in 0 until 10 step 2) // ...
如果是遞減數列(數字大往數字小的等差級數), 像是 c 或 Javascript 所寫雙端式範圍的迴圈:
               for (i=10; i>=0; i-=2) // 10, 8, 6, ..., 0
因為在 Kotlin 語法中, 步階值必須是正整數, 雙端式範圍遞減數列用 downTo 關鍵字來實現:
               for (i in 10 downTo 0 step 2) // ...
參考文章:    https://kotlinlang.org/docs/tutorials/kotlin-for-py/loops.html

2019年7月5日 星期五

使用 kotlinc-js 編譯程式碼快速學習 Kotlin 語言

1. 到官網下載 kotlin compiler 編譯器(kotlin-compiler-1.3.40.zip), 用 unzip 解壓縮並放在適當目錄(例如 pathToKotlinc), 編譯的執行檔會在目錄 bin 底下, 程式庫在目錄 lib 底下. 再將 lib 目錄下的檔案 kotlin-stdlib-js.jar 用 unzip 解壓縮至適當位置, 只提取裡面的 kotlin.jskotlin.js.map (debug 時會用到)檔案就可以了, 將他複製到工作目錄, 例如:
        cd
        mkdir   work
        cd work
        unzip  pathToKotlinc/lib/kotlin-stdlib-js.jar
2. 在工作目錄用文字編輯器寫一個 kotlin 程式, 例如:
        geany my.kt &
3. 將以下作複數運算的範例程式存成檔案 my.kt
import kotlin.math.*
data class C(var r:Double, var i:Double) { // C: Complex number, r: real part, i:image part
        val square:Double =  r*r + i*i   // |C|2
        val length:Double = sqrt(square) // abs(C) = |C|  = sqrt(|C|2)
        operator fun plus(a:C):C { // +
            r += a.r
            i += a.i
            return this
        }
        operator fun minus(a:C):C {  // -
            r -= a.r
            i -= a.i
            return this
        }
        operator fun times(a:C):C { // *
            val temp = a.r * r - a.i * i // real part
            i = a.i*r + a.r*i  // image part
            r = temp
            return this
        }
        operator fun div(a:C):C { //  ÷
            if (a.length > 0f) {
                val temp = (a.r*r + a.i * i) / a.square // real part
                i = (a.i*r - a.r*i) / a.square  // image part
                r = temp
            } else throw Exception("Divide by zero(length=0)")
            return this
        }
        operator fun invoke( ):C{ // dump self complex number
            val real   = floor(r*100+0.5).toFloat()/100 // 實部只顯示到小數點 2 位, 4 捨 5 入
            val imag = floor(i*100+0.5).toFloat()/100 // 虛部只顯示到小數點 2 位, 4捨 5 入
            if (imag < 0) println("$real - ${-imag} i")  else  println("$real + $imag i")
            return this
        }

fun main(){
    try {
         val c = C(3.0, 3.0) / C(5.0, 2.0)   //  c = (3 + 3i) / (5 + 2i)
          c( )  // dump
    } catch (e:Exception){
        println(e.message)
    }
}
4. 用 kotlin 的編譯器 kotlinc-js 將上述 kotlin 原始碼翻譯為 Javascript:
        pathToKotlinc/bin/kotlinc-js   my.kt   -output   my.js
或是利用 geany 的設定組建命令, 直接點選工具欄:  組建(B) - > 設定組件命令(S), 第一欄隨意輸入識別名稱(例如 kotlin2js),  第二欄輸入 kotlin2js 編譯器的位置及參數(%f 是目前編輯檔案名稱, %e 是不含副檔名的檔案名稱, -output 可以指定輸出的檔案名稱), 例如:
          pathToKotlinc/bin/kotlinc-js   %f   -output  %e.js
意思是說用  kotlinc-js 將目前的檔案翻譯成 Javascript, 輸出檔與編譯的同名稱但副檔名改為 js, 第三欄可以空白預設是目前的工作目錄, 設定好後, 用滑鼠點選: 組建 ->  kotlin2js 執行看看, 應該會在工作目錄編譯出 my.js.
若要在 vscode 裏面編譯, 參考  https://code.visualstudio.com/docs/editor/variables-reference, 編寫一個 tasks.json 檔案, 點選 Terminal -> Configure Tasks帶出編輯視窗, 把以下內容寫入:
{
    "tasks": [{
            "label": "Kotlinc2js",
            "type": "shell",
            "command": "pathToKotlinc/bin/kotlinc-js",
            "args": ["${file}", "-output", "${fileBasenameNoExtension}.js"],
            "group": {
                "kind": "build",
                "isDefault": true
            }
        }]
}
存檔後, 點選 Terminal -> Run Build task 或是用快速按鍵 Ctrl-Shift-B, 就能將目前編輯的 Kotlin 程式檔轉換成 Javascrip
5. 在工作目錄底下用文字編輯器另外編輯一個 my.html 準備讓瀏覽器開啟, 並在開發工具底下執行用 kotlin  語言所寫的程式碼(實際上是翻譯成兩個 Javascript 檔案: kotlin.js 及 my.js). 將以下內容存檔(my.html):
<html><head><meta charset="UTF-8"></head>
<body>
        <script type="text/javascript" src="kotlin.js"></script>
        <script type="text/javascript" src="my.js"></script>
</body>
</html>
6. 用瀏覽器 chrome 或是 firefox 打開上述 my.html 檔案, 接著按滑鼠右鍵點選檢查原始碼來開啟內建的開發工具, 在開發工具欄點選 console 就能看到程式的輸出結果

Kotlin 的 Delegated Properties (被代理的變數或特性)

參考文章:  https://kotlinlang.org/docs/reference/delegated-properties.html
針對類型裡的特性(propertys), 常數(val)的特性內定 getter 是用 get( ) 方法去讀取主角(field)的值, 對於可寫入的特性(var)而言, 內定 setter 則用 set(v: Type) 方法設定幕後的主角(field), 除非要另外處理 field 的運算才需改寫, 否則 kotlin 會自動加上必要程式碼:
      class SomeClass {
            var   v: String = "?"
                    //  get( ) { return field }
                    //  set(s:String) { field = s }
            val   readonly: Int = 0
                    //  get( ) { return field }
      }
物件在引用特性時不必後綴成對小括號, 因此很簡潔. 物件引用方法( ) 時可以看做是一個運算的結果, 也就是所謂的函數. Kotlin 針對特性或變數有個特別條款可以用 by 跑龍套, 也就是說留待跑龍套代為處置, Koltin  已經為跑龍套主角(object)指定了兩個運算 operator getValue( )  及 operator setValue( ) 來處理特性或變數的讀寫機制, 若想改寫,可以 import pacage 針對讀與寫操作分別作運算,其中對應 get( ) 方法就是 operator getValue( ), 對應  set( ) 方法就是 operator setValue( ), 詳如範例:
import kotlin.reflect.*
class InitString (str: String) { // 代理的主角類型
    var bakendField: String = str  // 真正被讀寫的幕後主角
    operator fun getValue(thisRef: Any?, property: KProperty<*>): String {
         print(property.name + " : ")
         return bakendField  // return value for method get( ), 讀取
    }
    operator fun setValue(thisRef: Any?, property: KProperty<*>, v: String) {
        println(property.name + " = " + v)
        bakendField = v     // set value to property for method set(v), 寫入
    }
}
fun main(){
    var c : String   by   InitString("???")  // 用物件 InitString( ) 代理改寫變數的讀/寫機制
    println(c)  // 讀取變數 c
    c = "dd"    // 設定變數 c
    println(c)  // 重新讀取變數 c
}
執行結果:
c : ???
c = dd
c : dd

2019年7月4日 星期四

Kotlin 的 data class 搭配有趣的運算子 operator fun

利用 data class 簡單就能定義複數(實部及虛部), 再定義出類型的運算方法:  operator fun plus( ), minus( ), times( ), div( ) 這四則運算, 就能用來計算複數, 若定義出 operator fun involke( ) 這個方法就能讓物件身兼特定功能(c++ 稱為 functor):
import kotlin.math.*
data class C(var r:Double, var i:Double) { // C: Complex number, r: real part, i:image part
        val square:Double =  r*r + i*i   // |C|2
        val length:Double = sqrt(square) // abs(C) = |C|  = sqrt(|C|2)
        operator fun plus(a:C):C { // +
            r += a.r
            i += a.i
            return this
        }
        operator fun minus(a:C):C {  // -
            r -= a.r
            i -= a.i
            return this
        }
        operator fun times(a:C):C { // *
            val temp = a.r * r - a.i * i // real part
            i = a.i*r + a.r*i  // image part
            r = temp
            return this
        }
        operator fun div(a:C):C { //  ÷
            if (a.length > 0f) {
                val temp = (a.r*r + a.i * i) / a.square // real part
                i = (a.i*r - a.r*i) / a.square  // image part
                r = temp
            } else throw Exception("Divide by zero(length=0)")
            return this
        }
        operator fun invoke( ):C{ // dump self complex number           
            val real = floor(r*100+0.5)/100 // 實部只顯示到小數點 2 位, 4 捨 5 入
            val imag = floor(i*100+0.5)/100 // 需部只顯示到小數點 2 位, 4 捨 5 入
            if (imag < 0) println("$real - ${-imag} i")  else  println("$real + $imag i")
            return this
        }
}   
fun main(){
    try {
         val c = ( ( C(3.0, 3.0) + C(2.0, 1.0) ) * C(9.0, 2.0) - C(10.0, 2.0) ) / C(5.0, 2.0)
          c( )  // Complex number dump: c = (((3 + 3i)+(2 + i))*(9 + 2i)-(10 + 2i))÷(5 + 2i)
    } catch (e:Exception){
        println(e.message)
    }
}
輸出結果:
7.68 - 5.72 i

2019年7月2日 星期二

理解 Kotlin 的高階函式

參考文章: https://kotlinlang.org/docs/reference/lambdas.html
Kotlin 是一個 typesafe 程式語言, 每個變數都必須需確認他的資料型態(type), 函式的名稱可以看成是常數(變數), 因此函式也不例外需要加以宣告. 程式資料型態(function type)可以用成對小括號加上箭頭符號組成 ( ) -> , 用 typealias 定義函式型態後可以讓程式碼變得簡單易讀, 同時也能重複利用. 定義函式時若將函式當作輸入參數或是輸出函數,他就是所謂的高階函式 :
typealias  funit =  ( ) -> Unit   // 函式資料型態: 無輸入參數, 也不傳回任何數值
fun g(f: funit) { // f 是一個輸入函式, 因此 g(f) 是高階函式,  f 可以在函式 g 裏面執行
    println("Run in HighOrderFunction")
    f( ) // 輸入的函式在此開始執行
}
fun main() {
    val  f: funit =  {  println("Hello")        }  // f 指定成 λ 函式, 符合"無輸入參數,無傳回值"
    g( f ) // 把函式丟進高階函式裏面去執行,
}
有一種有趣的高階函式:輸入參數是一個攜帶著物件類型的函式(lambda with Receiver), 透過綁定物件的手法, 讓呼叫的方法就好像侷限(scope)在物件裏面執行(context)一樣:
        class Classname( ) {
              fun doMethod( ) {
                    println("This is a method")
              }
        }
        fun HighOrderλwithReceiver(λ: Classname.( ) -> Unit) {
             val objectContext =  Classname( )            
             objectContext.λ ( )
        } 
        fun main( ) { 
               // fun doMethod { println("Global") }
                HighOrderλwithReceiver  {       
                        // fun doMethod { println("Local") }                   
                       doMethod( )   // this.doMethod( )
                }
        }
上述當呼叫 HighOrderλwithReceiver 函式時, 因為最後一項參數是 λ 函式, 所以將它搬離出小括號, 搬離後剛好沒有參數, 小括號也跟著不用寫, 這是 Kotlin 語言的特異功能. 傳進來的 λ 區塊 {   } , 在裏面的變數若沒有指定物件的範疇(scope)時, 編譯器就會在當地區域(Local scope),物件的本體區域(context 也就是 this), 及整體區域(Global scope) 推測(infer)並編譯(compile)出適當的變數或函數, 因為在高階函式 HighOrderλwithReceiver 的定義裏面, 已經預先設定進來的函式屬於 Classname 類型的一種方法, 就好像是先築起一道牆(scope), 利用類型建立起物件副本(instance)也就是物件文本(context),接著在物件的範疇下執行 λ 裏面的每一道程序, 因此 λ 區塊裏面, 呼叫的函式若是 Classname 裡面的從屬函數的話, this 就可以省略不寫了. 這是 Function literals with receiver 厲害的地方.

2019年7月1日 星期一

Kotin 的 Coroutines Channels

Coroutine 之間的訊息溝通可以透過 Channel(通道), 只要在程式開頭處 import package:
           import kotlinx.coroutines.channels.*
之後才能使用 Kotlin 所提供的函式庫. 參考文章:
https://play.kotlinlang.org/hands-on/Introduction%20to%20Coroutines%20and%20Channels/08_Channels
通道是一種物件類型, 他所提供的方法 send 和 receive 是一種會依據狀況而隨時暫停(suspend)或重啟(resume)執行的函式(suspend function), 因此也只能用在 coroutine 區塊內發動. 接收通道的介面是 ReceiveChannel, 發送通道的介面是 SendChannel, 兼具接收與發送雙向類型的通道就是 Channel, 根據通道內容物的數量又可以區分幾種型式:
1. 數量為 0 時, 也就是無緩衝, 稱為 Rendezvous buffer(不見不散型通道), 收發端發現沒有東西就會在那邊等(suspend), 直到對方出現後才各自分道揚鑣(resume), 這是預設的通道型式:
      val 不見不散通道 = Channel < String > ( )
2. 大於 0 且數量有限, 稱為 Buffer channel (緩衝型通道), 一旦緩衝區滿了,發送端才會暫停(suspend), 而接收端只要發現沒東西時就會在那邊等(suspend).狀況一旦解除就會重啟(resume):
      val 緩衝通道 = Channel < String > (10)
3. 緩衝數量是 1, 且發送端不受限, 因此收發端只會收到最後一筆(前面發送的資料會被覆蓋)稱為 CONFLATED buffer (合而為一型緩衝通道),只有當接收端沒看到東西才會等待:
      val 合而為一通道 = Channel < String > (CONFLATED)
4. 數量無限制時就是 UNLIMITED buffer(超級緩衝通道), 它就像是一個先進先出有秩序的隊伍(Queue), 只有當記憶體不夠時, 發送端才會產生例外(exception), 接收端只要收不到東西時就在那邊等待(suspend),狀況一旦解除就會重啟(resume):
      val 超級通道 = Channel < String > (UNLIMITED)

2019年6月27日 星期四

理解 Kotlin 的 Coroutines 的運作行為

參考文章: https://kotlinlang.org/docs/reference/coroutines/basics.html
簡單來說, Kotlin 的 Coroutines 是一個資源共享的程序區塊簡稱窟程序(一段程序走著走著跑到窟窿去了, 後續可以命令它再鑽出來), 它可以將一條執行緒(Thread)分裂(一個 launch 或 async 區塊只會分裂一次)成不同的程序路徑, 經多次分裂(launchs 或是 asyncs)後就像是(但不是)多條執行緒同時運行, 先了解一下 runBlocking {   } 的運作, 顧名思義,它就是一個阻礙式(blocking)函數 λ (lambda), 區塊內如同一般的函式, 等到區塊內的程序走完才能繼續運行下一步, 但在 λ 區塊 { } 作用(scope)下可以建立出同時進行的程序區塊(Coroutine), 也就是利用像是 async {  } 或是 launch { } 去分裂出 Coroutine,  讓原本循序的程式碼一分為二又不阻塞(block)程式繼續運行, launch 建立出程序區塊後會傳回一個在背景運作的工作代號(job), 後續透過方法 join( ) 可以同步確認程序的完成. 它適用於不需回傳結果的程序,或者稱之為射後不理程序區塊, 至於 async 建立的程序區塊則是從發動的 suspend fun 中獲取一個延遲物件 Deferred <T> , 其中的 <T> 視函式(function)的傳回值而定, 後續再透過延遲物件的方法 await( ) 去等待(suspend)回傳輸出結果, 這是 async 與 launch 區塊應用時最大的區別, 每一個 launch 或 async 區塊都是獨立並行運作的, 一經發動就在背景依序運行, Coroutine 區塊內每條程序也就循序的(sequential)運作起來. 我們可以把 runBlocking 看成是循序程式碼與程序區塊間的中介運作橋樑,如果想要在主程序發動 Coroutine, 另外可以用  GlobalScope.launch { } 去建立與主程序同時進行的程序區塊讓它在背景同時運行, 與 runBlocking 不同的是 GlobalScope.launch 直接回傳一個背景代碼就繼續運行而不會阻塞. 一旦產生 GlobalScope.launch 的 job 後, 就有責任需要呼叫其方法 join( ), 等待該 job 同步完成, 否則有可能程序未完成前就被終結掉, 但 join( ) 也只能放在 Coroutine 區塊內(scope)呼叫. 至於 runBlocking 本身就會等待內部 Coroutine 完成, 正如同程式名所言.因此不用呼叫對應的 join( ).以下範例程式可以貼到 https://play.kotlinlang.org  執行  Playground 看看:

import kotlinx.coroutines.*
fun main( ) {
     println("main start")
     val     job = GlobalScope.launch {
             println("Global Job 1 start")
             delay(2000L)   // suspend for 2 seconds
             println("Global Job 1 stop")
     }
    println("main running")
    runBlocking {
            launch {
                   println("Job 2 start")
                   delay(500L)   // suspend for  500ms = 0.5 sec
                   println("Job 2 stop")
            }        
            launch {
                   println("Job 3 start")
                   delay(1000L) // suspend for 1 sec
                   println("Job 3 stop")
            }  
            job.join()  // wait job in GlobalScope.launch to finish
      }
     println("main stop")
}
看輸出結果可以想像: 上述程序建構了(launch) 了 3 個並行的工作, 就好像從主程序分裂出 3 條執行緒同時運行, 主程序走到 runBlocking 會一直等待直到它結束, 最後主程序執行最後一行列印指令終結任務:
main start
main running

Global Job 1 start
Job 2 start
Job 3 start

Job 2 stop
Job 3 stop

Global Job 1 stop
main stop

Coroutine 運作的行為方式很像是(但不是)執行緒(Thread), 每一個 coroutine 都會攜帶 Context (文本), 在此架構下去運作一個輕量級執行緒, 透過執行緒派任工 Dispatchers 將任務(Task  or job)派發至專屬文本的執行緒去運行,換句話說, 他並不一定要產生執行緒, 而是將任務分配給執行緒,定義可供 coroutine 呼叫的函式, 之前要加上 suspend 關鍵字讓 Kotlin compiler 把它轉變成可以暫停或持續運作的函式.  Kotlin 幾個重要的派任工:
1. Dispatchers.Main: 通常是派任至前景執行緒
2. Dispatchers.IO: 通常是派任至背景,需長時間運作的執行緒
3. Dispatchers.Default: 通常是派任至背景, 短時間加速完成的執行緒
4. Dispatchers.Unconfined: 派任至目前呼叫的 Thread, 開始執行一直到第一個暫停點結束.
透過內建 withContext 及派任工, 可以將任務交給專屬執行緒在其所屬文本底下運行, 或是創建一群共同生命期(coutineScope) coroutines, 他既不組塞目前 Thread 運作, 又可以非同步操作函式, 等到裏面全數 coroutine結束才會終結生命:
suspend fun mySuspend( ) =   withContext(Dispatcher.Main) { // dispatch to Thread   
      //  dispatch to main UI thread
}
suspend fun myScope( ) = coroutineScope { // create a group of coroutines to live together
      async {
         // ...
      } 
      launch {

         // ...
      }
} // live until finishing all of coroutines (async or launch)
一個 Coroutine 是一個有生命周期(scope)的任務(Job),透過 coroutineContext[Job]  就可以獲取目前的任務. CoroutineScope 介面只含唯一屬性 coroutineContext,  至於 async 與 launch 是 CoroutineScope 介面的從屬函數, coroutineScope 創建工則是另一個 suspend 的函式, 它根據 coroutineContext 去建立 coroutine 上下父子關係(承上啟下). 參考文章:
         https://medium.com/@elizarov/coroutine-context-and-scope-c8b255d59055:
或許可以這樣理解: coutineScope { } 是 Kotlin 的一群 coroutines 區塊的生命期創建工(builder), 而 async { } 或 launch { } 僅是單一 coroutine 區塊生命期創建工.
Task, Context, Thread 都是作業系統的專有名詞, Task 是一個獨立作業的前景或背景任務簡稱任務, Context 是執行任務時的特定參數資料庫(database)簡稱文本, Thread 則是運行任務時可以走的程序路徑(program path)簡稱執行緒, 當 CPU 支援硬體的 multiThread (多工緒)或是透過軟體摹擬出 Thread pool(工緒池塘, 一整池的執行緒)時就可以將程式碼分配到不同路徑去執行,主要目地是增加運算效率並改善使用者體驗. 過多的 Threads 對記憶體需求將是一大考驗, 但對 Coroutines 卻不是問題, 這是他神奇的地方. 查了網路理解了一下 Coroutine 的運作原理: 首先透過 suspend 函式宣告後, 編譯器(compiler) 將 suspend 函式轉變成狀態機(state machine), 呼叫時再傳一個叫作 Continuation 的物件進入該函式, 之後透過 Continuation 物件去掌控函式暫停(suspend)或重啟(resume)的狀態, 有點類似Javascript yield 的運行方式, 暫停時傳回值並保留運作狀態, 後續回頭重啟時再從暫停的地方繼續執行,其壓根就不是 Thread, 因此可以透過派任的方式讓 Coroutines 在其他 Thread 上運行,最後來看一個例子:

    import kotlin.coroutines.*
    import kotlinx.coroutines.*
    lateinit var continuation1: Continuation < Unit >
    lateinit var continuation2: Continuation < Unit >
    fun main(args: Array <String> ) {
        println("Stat of main")
        GlobalScope.launch(Dispatchers.Unconfined) {
             println("Coroutine Start")   
             suspendCoroutine<Unit> { // it is a Continuation object
                println("Coroutine seq#1 running")
                continuation1 = it   // save the continuation object to be resume later
                println("Coroutine seq#1 suspend") // will suspend here
             }                
             println("Coroutine running") 
             suspendCoroutine<Unit> { // it is a Continuation object
                println("Coroutine seq#2 running")
                continuation2 = it   // save the continuation object to be resume later
                println("Coroutine seq#2 suspend") // will suspend here
             }   
             println("Coroutine stop")
         }
         println("Resume 1 in main")
         continuation1.resume(Unit) // resume through continuation
         println("Resume 2 in main")
         continuation2.resume(Unit) // resume through continuation
         println("End of main")
    }
輸出結果:
Stat of main
Coroutine Start
Coroutine seq#1 running
Coroutine seq#1 suspend
Resume 1 in main
Coroutine running
Coroutine seq#2 running
Coroutine seq#2 suspend
Resume 2 in main
Coroutine stop
End of main

可以看到兩個 suspend 函式放在一個 launch 區塊內時, 他們是接續(sequential)運行, 並非想像中同時(parallel)運作, 若真要同時(運作兩個程序區塊, 那就要把他放在不同的 launch 或是 async 區塊去運行, 像是這樣就可以了:
    import kotlin.coroutines.*
    import kotlinx.coroutines.*
    lateinit var continuation1: Continuation < Unit >
    lateinit var continuation2: Continuation < Unit >
    fun main(args: Array < String > ) {
        println("Stat of main")
        GlobalScope.launch(Dispatchers.Unconfined) {
             println("Coroutine Start") 
             val seq1 = async {
                 suspendCancellableCoroutine < Unit > { // it is a Continuation object
                     println("Coroutine seq#1 running")
                     continuation1 = it   // save the continuation object to be resume later
                     println("Coroutine seq#1 suspend") // will suspend here
                 }
                 "Coroutine seq#1 stop" // return a string
             }
             "Coroutine running"
             val seq2 = async {
                 suspendCancellableCoroutine < Unit > { // it is a Continuation object
                     println("Coroutine seq#2 running")
                     continuation2 = it   // save the continuation object to be resume later
                     println("Coroutine seq#2 suspend") // will suspend here
                 }
                 "Coroutine seq#2 stop" //  return a string
             }             
             println(seq1.await()) // will suspend until seq1 return
             println(seq2.await()) // will suspend until seq2 return
             println("Coroutine stop")
         }
         println("Resume 1 in main")
         continuation1.resume(Unit) // resume through continuation
         println("Resume 2 in main")
         continuation2.resume(Unit) // resume through continuation
         println("End of main")
    }
執行結果:
Stat of main
Coroutine Start
Coroutine running
Coroutine seq#1 running
Coroutine seq#1 suspend
Coroutine seq#2 running
Coroutine seq#2 suspend
Resume 1 in main
Coroutine seq#1 stop
Resume 2 in main
Coroutine seq#2 stop
Coroutine stop
End of main

2019年6月23日 星期日

Android 的多工機制 HandlerThread , 理解 Looper , Handler 協調與分工

參考資料:
1. https://blog.mindorks.com/android-core-looper-handler-and-handlerthread-bd54d69fe91a
2. http://www.mallaudin.com/looper-handler-api-part-4
單純使用 Thread 可以讓程式放在背景去執行, 但 Thread 執行完就消失了. 為了要讓 Thread 重複利用, 分派複雜的工作又不會阻塞程式運行, 首先要了解 Looper 與 Handler 如何分工:  Looper 稱為迴圈訊息搬運工, 他主要建構一個工作訊息串,把要工作的訊息駐列成隊, 當 Thread 裏面無任何工作訊息時, 就暫停進入休眠(剛開始仍需要發動 start( ) 啟動 Thread 開始運作), 一旦駐列有工作訊息就交給 Handler 在背景處理, Handler 可以想像是工頭(迴圈訊息處理工),負責處理程式運作之邏輯, 包含 message 的解釋(sendMessage),  或是處理像是 post 過來的片段程式碼, 若要自行定義含 Looper 的 Thread, 可以利用 Thread 裏面 run( ) 區塊, 去標示一個類似多工環境的程式迴圈, 介於 Looper.prepare( ) 與 Looper.loop( ) 之間, 從而讓 Thread 週而復始不停的運作又不阻塞(nonblock), 除非呼叫 Looper 的方法 quit( ) 打破迴圈, 才能結束 thread loop 讓系統回收資源. Android 內建的 HandlerThread 其實是一個內建了 Looper 的 Thread, 可以搭配後續的 Handler 去執行所要交代的任務:
        val background = HandlerThread("Hi").run {
            start() // HandlerThread.start( ) to run
            object : Handler(looper) { // return Handler for this looper
                override fun handleMessage(msg: Message) { //message handler
                    when(msg.what){ // get mesage
                        0 -> looper.quit() // quit the looper
                        //...
                    }
                }
            }
        }
        background.sendEmptyMessage(0) // send message 0  to HandlerThread

2019年6月18日 星期二

Android 使用 kotlin 語言讀取觸控螢幕訊息

 Android 手機產生觸控動作時,在Activity裏面會觸發 onTouchEvent(event: MotionEvent) 這隻函式, 只要覆寫該程式, 並利用 event 的一些成員就可操作整個觸控螢幕, 為了減少累綴物件程式碼, 可以利用 with(event}{when(actionMasked){ }} 將操控成員放在區域塊(scope)內操作.以下列出 MotionEvent 重要成員:
    pointerCount :螢幕觸控點累積的數量
    actionIndex :觸發動作的索引鍵(index), 0是第 1 個 UP 點, pointerCount - 1 是最後一點
    actionMasked :觸控動作遮罩用來判斷觸控點的動作, 共有以下 6 個工作狀態:
    MotionEvent.ACTION_CANCEL : 觸控動作被取消了
    MotionEvent.ACTION_DOWN :手指按壓觸控螢幕後, 當下 pointerCount==1 時觸發
    MotionEvent.ACTION_POINTER_DOWN :當手指按壓螢幕後, pointerCount>1 時觸發
    MotionEvent.ACTION_UP :手指離開觸控螢幕時, 當下 pointerCount==1 時觸發
    MotionEvent.ACTION_POINTER_UP :當手指離開螢幕時, pointerCount>1 觸發
    MotionEvent.ACTION_MOVE:只要有手指在螢幕上移動就觸發,此時actionIndex固定是 0,似乎是固定追蹤第一個 UP 的觸控點, 當pointerCount > 1 時, 額外觸控點座標就要限縮索引值後再去讀取:  for(p in 1 until pointerCount) {  getPointerId(p); getX(p) ; getY(p); ... } 而每個觸控點 id 透過 getPointerId(索引值) 加以區分, 其他常用的成員函數還有:
    getX(index):  取得索引觸控點的X(橫)軸座標值
    getY(index):  取得索引觸發點的Y(縱)軸座標值
    getPointerId(index): 取得索引觸發點的唯一序號(id)
    findPointerIndex(id): 取得觸發點唯一序號所對應的索引鍵
// Kotlin 程式碼: activity.kt    ...
override fun onTouchEvent(event: MotionEvent): Boolean {
    return with(event){
         val index =  actionIndex
         val id = getPointerId(index)
         var motion  = "(" + getX(index).toInt() + "," + getY(index).toInt() + ")#" + id +"#\n"
         motion += "[index=" + index + "]/"+ pointerCount+ "\n"
         when(actionMasked) {
                MotionEvent.ACTION_CANCEL -> motion +=":CANCEL"
                MotionEvent.ACTION_DOWN -> motion +=":DOWN"
                 MotionEvent.ACTION_POINTER_DOWN -> motion +=":POINT DOWN"
                MotionEvent.ACTION_UP -> motion +=":UP"
                MotionEvent.ACTION_POINTER_UP -> motion +=":POINT UP"
                MotionEvent.ACTION_MOVE -> {
                    motion +=":MOVE\n"
                    for(x in 1 until pointerCount)  motion += "(" + getX(x).toInt() + "," + getY(x).toInt() + ")" +"#" + getPointerId(x) +"#\n"                   
                }
         }
         Toast.makeText(applicationContext, motion, LENGTH_SHORT).show()
          return true
    }
}

2019年6月17日 星期一

理解 Android app 的 activity 與 context

參考文章
https://android-developers.googleblog.com/2009/01/avoiding-memory-leaks.html
一個 Android app 通常有兩種 context (用來承先啟後的上下文本): application context 及activity context, 只要 app 有任何(幕前或幕後的)Activity(活動)正在運作 applicationConext 都會存在, 但 activityContext 只存在當下(幕前)的活動, 一旦 app 被切換至幕後時, 例如翻轉螢幕時, 幕前的活動(activity)就會暫停(onPause), 接著 activityContext 就消失了, 系統要再重新喚醒(onResume)復出另一個幕前的 Activity, 因此活動暫停前, 一些在 Activity 的重要參數必須先保存, 當然 app 可以在 Activity 程式裏面動態分配空間來儲存資源並指定給變數參照, 一旦動態分配了資源, 後續資源回收機制(GC)若沒有適當釋放, 一旦活動結束, 參照的變數跟著消失, 就會造成記憶體洩漏危機(memory leak: 無法回收的資源), 萬一動態分配的空間夠大, 只要將手機多翻轉幾次, 很快的系統記憶體就不夠用了

2019年6月14日 星期五

Kotlin 的 try catch, if else, when( ) { else } 等區塊敘述與函數

例外處理一直是很煩瑣的任務, 要讓程式碼看起來簡潔易懂, 可以利用 kotlin 的 try-catch陳述函數, 也可以搭配 if else, when 等條件陳述函數去處理, 只要把結果擺在區塊內最後一行就可以回傳, 而且回傳的資料型態必須與描述宣告的(statement)一致, 例如 try expression(函數):
  val a: Boolean = try { TaskMayThrowException() ;     true } catch (e: typeName) { false }
意思是說: 嘗試執行任務, 正常執行最後要回傳 true, 否則當產生不可預期事件時回傳 false, 詳情參考: https://kotlinlang.org/docs/reference/exceptions.html
要注意的是:
1. 必需用雙括號將程式碼分別圈出 try 與 catch 的程式區塊
2. 要接收的變數或常數, 要宣告資料型態(包含 catch 的接收參數), 主要是讓回傳資料型態一致
3. 無論有無發生例外, 都會執行 finally { } 程式區塊, 但 finally 區塊並不會回傳最後敘述值
4. try 區塊必須跟隨至少一個 catch 區塊, 或是接續 finally 區塊(可以省略 catch 區塊)處理
5. 描述宣告(statement)不會取值, 但陳述函數(expression)會取最後一行結果當函數值
把 if { }  else { }, when(condition ) {  else { }   } 等當成陳述函, 詳情參考:
https://kotlinlang.org/docs/tutorials/kotlin-for-py/conditionals.html

Kotlin 的 run, apply, let, also, with

參考文章: https://kotlinlang.org/docs/reference/scope-functions.html
要分辨 run, apply, let, also 等擴展函式的用法, 首先要明瞭 λ(lambda) 的區塊表達式:
      { parameters  ->  retval }
或是參考之前的文章:
      http://amitmason.blogspot.com/2018/08/kotlin-function.html         
其中箭頭前面是: 分別用逗號分開的輸入參數,箭頭之後的最後一行是函式的傳回值(簡稱函數),當數入參數只有一個時就可以使用 it 代表唯一參數, 並且可以省略箭頭符號與參數的宣告,就只剩下:
           {   ... ; retval }
物件產生後, 內建的 run { } 及 let  {  } 就是屬於這種型態的 λ, 兩者差別是 let 會將物件本身當唯一輸入參數也就是 it, 但 run 完全無輸入參數, 因此不能在 run 區塊裏面使用 it, 兩者相同的是: 將最後一行結果當作函數值傳回. 至於 also 同樣會將物件本身當唯一參數 it, 但不管最後一行的結果如何, also 的函數傳回值還是物件本身, apply 與 also 類似 , 函數傳回值也是物件本身,無輸入參數 it,  這些 run, let, apply, also 函式共通點是: 所有在區域(scope)內的從屬函式(member function)不需要引用物件句號, kotlin 自動會引用物件, 因此可以減少不少程式代碼, 下表就是根據 λ 的輸入與輸出列出函式差異, X = no,  V = Yes
                            it=物件輸入     函數輸出=物件本身
           run           X                       X                                   .run { member( );  retval   }
           let            V                       X                                   .let   { member( );  it.  ; ... ;  retval  }
           apply       X                       V                                  .apply{  member( );       }.apply{ ... }
           also          V                       V                                  .also { member( );  it. ; ...  }.also{ ...   }
由於 apply 與 also 函數傳回值就是物件本身, 因此可以不斷的串下去, 至於 run 與 let 傳回值是函式最後一行來指定, 因此適用於轉換不同物件時(transform)使用
說到 run 與 let 實際上的應用,以從屬函式(member function)的角度來看, let 有了 it 的加持後,就可從物件本身為起點全面展開, 但 run 只能從所屬函式為起點來展開:
val ret1 = objectTo.run { member1( ) ; member2( ); ... ;  ret_val }
val ret2 = objectTo.let   { 
           val some_ret = it.let { member1( );  member2( ); ... ; retval } 
           ...  Transform(it)
           ret_val
    } 
同樣觀點也適用於分辨 apply 與 also (帶有 it 物件本身)運用上的差異. 他們與 run 和 let 差異只在傳回物件的不同,  run 與 let 可以自訂傳回值, apply 與 aslo 傳回物已經內定是物件本身, 因此程式碼裏面沒有傳回值:
   objectTo.apply { member1( ) ; member2( ); ... }
   objectTo.also {
           it.also { member1( );  member2( ); ... }
           ...  Transform(it)           
    }
而且 run, let, aslo, apply 可以互相穿插與搭配, 從單純需求(輸入參數 it , 輸出本身物件 this)的角度來看:
1. 不用再處理物件本身(it), 單純用 run 或 apply 就夠了, 之後若要傳回計算值(函數), 就用 run,  否則用 apply
2. 不用轉換物件, 最常用到的是 apply 或 also, 若又要處理物件本身(it)時就用 also
3. 若還需要處理物件本身並轉換成不同物件就只能用帶輸入參數(it)的 let 
記憶口訣: let it be and run return, also it apply this. 意思是區分 let, run 和 also, apply 為兩組擴展函數, 只有 let run 可以自訂 return, 而 let 與 also 可以接 it, also apply 最後傳自我. 最後一個 with 其實跟 run 差不多, 差異在語法的使用, with 是 kotlin 的語法, 但 run 是擴展函數 λ( ), 操作  with(obj) { ... } 的結果就如同  obj.run { ... }  同樣都會把區塊最後一行的結果當作函數

2019年4月6日 星期六

關於 linux mint 的作業系統(軟硬體)升級

最近採購一些電腦零件花了台幣共 6688 元, 硬體升級  CPU 成 AMD R3  2200G(NTD 3068元), 主機板用 MSI B450M PRO-VDH V2(NTD 2370元), 記憶體是 DDR4 8G bytes/2666 Mhz(NTD 1250元), 作業系統用 linux mint 19(linux kernel 是  4.15.0), 開機後, 螢幕呈現兩個畫面, 根本無法操作, 只能關機, 查了網路文章, 說可以試著在 kernel parameters 加上 nomodeset, 真的正常開機了, 聲音, 網路, USB 2.0/3.0 都可以正常運作,  後來又去下載 Ubuntu 18.10 iso(linux kernel 版本是 4.18) 檔, 可能是 grub 在這些硬體下, 無法讀取到 iso 檔的內容, 只好將 linux kernel(vmlinuz) 及 init ramdisk(initrd)  擷取出來放在硬碟的  /boot/kernel418 目錄底下, 稍為更改一些設定, 搜尋 linux mint iso 檔來 boot (啟動) linux 作業系統. 我還是習慣用 linux mint 的介面, 操作比較方便.  我也嘗試將 Ubuntu 18.10 iso 檔內的 kernel 擷取出來, 去啟動 linux mint 19 的 iso 檔, 還真的不需要加入 nomodeset 就可以正常開機, 但聲音出不來, 有可能是 loadable linux kernel module (可載入的動態核心模組)版本不合, 造成無法載入,  只好先放棄, 未來可以嘗試下載 4.19 以上新版本的核心原始碼(kernel source code)自行編譯並更新 linux mint iso 裡面的核心模組, 也許不需加入  nomodeset 核心參數就能啟動. 我的 grub.cfg 設定:

set timeout=3
set default=1
hiddenmenu
menuentry "GPT Linux mint 191 Read Only" {
set root=(hd0,1)
set iso=/boot/linux191.iso
loopback loop $iso
linux   (loop)/casper/vmlinuz    boot=casper   iso-scan/filename=$iso    locale=zh_TW.UTF-8    nomodeset
initrd   (loop)/casper/initrd.lz
}

menuentry "GPT Linux mint 191 iso persistent" {
set root=(hd0,1)
set iso=/boot/linux191.iso
loopback loop $iso
linux   (loop)/casper/vmlinuz    boot=casper   iso-scan/filename=$iso   locale=zh_TW.UTF-8     persistent   nomodeset
initrd   (loop)/casper/initrd.lz
}

menuentry "GPT Ubuntu-18.10 iso" {
set root=(hd0,1)
set iso=/boot/ubuntu-18.10-desktop-amd64.iso
linux    /boot/kernel418/vmlinuz   boot=casper    iso-scan/filename=$iso locale=zh_TW.UTF-8
initrd   /boot/kernel418/initrd
}

menuentry "GPT Test Linux mint 19 iso" {
set root=(hd0,1)
set iso=/boot/linux191.iso
linux    /boot/kernel418/vmlinuz   boot=casper    iso-scan/filename=$iso locale=zh_TW.UTF-8
initrd   /boot/kernel418/initrd
}

Linux Mint 19 開啟終端機執行 inxi  顯示 CPU 資訊:
CPU~Quad core AMD Ryzen 3 2200G with Radeon Vega Graphics (-MCP-) speed/max~1438/3500 MHz Kernel~4.15.0-20-generic x86_64 Up~9 min Mem~1180.1/6980.1MB HDD~1370.3GB(59.9% used) Procs~186 Client~Shell inxi~2.3.56

2019年3月26日 星期二

c++ 善用 smart pointer 在物件建構法中傳回不同類型的物件並統一函式介面的作法

每個物件通常有不同的函式介面, 想要將兩種不同物件的方法統一成相同介面, 可以利用物件建構法將它封裝起來, 用這新類型物件去實例化想要的物件, 達成傳回多型物件(polymorphic class)的目的:
#include  <stdio.h>
struct base    { // 基礎類型

  virtual void work( ){ }//擬統一的函式介面

  virtual  ~base(){ } // 避免繼承者,當在解構式中想刪除指標物件時,編譯器會報出錯務訊息

}; // 先建構基礎物件抽象類型, 統一函式介面


struct A: base { // 繼承 base 物件, 將物件用物件 A 封裝起來
    void work( ) override { printf("A\n");    } // 將方法封裝並實現函式介面
};


struct B: base {// 繼承 base 物件, 將另一物件用物件 B 封裝起來
    void work( ) override { printf("B\n");    } // 同樣將方法封裝並實現函式
};


struct C {     // 一個全新多型類型(polymorphic class)
    base *ptr;// 基礎物件的指標, 可以封裝物件 A 或 B
    ~C( )      { delete ptr; }
    C(int a) {  // 根據輸入值實例化不同物件, 奇數傳回 A, 偶數傳回 B, 等同於傳回不同物件
        if (a%2) ptr = (base *) new A();  // 實例化物件 A 指標
        else       ptr = (base *) new B();  // 實例化物件 B 指標
    }
    void work( ){ ptr->work( ); }// 將多型物件成員用指標連結函式介面,方便使用
};
int main(){
   for(int i=0; i < 10; i++)         C(i).work( );
}
如果害怕指標的用法(像是造成 memory leak or double free), 可以用 smart pointer 將它封裝起來, 讓系統自動處理, 就不用擔心記憶體釋放的問題. c++ 所提供的 std::shared_ptr < > 就是 smart pointer 其中之一用來封裝類型指標的標準樣板函式(STL), 但有一點要注意的是, 用 smart pointer 封裝類型時, 該類型的建構及解構方法必須放在 public: 區, 讓 STL 可以正常呼叫, 否則會出現一堆錯誤訊息. 又或者很不喜歡用指標箭頭的方法, 還可以用左值(&alias) 換化成正常物件, 將方法從指標箭頭轉成正常物件的使用方式.
#include  <stdio.h>
#include  <memory> // smart pointer 要使用 memory 函式庫
typedef std::shared_ptr <base> baseptr; // 封裝成 smart pointer, 讓系統自動管理
struct base    {  // 基礎類型

   virtual void work( ){}
   virtual  ~base(){ } // 避免繼承者若在解構式中想刪除指標物件時會報出錯務訊息

}; // 先建構基礎物件的抽象類型, 統一函式介面


struct A: base { // 繼承 base 類型, 可以將物件用類型 A 封裝起來
    void work( ) override { printf("A\n");  } // 將方法封裝並覆蓋虛擬函式
};


struct B: base {// 繼承 base 類型 , 將另一物件用類型 B 封裝起來
    void work( ) override { printf("B\n");    } // 同樣將方法封裝並覆蓋虛擬函式
};


struct C {  // 一個全新多型類型(polymorphic class)
    baseptr ptr;// 宣告 ptr 成為 smart pointer 可以封裝物件 A 或 B 類型的指標    

    // 無需解構式  ~C( ){ }
    C(int a) {  // 根據輸入值實例化不同物件, 奇數傳回 A, 偶數傳回 B, 等同傳回不同物件
        if (a%2) ptr = baseptr(new A( ));  // 實例化物件 A 指標, 同時轉換成 smart pointer
        else        ptr = baseptr(new B( ));  // 實例化物件 B 指標, 同時轉換成 smart pointer
    }

};


int main(){ // 呼叫時, 先用物件直取多型成員, 接著就能用指標 -> 呼叫函式介面
   for(int i=0; i < 10; i++)  C(i).ptr->work( );
}

2019年3月24日 星期日

linux mint 升級 openssl

Openssl:
1. 至官網下載原始碼 https://github.com/openssl/openssl
2. 解壓縮, 並進入目錄
3. 執行 ./config
4. 執行 make
5. 執行 sudo make install
預設程式庫會被安裝至 /usr/local/lib, 導致 openssl 找不到 libssl.so.3 及 libcrypto.so.3 等動態程式庫, 無法執行, 只要將符號連結過去就可以解決問題:
    sudo ln -sf /usr/local/lib/libcrypto.so.3  /usr/lib
    sudo ln -sf /usr/local/lib/libssl.so.3         /usr/lib

Libressl:
1. 這是另一個源出於 Oenssl 的分支, 程式碼是相容的, 號稱刪除一些不必要的程式碼, 讓它更精減, 目標也要讓程式更安全可靠,  編譯的方式大同小異, 參考文章: http://linuxg.net/how-to-install-libressl-2-1-6-on-linux-systems , 可到官網下載源碼:  https://ftp.openbsd.org/pub/OpenBSD/LibreSSL/libressl-2.9.0.tar.gz
2. 解壓縮, 並進入目錄
3. 執行 ./configure
4. 執行 make
5. 執行 sudo make install
6. 執行 sudo ldconfig
預設程式庫也是安裝至 /usr/local/lib, 如果導致 openssl 找不到 libssl.so.45 及 libcrypto.so.45 等動態程式庫, 只要將符號連結過去就可以解決問題:
    sudo ln -sf /usr/local/lib/libcrypto.so.45.0.1  /usr/lib/libcrypto.so.45
    sudo ln -sf /usr/local/lib/libssl.so.47.0.1         /usr/lib/libssl.so.45

編譯程式時要加上選項參數  -L/usr/local/lib -lssl -lcrypto, 例如:
g++   https.c -L/usr/local/lib -lssl -lcrypto  -pthread -o https
或是編寫一個簡單的 Makefile
#Makefile to link with ssl, crypto, pthead library
https:
    g++  https.c -L/usr/local/lib -lssl -lcrypto -pthread -o$@
clean:
    rm  -f  https
之後只要下一個命令, 不用再打那麼多字
make

Boringssl:
1. 同樣源自於 Openssl 的另一分支 , 由 google 負責維護的 tls/ssl 程式庫, 首先要安裝 cmake 及 golang 兩個必要開發工具:
     sudo apt-get install cmake golang
2. 到官網用 git 下載源碼, 複製整個 boringssl 目錄
     cd $HOME/Downloads
     git clone https://boringssl.googlesource.com/boringssl
3. 進入目錄, 建個子目錄 build, 進入子目錄 build,  cmake .. ,  make
     cd $HOME/Downloads/boringss
     mkdir build
     cd build
     cmake ..
     make
4. 例如要編譯 c 聯結 Boringssl 程式庫, 可以進到工作目錄, 把編譯好的程式庫複製過來作聯結:
     cd $HOME/work/c/https
     cp $HOME/Downloads/boringssl/build/ssl/libssl.a   .
     cp $HOME/Downloads/boringssl/build/crypto/libcrypto.a   .
     g++   https.c    -L.   -lssl   -lcrypto   -pthread
   

c++ 寫一個簡單的 https server

參考文章: https://wiki.openssl.org/index.php/Simple_TLS_Server
先安裝好 openssl 開發檔及準備自我認証的憑證
sudo apt-get install libssl–dev
openssl req -x509 -nodes -days 30 -newkey rsa:2048 -keyout mykey.pem -out mycert.pem
編輯 https  server 主程式:
// server.c
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>
#include <openssl/ssl.h>
int main()

    SSL_library_init(); 
    SSL_CTX *ctx = SSL_CTX_new(SSLv23_server_method());
    SSL_CTX_set_ecdh_auto(ctx, 1);
    SSL_CTX_use_certificate_file(ctx, "mycert.pem", SSL_FILETYPE_PEM);
    SSL_CTX_use_PrivateKey_file(ctx,  "mykey.pem", SSL_FILETYPE_PEM);
    struct sockaddr_in addr;
    socklen_t len = sizeof(addr);
    int server = socket(AF_INET, SOCK_STREAM, 0);
    bzero(&addr, sizeof(addr));
    addr.sin_family = AF_INET;
    addr.sin_port = htons(5000);
    addr.sin_addr.s_addr = INADDR_ANY;
    int  ok = 1;
    setsockopt(server, SOL_SOCKET, SO_REUSEADDR, &ok, sizeof(ok));
    bind(server, (struct sockaddr*)&addr, sizeof(addr));
    listen(server, 10);  
    while(1) {
        int client = accept(server, (struct sockaddr*)&addr, &len);
        printf("Accept client IP= %s : port=%d\n",inet_ntoa(addr.sin_addr), ntohs(addr.sin_port));
        SSL * ssl = SSL_new(ctx);
        SSL_set_fd(ssl, client);
        SSL_accept(ssl);
        char buf[1024];
        int bytes = SSL_read(ssl, buf, sizeof(buf));
        if (bytes > 0) { buf[bytes]=0; printf("Client: %s", buf); }
        char msg[] = "<html><head><meta charset='UTF-8'></head><Body>歡迎光臨</Body></html >";
        buf[0]=0;
        sprintf(buf            ,"HTTP/1.0 200 OK\n");
        sprintf(buf+strlen(buf),"Server: Welcome\n");
        sprintf(buf+strlen(buf),"Content-Type: text/html\n");
        sprintf(buf+strlen(buf),"Content-Length: %11ld\n",strlen(msg));
        sprintf(buf+strlen(buf),"Connection: close\n\n");  
        sprintf(buf+strlen(buf),"%s",msg);
        SSL_write(ssl, buf, strlen(buf));
        SSL_free(ssl);
        close(client);  
    };
    close(server);
    SSL_CTX_free(ctx);
}
編譯並執行
g++ server.c -lssl -lcrypto  -o server &&  ./server
用瀏覽器瀏覽  https://127.0.0.1:5000 , 如有警告信息, 選取並信任該憑證

c++ 寫一個簡單的 https client

寫 https 的程式基本上就是將 http 的封包透過 TLS(Transport Layer Security:傳輸層安全協議)/SSL 加密後再傳出去, 收到的封包也是先經過 TLS/SSL 解密後還原.參考文章: https://wiki.openssl.org/index.php/SSL/TLS_Client
設定完 https server(以下使用 local host: 127.0.0.1 TCP port: 5000). 安裝 SSL 開發檔:
sudo apt-get install libssl–dev
編輯一個簡單的 https client 文字檔:
// client.c
#include <stdio.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <string.h>
#include <openssl/ssl.h>
struct sessionTLS {
    int sid;
    SSL_CTX *ctx;
    SSL *ssl;
};
void freeTLS(sessionTLS &tls) {
    if (tls.ssl != NULL) SSL_free(tls.ssl);   
    if (tls.sid > 0)     close(tls.sid);
    if (tls.ctx != NULL) SSL_CTX_free(tls.ctx); 

}
void bindSSL(sessionTLS &tls) {
    tls.ssl = SSL_new(tls.ctx);
    if(tls.ssl != NULL ) {
        SSL_set_fd(tls.ssl, tls.sid);
        SSL_connect(tls.ssl);
        printf("\nsession TLS encryption: %s\n", SSL_get_cipher(tls.ssl));
        X509 * CA = SSL_get_peer_certificate(tls.ssl);

        if ( CA != NULL )  {
            char * subject= X509_NAME_oneline(X509_get_subject_name(CA), 0, 0);
            char * issuer = X509_NAME_oneline(X509_get_issuer_name(CA) , 0, 0);
            printf("CA: %s , %s\n", subject, issuer);
            free(subject);
            free(issuer);
            X509_free(CA);
        }
    }
}
sessionTLS connectIP(const char *ipaddress, int tcpPort){
    SSL_library_init();
    sessionTLS tls;// create a new TLS object
    tls.ctx = SSL_CTX_new(SSLv23_method());
    tls.sid = socket(AF_INET, SOCK_STREAM, 0);
    sockaddr_in host;
    host.sin_family = AF_INET;
    host.sin_port     = htons(tcpPort);
    inet_pton(AF_INET, ipaddress, &host.sin_addr) ;
    connect(tls.sid, (sockaddr*)&host, sizeof(host));
    bindSSL(tls);// bind tls with SSL
    return  tls;
}
int writeTLS(sessionTLS &tls, char *msg, int len) { return SSL_write(tls.ssl, msg, len); }
int  readTLS(sessionTLS &tls, char *buf, int len) { return SSL_read(tls.ssl, buf, len); }
int main() { 
    char msg[]="Hello\n";
    char buf[1024];
    sessionTLS tls = connectIP("127.0.0.1", 5000);
    writeTLS(tls, msg, strlen(msg));
    int bytes = readTLS(tls, buf, sizeof(buf));

    char *ptr = buf;
    while (bytes-- >0) printf("%c", *ptr ++);
    printf("\n");
    freeTLS(tls);
}

用 g++ 編譯並執行:
g++  client.c -lssl -lcrypto && ./a.out

2019年3月23日 星期六

用 Javascript 寫一個任意位元的亂數產生器


function random(maxnum) {
     // 將產生後的亂數用時間函數運算過, 傳回整數並把它限制在 {0, ..., 最大值-1} 之間:
     let srand = Math.floor(Math.random( )*maxnum) ^ new Date().getMilliseconds();
     return  srand % maxnum ;
}
function srandBits(bits) {
       // 產生任意位元數的亂數
        let r = bits % 8; // 以 8 位元為單位(次), 產生一個亂數
        let num = [ ];     // 初始化儲存陣列
        if (r > 0) {
               num.push(random(1 << r)); // 先產生餘數位元的亂數
               bits -= r;
        }
        while (bits > 0) { // 還剩下的位元數, 一次產生一個位元組(8 bits = 1 byte)的亂數
            num.push(random(256)); // 產生 8 位元的亂數, 0 <= 亂數 < 256
            bits -= 8; // 減掉每次產生的 8 位元
        }
        return num; // 傳回陣列
}
console.log("random:" + srandBits(32));

2019年3月19日 星期二

ECDSA 數位簽章

參考文章: https://en.wikipedia.org/wiki/Elliptic_Curve_Digital_Signature_Algorithm
橢圓曲線數位簽章演算法:
假設  ECC(x,y) 是橢圓曲線方程式, 座標產生器 G 將大整數映設到 ECC 曲線上,  使用質數 p 取餘數(%),   1< n < p-1,  假設 order number = n, nG = O
1.  模除數改用橢圓曲線的 order  number  計算
2. 把私章 d, 1 < d < n -1 映射到橢圓曲線的座標 Q = dG = d*G(1) = G(d) ,  Q 就是簽章者的公鑰
3. 用 sha256 擷取文件特徵值 z, z < p,並選一亂數 k, 映設到橢圓曲線 (x, y) = kG = G(k) , 1 < k < n -1
4. 只取出 G(k)的 x 座標,  r = x, 計算出 k 的模倒數 k-1  = 1/k,   (k * k-1) % n = 1
5. 計算 s = (z + rd) / k  %  n =  (z + rd)*k-1  % n,    1 < s < n - 1
6. (z, r,  s , Q)就是帶數位簽章的文件
驗證方式:
1. 數改用橢圓曲線的 order  number  計算,
2. 計算 s 的模倒數 s-1 = 1/s,  (s * s-1) % n  = 1
3. 計算  u = z*s-1  mod n
4. 計算  v  = r*s-1  mod n
5. 用 ECC 加法運算, 計算出座標 K = uG + vQ =G(z*s-1) + G(rd*s-1) = G( (z+rd)*s-1)=G(k)
6. 取出 K 的 x 座標, 應該要等於 r

2019年3月10日 星期日

WebSocket 一些重要觀念

WebSocket  定義在 RFC6455 文件當中, 有兩個重要的章節:
1. 交握協定: https://tools.ietf.org/html/rfc6455#section-1.3
WebSocket 使用的 tcp port 跟 http 相同, 當使用者用 http GET 方法時, 其中攜帶的標頭(header)就包含升級(Upgrade)至 websocket 的動作, 標籤(Sec-WebSocket-Key)後面附帶一串 base64 明文,每次開啟交握協定時都會不同,以一組 base64 明碼為例:
         dGhlIHNhbXBsZSBub25jZQ==
如果將它解開,其實裏面是一組 16 位元組的密碼.假設客戶端所發送封包內容是:
        GET /xxx HTTP/1.1
        Upgrade: websocket
        Connection: Upgrade
        Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
 http 伺服器端收到封包偵測到 Sec-WebSocket-Key, 就將明碼串接一個共通識別碼(GUID)形成長字串:
        dGhlIHNhbXBsZSBub25jZQ==258EAFA5-E914-47DA-95CA-C5AB0DC85B11
用雜湊函數 sha1 計算並轉成 base64 明文簡化成:
        s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
伺服器端產生一個封包把它放在 Sec-WebSocket-Accept: 標籤的後面, 回應用戶端切換通信協定,完成交握程序
        HTTP/1.1 101 Switching Protocols
        Upgrade: websocket
        Connection: Upgrade
        Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
2. 封包格式: https://tools.ietf.org/html/rfc6455#section-5.1
一旦交握完成, 雙方封包內就不再用 http 的標頭(header), 而是轉成 WebSocket 格式的標頭, WebSocket 客戶端送的資料必須使用 xor 加密, 所附帶 4 位元組(byte)遮蔽碼負責把資料每 4 個位元組循環作加密運算(xor)轉成亂碼,伺服器端則利用這 4 個遮蔽碼同樣每 4 個位元組循環作解密運算(xor)轉成明文.
標頭第 1 個位元組分上半部與下半部:
        上半部最高位元(MSB) 標示該封包是否是最後一筆封包, 後 3  個位元保留未使用
        下半部 4 個位元代表該封包的格式(opcode),分別如下:
            0000  表示是上一封包的延續
            0001 表示文字格式(ascII code)
            0010 表示16進制(binary code)
            1000 表示關閉 Websocket 通道
            1001 表示 ping 封包
            1010 表示 pong 封包
            其他保留未使用
        用 c++ 語言描述:
        bool  fin   =   p[0] & 0x80;
        auto opcode = p[0] & 0xf ;
        if( fin ) printf("final frame\n");
        switch(opcode){
          case 0 : printf("continue frame\n") ;break;
          case 1 : printf("text frame\n")      ;break;
          case 2 : printf("binary frame\n")  ;break;
          case 8 : printf("close frame\n")    ;throw "WebSocket: Connection close!";
          case 9 : printf("ping frame\n")     ;break;
          case 10: printf("pong frame\n")  ;break;
          default: printf("reserved frame\n") ;break;
        } 
標頭第 2 個位元組指示隨後的資料, 最高位元(MSB)代表是否資料經過 xor 運算, 剩下7個位元是資料長度數值, 當數值小於 126, 就當成是資料長度(payload length), 如果等於 126, 那要用隨後的另兩個位元組當資料長度, 但如果大於 126(也就是等於 127)時, 就用隨後 8 個位元組當資料長度.客戶端送來的資料必須用 xor 運算過, 因此資料長度後面,必定先緊隨 4 個位元組的遮蔽碼(mask key), 其後才是資料(payload). 而伺服器傳給客戶端並不需要加密, 也就無需遮蔽碼, 因此長度後面緊跟的就是資料(payload)了, 簡單用 c++ 語言來解碼:             
        if (p[1] & 0x80) { // maskbit should be true
           p[1] &= 0x7f; // data in buffer will be de-mask
           auto wslen = p[1];// length value copy
           p += 2; // don't need the first 2 bytes in header
           long long payloadlen;// 8 bytes integer to get payload length
           if (wslen < 126 )  payloadlen = wslen;
           else { // 126: following 2bytes, 127: following 8 bytes
               payloadlen = *p ++;
               payloadlen = (payloadlen << 8) + *p ++;     
               if (wslen>126)  for (auto i=0; i < 6; i++) payloadlen = (payloadlen << 8) + *p ++;
           }
           unsigned char maskkey[4]; // to store maskkey
           for (auto i=0; i < 4; i++ ) maskkey = p[i]; // maskkey copy
           for (auto i=0; i < payloadlen; i++) p[i] = p[i+4] ^ maskkey[i%4]; // decode & move
           size -= 4; // size of buffer was reduced by 4 bytes of maskkey
           auto &payload = p; // payload now point to data
        }
3. 客戶端可以從 http 伺服器端接收一個 index.html 檔案, 編寫的內容像是:
<html><head><title>WebSocket 發送與接收示範</title><meta charset="UTF-8"></head>
    <script>
     const ws = new WebSocket('ws://127.0.0.1:5000');// port 必須與 http 相同
      ws.addEventListener('open', () => {
           msg = "Hello, thhis is a test message.";
           console.log("Send:  " + msg);
           ws.send(msg);
       });
       ws.addEventListener('message', event => {
           console.log("Get::  " + event.data);
           ws.close();
       });
    </script>
    <body>打開瀏覽器的 Console 視窗觀看結果</body>
</html>
內嵌的 Javascript 是一個用來開啟 WebSocket 通道的程式碼, 假設 http 伺服器使用 tcp port = 5000, 將上述的 index.html 放在伺服器的 www 根目錄底下, 用瀏覽器開啟 URL 的頁面 http://127.0.0.1:5000/index.html