當前位置:首頁 » 操作系統 » netty源碼解析

netty源碼解析

發布時間: 2023-09-05 03:56:12

① Netty 源碼解析 ——— ChannelConfig 和 Attribute

嗯,本文與其說是ChannelConfig、Attribute源碼解析,不如說是對ChannelConfig以及Attribute結構層次的分析。因為這才是它們在Netty中使用到的重要之處。

在 Netty 源碼解析 ——— 服務端啟動流程 (下) 中說過,當我們在構建NioServerSocketChannel的時候同時會構建一個NioServerSocketChannelConfig對象賦值給NioServerSocketChannel的成員變數config。

而這一個NioServerSocketChannelConfig是當前NioServerSocketChannel配置屬性的集合。NioServerSocketChannelConfig主要用於對NioServerSocketChannel相關配置的設置(如,網路的相關參數配置),比如,配置Channel是否為非阻塞、配置連接超時時間等等。

NioServerSocketChannelConfig其實是一個ChannelConfig實例。ChannelConfig表示為一個Channel相關的配置屬性的集合。所以NioServerSocketChannelConfig就是針對於NioServerSocketChannel的配置屬性的集合。

ChannelConfig是Channel所需的公共配置屬性的集合,如,setAllocator(設置用於channel分配buffer的分配器)。而不同類型的網路傳輸對應的Channel有它們自己特有的配置,因此可以通過擴展ChannelConfig來補充特有的配置,如,ServerSocketChannelConfig是針對基於TCP連接的服務端ServerSocketChannel相關配置屬性的集合,它補充了針對TCP服務端所需的特有配置的設置setBacklog、setReuseAddress、setReceiveBufferSize。

DefaultChannelConfig作為ChannelConfig的默認實現,對ChannelConfig中的配置提供了默認值。

接下來,我們來看一個設置ChannelConfig的流程:
serverBootstrap.option(ChannelOption.SO_REUSEADDR, true);
我們可以在啟動服務端前通過ServerBootstrap來進行相關配置的設置,該選項配置會在Channel初始化時被獲取並設置到Channel中,最終會調用底層ServerSocket.setReuseAddress方法來完成配置的設置。
ServerBootstrap的init()方法:

首先對option和value進行校驗,其實就是進行非空校驗。
然後判斷對應的是哪個常量屬性,並進行相應屬性的設置。如果傳進來的ChannelOption不是已經設定好的常量屬性,則會列印一條警告級別的日誌,告知這是未知的channel option。
Netty提供ChannelOption的一個主要的功能就是讓特定的變數的值給類型化。因為從』ChannelOption<T> option』和』T value』可以看出,我們屬性的值類型T,是取決於ChannelOption的泛型的,也就屬性值類型是由屬性來決定的。

這里,我們可以看到有個ChannelOption類,它允許以類型安全的方式去配置一個ChannelConfig。支持哪一種ChannelOption取決於ChannelConfig的實際的實現並且也可能取決於它所屬的傳輸層的本質。

可見ChannelOption是一個Consant擴展類,Consant是Netty提供的一個單例類,它能安全去通過』==』來進行比較操作。通過ConstantPool進行管理和創建。
常量由一個id和name組成。id:表示分配給常量的唯一數字;name:表示常量的名字。

如上所說,Constant是由ConstantPool來進行管理和創建的,那麼ConstantPool又是個什麼樣的類了?

首先從constants中get這個name對應的常量,如果不存在則調用newConstant()來構建這個常量tempConstant,然後在調用constants.putIfAbsent方法來實現「如果該name沒有存在對應的常量,則插入,否則返回該name所對應的常量。(這整個的過程都是原子性的)」,因此我們是根據putIfAbsent方法的返回來判斷該name對應的常量是否已經存在於constants中的。如果返回為null,則說明當前創建的tempConstant就為name所對應的常量;否則,將putIfAbsent返回的name已經對應的常量值返回。(注意,因為ConcurrentHashMap不會允許value為null的情況,所以我們可以根據putIfAbsent返回為null則代表該name在此之前並未有對應的常量值)

正如我們前面所說的,這個ConstantPool<ChannelOption<Object>> pool(即,ChannelOption常量池)是ChannelOption的一個私有靜態成員屬性,用於管理和創建ChannelOption。

這些定義好的ChannelOption常量都已經存儲數到ChannelOption的常量池(ConstantPool)中了。

