系統(tǒng)程序員成長計(jì)劃-文本處理(三)
管道過濾器(Pipe-And-Filter)模式
按照《POSA(面向模式的軟件架構(gòu))》里的說法,管道過濾器(Pipe-And-Filter)應(yīng)該屬于架構(gòu)模式,因?yàn)樗ǔQ定了一個(gè)系統(tǒng)的基
本架構(gòu)。管道過濾器和生產(chǎn)流水線類似,在生產(chǎn)流水線上,原材料在流水線上經(jīng)一道一道的工序,最后形成某種有用的產(chǎn)品。在管道過濾器中,數(shù)據(jù)經(jīng)過一個(gè)一個(gè)的
過濾器,最后得到需要的數(shù)據(jù)。
o 基本的管道過濾器:

管道負(fù)責(zé)數(shù)據(jù)的傳遞,它把原始數(shù)據(jù)傳遞給第一個(gè)過濾器,把一個(gè)過濾器的輸出傳遞給下一個(gè)過濾器,作為下一個(gè)過濾器的輸入,重復(fù)這個(gè)過程直到處理結(jié)
束。要注意的是,管道只是對數(shù)據(jù)傳輸?shù)某橄螅赡苁枪艿溃部赡苁瞧渌ㄐ欧绞剑踔潦裁炊紱]有(所有過濾器都在原始數(shù)據(jù)基礎(chǔ)上進(jìn)行處理)。
過濾器負(fù)責(zé)數(shù)據(jù)的處理,過濾器可以有多個(gè),每個(gè)過濾器對數(shù)據(jù)做特定的處理,它們之間沒有依賴關(guān)系,一個(gè)過濾器不必知道其它過濾器的存在。這種松耦合
的設(shè)計(jì),使得過濾器只需要實(shí)現(xiàn)單一的功能,從而降低了系統(tǒng)的復(fù)雜度,也使得過濾器之間依賴最小,從而以更加靈活的組合來實(shí)現(xiàn)新的功能。
編譯器就是基于管道過濾器模式設(shè)計(jì)的:

輸入:源程序
預(yù)處理:負(fù)責(zé)宏展開和去掉注釋等工作。
編譯:進(jìn)行詞法分析、語法分析、語義分析、代碼優(yōu)化和代碼產(chǎn)生。
匯編:負(fù)責(zé)把匯編代碼轉(zhuǎn)換成機(jī)器指令,生成目標(biāo)文件。
鏈接:負(fù)責(zé)把多個(gè)目標(biāo)文件、靜態(tài)庫和共享庫鏈接成可執(zhí)行文件/共享庫。
輸出:可執(zhí)行文件/共享庫。
o 復(fù)合過濾器
過濾器可以由多個(gè)其它過濾器組合起來的,比如上面的“編譯”過程可以認(rèn)為是一個(gè)復(fù)合過濾器:
輸入:預(yù)處理之后的源代碼。
詞法分析:負(fù)責(zé)將源程序分解成一個(gè)一個(gè)的token,這些token是組成源程序的基本單元。
語法分析:把詞法分析得到的token解析成語法樹。
語義分析:對語法樹進(jìn)行類型檢查等語義分析。
代碼優(yōu)化:對語法樹進(jìn)行重組和修改,以優(yōu)化代碼的速度和大小。
代碼產(chǎn)生:根據(jù)語法樹產(chǎn)生匯編代碼。
輸出:匯編代碼。
o 支持多個(gè)輸入的過濾器
過濾器可以有多個(gè)輸入。比如上面“鏈接”,它接收多個(gè)輸入:

“鏈接”過濾器能接收多個(gè)數(shù)據(jù)源,如目標(biāo)文件、靜態(tài)庫和共享庫。
o 具有多個(gè)輸出的過濾器
過濾器可以有多個(gè)輸出。如多媒體播放器的解碼過程:

