2023/12/25

Daemon Thread in java

Java 的 thread 有分為 User 與 Daemon Thread 兩種。

User Thread 的執行優先順序比較高,JVM會等所有的 User Thread 都結束工作後,才會完全地停止工作。

Daemon Thead 的 priority 比較低,通常是用來提供 service 給 user thread 使用。如果有資料 IO 的工作,不建議在 daemon thead 裡面實作。因為JVM 不需要等待 daemon thread,所以可以隨時中止 daemon thread,通常在 daemon thread 會用無窮迴圈實作,如果有 finally 部分的程式,在 daemon thread 並不會保證一定會被執行。

Daemon thead 可用在 garbage collection,釋放記憶體,釋放 cache 的工作。

Daemon thead 要在產生物件後,start 前,用 setDaemon 設定為 daemon thread

NewThread daemonThread = new NewThread();
daemonThread.setDaemon(true);
daemonThread.start();

// 用 isDaemon() 檢查是否為 Daemon Thread
daemonThread.isDaemon();

References

Daemon Threads in Java | Baeldung

JAVA并发编程——守护线程(Daemon Thread) - Luochengor - 博客园

2023/12/18

Fluent API

fluent API 是一種 OO API design 方式,可讓 API 的使用者,透過 chain method 的方式持續呼叫該物件的 method。在實作這種 API 介面時,method 必須要在最後回傳物件本身的 reference,以最常見的 set methods 來說,以往都是回傳 void,但要改為回傳 this。

在實作支援 Fluent API 這樣的類別時,要注意物件的 immutability 特性,如果該類別是修改物件內部資料的狀態,那麼就直接回傳 this 就好了,但也有可能是持續產生相同類別的新物件。

Difference Between Fluent Interface and Builder Pattern in Java | Baeldung 這個網頁提出了兩種不同的例子

以下是 User 的 Builder,可透過 set method 不斷地修改使用者的各個欄位資料,一直到最後,確認要產生 User 時,就呼叫 build

public static class Builder {
    private String firstName;
    private String lastName;
    private String email;
    private String username;
    private Long id;

    public Builder firstName(String firstName) {
        this.firstName = firstName;
        return this;
    }

    public Builder lastName(String lastName) {
        this.lastName = lastName;
        return this;
    }

    // other methods

    public User build() {
         return new User(firstName, lastName, email, username, id);
    }
}

在使用時

Builder userBuilder = new Builder();
User user = userBuilder
            .firstName("John")
            .lastName("Will")
            .build();

另一個要持續產生新的物件的例子是 Html,因為 html 裡面的 String content 是不能直接修改的,故必須要在 method 最後面都產生一個新的物件

public class HtmlDocument {
    private final String content;

    public HtmlDocument() {
        this("");
    }

    public HtmlDocument(String html) {
        this.content = html;
    }

    public String html() {
        return format("<html>%s</html>", content);
    }

    public HtmlDocument header(String header) {
        return new HtmlDocument(format("%s <h1>%s</h1>", content, header));
    }

    public HtmlDocument paragraph(String paragraph) {
        return new HtmlDocument(format("%s <p>%s</p>", content, paragraph));
    }
}

使用時

HtmlDocument document = new HtmlDocument()
  .header("header")
  .paragraph("paragraph 1")
  .paragraph("paragraph 2");
String html = document.html();

References

Fluent API: Practice and Theory | SIGPLAN Blog

Fluent programming style

Fluent API — 流畅API(基于Java介绍) | 桃子爱吃桃子

2023/12/04

TSN (Time-Sensitive Networking)

時效性網路(Time-sensitive Networking, TSN)是電機電子工程師協會(IEEE)定義的專用於使乙太網路更具確定性的一種網路拓展。TSN 是區域網路(LAN)解決方案,只有在TSN LAN內部才能保證其即時性。

為解決乙太網路低延遲與時間同步的問題,Industrial Ethernet 的 Protocol

  • EtherCAT
  • EtherNet/IP
  • PROFINET
  • Powerlink
  • Modbus-TCP
  • SERCOS Ⅲ

在應用上來說Industrial Ethernet是與Standard Ethernet不相容的。TSN 解決了這些問題

  1.  TSN相容於Standards Ethernet IEEE802.1規範,可以與非TSN乙太網路一起使用的區域網路,TSN支援更高頻寬的傳輸速度 (Gbit/s以上)
  2.  TSN工作在Layer 2 technology of OSI model
  3.  在Standards Ethernet的區域網路中可以做到:
     高頻寬、低延遲、保障頻寬(Priority)、時間同步等功能
  4. 透過IEEE 802.1AS (協議簡稱精確時鐘協議Precision Timing Protocol - PTP) 實現TSN裝置之間共享時間戳記 (Time Stamping) 的設備。

TSN 並未取代 Layer 2 以上層級的協定,也未定義軟體介面或硬體配置與特點,因此可相容於多種應用程式開發介面 (API)

TSN主要功能是時間同步 (Time Sync)、優先權 (Priority)、可靠性 (Reliability)、資源管理 (Resource Management)。時間同步 (Time Sync) 是透過802.1AS標準,在傳送跟接收的封包上加上時間戳記 (Time Stamping),在區域網路之中可以將設備之間的訊號同步在微秒 (us) 範圍

優先權 (Priority) 是透過802.1Qbu & 802.1Qbv標準,允許將正在傳輸的資料中斷讓優先等級較高的資料進行傳送,等優先等級較高的資料傳送完成後再回到先前被中斷的資料繼續傳輸,確保優先等級較高的資料有最大的傳輸頻寬跟最低的傳輸延遲時間。

可靠性 (Reliability) 是透過802.1CB標準,將原本要傳送的封包複製成多個不同封包,每一個不同的封包會透過不同的路徑來做傳送,最後在接收端會自動消除其它的冗餘 (Redundancy) 封包,使其接收端只會收到一筆封包資料,即使在傳輸路經之中出現了單點的故障情況 (如設備損壞或是電纜線斷開等),都可以確保目的端可以接收到正確且完整的資料。

資源管理(Resource Management) 是透過802.1Qcc標準,將TSN配置分成三種模式:

  1. 完全分散模式(Fully Distributed Model)
  2. 完全集中模式(Fully Centralized Model)
  3. 集中&分散混合模式(Centralized & Distributed Model)

一些關鍵的 IEEE 802.1 TSN 子標準包括:

  • IEEE 802.1 AS – 時序與同步
  • IEEE 802.1Qbv – 時間感知塑形器
  • IEEE 802.3Qbr – 散佈快速流量
  • IEEE 802.1Qbu – 訊框搶佔
  • IEEE 802.1Qca – 路徑控制與保留
  • IEEE 802.1CB – 備援
  • IEEE 802.1 Qcc – 串流保留的增強與改善
  • IEEE 802.1 Qch – 迴圈佇列與轉送
  • IEEE 802.1Qci – 逐一串流過濾與監管
  • IEEE 802.1CM – 前傳網路的時效性網路

TSN的應用

影音設備

電影院、音樂廳控制系統,聲音從表演者經過傳輸到聽眾接收,中間如果有延遲,是很難被接受的

汽車控制

汽車控制系統裡面有四個主流的匯流排構成 LIN、CAN、FlexRay & MOST,不同系統採用不同的匯流排且涉及到相當複雜的佈線,汽車上的設計以及感知器會日趨複雜,連接的設備也愈來愈多,TSN的技術可以區分時間敏感度以及優先層級的資料,降低延遲以及時間同步等優點,系統就能輕易且正確傳送從感測器到到影音串流各種不同類型的資料,進而提供可靠性以及可預測性的高速網路系統。

工業應用

感測器連接的節點愈多,想要每個感測器跟控制器的無縫連接就愈困難,工業自動化中,精準的時間是關鍵要素,傳統的乙太網路並無法保證網路延遲範圍,透過TSN的網路環境,在傳統乙太網路上加入了即時性的控管,對網路流量進行優先排序,提供保證延遲範圍,以便對時間較敏感的資料可以在正確的時間傳到正確的目標端。

References

一讀就懂的TSN (Time-Sensitive Networking) 應用與架構 | Macnica Galaxy

時間敏感網路(TSN)中央控制器簡介 - 科技新知 - 產業學習網

## TSN 讓工業物聯網和工業 4.0 發揮更大效用 — 您不可不知的 5 件事

TSN技術說明與Avnu認證流程 | 百佳泰 Allion Labs

# 如何實作時效性網路以確保確定性通訊

保障時遲性/高傳輸速率 時效性網路掀工業自動化革命 | 新通訊

2023/11/27

Doppler Radar

都卜勒效應 波源和觀察者有相對運動時,觀察者接受到波的頻率與波源發出的頻率並不相同的現象。這一現象最初由奧地利物理學家都卜勒於1842年發現。

物體的相對運動會引起頻率的增大或減小。當物體和波源相背離時時,波長會增大,頻率會降低,稱為都卜勒紅移,當物體和波源相向運動時,波長減小,頻率增大,稱為都卜勒藍移,根據探測到的都卜勒頻移 f′ 可以計算出物體的速度。

在馬路上,最常遇到的是消防車與救護車的警報,我們通常會發現,當車子往我們的方向靠近跟遠離時,我們聽到的警報聲音是不一樣的,而且一直在改變。

都卜勒雷達 Doppler radar 是利用都卜勒效應測量物體在雷達波束方向上的路徑運動速度的一種雷達。常用於氣象觀測。

為了檢查心臟、血管的運動狀態,了解血液流動速度,可以通過發射超聲來實現。超聲振盪器產生一種高頻的等幅超聲信號,激勵發射換能器探頭,產生連續不斷的超音波,向人體心血管器官發射,當超音波束遇到運動的臟器和血管時,便產生都卜勒效應,就可以根據反射波與發射的頻率差異求出血流速度,根據反射波以頻率是增大還是減小判定血流方向。

交通警察取締超速行車所使用的雷達槍也是都卜勒雷達的一種。交通警察向行進中的車輛發射頻率已知的超音波同時測量反射波的頻率,根據反射波的頻率變化的多少就能知道車輛的速度。裝有都卜勒測速儀的監視器有時就裝在路的上方,在測速的同時把車輛牌號拍攝下來,並把測得的速度自動列印在照片上。

References

都卜勒雷達 - 維基百科,自由的百科全書

杜卜勒超音波(Doppler ultrasound) - 小小整理網站 Smallcollation

多普勒效應 Doppler Effect

2023/11/20

抽象洩漏定律 (The Law of Leaky Abstractions)

Joel Spolsky 於 2002 年提出 Leaky Abstractions 抽象洩漏定律

All non-trivial abstractions, to some degree, are leaky. 所有難以理解複雜的抽象機制,在某種程度上,都是有漏洞的。

因為軟體的開發與運作環境複雜,開發人員不可能自造所有的輪子,而必須依靠各種抽象化的機制(大部分是 API 函式庫)進行開發,在隱藏細節的環境下,進行開發。經過一個開發人員的實作與開發後,某個程度下,又多了一層抽象封裝。但這些抽象封裝機制,不可避免都會洩漏出底層的一些問題,洩漏出無法封裝的問題。

在使用者使用抽象化後的介面後,在遇到不可預期的問題時,就必須要去了解底層的細節,才能知道發生的原因,也才能解決問題與除錯。雖然抽象封裝節省了開發的時間,但踩雷與除錯所耗費的時間也不少。因此我們才會在很多 QA 網站中,找到一些其他人的踩雷經驗與技巧。有經驗的開發人員,也會因為這些經驗的累積,避開可能會遇到的問題。

以下是一些抽象洩漏的例子

  • TCP 是現今網路的基礎,大部分的網路溝通,都需要利用 TCP 的可靠傳輸協定傳送資料,但不可避免的是 TCP 的流量控制機制本身就是有缺陷的協定,網路無法在一個穩定的流量通道上進行傳送,延遲跟 throughput 的波動對於 TCP 來說,都是正常的現象。但一般來說,沒有經驗的開發人員是無法預知到這些問題。

  • SQL 查詢語言是關聯式資料庫的查詢語法,但某些 SQL 查詢語法卻是有性能差異的,例如 select * from table 就會比 select column1, column2 from table 速度來得慢。另外因為 AP Server 跟 DB 是分屬不同機器的狀況下,查詢時把整個資料表的所有欄位都取出來,也會造成網路頻寬的耗費而影像整體效能。

當我們遇到了一項新技術,宣稱因為良好的封裝,可以加速開發時。這時候最好是停下來想想看,這樣的封裝是不是真的有帶來實際的效益,還是會因為採用了這樣的抽象化封裝,而帶來一些無法預期的問題。

我們曾經使用過可以在 ios 與 android 同時運作的開發工具,但最終因為封裝後的函式庫本身的限制,無法微調,且函式庫無法跟隨作業系統的更新就馬上更新,最終只能放棄而採用原生的方式開發。但這不代表這種工具是不好的,對於畫面簡單的應用程式來說,使用者種方式開發,確實會帶來一些好處,但要有心理準備,可能會遇到一些根本且無法解決的問題。

References

The Law of Leaky Abstractions – Joel on Software

抽象泄漏_百度百科

為什麼任何系統都會存在Bug?什麼是抽象漏洞定律? - 每日頭條

抽象漏洞定律The Law of Leaky Abstractions

抽象泄漏定律 | 张吉的博客

抽象泄漏 - Wikiwand

2023/11/13

複雜性守恆定律 Tesler's Law