注意,ChannelOption本身並不維護選項值的信息,它只是維護選項名字本身。比如,「public static final ChannelOption<Integer> SO_RCVBUF = valueOf("SO_RCVBUF");」👈這只是維護了「SO_RCVBUF」這個選項名字的信息,同時泛型表示選擇值類型,即「SO_RCVBUF」選項值為Integer。

好了,到目前為止,我們對Netty的ChannelOption的設置以及底層的實現已經分析完了,簡單的來說:Netty在初始化Channel時會構建一個ChannelConfig對象,而ChannelConfig是Channel配置屬性的集合。比如,Netty在初始化NioServerSocketChannel的時候同時會構建一個NioServerSocketChannelConfig對象,並將其賦值給NioServerSocketChannel的成員變數config,而這個config(NioServerSocketChannelConfig)維護了NioServerSocketChannel的所有配置屬性。比如,NioServerSocketChannelConfig提供了setConnectTimeoutMillis方法來設置NioServerSocketChannel連接超時的時間。
同時,程序可以通過ServerBootstrap或Boostrap的option(ChannelOption<T> option, T value)方法來實現配置的設置。這里,我們通過ChannelOption來實現配置的設置,ChannelOption中已經將常用的配置項預定義為了常量供我們直接使用,同時ChannelOption的一個主要的功能就是讓特定的變數的值給類型化。因為從』ChannelOption<T> option』和』T value』可以看出,我們屬性的值類型T,是取決於ChannelOption的泛型的,也就屬性值類型是由屬性來決定的。

一個attribute允許存儲一個值的引用。它可以被自動的更新並且是線程安全的。
其實Attribute就是一個屬性對象,這個屬性的名稱為AttributeKey<T> key,而屬性的值為T value。

我們可以通過程序ServerBootstrap或Boostrap的attr方法來設置一個Channel的屬性,如:
serverBootstrap.attr(AttributeKey.valueOf("userID"), UUID.randomUUID().toString());
當Netty底層初始化Channel的時候,就會將我們設置的attribute給設置到Channel中:

如上面所說,Attribute就是一個屬性對象,這個屬性的名稱為AttributeKey<T> key,而屬性的值為T value。
而AttributeKey也是Constant的一個擴展,因此也有一個ConstantPool來管理和創建,這和ChannelOption是類似的。

Channel類本身繼承了AttributeMap類,而AttributeMap它持有多個Attribute,這些Attribute可以通過AttributeKey來訪問的。所以,才可以通過channel.attr(key).set(value)的方式將屬性設置到channel中了(即,這里的attr方法實際上是AttributeMap介面中的方法)。

AttributeKey、Attribute、AttributeMap間的關系:
AttributeMap相對於一個map,AttributeKey相當於map的key,Attribute是一個持有key(AttributeKey)和value的對象。因此在map中我們可以通過AttributeKey key獲取Attribute,從而獲取Attribute中的value(即,屬性值)。

Q:ChannelHandlerContext和Channel都提供了attr方法,那麼它們設置的屬性作用域有什麼不同了?
A:在Netty 4.1版本之前,它們兩設置的屬性作用域確實存在著不同,但從Netty 4.1版本開始,它們兩設置的屬性的作用域已經完全相同了。

若文章有任何錯誤,望大家不吝指教:)

聖思園《精通並發與Netty》

② Netty核心組件之NioEventLoop(一)

在接下來幾篇文章,我會通過Netty的源碼深入講解NioEventLoop的實現機制。
特別說明:基於4.1.52版本的源碼

先來看下NioEventLoop的類關系圖和重要的屬性,對其有一個整體的感知,便於後面詳細分析。

首先來看NioEventLoop的構造函數

默認情況下,會創建MPSC,即多生產者單消費者的隊列,這里最終會用到JCTools庫,這里不過多介紹,感興趣的可以自己去了解。

如果設置了優化開關(默認優化選項是開啟的),則通過反射的方式從Selector中獲取selectedKeys和publicSelectedKeys,將這兩個成員設置為可寫,通過反射,使用Netty構造的selectedKeySet將原生JDK的selectedKeys替換掉。
我們知道使用Java原生NIO介面時,需要先調Selector的select方法,再調selectedKeys方法才可以獲得有IO事件准備好的SelectionKey集合。這里優化過後,只通過一步select調用,就可以從selectedKeySet獲得需要的SelectionKey集合。
另外,原生Java的SelectionKey集合是一個HashSet,這里優化過後的SelectedSelectionKeySet底層是一個數組,效率更高。

