Swift 並行編程現狀和展望 - async/await 和參與者模式

来源:https://www.cnblogs.com/flutter-cn/archive/2019/07/21/11222634.html
-Advertisement-
Play Games

Swift 並行編程現狀和展望 async/await 和參與者模式 這篇文章不是針對當前版本 Swift 3 的,而是對預計於 2018 年發佈的 Swift 5 的一些特性的猜想。如果兩年後我還記得這篇文章,可能會回來更新一波。在此之前,請當作一篇對現代語言並行編程特性的不太嚴謹科普文來看待。 ...


Swift 並行編程現狀和展望 - async/await 和參與者模式

這篇文章不是針對當前版本 Swift 3 的,而是對預計於 2018 年發佈的 Swift 5 的一些特性的猜想。如果兩年後我還記得這篇文章,可能會回來更新一波。在此之前,請當作一篇對現代語言並行編程特性的不太嚴謹科普文來看待。

CPU 速度已經很多年沒有大的突破了,硬體行業更多地將重點放在多核心技術上,而與之對應,軟體中並行編程的概念也越來越重要。如何利用多核心 CPU,以及擁有密集計算單元的 GPU,來進行快速的處理和計算,是很多開發者十分感興趣的事情。在今年年初 Swift 4 的展望中,Swift 項目的負責人 Chris Lattern 表示可能並不會這麼快提供語言層級的並行編程支持,不過最近 Chris 又在 IBM 的一次關於編譯器的分享中明確提到,有很大可能會在 Swift 5 中添加語言級別的並行特性。

這對 Swift 生態是一個好消息,也是一個大消息。不過這其實並不是什麼新鮮的事情,甚至可以說是一門現代語言發展的必經路徑和必備特性。因為 Objective-C/Swift 現在缺乏這方面的內容,所以很多專註於 iOS 的開發者對並行編程會很陌生。我在這篇文章里結合 Swift 現狀簡單介紹了一些這門語言里並行編程可能的使用方式,希望能幫助大家初窺門徑。(雖然我自己也還摸不到門徑在何方…)

Swift 現有的並行模型

Swift 現在沒有語言層面的並行機制,不過我們確實有一些基於庫的線程調度的方案,來進行並行操作。

基於閉包的線程調度

雖然恍如隔世,不過 GCD (Grand Central Dispatch) 確實是從 iOS 4 才開始走進我們的視野的。在 GCD 和 block 被加入之前,我們想要新開一個線程需要用到 NSThread 或者 NSOperation,然後使用 delegate 的方式來接收回調。這種書寫方式太過古老,也相當麻煩,容易出錯。GCD 為我們帶來了一套很簡單的 API,可以讓我們線上程中進行調度。在很長一段時間里,這套 API 成為了 iOS 中多線程編程的主流方式。Swift 繼承了這套 API,並且在 Swift 3 中將它們重新導入為了更符合 Swift 語法習慣的形式。現在我們可以將一個操作很容易地派發到後臺進行,首先創建一個後臺隊列,然後調用 async 並傳入需要執行的閉包即可:

let backgroundQueue = DispatchQueue(label: "com.onevcat.concurrency.backgroundQueue")
backgroundQueue.async {
    let result = 1 + 2
}

在 async 的閉包中,我們還可以繼續進行派發,最常見的用法就是開一個後臺線程進行耗時操作 (從網路獲取數據,或者 I/O 等),然後在數據準備完成後,回到主線程更新 UI:

let backgroundQueue = DispatchQueue(label: "com.onevcat.concurrency.backgroundQueue")
backgroundQueue.async {
    let url = URL(string: "https://api.onevcat.com/users/onevcat")!
    guard let data = try? Data(contentsOf: url) else { return }

    let user = User(data: data)
    DispatchQueue.main.async {
        self.userView.nameLabel.text = user.name
        // ...
    }
}

當然,現在估計已經不會有人再這麼做網路請求了。我們可以使用專門的 URLSession 來進行訪問。URLSession 和對應的 dataTask 會將網路請求派發到後臺線程,我們不再需要顯式對其指定。不過更新 UI 的工作還是需要回到主線程:

