Spark RPC框架源碼分析(二)RPC運行時序

来源:https://www.cnblogs.com/listenfwind/archive/2019/02/25/10434380.html
-Advertisement-
Play Games

Spark RPC 框架的運行時序是怎樣的呢?讓我們深入到它的源碼裡面去看看~~ ...


前情提要:

一. Spark RPC概述

上一篇我們已經說明瞭Spark RPC框架的一個簡單例子,Spark RPC相關的兩個編程模型,Actor模型和Reactor模型以及一些常用的類。這一篇我們還是用上一篇的例子,從代碼的角度講述Spark RPC的運行時序,從而揭露Spark RPC框架的運行原理。我們主要將分成兩部分來講,分別從服務端的角度和客戶端的角度深度解析。

不過源碼解析部分都是比較枯燥的,Spark RPC這裡也是一樣,其中很多東西都是繞來繞去,牆裂建議使用上一篇中介紹到的那個Spark RPC項目,下載下來並運行,通過斷點的方式來一步一步看,結合本篇文章,你應該會有更大的收穫。

PS:所用spark版本:spark2.1.0

二. Spark RPC服務端

我們將以上一篇HelloworldServer為線索,深入到Spark RPC框架內部的源碼中,來看看啟動一個服務時都做了些什麼。

因為代碼部分都是比較繞的,每個類也經常會搞不清楚,我在介紹一個方法的源碼時,通常都會將類名也一併寫出來,這樣應該會更加清晰一些。

HelloworldServer{
  ......
  def main(args: Array[String]): Unit = {
    //val host = args(0)
    val host = "localhost"
    val config = RpcEnvServerConfig(new RpcConf(), "hello-server", host, 52345)
    val rpcEnv: RpcEnv = NettyRpcEnvFactory.create(config)
    val helloEndpoint: RpcEndpoint = new HelloEndpoint(rpcEnv)
    rpcEnv.setupEndpoint("hello-service", helloEndpoint)
    rpcEnv.awaitTermination()
  }
  ......
}

Spark RPC服務端運行主要時許

這段代碼中有兩個主要流程,我們分別來說

2.1 服務端NettyRpcEnvFactory.create(config)

首先是下麵這條代碼的運行流程:

val rpcEnv: RpcEnv = NettyRpcEnvFactory.create(config)

其實就是通過 NettyRpcEnvFactory 創建出一個 RPC Environment ,其具體類是 NettyRpcEnv 。

我們再來看看創建過程中會發生什麼。

object NettyRpcEnvFactory extends RpcEnvFactory {
    ......
    def create(config: RpcEnvConfig): RpcEnv = {
        val conf = config.conf
    
        // Use JavaSerializerInstance in multiple threads is safe. However, if we plan to support
        // KryoSerializer in future, we have to use ThreadLocal to store SerializerInstance
        val javaSerializerInstance =
        new JavaSerializer(conf).newInstance().asInstanceOf[JavaSerializerInstance]
        //根據配置以及地址,new 一個 NettyRpcEnv ,
        val nettyEnv =
        new NettyRpcEnv(conf, javaSerializerInstance, config.bindAddress)
        //如果是服務端創建的,那麼會啟動服務。服務端和客戶端都會通過這個方法創建一個 NettyRpcEnv ,但區別就在這裡了。
        if (!config.clientMode) {
        val startNettyRpcEnv: Int => (NettyRpcEnv, Int) = { actualPort =>
            //啟動服務的方法,下一步就是調用這個方法了
            nettyEnv.startServer(config.bindAddress, actualPort)
            (nettyEnv, nettyEnv.address.port)
        }
        try {
            Utils.startServiceOnPort(config.port, startNettyRpcEnv, conf, config.name)._1
        } catch {
            case NonFatal(e) =>
            nettyEnv.shutdown()
            throw e
        }
        }
        nettyEnv
    }
    ......
}

還沒完,如果是服務端調用這段代碼,那麼主要的功能是創建RPCEnv,即NettyRpcEnv(客戶端在後面說)。以及通過下麵這行代碼,

nettyEnv.startServer(config.bindAddress, actualPort)

去調用相應的方法啟動服務端的服務。下麵進入到這個方法中去看看。

