openssl框架閒談--BIO接口
在 OpenSSL中一共有兩種類型的BIO,一種是源/目的類型的,另一種是過濾類型的,其實可以統一到一種類型,那就是統一都是過濾類型,這種說法的前提 是一個古老的概念,早在unix時代,人們通常將程序看做一個過濾器,簡單的給它一個輸入就會得到一個輸出,具體會得到什麼輸出就看程序員的意圖了,那個 時候,程序沒有現在如此龐大,也沒有如此之多的智能和行爲邏輯,就是簡單的過濾功能,unix提出的一切皆文件的偉大思想就是在這個古老而又淳樸的概念之
上提出來的,unix將設備抽象成文件,將顯示器和鍵盤抽象成文件,按照程序即過濾器的思想是十分合理的,程序的輸入和輸出兩端都連接一個文件,數據從文 件中來經過過濾器到文件中去,顯示器是文件,磁盤是文件,鍵盤也是文件,甚至內存也是文件,想想標準輸入和標準輸出的概念就會很容易理解了,程序是過濾 器,被過濾的東西的載體就是文件,unix靠這個思想而穩定地運行到了今天。如此一來,考慮源/目的類型的BIO,其實也是一種過濾類型的BIO,所謂過 濾器就是一個輸入加上過濾邏輯得到一個輸出,源/目的類型的BIO的輸入就是源端,過濾邏輯就是直通,而輸出就是目的端,過濾邏輯不在乎輸出結構有沒有使
用者以及使用者是誰,它只要將輸出放到一個地方就可以。理解是可以按照統一的方式去理解,但是事實上openssl還是區分了兩種類型的BIO,在過濾類 型的BIO數據結構中,一般都提供了一個緩衝區,比如加密/解密BIO,對於write,就是加密數據,然後放到緩衝區中,就到此爲止了嗎?不,因爲過濾 數據只不過是IO的一個階段而已,openssl的BIO接口提供了BIO鏈機制,並且規定一個過濾類型的BIO必須是BIO鏈的一箇中間環節,這就是 說,最終必須將這些數據寫入一個源/目的的BIO,因爲在openssl中的BIO機制中,只有源/目的類型的BIO纔是真正的數據載體,而過濾類型的
BIO僅僅提供數據緩衝區作爲中間數據載體,這在下面的代碼體現明顯:
static int buffer_write(BIO *b, const char *in, int inl)
{
int i,num=0;
BIO_F_BUFFER_CTX *ctx;
if ((in == NULL) || (inl <= 0)) return(0);
ctx=(BIO_F_BUFFER_CTX *)b->ptr;
if ((ctx == NULL) || (b->next_bio == NULL)) return(0);
BIO_clear_retry_flags(b);
start:
i=ctx->obuf_size-(ctx->obuf_len+ctx->obuf_off);
if (i >= inl) //緩衝區不滿的情況下,就將數據加入緩衝區之後然後返回,只有在緩衝區滿了或者手動刷新的時候才刷新緩衝區
{
memcpy(&(ctx->obuf[ctx->obuf_len]),in,inl);
ctx->obuf_len+=inl;
return(num+inl);
}
if (ctx->obuf_len != 0) //加入當前的數據後刷新緩衝區
{
if (i > 0) /* lets fill it up if we can */
{
memcpy(&(ctx->obuf[ctx->obuf_len]),in,i);
...//偏移處理
ctx->obuf_len+=i;
}
for (;;) //循環刷新,直到終了或者出錯
{//調用BIO_write接口函數,注意傳入的BIO是當前BIO鏈的下一個BIO,數據就是緩衝區內的數據
i=BIO_write(b->next_bio,&(ctx->obuf[ctx->obuf_off]), ctx->obuf_len);
if (i <= 0)
...//結束或者出錯處理以及偏移處理
}
...//不考慮的情況
}
以 上是buffer類型的BIO的一個write回調函數,buffer類型的BIO是一種很簡單的過濾類型的BIO,僅僅提供兩個緩衝區用於緩衝數據,別 的什麼也不做,在buffer-BIO的上層,用戶調用BIO_write寫入數據,寫到哪裏了呢?事實上是緩存到buffer的輸出緩衝區裏面了,等到 緩衝區滿了的時候再將其刷入更底層的BIO,這個更底層的BIO可能仍然是一個過濾類型的BIO,也可能是一個源/目的類型的BIO,但是BIO鏈的最終 肯定是一個源/目的類型的BIO而不能是一個過濾類型的BIO或者是NUILL,因此在此回調函數的最後刷新緩衝區的時候還是用了BIO_write接口 函數進行數據寫入,最終肯定是寫入了一個源/目的類型的BIO代表的數據載體,比如套接字,文件,甚至內存。
更爲複雜的加密解密BIO複雜在處理細節,處理邏輯和上面的buffer-BIO是一樣的,只不過在寫入下層的BIO之前將數據加密了,在讀取的時候,從 下層BIO讀到數據之後先解密然後再傳遞給更爲上層的BIO。在openssl中,控制邏輯十分簡單,BIO_write接口函數可以被一切可以控制 BIO的實體使用,比如最終的用戶或者BIO本身,因爲BIO是鏈式的結構,因此整個過程在函數上體現了一個遞歸的過程,自己控制自己,於這個過程不同的 是有些程序的邏輯大量使用了MVC架構,其中抽象出一個控制器,這種方式固然不錯,但是我個人感覺openssl的方式看起來更加美麗。有的時候這種遞歸 的控制方式十分有效,它的特點在於將控制器集成到了回調函數本身,你難道在深感此方式難懂的同時不覺得它也很靈活嗎?
BIO的架構很簡單,基本就是一個結構體和一個回調函數集合:
struct bio_st
{
BIO_METHOD *method;//BIO方法結構,是決定BIO類型和行爲的重要參數,各種BIO的不同之處主要也正在於此項。
long (*callback)(struct bio_st *,int,const char *,int, long,long); //一個可選的回調函數,用戶可以更好的進行控制
char *cb_arg; //回調函數的參數
int init; //是否已經初始化標誌
int shutdown; //BIO是否已經打開
int flags;
int retry_reason;
int num;
void *ptr; //私有數據指針,對於不同的BIO類型有着不同的解釋,比如對於一個加密解密的BIO,它就是一個BIO_ENC_CTX
struct bio_st *next_bio;
struct bio_st *prev_bio;
int references;
unsigned long num_read;//讀出的數據長度
unsigned long num_write;//寫入的數據長度
CRYPTO_EX_DATA ex_data;
};
仍然以buffer-BIO爲例,methods_buffer就是它的重要的回調函數集合:
static BIO_METHOD methods_buffer=
{
BIO_TYPE_BUFFER,
"buffer",
buffer_write,
buffer_read,
buffer_puts,
buffer_gets,
buffer_ctrl,
buffer_new,
buffer_free,
buffer_callback_ctrl,
};
BIO_METHOD *BIO_f_buffer(void)
{
return(&methods_buffer);
}
BIO 機制提供BIO_write接口函數,該接口函數的實現就是調用不同BIO的回調函數集合中的write函數,對於這些回調函數怎麼實現就看用戶的策略 了,BIO僅僅是提供了一個總體的框架,它對內部的實現沒有任何要求,正如上面說的,控制權也被集成在了回調函數裏面,比如一個過濾類型的BIO的 write回調函數的實現中就有BIO_write(b->next_bio,...)接口函數的調用,再比如對於源/目的類型的BIO,一個 connect的socket的write回調函數就是:
static int sock_write(BIO *b, const char *in, int inl)
{
int ret;
clear_socket_error();
ret=writesocket(b->num,in,inl);
BIO_clear_retry_flags(b);
if (ret <= 0)
{
if (BIO_sock_should_retry(ret))
BIO_set_retry_write(b);
}
return(ret);
}
而 writesocket就是send函數。可能是由於openssl最初基於unix/linux家族吧,作爲一切皆文件的一種迎合,BIO提供了 BIO_set_fd接口函數,可以將一個文件描述符和一個BIO聯繫起來,當然這個接口函數的參數是void*類型的,其實你可以傳遞任何類型的參數給 它。BIO在某種意義上提供了一種關於IO的更高層次的抽象,將所有的IO操作分成了源/目的類型的和過濾類型的,其實正如前面所述,只要過濾類型就夠 了,畢竟所有的程序其實都是過濾器,IO操作就是過濾行爲,過濾本身被抽象成IO,但是那樣的話整個機制就成了一個完全的理論框架,沒有一點可操作性 了,openssl在統一視圖和可用上做了一個完美的折中,這樣就可以在不缺失可操作性的前提下最大限度的提供理論的和諧,openssl提供的BIO中 內置了很多的BIO,比如file類型,socket類型,buffer類型,加密解密類型,ssl類型等等,對於這些我們可以隨意方便的使用。
BIO提供了BIO_ctrl接口函數,使用這個函數可以對BIO進行控制,類似於標準文件操作的ioctl,類似於關聯BIO和文件描述符的操作都是使 用這個接口函數完成的,很多的操作都會定位到這個BIO_ctrl函數,對於不同的BIO類型,其ctrl也不同,其實ctrl函數也是回調函數集合中的 一個,BIO_ctrl最終會調用BIO類型特定的ctrl回調函數的。
static int buffer_write(BIO *b, const char *in, int inl)
{
int i,num=0;
BIO_F_BUFFER_CTX *ctx;
if ((in == NULL) || (inl <= 0)) return(0);
ctx=(BIO_F_BUFFER_CTX *)b->ptr;
if ((ctx == NULL) || (b->next_bio == NULL)) return(0);
BIO_clear_retry_flags(b);
start:
i=ctx->obuf_size-(ctx->obuf_len+ctx->obuf_off);
if (i >= inl) //緩衝區不滿的情況下,就將數據加入緩衝區之後然後返回,只有在緩衝區滿了或者手動刷新的時候才刷新緩衝區
{
memcpy(&(ctx->obuf[ctx->obuf_len]),in,inl);
ctx->obuf_len+=inl;
return(num+inl);
}
if (ctx->obuf_len != 0) //加入當前的數據後刷新緩衝區
{
if (i > 0) /* lets fill it up if we can */
{
memcpy(&(ctx->obuf[ctx->obuf_len]),in,i);
...//偏移處理
ctx->obuf_len+=i;
}
for (;;) //循環刷新,直到終了或者出錯
{//調用BIO_write接口函數,注意傳入的BIO是當前BIO鏈的下一個BIO,數據就是緩衝區內的數據
i=BIO_write(b->next_bio,&(ctx->obuf[ctx->obuf_off]), ctx->obuf_len);
if (i <= 0)
...//結束或者出錯處理以及偏移處理
}
...//不考慮的情況
}
以 上是buffer類型的BIO的一個write回調函數,buffer類型的BIO是一種很簡單的過濾類型的BIO,僅僅提供兩個緩衝區用於緩衝數據,別 的什麼也不做,在buffer-BIO的上層,用戶調用BIO_write寫入數據,寫到哪裏了呢?事實上是緩存到buffer的輸出緩衝區裏面了,等到 緩衝區滿了的時候再將其刷入更底層的BIO,這個更底層的BIO可能仍然是一個過濾類型的BIO,也可能是一個源/目的類型的BIO,但是BIO鏈的最終 肯定是一個源/目的類型的BIO而不能是一個過濾類型的BIO或者是NUILL,因此在此回調函數的最後刷新緩衝區的時候還是用了BIO_write接口 函數進行數據寫入,最終肯定是寫入了一個源/目的類型的BIO代表的數據載體,比如套接字,文件,甚至內存。
更爲複雜的加密解密BIO複雜在處理細節,處理邏輯和上面的buffer-BIO是一樣的,只不過在寫入下層的BIO之前將數據加密了,在讀取的時候,從 下層BIO讀到數據之後先解密然後再傳遞給更爲上層的BIO。在openssl中,控制邏輯十分簡單,BIO_write接口函數可以被一切可以控制 BIO的實體使用,比如最終的用戶或者BIO本身,因爲BIO是鏈式的結構,因此整個過程在函數上體現了一個遞歸的過程,自己控制自己,於這個過程不同的 是有些程序的邏輯大量使用了MVC架構,其中抽象出一個控制器,這種方式固然不錯,但是我個人感覺openssl的方式看起來更加美麗。有的時候這種遞歸 的控制方式十分有效,它的特點在於將控制器集成到了回調函數本身,你難道在深感此方式難懂的同時不覺得它也很靈活嗎?
BIO的架構很簡單,基本就是一個結構體和一個回調函數集合:
struct bio_st
{
BIO_METHOD *method;//BIO方法結構,是決定BIO類型和行爲的重要參數,各種BIO的不同之處主要也正在於此項。
long (*callback)(struct bio_st *,int,const char *,int, long,long); //一個可選的回調函數,用戶可以更好的進行控制
char *cb_arg; //回調函數的參數
int init; //是否已經初始化標誌
int shutdown; //BIO是否已經打開
int flags;
int retry_reason;
int num;
void *ptr; //私有數據指針,對於不同的BIO類型有着不同的解釋,比如對於一個加密解密的BIO,它就是一個BIO_ENC_CTX
struct bio_st *next_bio;
struct bio_st *prev_bio;
int references;
unsigned long num_read;//讀出的數據長度
unsigned long num_write;//寫入的數據長度
CRYPTO_EX_DATA ex_data;
};
仍然以buffer-BIO爲例,methods_buffer就是它的重要的回調函數集合:
static BIO_METHOD methods_buffer=
{
BIO_TYPE_BUFFER,
"buffer",
buffer_write,
buffer_read,
buffer_puts,
buffer_gets,
buffer_ctrl,
buffer_new,
buffer_free,
buffer_callback_ctrl,
};
BIO_METHOD *BIO_f_buffer(void)
{
return(&methods_buffer);
}
BIO 機制提供BIO_write接口函數,該接口函數的實現就是調用不同BIO的回調函數集合中的write函數,對於這些回調函數怎麼實現就看用戶的策略 了,BIO僅僅是提供了一個總體的框架,它對內部的實現沒有任何要求,正如上面說的,控制權也被集成在了回調函數裏面,比如一個過濾類型的BIO的 write回調函數的實現中就有BIO_write(b->next_bio,...)接口函數的調用,再比如對於源/目的類型的BIO,一個 connect的socket的write回調函數就是:
static int sock_write(BIO *b, const char *in, int inl)
{
int ret;
clear_socket_error();
ret=writesocket(b->num,in,inl);
BIO_clear_retry_flags(b);
if (ret <= 0)
{
if (BIO_sock_should_retry(ret))
BIO_set_retry_write(b);
}
return(ret);
}
而 writesocket就是send函數。可能是由於openssl最初基於unix/linux家族吧,作爲一切皆文件的一種迎合,BIO提供了 BIO_set_fd接口函數,可以將一個文件描述符和一個BIO聯繫起來,當然這個接口函數的參數是void*類型的,其實你可以傳遞任何類型的參數給 它。BIO在某種意義上提供了一種關於IO的更高層次的抽象,將所有的IO操作分成了源/目的類型的和過濾類型的,其實正如前面所述,只要過濾類型就夠 了,畢竟所有的程序其實都是過濾器,IO操作就是過濾行爲,過濾本身被抽象成IO,但是那樣的話整個機制就成了一個完全的理論框架,沒有一點可操作性 了,openssl在統一視圖和可用上做了一個完美的折中,這樣就可以在不缺失可操作性的前提下最大限度的提供理論的和諧,openssl提供的BIO中 內置了很多的BIO,比如file類型,socket類型,buffer類型,加密解密類型,ssl類型等等,對於這些我們可以隨意方便的使用。
BIO提供了BIO_ctrl接口函數,使用這個函數可以對BIO進行控制,類似於標準文件操作的ioctl,類似於關聯BIO和文件描述符的操作都是使 用這個接口函數完成的,很多的操作都會定位到這個BIO_ctrl函數,對於不同的BIO類型,其ctrl也不同,其實ctrl函數也是回調函數集合中的 一個,BIO_ctrl最終會調用BIO類型特定的ctrl回調函數的。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.