let url = URL(string: "https://api.onevcat.com/users/onevcat")!
URLSession.shared.dataTask(with: url) { (data, res, err) in
    guard let data = try? Data(contentsOf: url) else {
        return
    }
    let user = User(data: data)
    DispatchQueue.main.async {
        self.userView.nameLabel.text = user.name
        // ...
    }
}.resume()

回調地獄

基於閉包模型的方式,不論是直接派發還是通過 URLSession 的封裝進行操作,都面臨一個嚴重的問題。這個問題最早在 JavaScript 中臭名昭著,那就是回調地獄 (callback hell)。

試想一下我們如果有一系列需要依次進行的網路操作:先進行登錄,然後使用返回的 token 獲取用戶信息,接下來通過用戶 ID 獲取好友列表,最後對某個好友點贊。使用傳統的閉包方式,這段代碼會是這樣:

LoginRequest(userName: "onevcat", password: "123").send() { token, err in
    if let token = token {
        UserProfileRequest(token: token).send() { user, err in
            if let user = user {
                GetFriendListRequest(user: user).send() { friends, err in
                    if let friends = friends {
                        LikeFriendRequest(target: friends.first).send() { result, err in
                            if let result = result, result {
                                print("Success")
                                self.updateUI()
                            }
                        } else {
                            print("Error: \(err)")
                        }
                    } else {
                        print("Error: \(err)")                    
                    }
                }
            } else {
                print("Error: \(err)")
            }
        }
    } else {
        print("Error: \(err)")
    }
}

這已經是使用了尾隨閉包特性簡化後的代碼了,如果使用完整的閉包形式的話,你會看到一大堆 }) 堆疊起來。else路徑上幾乎不可能確定對應關係,而對於成功的代碼路徑來說,你也需要很多額外的精力來理解這些代碼。一旦這種基於閉包的回調太多,並嵌套起來,閱讀它們的時候就好似身陷地獄。

image

不幸的是,在 Cocoa 框架中我們似乎對此沒太多好辦法。不過我們確實有很多方法來解決回調地獄的問題,其中最成功的應該是 Promise 或者 Future 的方案。

Promise/Future

在深入 Promise 或 Future 之前,我們先來將上面的回調做一些整理。可以看到,所有的請求在回調時都包含了兩個輸入值,一個是像 tokenuser 這樣我們接下來會使用到的結果,另一個是代表錯誤的 err。我們可以創建一個泛型類型來代表它們:

enum Result<T> {
    case success(T)
    case failure(Error)
}

重構 send 方法接收的回調類型後,上面的 API 調用就可以變為:

LoginRequest(userName: "onevcat", password: "123").send() { result in
    switch result {
    case .success(let token):
        UserProfileRequest(token: token).send() { result in
            switch result {
            case .success(let user):
               // ...
            case .failure(let error):
                print("Error: \(error)")
            }
        }
    case .failure(let error):
        print("Error: \(error)")
    }
}

看起來並沒有什麼改善,對麽?我們只不過使用一堆 ({}) 的地獄換成了 switch...case 的地獄。但是,我們如果將 request 包裝一下,情況就會完全不同。

struct Promise<T> {
    init(resolvers: (_ fulfill: @escaping (T) -> Void, _ reject: @escaping (Error) -> Void) -> Void) {
        //...
        // 存儲 fulfill 和 reject。
        // 當 fulfill 被調用時解析為 then;當 reject 被調用時解析為 error。
    }

    // 存儲的 then 方法,調用者提供的參數閉包將在 fulfill 時調用
    func then<U>(_ body: (T) -> U) -> Promise<U> {
        return Promise<U>{
            //...
        }
    }

    // 調用者提供該方法,參數閉包當 reject 時調用
    func `catch`<Error>(_ body: (Error) -> Void) {
        //...
    }
}