class NettyRpcEnv(
                   val conf: RpcConf,
                   javaSerializerInstance: JavaSerializerInstance,
                   host: String) extends RpcEnv(conf) {
  ......
  def startServer(bindAddress: String, port: Int): Unit = {
    // here disable security
    val bootstraps: java.util.List[TransportServerBootstrap] = java.util.Collections.emptyList()
    //TransportContext 屬於 spark.network 中的部分,負責 RPC 消息在網路中的傳輸
    server = transportContext.createServer(bindAddress, port, bootstraps)
    //在每個 RpcEndpoint 註冊的時候都會註冊一個預設的 RpcEndpointVerifier,它的作用是客戶端調用的時候先用它來詢問 Endpoint 是否存在。
    dispatcher.registerRpcEndpoint(
      RpcEndpointVerifier.NAME, new RpcEndpointVerifier(this, dispatcher))
  }
  ......
}

執行完畢之後這個create方法就結束。這個流程主要就是開啟一些服務,然後返回一個新的NettyRpcEnv。

2.2 服務端rpcEnv.setupEndpoint("hello-service",helloEndpoint)

這條代碼會去調用NettyRpcEnv中相應的方法

class NettyRpcEnv(
                   val conf: RpcConf,
                   javaSerializerInstance: JavaSerializerInstance,
                   host: String) extends RpcEnv(conf) {
  ......
  override def setupEndpoint(name: String, endpoint: RpcEndpoint): RpcEndpointRef = {
    dispatcher.registerRpcEndpoint(name, endpoint)
  }
  ......
}

我們看到,這個方法主要是調用dispatcher進行註冊的。dispatcher的功能上一節已經說了,

Dispatcher的主要作用是保存註冊的RpcEndpoint、分發相應的Message到RpcEndPoint中進行處理。Dispatcher即是上圖中ThreadPool的角色。它同時也維繫一個threadpool,用來處理每次接受到的 InboxMessage。而這裡處理InboxMessage是通過inbox實現的。

這裡我們就說一說dispatcher的流程。

dispatcher

dispatcher在NettyRpcEnv被創建的時候創建出來。

class NettyRpcEnv(
                   val conf: RpcConf,
                   javaSerializerInstance: JavaSerializerInstance,
                   host: String) extends RpcEnv(conf) {
    ......
    //初始化時創建 dispatcher
    private val dispatcher: Dispatcher = new Dispatcher(this)
    ......
}

dispatcher類被創建的時候也有幾個屬性需要註意:

private[netty] class Dispatcher(nettyEnv: NettyRpcEnv) {
    ......
    //每個 RpcEndpoint 其實都會被整合成一個 EndpointData 。並且每個 RpcEndpoint 都會有一個 inbox。
    private class EndpointData(
                                val name: String,
                                val endpoint: RpcEndpoint,
                                val ref: NettyRpcEndpointRef) {
        val inbox = new Inbox(ref, endpoint)
    }
    
    //一個阻塞隊列,當有 RpcEndpoint 相關請求(InboxMessage)的時候,就會將請求塞到這個隊列中,然後被線程池處理。
    private val receivers = new LinkedBlockingQueue[EndpointData]
    
    //初始化便創建出來的線程池,當上面的 receivers 隊列中沒內容時,會阻塞。當有 RpcEndpoint 相關請求(即 InboxMessage )的時候就會立刻執行。
    //這裡處理 InboxMessage 本質上是調用相應 RpcEndpoint 的 inbox 去處理。
    private val threadpool: ThreadPoolExecutor = {
        val numThreads = nettyEnv.conf.getInt("spark.rpc.netty.dispatcher.numThreads",
        math.max(2, Runtime.getRuntime.availableProcessors()))
        val pool = ThreadUtils.newDaemonFixedThreadPool(numThreads, "dispatcher-event-loop")
        for (i <- 0 until numThreads) {
            pool.execute(new MessageLoop)
        }
        pool
    }
    ......
}

瞭解一些Dispatcher的邏輯流程後,我們來正式看看Dispatcher的registerRpcEndpoint方法。

顧名思義,這個方法就是將RpcEndpoint註冊到Dispatcher中去。當有Message到來的時候,便會分發Message到相應的RpcEndPoint中進行處理。

private[netty] class Dispatcher(nettyEnv: NettyRpcEnv) {
  ......
  def registerRpcEndpoint(name: String, endpoint: RpcEndpoint): NettyRpcEndpointRef = {
    val addr = RpcEndpointAddress(nettyEnv.address, name)
    //註冊 RpcEndpoint 時需要的是 上面的 EndpointData ,其中就包含 endpointRef ,這個主要是供客戶端使用的。
    val endpointRef = new NettyRpcEndpointRef(nettyEnv.conf, addr, nettyEnv)
    //多線程環境下,註冊一個 RpcEndpoint 需要判斷現在是否處於 stop 狀態。
    synchronized {
      if (stopped) {
        throw new IllegalStateException("RpcEnv has been stopped")
      }
      //新建 EndpointData 並存儲到一個 ConcurrentMap 中。
      if (endpoints.putIfAbsent(name, new EndpointData(name, endpoint, endpointRef)) != null) {
        throw new IllegalArgumentException(s"There is already an RpcEndpoint called $name")
      }
      val data = endpoints.get(name)
      endpointRefs.put(data.endpoint, data.ref)
      //將 這個 EndpointData 加入到 receivers 隊列中,此時 dispatcher 中的 threadpool 會去處理這個加進來的 EndpointData 
      //處理過程是調用它的 inbox 的 process()方法。然後 inbox 會等待消息到來。
      receivers.offer(data) // for the OnStart message
    }
    endpointRef
  }
  ......
}

