在 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回调函数的。