Larry Tesler 於 1984 年提出複雜性守恆定律 Tesler's Law。每個程式都有其內在無法簡化的複雜程度,無法被刪除或是隱藏,到了臨界點,就無法再被簡化,因此,必須在人機介面的設計中,不斷地調整與平衡,適當地將使用介面跟產品內部的複雜度調整與轉移。

例如傳統的電視機,畫面裡面的顯示設定很簡單,複雜的是在電視遙控器,遙控器上有數十個按鈕可以進行設定。新的智慧電視,遙控器的介面非常簡潔,但畫面內的功能與設定卻非常多。使用者跟電視之間的交互作用關係的整體複雜度不變,但透過介面設計的不同,轉移了複雜度的比重關係。

Macbook Pro 在外接的介面孔,逐漸地簡化,導致使用者無法直接使用 HDMI、SD card、USB,機器因為孔位減少,外觀簡潔更好看。但使用者卻必須要透過外接的轉接線路,才能連接 USB、SD card。

Tesler's Law 說明了,系統「整體複雜度」是固定的,如果要簡化使用者操作,就會增加其他部分的複雜度,因為產品設計與開發的時間跟人力都需要耗費成本,無法追求極致的使用者體驗。複雜度在平衡與轉移的過程中,都會產生 cost,如果有低成本的轉移方式,就能持續進行轉移。

太過於精簡的介面,也會讓使用者嗤之以鼻,沒有難度的操作方式,會失去眼球焦點,適當的難度可讓使用者持續沈浸於產品的體驗中。更簡單的使用者體驗,通常代表更複雜的系統設計。需要由設計師跟開發團隊,進行複雜度的權衡。

在產品的發展初期,因為核心功能不多,通常介面也會比較簡單,但隨著時間與產品的演進,功能慢慢的擴張,涵蓋到其他的範圍,複雜度也會慢慢地提升。

References

「自然交互· 泰斯勒定律」如何平衡设计的复杂度?

複雜度守恆定律_百度百科

什么是特斯勒定律?

2023/11/06

分布式計算的謬論 (The Fallacies of Distributed Computing)

Sun MicroSystem 的幾位工程師,提出了分散式系統的八個謬論,是一般開發人員,對於分散式系統的錯誤認知假設,有了這些基本的認知錯誤,設計的系統常會發生一些不能預期的問題。

前四個是 Bill Joy 與 Dave Lyon 定義的,後來 Peter Deutsch 增加了 5,6,7 三個,最後 James Gosling 定義了第八個。

  1. 網路是可靠的

    任何透過網路的遠端系統呼叫,都有可能會發生意外而失敗。為了處理遠端系統可能的離線異常,通常會帶入 MQ 系統,將遠端呼叫的需求放入 MQ,然後自動 retry,但加入了 MQ,就會用非同步的方式,處理一開始的 Request,這會直接影響原始的系統設計。

  2. 延遲是零

    網路因為頻寬的限制,以及客戶端到伺服器端的距離,一定會發生資料傳遞的延遲。

    傳統的電話線路是獨佔式的,一條線路只能讓一通電話使用。網路電話是不同的,需要經過語音及類比、數位轉換,然後透過分享的網路線路進行封包傳送,網路電話的延遲通常會比傳統電話還要大。

  3. 頻寬是無限的

    網路封包是透過無線電或網路線傳送,但實體的傳送媒體,都會因為該傳輸媒體及資料轉換機制,而有資料傳送速度的限制。在處理大資料量的應用,例如網路電話,直播等等,更需要注意頻寬限制的問題。

  4. 網路是安全的

    網路技術會進步與更新,但網路攻擊的技術也同時在進步,網路攻擊一定存在,也不存在無法被攻擊的系統,系統只能應付攻擊,而做對應的處理。

  5. 拓撲結構不會改變

    機器與連線的配置與架構會不斷改變,當系統故障/更新時,會需要改變現有的系統架構。

  6. 有一個管理員

    任何地方都可能會出錯,當系統發生問題,沒有一個管理員可以知道所有的狀況,並能了解所有的問題。

  7. 運輸成本為零

    因為需要有頻寬、伺服器、網路、Load Balancer、防火牆等等網路架構與機制,所以網路服務都是需要花錢的,沒有能夠免費提供的服務,但現在能使用免費網路服務的原因,都是因為公司用另一個方式,取得了營運資金,用來支撐免費的服務。

  8. 網路是同質的

    網路是透過各種協定交互運作組成的,如果是開放的標準通訊協定,可在不同系統上交互運作,如果是自訂的通訊規格,就可能會發生無法相容的問題。

References

驾驭分布式计算的8个谬误 - 掘金

# 圖說分佈式計算的 8 大謬誤

分布式系统中经典的八个谬误-51CTO.COM

2023/10/30

Vue 的 js 建構版本資訊

vue - Libraries - cdnjs vue 有多個版本的 js file,每一個版本有不同的用途

cjs

  • vue.cjs.js

  • vue.cjs.prod.js 有壓縮的正式版

CommonJS,是一種模組的定義,伺服器端使用,透過 require() 在 NodeJS 裡面使用。但目前 NodeJS 已宣布放棄了 CommonJS

global

  • vue.global.js    完整版,包含編譯器及 runtime

  • vue.global.prod.js 正式版

  • vue.runtime.global.js

  • vue.runtime.global.prod.js

這是在瀏覽器直接用 <script src""> 引用時使用,會得到一個 global Vue 物件,可直接使用。

完整版跟 runtime 版本的差異是,完整版包含編譯器跟 runtime。編譯器可處理將 template 編譯為 js

browser

  • vue.esm-browser.js
  • vue.esm-browser.prod.js
  • vue.runtime.esm-browser.js
  • vue.runtime.esm-browser.prod.js

透過 ES6 原生的 module 使用,可以在瀏覽器內,透過 <script type="module"> 使用

bundler

  • vue.esm-bundler.js
  • bue.runtime.esm-bundler.js

用在 webpack, rollup, parcel 等建構工具,通常預設是使用 vue.runtime.esm-bundler.js

References

Vue:浅析vue.js完整版 和 vue.runtime.js运行时版 - 掘金

大前端学习笔记--Vue.js 3.0-云社区-华为云

2023/10/23

Chicken-or-Egg Problem in Network

在網路效應中,常見到 2-sided martketplace,nfx 提出 19 個策略,用來解決先有雞還是先有蛋的問題。

  1. Get the hardest side first

    比較困難的那一邊,也代表其網路價值比較高

    ex: Ourdoorsy 是 RV 租借市場,困難點在於如何吸引 RV owners

  2. Appeal tightly to a niche and repeat

    找到市場的痛點

    ex: eBay 一開始 Beanie Babies。Craigslist 一開始是Craig的朋友想要找公寓,找工作

  3. Subsidize the most valuable side of the market

    付費補貼給該網路中最有價值的一方,請他們加入網路

    ex: Uber 付費給司機

  4. Make the supply look bigger with automation

    從 supply-side 收集資料,進而讓該活動被注意(產生光環)

    ex: Yelp 在平台收集評論資料

  5. Buid one side as an email list

    這是一個啟動 martketplace 最簡單的方法,特別是 buyer 同時也是 sellers 的狀況

    ex: Craigslist

  6. Host meetups & gatherings 辦聚會

    舉辦社交活動,可得到客戶直接的反饋

    ex: Poshmark 舉辦 "Posh Parties" 讓客人交換時尚資訊

  7. Buid a SaaS tool for one side of the market

    為某一方建立 SaaS 工具,這樣就能吸引另一方

    ex: OpenTable 為餐廳建立預訂軟體服務

  8. Give software to a thrid party who bring you one side of the market

    提供軟體給第三方

    ex: Android 為手機製造商提供軟體,手機可帶來消費者需求

  9. Find one giant user for the initial supply or demand

    一開始先找一個大客戶

    ex: Candex 提供軟體給 Siemens 使用

  10. Only make one side to change their behavior

    讓某一方改變行為

    ex: Square 改變店家行為,最終讓消費者也使用他們的信用卡

  11. Make something free suddenly

    把原本要付費的東西,突然變成免費,吸引客戶

    ex: Robinhood 免費交易。Skype 讓電話免費。Napster 讓音樂免費。

  12. Buid a product first, then open a marketplace

    先做產品,再打開市場

    ex: Salesforce 建立線上 CRM 工具

  13. Connect the two sides by hand

    先以人工(代工)方式連接兩端

    ex: Zappos 早期處理網路訂單,是以人工去買鞋,然後送貨完成

  14. Favor markets where buyers are sellers,too

    先做 buyers 也是 sellers 的市場

    ex: Poshmark 使用者買衣服,也會賣衣服

  15. Create exclusive access

    建立獨佔式存取,該想法可產生病毒式行銷

    ex: Gmail, clubhouse

  16. Set a geographic constraint

    設定地理限制,在初期先限制在某些地方發布服務

    ex: Lyft, Yelp, Craigslist

  17. Set a time constraint

    設定時間限制,初期只在某些時間提供服務

    ex: Tophatter 只提供在 8-9pm 可作競標

  18. Set a demand constraint

    設定需求限制

    ex: Groupon 限制店家每天產生一個 groupon

  19. Pay users with tokens

    以代幣支付給使用者,為市場創造代幣,提供代幣給使用者

References

19 Tactics to Solve the Chicken-or-Egg Problem and Grow Your Marketplace

2023/10/16

網路效應

nfx.com 有一份 The Network Effects Bible 討論網路效應的重要性,每增加一個使用者,會讓這個產品/服務更有價值,也就是大者恆大的局面,在網路時代,所有系統發展的初期,都還是在獲取使用者的眼球的競爭下,只有大量的使用者,才算是拿到了入場門票,才會有下一個維持跟進化的問題。

分類

文中提到 16 種不同的網路效應(該文章有持續在更新),根據網路化的程度由小到大排列

  1. Physical (e.g. landline telephones)
  2. Protocol (e.g. Ethernet)
  3. Personal Utility (e.g. iMessage, WhatsApp)
  4. Personal (e.g. Facebook)
  5. Market Network (e.g. HoneyBook, AngelList)
  6. Marketplace (e.g. eBay, Craigslist)
  7. Platform (e.g. Windows, iOS, Android)
  8. Asymptotic Marketplace (e.g. Uber, Lyft)
  9. Data (e.g. Waze, Yelp!)
  10. Tech Performance (e.g. Bittorrent,Skype)
  11. Language (e.g. Google, Xerox)
  12. Belief (currencies, religions)
  13. Bandwagon (e.g. Slack, Apple)
  14. Expertise (Figma, Microsoft Excel)
  15. Tribal (Apple, Harvard, NY Yankees…)
  16. Hub-and-Spoke  (TikTok, Medium, Craigslist)

網路就是 node 與 link 的組合,網路大小以 node 的數量計算,但該數量不完全代表價值,因為該網路內的活動頻繁程度,也會影響價值。

網路密度 Density 是另一個網路的價值屬性,密度越高,代表價值越高,也就是 nodes 之間的 link 強度跟數量。

link 是有方向性的,例如 twitter 的知名人士,以單向方式跟 fans 提供資訊,單向交流,也有可能沒有方向,例如 Messenger 的交談一定是雙向的,因此沒有必要定義方向。

nodes 之間的關係有 1-1 (ex: Facebook) 與 1-to-many (ex: Youtube),1-1 的互動是雙向的,1-to-many 的互動是單向的。網路的 nodes 分佈會有 cluster 的特性,是一群一群的。

數學模型

有幾個網路效應的法則,試圖以數學模型說明網路效應的價值

  1. Sarnoff’s Law

    早期的電視廣播,其價值直接跟使用者數量成正比。該網路都是從網路中心單向發送連結到使用者

    V = N
  2. Metcalfe’s Law

    在 Internet 時代,連上網路的電腦都可以用任何方式互相傳送資訊。因此網路的價值每增加一個 node,該 node 都可以跟原本的其他 node 連結,價值應該要是所有的節點數量。

    V = N * (N-1) /2 => 約等於 N^2
  3. Reed’s Law

    David P. Reed 於 1999 年提出,增加一個 node 的網路價值,除了連結的節點數量以外,還要再乘上該 node 帶來的潛在的 cluster,該 node 還會產生更多 sub-groups/clusters

    V = 2^N

網路特徵

  1. 不規則性

    nodes 與 cluster 的分佈並不均勻,各節點之間的 link 狀況,也跟真實世界一樣,沒有固定的樣式

  2. 身份:真實、假名、匿名

    有真實身份的網路,會比假名的網路能更有效地建構網路。但在加密或間諜應用中,反而需要匿名的網路,但卻可能因為匿名,該網路關係更容易被破壞。

  3. 不對稱性

    有不同的角色組合的網路,且某一邊的使用者取得比其他的困難。這被稱為是 Demand-Side 或是 Supply-Side Marketplace,例如 Uber

  4. 同質、異質網路

    當所有 nodes 的角色都一樣時,這是同質網路,如果網路內有分不同的角色,則是異質網路,例如網拍網站,有分買家及賣家兩種角色。

  5. 漸近 Asymptotic

    通常使用者增加,每個使用者產生的價值也在增加,但當網路成長到某個程度後,網路效應的增加就開始減弱。ex: Uber 乘客的等待時間,隨著司機數量增加到某個程度後,等待時間就無法再一直縮短了

  6. 同邊 Same-Side

    同樣使用 Windows Office 的使用者,因為檔案相容性的關係,會因為使用者增加而讓該檔案更加流通

  7. 橫向 Cross-Side

    例如 Uber,driver 增加會增加客人,客人增加也會再吸引更多 driver

  8. Indirect

    例如 ebay,新賣家的加入,只會讓賣家之間的競爭更激烈,但由於賣家增加,更吸引買家加入,整體來說,還是會讓所有賣家間接受惠。

  9. Negative

    網路的負面效應有兩種:Congestion (增加使用量) 及 Pollution (增加規模)

    因為更多人使用網路,會讓網路壅塞,速度變慢。

    FB, Twitter 會因為更多人使用,而污染自己個人的 News Feed

