1.概覽:
談到raft協議實現就繞不開網上流行的mit6.824,但其為go語言,官方沒有lab的答案,框架也很晦澀難懂,且全網沒有一個部落格對其有清晰的解釋,有的只是甩一堆名詞然後直接貼沒有任何註釋的程式碼,非常不適合學習。
但是github上面的cornerstone是純c++實現的一個非常優雅且精簡的raft協議,碼風優美,程式碼易懂,介面清晰,對c++黨非常友好,也很適合初學raft的人來學習。
鑑於cornerstone這麼優秀的程式碼還沒人對其有過原始碼級解析,我決定記錄自己學習其原始碼過程並對其原始碼進行詳細解析。
那我們得從哪裡開始分析cornerstone呢?開始的切入點應該越小越好,同時具有很強的通用性,在很多環節又用得到。
而buffer是cornerstone中的一個非常重要的概念,從rpc傳送請求,接受response,到log記錄等等方面都採用buffer來儲存資訊。
因此讓我們先從buffer開始我們對cornerstone原始碼的解析。
2.buffer的總體架構
如圖,總體buffer可分為3部分:
size
:記錄data
塊的位元組個數,從0開始編號(size = 0代表data塊為空而不是buffer為空),不包括前面的size
與pos
(size
+ pos
統稱header)。
根據size的大小(也就是data塊的位元組數)可將buffer分為大塊與小塊,其中size >= 0x8000 為大塊,否則為小塊。(這裡有個問題:大小塊size都是一個範圍,我們要怎麼快速來確定buffer是大塊還是小塊呢?這個問題答案我們放到後面再細說)
pos
:記錄data
塊的讀寫指標,也是從0開始編號(這裡pos
既記錄讀又記錄寫操作的位置,本身是1個指標,存不了兩條資訊,所以我們需要自己手動調整pos
)
data
:儲存buffer裡面的實際資料,可以是int,ulong,str
等多種型別
3.buffer的記憶體分配
我們先上原始碼:
bufptr buffer::alloc(const size_t size) {
if (size >= 0x80000000) {
throw std::out_of_range("size exceed the max size that cornrestone::buffer could support");
}
if (size >= 0x8000) {
bufptr buf(reinterpret_cast<buffer*>(new char[size + sizeof(uint) * 2]), &free_buffer);
any_ptr ptr = reinterpret_cast<any_ptr>(buf.get());
__init_b_block(ptr, size);
return buf;
}
bufptr buf(reinterpret_cast<buffer*>(new char[size + sizeof(ushort) * 2]), &free_buffer);
any_ptr ptr = reinterpret_cast<any_ptr>(buf.get());
__init_s_block(ptr, size);
return buf;
}
我們這裡只分析大塊的分配,小塊的程式碼同理。
(1)首先判斷要求分配size的大小,如果size >= 0x80000000
,直接丟擲異常
(2)當size >= 0x8000
(也就是INT_MAX = 32768)的時候意味著要分配大塊。透過new char[size + sizeof(uint) * 2]
分配了要求的size + header的位元組數。這裡bufptr的定義在buffer.hxx
裡面using bufptr = uptr<buffer, void (*)(buffer*)>;
。根據bufptr的定義可以知道這是一個指向buffer類的unique_ptr,第二個引數void(*)(buffer*)
是一個函式指標, 返回值為void,引數是buffer*,對應著原始碼裡面的&free_buffer
,是一個自定義的釋放bufptr指向內容的函式。
(3)把bufptr展開來就是unique_ptr<buffer, &free_buffer> buf(reinterpret_cast<buffer*>(new char[size + sizeof(uint) * 2]), &free_buffer)
, 這裡reinterpret_cast是用於無關型別的相互轉換。new char[]返回的是char *指標,但是根據unique_ptr<T> A(xxx)
的語法括號裡面的xxx是指向T型別的指標,所以我們需要用reinterpret_cast將char *指標轉換為buffer *
(4)完成了記憶體的分配然後到any_ptr ptr = reinterpret_cast<any_ptr>(buf.get());
這裡的any_ptr在basic_types.hxx
裡面的定義是typedef void* any_ptr;
。而buf.get()
是unique_ptr的一個成員函式,用於獲取其原始指標,那麼any_ptr ptr = reinterpret_cast<any_ptr>(buf.get());
這一行實現的便是將原始指標提取出來並轉換為void*
型別
(5)接著是__init_b_block(ptr, size);
這個宏定義
#define __init_block(p, s, t) ((t*)(p))[0] = (t)s;\
((t*)(p))[1] = 0
#define __init_s_block(p, s) __init_block(p, s, ushort)
#define __init_b_block(p, s) __init_block(p, s, uint);\
*((uint*)(p)) |= 0x80000000
b_block表示大塊,s_block表示小塊
(5.1)不管大塊還是小塊都透過__init_block(p, s, t)
來初始化,t表示型別(ushort
或uint
),p就是指向buffer的指標,s是buffer的size
引數。
(5.2)前面buffer的總體架構裡面我們說過buffer分為三個部分,那麼這裡的p[0],p[1]很明顯就是對應的size
與 pos
引數。
(5.3)為什麼初始化size
與 pos
引數不直接用p[0] = size, p[1] = pos
呢?這裡的((t*)(p))[0],((t*)(p))[1]
又是什麼?
由於我們規定size >= 0x8000
(USHORT_MAX = 32768), 說明我們p[0]存的size在大塊的時候就不能用ushort來表示了,必須得用uint型別,所以我們將p指標強轉為uint*型別,這樣uint*意義下的p[0]便表示以p開始往後數uint個位元組來儲存我們的size。pos也是同理,因為pos是描述data塊的讀寫指標,所以pos \(\in\) [0, size),也需要考慮是用uint型別還是ushort型別。
(5.4)在初始化大塊的時候為什麼要*((uint*)(p)) |= 0x80000000
?
這裡就是我們前面說的如何確定buffer是大塊還是小塊問題的關鍵。
0x80000000轉換為10進位制是231,由於大塊的size
、pos
是uint,所以有32位且無符號位,231剛好佔據的是uint的最高位。
讓p強轉為uint*型別後又用*取內容得到uint型別的值(實際上就是uint型別的p[0]),接著將其|= 0x80000000使得最高位為1。
那具體這個最高位為1是怎麼用於判斷大小塊的呢?
#define __is_big_block(p) (0x80000000 & *((uint*)(p)))
我們將p[0]轉為uint型別,接著與0x80000000進行相與。
- 如果是大塊,由於我們
__init_b_block
的時候將p[0]最高位置1,與0x80000000相與的結果就是1,對應的__is_big_block
返回值是1 - 如果是小塊,由於&操作是按位進行,所以最高位為0,而後面的位由於0x80000000全為0得到的也是0,對應的
__is_big_block
返回值是0
我們便透過位運算而不是進行size >= 0x8000
的判斷從而快速確定buffer是大塊還是小塊。
(6)瞭解完__init_b_block
的宏定義後,我們還有一個問題沒有解決,那就是為什麼要將bufptr
取原始指標後再轉化為any_ptr
?
首先我們得知道智慧指標中的unique_ptr有獨佔所有權的概念,而uint*與ushort*都是沒有所有權管理的普通指標,所以不能進行轉換。
但是unique_ptr給我們提供了get()成員函式,允許我們不轉移所有權的使用原始指標,而原始指標是可以轉換成我們需要的uint*或者ushort*的。
因此我們需要先呼叫bufptr.get()
取出原始指標,然後轉換為void*型別的any_ptr,再根據需要轉換為uint*或者ushort*。
4.buffer資料的寫入
4.1 byte資料的寫入
void buffer::put(byte b) {
if (size() - pos() < sz_byte) {
throw std::overflow_error("insufficient buffer to store byte");
}
byte* d = data();
*d = b;
__mv_fw_block(this, sz_byte);
}
再具體解釋怎麼寫入之前,我們先把程式碼裡面的陌生函式解釋一遍。
(1)size()
函式
size_t buffer::size() const {
return (size_t)(__size_of_block(this));
}
__size_of_block
的宏定義是
#define __size_of_block(p) (__is_big_block(p)) ? (*((uint*)(p)) ^ 0x80000000) : *((ushort*)(p))
- 如果是大塊,就講轉成int型別的p[0]異或上0x80000000。前面我們說過大塊的p[0]需要 |= 0x80000000將最高位置1達到快速判斷大小塊的目的。在獲取大塊真實的p[0]表示的size資料時,我們需要反過來取異或將1消掉得到真實的size。
- 如果是小塊,則直接取ushort型別的p[0]
(2)pos()
函式
size_t buffer::pos() const {
return (size_t)(__pos_of_block(this));
}
對應的宏定義是
#define __pos_of_s_block(p) ((ushort*)(p))[1]
#define __pos_of_b_block(p) ((uint*)(p))[1]
根據大塊還是小塊選擇uint或者ushort型別的p[1]
(3)data()
函式
byte* buffer::data() const {
return __data_of_block(this);
}
對應的宏定義:
#define __data_of_block(p) (__is_big_block(p)) ? (byte*) (((byte*)(((uint*)(p)) + 2)) + __pos_of_b_block(p)) : (byte*) (((byte*)(((ushort*)p) + 2)) + __pos_of_s_block(p))
這裡的__data_of_block有點複雜,我們以大塊為例逐步分解來看,小塊同理。
- 首先透過
(uint*)(p)
將p轉成uint*型別,然後再此基礎上 + 2(2個uint的位元組)。根據前面buffer的總體架構我們知道,buffer前兩個區域是size
與pos
。大塊的size 與 pos均為uint型別,將p轉成uint*然後再 + 2便可以實現跳轉到data塊的開始處。 - 但是讀寫buffer的過程中讀寫指標會變化,比如我們寫入了1,然後又想寫入0,如果還是從data開始處寫的話會直接覆蓋開始的1,只有從1的末尾繼續寫0才合理,換句話說我們要實現追加(append)模式。因此
((byte*)(((uint*)(p)) + 2)) + __pos_of_b_block(p))
透過+__pos_of_b_block(p)
跳轉到當前讀寫指標的位置。
小塊也是同理,只不過從uint*換成ushort*。
透過這兩步我們便可以得到當前讀寫指標所在位置的指標。
(4)__mv_fw_block(this, sz_byte);
這是一個宏定義
#define __mv_fw_block(p, d) if(__is_big_block(p)){\
((uint*)(p))[1] += (d);\
}\
else{\
((ushort*)(p))[1] += (ushort)(d);\
}
每次讀或者寫buffer的時候,我們都要更新p[1]代表的pos
,實現流的讀入或者流的寫入。
比如說要讀入12345,我們讀了1,讓pos += 1,這樣再讀就可以讀到2。如果一次讀了123, 就讓pos += 3,下次再讀就可以從4開始。寫也是同理。
介紹完這幾個函式後,我們再回到byte資料的寫入。
- 首先判斷寫入的資料是否超過buffer的大小,如果是,丟擲overflow異常
- 否則取到當前讀寫指標所在位置的
byte *d = data();
- 透過
*d = b
寫入位元組型資料b __mv_fw_block(this, sz_byte);
更新讀寫指標
4.2 int32型別資料的寫入
與簡單的byte資料直接寫入不同,多位元組型資料寫入有著順序上的講究。
我們先來看原始碼:
void buffer::put(int32 val) {
if (size() - pos() < sz_int) {
throw std::overflow_error("insufficient buffer to store int32");
}
byte* d = data();
for (size_t i = 0; i < sz_int; ++i) {
*(d + i) = (byte)(val >> (i * 8));
}
__mv_fw_block(this, sz_int);
}
前面都與byte資料的寫入大同小異,重點是後面的for迴圈
for (size_t i = 0; i < sz_int; ++i) {
*(d + i) = (byte)(val >> (i * 8));
}
sz_int
是指int的位元組個數,for迴圈遍歷了要寫入的int val
的每一個位元組。*(d + i)
是給從d開始數的第i個位元組的位置賦值。(byte)(val >> (i * 8));
是將val右移了i * 8
位,然後透過byte轉換取到右移後的低8位。
合起來就是從d開始數的第i個位元組的位置賦值成val右移了i * 8位的低8位
。
我們具體舉例來說明:
比如要放入1010001111111110010(10進位制 = 327658)。
- 第一個i = 0, 放入11110010 到buffer的d[0]。
- 第二次i = 1, 放入01111111 到buffer的d[1]。
- 第三次i = 2, 放入00010100 到buffer的d[2]。
- 第四次i = 3, 放入00000000 到buffer的d[3]。
很明顯可以看到這是將val按位元組進行了逆序儲存,為什麼不直接按原順序正向儲存呢?
答案就是因為難,我們進行位元組的轉換是透過(byte)來轉的,而這種轉換是擷取低8位而非高8位,所以我們for迴圈遍歷val的每個位元組時只能做到逆序儲存val。
4.3 ulong型別資料的寫入
void buffer::put(ulong val) {
if (size() - pos() < sz_ulong) {
throw std::overflow_error("insufficient buffer to store int32");
}
byte* d = data();
for (size_t i = 0; i < sz_ulong; ++i) {
*(d + i) = (byte)(val >> (i * 8));
}
__mv_fw_block(this, sz_ulong);
}
解析同4.2,for迴圈遍歷val的每個位元組,並且逆序儲存。
4.4 string型別資料的寫入
void buffer::put(const std::string& str) {
if (size() - pos() < (str.length() + 1)) {
throw std::overflow_error("insufficient buffer to store a string");
}
byte* d = data();
for (size_t i = 0; i < str.length(); ++i) {
*(d + i) = (byte)str[i];
}
*(d + str.length()) = (byte)0;
__mv_fw_block(this, str.length() + 1);
}
- 由於string型別每一個元素都是char型別(byte其實就是unsigned char),所以即使string也是多位元組資料,但是其不需要像int32或者ulong逆序儲存。
*(d + str.length()) = (byte)0;
將最後一個位元組置為0,表示字串的終止,與“hello World!”等c-string在末尾自動新增'\0'同理。
4.5 buffer型別資料的寫入
void buffer::put(const buffer& buf) {
size_t sz = size();
size_t p = pos();
size_t src_sz = buf.size();
size_t src_p = buf.pos();
if ((sz - p) < (src_sz - src_p)) {
throw std::overflow_error("insufficient buffer to hold the other buffer");
}
byte* d = data();
byte* src = buf.data();
::memcpy(d, src, src_sz - src_p);
__mv_fw_block(this, src_sz - src_p);
}
透過memcpy實現deep copy。
5.buffer資料的讀取
5.1 byte資料的讀取
byte buffer::get_byte() {
size_t avail = size() - pos();
if (avail < sz_byte) {
throw std::overflow_error("insufficient buffer available for a byte");
}
byte val = *data();
__mv_fw_block(this, sz_byte);
return val;
}
- 先判斷buffer是否有足夠空間,沒有就丟擲overflow異常
- 直接呼叫data(),獲取讀寫指標所在位置,並用*符號取其內容,得到所需val
__mv_fw_block(this, sz_byte);
更新讀寫指標
5.2 int32資料的讀取
int32 buffer::get_int() {
size_t avail = size() - pos();
if (avail < sz_int) {
throw std::overflow_error("insufficient buffer available for an int32 value");
}
byte* d = data();
int32 val = 0;
for (size_t i = 0; i < sz_int; ++i) {
int32 byte_val = (int32)*(d + i);
val += (byte_val << (i * 8));
}
__mv_fw_block(this, sz_int);
return val;
}
重點關注for迴圈,從d開始遍歷一個int32的所有位元組。
- 對每個位元組取出
byte_val
- 將
byte_val
左移i * 8
位後累加到val
- 返回
val
在前面4.2 int32資料的寫入中我們已經說過多位元組資料是逆序儲存的(除string型別)
我們再以1010001111111110010(10進位制 = 327658)來舉例說明
由於逆序儲存buffer中資料應該是11110010|01111111|00010100 |00000000
- i = 0, 取出byte_val = 11110010, 這個byte_val應該在原始資料裡面佔據0-7位,因此我們左移 0 * 8 = 0位並累加到val
- i = 1, 取出byte_val = 01111111, 這個byte_val應該在原始資料裡面佔據8-15位,因此我們左移 1 * 8 = 8位並累加到val
- i = 2, 取出byte_val = 00010100, 這個byte_val應該在原始資料裡面佔據16-23位,因此我們左移 2 * 8 = 16位並累加到val
- i = 3, 取出byte_val = 00000000, 這個byte_val應該在原始資料裡面佔據24-31位,因此我們左移 3 * 8 = 24位並累加到val
最後我們便得到正常順序的資料val
5.3 ulong資料的讀取
ulong buffer::get_ulong() {
size_t avail = size() - pos();
if (avail < sz_ulong) {
throw std::overflow_error("insufficient buffer available for an ulong value");
}
byte* d = data();
ulong val = 0L;
for (size_t i = 0; i < sz_ulong; ++i) {
ulong byte_val = (ulong)*(d + i);
val += (byte_val << (i * 8));
}
__mv_fw_block(this, sz_ulong);
return val;
}
分析同5.2,只是遍歷的位元組數從sz_int 變為sz_ulong
5.4 string型別的讀取
const char* buffer::get_str() {
size_t p = pos();
size_t s = size();
size_t i = 0;
byte* d = data();
while ((p + i) < s && *(d + i)) ++i;
if (p + i >= s || i == 0) {
return nilptr;
}
__mv_fw_block(this, i + 1);
return reinterpret_cast<const char*>(d);
}
string型別資料的讀取有點不一樣。
while ((p + i) < s && *(d + i)) ++i;
if (p + i >= s || i == 0) {
return nilptr;
}
- 首先我們不知道string型別資料的長度,我們只知道其以0結尾。所以要用while來一直取
- while裡面判斷0直接用
while(*(d + i)) ++i
就行了,為什麼還要加一個(p + i) < s
呢?
因為只有*(d + i)
我們並不清楚自己是否會越界,可能由於某些原因即使遍歷完buffer依舊沒找到0,
這個時候就需要我們再加一個(p + i) < s
保證不會越界 - 如果
p + i >= s
說明越界,i == 0
說明根本沒資料,都應該返回nilptr
__mv_fw_block(this, i + 1);
return reinterpret_cast<const char*>(d);
最後調整讀寫指標的__mv_fw_block(this, i + 1);
是i + 1
個位元組而不是i
的原因:
假設string = "abc"(以'\0'標記結尾),i = 2的時候while條件依舊滿足,i++→i = 3
不妨設沒讀入string前的pos = 0, 這時候__mv_fw_block
將pos += (i + 1)→pos = 4
表示下次讀寫操作都要從pos = 4開始。即跳過了"abc"與'\0'後的第一個位置。
如果__mv_fw_block(this, i + 1);
是i
個位元組,則表示下次讀寫操作要從'\0'開始,會改變'\0'導致無法該字串無法被識別。
5.5 讀取資料並存到指定的buffer裡面
void buffer::get(bufptr& dst) {
size_t sz = dst->size() - dst->pos();
::memcpy(dst->data(), data(), sz);
__mv_fw_block(this, sz);
}
- 先計算要copy的位元組數
sz
,dst->size() - dst->pos();
表示是dst所剩的所有位元組 ::memcpy(dst->data(), data(), sz);
將data()開始往後數sz
位元組的資料全部複製到dst裡面- 調整讀寫指標
__mv_fw_block(this, sz);
6.總結
- 1.在判斷大小塊面前,我們巧妙的運用最高位透過位運算來快速判斷。
- 2.buffer最重要的兩個操作便是
讀(get)
與寫(put)
,在面對多位元組資料的時候我們可以看到逆序儲存的想法。 - 3.面對智慧指標需要轉換的情況,我們應該先採取
get()
得到原始指標,然後再透過reinterpret_cast
進行轉換。