EventLoop的職責可以用下面這張圖形象的表示

下面詳細解析:

每次循環,都會檢測任務隊列和IO事件,如果任務隊列中沒有任務,則直接返回SelectStrategy.SELECT;如果任務隊列中有任務,則會調用非阻塞的 selectNow 檢測有IO事件准備好的Channel數。

nextScheledTaskDeadlineNanos 方法返回下一個將要被執行的定時任務的截止時間

NioEventLoop的定時任務隊列是一個優先順序隊列,隊列中存儲的是ScheledFutureTask對象

通過ScheledFutureTask的 compareTo 方法可以看出,優先順序隊列中的元素是以任務的截止時間來排序的,隊首元素的截止時間最小,當截止時間相同時,以任務ID排序,ID小的排在前面。

當定時任務ScheledFutureTask執行後,會根據 periodNanos 的取值決定是否要將任務重新放回隊列。從netty的注釋可以清晰看到:

看下ScheledFutureTask的 run 方法

當任務的執行時間還未到,則判斷任務是否已經取消,如果已取消則移除任務,否則重新加入隊列。對於只執行一次的任務,執行完了不會再放回隊列。其他的任務,則根據 periodNanos 的類型,重新計算截止時間,重新放回隊列,等待下次調度。
定時任務的優先順序隊列到此介紹完畢,接著看NioEventLoop的 run 方法

在調用 select 之前,再次調用 hasTasks() 判斷從上次調用該方法到目前為止是否有任務加入,多做了一層防護,因為調用 select 時,可能會阻塞,這時,如果任務隊列中有任務就會長時間得不到執行,所以須小心謹慎。
如果任務隊列中還是沒有任務,則會調用 select 方法。在這個方法中會根據入參 deadlineNanos 來選擇調用NIO的哪個select方法:

到這里,可能有人要問了: 在上面的方法中,如果調用了Java NIO的無參的 select 方法,就會進入阻塞,除非檢測到Channel的IO事件,那麼在檢測到IO事件之前,加入到任務隊列中的任務怎麼得到執行呢?

好,你想,在檢測到IO事件之前,可以退出阻塞的方法是什麼?對,調用 wakeup 方法。那麼我們來搜一下NioEventLoop中有調用Selector的 wakeup 方法的地方嗎:

還真搜到了,再看一下這個方法被調用的地方

看到SingleThreadEventExecutor的 execute 方法了嗎,就是說在調 execute 方法,向EventLoop提交任務時,會將EventLoop線程從Java NIO的select阻塞中喚醒。

到這里,NioEventLoop的run方法的職責之一:檢測Channel的IO事件就講解完畢。

至於IO事件的處理以及任務隊列中任務的處理會在後面的文章中解析,敬請期待。

在本文中,對Netty的NioEventLoop進行了深入的解讀,並且詳細講解了它的三大職責之一:檢測Channel的IO事件的機制。
NioEventLoop是Netty最核心的概念,內部運行機制很復雜,在接下來的兩篇文章中會繼續分析。

③ Netty源碼-內存泄漏檢測toLeakAwareBuffer

Netty在實現 ByteBuf 時採用了引用計數法進行 ByteBuf 的回收,使用引用計數法進行回收的 ByteBuf 都擴展了 類,在使用 時需要調用 .retain 方法遞增引用計數器,在使用完畢時則需要調用 .release 方法遞減引用計數器,當計數器為 0 時,會進行 ByteBuf 的回收工作:池化的 ByteBuf 不會進行實際的內存釋放,會將佔用的內存歸還給內存池,非池化的 ByteBuf 則會直接釋放內存(為了敘述簡單,後面釋放內存則指真正釋放內存或者將內存歸還給內存池)。

通過上面的描述可知, ByteBuf 的正確回收依賴 retain 和 release 方法的正確調用,內存提前釋放(即在使用 ByteBuf 時沒有調用 retain 方法,導致提前釋放)應用會報錯,用戶也能及時感知到;但是如果使用完 ByteBuf 忘了調用 release 則會導致內存不能及時得到回收,造成內存泄漏,且內存泄漏用戶無法及時感知,久而久之就會發生OOM。為了解決這種問題,Netty採用了內存泄漏檢測機制,發生內存泄漏時會通過日誌將內存泄漏信息列印出來,報告給用戶。