建構與維護

  1. Multiplayer vs. Single-Player Mode

    單人模式只會線性增長,多人的聯網產品同時具有單人與多人的價值

  2. 轉換成本 switching cost

    切換到不相容的產品,會花時間、精神與金錢,這會讓使用者傾向於使用同一個廠商的商品,並待在同一個生態體系下

  3. Chicken or Egg Problem (Cold Start Problem)

    這是 2-sided martketplace 常見的問題,buyers/sellers 或是 developers/users

    當有一邊的使用者增加,才會讓另一邊也加入。有 19 Tactics to Solve the Chicken-or-Egg Problem and Grow Your Marketplace 能解決此問題

  4. Multi-Tenanting

    同時加入多個互相競爭的網路 ex: Lyft, Uber,同時加入多個社交平台

    越大的網路會越有知名度,越能吸引新客戶

  5. Disintermediation 去中心化

    在 marketplace 或 merket network 產品中很常見。一開始透過該網路進行交易,後續就改為不透過網路平台而直接交易。

  6. Retention 留存率

    客戶回頭再使用產品的頻率。留存率高的網路效應才高。

結語

經過這些年的社交網路更迭,有很明顯的世代交替狀況,使用者的增加,確實放大了網路的價值,也許大部分的人會因為要跟其他人溝通與交流,而加入某些網路 ,雖然人數增加了,卻也凸顯了沒有一個系統能夠適用於所有人的情況,系統可以瞬間爆紅,也可能會一下子就因為技術替換與世代交替消失。

References

# 网络效应“圣经”(上):网络效应为什么重要?其运作原理是什么?

# 网络效应“圣经”(下):如何建立和维护网络效应?相关概念有哪些?

《NFX:网络效应圣经》笔记

The Network Effects Manual: 16 Different Network Effects (and counting)

# 网络效应是什么?网络效应定律简介

2023/10/02

erlang poolboy

poolboy: A hunky Erlang worker pool factory 是一個通用用途的 erlang pool library,主要是用在 pool 管理,對於 server 程式來說,最常見的就是連接資料庫的 connection pool。

連接資料庫最簡單的做法,是在程式中要呼叫使用資料庫時,要經過連線與認證,使用完畢後,就馬上關閉該資料庫連線,但對於 server 來說,因為大量的連線建立關閉,消耗了很多運算資源,故通常會以資料庫 connection pool 的方式來處理。

一般的 connection pool,會有初始的連線 pool 數量,就是一開始啟動時,固定會產生並維持幾個連線數量,然後在所有連線都被佔用時,有一個 overflow 的數量,設定超量使用時,一次多產生幾個連線。另外為了確保該網路連線是正常連線的狀態,DB pool 通常會定時發送一個 SQL 指令,以該指令有沒有回應來確認連線狀態。例如 MySQL 可以使用 SELECT CURRENT_TIMESTAMP 這樣的 SQL 指令。

poolboy 就是用 erlang process 來作為一個 connection 的資源,該 process 的 state 裡面,就記錄該資料庫連線,另外有個 supervisor 用來做連線的資源管理。

使用方式

poolboy 以 epgsql 為例,說明如何使用 poolboy,要實作兩個部分 worker 跟 supervisor,另外要做 pool 的設定。細節直接看 poolboy 的 github 首頁即可。

poolboy 裡面的 transaction 就是使用該 pool 的最簡單的方式,先 checkout 一個 worker,然後呼叫 worker 裡面的某一個 function,最終使用完成後,呼叫 checkin 釋放 worker。

transaction(Pool, Fun, Timeout) ->
     Worker = poolboy:checkout(Pool, true, Timeout),
     try
         Fun(Worker)
     after
         ok = poolboy:checkin(Pool, Worker)
     end.

checkout 有三種方式

% 取得可以使用的 worker process,如果沒有,預設會等待 5s
1> Worker = poolboy:checkout(PoolName).
% returns PID in under 5 seconds or full.

% 不等待,直接回傳可使用的 worker
2> Worker = poolboy:checkout(PoolName, false).
% no waiting, either you have an idle worker for me or not.

% 等待 10s
3> Worker = poolboy:checkout(PoolName, true, 10000).
% like the first one but wait 10 secs instead of 5

注意事項

poolboy 的坑(Erlang) - 开发者头条

使用 poolboy 時,要注意幾個問題,產生 worker process 時,不能失敗,如果 worker 失敗,會導致整個 gen_server crash,無法啟動 pool。

解決方法是使用 proxy,也就是在建立 worker 時,並不直接產生 db 的連線,而是直接將 worker process 建立起來,後續在使用到該 worker 時,才去檢查 worker 裡面的 conn state。

這邊的問題是在如何做連線維持,因為 worker 把連線建立延後了,這表示使用 worker 以前,不能確認該 worker 擁有的 conn 到底是不是正常的連線,連線也可能因為 idle 太久而被資料庫直接斷線。

連線維護的部分,會需要在 worker 裡面另外寫 timer,定時做 callback,然後定時發送一個 SQL 指令,以該指令有沒有回應來確認連線狀態。

References

GitHub - interline/epgsql_pool: Pooled epgsql connections using Poolboy

GitHub - hiroeorz/eredis_pool: eredis_pool is Pool of Redis clients, using eredis and poolboy.

erlang进程池-poolboy源码分析_食鱼酱的博客-CSDN博客

# erlang线程池poolboy源码阅读

Erlang poolboy notes – Triggers-World

2023/09/25

firewalld

在 rockylinux 8 以前,可以不使用firewalld,還是沿用舊的 iptables

# Stop firewalld
systemctl stop firewalld
# disable firewalld
systemctl disable firewalld
# hide firewalld
systemctl mask firewalld

安裝 iptables

dnf -y install iptables iptables-services

systemctl enable iptables
systemctl start iptables
systemctl status --no-pager iptables

在 rockylinux 9 以後,firewalld 成為預設的 firewall

  • firewalld 支援 network/firewall zones,定義 trust level of network connections

  • 同時支援 IPv4, IPv6

  • 支援由 service/applcation 直接建立 firewall rules

如果有圖形介面,可透過 firewall-config 進行 firewall 設定

啟用 firewalld

systemctl enable --now firewalld
systemctl restart firewalld

systemctl status --no-pager firewalld

基本指令

以下是 firewalld 幾個基本常用的指令

# 檢查 firewalld 狀態
firewall-cmd --state

# 設定完成後,要重新載入設定,讓設定永久生效
firewall-cmd --reload

# 查閱設定
firewall-cmd --list-all

# 查閱詳細設定
firewall-cmd --list-rich-rules

# 將設定永久儲存
firewall-cmd --runtime-to-permanent

# 在設定過程中,直接增加rule 並永久儲存
firewall-cmd --permanent [the rest of your command]

zone

firewalld 最重要的是加入了 zone 的概念,以下是內建基本的 zones