extension Request {
    var promise: Promise<Response> {
        return Promise<Response> { fulfill, reject in
            self.send() { result in
                switch result {
                case .success(let r): fulfill(r)
                case .failure(let e): reject(e)
                }
            }
        }
    }
}

我們這裡沒有給出 Promise 的具體實現,而只是給出了概念性的說明。Promise 是一個泛型類型,它的初始化方法接受一個以 fulfill 和 reject 作為參數的函數作為參數 (一開始這可能有點拗口,你可以結合代碼再讀一次)。這個類型里還提供了 then 和 catch 方法,then 方法的參數是另一個閉包,在 fulfill 被調用時,我們可以執行這個閉包,並返回新的 Promise (之後會看到具體的使用例子):而在 reject 被調用時,通過 catch 方法中斷這個過程。

在接下來的 Request 的擴展中,我們定義了一個返回 Promise 的計算屬性,它將初始化一個內容類型為 Response 的 Promise (這裡的 Response 是定義在 Request 協議中的代表該請求對應的響應的類型,想瞭解更多相關的內容,可以看看我之前的一篇使用面向協議編程的文章)。我們在 .success 時調用 fulfill,在 .failure 時調用 reject

現在,上面的回調地獄可以用 then 和 catch 的形式進行展平了:

LoginRequest(userName: "onevcat", password: "123").promise
 .then { token in
    return UserProfileRequest(token: token).promise
}.then { user in
    return GetFriendListRequest(user: user).promise
}.then { friends in
    return LikeFriendRequest(target: friends.first).promise
}.then { _ in
    print("Succeed!")
    self.updateUI()
    // 我們這裡還需要在 Promise 中添加一個無返回的 then 的重載
    // 篇幅有限,略過
    // ...
}.catch { error in
    print("Error: \(error)")
}

Promise 本質上就是一個對閉包或者說 Result 類型的封裝,它將未來可能的結果所對應的閉包先存儲起來,然後當確實得到結果 (比如網路請求返回) 的時候,再執行對應的閉包。通過使用 then,我們可以避免閉包的重疊嵌套,而是使用調用鏈的方式將非同步操作串接起來。Future 和 Promise 其實是同樣思想的不同命名,兩者基本指代的是一件事兒。在 Swift 中,有一些封裝得很好的第三方庫,可以讓我們以這樣的方式來書寫代碼,PromiseKit 和 BrightFutures 就是其中的佼佼者,它們確實能幫助避免回調地獄的問題,讓嵌套的非同步代碼變得整潔。

image

async/await,“串列”模式的非同步編程

雖然 Promise/Future 的方式能解決一部分問題,但是我們看看上面的代碼,依然有不少問題。

  1. 我們用了很多並不直觀的操作,對於每個 request,我們都生成了額外的 Promise,並用 then 串聯。這些其實都是模板代碼,應該可以被更好地解決。
  2. 各個 then 閉包中的值只在自己固定的作用域中有效,這有時候很不方便。比如如果我們的 LikeFriend 請求需要同時發送當前用戶的 token 的話,我們只能在最外層添加臨時變數來持有這些結果:

     var myToken: String = ""
     LoginRequest(userName: "onevcat", password: "123").promise
      .then { token in
         myToken = token
         return UserProfileRequest(token: token).promise
     } //...
     .then {
         print("Token is \(myToken)")
         // ...
     }
    
  3. Swift內建的 throw 的錯誤處理方式並不能很好地和這裡的 Result 和 catch { error in ... } 的方式合作。Swift throw 是一種同步的錯誤處理方式,如果想要在非同步世界中使用這種的話,會顯得格格不入。語法上有不少理解的困難,代碼也會迅速變得十分醜陋。

如果從語言層面著手的話,這些問題都是可以被解決的。如果對微軟技術棧有所關心的同學應該知道,早在 2012 年 C# 5.0 發佈時,就包含了一個讓業界驚為天人的特性,那就是 async 和 await 關鍵字。這兩個關鍵字可以讓我們用類似同步的書寫方式來寫非同步代碼,這讓思維模型變得十分簡單。Swift 5 中有望引入類似的語法結構,如果我們有 async/await,我們上面的例子將會變成這樣的形式:

@IBAction func bunttonPressed(_ sender: Any?) {
    // 1
    doSomething()
    print("Button Pressed")
}

// 2
async func doSomething() {
    print("Doing something...")
    do {
        // 3
        let token   = await LoginRequest(userName: "onevcat", password: "123").sendAsync()
        let user    = await UserProfileRequest(token: token).sendAsync()
        let friends = await GetFriendListRequest(user: user).sendAsync()
        let result  = await LikeFriendRequest(target: friends.first).sendAsync()
        print("Finished")

        // 4
        updateUI()
    } catch ... {
        // 5
        //...
    }
}

extension Request {
    // 6
    async func sendAsync() -> Response {
        let dataTask = ...
        let data = await dataTask.resumeAsync()
        return Response.parse(data: data)
    }
}

註意,以上代碼是根據現在 Swift 語法,對如果存在 async 和 await 時語言的形式的推測。雖然這不代表今後 Swift 中非同步編程模型就是這樣,或者說 async 和 await 就是這樣使用,但是應該代表了一個被其他語言驗證過的可行方向。

按照註釋的編號,進行一些簡單的說明:

  1. 這就是我們通常的 @IBAction,點擊後執行 doSomething
  2. doSomething 被 async 關鍵字修飾,表示這是一個非同步方法。async 關鍵字所做的事情只有一件,那就是允許在這個方法內使用 await 關鍵字來等待一個長時間操作完成。在這個方法里的語句將被以同步方式執行,直到遇到第一個 await。控制台將會列印 “Doing something…“。
  3. 遇到的第一個 await。此時這個 doSomething 方法將進入等待狀態,該方法將會“返回”,也即離開棧域。接下來 bunttonPressed 中 doSomething 調用之後的語句將被執行,控制台列印 “Button Pressed”。
  4. tokenuserfriends 和 result 將被依次 await 執行,直到獲得最終結果,併進行 updateUI
  5. 理論上 await 關鍵字在語義上應該包含 throws,所以我們需要將它們包裹在 do...catch 中,而且可以使用 Swift 內建的異常處理機制來對請求操作中發生的錯誤進行捕獲和處理。換句話說,我們如果對錯誤不感興趣,也可以使用類似 try? 和 try! 的
  6. 對於 Request,我們需要添加 async 版本的發送請求的方法。dataTask 的 resumeAsync 方法是在 Foundation 中針對內建非同步編程所重寫的版本。我們在此等待它的結果,然後將結果解析為 model 後返回。

我們上面已經說過,可以將 Promise 看作是對 Result 的封裝,而這裡我們依然可以類比進行理解,將 async 看作是對 Promise 的封裝。對於 sendAsync 方法,我們完全可以將它理解返回 Promise,只不過配合 await,這個 Promise 將直接以同步的方式被解包為結果。(或者說,await 是這樣一個關鍵字,它可以等待 Promise 完成,並獲取它的結果。)

func sendAsync() throws -> Promise<Response> {
   // ...
}

// await request.sendAsync()
// doABC()

// 等價於

(try request.sendAsync()).then {
    // doABC()
}

不僅在網路請求中可以使用,對於所有的 I/O 操作,Cocoa 應當也會提供一套對應的非同步 API。甚至於對於等待用戶操作和輸入,或者等待某個動畫的結束,都是可以使用 async/await 的潛在場景。如果你對響應式編程有所瞭解的話,不難發現,其實響應式編程想要解決的就是非同步代碼難以維護的問題,而在使用 async/await 後,部分的非同步代碼可以變為以同步形式書寫,這會讓代碼書寫起來簡單很多。

Swift 的 async 和 await 很可能將會是基於 Coroutine 進行實現的。不過也有可能和 C# 類似,編譯器通過將 async和 await 的代碼編譯為帶有狀態機的片段,併進行調度。Swift 5 的預計發佈時間會是 2018 年底,所以現在談論這些技術細節可能還為時過早。

參與者 (actor) 模型