Spark RPC服務端邏輯小結:我們說明瞭Spark RPC服務端啟動的邏輯流程,分為兩個部分,第一個是RPC env,即NettyRpcEnv的創建過程,第二個則是RpcEndpoint註冊到dispatcher的流程。
1. NettyRpcEnvFactory 創建 NettyRpcEnv

  • 根據地址創建NettyRpcEnv。
  • NettyRpcEnv開始啟動服務,包括TransportContext根據地址開啟監聽服務,向Dispacther註冊一個RpcEndpointVerifier等待。

2. Dispatcher註冊RpcEndpoint

  • Dispatcher初始化時便創建一個線程池並阻塞等待receivers隊列中加入新的EndpointData
  • 一旦新加入EndpointData便會調用該EndpointData的inbox去處理消息。比如OnStart消息,或是RPCMessage等等。

三.Spark RPC客戶端

依舊是以上一節 HelloWorld 的客戶端為線索,我們來逐層深入在 RPC 中,客戶端 HelloworldClient 的 asyncCall() 方法。

object HelloworldClient {
  ......
  def asyncCall() = {
    val rpcConf = new RpcConf()
    val config = RpcEnvClientConfig(rpcConf, "hello-client")
    val rpcEnv: RpcEnv = NettyRpcEnvFactory.create(config)
    val endPointRef: RpcEndpointRef = rpcEnv.setupEndpointRef(RpcAddress("localhost", 52345), "hello-service")
    val future: Future[String] = endPointRef.ask[String](SayHi("neo"))
    future.onComplete {
      case scala.util.Success(value) => println(s"Got the result = $value")
      case scala.util.Failure(e) => println(s"Got error: $e")
    }
    Await.result(future, Duration.apply("30s"))
    rpcEnv.shutdown()
  }
  ......
}

Spark RPC客戶端時許

創建Spark RPC客戶端Env(即NettyRpcEnvFactory)部分和Spark RPC服務端是一樣的,只是不會開啟監聽服務,這裡就不詳細展開。

我們從這一句開始看,這也是Spark RPC客戶端和服務端區別的地方所在。

val endPointRef: RpcEndpointRef = rpcEnv.setupEndpointRef(RpcAddress("localhost", 52345), "hello-service")

setupEndpointRef()

上面的的setupEndpointRef最終會去調用下麵setupEndpointRef()這個方法,這個方法中又進行一次跳轉,跳轉去setupEndpointRefByURI這個方法中。需要註意的是這兩個方法都是RpcEnv裡面的,而RpcEnv是抽象類,它裡面只實現部分方法,而NettyRpcEnv繼承了它,實現了全部方法。

abstract class RpcEnv(conf: RpcConf) {
  ......
  def setupEndpointRef(address: RpcAddress, endpointName: String): RpcEndpointRef = {
    //會跳轉去調用下麵的方法
    setupEndpointRefByURI(RpcEndpointAddress(address, endpointName).toString)
  }
  
  def setupEndpointRefByURI(uri: String): RpcEndpointRef = {
    //其中 asyncSetupEndpointRefByURI() 返回的是 Future[RpcEndpointRef]。 這裡就是阻塞,等待返回一個 RpcEndpointRef。
    // defaultLookupTimeout.awaitResult 底層調用 Await.result 阻塞 直到結果返回或返回異常
    defaultLookupTimeout.awaitResult(asyncSetupEndpointRefByURI(uri))
  }
  ......
}  

這裡最主要的代碼其實就一句,

defaultLookupTimeout.awaitResult(asyncSetupEndpointRefByURI(uri))

這一段可以分為兩部分,第一部分的defaultLookupTimeout.awaitResult其實底層是調用Await.result阻塞等待一個非同步操作,直到結果返回。

而asyncSetupEndpointRefByURI(uri)則是根據給定的uri去返回一個RpcEndpointRef,它是在NettyRpcEnv中實現的:

class NettyRpcEnv(
                   val conf: RpcConf,
                   javaSerializerInstance: JavaSerializerInstance,
                   host: String) extends RpcEnv(conf) {
  ......
  def asyncSetupEndpointRefByURI(uri: String): Future[RpcEndpointRef] = {
    //獲取地址
    val addr = RpcEndpointAddress(uri)
    //根據地址等信息新建一個 NettyRpcEndpointRef 。
    val RpcendpointRef = new NettyRpcEndpointRef(conf, addr, this) 
    //每個新建的 RpcendpointRef 都有先有一個對應的verifier 去檢查服務端存不存在對應的 Rpcendpoint 。  
    val verifier = new NettyRpcEndpointRef(
      conf, RpcEndpointAddress(addr.rpcAddress, RpcEndpointVerifier.NAME), this)
    //向服務端發送請求判斷是否存在對應的 Rpcendpoint。
    verifier.ask[Boolean](RpcEndpointVerifier.createCheckExistence(endpointRef.name)).flatMap { find =>
      if (find) {
        Future.successful(endpointRef)
      } else {
        Future.failed(new RpcEndpointNotFoundException(uri))
      }
    }(ThreadUtils.sameThread)
  }
  ......
}
  

asyncSetupEndpointRefByURI()這個方法實現兩個功能,第一個就是新建一個RpcEndpointRef。第二個是新建一個verifier,這個verifier的作用就是先給服務端發送一個請求判斷是否存在RpcEndpointRef對應的RpcEndpoint。

這段代碼中最重要的就是verifiter.ask[Boolean](...)了。如果有找到之後就會調用Future.successful這個方法,反之則會通過Future.failed拋出一個異常。

ask可以算是比較核心的一個方法,我們可以到ask方法中去看看。

class NettyRpcEnv{
    ......
    private[netty] def ask[T: ClassTag](message: RequestMessage, timeout: RpcTimeout): Future[T] = {
      val promise = Promise[Any]()
      val remoteAddr = message.receiver.address
      //
      def onFailure(e: Throwable): Unit = {
  //      println("555");
        if (!promise.tryFailure(e)) {
          log.warn(s"Ignored failure: $e")
        }
      }
  
      def onSuccess(reply: Any): Unit = reply match {
        case RpcFailure(e) => onFailure(e)
        case rpcReply =>
          println("666");
          if (!promise.trySuccess(rpcReply)) {
            log.warn(s"Ignored message: $reply")
          }
      }
  
      try {
        if (remoteAddr == address) {
          val p = Promise[Any]()
          p.future.onComplete {
            case Success(response) => onSuccess(response)
            case Failure(e) => onFailure(e)
          }(ThreadUtils.sameThread)
          dispatcher.postLocalMessage(message, p)
        } else {
          //跳轉到這裡執行
          //封裝一個 RpcOutboxMessage ,同時 onSuccess 方法也是在這裡註冊的。
          val rpcMessage = RpcOutboxMessage(serialize(message),
            onFailure,
            (client, response) => onSuccess(deserialize[Any](client, response)))
          postToOutbox(message.receiver, rpcMessage)
          promise.future.onFailure {
            case _: TimeoutException =>  println("111");rpcMessage.onTimeout()
  //          case _ => println("222");
          }(ThreadUtils.sameThread)
        }
        
        val timeoutCancelable = timeoutScheduler.schedule(new Runnable {
          override def run(): Unit = {
  //          println("333");
            onFailure(new TimeoutException(s"Cannot receive any reply in ${timeout.duration}"))
          }
        }, timeout.duration.toNanos, TimeUnit.NANOSECONDS)
        //promise 對應的 future onComplete時會去調用,但當 successful 的時候,上面的 run 並不會被調用。
        promise.future.onComplete { v =>
  //        println("4444");
          timeoutCancelable.cancel(true)
        }(ThreadUtils.sameThread)
  
      } catch {
        case NonFatal(e) =>
          onFailure(e)
      }
  
      promise.future.mapTo[T].recover(timeout.addMessageIfTimeout)(ThreadUtils.sameThread)
    }
    ......
}

這裡涉及到使用一些scala多線程的高級用法,包括Promise和Future。如果想要對這些有更加深入的瞭解,可以參考這篇文章

這個函數的作用從名字中就可以看得出,其實就是將要發送的消息封裝成一個RpcOutboxMessage,然後交給OutBox去發送,OutBox和前面所說的InBox對應,對應Actor模型中的MailBox(信箱)。用於發送和接收消息。

其中使用到了Future和Promise進行非同步併發以及錯誤處理,比如當發送時間超時的時候Promise就會返回一個TimeoutException,而我們就可以設置自己的onFailure函數去處理這些異常。

