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回調函數的。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章