发布时间:2010-5-28 16:11
分类名称:OpenSSL
其实包含了很多种接口,用通用的函数接口,主要控制在BIO_METHOD中的不同实现函数控制,我初步估计了一下,大概有14种,包括4种filter型和10种source/sink型。
BIO是在底层覆盖了许多类型I/O接口细节的一种应用接口,如果你在程序中使用BIO,那么就可以和SSL、非加密的网络以及文件IO进行透明的连接。有两种不通的BIO接口,一种是source/sink型,一种是fileter型。
顾名思义,source/sink类型的BIO是数据源或输入数据源(我不知道sink该怎么翻译),例如,sokect BIO和文件BIO。而filter BIO就是把数据从一个BIO转换到另外一个BIO或应用接口,在转换过程中,这些数据可以不修改(如信息摘要BIO),也可以进行转换。例如在加密BIO中,如果写操作,数据就会被加密,如果是读操作,数据就会被解密。
BIO可以连接在一起成为一个BIO链(单个的BIO就是一个环节的BIO链的特例),如下是BIO的结构定义,可以看到它有上下环节的:
struct bio_st
{
BIO_METHOD *method;
/* bio, mode, argp, argi, argl, ret */
long (*callback)(struct bio_st *,int,const char *,int, long,long);
char *cb_arg; /* first argument for the callback */
int init;
int shutdown;
int flags; /* extra storage */
int retry_reason;
int num;
void *ptr;
struct bio_st *next_bio; /* used by filter BIOs */BIO下联
struct bio_st *prev_bio; /* used by filter BIOs */BIO上联
int references;
unsigned long num_read;
unsigned long num_write;
CRYPTO_EX_DATA ex_data;
};
一个BIO链通常包括一个source BIO和一个或多个filter BIO,数据从第一个BIO读出或写入,然后经过一系列BIO变化到输出(通常是一个source/sink BIO)。
BIO的结构定义和相关项解析如下:(包含在bio.h文件中,其主文件为bio_lib.c)
typedef struct bio_st BIO;
struct bio_st
{
BIO_METHOD *method;
/*BIO方法结构,是决定BIO类型和行为的重要参数,各种BIO的不同之处主要也正在于此项。*/
/* bio, mode, argp, argi, argl, ret */
long (*callback)(struct bio_st *,int,const char *,int, long,long); //BIO回调函数
char *cb_arg; /* first argument for the callback */ //回调函数的第一个参量
int init; //初始化标志,初始化了为1,否则为0
int shutdown; //BIO开关标志,如果为1,则处于关闭状态,如果为0,则处于打开的状态。
int flags; /* extra storage */
int retry_reason;
int num;
void *ptr;
struct bio_st *next_bio; /* used by filter BIOs */BIO下联
struct bio_st *prev_bio; /* used by filter BIOs */BIO上联
int references;
unsigned long num_read;//读出的数据长度
unsigned long num_write;//写入的数据长度
CRYPTO_EX_DATA ex_data;
};
在BIO的所用成员中,method可以说是最关键的一个成员,它决定了BIO的类型,可以看到,在声明一个新的BIO结构时,总是使用下面的声明:
BIO* BIO_new(BIO_METHOD *type);
在源代码可以看出,BIO_new函数除了给一些初始变量赋值外,主要就是把type中的各个变量赋值给BIO结构中的method成员。一般来说,上述type参数是以一个类型生成函数的形式提供的,如生成一个mem型的BIO结构,就使用下面的语句:
BIO *mem = BIO_new(BIO_s_mem());
这样的函数有以下一些:
1. BIO_s_accept():是一个封装了类似TCP/IP socket Accept规则的接口,并且使TCP/IP操作对于BIO接口是透明的。
2. BIO_s_bio():封装了一个BIO对,数据从其中一个BIO写入,从另外一个BIO读出
3. BIO_s_connect():是一个封装了类似TCP/IP socket Connect规则的接口,并且使TCP/IP操作对于BIO接口是透明的
4. BIO_s_fd():是一个封装了文件描述符的BIO接口,提供类似文件读写操作的功能
5. BIO_s_file():封装了标准的文件接口的BIO,包括标志的输入输出设备如stdin等
6. BIO_s_mem():封装了内存操作的BIO接口,包括了对内存的读写操作
7. BIO_s_null():返回空的sink型BIO接口,写入这种接口的所有数据读被丢弃,读的时候总是返回EOF
8. BIO_s_socket():封装了socket接口的BIO类型
1. BIO_f_base64():封装了base64编码方法的BIO,写的时候进行编码,读的时候解码
2. BIO_f_buffer():封装了缓冲区操作的BIO,写入该接口的数据一般是准备传入下一个BIO接口的,从该接口读出的数据一般也是从另一个BIO传过来的。
3. BIO_f_cipher():封装了加解密方法的BIO,写的时候加密,读的时候解密
4. BIO_f_md():封装了信息摘要方法的BIO,通过该接口读写的数据都是已经经过摘要的。
5. BIO_f_null():一个不作任何事情的BIO,对它的操作都简单传到下一个BIO去了,相当于不存在。
6. BIO_f_ssl():封装了openssl 的SSL协议的BIO类型,也就是为SSL协议增加了一些BIO操作方法。
上述各种类型的函数正是构成BIO强大功能的基本单元,所以,要了解BIO的各种结构和功能,也就应该了解这些函数类型相关的操作函数。所有这些源文件,都基本上包含于/crypto/bio/目录下的同名.c文件(大部分是同名的)中。
在BIO_METHOD里面,定义了一组行为函数,上述不同类型的BIO_METHOD行为函数的定义是不同的,其结构如下(以非16位系统为例):
typedef struct bio_method_st
{
int type;
const char *name;
int (*bwrite)(BIO *, const char *, int);
int (*bread)(BIO *, char *, int);
int (*bputs)(BIO *, const char *);
int (*bgets)(BIO *, char *, int);
long (*ctrl)(BIO *, int, long, void *);
int (*create)(BIO *);
int (*destroy)(BIO *);
long (*callback_ctrl)(BIO *, int, bio_info_cb *);
} BIO_METHOD;
在BIO的成员中,callback也是比较重要的,它能够用于程序调试用或者自定义改变BIO的行为。详细会在以后相关的部分介绍。BIO的很多操作,都是BIO_ctrl系列函数根据不同参数组成的宏定义来完成的。所以要了解BIO的行为,了解BIO_ctrl系列函数以及其各个参数的意义也是很重要的。
1. bio.h:主定义的头文件,包括了很多通用的宏的定义。
2. bio_lib.c主要的BIO操作定义文件,是比较上层的函数了。
3. bss_*系列:是soruce/sink型BIO具体的操作实现文件
4. bf_*系列:是filter型BIO具体的操作实现文件
5. bio_err.c:是错误信息处理文件
6. bio_cb.c:是callback函数的相关文件
7. b_print.c:是信息输出的处理函数文件
8. b_socket.c:是Socket连接的一些相关信息处理文件
9. b_dump.c:是对内存内容的存储操作处理
在BIO的基本操作系列函数中,他们用来BIO分配和释放操作,其声明形式如下(openssl/bio.h):
BIO * BIO_new(BIO_METHOD *type);
int BIO_set(BIO *a,BIO_METHOD *type);
int BIO_free(BIO *a);
void BIO_vfree(BIO *a);
void BIO_free_all(BIO *a);
下面分别对这些函数进行解释:
这个函数创建并返回一个相应的新的BIO,并根据给定的BIO_METHOD类型调用下述的BIO_set()函数给BIO结构的method成员赋值,如果创建或给method赋值失败,则返回NULL。创建一个Memory类型的BIO例子如下:
BIO* mem=BIO_new(BIO_s_mem());
有些类型的BIO使用BIO_new()函数之后就可以直接使用了,如memory类型的BIO;而有些BIO创建之后还需要一些初始化工作,如文件BIO。一般来说,也提供了这样的一些函数来创建和初始化这种类型的BIO。
这是什么意思呢,举个简单的例子大家就明白了。比如创建一个文件BIO,使用下面的代码:
BIO* in=NULL;
in=BIO_new(BIO_s_file());
BIO_read_filename(in,"rsa512.pem");
这样,BIO in才能使用,而如果是创建一个memory类型的BIO,则只需要如下一句代码:
BIO* mem=BIO_new(BIO_s_mem());
然后就可以对该BIO mem进行操作了。
另外,需要补充的是(这个大家从前面两篇文章可能已经认识到了),对于source/sink类型的BIO,其类型创建函数一般为BIO_s_*的形式,对于filter型的函数,其类型创建函数一般为BIO_f_*的形式。
该函数功能比较简单,就是对一个已经存在的BIO设置新的BIO_METHOD类型。其实就是简单的对BIO的各个成员进行初始化,并将参数type赋值给该BIO。其实,BIO_new函数在使用OPENSSL_malloc给BIO分配了内存之后,就简单调用了BIO_set函数进行初始化工作。所以一般来说,除非你要重新设置你已经存在的BIO,否则是不需要直接调用这个函数的。成功操作返回1,否则返回0。
该函数释放单个BIO的内存和资源,成功操作返回1,失败返回0。BIO的操作不仅仅是释放BIO结构所占用的资源,也会释放其下层的I/O资源,比如关闭释放相关的文件符等,这对不同类型的BIO是不一样的,详细的请参看各种类型BIO本身的说明文件和源文件。需要注意的是,BIO_free只释放当前的一个BIO,如果用来释放一个BIO链,就可能会导致内存泄漏,这种情况应该使用下述的BIO_free_all函数。
该函数功能与BIO_free完全相同,只是没有返回值。事实上,它简单调用了BIO_free函数,但不返回该函数的返回值,所以它的函数实现代码只有一个语句。
该函数释放这个BIO链,并且即使在这个过程中,如果释放其中一个BIO出错,释放过程也不会停止,会继续释放下面的BIO,这保证了尽量避免内存泄漏的出现。如果你非要调用这个函数释放单个的BIO,那么效果跟BIO_free是一样的。事实上,该函数只是简单的遍历整个BIO链,并调用BIO_free释放各个环节的BIO。
BIO控制函数有许多,并且不同的BIO类型还有不同的控制函数,这里只简单介绍一些通用的BIO控制函数,至于某种类型BIO的特定控制函数,则参考后续的文件。
BIO的通用控制函数有以下几种,其声明如下(openssl/bio.h):
long BIO_ctrl(BIO *bp,int cmd,long larg,void *parg);
long BIO_callback_ctrl(BIO *b, int cmd, void (*fp)(struct bio_st *, int, const char *, int, long, long));
char * BIO_ptr_ctrl(BIO *bp,int cmd,long larg);
long BIO_int_ctrl(BIO *bp,int cmd,long larg,int iarg);
int BIO_reset(BIO *b);
int BIO_seek(BIO *b, int ofs);
int BIO_tell(BIO *b);
int BIO_flush(BIO *b);
int BIO_eof(BIO *b);
int BIO_set_close(BIO *b,long flag);
int BIO_get_close(BIO *b);
int BIO_pending(BIO *b);
int BIO_wpending(BIO *b);
size_t BIO_ctrl_pending(BIO *b);
size_t BIO_ctrl_wpending(BIO *b);
其实,在这些函数中,除了BIO_ctrl、BIO_callback_ctrl、BIO_ptr_ctrl,BIO_int_ctrl、BIO_ctrl_pending、BIO_ctrl_wpending是真正的函数外,其它都是宏定义,而且,在这些函数中,除了BIO_ctrl、BIO_callback_ctrl,其它基本上都是简单的BIO_ctrl输入不同的参数的调用。下面就一个一个介绍这些函数。
从上面的叙述可以知道,BIO_ctrl是整个控制函数中最基本的函数,它支持不同的命令输入,从而产生不同的功能,由此,它也就衍生了许多其它函数,作为一个比较地层的控制函数,一般来说用户并不需要直接调用它,因为在它之上已经使用宏定义和函数调用的形式建造了许多直接面向用户的函数。
filter型的BIO没有定义BIO_ctrl功能,如果对他们调用这个函数,他们就简单的把命令传到BIO链中的下一个BIO。也就是说,通常可以不用直接调用一个BIO的BIO_ctrl函数,只需要在它所在的BIO链上调用该函数,那么BIO链就会自动将该调用函数传到相应的BIO上去。这样可能就会导致一些意想不到的结果,比如,在目前的filter型BIO中没有实现BIO_seek()函数(大家待会就会明白BIO_seek就是BIO_ctrl的简单宏定义),但如果在这个BIO链上的末尾是一个文件或文件描述符型BIO,那么这个调用也会返回成功的结果。
对于source/sink型BIO来说,如果他们不认得BIO_ctrl所定义的操作,那么就返回0。
这个函数是这组控制函数中唯一一个不是通过调用BIO_ctrl建立起来的,它有自己的实现函数,而且跟BIO_ctrl毫不相干。跟BIO_ctrl一样,它也是比较底层的控制函数,在它上面也定义了一些直接面向用户的控制函数,一般来说,用户不需要直接调用该函数。
需要说明的是,该函数和BIO_ctrl函数为了实现不同类型BIO具有不同的BIO_ctrl控制功能,他们的操作基本上都是由各个BIO的callback函数来定义的。这是不同的BIO能灵活实现不同功能的根本所在。
这两个函数都是简单的调用了BIO_ctrl函数,不同的是,后者是输入了四个参数并传入到BIO_ctrl函数中,简单返回了调用BIO_ctrl返回的返回值;而前者只输入了三个参数,最后一个BIO_ctrl参数是作为输出参数并作为返回值的。
该函数是BIO_ctrl的宏定义函数,为了大家对BIO_ctrl的宏定义函数有一个感性的认识,我把这个宏定义写出来,如下:
#define BIO_reset(b) (int)BIO_ctrl(b,BIO_CTRL_RESET,0,NULL)
这就是BIO_ctrl的典型宏定义方式,它通过这种方式产生了大量的控制函数。顾名思义,BIO_reset函数只是简单的将BIO的状态设回到初始化的时候的状态,比如文件BIO,调用该函数就是将文件指针指向文件开始位置。一般来说,调用成功的时候该函数返回1,失败的时候返回0或-1;但是文件BIO是一个例外,成功调用的时候返回0,失败的时候返回-1。
该函数也是BIO_ctrl的宏定义函数,其定义如下:
#define BIO_seek(b,ofs) (int)BIO_ctrl(b,BIO_C_FILE_SEEK,ofs,NULL)
该函数将文件相关的BIO(文件和文件描述符类型)的文件指针知道距离开始位置ofs(输入参数)字节的位置上。调用成功的时候,返回文件的位置指针,否则返回-1;但是文件BIO例外,成功的时候返回0,失败的时候返回-1。
该函数也是BIO_ctrl的宏定义函数,其定义如下:
#define BIO_tell(b) (int)BIO_ctrl(b,BIO_C_FILE_TELL,0,NULL)
该函数返回了文件相关BIO的当前文件指针位置。跟BIO_seek一样,调用成功的时候,返回文件的位置指针,否则返回-1;但是文件BIO例外,成功的时候返回0,失败的时候返回-1。
该函数也是BIO_ctrl的宏定义函数,其定义如下:
#define BIO_flush(b) (int)BIO_ctrl(b,BIO_CTRL_FLUSH,0,NULL)
该函数用来将BIO内部缓冲区的数据都写出去,有些时候,也用于为了根据EOF查看是否还有数据可以写。调用成功的时候该函数返回1,失败的时候返回0或-1。之所以失败的时候返回0或者-1,是为了标志该操作是否需要稍后以跟BIO_write()相同的方式重试。这时候,应该调用BIO_should_retry()函数,当然,正常的情况下该函数的调用应该是失败的。
该函数也是BIO_ctrl的宏定义函数,其定义如下
#define BIO_eof(b) (int)BIO_ctrl(b,BIO_CTRL_EOF,0,NULL)
如果BIO读到EOF,该函数返回1,至于EOF的具体定义,根据BIO的类型各不相同。如果没有读到EOF,该函数返回0。
该函数也是BIO_ctrl的宏定义函数,其定义如下:
#define BIO_set_close(b,c) (int)BIO_ctrl(b,BIO_CTRL_SET_CLOSE,(c),NULL)
该函数设置BIO的关闭标志,该标志可以为BIO_CLOSE或BIO_NOCLOSE。一般来说,该标志是为了指示在source/sink型BIO释放该BIO的时候是否关闭其下层的I/O流。该函数总是返回1。
该函数也是BIO_ctrl的宏定义函数,其定义如下:
#define BIO_get_close(b) (int)BIO_ctrl(b,BIO_CTRL_GET_CLOSE,0,NULL)
该函数读取BIO的关闭标志,返回BIO_CLOSE或BIO_NOCLOSE。
这些函数都是用来得到BIO中读缓存或写缓存中字符的数目的,返回相应缓存中字符的数目。前面两个函数也是BIO_ctrl的宏定义函数,其定义如下:
#define BIO_pending(b) (int)BIO_ctrl(b,BIO_CTRL_PENDING,0,NULL)
#define BIO_wpending(b) (int)BIO_ctrl(b,BIO_CTRL_WPENDING,0,NULL)
后两个函数功能跟他们是一样的,只不过他们是通过调用BIO_ctrl函数实现的,而不是宏定义。此外,前面两个函数返回的是int型,而后面两个函数返回的是size_t型。
需要注意的是,BIO_pending和 BIO_wpending并不是在所有情况下都能很可靠地得到缓存数据的数量,比如在文件BIO中,有些数据可能在文件内部结构的缓存中是有效的,但是不可能简单的在BIO中得到这些数据的数量。而在有些类型BIO中,这两个函数可能还不支持。基于此,Openssl作者本身也建议一般不要使用这两个函数,而是使用后面两个,除非你对你所做的操作非常清楚和了解。
通过前面的介绍大家已经知道,BIO的callback函数是非常重要的,是实现BIO多态性的一个关键因素之一,BIO提供的callback控制系列函数有五个,其实都是一些宏定义,下面是它的声明和定义(openssl/bio.h):
#define BIO_set_callback(b,cb) ((b)->callback=(cb))
#define BIO_get_callback(b) ((b)->callback)
#define BIO_set_callback_arg(b,arg) ((b)->cb_arg=(char *)(arg))
#define BIO_get_callback_arg(b) ((b)->cb_arg)
其中,callback函数本身的声明如下:
typedef long callback(BIO *b, int oper, const char *argp, int argi, long argl, long retvalue);
此外,还有一个用于调试目的的函数,其实声明如下:
long BIO_debug_callback(BIO *bio,int cmd,const char *argp,int argi,long argl,long ret);
如果要看具体的例子,那么在文件crypto/bio/bio_cb.c的函数BIO_debug_callback()本身就是一个非常好的例子。
下面,我们从callback函数本身开始分别简单介绍这些函数的作用。
callback函数在BIO中非常重要,许多控制功能都是要通过callbank函数协助完成的,比如BIO要执行释放的操作BIO_free,那么其实它是先调用callback函数设置下面的操作将是释放操作(控制码:BIO_CB_FREE),然后才调用别的相关函数执行真正的操作,在后面我们会列出这些控制功能函数,并简单说明callback函数是怎么在这些功能的实现中使用的。现在,我先简单介绍callback函数的各个参数:(参数名字参看说明的函数的声明)
1. 参数-b:这是callback函数的输入参数,也就是callback函数对应的BIO
2. 参数-oper:设置BIO将要执行的操作,有些操作,callback函数将被调用两次,一次实在实际操作之前,一次实在实际操作之后,在后面的调用的时候,一般是将oper和BIO_CB_RETURN相或操作后作为参数的。也就是说,后一次调用的时候oper参数应该使用oper|BIO_CB_RETURN。
3. 参数-argp、argi、argl:这些参数根据oper定义的操作的不同而不一样,是在相应操作中要用到的参数。
4. 参数-retvalue:这是默认的callback函数返回值,也就是说,如果没有提供BIO没有提供相应的callback函数,那么就会返回这个值。真正的返回值是callback函数本身提供的。如果在实际的操作之前调用callback函数,并且这时候retvalue参数设置为1,如果callback的函数返回值无效,那么对callback函数的调用就会导致程序立刻返回,BIO的操作就不会执行。
一般情况下,callback函数在执行完后都应该返回retvalue的值,除非该操作有特别的目的要修改这个返回值。下面简单列出我们比较熟悉的一些跟callback函数相关的BIO函数使用callback函数的情况:
1. BIO_free(b):在执行该操作之前,调用了callback(b, BIO_CB_FREE, NULL,
2. BIO_read(b,out,outl):在执行该操作之前,调用了callback(b, BIO_CB_READ, out, outl,
3. BIO_write(b,in,inl):在执行该操作之前,调用了callback(b, BIO_CB_WRITE, in, inl,
4. BIO_gets(b,out,outl):在执行该操作之前,调用了callback(b, BIO_CB_GETS, out, outl,
5. BIO_puts(b, in):在执行该操作之前,调用了callback(b, BIO_CB_WRITE, in, 0,
6. BIO_ctrl(BIO *b, int cmd, long larg, void *parg):在执行该操作之前,调用了callback(b,BIO_CB_CTRL,parg,cmd,larg,
这两个函数用于设置和返回BIO中的callback函数,它们都是宏定义,根据前面的叙述我们已经知道,callback函数在许多高层的操作中都使用了,因为它能用于调试跟踪的目的或更改BIO的操作,具有很大的灵活性,所以这两个函数也就有用武之地了。
顾名思义,这两个函数用了设置和得到callback函数中的参数。
这是一个标准的调试信息输出函数,它把相关BIO执行的所有操作信息都打印输出到制定的地方。如果callback参数没有指定输出这些信息的BIO口,那么就会默认使用stderr作为信息输出端口。
这些函数是BIO的基本读写操作函数,包括四个,他们的定义如下(openssl/bio.h):
int BIO_read(BIO *b, void *buf, int len);
int BIO_gets(BIO *b,char *buf, int size);
int BIO_write(BIO *b, const void *buf, int len);
int BIO_puts(BIO *b,const char *buf);
从BIO接口中读出指定数量字节len的数据并存储到buf中。成功就返回真正读出的数据的长度,失败返回0或-1,如果该BIO没有实现本函数则返回-2。
该函数从BIO中读取一行长度最大为size的数据。通常情况下,该函数会以最大长度限制读取一行数据,但是也有例外,比如digest型的BIO,该函数会计算并返回整个digest信息。此外,有些BIO可能不支持这个函数。成功就返回真正读出的数据的长度,失败返回0或-1,如果该BIO没有实现本函数则返回-2。需要注意的时,如果相应的BIO不支持这个函数,那么对该函数的调用可能导致BIO链自动增加一个buffer型的BIO。
往BIO中写入长度为len的数据。成功就返回真正写入的数据的长度,失败返回0或-1,如果该BIO没有实现本函数则返回-2。
往BIO中写入一个以NULL为结束符的字符串,成功就返回真正写入的数据的长度,失败返回0或-1,如果该BIO没有实现本函数则返回-2。
需要注意的是,返回指为0或-1的时候并不一定就是发生了错误。在非阻塞型的source/sink型或其它一些特定类型的BIO中,这仅仅代表目前没有数据可以读取,需要稍后再进行该操作。
有时候,你可能会使用了阻塞类型的sokect使用的一些系统调用技术(如select、poll、equivalent)来决定BIO中是否有有效的数据被read函数读取,但建议不要在阻塞型的接口中使用这些技术,因为这样的情况下如果调用BIO_read就会导致在底层的IO中多次调用read函数,从而导致端口阻塞。建议select(或equivalent)应该和非阻塞型的IO一起使用,可以在失败之后能够重新读取该IO,而不是阻塞住了。
关于BIO的IO操作为什么会失败以及怎么处理这些情况请参加BIO_should_retry()函数的说明文档。
BIO结构其实是一个链式结构,单个BIO是只有一个环节的BIO链的特例,那么我们怎么构造或在一个BIO链中增加一个BIO,怎么从一个BIO链中删除一个BIO呢,本节就是专门讲述这个问题的。
在Openssl中,针对BIO链的操作还是很简单的,仅仅包括两个函数(openssl/bio.h):
BIO * BIO_push(BIO *b,BIO *append);
BIO * BIO_pop(BIO *b);
该函数把参数中名为append的BIO附加到名为b的BIO上,并返回b。其实,openssl作者本身也认识到,BIO_push的函数名字可能会导致误会,因为BIO_push函数其实只是将两个BIO连接起来,而不是Push的功能,应该是join才对。
我们举几个简单的例子说明BIO_push的作用,假设md1、md2是digest类型的BIO,b64是Base64类型的BIO,而f是file类型的BIO,那么如果执行操作
BIO_push(b64, f);
那么就会形成一个b64-f的链。然后再执行下面的操作:
BIO_push(md2, b64);
BIO_push(md1, md2);
那么就会形成md1-md2-b64-f的BIO链,大家可以看到,在构造完一个BIO后,头一个BIO就代表了整个BIO链,这根链表的概念几乎是一样的。这时候,任何写往md1的数据都会经过md1、md2的摘要,然后经过base64编码,最后写入文件f。可以看到,构造一条好的BIO链后,操作是非常方便的,你不用再关心具体的事情了,整个BIO链会自动将数据进行指定操作的系列处理。
需要注意的是,如果是读操作,那么数据会从相反的方向传递和处理,对于上面的BIO链,数据会从f文件读出,然后经过base64解码,然后经过md1、md2编码,最后读出。
该函数把名为b的BIO从一个BIO链中移除并返回下一个BIO,如果没有下一个BIO,那么就返回NULL。被移除的BIO就成为一个单个的BIO,跟原来的BIO链就没有关系了,这样你可以把它释放或连接到另一个BIO上去。可以看到,如果是单个BIO的时候,该操作是没有任何意义的。如果你执行操作
BIO_pop(md2);
那么返回值将为b64,而md2从上述的链中移除,形成一个新的md1-b64-f的BIO链,对于数据操作来说,还是往md1读写,没有什么变化,但是底层处理过程已经发生变化了,这就是封装与透明的概念。可以看到,虽然BIO_pop参数只是一个BIO,但该操作直接的后果会对该BIO所在的链产生影响,所以,当BIO所在的链不一样的时候,其结果是不一样的。
此外:BIO_push和BIO_pop操作还可能导致其它一些附加的结果,一些相关的BIO可能会调用一些控制操作,这些具体的细节因为各个类型的BIO不一样,在他们各自的说明中会有说明。