Netty的內存泄漏檢測使用了 WeakReference ,即弱引用,了解過Java四種引用類型(強、軟、弱、虛)和引用隊列( ReferenceQueue )的讀者知道,弱引用持有的對象會在虛擬機觸發GC時(不管回收之後內存是否夠用)被回收掉,如果使用具有引用隊列參數的構造函數實例化 WeakReference 時,弱引用持有的對象在GC被回收時,弱引用自身會被放入引用隊列。

為了後面能更好的理解Netty內存泄漏檢測的細節,下面先看幾個弱引用的例子,在下面的幾個例子中,我們使用的數據類和自定義的弱引用類子類如下:

好了,三個例子已經介紹完畢,後面在介紹Netty內存泄漏檢測時就使用了這里的例子結果,在具體介紹時會和這里的例子一一對應。

Netty中將普通 ByteBuf 轉為具有內存泄漏檢測功能的 ByteBuf 是通過 AbstractByteBufAllocator.toLeakAwareBuffer 方法實現的,我們直接在Eclipse中看該方法的調用層次即可知道Netty在哪裡對 ByteBuf 進行了轉換,該方法調用如下圖所示:

可見池化內存分配器在分配heap或者direct ByteBuf 時都進行了轉換,非池化內存分配器僅在分配direct ByteBuf 時進行了轉換。個人理解時採用池化內存需要特別關注內存釋放,否則為了實現池化內存預先分配的一大塊內存會因為沒有釋放被很快分配完,造成後面沒有內存進行分配。非池化分配的直接內存也需要特別注意釋放,放置內存泄漏;非池化分配的heap內存(其實就是一個 byte 數組)則可以在對象被回收時同時被回收掉,發生內存泄漏的可能性較小。

本節介紹Netty中內存泄漏檢測相關的類,僅做一個大致介紹,類中的重要方法我們放在後面介紹。

主要負責使用 track 方法對指定的 ByteBuf 進行內存檢測泄漏進行追蹤,並返回負責追蹤的 ResourceLeakTracker 類實例,同時在調用 track 方法時,也會根據指定的檢測級別匯報最近的內存泄漏檢測結果。該類由工廠類 ResourceLeakDetectorFactory 負責實例化,默認的實現為 ResourceLeakDetector ,在 ResourceLeakDetectorFactory 類的默認實現 中,也會根據用戶是否配置了 io.netty.customResourceLeakDetector 來決定採用默認實現 ResourceLeakDetector 還是使用用戶自定義的 ResourceLeakDetector ,用戶自定義的 ResourceLeakDetector 必須是其子類。

默認實現為 DefaultResourceLeak , DefaultResourceLeak 實現了 ResourceLeakTracker 和 ResourceLeak 介面,同時也繼承了類 WeakReference ,是一個弱引用實現。首先,同上面 例2 的結果一樣,如果在使用 ByteBuf 時忘了調用 .release 方法,那麼將不會調用 DefaultResourceLeak.clear 方法去手動清空該弱引用持有的實際對象,在發生GC時,會由垃圾收集器對弱引用持有的實際對象進行回收,即發生了內存泄漏,同時該弱引用自身也會被加入到引用隊列中,該引用隊列是 ResourceLeakDetector 的成員域,上面介紹 ResourceLeakDetector 類時說到該類會在用戶 track 指定 ByteBuf 是匯報檢測結果,該類的匯報數據來源就是引用隊列。 DefaultResourceLeak 同時還提供了 record 方法可以讓用戶在指定時機選擇調用,這個方法可以記錄用戶的調用軌跡(堆棧)。 Record 同時也是一種單鏈表,在 DefaultResourceLeak 中就使用單鏈表記錄用戶的調用軌跡。

DefaultResourceLeak 供用戶記錄程序調用軌跡的類,也就是 DefaultResourceLeak.record 方法返回的對象,繼承自 Throwable ,因此可以使用 Throwable.getStackTrace 方法獲得調用軌跡信息,列印在內存泄漏報告中可以讓用戶更好的排除內存泄漏問題。

在上面介紹 ResourceLeakTracker 時,說到其默認實現為 DefaultResourceLeak , DefaultResourceLeak 提供了 record 方法記錄用戶的調用軌跡,用戶可在調用 ByteBuf 方法時調用 record 方法記錄調用軌跡,調用的頻率越多,後面在匯報內存泄漏情況時就能列印出越詳細的信息,這樣也能更方便的排查問題。

