节选自《Apache源代码解析-基于Apache0.6.5》第三章。
本章描述Apache0.6.5的自定义函数库。这些函数供实现Apache功能模块的代码调用。本章代码可以按照功能分成两类,一类实现对文件的读操作 进行缓冲,包括stream.h和stream.c;另外一类由单独的util.c文件组成,实现了一些供其它函数调用的工具函数。
3.1 背景知识
本章涉及到4个相关知识点:url编/解码,时间格式,夏令时以及BASE64编码,了解这些知识点是您能够理解代码的前提,如果您具备了相关知识,可以跳过相关的小节。下面我们对这几个背景知识进行简单说明。
3.1.1 URL编码/解码
发送给服务器的URL请求中,如果含有非数字字母数据的,浏览器会编码后传递,比如您在注册个人信息的时候,需要填写您的中文名和公司英文名两项数据,如果您的中文名字是张三,公司英文名字是Microsoft Corporation,请求方法是GET,表单中中文名字的输入框的name属性是cname,公司英文名输入框的name属性是corpname,服务器端处理表单数据的文件是/user/reg.php ,那么浏览器发送的请求将类似于http://www. server.com/user/reg.php?cname=%D5%C5%C8%FD&corpname=Microsoft%20Corporation
其中%D5%C5%C8%FD就是“张三”两个汉字URL编码后的效果,而公司名称中的%20就是空格字符URL编码后的效果。
由于浏览器的这个特性,服务器需要在接收数据之后进行URL解码操作,同时服务器提供了编码操作。
编码和解码操作是通过函数escape_url和unescape_url完成的。
编码的方法是:不是字母或数字的字符将被替换成百分号(%)后跟其16进制ASCII编码。由于历史原因,将加号(+)编码成%20(这点和RFC1738不兼容)。
解码就是编码的逆向工程。
3.1.2 时间格式
在HTTP协议里面用到了几个时间格式,在Apache0.6.5里面除了用户自定义时间格式外,还涉及到一下三种:
ctime格式时间:Fri Mar 13 13:58:46 2009
RFC860格式时间:Monday, 15-Aug-05 15:52:01
RFC822格式时间:Mon, 15 Aug 2005 15:52:01
3.1.3 夏令时
夏令时是一种为节约能源而人为规定地方时间的制度。在这一制度实行期间所采用的统一时间称为“夏令时间”。一般在天亮早的夏季人为将时间提前一小时,可以 使人早起早睡,减少照明量,以充分利用光照资源,从而节约照明用电。中国曾在1986年到1991年实行过夏令时,80前的同学们应该还有印象。
在C语言中,结构tm中的域tm_isdst来标识夏令时,实行夏令时的时候,tm_isdst为正;不实行夏令时的进候,tm_isdst为0;不了解情况时,tm_isdst()为负。
结构tm的详细组成及其含义可以在/usr/include/time.h中找到。在此不再赘述。
3.1.4 BASE64编码
BASE64编码最早使用在邮件传输上,由于历史原因,Email被设计成只允许传送ASCII字符,这样一来,如果您发送了一封带有非ASCII字符的Email通过有“历史原因”的网关就可能出现问题:问题网关有可能将每个字节的最高位置为0。
此时我们就需要对邮件中传输的信息进行编码,使之全部用ASCII字符表示。
举例说明,您可以给自己发一封邮件,标题是test,内容简单些,只有两个字“张三”,这时邮箱收到的数据是什么呢?
我是通过Firefox接收的邮件(OutLook类似),从Firefox里面看起来一切正常,显示发件人,收件人,邮件内容,但是网关上传输的数据并不是您看起来这么简单。
有一个方法可以知道网关上传输的数据内容都有那些,在Firefox里面选中这封邮件,从菜单中选择“文件”,从子菜单里面选择“导出邮件”,格式选择 “OutLook邮件(*.eml)”,随便起一个名字,比如abc.eml。用文本编辑器打开这个邮件,你会发现内容有点“变态”:
X-Kaspersky: Original server data starting here: +OK 2303 octets
………………
Subject: test
From: Lee tsingien <***@***.com>
To: ***@***.com
Content-Type: text/plain; charset=GB2312
Content-Transfer-Encoding: base64
………………
1cXI/Q==
从高亮部分可以看到,传输的编码类型是BASE64,那邮件内容呢?就是“1cXI/Q==”,这就是“张三”两个汉字的BASE64编码结果。看到了,全是ASCII字符,这样就跟有“历史问题”的网关兼容了。
很神奇是不是,您也许要问,它是怎么“编”的呢?
其实很简单,将需要编码的二进制数据每次取3个byte,顺序放入一个24bit的缓冲区中,不足3byte,将缓冲区中剩余部分补0。然后每次从缓冲区中取出6个bit,根据RFC2045中定义的码表得到对应的字符即可。
比如我们的张三,16进制数据就是D5C5C8FD,转换成二进制就是11010101110001011100100011111101。每次取6个bit,取出的二进制序列转换成十进制就是53(110101)、28(011100)、23(010111)、8(001000)、63(111111)、16(010000),在表3-1中找到对应的字符就是1cXI/Q
你也看到了,似乎不对,因为邮件里面的数据比我们操作的结果多出两个等号(=),这是怎么回事呢?
因为上面的编码规则说的并不完整,相信您也看到了,规则里面是“每次取3个byte”,而需要编码的数据的字节数不一定是3的倍数啊,就像我们的字节数是4字节,不满足条件,这该怎么处理呢?
补充规则:在编码的字符串后面添加“3-(原文字节数 MOD 3)”个等号(=)。在我们的例子里面,就是添加3-(4mod3)=2个等号。
所以最终编码结果为1cXI/Q==,也就是您在邮件里面看到的内容。
表3-1 The Base64 Alphabet
值 编码 值 编码 值 编码 值 编码
0 A 17 R 34 i 51 z
1 B 18 S 35 j 52 0
2 C 19 T 36 k 53 1
3 D 20 U 37 l 54 2
4 E 21 V 38 m 55 3
5 F 22 W 39 n 56 4
6 G 23 X 40 o 57 5
7 H 24 Y 41 p 58 6
8 I 25 Z 42 q 59 7
9 J 26 a 43 r 60 8
10 K 27 b 44 s 61 9
11 L 28 c 45 t 62 +
12 M 29 d 46 u 63 /
13 N 30 e 47 v
14 O 31 f 48 w (pad) =
15 P 32 g 49 x
16 Q 33 h 50 y
说了这一通,全是和邮件相关的,我们在讨论的是Web服务器啊。别忙,如果您看一下解码函数uudecode调用位置您就明白了,它是为第九章将要讲到的Basic认证方式服务的,这种认证方式从客户端传递过来的认证数据也是BASE64编码的。
说点题外话,其实在IE浏览器里面还有一个地方用到了BASE64编码了,在IE6之后的版本里面提供了一个功能,就是把网页保存成mht格式。
您可以用IE打开百度首页http://www.baidu.com , 然后在“文件”菜单中选择“另存为”,在“保存类型”中选择“Web档案,单一文件(*.mht)”选项,然后指定一个文件名,保存成功后您会发现,虽然 百度的首页里面包含的图片,IE只生成了一个.mht文件。然后您可以断开网络连接,在脱机的情况用IE打开这个mht文件,您会发现百度的LOGO图片 也能正常显示,很奇妙?其实您可以用文本编辑器打开这个mht文件,其中有类似这样的一段信息:
------=_NextPart_000_0000_01C9C495.CF9C2DA0
Content-Type: image/gif
Content-Transfer-Encoding: base64
Content-Location: http://www.baidu.com/img/baidu_logo.gif
R0lGODlhDgGBALMAAPHs98zK91RM5JGM7nBq6O1raPfBv+lEQfSdmyMZ3OEGAf///wAAAAAAAAAA
………………
2rIJEQAAOw==
看到了熟悉的==?再看看上面,注明了Content-Type是image/gif,编码方式也是BASE64,聪明的您一定想到了,原来LOGO文件http://www.baidu.com/img/baidu_logo.gif 的二进制信息已经通过BASE64编码保存在mht文件中了,所以即便是脱机,您也可以看到这个图片。
如果您有兴趣,可以把这部分内容解码成二进制,然后把二进制信息写入到一个文件中,并把这个命名为baidu_logo.gif,使用图片查看工具查看,看看是不是您当初保存的百度的LOGO图片。
3.2代码注释
3.2.1 stream.h、stream.c
您可能听说过包裹函数(或者叫包装函数),实际上就是对常用操作的一个封装,比如原子日志,在多进程或多线程同时操作一个日志文件的时候,可能会有竞争问 题存在,为了避免竞争,可以设计一个原子日志系统,这个系统提供给调用者几个简单的接口(函数)来完成原子日志的记录,而竞争问题的解决、文件读写的错 误、信号的处理等等都在原子日志系统里面完成,对调用者来说是透明的。
同样stream.h和stream.c就提供了几个包裹函数,主要用来对文件读操作进行缓冲,这对socket这种特殊文件来说是很有意义的。
限于篇幅,注释代码部分请参看本书官网。
节选自《Apache源代码解析-基于Apache0.6.5》第三章。