Ⅰ Android socket源碼解析(三)socket的connect源碼解析
上一篇文章著重的聊了socket服務端的bind,listen,accpet的邏輯。本文來著重聊聊connect都做了什麼?
如果遇到什麼問題,可以來本文 https://www.jianshu.com/p/da6089fdcfe1 下討論
當服務端一切都准備好了。客戶端就會嘗試的通過 connect 系統調用,嘗試的和服務端建立遠程連接。
首先校驗當前socket中是否有正確的目標地址。然後獲取IP地址和埠調用 connectToAddress 。
在這個方法中,能看到有一個 NetHooks 跟蹤socket的調用,也能看到 BlockGuard 跟蹤了socket的connect調用。因此可以hook這兩個地方跟蹤socket,不過很少用就是了。
核心方法是 socketConnect 方法,這個方法就是調用 IoBridge.connect 方法。同理也會調用到jni中。
能看到也是調用了 connect 系統調用。
文件:/ net / ipv4 / af_inet.c
在這個方法中做的事情如下:
注意 sk_prot 所指向的方法是, tcp_prot 中 connect 所指向的方法,也就是指 tcp_v4_connect .
文件:/ net / ipv4 / tcp_ipv4.c
本質上核心任務有三件:
想要能夠理解下文內容,先要明白什麼是路由表。
路由表分為兩大類:
每個路由器都有一個路由表(RIB)和轉發表 (fib表),路由表用於決策路由,轉發表決策轉發分組。下文會接觸到這兩種表。
這兩個表有什麼區別呢?
網上雖然給了如下的定義:
但實際上在Linux 3.8.1中並沒有明確的區分。整個路由相關的邏輯都是使用了fib轉發表承擔的。
先來看看幾個和FIB轉發表相關的核心結構體:
熟悉Linux命令朋友一定就能認出這裡面大部分的欄位都可以通過route命令查找到。
命令執行結果如下:
在這route命令結果的欄位實際上都對應上了結構體中的欄位含義:
知道路由表的的內容後。再來FIB轉發表的內容。實際上從下面的源碼其實可以得知,路由表的獲取,實際上是先從fib轉發表的路由字典樹獲取到後在同感加工獲得路由表對象。
轉發表的內容就更加簡單
還記得在之前總結的ip地址的結構嗎?
需要進行一次tcp的通信,意味著需要把ip報文准備好。因此需要決定源ip地址和目標IP地址。目標ip地址在之前通過netd查詢到了,此時需要得到本地發送的源ip地址。
然而在實際情況下,往往是面對如下這么情況:公網一個對外的ip地址,而內網會被映射成多個不同內網的ip地址。而這個過程就是通過DDNS動態的在內存中進行更新。
因此 ip_route_connect 實際上就是選擇一個緩存好的,通過DDNS設置好的內網ip地址並找到作為結果返回,將會在之後發送包的時候填入這些存在結果信息。而查詢內網ip地址的過程,可以成為RTNetLink。
在Linux中有一個常用的命令 ifconfig 也可以實現類似增加一個內網ip地址的功能:
比如說為網卡eth0增加一個IPV6的地址。而這個過程實際上就是調用了devinet內核模塊設定好的添加新ip地址方式,並在回調中把該ip地址刷新到內存中。
注意 devinet 和 RTNetLink 嚴格來說不是一個存在同一個模塊。雖然都是使用 rtnl_register 注冊方法到rtnl模塊中:
文件:/ net / ipv4 / devinet.c
文件:/ net / ipv4 / route.c
實際上整個route模塊,是跟著ipv4 內核模塊一起初始化好的。能看到其中就根據不同的rtnl操作符號注冊了對應不同的方法。
整個DDNS的工作流程大體如下:
當然,在tcp三次握手執行之前,需要得到當前的源地址,那麼就需要通過rtnl進行查詢內存中分配的ip。
文件:/ include / net / route.h
這個方法核心就是 __ip_route_output_key .當目的地址或者源地址有其一為空,則會調用 __ip_route_output_key 填充ip地址。目的地址為空說明可能是在回環鏈路中通信,如果源地址為空,那個說明可能往目的地址通信需要填充本地被DDNS分配好的內網地址。
在這個方法中核心還是調用了 flowi4_init_output 進行flowi4結構體的初始化。
文件:/ include / net / flow.h
能看到這個過程把數據中的源地址,目的地址,源地址埠和目的地址埠,協議類型等數據給記錄下來,之後內網ip地址的查詢與更新就會頻繁的和這個結構體進行交互。
能看到實際上 flowi4 是一個用於承載數據的臨時結構體,包含了本次路由操作需要的數據。
執行的事務如下:
想要弄清楚ip路由表的核心邏輯,必須明白路由表的幾個核心的數據結構。當然網上搜索到的和本文很可能大為不同。本文是基於LInux 內核3.1.8.之後的設計幾乎都沿用這一套。
而內核將路由表進行大規模的重新設計,很大一部分的原因是網路環境日益龐大且復雜。需要全新的方式進行優化管理系統中的路由表。
下面是fib_table 路由表所涉及的數據結構:
依次從最外層的結構體介紹:
能看到路由表的存儲實際上通過字典樹的數據結構壓縮實現的。但是和常見的字典樹有點區別,這種特殊的字典樹稱為LC-trie 快速路由查找演算法。
這一篇文章對於快速路由查找演算法的理解寫的很不錯: https://blog.csdn.net/dog250/article/details/6596046
首先理解字典樹:字典樹簡單的來說,就是把一串數據化為二進制格式,根據左0,右1的方式構成的。
如圖下所示:
這個過程用圖來展示,就是沿著字典樹路徑不斷向下讀,比如依次讀取abd節點就能得到00這個數字。依次讀取abeh就能得到010這個數字。
說到底這種方式只是存儲數據的一種方式。而使用數的好處就能很輕易的找到公共前綴,在字典樹中找到公共最大子樹,也就找到了公共前綴。
而LC-trie 則是在這之上做了壓縮優化處理,想要理解這個演算法,必須要明白在 tnode 中存在兩個十分核心的數據:
這負責什麼事情呢?下面就簡單說說整個lc-trie的演算法就能明白了。
當然先來看看方法 __ip_dev_find 是如何查找
文件:/ net / ipv4 / fib_trie.c
整個方法就是通過 tkey_extract_bits 生成tnode中對應的葉子節點所在index,從而通過 tnode_get_child_rcu 拿到tnode節點中index所對應的數組中獲取葉下一級別的tnode或者葉子結點。
其中查找index最為核心方法如上,這個過程,先通過key左移動pos個位,再向右邊移動(32 - bits)演算法找到對應index。
在這里能對路由壓縮演算法有一定的理解即可,本文重點不在這里。當從路由樹中找到了結果就返回 fib_result 結構體。
查詢的結果最為核心的就是 fib_table 路由表,存儲了真正的路由轉發信息
文件:/ net / ipv4 / route.c
這個方法做的事情很簡單,本質上就是想要找到這個路由的下一跳是哪裡?
在這裡面有一個核心的結構體名為 fib_nh_exception 。這個是指fib表中去往目的地址情況下最理想的下一跳的地址。
而這個結構體在上一個方法通過 find_exception 獲得.遍歷從 fib_result 獲取到 fib_nh 結構體中的 nh_exceptions 鏈表。從這鏈表中找到一模一樣的目的地址並返回得到的。
文件:/ net / ipv4 / tcp_output.c
Ⅱ JDK1.8中的HashMap底層數據結構的圖解
hashmap是java開發最常用的一種數據模型,hashmap屬於map介面的一種實現。以key-value的這種形式存儲數據,其中key是不允許重復的但是允許為空,value是可以重復或為空的。其中key只能使用基本數據類型(int,double...)的封裝類(interger,double。。。。)。
JDK1.7中HashMap的底層是由數組+單向鏈表這兩種數據結構組合而成的,而在JDK1.8中HashMap是由數組+單向鏈表+紅黑樹三種數據結構組合而成的。
初始化有固定的大小長度,有順序的下標(下標從0開始),圖1隻是示例。
由每個節點組成,每個節點包含一個data(存儲數據)和指向下一個節點地址的next,如圖二。
(1)每個節點或者是黑色,或者是紅色。
(2)根節點是黑色。
(3)每個葉子節點(NIL)是黑色。 注意:這里葉子節點,是指為空(NIL或NULL)的葉子節點!
(4)如果一個節點是紅色的,則它的子節點必須是黑色的。
(5)從一個節點到該節點的子孫節點的所有路徑上包含相同數目的黑節點。
介紹完三種基本結構後,我們再來看看hashmap的數據結構圖,如圖4。
圖5是我截自JDK1.8的HashMap底層源碼。
Ⅲ Hermes源碼分析(二)——解析位元組碼
前面一節 講到位元組碼序列化為二進制是有固定的格式的,這里我們分析一下源碼裡面是怎麼處理的
這里可以看到首先寫入的是魔數,他的值為
對應的二進制見下圖,注意是小端位元組序
第二項是位元組碼的版本,筆者的版本是74,也即 上圖中的4a00 0000
第三項是源碼的hash,這里採用的是SHA1演算法,生成的哈希值是160位,因此佔用了20個位元組
第四項是文件長度,這個欄位是32位的,也就是下圖中的為0aa030,轉換成十進制就是696368,實際文件大小也是這么多
後面的欄位類似,就不一一分析了,頭部所有欄位的類型都可以在 BytecodeFileHeader.h 中看到,Hermes按照既定的內存布局把欄位寫入後再序列化,就得到了我們看到的位元組碼文件。
這里寫入的數據很多,以函數頭的寫入為例,我們調用了visitFunctionHeader方法,並通過byteCodeMole拿到函數的簽名,將其寫入函數表(存疑,在實際的文件中並沒有看到這一部分)。注意這些數據必須按順序寫入,因為讀出的時候也是按對應順序來的。
我們知道react-native 在載入位元組碼的時候需要調用hermes的prepareJavaScript方法, 那這個方法做了些什麼事呢?
這里做了兩件事情:
1. 判斷是否是位元組碼,如果是則調用createBCProviderFromBuffer,否則調用createBCProviderFromSrc,我們這里只關注createBCProviderFromBuffer
2.通過BCProviderFromBuffer的構造方法得到文件頭和函數頭的信息(populateFromBuffer方法),下面是這個方法的實現。
BytecodeFileFields的populateFromBuffer方法也是一個模版方法,注意這里調用populateFromBuffer方法的是一個 ConstBytecodeFileFields對象,他代表的是不可變的位元組碼欄位。
細心的讀者會發現這里也有visitFunctionHeaders方法, 這里主要為了復用visitBytecodeSegmentsInOrder的邏輯,把populator當作一個visitor來按順序讀取buffer的內容,並提前載入到BytecodeFileFields裡面,以減少後面執行位元組碼時解析的時間。
Hermes引擎在讀取了位元組碼之後會通過解析BytecodeFileHeader這個結構體中的欄位來獲取一些關鍵信息,例如bundle是否是位元組碼格式,是否包含了函數,位元組碼的版本是否匹配等。注意這里我們只是解析了頭部,沒有解析整個位元組碼,後面執行位元組碼時才會解析剩餘的部分。
evaluatePreparedJavaScript這個方法,主要是調用了HermesRuntime的 runBytecode方法,這里hermesPrep時上一步解析頭部時獲取的BCProviderFromBuffer實例。
runBytecode這個方法比較長,主要做了幾件事情:
這里說明一下,Domain是用於垃圾回收的運行時模塊的代理, Domain被創建時是空的,並跟隨著運行時模塊進行傳播, 在運行時模塊的整個生命周期內都一直存在。在某個Domain下創建的所有函數都會保持著對這個Domain的強引用。當Domain被回收的時候,這個Domain下的所有函數都不能使用。
未完待續。。。
Ⅳ ViewPager系列文章(一)- ViewPager源碼分析及載入頁面原理圖
1>:點擊 viewPager.setAdapter進入下邊源碼,會調用 populate() 方法,這個方法作用是創建和銷毀子條目(子item):
在populate()方法中:
創建ItemView:mAdapter.instantiateItem(this, position);
銷毀ItemView:mAdapter.destroyItem(this, pos, ii.object);
所以由ViewPager的源碼可以看出,ViewPager里邊無論放多少個頁面都不會內存溢出,它會不斷的去創建和銷毀view;
和 ListView、RecyclerView不一樣,ListView、RecyclerView是會不斷的復用view,而viewpager是不斷的創建和銷毀view
輪播圖剛打開默認顯示當前頁,是第一頁,默認會緩存左右兩個頁面,如果左邊沒有,只有右邊有,那麼右邊是第0頁,當前頁是第一頁;
如果你滑動到第1頁,ViewPager會默認把 左邊第0頁 和 右邊第2頁 創建出來;
如果你滑動到第2頁,ViewPager會默認把第1頁和第3頁創建出來,而原來的第0頁就會變成需要銷毀的頁面;
如果想要緩存多頁,可以調用setOffscreenPageLimit()方法:
setOffscreenPageLimit(1):ViewPager機制默認就是緩存1,表示左邊、右邊各緩存1頁,加上自己,總共是3頁,其餘頁面全部銷毀;
setOffscreenPageLimit(2):表示默認給左右各緩存2頁,共4頁,加上自己,總共緩存5頁,其餘頁面全部銷毀;
setOffscreenPageLimit(3):表示默認給左右各緩存3頁,共6頁,加上自己,總共緩存7頁,其餘頁面全部銷毀;
因為 smoothScrollTo()滑動方法也調用populate(),而populate()方法維護了當前顯示頁面和 左右緩存的頁面,就能做到無限滑動而不出問題;
A:從populate()源碼中可知:先判斷頁面是否在緩存范圍內:如果在,則addNewItem添加進來,否則在destroyItem掉;
B:ViewPager會緩存左右兩邊頁面+1(當前顯示頁面),默認認為當前頁面的 左右兩邊各有1個,用戶可以手動調用setOffscreenPageLimit()方法設置數量,如果傳的值小於1,就默認設置為1;
ViewPager實際示意圖如下:
Ⅳ flutter框架(源碼分析)
准備寫這一系列的文章自己是下了很大的決心的,自知會遇到很大的困難。因能力有限,如有疏漏之處,還請大家斧正。
相信大家都看過很多遍下面那張圖,這個系列的文章就是要分析紅框內的源碼:
我們看源碼的結構其實是這樣的:
總共十二個模塊,對應的模塊我一旦寫完就會在下面更新鏈接:
Ⅵ Category實現的原理一:底層結構及源碼分析
❓比如說有如下3個類,思考一下如下圖所示的實例方法存放在哪裡?
下面我們就來驗證一下上面的結論.
我們的分類 HHPerson+eat 被轉換為類型為 static struct _category_t ,變數名為: _OBJC_$_CATEGORY_HHPerson_$_eat :
實例方法列表:
協議列表: (HHPerson+eat實現了 NSCopying,NSCoding 協議)
屬性列表:
以上就是分類的底層結構,可以看到,分類的信息在編譯期間都被分離出來了,下面我們從 runtime 源碼研究一下分類.
我們梳理一下 attachCategories (cls,cats,true) 方法. attach 一詞是 附加 的意思,從名字上我們可以看出這個方法大概意思是: 附加分類 .事實上它的確如此,下面我開始研究:
本篇主要講了 category 的底層數據結構,和分析 runtime 如何處理 category 分類信息的.在下篇文章中-- Category實現的原理二:分類信息如何添加到本類中 將介紹 runtime 如何將分類信息添加到本類中.
Ⅶ YTKNetWork源碼解析
YTKNetWork是一個開源的第三方網路請求框架,具有比較好的網路請求緩存機制的控制。近期項目中想要採取HTTP Cache來優化網路請求,需要實現以下兩點:
經過簡單的調研發現,YTKNetWork雖然底層是使用的AFNetWorking的框架,但是使用AFNetWorking能夠通過設置緩存空間和緩存協議就能快速簡單實現的方式在YTKNetWork中並沒有生效。YTKNetWork已經很少維護了,所以,只能自己動手來分析YTKNetWork的實現。
廢話不多說,下面就開始吧。
先上兩張圖
首先,我們從目錄中找到YTKNetWork.h的頭文件,從中可以看到作者想對我們開放的基礎功能模塊,根據字面意思可以分為幾種,一種是Request類型的,一種是Config,還一種是Agent。很容易理解,request類型的是用來發送網路請求的,config是用來配置請求信息的,agent暫時不清楚,用到時我們再來具體分析。那麼項目結構就很清晰了,我們就一步一步來分析就好了。
我們從這個最簡單的單體類來入手。
m文件中沒什麼可以分析的,就是對兩個過濾池數組的增刪操作,唯一注意的就是AFSecurityPolicy初始化的時候使用了defaultPolicy類方法實例化
總結下來,YTKNetworkConfig這個類的作用就是設置base URL、安全策略、url的過濾、緩存路徑的過濾。
但是現在對於兩個協議的具體使用方式還有疑問,我們帶著這個疑問繼續往下看。
Ⅷ iOS-底層探索03:isa底層結構分析
在上一篇的文章 iOS alloc 流程分析 中我們分析了 alloc ,知道了 alloc 創建了對象並且分配內存,同時初始化 isa 屬性。我們也知道了 Objective-C 對象在底層本質上是結構體 ,所有的對象裡面都會包含有一個 isa ,這篇文章我們來分析探究 isa底層是如何實現的 。
其中的 nonpointer 字面的意思是沒有指針的,一般情況下nonpointer是為true的,只有在例如實現了 allocwithzone 方法,retain,release等的時候會是false。如果為false是直接將傳進來的cls為isa的關聯的cls賦值。
其他的剩下的部分就是對isa的初始化賦值了。但是具體的isa內部是怎樣的還是不知道的,從源碼中 isa_t 點擊進去可以查看。
通過源碼可以知道 isa 的 isa_t 類型的內部結構
從中可以知道, isa 是一個 聯合體 union ,裡面有關聯的類cls和long型的bits。
聯合體(union)、位域不清楚的可以參考 這篇文章
由上面的概念可以知道, cls 和 bits 之間是互斥的,即有cls就沒有bits,有bits就沒有cls。這就很好地解釋了為什麼上面的源碼在初始化isa的時候會用nonpointer來區分開。所以isa的大小佔8個位元組,64位。其中這64位中分別存儲了什麼呢?通過ISA_BITFIELD位域源碼:
這兩種是分別 arm64 和 x86 系統架構下isa的內部結構,但是都是64位的。iPhone 採用 arm64 架構,MacOS 採用 x86 架構,本文介紹 x86 架構下 isa的聯合體結構 。
通過 objc4-781 蘋果官方的源碼,使用object_getClass這個方法獲取到類。
通過源碼找到object_getClass的方法
從源碼中可以知道返回的 isa 最終是 (Class)(isa.bits & ISA_MASK)
其中源碼有一個判斷 isTaggedPointer() ,其中蘋果對於 Tagged Pointer 的概念引入,是為了節省內存和提高執行效率,對於 64 位程序,相關邏輯能減少一半的內存佔用,以及 3 倍的訪問速度提升,100 倍的創建、銷毀速度提升。如果想了解這部分的內容可以看看 深入理解 Tagged Pointer
下面就是用 lldb 的指令來驗證一下的。通過 x/4gx objc 列印出對象的內存地址
由源碼知道類Class的最終返回是 (Class)(isa.bits & ISA_MASK) ,所以將 x/4gx objc 列印出來的 0x001d800100002119 & ISA_MASK 的值得到如下:
由前面的文章知道由於內存的優化對象的其他屬性的位置實際會發生變化的,所以對象的第一個屬性就是 isa
通過測試我們發現 對象的內存地址 和通過 isa取出來的內存地址 是一樣的,所以 isa 是關聯著對象與類的
通過上面的介紹,可以知道了isa是關聯著對象與類的,並且對象的isa指向類,因為萬物皆對象,那麼類的isa指向的是誰呢?可以通過蘋果官方的isa的走位流程圖:
其中虛線是代表 isa 的走位,實線代表的是 繼承關系 的走位。
通過以上的源碼分析,我們認識到對象的 isa指針 指向了對象所屬的類。而類本身也有一個 isa指針 ,它指向的又是什麼呢?
此時要引入 meta class (即元類)的概念了。我們先了解一下元類的信息:
驗證過程見參考文章
具體思路是, shiftcls 在 x86_64 架構下長度是44位,存儲在 isa 的 [3, 46]位上,所以可以通過將isa的 [0, 2]位、[47, 63]位清零,同樣能得到 shiftcls 的值,進而確定類。(先右移3位、再左移20位、然後右移17位即可)
如果 Person 類繼承的是 NSProxy ,相關 isa 指向是怎樣的呢?
答案:跟 NSObject 一樣,兩者都是 根類 。
OC源碼分析之isa
iOS的OC的isa的底層原理
Ⅸ Doris 源碼分析 (二) 代碼結構分析
注 正如上圖所示, FE 端主要是 PaloFe 開啟的服務入口, 後端為 doris_main 開啟的服務入口,前後端交互主要靠 thrift rpc 進行調用。 FE 中主從選舉及元數據操作日誌同步等均依託 bdbje 主從方案來實現。
元數據變更日誌主要靠 bdbje 的主從復制來完成如下圖:
注 元數據的數據流具體過程如上圖所示,步驟如下:
Ⅹ coredns源碼分析
CoreDNS是使用go語言編寫的快速靈活的DNS服務,採用鏈式插件模式,每個插件實現獨立的功能,底層協議可以是tcp/udp,也可以是TLS,gRPC等。默認監聽所有ip地址,可使用bind插件指定監聽指定地址。
格式如下
SCHEME是可選的,默認值為dns://,也可以指定為tls://,grpc://或者https://。
ZONE是可選的,指定了此dnsserver可以服務的域名前綴,如果不指定,則默認為root,表示可以接收所有的dns請求。
PORT是選項的,指定了監聽埠號,默認為53,如果這里指定了埠號,則不能通過參數-dns.port覆蓋。
一塊上面格式的配置表示一個dnsserver,稱為serverblock,可以配置多個serverblock表示多個dnsserver。
下面通過一個例子說明,如下配置文件指定了4個serverblock,即4個dnsserver,第一個監聽埠5300,後面三個監聽同一個埠53,每個dnsserver指定了特定的插件。
下圖為配置的簡略圖
a. 從圖中可看到插件執行順序不是配置文件中的順序,這是因為插件執行順序是在源碼目錄中的plugin.cfg指定的,一旦編譯後,順序就固定了。
b. .根serverblock雖然指定了health,但是圖中卻沒有,這是因為health插件不參與dns請求的處理。能處理dns請求的插件必須提供如下兩個介面函數。
dns請求處理流程
收到dns請求後,首先根據域名匹配zone找到對應的dnsserver(最長匹配優先),如果沒有匹配到,則使用默認的root dnsserver。
找到dnsserver後,就要按照插件順序執行其中配置的插件,當然並不是配置的插件都會被執行,如果某個插件成功找到記錄,則返回成功,否則根據插件是否配置了fallthrough等來決定是否執行下一個插件。
plugin.cfg
源碼目錄下的plugin.cfg指定了插件執行順序,如果想添加插件,可按格式添加到指定位置。
源碼目錄下的Makefile根據plugin.cfg生成了兩個go文件:zplugin.go和zdirectives.go。
core/dnsserver/zdirectives.go將所有插件名字放在一個數組中。
codedns 主函數
codedns.go 首先導入了包"github.com/coredns/coredns/core/plugin",此包內只有一個文件zplugin.go,此文件為自動生成的,主要導入了所有的插件,執行每個插件的init函數。
接著執行 run.go Run
此文件又引入了包"github.com/coredns/coredns/core/dnsserver",其init函數在 dnsserver/register.go 文件中,如下所示,主要是注冊了serverType
剩下的就是解析參數,解析配置文件後,執行caddy.Start。
這里就是根據配置文件中指定的serverblock,執行插件的setup進行初始化,創建對應的server,開始監聽dns請求
tcp協議調用Serve,udp協議調用ServePacket
收到DNS請求後,調用ServeDNS,根據域名匹配dnsserver,如果沒有匹配不到則使用根dnsserver,然後執行dnsserver中配置的插件
以k8s插件為例
參考
//如何寫coredns插件
http://dockone.io/article/9620
//coredns源碼分析
https://wenku..com/view/.html
https://blog.csdn.net/zhonglinzhang/article/details/99679323
https://www.codercto.com/a/89703.html
//NodeLocal DNSCache
https://www.cnblogs.com/sanzxcvbnm/p/16013560.html
https://blog.csdn.net/xixihahalelehehe/article/details/118894971