Netty提供了兩個 ByteBuf 的封裝類供選擇,就對應不同的 record 調用頻率,每個封裝類都持有 ResourceLeakTracker 對象,Netty根據配置的內存檢測級別(下一節介紹相關配置參數)使用不同的 ByteBuf 封裝類。

Netty提供的兩個 ByteBuf 封裝類就是 和 , 是 的子類, 類僅僅持有 ResourceLeakTracker 對象,但是看其源碼,發現沒有調用過 record 方法,所以只能知道是否發生了內存泄漏時,無法列印出任何調用軌跡信息。 作為 的子類,在 ByteBuf 的多個方法中調用了 record 方法,所以在發生內存泄漏時,能夠列印出比較詳細的調用軌跡信息。

在 類中使用了配置參數 io.netty.leakDetection.acquireAndReleaseOnly 來控制是否只是在調用增加或減少引用計數器的方法時才調用 record 方法記錄調用軌跡,默認為false。 中 retain 和 release 方法因為改變了引用計數器就直接調用了 record 方法,而該類中的其他方法則根據 io.netty.leakDetection.acquireAndReleaseOnly 的配置決定是否調用 record 方法,這里為了節省篇幅就不列出 類中調用 record 的方法了,讀者可自行查看。

在介紹相關配置參數之前,我們先看下Netty提供的內存泄漏檢測級別:

Level.ADVANCED 和 Level.PARANOID 使用的 ByteBuf 包裝類都是 ,我們上面介紹 ResourceLeakDetector 類時提到該類使用 track 方法對指定的 ByteBuf 進行內存檢測泄漏進行追蹤,並返回負責追蹤的 ResourceLeakTracker 類實例,同時在調用 track 方法時,也會根據指定的檢測級別匯報最近的內存泄漏檢測結果。如果內存泄漏檢測級別為 Level.PARANOID 時則每次調用 track 方法都會進行內存泄漏報告;如果級別為 Level.ADVANCED 或者 Level.SIMPLE 則會以一定頻率進行內存泄漏報告,而不是每次 track 都進行報告。

是否關閉Netty內存泄漏檢測功能,默認為false。如果該參數配置為false,則默認的內存泄漏檢測級別根據此參數的配置為 Level.DISABLED ,否則默認的級別為 Level.SIMPLE 。

配置內存泄漏檢測級別的參數,用於老版本的配置參數。

新的內存泄漏檢測級別參數,如果沒有配置,則會採用老版本參數配置的級別作為最終配置。

在第4節介紹內存泄漏檢測相關類時,我們介紹過 DefaultResourceLeak 提供了 record 方法記錄用戶的調用軌跡,如果當前保存的調用軌跡記錄數 Record 大於參數 io.netty.leakDetection.targetRecords 配置的值,那麼會以一定的概率(1/2^n)刪除頭結點之後再加入新的記錄,當然也有可能不刪除頭結點直接新增新的記錄。

該參數的默認為4。

上面介紹過,在 類中使用了配置參數 io.netty.leakDetection.acquireAndReleaseOnly 來控制是否只是在調用增加或減少引用計數器的方法時才調用 record 方法記錄調用軌跡,默認為false。

在介紹 ResourceLeakDetector 類時提到過,默認的 ResourceLeakDetector 類就是 ResourceLeakDetector ,但是用戶可以使用參數 io.netty.customResourceLeakDetector 來決定採用默認實現 ResourceLeakDetector 還是使用用戶自定義的 ResourceLeakDetector 。

我們在第二節介紹了Netty中將普通 ByteBuf 轉為具有內存泄漏檢測功能的 ByteBuf 是通過 AbstractByteBufAllocator.toLeakAwareBuffer 方法實現的。

這里我們先看下該方法的源碼:

上面的源碼中是調用 AbstractByteBuf.leakDetector.track(buf) 返回 ResourceLeakTracker 類對象的,這里我們看下默認的 ResourceLeakDetector 中 track 方法實現:

我們看到 AbstractByteBufAllocator.toLeakAwareBuffer 對 ResourceLeakDetector.track 返回的 DefaultResourceLeak 和傳入的 ByteBuf 對象進行封裝,返回了具有內存泄漏檢測功能的 ByteBuf 封裝類 或其子類 。如果應用程序在使用 ByteBuf 正確調用了 retain 和 release 方法,則在引用計數器為0時,則會清除弱引用持有的實際對象,發生GC時, DefaultResourceLeak 也不會被放入引用隊列中(見前面第2節 例3 結果)。

