分布式散列表(DHT)及具体实现Kademlia(kad)/Chord
分布式散列列表也稱為分布式哈希表,英文distributed hash table,簡稱 DHT。
分布式散列列表在概念上類似與傳統(tǒng)的散列列表,差異在于傳統(tǒng)的散列列表主要是?用于單機。
分布式散列列表主要是?用于分布式系統(tǒng)(此時,分布式系統(tǒng)的節(jié)點可以通俗理理解為散列列表中的 bucket),分布式散列列表主要是?用來存儲?大量量的(甚?至是海?量量的)數據。在實際使?用場景中,直接對所存儲的“每?一個業(yè)務數據”計算散列列值,然后用散列列值作為 key,業(yè)務數據本身是 value。
分布式散列列表(DHT)的難點:
1、無中心導致的難點
DHT的誕生,是為了解決之前P2P技術的缺陷。其中一個缺陷是中央服務器導致的單點故障。
因此 DHT 就不能再依靠中央服務器。而沒有了中央服務器,就需要提供一系列機制來實現節(jié)點之間的通訊。
2、海量數據導致的難點
? ? ? ?DHT的很多使用場景是為了承載海量數據(PB 或更高級別)。
由于數據是海量的,每個節(jié)點只能存儲(整個系統(tǒng)的)一小部分數據。需要把數據均勻分攤到每個節(jié)點。
3、節(jié)點動態(tài)變化導致的難點
很多DHT的使用場景是在公網(互聯網)上,參與DHT的節(jié)點(主機)會出現頻繁變化,每時每刻都有新的節(jié)點上線,也
會有舊的節(jié)點下線。在這種情況下,需要確保數據依然是均勻分攤到所有節(jié)點。
(特別強調一下:傳統(tǒng)的散列表在這種情況下的困難)
因為傳統(tǒng)散列表在針對 key 計算出散列值之后,需要用散列值和桶數進行某種運算(比如:取模運算),從而得到桶的編號。
如果桶的數量出現變化,就會影響到上述取模運算的結果,然后導致數據錯亂。
4、高效查詢導致的難點
對于節(jié)點數很多的分布式系統(tǒng),如何快速定位節(jié)點,同時又不消耗太多網絡資源,這也是一個挑戰(zhàn)。
? ? ? ?DHT 必須有更高效的查找機制。而且這種查找機制要能適應節(jié)點動態(tài)變化這個特點。
分布式散列表(DHT)解決上述難點
DHT 采用如下一些機制來解決上述問題,并滿足分布式系統(tǒng)比較苛刻的需求。
1、散列算法的選擇
前面提到: DHT 通常是直接拿業(yè)務數據的散列值作為 key,業(yè)務數據本身作為 value。
考慮到 DHT 需要承載的數據量通常比較大,散列函數產生的散列值范圍(keyspace)要足夠大,以防止太多的碰撞。更進一步,如果 keyspace大到一定程度,使得隨機碰撞的概率小到忽略不計,就有助于簡化 DHT 的系統(tǒng)設計。
通常的 DHT 都會采用大于等于128比特的散列值。
2、同構的node ID與data key
? ?DHT屬于分布式系統(tǒng)的一種。既然是分布式系統(tǒng),意味著存在多個節(jié)點。在設計分布式系統(tǒng)的時候,一種常見的做法是:給每一個節(jié)點(node)分配唯一的ID。
很多 DHT 的設計會讓node ID采用跟data key同構的散列值。這么搞的好處是:
? ? 2.1、當散列值空間足夠大的時候,隨機碰撞忽略不計,因此也就確保了node ID 的唯一性
? ? 2.2、可以簡化系統(tǒng)設計——比如簡化路由算法(下面會提及)
3、拓撲結構的設計
作為分布式系統(tǒng), DHT 必然要定義某種拓撲結構;有了拓撲結構,自然就要設計某種路由算法。
如果某個DHT 采用前面所說的node ID與data key同構,那么很自然的就會引入Key-based routing。
當某個分布式系統(tǒng)具有自己的拓撲結構,它本身成為一個Overlay網絡(Overlay Network)。所謂的Overlay網絡,通俗地
說就是網絡之上的網絡。對于大部分 DHT 而言,它們是基于互聯網之上的覆蓋網絡,它們的數據通訊是依賴下層的互聯網來實現
的。
前面提到的node ID,其解耦的作用就體現在分布式系統(tǒng)在設計拓撲結構和路由算法時,只需要考慮 node ID,而不用考慮其下層網絡的屬性(比如:協(xié)議類型、 IP 地址、端?口號)。
4、路由算法的權衡
由于DHT中的節(jié)點數可能非常多(比如:幾十萬、幾百萬),而且這些節(jié)點是動態(tài)變化的。因此就不可能讓每一個節(jié)點都記錄所有其它節(jié)點的信息。實際情況是:每個節(jié)點通常只知道少數一些節(jié)點的信息。
這時候就需要設計某種路由算法,盡可能利用已知的節(jié)點來轉發(fā)數據。 路由算法很重要,直接決定了DHT 的速度和資源消耗。
在確定了路由算法之后,還需要做一個兩難的權衡路由表的大小。路由表越大,可以實現越短(跳數越少)的路由;缺點是:(由于節(jié)點動態(tài)變化)路由表的維護成本也就越高。路由表數越小,其維護成本越小;缺點是:路由就會變長(跳數變多)。
5、距離算法
某些 DHT 系統(tǒng)還會定義一種距離算法,用來計算: 節(jié)點之間的距離、 數據之間的距離、 節(jié)點與數據的距離。
請注意:此處所說的距離屬于邏輯層面,對應的是 DHT 自己的拓撲結構;它與地理位置無關,也與互聯網的拓撲結構無關。
這里就能明白為什么前面要強調node ID與data key同構。當這兩者同構,就可以用同一種距離算法;反之,如果這兩者不同構,多半要引入幾種不同的距離算法。
6、數據定位
對 DHT 而言,這是最關鍵的。DHT 與傳統(tǒng)的散列表在功能上是類似的。說白了,他們最關鍵的功能只有兩個保存數據和獲取數據。
保存數據
(大致原理,具體的協(xié)議實現可能會有差異)
當某個節(jié)點得到了新加入的數據(K/V),它會先計算自己與新數據的 key 之間的距離;然后再計算它所知道的其它節(jié)點與這個key 的距離。
如果計算下來,自己與 key 的距離最小,那么這個數據就保持在自己這里。
否則的話,把這個數據轉發(fā)給距離最小的節(jié)點。
收到數據的另一個節(jié)點,也采用上述過程進行處理(遞歸處理)。
獲取數據
(大致原理,具體的協(xié)議實現可能會有差異)
當某個節(jié)點接收到查詢數據的請求(key),它會先計算自己與 key 之間的距離;然后再計算它所知道的其它節(jié)點與這個 key 的距離。
如果計算下來,自己與 key 的距離最小,那么就在自己這里找有沒有 key 對應的 value。有的話就返回 value,沒有的話就報錯。
否則的話,把這個數據轉發(fā)給距離最小的節(jié)點。
收到數據的另一個節(jié)點,也采用上述過程進行處理(遞歸處理)。
Chord 協(xié)議
Chord 誕生于2001年。第一批 DHT 協(xié)議都是在那年涌現的,另外幾個是: CAN、 Tapestry、 Pastry。
拓撲結構——環(huán)形
Chord 的拓撲,必然要提到Consistent Hashing(譯作一致散列)。搞明白一致散列也就知道 Chord的拓撲設計了。
一致散列主要是為了解決節(jié)點動態(tài)變化這個難點。為了解決這個難點, 一致散列把散列值空間(keyspace)構成一個環(huán)。對于 m 比特的散列值,其范圍是 [0, 2m-1]。你把這個區(qū)間頭尾相接就變成一個環(huán),其周長是 2m。然后對這個環(huán)規(guī)定了一個移動方向(比如順時針)。
如果 node ID 和 data key 是同構的,那么這兩者都可以映射到這個環(huán)上(對應于環(huán)上的某點)。
? ? ? ? ? ? ? ? ? ? ? ? ? ? ????
?假設有某個節(jié)點A,距離它最近的是節(jié)點B(以順時針方向衡量距離)。那么稱 B 是 A 的繼任(successor), A 是 B 的前任(predecessor)。
數據隸屬于【距離最小】的節(jié)點。以 m=6 的環(huán)形空間為例:
? ? 數據區(qū)間 [5,8] 隸屬于節(jié)點8
? ? 數據區(qū)間 [9,15] 隸屬于節(jié)點15
? ? ......
? ? 數據區(qū)間 [59,4] 隸屬于節(jié)點4(注: 6比特的環(huán)形空間, 63之后是0)
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ??
?路由機制
接下來簡單說?一下路由的玩法。
基本路由(簡單遍歷)
當收到請求(key),先看 key 是否在自己這里。如果在自己這里,就直接返回信息;否則就把 key 轉發(fā)給自己的繼任者。以此類推。
這種玩法的時間復雜度是 O(N)。對于一個節(jié)點數很多的 DHT 網絡,這種做法顯然非常低效。
高級路由(Finger Table)
由于基本路由非常低效,自然就引入更高級的玩法——基于“Finger Table”的路由。
? ? Finger Table是一個列表,最多包含 m 項(m 就是散列值的比特數),每一項都是節(jié)點 ID。
假設當前節(jié)點的 ID 是 n,那么表中第 i 項的值是: successor( (n + 2i) mod 2m )
當收到請求(key),就到Finger Table中找到最大的且不超過 key的那一項,然后把 key 轉發(fā)給這一項對應的節(jié)點。有了Finger Table之后,時間復雜度可以優(yōu)化為 O(log N)。
?節(jié)點的加入
1任何一個新來的節(jié)點(假設叫 A),需要先跟 DHT 中已有的任一節(jié)點(假設叫 B)建立連接。
2 A 隨機生成一個散列值作為自己的 ID(對于足夠大的散列值空間, ID 相同的概率忽略不計)
3 A 通過跟 B 進行查詢,找到自己這個 ID 在環(huán)上的接頭人。也就是——找到自己這個 ID 對應的“繼任”(假設叫 C)與“前任”(假設叫 D)
4 接下來, A 需要跟 C 和 D 進行一系列互動,使得自己成為 C 的前任,以及 D 的繼任。
這個互動過程,大致類似于在雙向鏈表當中插入元素。
節(jié)點的正常退出
如果某個節(jié)點想要主動離開這個 DHT 網絡,按照約定需要作一些善后的處理工作。比如說,通知自己的前任去更新其繼任者......
這些善后處理,大致類似于在雙向鏈表中刪除元素。
節(jié)點的異常退出
作為一個分布式系統(tǒng),任何節(jié)點都有可能意外下線(也就是說,來不及進行善后就掛掉了)
假設 節(jié)點A 的繼任者異常下線了,那么 節(jié)點A 就抓瞎了。
為了保險起見, Chord 引入了一個繼任者候選列表”的概念。每個節(jié)點都用這個列表來包含:距離自己最近的 N 個節(jié)點的信息,順序是由近到遠。一旦自己的繼任者下線了,就在列表中找到一個距離最近且在線的節(jié)點,作為新的繼任者。然后 節(jié)點A 更新該列表,確保依然有 N 個候選。更新完繼任者候選列表后,節(jié)點A 也會通知自己的前任,那么 A 的前任也就能更新自己的繼任者候選列表。
Kademlia(Kad)協(xié)議?
Kad 的原理比Chord 稍微晦澀一些。之所以選 Kad 來介紹,是因為實際應用的 DHT大部分都采用 Kad 及其變種。
拓撲結構二叉樹
Kad 也采用了node ID 與 data key同構的設計思路。然后 Kad 采用某種算法把 key 映射到一個二叉樹,每一個 key 都是這個二叉樹的葉子。
在映射之前,先做一下預處理。
1. 先把 key 以二進制形式表示。
2. 把每一個 key 縮短為它的最短唯一前綴。
為什么要搞“最短唯一前綴”?
Kad 使用 160 比特的散列算法(比如 SHA1),完整的 key 用二進制表示有 160 個數位。
首先,實際運行的 Kad 網絡,即使有幾百萬個節(jié)點,相比 keyspace(2160)也只是很小的一個子集。
其次,由于散列函數的特點, key 的分布是高度隨機的。因此也是高度離散的——任何兩個 key 都不會非常臨近。
所以,使用最短唯一前綴來處理 key 的二進制形式,得到的結果就會很短(遠遠小于 160 個數位)。
散列值的映射
完成上述的預處理后,接下來的映射規(guī)則是:
1. 先把 key 以二進制形式表示,然后從高位到低位依次處理。
2. 二進制的第 n 個數位就對應了二叉樹的第 n 層
3. 如果該位是1,進入左子樹,是0則進入右子樹(這只是人為約定,反過來處理也可以)
4. 全部數位都處理完后,這個 key 就對應了二叉樹上的某個葉子
距離算法——異或(XOR)
接下來要聊的是 Kad 最精妙之處——采用 XOR(按比特異或操作)算法計算 key 之間的距離。
這種搞法使得它具備了類似于幾何距離的某些特性(下面用 ⊕ 表示 XOR)
路由機制
二叉樹的拆分
對每一個節(jié)點,都可以按照自己的視角對整個二叉樹進行拆分。
拆分的規(guī)則是:先從根節(jié)點開始,把不包含自己的那個子樹拆分出來;然后在剩下的子樹再拆分不包含自己的下一層子樹;以此類推,直到最后只剩下自己。
Kad 默認的散列值空間是 m=160(散列值有 160 比特),因此拆分出來的子樹最多有 160 個(考慮到實際的節(jié)點數遠遠小于2160,子樹的個數會明顯小于160)。
對于每一個節(jié)點而言,當它以自己的視角完成子樹拆分后,會得到 n 個子樹;對于每個子樹,如果它都能知道里面的一個節(jié)點,那么它就可以利用這 n個節(jié)點進行遞歸路由,從而到達整個二叉樹的任何一個節(jié)點(考慮到篇幅,具體的數學證明就不貼出來了)
K-桶(K-bucket)
前面說了,每個節(jié)點在完成子樹拆分后,只需要知道每個子樹里面的一個節(jié)點,就足以實現全遍歷。但是考慮到健壯性(分布式系統(tǒng)的節(jié)點是動態(tài)變化的),光知道一個是不夠滴,需要知道多個才比較保險。
所以 Kad 論?文中給出了一個K-桶(K-bucket)的概念。也就是說:每個節(jié)點在完成子樹拆分后,要記錄每個子樹里面的 K 個節(jié)點。這里所說的 K 值是一個系統(tǒng)級的常量。由使用 Kad 的軟件系統(tǒng)自己設定(比如 BT 下載使用的 Kad 網絡, K 設定為 8)。
K 桶其實就是路由表。對于某個節(jié)點而言,如果以它為視角拆分了n 個子樹,那么它就需要維護 n 個路由表,并且每個路由表的上限是 K。
說 K 只是一個上限,是因為有兩種情況使得 K 桶的尺寸會小于 K。
1. 距離越近的子樹就越小。如果整個子樹可能存在的節(jié)點數小于 K,那么該子樹的 K 桶尺寸永遠也不可能達到 K。
2. 有些子樹雖然實際上線的節(jié)點數超過 K,但是因為種種原因,沒有收集到該子樹足夠多的節(jié)點,這也會使得該子樹的 K 桶尺寸小于K。
應用實例
1、文件的存儲及查找
原來收藏在圖書館里,按索引號碼得整整齊齊的書,以一種什么樣的方式分發(fā)到同學們手里呢?大致的原則,包括:1)書本能夠比較均衡地分布在同學們的手里,不會出現部分同學手里書特別多、而大部分同學連一本書都沒有的情況;2)同學想找一本特定的書的時候,能夠一種相對簡單的索引方式找到這本書。
Kademlia作了下面這種安排:
假設《分布式算法》這本書的書名的hash值是?00010000,那么這本書就會被要求存在學號為00010000的同學手上。(這要求hash算法的值域與node ID的值域一致。Kademlia的Node ID是160位2進制。這里的示例對Node ID進行了簡略)
但還得考慮到會有同學缺勤。萬一00010000今天沒來上學(節(jié)點沒有上線或徹底退出網絡),那《分布式算法》這本書豈不是誰都拿不到了?那算法要求這本書不能只存在一個同學手上,而是被要求同時存儲在學號最接近00010000的k位同學手上,即00010001、00010010、00010011…等同學手上都會有這本書。
同樣地,當你需要找《分布式算法》這本書時,將書名hash一下,得到?00010000,這個便是索書號,你就知道該找哪(幾)位同學了。剩下的問題,就是找到這(幾)位同學的手機號。
2、節(jié)點的異或距離
由于你手上只有一部分同學的通訊錄,你很可能并沒有00010000的手機號(IP地址)。那如何聯系上目標同學呢?
一個可行的思路就是在你的通訊錄里找到一位擁有目標同學的聯系方式的同學。前面提到,每位同學手上的通訊錄都是按距離分層的。算法的設計是,如果一個同學離你越近,你手上的通訊錄里存有ta的手機號碼的概率越大。而算法的核心的思路就可以是:當你知道目標同學Z與你之間的距離,你可以在你的通訊錄上先找到一個你認為與同學Z最相近的同學B,請同學B再進一步去查找同學Z的手機號。
上文提到的距離,是學號(Node ID)之間的異或距離(XOR distance)。異或是針對yes/no或者二進制的運算.
舉2個例子:
01010000與01010010距離(即是2個ID的異或值)為00000010(換算為十進制即為2);
01000000與00000001距離為01000001(換算為十進制即為26+1,即65);
如此類推。
那通訊錄是如何按距離分層呢?下面的示例會告訴你,按異或距離分層,基本上可以理解為按位數分層。設想以下情景:
以0000110為基礎節(jié)點,如果一個節(jié)點的ID,前面所有位數都與它相同,只有最后1位不同,這樣的節(jié)點只有1個——0000111,與基礎節(jié)點的異或值為0000001,即距離為1;對于0000110而言,這樣的節(jié)點歸為“k-bucket 1”;
如果一個節(jié)點的ID,前面所有位數相同,從倒數第2位開始不同,這樣的節(jié)點只有2個:0000101、0000100,與基礎節(jié)點的異或值為0000011和0000010,即距離范圍為3和2;對于0000110而言,這樣的節(jié)點歸為“k-bucket 2”;
……
如果一個節(jié)點的ID,前面所有位數相同,從倒數第n位開始不同,這樣的節(jié)點只有2(i-1)個,與基礎節(jié)點的距離范圍為[2(i-1), 2i);對于0000110而言,這樣的節(jié)點歸為“k-bucket i”;
?
按位數區(qū)分k-bucket
對上面描述的另一種理解方式:如果將整個網絡的節(jié)點梳理為一個按節(jié)點ID排列的二叉樹,樹最末端的每個葉子便是一個節(jié)點,則下圖就比較直觀的展現出,節(jié)點之間的距離的關系。
k-bucket示意圖:右下角的黑色實心圓,為基礎節(jié)點(按wiki百科的配圖修改)
回到我們的類比。每個同學只維護一部分的通訊錄,這個通訊錄按照距離分層(可以理解為按學號與自己的學號從第幾位開始不同而分層),即k-bucket1, k-bucket 2, k-bucket 3…雖然每個k-bucket中實際存在的同學人數逐漸增多,但每個同學在它自己的每個k-bucket中只記錄k位同學的手機號(k個節(jié)點的地址與端口,這里的k是一個可調節(jié)的常量參數)。
由于學號(節(jié)點的ID)有160位,所以每個同學的通訊錄中共分160層(節(jié)點共有160個k-bucket)。整個網絡最多可以容納2^160個同學(節(jié)點),但是每個同學(節(jié)點)最多只維護160 * k 行通訊錄(其他節(jié)點的地址與端口)。
3、節(jié)點定位
我們現在來闡述一個完整的索書流程。
A同學(學號00000110)想找《分布式算法》,A首先需要計算書名的哈希值,hash(《分布式算法》) =?00010000。那么A就知道ta需要找到00010000號同學(命名為Z同學)或學號與Z鄰近的同學。
Z的學號00010000與自己的異或距離為?00010110,距離范圍在[24, 25),所以這個Z同學可能在k-bucket 5中(或者說,Z同學的學號與A同學的學號從第5位開始不同,所以Z同學可能在k-bucket 5中)。
然后A同學看看自己的k-bucket 5有沒有Z同學:
- 如果有,那就直接聯系Z同學要書;
- 如果沒有,在k-bucket 5里隨便找一個B同學(注意任意B同學,它的學號第5位肯定與Z相同,即它與Z同學的距離會小于24,相當于比Z、A之間的距離縮短了一半以上),請求B同學在它自己的通訊錄里按同樣的查找方式找一下Z同學:
-- 如果B知道Z同學,那就把Z同學的手機號(IP Address)告訴A;
-- 如果B也不知道Z同學,那B按同樣的搜索方法,可以在自己的通訊錄里找到一個離Z更近的C同學(Z、C之間距離小于23),把C同學推薦給A;A同學請求C同學進行下一步查找。
查詢方式示意
Kademlia的這種查詢機制,有點像是將一張紙不斷地對折來收縮搜索范圍,保證對于任意n個學生,最多只需要查詢log2(n)次,即可找到獲得目標同學的聯系方式(即在對于任意一個有[2(n?1), 2n)個節(jié)點的網絡,最多只需要n步搜索即可找到目標節(jié)點)。
每次搜索都將距離至少收縮一半
以上便是Kademlia算法的基本原理。以下再簡要介紹協(xié)議中的技術細節(jié)。
4、算法的三個參數:keyspace,k和α
keyspace
-- 即ID有多少位
-- 決定每個節(jié)點的通訊錄有幾層
k
-- 每個一層k-bucket里裝k個node的信息,即
-- 每次查找node時,返回k個node的信息
-- 對于某個特定的data,離其key最近的k個節(jié)點被會要求存儲這個data
α
-- 每次向其他node請求查找某個node時,會向α個node發(fā)出請求
5、節(jié)點的指令
Kademlia算法中,每個節(jié)點只有4個指令
PING
-- 測試一個節(jié)點是否在線
STORE
-- 要求一個節(jié)點存儲一份數據
FIND_NODE
-- 根據節(jié)點ID查找一個節(jié)點
FIND_VALUE
-- 根據KEY查找一個數據,實則上跟FIND_NODE非常類似
6、k-bucket的維護及更新機制
刷新機制大致有如下幾種:
1. 主動收集節(jié)點
任何節(jié)點都可以主動發(fā)起“查詢節(jié)點”的請求(對應于協(xié)議類型 FIND_NODE),從而刷新 K 桶中的節(jié)點信息(下面聊“節(jié)點的加入”時,會提及這種)
2. 被動收集節(jié)點
如果收到其它節(jié)點發(fā)來的請求(協(xié)議類型 FIND_NODE 或 FIND_VALUE),會把對方的 ID 加入自己的某個 K 桶中。
3. 探測失效節(jié)點
? ? Kad 還是支持一種探測機制(協(xié)議類型 PING),可以判斷某個 ID 的節(jié)點是否在線。因此就可以定期探測路由表中的每一個節(jié)點,然后把下線的節(jié)點從路由表中干掉。
7、節(jié)點的加入
7.1
任何一個新來的節(jié)點(假設叫 A),需要先跟 DHT 中已有的任一節(jié)點(假設叫 B)建立連接。
7.2
? ? A 隨機生成一個散列值作為自己的 ID(對于足夠大的散列值空間, ID 相同的概率忽略不計)
7.3
? ? A 向 B 發(fā)起一個查詢請求(協(xié)議類型 FIND_NODE),請求的 ID 是自己(通俗地說,就是查詢自己)
7.4
? ? B 收到該請求之后,(如前面所說)會先把 A 的 ID 加入自己的某個 K 桶中。
然后,根據 FIND_NODE 協(xié)議的約定, B 會找到K個最接近 A 的節(jié)點,并返回給 A。
(B 怎么知道哪些節(jié)點接近 A 捏?這時候,用 XOR 表示距離的算法就發(fā)揮作用啦)
7.5
? ? A 收到這 K 個節(jié)點的 ID 之后,(僅僅根據這批 ID 的值)就可以開始初始化自己的 K 桶。
7.6
然后 A 會繼續(xù)向剛剛拿到的這批節(jié)點發(fā)送查詢請求(協(xié)議類型 FIND_NODE),如此往復(遞歸),直至 A 建立了足夠詳細的路由表。
8、節(jié)點的退出
?? ?與 Chord 不同, Kad 對于節(jié)點退出沒有額外的要求(沒有“主動退出”的說法)。
所以, Kad 的節(jié)點想離開 DHT 網絡不需要作任何操作
總結:
Kad 成為 DHT 的主流實現?方式,這已經是很明顯的事實。
?
總結
以上是生活随笔為你收集整理的分布式散列表(DHT)及具体实现Kademlia(kad)/Chord的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 如何获得通信达交易接口?
- 下一篇: 艾宾浩斯记忆遗忘曲线