講了半天 async 和 await,它們所要解決的是非同步編程的問題。而從非同步編程到並行編程,我們還需要一步,那就是將多個非同步操作組織起來同時進行。當然,我們可以簡單地同時調用多個 async 方法來進行並行運算,或者是使用某些像是 GCD 里 group 之類的特殊語法來將複數個 async 打包放在一起進行調用。但是不論何種方式,都會面臨一個問題,那就是這套方式使用的是命令式 (imperative) 的語法,而非描述性的 (declarative),這將導致擴展起來相對困難。

並行編程相對複雜,而且與人類天生的思考方式相違背,所以我們希望儘可能讓並行編程的模型保持簡單,同時避免直接與線程或者調度這類事務打交道。基於這些考慮,Swift 很可能會參考 Erlang 和 AKKA 中已經很成功的參與者模型 (actor model) 的方式實現並行編程,這樣開發者將可以使用預設的分散式方式和描述性的語言來進行並行任務。

所謂參與者,是一種程式上的抽象概念,它被視為併發運算的基本單元。參與者能做的事情就是接收消息,並且基於收到的消息做某種運算。這和麵向對象的想法有相似之處,一個對象也接收消息 (或者說,接受方法調用),並且根據消息 (被調用的方法) 作出響應。它們之間最大的不同在於,參與者之間永遠相互隔離,它們不會共用某塊記憶體。一個參與者中的狀態永遠是私有的,它不能被另一個參與者改變。

和麵向對象世界中“萬物皆對象”的思想相同,參與者模式里,所有的東西也都是參與者。單個的參與者能力十分有限,不過我們可以創建一個參與者的“管理者”,或者叫做 actor system,它在接收到特定消息時可以創建新的參與者,並向它們發送消息。這些新的參與者將實際負責運算或者操作,在接到消息後根據自身的內部狀態進行工作。在 Swift 5 中,可能會用下麵的方式來定義一個參與者:

// 1
struct Message {
    let target: String
}

// 2
actor NetworkRequestHandler {
    var localState: UserID
    async func processRequest(connection: Connection) {
       // ...
       // 在這裡你可以 await 一個耗時操作
       // 並改變 `localState` 或者向 system 發消息
    }

    // 3
    message {
        Message(let m): processRequest(connection: Connection(m.target))
    }
}

// 4
let system = ActorSystem(identifier: "MySystem")
let actor = system.actorOf<NetworkRequestHandler>()
actor.tell(Message(target: "https://onevcat.com"))

再次註意,這些代碼只是對 Swift 5 中可能出現的參與者模式的一種猜想。最後的實現肯定會和這有所區別。不過如果 Swift 中要加入參與者,應該會和這裡的表述類似。

  1. 這裡的 Message 是我們定義的消息類型。
  2. 使用 actor 關鍵字來定義一個參與者模型,它其中包含了內部狀態和非同步操作,以及一個隱式的操作隊列。
  3. 定義了這個 actor 需要接收的消息和需要作出的響應。
  4. 創建了一個 actor system (ActorSystem 這裡沒有給出實現,可能會包含在 Swift 標準庫中)。然後創建了一個 NetworkRequestHandler 參與者,並向它發送一條消息。

這個參與者封裝了一個非同步方法以及一個內部狀態,另外,因為該參與者會使用一個自己的 DispatchQueue 以避免和其他線程共用狀態。通過 actor system 進行創建,併在接收到某個消息後執行非同步的運算方法,我們就可以很容易地寫出並行處理的代碼,而不必關心它們的內部狀態和調度問題了。現在,你可以通過 ActorSystem 來創建很多參與者,然後發送不同消息給它們,併進行各自的操作。並行編程變得前所未有的簡單。

參與者模式相比於傳統的自己調度有兩個顯著的優點:

首先,因為參與者之間的通訊是消息發送,這意味著並行運算不必被局限在一個進程里,甚至不必局限在一臺設備里。只要保證消息能夠被髮送 (比如使用 IPC 或者 DMA),你就完全可以使用分散式的方式,使用多種設備 (多臺電腦,或者多個 GPU) 進行並行操作,這帶來的是無限可能的擴展性。