但是如果應用程序在使用 ByteBuf 沒有正確調用 retain 和 release 方法,則不會清除弱引用持有的實際對象,此時如果實際上已經沒有強引用指向該 ByteBuf ,那麼在發生GC時,垃圾收集器會回收該 ByteBuf ,而弱引用 DefaultResourceLeak 會被放入引用隊列中(見前面第2節 例2 結果),加入到引用隊列中的就是識別到的發生內存泄漏的 ByteBuf 。在 ResourceLeakDetector.track 方法中調用的 reportLeak 輸出的就是引用隊列中的弱引用 DefaultResourceLeak :

到這里,已經基本上介紹完Netty內存檢測的實現原理,下面我們再看下 DefaultResourceLeak.record 是如何記錄調用軌跡的:

最後我們再看下 Record 是如何輸出調用軌跡的,前面我們說到 Record 繼承自類 Throwable ,因此可使用 getStackTrace 方法獲取實例化該對象時的調用軌跡,所以上面在輸出內存泄漏報告時就調用了 Record.toString 方法:

④ Netty源碼_UnpooledDirectByteBuf詳解

本篇文章我們講解緩存區 ByteBuf 八大主要類型中兩種,未池化直接緩沖區 UnpooledDirectByteBuf 和 未池化不安全直接緩沖區 UnpooledUnsafeDirectByteBuf 。

UnpooledDirectByteBuf 一個基於 NIO ByteBuffer 的緩沖區。
建議使用 UnpooledByteBufAllocator.directBuffer(int, int) , Unpooled.directBuffer(int) 和 Unpooled.wrappedBuffer(ByteBuffer) ;而不是顯式調用構造函數。

有四個成員屬性:

通過 allocateDirect(initialCapacity) 方法創建一個新的 NIO 緩存區實例來初始化此緩存區對象。

利用現有的 NIO 緩存區創建此緩存區。

通過 NIO 緩存區 buffer 對應方法獲取基本數據類型數據。

根據目標緩存區 dst 類型不同,處理的方式也不同。

你會發現這些方法都是獲取此緩存區對應 NIO 緩存區 ByteBuffer 對象,調用 ByteBuffer 對象的方法,與 IO 流的交互,進行數據傳輸

和 get 系列方法一樣, set 系列的實現也是靠 NIO 緩存區 ByteBuffer 對應方法。

剩餘方法也幾乎都是和 NIO 緩存區 ByteBuffer 有關,而且也不難,就不做過多介紹了。

UnpooledDirectByteBuf 主要是通過 NIO 緩存區 buffer 來存儲數據。而它獲取和設置數據,也都是通過 NIO 緩存區對應方法實現的。

光看介紹,和 UnpooledDirectByteBuf 沒有任何區別。它也是 UnpooledDirectByteBuf 的子類。

那麼 UnpooledUnsafeDirectByteBuf 和 UnpooledDirectByteBuf 不同處在那裡呢?

通過復習 setByteBuffer 方法,獲取 NIO 緩存區 buffer 對應的直接內存地址。

通過 UnsafeByteBufUtil 對應方法,直接從內存地址獲取對應基本類型數據。

通過 UnsafeByteBufUtil 對應方法,直接向內存地址設置對應基本類型數據。

只有這個類型 hasMemoryAddress() 方法才會返回 true 。

UnpooledUnsafeDirectByteBuf 就是通過直接從內存地址中獲取和設置數據的方式,提高性能。

熱點內容
batsql語句 發布:2025-01-31 14:00:13 瀏覽:733
沈陽加密狗 發布:2025-01-31 13:54:58 瀏覽:705
聯想伺服器怎麼裝windows7 發布:2025-01-31 13:54:52 瀏覽:874
java二級考試歷年真題 發布:2025-01-31 13:50:31 瀏覽:171
編程一刻 發布:2025-01-31 13:36:44 瀏覽:585
編程小草出土 發布:2025-01-31 13:33:27 瀏覽:579
如何設置伺服器屏蔽你的ip 發布:2025-01-31 13:25:58 瀏覽:243
扣扣的獨立密碼是什麼密碼 發布:2025-01-31 13:23:42 瀏覽:132
pythonlist的用法 發布:2025-01-31 12:56:15 瀏覽:130
搭建美國節點伺服器 發布:2025-01-31 12:55:27 瀏覽:858