OK,註冊完RpcEndpointRef後我們便可以用它來向服務端發送消息了,而其實RpcEndpointRef發送消息還是調用ask方法,就是上面的那個ask方法。上面也有介紹,本質上就是通過OutBox進行處理。

我們來梳理一下RPC的客戶端的發送流程。

客戶端邏輯小結:客戶端和服務端比較類似,都是需要創建一個NettyRpcEnv。不同的是接下來客戶端創建的是RpcEndpointRef,並用之向服務端對應的RpcEndpoint發送消息。

1.NettyRpcEnvFactory創建NettyRpcEnv

  • 根據地址創建NettyRpcEnv。根據地址開啟監聽服務,向Dispacther註冊一個RpcEndpointVerifier等待。

2. 創建RpcEndpointRef

  • 創建一個新的RpcEndpointRef
  • 創建對應的verifier,使用verifier向服務端發送請求,判斷對應的RpcEndpoint是否存在。若存在,返回該RpcEndpointRef,否則拋出異常。

3. RpcEndpointRef使用同步或者非同步的方式發送請求。

OK,以上就是SparkRPC時序的源碼分析。下一篇會將一個實際的例子,Spark的心跳機制和代碼。喜歡的話就關註一波吧


推薦閱讀 :
從分治演算法到 MapReduce
Actor併發編程模型淺析
大數據存儲的進化史 --從 RAID 到 Hadoop Hdfs
一個故事告訴你什麼才是好的程式員


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

-Advertisement-
Play Games
更多相關文章
  • Java基本數據類型 java的基本數據類型可以分為4類8種 布爾型(boolean):true,false 整數類型:byte、short、int、long 浮點數類型:float、double 字元型:char 定義變數時:long,float類型後面要加上字母 8種數據類型的大小 一個漢字占2 ...
  • pythn print格式化輸出。 %r 用來做 debug 比較好,因為它會顯示變數的原始數據(raw data),而其它的符號則是用來向用戶顯示輸出的。 1. 列印字元串 print ("His name is %s"%("Aviad"))效果: 2.列印整數 print ("He is %d ...
  • [TOC] 一、為什麼要重載賦值運算符 ​ 在前面的內容中講解 "拷貝構造函數調用的時機" 時說明瞭初始化和賦值的區別:在定義的同時進行賦值叫做 ,定義完成以後再賦值(不管在定義的時候有沒有賦值)就叫做 。初始化只能有一次,賦值可以有多次。 ​ 當以拷貝的方式初始化一個對象時,會調用拷貝構造函數;當 ...
  • 今天真的是累哭了,周一課從早八點半一直上到晚九點半,整個人要虛脫的感覺,因為時間不太夠鴨所以就回頭看看找了一些比較有知識點的題來總結總結分析一下,明天有空了就開始繼續打題,嘻嘻嘻。 今日興趣電影: 《超能查派》 這是一部關於未來人工智慧的一個故事,感覺特別有思維開拓性,一個程式員寫出了真正的AI智能 ...
  • 代碼基本結構 url.py: views.py: 說明: 1)在authenticate方法的返回值是一個元組,元組中第一個元素是用戶名,第二個元素是認證數據token。這個返回值會在我們的視圖類中通 過request.user 和 request.auth獲取到。具體為什麼是這兩個值,會在後面的源 ...
  • [TOC] 近期廣泛閱讀券商關於 巨集觀高頻數據 的研報,發現了兩點不足: 就研究手段而言,比較粗放,普遍停留在僅僅比較數據相關係數的層面; 就理論高度而言,很少探討數據背後的因果關聯。 不過有些理念先進的券商團隊已經開始從 產業鏈傳導 的角度試圖細緻的描述數據間的關聯,這正好契合了下麵這篇文章的核心 ...
  • 「HW面試題」 【題目】 給定一個整數數組,如何快速地求出該數組中第k小的數。假如數組為[4,0,1,0,2,3],那麼第三小的元素是1 【題目分析】 這道題涉及整數列表排序問題,直接使用sort方法按照ASCII碼排序即可 【解答】 1 #!/Users/minutesheep/.pyenv/sh ...
  • 第5章 字元串及正則表達式 5.1 字元串常用操作 在Python開發過程中,為了實現某項功能,經常需要對某些字元串進行特殊處理,如拼接字元串、截取字元串、格式化字元串等。下麵將對Python中常用的字元串操作方法進行介紹。 5.1.1 拼接字元串 使用“+” 運算符可完成對多個字元串的拼接,“+” ...
一周排行
    -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.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...