另外,由於參與者之間可以發送消息,那些操作發生異常的參與者有機會通知 system 自己的狀態,而 actor system 也可以根據這個狀態來重置這些出問題的參與者,或者甚至是無視它們並創建新的參與者繼續任務。這使得整個參與者系統擁有“自愈”的能力,在傳統並行編程中想要處理這件事情是非常困難的,而參與者模型的系統得益於此,可以最大限度保障系統的穩定性。

這些東西有什麼用

兩年下來,Swift已經證明瞭自己是一門非常優秀的 app 語言。即使 Xcode 每日虐我千百遍,但是現在讓我回去寫 Objective-C 的話,我從內心是絕對抗拒的。Swift 的野心不僅於此,從 Swift 的開源和進化方向,我們很容易看出這門語言希望在伺服器端也有所建樹。而內建的非同步支持以及參與者模式的並行編程,無疑會為 Swift 在伺服器端的運用添加厚重的砝碼。非同步模型對寫 app 也會有所幫助,更簡化的控制流程以及隱藏起來的線程切換,會讓我們寫出更加簡明優雅的代碼。

C# 的 async/await 曾經為開發者們帶來一股清流,Elixir 或者說 Erlang 可以說是世界上最優秀的並行編程語言,JVM 上的 AKKA 也正在支撐著無數的億級服務。我很好奇當 Swift 遇到這一切的時候,它們之間的化學反應會迸發出怎樣的火花。雖然每天還在 Swift 3 的世界中掙扎,但是我想我的心已經飛躍到 Swift 5 的並行世界中去了。


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 07.21自我總結 pymysql模塊 一.創建連接庫 二.建立游標 三.提交sql語句 四.查看內容 五.移動游標 相對位置 cursor.scroll(1, "relative") cursor.scroll() 預設是相對位置 絕對位置 cursor.scroll(0, "absolute") ...
  • "分散式架構" "CAP 與 BASE 理論" "一致性協議" "初識 Zookeeper" "Zookeeper 介紹" "Zookeeper 工作機制" "Zookeeper 特點" "Zookeeper 數據結構" "Zookeeper 應用場景" "統一命名服務" "統一配置管理" "統一集 ...
  • Scala當中什麼是Transformation和 Action,以及它們倆的區別是什麼? ...
  • 07.21自我總結 資料庫用戶管理 一.用戶創建語法 語法: create user 用戶名@"ip地址" "identified" by 密碼; 舉例: create user tom@"192.168.101" identified by "123"; 創建除本機以外其他ip第能登入 creat ...
  • 一、複合查詢 1、在ElasticSearch中,有Query和Filter兩種不同的Context。Query Context進行了相關性算分,Filter Context不需要進行算分,同時可以利用Cache,獲取更好的性能。 2、bool Query:一個布爾查詢,是一個或者多個查詢子句的組合 ...
  • 本文參考自:YuniKorn 中文官網:http://www.yunikorn.cn/ 一、YuniKorn 簡介 YuniKorn 是一種輕量級的通用資源調度程式,適用於容器編排系統。它的創建是為了一方面在大規模,多租戶環境中有效地實現各種工作負載的細粒度資源共用,另一方面可以動態地創建雲原生環境 ...
  • 在Flutter中,所有的顯示都是Widget,Widget是一切的基礎,我們可以通過修改數據,再用setState設置數據(調用setState()來通知框架,框架會再次調用State的構建方法來更新用戶界面),Flutter會自動通過綁定的數據更新Widget,所以你需要做的就是實現Widget... ...
  • Swift實戰技巧 從OC轉戰到Swift,差別還是蠻大的,本文記錄了我再從 "OC" 轉到Swift開發過程中遇到的一些問題,然後把我遇到的這些問題記錄形成文章,大體上是一些Swift語言下麵的一些技巧,希望對有需要的人有幫助 OC調用方法的處理 給OC調用的方法需要添加 標記,一般的action ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...