輸入:AVI文件,包括音頻和視頻數(shù)據(jù)。
分離器:把音頻和視頻數(shù)據(jù)分離成兩個(gè)流,音頻數(shù)據(jù)傳遞給音頻解碼器,視頻數(shù)據(jù)傳遞給視頻解碼器。
音頻解碼器:把壓縮的音頻數(shù)據(jù)解碼成原始的音頻數(shù)據(jù)。
視頻解碼器:把壓縮的視頻數(shù)據(jù)解碼成原始的圖像數(shù)據(jù)。
輸出:音頻數(shù)據(jù)傳遞給聲卡,圖像數(shù)據(jù)傳遞給顯示器。
管道過濾器是最貼近程序員生活的模式,也是Unix-like系統(tǒng)的基本設(shè)計(jì)理念之一。作為Linux下的程序員,我們天天都使用這個(gè)模式。比如:
o刪除當(dāng)前目錄及子目錄下的目標(biāo)文件。
find -name \*.o|xargs rm –f
find是過濾器:它找出所有目標(biāo)文件,它不需要關(guān)心查找文件的目的。
rm是過濾器:它刪除找到目標(biāo)文件,rm不需要關(guān)心文件名是如何得來的。
o 查看某個(gè)進(jìn)程的棧的大小
grep stack /proc/2976/maps|sed -e “s/-/ /”|awk ‘{print
strtonum(“0x”$2)-strtonum(“0x”$1)}’
/proc/2976/maps是進(jìn)程2976內(nèi)存映射表。內(nèi)容可能如下:
00110000-00111000 r-xp 00110000 00:00 0 [vdso]
00111000-0011b000 r-xp 00000000 08:01 154857 /lib/libnss_files-2.8.so
0011b000-0011c000 r--p 0000a000 08:01 154857 /lib/libnss_files-2.8.so
0011c000-0011d000 rw-p 0000b000 08:01 154857 /lib/libnss_files-2.8.so
00907000-00923000 r-xp 00000000 08:01 157280 /lib/ld-2.8.so
00923000-00924000 r--p 0001c000 08:01 157280 /lib/ld-2.8.so
00924000-00925000 rw-p 0001d000 08:01 157280 /lib/ld-2.8.so
00927000-00a8a000 r-xp 00000000 08:01 157281 /lib/libc-2.8.so
00a8a000-00a8c000 r--p 00163000 08:01 157281 /lib/libc-2.8.so
00a8c000-00a8d000 rw-p 00165000 08:01 157281 /lib/libc-2.8.so
00a8d000-00a90000 rw-p 00a8d000 00:00 0
00abd000-00ac0000 r-xp 00000000 08:01 157284 /lib/libdl-2.8.so
00ac0000-00ac1000 r--p 00002000 08:01 157284 /lib/libdl-2.8.so
00ac1000-00ac2000 rw-p 00003000 08:01 157284 /lib/libdl-2.8.so
0383f000-03855000 r-xp 00000000 08:01 157307 /lib/libtinfo.so.5.6
03855000-03858000 rw-p 00015000 08:01 157307 /lib/libtinfo.so.5.6
08047000-080fa000 r-xp 00000000 08:01 1180910 /bin/bash
080fa000-080ff000 rw-p 000b3000 08:01 1180910 /bin/bash
080ff000-08104000 rw-p 080ff000 00:00 0
088bd000-088ff000 rw-p 088bd000 00:00 0 [heap]
b7bfb000-b7bfd000 rw-p b7bfb000 00:00 0
b7bfd000-b7c04000 r--s 00000000 08:01 237138 /usr/lib/gconv/gconv-modules.cache
b7c04000-b7d1e000 r--p 047d3000 08:01 237437 /usr/lib/locale/locale-archive
b7d1e000-b7d5e000 r--p 0236e000 08:01 237437 /usr/lib/locale/locale-archive
b7d5e000-b7f5e000 r--p 00000000 08:01 237437 /usr/lib/locale/locale-archive
b7f5e000-b7f60000 rw-p b7f5e000 00:00 0
bfe5e000-bfe73000 rw-p bffeb000 00:00 0 [stack]
grep是過濾器:它從文件/proc/2976/maps里找到下面這行數(shù)據(jù)。
bfe5e000-bfe73000 rw-p bffeb000 00:00 0 [stack]
sed是過濾器:它把‘-’替換成‘ ’,數(shù)據(jù)變成下面的內(nèi)容。
bfe5e000 bfe73000 rw-p bffeb000 00:00 0 [stack]
awk是過濾器:它計(jì)算0xbfe73000和0x bfe5e000差值,并打印出來。
下面我們來看看,管道過濾器在程序里的實(shí)現(xiàn)方式。這里我們以TinyMail為例,TinyMail是一款針對移動(dòng)設(shè)備定制的郵件客戶端軟件。它使
用camel-lite完成郵件內(nèi)容解析和傳輸。Camel-lite對郵件內(nèi)容的處理基本上基于管道過濾器模式的。
CamelMimeFilter是過濾器接口,所有過濾器都要實(shí)現(xiàn)它要求的接口函數(shù):
struct _CamelMimeFilterClass {
CamelObjectClass parent_class;
void (*filter)(CamelMimeFilter *f,
char *in, size_t len, size_t prespace,
char **out, size_t *outlen, size_t *outprespace);
void (*complete)(CamelMimeFilter *f,
char *in, size_t len, size_t prespace,
char **out, size_t *outlen, size_t *outprespace);
void (*reset)(CamelMimeFilter *f);
};
CamelMimeFilterClass是從CamelObjectClass繼承過來的。這里的接口定義和我們前面所講的接口定義有些差別,但
原理上都是一樣,通過函數(shù)指針來抽象具體的功能。這里要求實(shí)現(xiàn)三個(gè)接口函數(shù):
filter:過濾器的處理函數(shù)。
complete:過濾器的處理函數(shù)。與filter不同的是,調(diào)用complete之后不能調(diào)其它filter。
reset:重置當(dāng)前filter的狀態(tài)。
Camel實(shí)現(xiàn)了很多Filter,其中CamelMimeFilterBasic實(shí)現(xiàn)了郵件基本的編碼和解析功能。它的filter函數(shù)實(shí)現(xiàn)如
下:
static void
filter(CamelMimeFilter *mf, char *in, size_t len, size_t prespace, char **out, size_t *outlen, size_t *outprespace)
{
CamelMimeFilterBasic *f = (CamelMimeFilterBasic *)mf;
size_t newlen;
switch(f->type) {
case CAMEL_MIME_FILTER_BASIC_BASE64_ENC:
/* wont go to more than 2x size (overly conservative) */
camel_mime_filter_set_size(mf, len*2+6, FALSE);
newlen = g_base64_encode_step((const guchar *) in, len, TRUE, mf->outbuf, &f->state, &f->save);
g_assert(newlen <= len*2+6);
break;
case CAMEL_MIME_FILTER_BASIC_QP_ENC:
/* *4 is overly conservative, but will do */
camel_mime_filter_set_size(mf, len*4+4, FALSE);
newlen = camel_quoted_encode_step((unsigned char *) in, len, (unsigned char *) mf->outbuf, &f->state, (gint *) &f->sa
ve);
g_assert(newlen <= len*4+4);
break;
case CAMEL_MIME_FILTER_BASIC_UU_ENC:
/* won't go to more than 2 * (x + 2) + 62 */
camel_mime_filter_set_size (mf, (len + 2) * 2 + 62, FALSE);
newlen = camel_uuencode_step ((unsigned char *) in, len, (unsigned char *) mf->outbuf, f->uubuf, &f->state, (guint32
*) &f->save);
g_assert (newlen <= (len + 2) * 2 + 62);
break;
case CAMEL_MIME_FILTER_BASIC_BASE64_DEC:
/* output can't possibly exceed the input size */
camel_mime_filter_set_size(mf, len+3, FALSE);
newlen = g_base64_decode_step(in, len, (guchar *) mf->outbuf, &f->state, (guint *) &f->save);
g_assert(newlen <= len+3);
break;
case CAMEL_MIME_FILTER_BASIC_QP_DEC:
/* output can't possibly exceed the input size */
camel_mime_filter_set_size(mf, len + 2, FALSE);
newlen = camel_quoted_decode_step((unsigned char *) in, len, (unsigned char *) mf->outbuf, &f->state, (gint *) &f->sa
ve);
g_assert(newlen <= len + 2);
break;
case CAMEL_MIME_FILTER_BASIC_UU_DEC:
if (!(f->state & CAMEL_UUDECODE_STATE_BEGIN)) {
register char *inptr, *inend;
size_t left;
inptr = in;
inend = inptr + len;
while (inptr < inend) {
left = inend - inptr;
if (left < 6) {
if (!strncmp (inptr, "begin ", left))
camel_mime_filter_backup (mf, inptr, left);
break;
} else if (!strncmp (inptr, "begin ", 6)) {
for (in = inptr; inptr < inend && *inptr != '\n'; inptr++);
if (inptr < inend) {
inptr++;
f->state |= CAMEL_UUDECODE_STATE_BEGIN;
/* we can start uudecoding... */
in = inptr;
len = inend - in;
} else {
camel_mime_filter_backup (mf, in, left);
}
break;
}
/* go to the next line */
for ( ; inptr < inend && *inptr != '\n'; inptr++);
if (inptr < inend)
inptr++;
}
}
if ((f->state & CAMEL_UUDECODE_STATE_BEGIN) && !(f->state & CAMEL_UUDECODE_STATE_END)) {
/* "begin <mode> <filename>\n" has been found, so we can now start decoding */
camel_mime_filter_set_size (mf, len + 3, FALSE);
newlen = camel_uudecode_step ((unsigned char *) in, len, (unsigned char *) mf->outbuf, &f->state, (guint32 *) &f-
>save);
} else {
newlen = 0;
}
break;
default:
g_warning ("unknown type %u in CamelMimeFilterBasic", f->type);
goto donothing;
}
*out = mf->outbuf;
*outlen = newlen;
*outprespace = mf->outpre;
return;
donothing:
*out = in;
*outlen = len;
*outprespace = prespace;
}
這個(gè)過濾器實(shí)現(xiàn)了下面三種編碼方式的編碼和解碼:
1. UU(Unix-to-Unix encoding):
2. Base64
3. QP(Quote-Printable)
Camel還提供了其它一些過濾器,如:
CamelMimeFilterGZip:壓縮和解壓
CamelMimeFilterHTML:去掉HTML Tag。
CamelMimeFilterCRLF:使用\r\n作為換行符。
CamelMimeFilterToHTML:加上HTML Tag。
CamelMimeFilterCharset:字符集轉(zhuǎn)換。
所有的過濾器由CamelStreamFilter來組合,CamelStreamFilter提供了下面兩個(gè)函數(shù):
增加過濾器:
int camel_stream_filter_add (CamelStreamFilter *stream, CamelMimeFilter
*filter);
移除過濾器:
void camel_stream_filter_remove (CamelStreamFilter *stream, int id);
CamelStreamFilter實(shí)現(xiàn)了CamelStream接口,這里應(yīng)用了前面所講的裝飾模式,它不改變CamelStream的接口,但
給CamelStream加上了數(shù)據(jù)轉(zhuǎn)換功能。它的創(chuàng)建函數(shù)如下:
CamelStreamFilter *camel_stream_filter_new_with_stream
(CamelStream *stream);
傳入一個(gè)CamelStream對象,然后對這個(gè)對象進(jìn)行裝飾。在讀寫數(shù)據(jù)時(shí),調(diào)用相應(yīng)的Filter,下面是寫函數(shù)的實(shí)現(xiàn):
do_read (CamelStream *stream, char *buffer, size_t n)
{
CamelStreamFilter *filter = (CamelStreamFilter *)stream;
struct _CamelStreamFilterPrivate *p = _PRIVATE(filter);
ssize_t size;
struct _filter *f;
p->last_was_read = TRUE;
g_check(p->realbuffer);
if (p->filteredlen<=0) {
size_t presize = READ_PAD;
size = camel_stream_read(filter->source, p->buffer, READ_SIZE);
if (size <= 0) {
/* this is somewhat untested */
if (camel_stream_eos(filter->source)) {
f = p->filters;
p->filtered = p->buffer;
p->filteredlen = 0;
while (f) {
camel_mime_filter_complete(f->filter, p->filtered, p->filteredlen,
presize, &p->filtered, &p->filteredlen, &presize);
g_check(p->realbuffer);
f = f->next;
}
size = p->filteredlen;
p->flushed = TRUE;
}
if (size <= 0)
return size;
} else {
f = p->filters;
p->filtered = p->buffer;
p->filteredlen = size;
d(printf ("\n\nOriginal content (%s): '", ((CamelObject *)filter->source)->klass->name));
d(fwrite(p->filtered, sizeof(char), p->filteredlen, stdout));
d(printf("'\n"));
while (f) {
camel_mime_filter_filter(f->filter, p->filtered, p->filteredlen, presize,
&p->filtered, &p->filteredlen, &presize);
g_check(p->realbuffer);
d(printf ("Filtered content (%s): '", ((CamelObject *)f->filter)->klass->name));
d(fwrite(p->filtered, sizeof(char), p->filteredlen, stdout));
d(printf("'\n"));
f = f->next;
}
}
}
size = MIN(n, p->filteredlen);
memcpy(buffer, p->filtered, size);
p->filteredlen -= size;
p->filtered += size;
g_check(p->realbuffer);
return size;
}
這里先調(diào)用camel_stream_read讀取數(shù)據(jù),然后依次調(diào)用fitler對數(shù)據(jù)進(jìn)行處理,最后把數(shù)據(jù)返回給調(diào)用者。do_write的過
程類似,CamelStreamFilter對編碼和解碼都支持,而使用者不用關(guān)心。
管道過濾器模式應(yīng)用相當(dāng)廣泛,它不限于文本數(shù)據(jù)處理,任何以數(shù)據(jù)處理為中心的系統(tǒng),都可以用管道過濾器模式作為基本架構(gòu)。