zone 說明 example use
drop 不回應任何封包,直接拒絕所有外部連線,只允許內部往外傳送的 packets。 drop incoming connections without reply - only outgoing packets are allowed
block 以 icmp-host-prohibited, icmp6-adm-prohibited 拒絕外部連線 incoming connections are rejected with an icmp-host-prohibited message for IPv4 and icmp6-adm-prohibited for IPv6
public 允許所有外部連線 all incoming connections are allowed
external 有使用 IP 偽裝時,用在外部網路 for use on external networks with masquerading enabled
dmz 給 DMZ 的電腦使用 for computers on your demilitarized zone that are publicly-accessible with limited access to your internal network
work for computers in work areas (nope, I don't get this one either)
home for use in home areas (nope, I don't get this one either)
internal 內部網路使用 for your internal network device access
trusted 允許所有網路連線 all network connections are accepted

如果滿足以下兩個條件中某一個,該 zone 就會是在 active 狀態

  1. zone 被綁定到某一個 network interface

  2. zone 被綁定 source IPs 或 network ranges

一般使用者比較會使用 trusted, home, public 這幾個 zone

zone 的相關指令

# 查詢 default zone
firewall-cmd --get-default-zone

# 查詢 active zones
firewall-cmd --get-active-zones

# 修改 default zone
firewall-cmd --set-default-zone [your-zone]

# 將某個 zone 綁定 network interface
firewall-cmd --zone=[your-zone] --add-interface=[your-network-device]

# 修改 zone 的 network interface
firewall-cmd --zone=[your-zone] --change-interface=[your-network-device]

# 移除 network interface
firewall-cmd --zone=[your-zone] --remove-interface=[your-network-device]

# 新增 zones
firewall-cmd --new-zone=[your-new-zone]
firewall-cmd --get-zones

rule

port

# 查詢
firewall-cmd --list-ports

# add remove
firewall-cmd --zone=public --add-port=9001/tcp
firewall-cmd --zone=public --add-port=20000-20100/tcp

firewall-cmd --zone=public --remove-port=9001/tcp

service

# 查詢可使用的 services
firewall-cmd --get-services

# 查詢目前的 active services
firewall-cmd --list-services

# add/remove
firewall-cmd --zone=public --add-service=http
firewall-cmd --zone=public --remove-service=http

ip

firewall-cmd --permanent --zone=trusted --add-source=192.168.1.0/24

firewall-cmd --permanent --zone=trusted --remove-source=192.168.1.0/24

rich rule

#單一IP

firewall-cmd --add-rich-rule="rule family='ipv4' source address='192.168.1.100' reject"
#除了針對特定單一IP外,再針對特別連線port進行設定
firewall-cmd --add-rich-rule='rule family="ipv4" source address="192.168.1.100" port protocol="tcp" port="3306" accept'
#針對IP範圍
firewall-cmd --add-rich-rule="rule family='ipv4' source address='192.168.1.0/24' reject"

firewall-cmd --add-rich-rule='rule family="ipv4" source address="192.168.77.100/32" accept'

在 iptables, firewalld 允許某個 ip 使用 ssh

# iptables
iptables -A INPUT -p tcp -m tcp -s 192.168.1.122 --dport 22 -j ACCEPT

# firewalld
firewall-cmd --zone=trusted --add-source=192.168.1.122 --permanent
firewall-cmd --zone=trusted --add-service=ssh --permanent

# remove
firewall-cmd --zone=trusted --remove-source=192.168.1.122
firewall-cmd --zone=trusted --remove-service ssh

# 儲存設定
firewall-cmd --runtime-to-permanent
firewall-cmd --reload

ICMP rule

# iptables
iptables -A INPUT -p icmp -m icmp --icmp-type 8 -s 192.168.1.136 -j ACCEPT

# "public" zone 的 ICMP 預設是開啟的
# block ICMP in "public" "trusted" zone
firewall-cmd --zone=public --add-icmp-block={echo-request,echo-reply} --permanent
firewall-cmd --zone=trusted --add-icmp-block={echo-request,echo-reply} --permanent

web server ports

# iptables
iptables -A INPUT -p tcp -m tcp --dport 80 -j ACCEPT
iptables -A INPUT -p tcp -m tcp --dport 443 -j ACCEPT

# add service
firewall-cmd --zone=public --add-service=http --add-service=https --permanent

# 移除
firewall-cmd --zone=public --remove-service=http --remove-service=https --permanent

DNS

# iptables
iptables -A INPUT -p udp -m udp -s 192.168.1.0/24 --dport 53 -j ACCEPT

# 
firewall-cmd --zone=trusted --add-service=dns
firewall-cmd --zone=public --add-service=dns

TCP state

iptables -A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT

# firewalld 不需要做這個設定

mysql

# iptables
iptables -A INPUT -p tcp -m tcp --dport=3600 -j ACCEPT

# firewalld
firewall-cmd --zone=public --add-service=mysql --permanent

postgresql

# iptables
iptables -A INPUT -p tcp -m tcp --dport 5432 -s 192.168.1.0/24 -j ACCEPT

# firewalld
firewall-cmd --zone=trusted --add-service=postgresql

References

Enabling iptables Firewall - Documentation

firewalld from iptables - Documentation

firewalld for Beginners - Documentation

基本防火牆設定 | 晟鑫科技線上手冊

Firewalld 防火牆 - HackMD

Linux Firewall-cmd 防火牆安裝, 允許/禁止 IP, Port 用法介紹

2023/09/18

OAuth

傳統的 Client-Server 架構裡, Client 要拿取受保護的資源 (Protected Resoruce) 的時候,要向 Server 出示使用者 (Resource Owner) 的帳號密碼才行。如果要讓第三方應用程式也可以使用這些 Resources ,則需要 Resource Owner 把帳號密碼給這個第三方應用程式,這時候會產生以下的問題:

  • 第三方應用程式必須以明碼儲存 Resource Owner 的帳號密碼
  • Server 要支援密碼認證
  • 第三方應用程式會得到完整存取 Protected Resources 的權限,無法限制時效
  • Resource Owner 無法只撤回某一個第三方應用程式的存取權,必須要修改密碼才能撤回。
  • 當某一個第三方應用程式被破解,就會導致使用該密碼的所有資料被破解。

OAuth 解決這些問題的方式,是引入一個認證層 (authorization layer) ,並把 client 跟 resource owner 的角色分開。Client 會先索取存取權,來存取 Resource Owner 擁有的資源,這些資源會放在 Resource Server 上面,並且 Client 會得到一組不同於 Resource Owner 所持有的認證碼,也就是 access token。

角色定義

  • Resource Owner

    可授權別人去存取 Protected Resource。如果這個角色是人類的話,就是指使用者 (end-user)。

  • Resource Server

    存放 Protected Resource 的伺服器,可以根據 Access Token 來接受使用 Protected Resource 的請求。

  • Client

    讓 Resource Owner 授權後,可以去存取 Protected Resource 的應用程式。

  • Authorization Server

    認證 Resource Owner 並獲得 Resource Owner 許可後,核發 Access Token 的伺服器。

+--------+                               +---------------+
|        |--(A)- Authorization Request ->|   Resource    |
|        |                               |     Owner     |
|        |<-(B)-- Authorization Grant ---|               |
|        |                               +---------------+
|        |
|        |                               +---------------+
|        |--(C)-- Authorization Grant -->| Authorization |
| Client |                               |     Server    |
|        |<-(D)----- Access Token -------|               |
|        |                               +---------------+
|        |
|        |                               +---------------+
|        |--(E)----- Access Token ------>|    Resource   |
|        |                               |     Server    |
|        |<-(F)--- Protected Resource ---|               |
+--------+                               +---------------+

Refresh Token 流程

+--------+                                           +---------------+
|        |--(A)------- Authorization Grant --------->|               |
|        |                                           |               |
|        |<-(B)----------- Access Token -------------|               |
|        |               & Refresh Token             |               |
|        |                                           |               |
|        |                            +----------+   |               |
|        |--(C)---- Access Token ---->|          |   |               |
|        |                            |          |   |               |
|        |<-(D)- Protected Resource --| Resource |   | Authorization |
| Client |                            |  Server  |   |     Server    |
|        |--(E)---- Access Token ---->|          |   |               |
|        |                            |          |   |               |
|        |<-(F)- Invalid Token Error -|          |   |               |
|        |                            +----------+   |               |
|        |                                           |               |
|        |--(G)----------- Refresh Token ----------->|               |
|        |                                           |               |
|        |<-(H)----------- Access Token -------------|               |
+--------+           & Optional Refresh Token        +---------------+

Grant Flow

Authorization Code Grant Type Flow

+----------+
| Resource |
|   Owner  |
|          |
+----------+
     ^
     |
    (B)
+----|-----+          Client Identifier      +---------------+
|         -+----(A)-- & Redirection URI ---->|               |
|  User-   |                                 | Authorization |
|  Agent  -+----(B)-- User authenticates --->|     Server    |
|          |                                 |               |
|         -+----(C)-- Authorization Code ---<|               |
+-|----|---+                                 +---------------+
  |    |                                         ^      v
 (A)  (C)                                        |      |
  |    |                                         |      |
  ^    v                                         |      |
+---------+                                      |      |
|         |>---(D)-- Authorization Code ---------'      |
|  Client |          & Redirection URI                  |
|         |                                             |
|         |<---(E)----- Access Token -------------------'
+---------+       (w/ Optional Refresh Token)
  • 要向 Authorization Server 先取得 Grant Code 再取得 Access Token。
  • 適合 Confidential Clients ,如部署在 Server 上面的應用程式。
  • 可以核發 Refresh Token。
  • 需要 User-Agent Redirection。

Implicit Grant Type Flow

+----------+
| Resource |
|  Owner   |
|          |
+----------+
     ^
     |
    (B)
+----|-----+          Client Identifier     +---------------+
|         -+----(A)-- & Redirection URI --->|               |
|  User-   |                                | Authorization |
|  Agent  -|----(B)-- User authenticates -->|     Server    |
|          |                                |               |
|          |<---(C)--- Redirection URI ----<|               |
|          |          with Access Token     +---------------+
|          |            in Fragment
|          |                                +---------------+
|          |----(D)--- Redirection URI ---->|   Web-Hosted  |
|          |          without Fragment      |     Client    |
|          |                                |    Resource   |
|     (F)  |<---(E)------- Script ---------<|               |
|          |                                +---------------+
+-|--------+
  |    |
 (A)  (G) Access Token
  |    |
  ^    v
+---------+
|         |
|  Client |
|         |
+---------+
  • Authorization Server 直接向 Client 核發 Access Token (一步)。
  • 適合非常特定的 Public Clients ,例如跑在 Browser 裡面的應用程式。
  • Authorization Server 不必(也無法)驗證 Client 的身份。
  • 禁止核發 Refresh Token。
  • 需要 User-Agent Redirection。
  • 有資料外洩風險。

Resource Owner Password Credentials Grant Type Flow

+----------+
| Resource |
|  Owner   |
|          |
+----------+
     v
     |    Resource Owner
    (A) Password Credentials
     |
     v
+---------+                                  +---------------+
|         |>--(B)---- Resource Owner ------->|               |
|         |         Password Credentials     | Authorization |
| Client  |                                  |     Server    |
|         |<--(C)---- Access Token ---------<|               |
|         |    (w/ Optional Refresh Token)   |               |
+---------+                                  +---------------+
  • Resource Owner 的帳號密碼直接拿來當做 Grant。
  • 適用於 Resource Owner 高度信賴的 Client (像是 OS 內建的)或是官方應用程式。
  • 其他流程不適用時才能用。
  • 可以核發 Refresh Token。
  • 沒有 User-Agent Redirection。

Client Credentials Grant Type Flow

+---------+                                  +---------------+
|         |                                  |               |
|         |>--(A)- Client Authentication --->| Authorization |
| Client  |                                  |     Server    |
|         |<--(B)---- Access Token ---------<|               |
|         |                                  |               |
+---------+                                  +---------------+
  • Client 的帳號與密碼直接用來 Grant
  • 適用於跑在 Server 上面的 Confidential Client
  • 不建議核發 Refresh Token
  • 沒有 User-Agent Redirection

References

OAuth 2.0 筆記 (1) 世界觀

OAuth 2.0 筆記 (4.1) Authorization Code Grant Flow 細節

OAuth 2.0 筆記 (4.2) Implicit Grant Flow 細節

OAuth 2.0 筆記 (4.3) Resource Owner Password Credentials Grant Flow 細節

OAuth 2.0 筆記 (4.4) Client Credentials Grant Flow 細節

各大網站 OAuth 2.0 實作差異 2013

2023/09/11

mDNS DNS-SD

DNS-SD(DNS Service Discovery) 跟 mDNS (multicast DNS) 是不同的 protocol,但可以互相相容。DNS-SD 能透過標準的 DNS 技術,尋找區域網路中,提供某些服務的協定。而 mDNS 是能夠在區域網路中,在不需要 DNS Server 的情況下,就能透過機器名稱,直接查出 IP。

apple 的 Bonjour protocol 就是合併了 mDNS 與 DNS-SD 實作的。

mDNS

可用在沒有 DNS server 的區域網路中,用來作機器 domain name 的 IP 查詢。設備會透過對 224.0.0.251 這個 multicast address,Port 為 5353,進行廣播,mDNS 使用跟 DNS 一樣的封包格式。mDNS 只接受 .local 的網域名稱,可同時運作在一個設備中,跟原本的 DNS 並存。

mDNS 也有為自己命名的功能,在自選了一個 domain name 後,會用記錄類型為 any 的 mDNS 封包查詢是否有同樣的 domain name 的另一台機器,如果沒有,就會設定為自己的 domain name。

DNS-SD

基於 DNS,主要用到三種記錄類型:PTR、SRV、TXT

  • Service Discovery

    設備會先發送一個 PTR 記錄的 multicast 查詢封包,查詢的格式為

    <service>.<transport>.<domain>

    service 是查詢的服務,transport 為傳輸協定: TCP 或 UDP,domain 為查詢網域,在 mDNS 為 .local。查詢後,具有該服務的設備就會回應

    <instance>.<service>.<transport>.<domain>

    ex: 查詢 _easylink._tcp.localEMW3031 Module#500A3F._easylink._tcp.local 回應

    就表示 EMW3031 Module#500A3F 是符合該 service 的 instance

  • 取得 instance 的 domain name 與 port

    當有多個 instance 回應後,選擇某一個 instance,然後需要查詢該 instance 的 domain name 與 port,也就是查詢 SRV 記錄

    _service._proto.name. TTL class SRV priority weight port target.

    priority 和 weight 沒有作用,通常設定為 0

    port 與 target 就分別是 port 與 domain name

    ex:

    EMW3031 Module#500A3F._easylink._tcp.local. 3 IN SRV 0 0 8002 EMW3031 Module#500A3F.local.
  • 更詳細的資訊

    除了 domain name 與 port 以外,還能夠提供更多資訊,就是記錄在 TXT record 中,以 key=value 格式記錄的資訊

    ex:

    MAC=D0:BA:E4:50:0A:3F

CentOS7 avahi

Avahi is a system which facilitates service discovery on a local network via the mDNS/DNS-SD protocol suite.

安裝 avahi

yum install nss-mdns avahi avahi-tools

啟動

systemctl start avahi-daemon

出現錯誤訊息

 dbus_bus_request_name(): Connection ":1.44573" is not allowed to own the service "org.freedesktop.Avahi" due to security policies in the configuration file

要重新啟動 dbus, NetworkManager

systemctl restart dbus.service

# /var/log/messages 會出現錯誤訊息
# (NetworkManager:624): GLib-GIO-CRITICAL **: Error while sending AddMatch () message: 這個連線已關閉
systemctl restart NetworkManager
# 避免 ssh 登入問題
# Failed to activate service 'org.freedesktop.login1': timed out
systemctl restart systemd-logind

再啟動 avahi-demon

systemctl start avahi-daemon

然後就能查詢 LAN 的機器

# avahi-browse -a
+ enp3s0 IPv4 lzstg [6c:62:6d:ce:71:c7]                     Workstation          local
+ enp3s0 IPv4 macmini2                                      Microsoft Windows Network local
+ enp3s0 IPv4 macmini2                                      Apple File Sharing   local
+ enp3s0 IPv4 macmini2                                      Apple Net Assistant  local
+ enp3s0 IPv4 Michael's MacBook Pro                         Microsoft Windows Network local
+ enp3s0 IPv4 Michael's MacBook Pro                         _companion-link._tcp local

也可以直接 ping *.local 的機器

# ping macmini2.local
PING macmini2.local (192.168.1.159) 56(84) bytes of data.
64 bytes from 192.168.1.159 (192.168.1.159): icmp_seq=1 ttl=64 time=0.675 ms
64 bytes from 192.168.1.159 (192.168.1.159): icmp_seq=2 ttl=64 time=0.374 ms

發布 service

avahi-publish-service SERVICE-NAME _APPLICATIONPROTOCOL._TRANPOSRT-PROTOCOL PORT "DESCRIPTION" --sub SUBPROTOCOL

ex:

avahi-publish-service light _coap._udp 5683 “/mylight” --sub
_floor1._sub._coap._udp

發布 service name: light,使用 CoAP protocol,在 UDP 5683 提供服務

該 service 可透過 _coap._udp.local_floor1._sub._coap._udp.local 被發現

References

UWP - mDNS 找尋附近的設備

MDNS/DNS-SD TUTORIAL

使用 mDNS 在區域網中輕鬆發現系統

Bonjour手把手搭建一:mDNS(apple & multicastdns.org)

區域網設備發現之Bonjour協議

2023/08/21

柯林漢定律 (Kernighan's Law)

Brian Kernighan 在其著作 "The Elements of Programming Style" 提出一個經驗法則,稱為 Kernighan's Law。

Debugging is twice as hard as writing the code in the first place.
Therefore, if you write the code as cleverly as possible, 
you are, by definition, 
not smart enough to debug it.

除錯要比寫程式困難兩倍,
因此,如果程式寫得很精巧,
那你就沒有足夠的智慧能夠除錯。

對這句話的理解,我認為是跟程式的簡潔度及可讀性有關。

越複雜冗長的程式碼,因為程式碼太長,在 debug 時,就越不容易找到問題點。但有些人會在寫程式時,追求到極致,希望能用非常短的程式碼完成。One-liner program 的目標是,用一行程式碼完成一項功能,追求簡潔的程式碼。

這種做法沒有對錯,在高階程式語言,用很短的程式碼完成很多事情,比較容易發生,但在中低階語言,像是 C 語言,刻意追求 one-liner,會產生反效果,因為程式碼太短,而讓人難以理解在寫什麼。

因此良好的程式,應該要具有相當程度的可讀性,可讀性並不代表程式碼很多或很少,而是程式碼以大家都能理解的方式撰寫,程式的結構良好,一個可讀性高的程式,程式碼的長度可長可短。

原文中的精巧,可能是針對 C 語言的說法,因為 C 語言是中低階程式語言,精巧的程式,是聰明的寫法,也代表能用比較短的程式完成工作,在 CPU 及記憶體貧乏的時代,這種做法是有必要的,但這也代表這些程式碼會讓人難以理解,也就更難除錯。

Brian Kernighan 跟 Dennis Ritchie 是 "The C Programming Language" 這本經典書籍的作者,這本書是所有寫 C 語言要閱讀的經典書籍,也因為這本書,建立了一個規則,所有程式語言的第一個測試程式,都是要列印 "Hello, World!" 這個字串到螢幕上。另外這本書的 C 語言 coding style 也被稱為 K & R style。

Kernighan 還是 Unix 作業系統的命名者,原本被稱為 UNICS (Uniplexed Information and Computing System),另外還是 awk 這個工具的作者。

References

80歲了還在改程式碼的大神:他是Unix命名人、寫下所有程式新手的「Hello World」起手式 | T客邦

2023/08/14

過早優化效應 (Premature Optimization Effect)

Donald Knuth 在 The Art of Computer Programming 提出了過早優化法則

We should forget about small efficiencies,
say about 97% of the time:
premature optimization is the root fo all evel.
Yet we should not pass up our opportunities
in that critical 3%.

有 97% 的優化是不值得花時間去做的,過早優化是萬惡根源。
但還是要注意在關鍵的 3% 要提早去優化。

把開發時間花在不重要的優化上,可能會因為過度優化,而化簡為繁,讓系統太過複雜。過早的優化,會浪費大量資源,包含時間、金錢、人力,同時也可能因為這些優化,而衍生出其他的問題。

對於這個法則的爭論點在於,系統的哪個部分是關鍵的 3%。

這個問題跟開發人員的經驗、能力,專案的時程跟預算,系統規劃時設定的最大容量這些有關係。

要知道系統的 3% 關鍵,需要在產品規劃與設計時,就能夠預判系統的使用環境,同時的上線人數。但要能正確預判專案的基本條件並不容易,尤其是一個面對未知系統人數的服務,因為我們永遠不知道,這個系統是不是會爆紅而導致系統的瞬間流量突然增加。一但發生這個狀況,也只能面對,因為一般使用者,甚至是系統的 stake holder 都無法正確預判,有時候就只能等到事情發生了才知道,但也過了那個大流量的使用時機,因為使用者已經失去了信心,不會再用。

有經驗的開發人員,因為經驗的累積,能夠用以往的專案經驗,在新專案一開始的時候,就套用了一個基本能夠彈性修改的系統架構,但難免會遇到一個根本的問題,就是系統的運作硬體環境,程式語言或是 framework 本身的性能瓶頸,或是專案預算的限制。

問題都是存在的,但我們要知道,所有的系統使用時都有極限與限制,盡可能以漸進的方式使用某項新的系統與技術,用保守的態度面對,這樣應該比較能避免遇到問題。激進的方法當然也有可能成功,就看後續的問題處理,運氣好的話,也能撐過動蕩期。

References

过早优化是万恶之源——克努特优化原则 (Knuth's optimization principle) - 腾讯云开发者社区-腾讯云

# 流言終結者:過早進行優化是萬惡之源?

# 「过早的优化是万恶之源」这种说法对不对,为什么?

2023/08/07

帕金森瑣碎定理 (The Law of Triviality)

Cyril Northcote Parkinson 於 1957年提出,大型組織會花費大量時間在無關禁藥的瑣事上,但是遇到重大議題時,卻能很輕易地通過。這是因為一般人對於重大議題,無法完全理解而怕貿然提出建議而失言。對於一般簡單瑣碎的事情,因為基本認知足夠,故會有很多意見,造成大型組織在事項的討論度,花費的時間與其重要性成反比。

Parkinson 在 Parkinson's law, and other studies in administration 這本書中提出了幾個實例:

  1. 1000萬美元的核子反應爐,審查委員中有四個不知道反應爐是什麼,三個不知道有什麼用,兩個不知道預算多少,剩下的其中有兩個對預算有疑慮,一個提出要找專家,一個覺得無法講清楚,所以就不表示意見,整個議題花了2.5mins 通過。

  2. 建自行車棚,大家的意見很多,爭論要用什麼材料跟預算多少才合理,花了很多時間大家聽懂了,討論了 45mins,節省 300 美元。

  3. 飲料要選什麼?他們花了很多時間討論,有些人覺得不要花時間討論,但有些人爭論要不要提供咖啡,最終他們要求秘書弄清楚每個人的需求再決定。

瑣碎定理是對議題的討論狀況的描述,大部分都會是在一個會議中會發生,要避免發生瑣碎定理的問題,有幾個方法

  1. 事先準備

    提前告知與會人員會議的議題,大家能預先了解議題內容,才能快速針對議題進行討論。臨時告知的議題,多數人會覺得是意外,沒有先備知識,就無法提出適當的意見進行討論。

    事先對於會議定下規則,限制每個人的發言時間,或是要求大家都要發言。

  2. 預先安排議程

    一般傾向把最重要的事情留到最後再說,但應該把最重要的議題放在最前面討論,要能面對並解決問題,最好的方法就是開門見山

  3. 盡可能減少或不開會

    減少沒有用的會議,訊息傳遞在現今的科技下,已經不是問題。並不是所有的資訊,對每個人來說都是必要要知道的。

References

帕金森瑣碎定理 - 維基百科,自由的百科全書

帕金森瑣碎定理 - MBA智库百科

經濟學人:如何避免出現「帕金森瑣碎定律」 - 每日頭條

2023/07/31

帕金森定理 (Parkinson's Law)

1955年 Cyril Northcote Parkinson 在 The Economist 發表一篇短文

Work expands so as to fill the time available for its completion.

在工作能夠完成的時限內,工作量會一直增加,直到所有可用時間都被填充為止

後來在 1958年,擴充為一本書:Parkinson's Law: The Pursuit of Progress

The demand upon a resource tends to expand to match the supply of the resource (If the price is zero).

在預算之內,支出的需求會一直增加,直到所有資源被用完為止

員工會製造很多瑣碎的工作,讓自己看起來很忙,就算時間充裕,也會放慢工作速度,或是找其他的事情做,直到預定的交付時間被填滿。後續又以工作量增加要求招僱更多員工,組織漸漸膨脹,但從外面看起來,大家看起來很忙,實際上是工作效率低下。

  1. 增加下屬法則

  2. 增加工作法則

為避免工作拖延的狀況,管理者可以安排更多工作,或是制定不合理的交付時間。但這樣的作法可能會有副作用:

  1. 期限內無法完成的事情會越來越多

  2. 為了在期限內完成,無法確保工作品質

  3. 員工因不堪負荷而離職

本定律要發生有四個條件

  1. 要有一個組織,管理階層在組織中佔有一定的地位

  2. 該管理者能力平庸,是個不稱職的管理者

  3. 對該管理者而言,可能因為某些事情而喪失權力

  4. 該組織是個不斷自我要求完善的組織,能夠不斷吸收新人

References

帕金森定理 - 維基百科,自由的百科全書

# 帕金森定律》為何公司的人越來越多,生產效率卻越來越差?

帕金森定律:如何克服它以提高生產力 • Asana

《每個人的商學院・管理基礎》:大企業最常犯的「帕金森定律」與「彼得原理」 - The News Lens 關鍵評論網

帕金森定律 - MBA智库百科

2023/07/24

隱式接口法則 (Hyrum's Law, The Law of Implicit Interfaces)

在 Google 開發 C++ library 的工程師 Hyrum Wright 在網站 https://www.hyrumslaw.com 提出一個軟體工程中觀察的經驗法則:

With a sufficient number of users of an API,
it does not matter what you promise in the contract:
all observable behaviors of your system
will be depended on by somebody.

當某個 API 有了相當多的使用者以後,
原本的規格已經不重要了:
因為所有可被發現的系統行為,
都會被某人使用,並依賴該系統行為。

API 是系統模組之間互相交互運作的介面,也是某個功能模組的抽象化,因為單一功能模組的內容細節過於複雜,無法讓所有人都理解細節,故使用者會透過抽象介面使用該功能。

隨著系統的使用者增加,會漸漸地透過實作的細節,慢慢披露出該模組的未公開的功能內容,導致有越來越多的使用者,依賴於該 API 的所有系統行為。也就是隱式接口法則。

隱式接口通常是慢慢發生的,使用者並不會意識到正在發生,例如:API 通常沒有性能保證,但使用者會期待該 API 能夠達到某個程度的運算能力,這表示 API 可能需要被慢慢修改到符合使用者的期待。

例如 Hash 的輸出順序,在原本的定義中,並不會保證有固定輸出的順序,但某些語言實作時,會發生固定輸出順序的狀況,導致有使用者套用這個規則使用 Hash。

References

海拉姆定律——一个软件工程的观察

软件工程中的海仑定律 - hyrumslaw

程序员应知必会的思维模型之 16 隐式接口定律 (Hyrum‘s Law or The Law of Implicit Interfaces)_知识大胖的博客-CSDN博客_hyrum's law

2023/07/17

赫特伯定律 Hutber's Law

Patrick Hutber 於 1970 年代提出 Hutber's Law

Improvement means deterioration.

改善等同於惡化。任何改善的細節中,都隱藏著可能的惡化因子。對於系統的某個部份的功能改進,可能會影響到其他部分,導致其他功能發生問題。

在對軟體進行改版,修改功能的時候,常會發生 side effect 副作用,有可能是功能本身定義的問題,因為該功能的定義修改,而造成其他功能的條件設定跟原本的不同了。也有可能是因為共用程式碼的關係,某個部份的修改,在另一個部分重用的時候,造成條件不符的問題。

但這種問題,常常很難在第一時間就被發現。

Test Driven 軟體開發方法,就是在開發時,要寫足夠多的測試程式碼,這些測試程式就可以用來預先發生這些可能的未知問題。用大量自動化的測試,來提醒開發人員無法預先知道的問題。

但在使用者介面測試這個部分,目前還比較不容易做自動化,目前是有自動化 UI 測試的工具,但要維護 UI Test 的成本太高,有些問題也無法用自動化測試察覺。

References

Hutber's law - Wikipedia

Hutber’s Law Explained - YouTube

2023/07/10

UUID

UUID: Universally Unique Identifier 通用唯一識別碼,是 128 位元的識別碼,被開源軟體基金會 (Open Software Foundation, OSF) 的組織使用在分散式計算環境 (Distributed Computing Environment, DCE)。用途是讓分散式系統中的元素,能有唯一的辨識資訊,不需要透過一個集中的系統辨識資訊的唯一性。2005年7月,RFC4122 定義了 UUID 的 URN name space,並制定 UUID 的規格。

結構

UUID 的結構為 16 個 8 bits 的 byte array,總共是 32 個 16 進位的數字,以 "8-4-4-4-12" 的形式表示

xxxxxxxx-xxxx-Bxxx-Axxx-xxxxxxxxxxxx

Variant

其中 A 代表 UUID variants 欄位,目前的規格使用 variant 2

Name binary bits description
Apollo NCS Variant 0XXX 1988 年 Apollo 系統使用
OSF DCE Variant 10XX RFC 4122/DCE 1.1 UUIDs
Microsoft COM/DCOM Variant 110X reserved, Microsoft Corporation backward compatibility
Reserved Variant 未定義 所有未定義的格式

B 代表 Version,以一整個 byte 的值代表 UUID 的版本

目前 Version 有 1,2,3,4,5 五種,最常使用的是隨機生成的 Version 4。

  • Time-Based (UUIDv1)
  • DCE Security (UUIDv2)
  • Name Based (UUIDv3 and UUIDv5)
  • Random (UUIDv4)

Version

Version 1

根據時間, Version, Variant 產生前四個部分的值,根據 Mac Address 產生 Node。這個版本的 UUID 優點是依照時間順序產生,因此排序速度比較快,但也比較有可能會產生相同的 UUID。

如果是真實的 MAC Address,可以追蹤到原始產生 UUID 的電腦,但這部分也可以用亂數產生。

98a956f7-0188-1000-88fc-6650c08fc392

98a956f7: Low Time
0188: Mid Time
1000: High Time and Version
88fc: Clock Sequence and Variant
6650c08fc392: Node

Version 2

比較少被使用,規格定義於 DCE 1.1: Authentication and Security Services - Privilege (Authorisation) Services

Version 3, 5

根據 namespace 及唯一的 name 產生,當輸入的字串固定時,產生的 UUID 也會是固定的。

Version 3 是使用 MD5 hash of the name and namespace

Version 5 是使用 SHA-1 hash of the name and namespace,當 hash 結果長度太長時,就直接 truncated

Version 4

完全是亂數產生的,有超過 5.3 x 10^36 個 UUIDs,這是最常被使用的版本

Java Code

import java.util.Random;
import java.util.UUID;

public class UUIDTest {
    public static void main(String... args) {
        v1();
        v3();
        v4();
    }

    private static void uuid_field(UUID uuid1) {
        int version = uuid1.version();
        int variant = uuid1.variant();
        int clockSequence = -1;
        long node = -1;
        long timestamp = -1;
        try {
            clockSequence = uuid1.clockSequence();
            node = uuid1.node();
            timestamp = uuid1.timestamp();
        } catch(UnsupportedOperationException e) {

        }
        System.out.println("uuid="+uuid1+", version="+version+", variant="+variant+", clockSequence="+clockSequence+", node="+node+", timestamp="+timestamp);
    }

    private static void v4() {
        UUID uuid1 = UUID.randomUUID();
        UUID uuid2 = UUID.randomUUID();
        UUID uuid3 = UUID.randomUUID();

        System.out.println("UUID v4");
        uuid_field(uuid1);
        uuid_field(uuid2);
        uuid_field(uuid3);
        System.out.println("");
    }

    private static void v3() {
        String s1 = "Test String";
        String s3 = "Test String3";
        UUID uuid1 = UUID.nameUUIDFromBytes(s1.getBytes());
        UUID uuid2 = UUID.nameUUIDFromBytes(s1.getBytes());
        UUID uuid3 = UUID.nameUUIDFromBytes(s3.getBytes());

        System.out.println("UUID v3");
        uuid_field(uuid1);
        uuid_field(uuid2);
        uuid_field(uuid3);
        System.out.println("");
    }
    // UUID.java 裡面的 v3 實作
//    public static UUID nameUUIDFromBytes(byte[] name) {
//        MessageDigest md;
//        try {
//            md = MessageDigest.getInstance("MD5");
//        } catch (NoSuchAlgorithmException nsae) {
//            throw new InternalError("MD5 not supported", nsae);
//        }
//        byte[] md5Bytes = md.digest(name);
//        md5Bytes[6]  &= 0x0f;  /* clear version        */
//        md5Bytes[6]  |= 0x30;  /* set to version 3     */
//        md5Bytes[8]  &= 0x3f;  /* clear variant        */
//        md5Bytes[8]  |= 0x80;  /* set to IETF variant  */
//        return new UUID(md5Bytes);
//    }

    private static void v1() {
        UUID uuid1 = generateUUID_V1();
        UUID uuid2 = generateUUID_V1();
        UUID uuid3 = generateUUID_V1();

        System.out.println("UUID v1");
        uuid_field(uuid1);
        uuid_field(uuid2);
        uuid_field(uuid3);
        System.out.println("");
    }

    public static UUID generateUUID_V1() {
        long most64SigBits = get64MostSignificantBitsForVersion1();
        long least64SigBits = get64LeastSignificantBitsForVersion1();
        return new UUID(most64SigBits, least64SigBits);
    }

    private static long get64LeastSignificantBitsForVersion1() {
        Random random = new Random();
        long random63BitLong = random.nextLong() & 0x3FFFFFFFFFFFFFFFL;
        long variant3BitFlag = 0x8000000000000000L;
        return random63BitLong | variant3BitFlag;
    }

    private static long get64MostSignificantBitsForVersion1() {
        final long currentTimeMillis = System.currentTimeMillis();
        final long time_low = (currentTimeMillis & 0x0000_0000_FFFF_FFFFL) << 32;
        final long time_mid = ((currentTimeMillis >> 32) & 0xFFFF) << 16;
        final long version = 1 << 12;
        final long time_hi = ((currentTimeMillis >> 48) & 0x0FFF);
        return time_low | time_mid | version | time_hi;
    }
}

執行結果

UUID v1
uuid=98a956f7-0188-1000-88fc-6650c08fc392, version=1, variant=2, clockSequence=2300, node=112497014064018, timestamp=1686188414711
uuid=98a956fb-0188-1000-b9c6-84e9d3083144, version=1, variant=2, clockSequence=14790, node=146139802775876, timestamp=1686188414715
uuid=98a956fb-0188-1000-9bdc-540d86968ed3, version=1, variant=2, clockSequence=7132, node=92417069321939, timestamp=1686188414715

UUID v3
uuid=bd08ba3c-982e-3ad7-a860-2536fb8e1184, version=3, variant=2, clockSequence=-1, node=-1, timestamp=-1
uuid=bd08ba3c-982e-3ad7-a860-2536fb8e1184, version=3, variant=2, clockSequence=-1, node=-1, timestamp=-1
uuid=c1a59283-7a55-3e83-b9ed-864b5367685b, version=3, variant=2, clockSequence=-1, node=-1, timestamp=-1

UUID v4
uuid=2de4d27e-9abc-4b9d-83bd-b721ef6b2882, version=4, variant=2, clockSequence=-1, node=-1, timestamp=-1
uuid=e82fca87-acd0-4000-8a4f-f4d7001d56a4, version=4, variant=2, clockSequence=-1, node=-1, timestamp=-1
uuid=fb3a6890-57e3-4a13-a238-ef6f3de63fbe, version=4, variant=2, clockSequence=-1, node=-1, timestamp=-1

References

通用唯一辨識碼 - 維基百科,自由的百科全書

# 閒談軟體架構:UUID

# 閒談軟體架構:UUID 之三部曲

Guide to UUID in Java | Baeldung

2023/06/26

Java Stream API

Collection 是程式中存放於內部記憶體的資料結構,而 Stream 是一種資料流 pipeline,可以依照需要使用這些內部記憶體的資料。

Stream 的 operation 有兩類:intermediate 或是 terminal,intermediate opertaion 會回傳 stream,故能夠以 chain 的方式依序串接多個 intermediate operation,針對資料進行運算。terminal operation 會於計算後,回傳某個類別的結果。

Stream operation 有循序跟平行化處理兩種,平行化的 operations 能夠在多個 thread 同時運算,能充分運用多核心處理器,加速運算的過程。

Stream 的資料來源是 java.util.Collection,例如 List, Set。Map 無法直接支援,但能分別對 keys, values 或 entries 建立 Stream。

建立 Stream

從 data collections 產生 Stream 的方法

    private static void createStream() {
        // Stream.of
        System.out.println("Stream.of");
        Stream<Integer> stream = Stream.of(1,2,3,4,5,6,7,8,9);
        stream.forEach(p -> System.out.print(p + " "));
        System.out.println("");

        // Stream.of(array)
        System.out.println("Stream.of(array)");
        Stream<Integer> stream2 = Stream.of( new Integer[]{1,2,3,4,5,6,7,8,9} );
        stream2.forEach(p -> System.out.print(p + " "));
        System.out.println("");

        // List.stream()
        System.out.println("List.stream()");
        List<Integer> list = new ArrayList<Integer>();
        for(int i = 1; i< 10; i++){
            list.add(i);
        }
        Stream<Integer> stream3 = list.stream();
        stream3.forEach(p -> System.out.print(p + " "));
        System.out.println("");

        // Stream.generate() or Stream.iterate()
        // 透過 generator function 產生 Stream elements
        // 以 limit() 限制 element 個數
        System.out.println("Stream.generate()");
        Stream<Integer> randomNumbers = Stream
                .generate(() -> (new Random()).nextInt(100));
        randomNumbers.limit(10).forEach(s -> System.out.print(s + " "));
        System.out.println("");

        //  Stream of String chars or tokens
        System.out.println("Stream of String chars");
        IntStream stream5 = "12345_abcdefg".chars();
        stream5.forEach(p -> System.out.print(p + " "));
        System.out.println("");
        System.out.println("Stream of tokens");
        Stream<String> stream6 = Stream.of("A$B$C".split("\\$"));
        stream6.forEach(p -> System.out.print(p + " "));
        System.out.println("");
    }

Stream Collector

透過 Collector 可將 Stream 裡面的 elements 再轉換為 Collection

    private static void streamCollector() {
        // List 轉換為 Stream
        List<Integer> list = new ArrayList<Integer>();
        for(int i = 1; i< 10; i++){
            list.add(i);
        }
        Stream<Integer> stream = list.stream();

        // 用 collect 再將 Stream 轉換為 List
        System.out.println("collect");
        List<Integer> evenNumbersList = stream.filter(i -> i%2 == 0)
                .collect(Collectors.toList());
        System.out.print(evenNumbersList);
        System.out.println("");

        // 用 collect 再將 Stream 轉換為 Array
        System.out.println("toArray");
        Stream<Integer> stream2 = list.stream();
        Integer[] evenNumbersArr = stream2.filter(i -> i%2 == 0).toArray(Integer[]::new);
        for (Integer i : evenNumbersArr) {
            System.out.print(i + " ");
        }
        System.out.println("");
    }

Operations

Intermediate Operations

method description
filter() 條件過濾
map() 對每個 elements 執行某個運算
flatMap() 將多個Stream連接成一個Stream
distinct() 刪除相同的 elements
sorted() 排序
peek() 用在debug
limit() 限制回傳的 elements 數量
skip() 略過前面幾個 elements

java sample

    private static void intermediateOperation() {
        List<String> names = new ArrayList<>();
        names.add("Allen");
        names.add("George");
        names.add("Anderson");
        names.add("Danny");

        // filter 條件過濾
        System.out.println("filter");
        names.stream().filter((s) -> s.startsWith("A"))
                .forEach(n -> System.out.print(n + " "));
        System.out.println("");
        System.out.println("");

        // map  對每個 elements 執行某個運算
        System.out.println("map");
        names.stream().filter((s) -> s.startsWith("A"))
                .map(String::toUpperCase)
                .forEach(n -> System.out.print(n + " "));
        System.out.println("");
        System.out.println("");

        // flatMap  將多個Stream連接成一個Stream
        List<String> names2 = new ArrayList<>();
        names2.add("Jim");
        System.out.println("flatMap");
        List<String> flapMapStream = Stream.of(names, names2).flatMap(u -> u.stream()).collect(Collectors.toList());
        System.out.println(flapMapStream);
        System.out.println("");
        System.out.println("");

        // distinct
        // 刪除相同的 elements
        List<String> names3 = new ArrayList<>();
        names3.add("Jim");
        names3.add("Jimmy");
        names3.add("Jim");
        System.out.println("distinct");
        names3.stream()
                .distinct()
                .forEach(n -> System.out.print(n + " "));
        System.out.println("");
        System.out.println("");

        //sorted  排序
        System.out.println("sorted");
        names.stream()
                .sorted()
                .forEach(n -> System.out.print(n + " "));
        System.out.println("");
        System.out.println("");

        // peek 用在debug
        System.out.println("peek");
        List res = names.stream().filter((s) -> s.startsWith("A"))
                .peek(e -> System.out.println("filter res: " + e))
                .map(String::toUpperCase)
                .peek(e -> System.out.println("map res: " + e))
                .collect(Collectors.toList());
        System.out.println(res);
        System.out.println("");
        System.out.println("");

        // limit
        // 限制回傳的 elements 數量
        System.out.println("limit");
        List res2 = names.stream().filter((s) -> s.startsWith("A"))
                .map(String::toUpperCase)
                .limit(1)
                .collect(Collectors.toList());
        System.out.println(res2);
        System.out.println("");
        System.out.println("");

        // skip
        // 略過前面幾個 elements
        System.out.println("skip");
        List res3 = names.stream().filter((s) -> s.startsWith("A"))
                .map(String::toUpperCase)
                .skip(1)
                .collect(Collectors.toList());
        System.out.println(res3);
        System.out.println("");
        System.out.println("");
    }

Terminal Operations

method description
forEach 對 steam 所有 elements 進行運算
forEachOrdered 會依照原本 stream 元素的順序輸出,平行化 stream 後,順序也會固定
toArray 轉換為 Array
reduce 把一個Stream的所有元素按照聚合函數聚合成一個結果
collect 透過 Collector 可將 Stream 裡面的 elements 再轉換為 Collection
min 找最小元素
max 找最大元素
count 元素數量
anyMatch 檢查是否有任何一個元素,滿足給定的條件
allMatch 檢查是否有所有的元素,都滿足給定的條件
nonMatch 檢查是否沒有任何一個元素,滿足給定的條件
findFirst 回傳 stream 的第一個元素
findAny 回傳 stream 的任一個元素
    private static void terminalOperation() {
        List<String> names = new ArrayList<>();
        names.add("Allen");
        names.add("George");
        names.add("Anderson");
        names.add("Danny");

        // forEach
        System.out.println("forEach");
        names.stream().forEach(n -> System.out.print(n + " "));
        System.out.println("");
        System.out.println("");

        // forEachOrdered
        System.out.println("1. Sequential Stream");
        System.out.println("1.1 forEach 會依照原本的順序輸出");
        names.stream().forEach(n -> System.out.print(n + " "));
        System.out.println("");
        System.out.println("1.2 forEachOrdered 會依照原本的順序輸出");
        names.stream().forEachOrdered(n -> System.out.print(n + " "));
        System.out.println("");
        System.out.println("2. Parallel Stream");
        System.out.println("2.1 forEach 輸出順序不固定");
        names.stream().parallel().forEach(n -> System.out.print(n + " "));
        System.out.println("");
        System.out.println("2.2 forEachOrdered 會依照原本的順序輸出");
        names.stream().parallel().forEachOrdered(n -> System.out.print(n + " "));
        System.out.println("");
        System.out.println("");

        // toArray
        System.out.println("toArray");
        String[] nameArray = names.stream().toArray(String[]::new);
        for( String n: nameArray) {
            System.out.print(n+" ");
        }
        System.out.println("");
        System.out.println("");

        // reduce 傳入 BinaryOperator
        System.out.println("reduce");
        int sum = Stream.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10).reduce(0, (s, n) -> s + n);
        int mul = Stream.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10).reduce(1, (m, n) -> m * n);
        System.out.println("累加 sum="+sum);
        System.out.println("連乘 mul="+mul);
        System.out.println("");
        System.out.println("");

        // collect
        System.out.println("collect");
        List<String> filterNames = names.stream().filter((s) -> s.startsWith("A"))
                .collect(Collectors.toList());
        for( String n: filterNames) {
            System.out.print(n+" ");
        }
        System.out.println("");
        System.out.println("");

        // min, max
        System.out.println("min, max");
        Optional<String> minName = names.stream()
                .min(Comparator.comparing(String::valueOf));
        if( minName.isPresent() ) {
            System.out.println("min = "+minName.get());
        }
        Optional<String> maxName = names.stream()
                .max(Comparator.comparing(String::valueOf));
        if( maxName.isPresent() ) {
            System.out.println("max = "+maxName.get());
        }
        System.out.println("");
        System.out.println("");

        // count
        System.out.println("count");
        long count = names.stream().filter((s) -> s.startsWith("A"))
                .count();
        System.out.println("count="+count);
        System.out.println("");
        System.out.println("");

        // anyMatch allMatch noneMatch
        System.out.println("anyMatch allMatch noneMatch");
        boolean anyMatch = names.stream()
                .anyMatch(n -> Character.isUpperCase(n.charAt(0)));
        boolean allMatch = names.stream()
                .allMatch(n -> Character.isUpperCase(n.charAt(0)));
        boolean noneMatch = names.stream()
                .noneMatch(n -> Character.isUpperCase(n.charAt(0)));
        System.out.println("anyMatch="+anyMatch);
        System.out.println("allMatch="+allMatch);
        System.out.println("noneMatch="+noneMatch);
        System.out.println("");
        System.out.println("");

        // findFirst  findAny
        System.out.println("findFirst  findAny");
        Optional<String> findFirst = names.stream().filter((s) -> s.startsWith("A"))
                .findFirst();
        if( findFirst.isPresent() ) {
            System.out.println("findFirst = "+findFirst.get());
        }
        Optional<String> findAny = names.stream().filter((s) -> s.startsWith("A"))
                .findAny();
        if( findAny.isPresent() ) {
            System.out.println("findAny = "+findAny.get());
        }
        System.out.println("");
        System.out.println("");
    }

parallel

parallelStream() 及 stream().parallel() 可將 stream 平行化

    private static void parallel() {
        // List.stream()
        // List.parallelStream()
        System.out.println("List.stream()");
        List<Integer> list = new ArrayList<Integer>();
        for (int i = 1; i < 10; i++) {
            list.add(i);
        }
        Stream<Integer> stream3 = list.stream();
        stream3.forEach(p -> System.out.print(p + " "));
        System.out.println("");
        System.out.println("List.parallelStream()");
        Stream<Integer> stream4 = list.parallelStream();
        stream4.forEach(p -> System.out.print(p + " "));
        System.out.println("");

        // stream().parallel()
        System.out.println("stream().parallel()");
        Stream<Integer> stream5 = list.stream().parallel();
        stream5.forEach(p -> System.out.print(p + " "));
        System.out.println("");
    }

References

[JAVA] Java 8 Streams API

Java Stream API (with Examples) - HowToDoInJava

Understanding Java 8 Streams API - amitph

10 Examples of Stream API in Java 8 - count + filter + map + distinct + collect() Examples | Java67

Java 8 Stream | 菜鸟教程

Difference Between parallelStream() and stream().parallel() in Java | Baeldung

2023/06/19

Java CompletableFuture

CompletableFuture 是 Java 8 在 java.util.concurrent 中新增的非同步處理類別

該類別主要有兩種執行非同步工作的方法:runAsync, supplyAsync,差別是有沒有需要回傳結果,在沒有 executor 的狀況下,會使用預設的 thread pool

public static CompletableFuture<Void> runAsync(Runnable runnable)
public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier)
public static CompletableFuture<Void> runAsync(Runnable runnable,Executor executor)
public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier, Executor executor)

java sample

    private static void test1() {
        try {
            CompletableFuture<String> hello = CompletableFuture.supplyAsync(() -> {
                System.out.println("supplyAsync START");
                System.out.println("say supplyAsync Hello World");
                return "supplyAsync Hello World";
            });
            System.out.println(hello.get());

            CompletableFuture<Void> world = CompletableFuture.runAsync(() -> {
                System.out.println("runAsync START");
                System.out.println("say runAsync Hello World");
                return ;
            });

        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }

        // console 結果
//        supplyAsync START
//        say supplyAsync Hello World
//        supplyAsync Hello World
//        runAsync START
//        say runAsync Hello World

    }

用 Executors 產生自訂的 thread pool

ExecutorService threadPool = Executors.newFixedThreadPool(10);
CompletableFuture<Void> runAsync = CompletableFuture.runAsync(() -> System.out.println("runAsync"), threadPool);
CompletableFuture<String> supplyAsync = CompletableFuture.supplyAsync(() -> "supplyAsync", threadPool);

java sample


處理 exception

可在工作處理完成後,取得結果,在處理過程中,遇到 exception 時,可以攔截 exception 並執行特定的工作

相關的 method 是

CompletableFuture<T> whenComplete(BiConsumer<? super T, ? super Throwable> action)
CompletableFuture<T> whenCompleteAsync(BiConsumer<? super T, ? super Throwable> action)
// catch exception
CompletableFuture<T> exceptionally(Function<Throwable, ? extends T> fn)

// use your own thread pool
CompletableFuture<T> whenCompleteAsync(BiConsumer<? super T, ? super Throwable> action, Executor executor)

sample code:

// 在工作處理完成後,取得結果,在處理過程中,遇到 exception 時,可以攔截 exception 並執行特定的工作
    private static void test3() {
        ExecutorService threadPool = Executors.newFixedThreadPool(10);
        CompletableFuture<String> supplyAsync = CompletableFuture.supplyAsync(() -> {
            String doSomething = "Hello World";
            return doSomething;
        }, threadPool);

        supplyAsync.whenCompleteAsync((result, ex) -> {
            System.out.println("---supplyAsync---");
            System.out.println("supplyAsync result: " + result);
            System.out.println("exception: " + ex);
        }, threadPool).exceptionally(ex -> {
            System.out.println("exceptionally: " + ex.getMessage());
            return ex.getMessage();
        }).join();

        CompletableFuture<String> supplyAsyncException = CompletableFuture.supplyAsync(() -> {
            throw new CompletionException(new Exception("throw exception"));
        }, threadPool);

        supplyAsyncException.whenCompleteAsync((result, ex) -> {
            System.out.println("---supplyAsyncException---");
            System.out.println("supplyAsyncException result: " + result);
            System.out.println("exception: " + ex);
        }, threadPool).exceptionally(ex -> {
            System.out.println("exceptionally: " + ex.getMessage());
            return ex.getMessage();
        }).join();
        // 執行結果:
//        ---supplyAsync---
//        supplyAsync result: Hello World
//        exception: null
//        ---supplyAsyncException---
//        supplyAsyncException result: null
//        exception: java.util.concurrent.CompletionException: java.lang.Exception: throw exception
//        exceptionally: java.lang.Exception: throw exception
        threadPool.shutdown();
    }

也可以改用 handle 處理 exception,差別是 handle 可以有回傳值

CompletableFuture<U> handle(BiFunction<? super T, Throwable, ? extends U> fn)
CompletableFuture<U> handleAsync(BiFunction<? super T, Throwable, ? extends U> fn)

// use your own thread pool
CompletableFuture<U> handleAsync(BiFunction<? super T, Throwable, ? extends U> fn, Executor executor)

sample code:

    private static void test4() {
        ExecutorService threadPool = Executors.newFixedThreadPool(2);
        CompletableFuture<String> supplyAsync = CompletableFuture.supplyAsync(() -> {
            throw new CompletionException(new Exception("throw exception"));
        }, threadPool);

        String res = supplyAsync.handle((result, ex) -> (null != ex) ? ex.getMessage() : result).join();
        System.out.println("res: " + res);

        // 結果
//        res: java.lang.Exception: throw exception
        threadPool.shutdown();
    }

資料轉換

可使用 thenApply 進行資料轉換,還有 thenCompose,不需要回傳值的 thenAccept

CompletableFuture<U> thenApply(Function<? super T, ? extends U> fn)
CompletableFuture<U> thenApplyAsync(Function<? super T, ? extends U> fn)
// use your own thread pool
CompletableFuture<U> thenApplyAsync(Function<? super T, ? extends U> fn, Executor executor)
//-----------
CompletableFuture<U> thenCompose(Function<? super T, ? extends CompletionStage<U>> fn)
CompletableFuture<U> thenComposeAsync(Function<? super T, ? extends CompletionStage<U>> fn)

CompletableFuture<U> thenComposeAsync(Function<? super T, ? extends CompletionStage<U>> fn, Executor executor)
//-----------
CompletableFuture<Void> thenAccept(Consumer<? super T> action)
CompletableFuture<Void> thenAcceptAsync(Consumer<? super T> action)
// use your own thread pool
CompletableFuture<Void> thenAcceptAsync(Consumer<? super T> action, Executor executor)

"兩個"獨立 CompletableFuture 要整合結果

當有"兩個"獨立 CompletableFuture 要整合結果時,可使用

thenCombine 可將兩個獨立的 CompletableFuture 執行結果整合在一起

thenAcceptBoth 類似 thenCombine,但不需要回傳值

//-----------
CompletableFuture<V> thenCombine(CompletionStage<? extends U> other, BiFunction<? super T, ? super U, ? extends V> fn)
CompletableFuture<V> thenCombineAsync(CompletionStage<? extends U> other, BiFunction<? super T, ? super U, ? extends V> fn)
// use your own thread pool
CompletableFuture<V> thenCombineAsync(CompletionStage<? extends U> other, BiFunction<? super T, ? super U, ? extends V> fn, Executor executor)

///////
CompletableFuture<Void> thenAcceptBoth(CompletionStage<? extends U> other, BiConsumer<? super T, ? super U> action)
CompletableFuture<Void> thenAcceptBothAsync(CompletionStage<? extends U> other, BiConsumer<? super T, ? super U> action)
CompletableFuture<Void> runAfterBoth(CompletionStage<?> other,  Runnable action)

// use your own thread pool
CompletableFuture<Void> thenAcceptBothAsync(CompletionStage<? extends U> other, BiConsumer<? super T, ? super U> action, Executor executor)

java sample

    // 將 supplyAsync1 和 supplyAsync2 結果整合
    public static void test5() {
        ExecutorService threadPool = Executors.newFixedThreadPool(2);
        CompletableFuture<String> supplyAsync1 = CompletableFuture.supplyAsync(() -> "supplyAsync 1", threadPool);
        CompletableFuture<String> supplyAsync2 = CompletableFuture.supplyAsync(() -> "supplyAsync 2", threadPool);

        String ans = supplyAsync1.thenCombine(supplyAsync2, (result1, result2) -> result1 + ", " + result2).join();
        System.out.println("ans: " + ans);
        threadPool.shutdown();
        // ans: supplyAsync 1, supplyAsync 2

        //-----------
        // 不需要回傳值
        ExecutorService threadPool2 = Executors.newFixedThreadPool(10);
        CompletableFuture<String> supplyAsync21 = CompletableFuture.supplyAsync(() -> "supplyAsync 1", threadPool2);
        CompletableFuture<String> supplyAsync22 = CompletableFuture.supplyAsync(() -> "supplyAsync 2", threadPool2);

        supplyAsync21.thenAcceptBothAsync(supplyAsync22, (result1, result2) -> System.out.println(result1 + ", " + result2), threadPool2).join();
        threadPool2.shutdown();

        // supplyAsync 1, supplyAsync 2
    }

多個 CompletableFuture

如果超過兩個以上的 CompletableFuture,可使用 allOf 等待所有的 CompletableFuture 結果,串接一個 thenRun

    public static void test6() {
        ExecutorService threadPool = Executors.newFixedThreadPool(10);
        CompletableFuture<String> supplyAsync1 = CompletableFuture.supplyAsync(() -> "supplyAsync 1", threadPool);
        CompletableFuture<String> supplyAsync2 = CompletableFuture.supplyAsync(() -> "supplyAsync 2", threadPool);
        CompletableFuture<String> supplyAsync3 = CompletableFuture.supplyAsync(() -> "supplyAsync 3", threadPool);

        CompletableFuture.allOf(supplyAsync1, supplyAsync2, supplyAsync3).thenRun(() -> {
            try {
                StringBuffer ans = new StringBuffer();
                ans.append(supplyAsync1.get()).append(", ")
                        .append(supplyAsync2.get()).append(", ")
                        .append(supplyAsync3.get());
                System.out.println("ans: " + ans.toString());
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (ExecutionException e) {
                e.printStackTrace();
            }
        }).join();

        threadPool.shutdown();
        // ans: supplyAsync 1, supplyAsync 2, supplyAsync 3

    }

只想要其中一個有完成就往下執行,可使用 applyToEither,沒有回傳值的 acceptEither,用在多個 CompletableFuture 的anyOf

CompletableFuture<U> applyToEither(CompletionStage<? extends T> other, Function<? super T,U> fn)
CompletableFuture<U> applyToEitherAsync(CompletionStage<? extends T> other, Function<? super T,U> fn)

// use your own thread pool
CompletableFuture<U> applyToEitherAsync(CompletionStage<? extends T> other, Function<? super T,U> fn, Executor executor)

///////

CompletableFuture<Void>  acceptEither(CompletionStage<? extends T> other, Consumer<? super T> action)
CompletableFuture<Void>  acceptEitherAsync(CompletionStage<? extends T> other, Consumer<? super T> action)

// use your own thread pool
CompletableFuture<Void>  acceptEitherAsync(CompletionStage<? extends T> other, Consumer<? super T> action, Executor executor)


///////
CompletableFuture<Object> anyOf(CompletableFuture<?>... cfs)

References

Guide To CompletableFuture | Baeldung

Java 9 CompletableFuture API Improvements | Baeldung

JDK8 - CompletableFuture 非同步處理簡介

CompletableFuture

【Java 8 新特性】Java CompletableFuture thenApply()_.thenapply_猫巳的博客-CSDN博客

【JDK8】CompletableFuture 非同步處理

關於 Java CompletableFuture 的用法 | HengLin31

2023/06/05

HttpClient in JDK 11

JDK 8 以前的 HTTP Client 通常是使用第三方函式庫,最常使用的是 Apache HTTPComponents 及 OkHttp。在 JDK 9 以後,標準函式庫裡面也有 HTTP Client 可以使用。

程式包含三個部分:HttpRequest, HttpResponse 及 HttpClient

HttpRequst

  1. URI 就是 request 要發送的目標網址

  2. HTTP Method

    • GET()
    • POST(BodyPublisher body)
    • PUT(BodyPublisher body)
    • DELETE()
  3. HTTP Protcol Version

    可指定這個 http request 的版本,以往常見的是 1.1,目前是 2

  4. Headers

    設定 http request header

  5. Timeout

    設定等待 http response 的 timeout 時間

  6. Request Body

    如果是 POST, PUT, DELETE method,需要再增加 body content,對應的 content

Request Body

可使用以下這些 API 實作 body content

  1. StringProcessor

    從 String 產生 body,以 HttpRequest.BodyPublishers.ofString 產生

    HttpRequest request = HttpRequest.newBuilder()
     .uri(new URI("https://postman-echo.com/post"))
     .headers("Content-Type", "text/plain;charset=UTF-8")
     .POST(HttpRequest.BodyPublishers.ofString("Sample request body"))
     .build();
  2. IputStreamProcessor

    從 InputSteam 產生 body,以 HttpRequest.BodyPublishers.ofInputStream 產生

    byte[] sampleData = "Sample request body".getBytes();
    HttpRequest request = HttpRequest.newBuilder()
           .uri(new URI("https://postman-echo.com/post"))
           .headers("Content-Type", "text/plain;charset=UTF-8")
           .POST(HttpRequest.BodyPublishers
                   .ofInputStream(() -> new ByteArrayInputStream(sampleData)))
           .build();
  3. ByteArrayProcessor

    從 ByteArray 產生 Body,以 HttpRequest.BodyPublishers.ofByteArray 產生

    byte[] sampleData = "Sample request body".getBytes();
    HttpRequest request = HttpRequest.newBuilder()
           .uri(new URI("https://postman-echo.com/post"))
           .headers("Content-Type", "text/plain;charset=UTF-8")
           .POST(HttpRequest.BodyPublishers.ofByteArray(sampleData))
           .build();
  4. FileProcessor

    從某個路徑的檔案產生 body,以 HttpRequest.BodyPublishers.ofFile 產生

    HttpRequest request = HttpRequest.newBuilder()
           .uri(new URI("https://postman-echo.com/post"))
           .headers("Content-Type", "text/plain;charset=UTF-8")
           .POST(HttpRequest.BodyPublishers.ofFile(
                   Paths.get("sample.txt")))
           .build();
  5. noBody

    如果沒有 body content,可使用 HttpRequest.BodyPublishers.**noBody()

    HttpRequest request = HttpRequest.newBuilder()
     .uri(new URI("https://postman-echo.com/post"))
     .POST(HttpRequest.BodyPublishers.noBody())
     .build();

HttpClient

  • 透過 HttpClient.newBuilder() 或是 HttpClient.newHttpClient() 產生 instance

  • 透過 Handler 處理 response body

    BodyHandlers.ofByteArray
    BodyHandlers.ofString
    BodyHandlers.ofFile
    BodyHandlers.discarding
    BodyHandlers.replacing
    BodyHandlers.ofLines
    BodyHandlers.fromLineSubscriber
    
    // jdk 11 以前
    HttpResponse<String> response = client.send(request, HttpResponse.BodyHandler.asString());
    
    // 新的 jdk
    HttpResponse<String> response = client.send(request, BodyHandlers.ofString());
  • Proxy

    可定義 proxy

    HttpResponse<String> response = HttpClient
      .newBuilder()
      .proxy(ProxySelector.getDefault())
      .build()
      .send(request, BodyHandlers.ofString());
  • Direct Policy

    如果 reponse 收到 3XX 的 redirect 結果,可設定 redirect policy 直接轉址

    HttpResponse<String> response = HttpClient.newBuilder()
      .followRedirects(HttpClient.Redirect.ALWAYS)
      .build()
      .send(request, BodyHandlers.ofString());
  • HTTP Authentication

    HttpResponse<String> response = HttpClient.newBuilder()
      .authenticator(new Authenticator() {
        @Override
        protected PasswordAuthentication getPasswordAuthentication() {
          return new PasswordAuthentication(
            "username", 
            "password".toCharArray());
        }
    }).build()
      .send(request, BodyHandlers.ofString());
  • Cookie

    // 透過 cookieHandler(CookieHandler cookieHandler)  定義 CookieHandler
    // 設定不接受 cookie
    HttpClient.newBuilder()
      .cookieHandler(new CookieManager(null, CookiePolicy.ACCEPT_NONE))
      .build();
    
    // 取得 CookieStore
    ((CookieManager) httpClient.cookieHandler().get()).getCookieStore()
  • SSL Context

    在 HttpClient 指定 SSL Context,忽略 ssl key 檢查

    private static SSLContext disabledSSLContext() throws KeyManagementException, NoSuchAlgorithmException {
            SSLContext sslContext = SSLContext.getInstance("TLS");
            // https://docs.oracle.com/en/java/javase/11/docs/specs/security/standard-names.html#sslcontext-algorithms
            sslContext.init(
                    null,
                new TrustManager[] {
                    new X509TrustManager() {
                        public X509Certificate[] getAcceptedIssuers() {
                            return null;
                        }
    
                        public void checkClientTrusted(X509Certificate[] certs, String authType) {
                        }
    
                        public void checkServerTrusted(X509Certificate[] certs, String authType) {
                        }
                    }
                },
                new SecureRandom()
            );
            return sslContext;
        }

Sync or Async 同步或是非同步呼叫

HttpClient 有同步或非同步的發送 request 的方式

  • send 同步

    程式會停在這邊,等待 response 或是 timeout,下一行,可直接取得 response body

    HttpResponse<String> response = HttpClient.newBuilder()
      .build()
      .send(request, BodyHandlers.ofString());
  • sendAsync 非同步 non-blocking

    CompletableFuture<HttpResponse<String>> response = HttpClient.newBuilder()
      .build()
      .sendAsync(request, HttpResponse.BodyHandlers.ofString());
  • 可指定 Executor 限制 threads

    預設是使用 java.util.concurrent.Executors.newCachedThreadPool()

    ExecutorService executorService = Executors.newFixedThreadPool(2);
    
    CompletableFuture<HttpResponse<String>> response1 = HttpClient.newBuilder()
      .executor(executorService)
      .build()
      .sendAsync(request, HttpResponse.BodyHandlers.ofString());
    
    CompletableFuture<HttpResponse<String>> response2 = HttpClient.newBuilder()
      .executor(executorService)
      .build()
      .sendAsync(request, HttpResponse.BodyHandlers.ofString());

HttpResponse

  • URI

    HttpResponse 也有一個 uri() method,可取得 uri,因為有時候會遇到 redirect uri 的回應,因此 response 的 uri,會取得 redirect 後的網址

  • Response Header

    取得 response header list

    HttpResponse<String> response = HttpClient.newHttpClient()
      .send(request, HttpResponse.BodyHandlers.ofString());
    HttpHeaders responseHeaders = response.headers();
  • Http Version

    server 是以哪一個 http version 回應的

    response.version();
  • Response Body

    String body = response.body();

完整 Java Code

import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
import java.io.ByteArrayInputStream;
import java.io.FileNotFoundException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.http.HttpClient;
import java.net.http.HttpHeaders;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.file.Paths;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.cert.X509Certificate;
import java.time.Duration;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;

public class HttpClientTest {

    public static void main(String[] args) throws Exception {

        // 建立HttpClient實例
        HttpClient httpClient = HttpClient.newBuilder()
                .version(HttpClient.Version.HTTP_1_1) // http 1.1
                .connectTimeout(Duration.ofSeconds(5)) // timeout after 5 seconds
                .sslContext(disabledSSLContext()) // disable SSL verify
                .build();

        // 建立 HttpRequest請求
//        HttpRequest request = getMethod();
//        HttpRequest request = postNoBody();
        HttpRequest request = postStringBody();
//        HttpRequest request = postInputStreamBody();
//        HttpRequest request = postByteArrayBody();
//        HttpRequest request = postFileBody();

        // 錯誤的 URI 測試 Timeout
//        HttpRequest request = postStringBody_InvalidUri();

        // Sync 同步呼叫
//        sync(httpClient, request);
        // 非同步呼叫
        async(httpClient, request);

    }

    private static void async(HttpClient httpClient, HttpRequest request) throws InterruptedException, java.util.concurrent.ExecutionException, java.util.concurrent.TimeoutException {
        // 非同步
        CompletableFuture<HttpResponse<String>> response = httpClient
                .sendAsync(request, HttpResponse.BodyHandlers.ofString());
        String result = response
                .thenApply(HttpResponse::body)
                .exceptionally(t -> {
                    t.printStackTrace();
                    return "fallback";
                })
                .get(10, TimeUnit.SECONDS);
        System.out.println(result);
    }

    private static void sync(HttpClient httpClient, HttpRequest request) throws java.io.IOException, InterruptedException {
        // 發送請求並接收回應
        HttpResponse<String> response = httpClient.send(
                request, HttpResponse.BodyHandlers.ofString());

        HttpClient.Version version = response.version();
        System.out.println("---response version---");
        System.out.println(version);

        System.out.println("---response headers---");
        HttpHeaders responseHeaders = response.headers();
        Map<String, List<String>> responseHeadersMap = responseHeaders.map();
        for (String key : responseHeadersMap.keySet()) {
            System.out.println(key + ":" + responseHeadersMap.get(key));
        }

        // 取得回應主體內容
        String body = response.body();
        System.out.println("---response body---");
        System.out.println(body);
    }

    private static HttpRequest getMethod() {
        // 臺灣證券交易所0056個股日成交資訊API
        String url = "https://www.twse.com.tw/exchangeReport/STOCK_DAY?response=json&date=20230531&stockNo=0056";

        // 建立 HttpRequest請求  get method
        HttpRequest request = HttpRequest.newBuilder()
                .uri(URI.create(url))
                .version(HttpClient.Version.HTTP_2)
                .header("cache-control", "no-cache")
                .GET()
                .build();
        return request;
    }

    private static HttpRequest postNoBody() throws URISyntaxException {
        HttpRequest request = HttpRequest.newBuilder()
                .uri(new URI("https://postman-echo.com/post"))
                .POST(HttpRequest.BodyPublishers.noBody())
                .build();
        return request;
    }

    private static HttpRequest postStringBody() throws URISyntaxException {
        HttpRequest request = HttpRequest.newBuilder()
                .uri(new URI("https://postman-echo.com/post"))
                .headers("Content-Type", "text/plain;charset=UTF-8")
                .POST(HttpRequest.BodyPublishers.ofString("Sample request body"))
                .build();
        return request;
    }

    private static HttpRequest postInputStreamBody() throws URISyntaxException {
        byte[] sampleData = "Sample request body".getBytes();
        HttpRequest request = HttpRequest.newBuilder()
                .uri(new URI("https://postman-echo.com/post"))
                .headers("Content-Type", "text/plain;charset=UTF-8")
                .POST(HttpRequest.BodyPublishers
                        .ofInputStream(() -> new ByteArrayInputStream(sampleData)))
                .build();
        return request;
    }

    private static HttpRequest postByteArrayBody() throws URISyntaxException {
        byte[] sampleData = "Sample request body".getBytes();
        HttpRequest request = HttpRequest.newBuilder()
                .uri(new URI("https://postman-echo.com/post"))
                .headers("Content-Type", "text/plain;charset=UTF-8")
                .POST(HttpRequest.BodyPublishers.ofByteArray(sampleData))
                .build();
        return request;
    }

    private static HttpRequest postFileBody() throws URISyntaxException, FileNotFoundException {
        HttpRequest request = HttpRequest.newBuilder()
                .uri(new URI("https://postman-echo.com/post"))
                .headers("Content-Type", "text/plain;charset=UTF-8")
                .POST(HttpRequest.BodyPublishers.ofFile(
                        Paths.get("sample.txt")))
                .build();
        return request;
    }

    private static HttpRequest postStringBody_InvalidUri() throws URISyntaxException {
        HttpRequest request = HttpRequest.newBuilder()
                .uri(new URI("https://test.com/post"))
                .headers("Content-Type", "text/plain;charset=UTF-8")
                .POST(HttpRequest.BodyPublishers.ofString("Sample request body"))
                .build();
        return request;
    }

    private static SSLContext disabledSSLContext() throws KeyManagementException, NoSuchAlgorithmException {
        SSLContext sslContext = SSLContext.getInstance("TLS");
        // https://docs.oracle.com/en/java/javase/11/docs/specs/security/standard-names.html#sslcontext-algorithms
        sslContext.init(
                null,
            new TrustManager[] {
                new X509TrustManager() {
                    public X509Certificate[] getAcceptedIssuers() {
                        return null;
                    }

                    public void checkClientTrusted(X509Certificate[] certs, String authType) {
                    }

                    public void checkServerTrusted(X509Certificate[] certs, String authType) {
                    }
                }
            },
            new SecureRandom()
        );
        return sslContext;
    }

}

References

菜鳥工程師 肉豬: Java 11 HttpClient發送請求範例

Exploring the New HTTP Client in Java | Baeldung