2008 年 08 月

浏览模式: 标准 | 列表 | 图文
闲话软件业的竞争

闲话软件业的竞争

本来也想不到谈这些,突然在群里看到石头发牢骚,并且发了链接,于是看了一下,感慨颇多,先贴上他写的东西。。。

很久没写BLGO了,发篇硬一点的文章,也许要挨骂了。
最近做项目,接触了很多企业和团队,感觉在技术市场,价格战已经打的相当的激烈了,一个企业站,简单的文章发布管理功能,一般的界面,价格已经低到1、2 千甚至几百了,这还是北京市场的价格。其实,技术根本不是这么不值钱的,即便没有什么技术含量,单也是相对于技术圈子里面而言,在任何市场里面,对于圈子内的人而言,都是没有什么技术含量的。
那最终为何导致,继家电价格战,打倒了一批家电企业后,这种事情又蔓延到软件行业呢,甚至我接触的一些传统行业,竟然也在暗流涌动。
我下面穿插着举几个例子,不一定是技术行业的,因为很多数字需要量化,软件项目不好讲清楚,用其他行业的,许是更能说明问题一些。
打价格战,其实初衷还是占领市场,可能制定价格战策略时,想的就是降一些价格,然后多拿一些单,利润不会下降,还能形成一定的客户群,即便不赚钱,持平,当把市场占领后,还是可以提价的。
这种说法对不对,我不会多说什么,我拿一组数字来说明一下问题吧:
我以一般的商品流通行业的利润率距离,通常,一个生产商可以赚到的毛利率在30%-35%,零售商赚到35%-40%,批发商赚到25%-35%,这样,我为了方便计算,把一个产品的毛利率,定为35%,应该是比较合理的。
在确定了一般的毛利率后,让我们计算一下,为什么不能降价,
比如我们卖100元的商品,会赚取35块钱,那么我们降价10块钱,但是我们的成本65块钱不会变动,也就是我们卖出一件商品,90元,只能获得25元利润,那么我们计算一下:
未降价:一件商品,65%的成本,35%的利润
降价后:一件商品,72%的成本,28%的利润
等于是我们降价10%后,成本上升了7%,也就是利润下降了7%
这个数值貌似很小,那么能说明什么呢?就意味着,我们要取得原来的利润,必须要多卖东西,这事所有人都能想象到的,那么我们在计算一下要多付出多少工作才能维持利润:
X×28%=35
X=125
也就是我们要卖出125块钱的产品,才能获得原来的利润,我们可以看到,我们的付出,可能要达到原来的140%才可以
当然,我这是粗略计算,也许事实上,你付出的还要更多,因为会牵扯到人员、物流、售后等成本,我只是用产品举例,其实,在软件外包中,这种降价导致的成本增长和工作量的加大,会更为明显,但是软件不好量化,我只能凭经验说,这种降价很恐怖。

我不指望能在一篇文章中,说明为什么不能打价格战,只是想引发个思考,也许我后面有时间,会写一些我如何避免价格战的经验,并且阐述一下,其实客户关注的,并不主要是价格,关注价格的客户,自然有其另一套应对的方式,而这么多年我接的项目,也没有一个通过价格战而获得的。

我再次说明我的观点,即便因为经营不善公司倒闭了,也不要因为价格战不能获利,而把自己累死,而不加入价格战,也未必不能很好存活下去,我希望有时间,我能把这些也写下来。
原文网址:http://blog.sina.com.cn/s/blog_507bdaba0100ad65.html

石头是搞过财务的,我是没有搞过,但我对这种情况也是很反感,05年我也就这方面探讨过(用探讨两个字是深了点,不如说是同样的发过牢骚吧)

当时的文章地址:http://www.cnitblog.com/neatstudio/archive/2005/07/07/804.html
http://www.cnitblog.com/neatstudio/archive/2005/07/09/841.html

两篇内容如下:

一直以来,在很多人心里,国产软件的性能比不上国外的同类软件,但是价格上却能低于国外同类软件,因此有部分人一直在鄙视着国内的软件从业人员。
可是,事实上呢?国内高性能的软件非常多,但是很多软件在发展到后面都遇上了瓶颈,结果就此停步,终止了软件的开发,造成软件在后续版本上由国外坐大。
国内编程人员的素质差!这句话是谁说的?确实有点道理,我指的是个人素质,而不是编程的素质。国内有一小部分程序员,到SF上下载一些OPENSOURCE的软件,稍作改头换面,就当成自己的软件,并进行销售,这类事件曾经电脑报上有报道过。
在这里,不得不提的是国内一些对程序员的看法以及所给的待遇。一些大公司都拿程序员当人看,而一些小公司则是把程序员当成挣钱的机器,每天工作时间超长,工资超低,这也就难保程序员在编程时偷懒而直接到网上找现成的代码来拼凑程序。
由于自己也经常接触到程序员,听听他们的诉苦得到的结论就是,程序员干的都不是人干的活呀。理由是现在程序员太多了,而且部分程序员的品德也比较差,辛苦接到一个单和人谈好价钱,结果被人横空插入,以绝对低价把单抢走,最后得到的消息是抢单的人拿网上现成的程序改改就交付了,客户在这种情况下又不满意,而抢单的人却消失了。这样的事情发生多了之后,就有了一个现象:程序不值钱,再便宜都有人做;程序员素质低,打一枪换一个地方;对程序员一定要能榨光剩余价值就榨光。
梁启超说过:长此以往,国将不国。在这里我只是想说,长此以往,程序员将消失掉。试想,很多人抱着开源的思想在网上公布代码后,后果却是:1、人身攻击,言辞大多是:垃圾程序也好意思拿出来公布这;2、恶意抵毁,这种情况往往是公布的程序与某商业程序产生了冲突,影响了销售。其他情况也不好意思说了,我自己就曾遇到这一点。
国产软件究竟该如何走?
在经历了种种挫折后,很多软件作者开始把目光投向海外。事实证明,这样的策略是正确的,一来国外对版权比较重视,二来国外的客户把程序员当人看(其实也可 能是国外的消费水平和国内不同罢了)。很多共享软件因此而获得了较高的收益。这种情况造成国内的客户看不到高手,高手也不愿意再兼顾国内。很多人就开始认 为,国内没有编程高手,现有的程序员都是一些水平比较低的人,给他们的项目价钱不能太高,如此循环往复,造成了恶性循环。
目前,我们这些程序员要做的事情就是:以国外软件开发为主,同时为国内的市场提供一些比较有针对性的软件。不要让国内的客户对程序员失望,同时要清理开发市场,对那些开发水平不高,还要乱报价的程序员应该进行强烈批评并查实后进行通报。
在这种情况下,程序员的地位可能会得到一定的提高。然而,程序员实在太多了,要好好的洗牌毕竟只是一个构想。
希望各位有识之士能够认同,并为提高程序员的修养尽一份力。

牢骚也发过了。可惜市场并不因为我们的牢骚而改变。现在,只能多发发牢骚了

国学大师林语堂献宝:英文学习八要诀

国学大师林语堂献宝:英文学习八要诀

一直让我最郁闷的就是自己的英文水平了,看吧,能看得懂一点点,听吧,一点点也听不懂,连哑巴英语也算不上。
看到新东方上有林语堂老先生的心得,不禁摘录下来,也可以多学习一些前辈的先进经验吧。

林语堂先生(1895-1976),现代文学家、翻译家和英语大师。他一生共出版中文集3种,英文著作36种。他从中国人学习英语的实际出发,提出了一系列的英语教学方法和英语学习方法,至今仍然值得我们借鉴。

  一、学英文时须学全句,勿专念单字。学时须把全句语法、语音及腔调整个读出来。

  二、学时不可以识字为足。识之必然兼用之。凡遇新字,必至少学得该字之一种正确用法。以后见有多种用法,便多记住。

  三、识字不可强记。得其句中用法,自然容易记得。

  四、读英文时须耳目口手并到。耳闻、目见、口讲、手抄,缺一不可。四者备,字句自然记得。

  五、“四到”中以口到为主要。英语便是英国话,如果不肯开口,如何学得说话?

  六、口讲必须重叠练习,凡习一字一句必须反复习诵十数次至数十次,到口音纯熟为止。学外国语与学古文同一道理,须以背诵为入门捷径。每谋取一二句背诵之。日久必有大进。

  七、口讲练习有二忌。

  (一)忌怕羞。学者在课堂上怕羞,则他处更无练习机会。

  (二)忌想分数。一想到分数,便怕说错,怕说错,便开口不得。最后的胜利者,还是不怕羞、不怕错、充分练习的学生。若得教员随时指正,自然可由多错而少错,由少错而纯正,由纯正而流利,甚至由流利而精通。此是先苦后甘之法。

  八、读节要精。读音拼写,皆须注意。马马虎虎,糊涂了事,不但英文学不好,任何学问也学不好。

原文:http://www.neworiental.org/Default.aspx?TabID=1130&InfoID=241419&SettingModuleID=2483

好东西,不得不贴,继续上传两个文件

好东西,不得不贴,继续上传两个文件

今天看到一个好东西,是jquery的API,这次的API不是象以前那样用XML的,也不是那种根据官方的数据简单的汉化而来的,这次。。。。阿健同学编译成了CHM文件(虽然以前的也是CHM文件),并且有索引和搜索功能哦。只是由于编译方式的问题吧?不支持中文搜索,不过也总比以前那种纯粹的一个页面来的爽。OH YEAH。。。

剩下一个嘛,就是传统的CSS手册了,这个手册估计只要是WEB开发人员就会人手一份,苏沈小雨同学实在是太让人尊敬了,估计国内开发人员提到CSS手册,都会说,哦,就是那个苏沈小雨的呀。。。

不多说,放上链接。黑黑
注意:jqueryApi文件打开后会生成一个chw的文件,如果你看不顺眼可以每次删掉

IIS 6 下的 PHP 最佳配置方法

IIS 6 下的 PHP 最佳配置方法

虽然 LAMP 组合很不错,但是如果想要架设一台同时支持 PHP、ASP、ASP.NET、JSP、Perl 的 Web 虚拟主机服务器,还是用 Windows 2003 的 IIS 6 最好。网上有很多介绍在 IIS 6 上配置 PHP 的文章,但是那些方法不是性能不好,就是升级麻烦。下面的方法可以让你在第一次配置好后,能够非常方便的进行升级。

这里所说的升级,是指从某个 php4 版本升级到另一个 php4 版本,或者从某个 php5 版本升级到另一个 php5 版本,而不是指从 php4 升级到 php5。

准备:

一台安装好的 Windows 2003 服务器,并且已经安装了 IIS 6。

下载 Windows 版的 PHP 二进制压缩包。

安装:

解压缩 PHP 二进制压缩包到 C:\php 目录下(这里假设 C: 盘是系统盘,即安装了Windows 系统的盘,如果系统盘是 D: 盘,则解压缩到 D:\php 目录下,以此类推,下同)。

然后打开“我的电脑”->“属性”->“高级”->“环境变量”->“系统变量”->“path”,编辑其值,在前面增加下面的路径地址:

C:\php;C:\php\dlls;C:\php\extensions;C:\php\sapi;

将 php.ini-dist 或 php.ini-recommended 复制到 C:\Windows 目录下,并改名为 php.ini,一般正式发布网站的服务器用 php.ini-dist,而作为调试用的服务器用 php.ini-recommended 更好。当然一般情况下,这个 php.ini 还是需要根据实际情况来修改的。

下面来介绍一下几个必要的修改选项:

extension_dir = "C:\php\extensions"

这个是 PHP 扩展所放置的目录,请确保跟你实际安装的目录相同。

extension=php_mbstring.dll
;extension=php_big_int.dll
extension=php_bz2.dll
extension=php_cpdf.dll
extension=php_crack.dll
extension=php_curl.dll
extension=php_db.dll
extension=php_dba.dll
extension=php_dbase.dll
extension=php_dbx.dll
extension=php_domxml.dll
;extension=php_exif.dll
;extension=php_fdf.dll
;extension=php_filepro.dll
extension=php_gd2.dll
extension=php_gettext.dll
extension=php_hyperwave.dll
extension=php_iconv.dll
;extension=php_ifx.dll
;extension=php_iisfunc.dll
extension=php_imap.dll
;extension=php_interbase.dll
extension=php_java.dll
extension=php_ldap.dll
;extension=php_mcrypt.dll
extension=php_mhash.dll
extension=php_mime_magic.dll
extension=php_ming.dll
extension=php_mssql.dll
extension=php_msql.dll
;extension=php_oci8.dll
extension=php_openssl.dll
;extension=php_oracle.dll
extension=php_pdf.dll
extension=php_pgsql.dll
;extension=php_printer.dll
extension=php_shmop.dll
;extension=php_snmp.dll
extension=php_sockets.dll
;extension=php_sybase_ct.dll
extension=php_w32api.dll
extension=php_xmlrpc.dll
extension=php_xslt.dll
extension=php_yaz.dll
extension=php_zip.dll

上面这些,开头没有加分号的是打开的扩展,加了分号的是没有打开的扩展。上面的设置包含了在 Windows 2003 上默认安装情况下可以打开所有扩展(这里列出的是 php 4 的)。

session.save_path = b:\sessions

这个是 session 文件默认保存的目录,这个目录必须是一个存在的目录,不然默认的 session 功能会无效。我这里设置的是一个 ramdisk 上的一个目录。将 session.save_path 设置在 ramdisk 上可以加快 session 处理的速度。如果你没有安装 ramdisk,你可以把它指定到其他盘的任何一个目录下,如 C:\sessions 目录、C:\Windows\Temp 目录等。

OK,基本工作作完了,现在该配置 IIS 了。

打开“ Internet 信息服务(IIS)管理器”,在“ Web 服务扩展”里,选择“添加一个新的 Web 服务扩展”,扩展名可填写“PHP ISAPI 扩展”,要求的文件选择:C:\php\sapi\php4isapi.dll(如果安装的是 PHP5,则此处是 C:\php\sapi\php5isapi.dll,下同),并设置扩展状态为允许。

打开“网站”->“属性”->“主目录”->“应用程序设置”->“配置”->“应用程序扩展”->“添加 ”,可执行文件还是选择 C:\php\sapi\php4isapi.dll。扩展名填写“.php”,动作限制为“HEAD,GET,POST”。

打开“网站”->“属性”->“文档”->“启用默认内容文档”->“添加”,可以将 index.php 添加为默认内容文档。

然后选择“服务器机器名”->“所有任务”->“重新启动 IIS”来重启 IIS。

测试

在默认网站发布目录下,建立一个测试页面:

<?php
phpinfo();
?>

如果打开这个页面能够看到 php 安装配置信息,就算是安装成功了。

如果想要更优化的执行 php 程序,可以安装 ZendOptimizer-3.0.1-Windows-i386.exe,这个东西安装非常简单,这里就不介绍了。

升级

现在升级就非常简单了。只需要将新版本的 PHP 二进制压缩包下载下来,将原来的 C:\php 目录删除,将新版本解压缩到 C:\php 目录中,然后重新启动一下 IIS 就可以了。不需要修改任何配置,也不需要往 System32 目录中复制任何文件。是不是很方便啊?


膘叔的小技巧:
由于在IIS下面建立站点和关闭站点都比较方便,但是象上面说的升级就不是很方便了。我是指启用站点,虽然直接点击控制台上的restart按钮很方便,但。。。WINDOWS有时候会突然卡死,那时候是关也不能关,开也不能开,只能注销,在这里,我提供一个小小的批处理文件,只要放在桌面上,想重启的时候运行一下就行了。。
文件内容如下:

net stop w3svc
net start w3svc

是不是很简单?但不要小看这两行,WINDOWS好象是对命令行的处理有一定的优先级,而且用命令行不会被卡死。

关于 PHP 中的 iconv 函数的一点小问题[转]

关于 PHP 中的 iconv 函数的一点小问题[转]

作者:andot,来自:www.coolcode.cn,出处:http://www.coolcode.cn/show-41-1.html

昨天在调试 WAP 网站时发现,在增加了 GB2312 到 UTF-8 转化以后,有些页面显示不正常了——有些页面只有一半的内容,另一半被截掉了。因为被截掉的部分包含了<p>的后半个标签</p>,因此整个页面都显示不出来,而报告错误。经过猜测、尝试,最后终于把问题集中在了 iconv 函数上。在经过高人指点以后,发现这个函数的第二个参数,除了可以指定要转化到的编码以外,还可以增加两个后缀://TRANSLIT 和 //IGNORE,其中 //TRANSLIT 会自动将不能直接转化的字符变成一个或多个近似的字符,//IGNORE 会忽略掉不能转化的字符,而默认效果是从第一个非法字符截断。但是我尝试了//TRANSLIT 和 //IGNORE 这两个后缀,效果还是不对。于是我想问题可能不是出在这里。从 GB2312 到 UTF-8 转化应该不会有不能转化的字符,因为 UTF-8 的字符集完全包含了 GB2312 中的字符,所以我想大概是前面要转化的字符集指定错了,于是我尝试着把 GB2312 改成 GBK,问题解决!虽然那两个后缀在这里没派上用场,不过也算学了一招,以后肯定会用到的。补记:改成 GBK 后,发现仍然有一封邮件的内容解析不正确。在另一位高人指点下,先换成 GB18030,问题依旧,然后改用 mb_convert_encoding 进行转换,问题解决!不知道是 mb_convert_encoding 问题,还是我的系统问题,我用 mb_convert_encoding 时不支持 GB18030 编码。另外,用 GBK 或者GB18030 作为输入编码,并在输出编码中加上 //IGNORE 后缀,用 iconv 函数也能解决那封含有错误编码的邮件内容解析不正确的问题。不过用 mb_convert_encoding 可以指定多种输入编码,它会根据内容自动识别,这个比 iconv 要好的多。

其实,同事在生成图片文字水印的时候也遇到了这种问题,同事最初用的是GB2312字符集,结果直接报错,说是字符串的offset有问题,但仔细检查后却没有这种问题。后来才发现是直接调用的这个iconv转换出错了。
原来的转换是从gb2312往 UTF8转换,表面上确实没有什么问题,然而,现在的人特别爱装酷,受影响的那位同志,用的是繁体字,繁体字的字库大多情况是属于GBK的,所以后来换成GBK后就正常了。
估计以后再遇上用火星文的朋友,就真的只能使用andot提出的这种方法了。转换成18030,再使用ignore参数。哈哈

mbstring好象最初的版本里没有使用,如果换成这个,估计代码工作量非常大,先将就着点了

单点登录系统的设计与实现方案

单点登录系统的设计与实现方案

目的:

对目前已有的 Web 应用系统,和将来待开发的 Web 应用系统进行集成,实现单点登录。

要求:

  1. 对已有的 Web 应用系统不作大规模改造。
  2. 不限制待开发的 Web 应用系统的开发工具。
  3. 不增加待开发系统的开发难度。

分析:

  1. 目前,已有的系统都各自维护自己的一套用户库,每个系统中的用户数、用户名、密码几乎都各不相同。要将已有的用户库进行统一是不现实的。因此,我们可以通过将单点登录系统中的用户与其它个系统中的用户建立映射,来实现用一个帐号来管理多个系统的目的。
  2. 已 有的 Web 应用系统、以及待开发的 Web 应用系统,可能不在同一个域下,虽然会话本身是保存在服务器端,但是会话 id 是需要 cookie 来传递的,而 cookie 不允许跨域访问,而且考虑到各个系统的开发工具也各不相同,即使在同一个域下,不同的开发工具所开发的应用程序之间也很难共享会话,因此要用共享会话的方 式来实现单点登录也不现实。因此我们通过在客户端浏览器、单点登录系统和 Web 应用系统之间传递临时会话,并让 Web 应用系统直接到单点登录系统中获取认证信息来实现单点登录。为保证不同开发工具都能够到单点登录系统获取认证信息,我们采用 xml-rpc 在 Web 应用系统和单点登录系统之间进行通讯。

实现:

单点登录系统中设置 4 个表:

  1. 单点登录系统用户表,包含 user_id,name,password 3 个字段。
  2. Web 应用系统表,包含 app_id,name(Web 应用系统名称),checkurl(Web 应用系统中用来验证用户登录的程序地址) 3 个字段。
  3. 单点登录系统用户到各个 Web 应用系统的用户映射表,包含id,user_id,app_id,name,password 5 个字段。
  4. 临时会话表,包含 hash(临时会话的 hash 编号),id(对应单点登录系统用户到各个 Web 应用系统的用户映射表中的 id 字段) 2个字段。

用户登录单点登录系统时,通过单点登录系统用户表中的字段来验证用户身份。登录以后,用户可以设置各个系统到该系统用户的映射关系。设置好以后,当通过该 系统进入其他某个 Web 应用系统时,该系统会为该用户和该系统生成一个临时会话编号(hash),并转到 Web 应用系统中的登录检测页面,登录检测页面通过获取到的临时会话编号,来调用单点登录系统的获取用户名和密码的 xml-rpc API,如果用户名密码如果正确,则转到正常登录后的页面,如果不正确,则转到登录错误的页面。这里,xml-rpc API 在返回用户名和密码后,将删除单点登录系统数据库中相应的临时会话,这样不但用户名、密码都是在服务器之间进行传递的,并且临时会话存在的时间也是尽可能 的短,因此只要保证服务器之间的对话不能被监听,即可保证安全性。 已有系统需要增加一个用于单点登录系统的登录验证页面,该页面工作过程大致如下:

  1. 获取 客户端 hash 值
  2. 通过 hash 值得到用户名和密码(xml-rpc 调用)
  3. 通过用户名和密码进行身份验证
  4. 返回身份验证后的页面

原作者:andot,来源coolcode.cn,原文:http://www.coolcode.cn/show-89-1.html

用 PHP 生成 JavaScript 字符串

用 PHP 生成 JavaScript 字符串

闲来无事,到coolcode.cn上去闲逛。发现了一些比较旧的文章,以前也都看过,但,那时候没有开博,所以也就没有记录。如今博客也开了,coolcode.cn估计要成为我搜刮的对象了。

coolcode.cn的作者是andot,PHPRPC的作者,我06年年中(应该没记错)就使用了这个软件,同年放弃使用xajax,当然,现在我更多的使用jquery来操作ajax,但PHPRPC的功能不完全局限于ajax,我这里就不详谈了。先谈这篇文章。。。

无耻的分隔线:


原来,在从 PHP 中直接传递字符串给 JavaScript 时,直接用了 addcslashes,如果传递的是带有 utf-8 编码汉字的,就用 "\0..\037\042\134",如果纯 ascii 范围内的字符串,就用 "\0..\037\042\134\177..\377"。但是今天在写加密程序时发现,发现程序有时行,有时不行。后来发现原来是 \v \a 搞的。addcslashes 会把 \007..\015(八进制) 转义成 \a\b\t\n\v\f\r,而其中 \a 这个 IE 和 Firefox 都不认识,\v 这个 Firefox 认识,IE 不认识。所以我写了下面这个函数,用它可以就可以转化成 JavaScript/JScript 所认识的字符串了(其中 $flag 表示是否转义 ascii 码大于 127 的字符)。感谢五帝同学的帮助!

/* @author andot & wudi */
function addjsslashes($str, $flag = true)
{
if ($flag) {
$str = addcslashes($str, "\0..\006\010..\012\014..\037\042\047\134\177..\377");
}
else {
$str = addcslashes($str, "\0..\006\010..\012\014..\037\042\047\134");
}
return str_replace(
array(chr(7), chr(11)),
array('\007', '\013'),
$str
);
}
MySQL 5 中 blob 类型字段的插入[转]

MySQL 5 中 blob 类型字段的插入[转]

原文内容如下:
MySQL 4.1 和 MySQL 5 跟 MySQL 4 有一些区别,许多原来在 MySQL 4 上的程序直接移植到 MySQL 4.1 或者 MySQL 5 上时,常常会出现许多问题。因此,一直以来都用 MySQL 4.0,没有升级过,因为原来许多系统在开发时都没有考虑 MySQL 4.1 之后的数据库。这几天被迫移植一个 3 年前写的一个 CMS 到 MySQL 5 上,字符集转换的那个问题因为在以前开发时遇到过了,因此这个很容易就解决了。那个系统中的图片和其它附件内容都是保存在数据库中的,用的是 longblob 类型的字段。原来是把文件内容读出来之后,直接用 addslashes 函数转义一下,写入数据库就可以了。但是这次到了 MySQL 5 上居然不好用了。写入文本文件还可以,但是 Word 之类的文档死活写不进去。将 addslashes 换成了 mysql_escape_string 和 mysql_real_escape_string 后还是都不好用。于是心灰意冷,决定把数据导出来,再换到 MySQL 4 上去,数据导出来以后是个 sql 文件,打开一看,发现 longblob 字段在插入时居然是 0xHHHHHHHH... 的形式,于是试着把 addslashes 改称 bin2hex,然后开头再加上 0x,再一试,好了!原来如此啊,这回不用换数据库了,继续用 MySQL 5。我没测试 MySQL 4.1 是否也有这个问题,不过我估计也应该用这种方式才能插入 blob 类型的字段值。

PHP 序列化(serialize)格式详解

PHP 序列化(serialize)格式详解

PHP 序列化(serialize)格式详解

  1. 前言
  2. 概述
  3. NULL 和标量类型的序列化
  4. 简单复合类型的序列化
  5. 嵌套复合类型的序列化
  6. 自定义对象序列化
  7. Unicode 字符串的序列化
  8. 参考文献

1.前言

PHP (从 PHP 3.05 开始)为保存对象提供了一组序列化和反序列化的函数:serialize、unserialize。不过在 PHP 手册中对这两个函数的说明仅限于如何使用,而对序列化结果的格式却没做任何说明。因此,这对在其他语言中实现 PHP 方式的序列化来说,就比较麻烦了。虽然以前也搜集了一些其他语言实现的 PHP 序列化的程序, 不过这些实现都不完全,当序列化或反序列化一些比较复杂的对象时,就会出错了。于是我决定写一份关于 PHP 序列化格式详解的文档(也就是这一篇文档),以便在编写其他语言实现的 php 序列化程序时能有一个比较完整的参考。这篇文章中所写的内容是我通过编写程序测试和阅读 PHP 源代码得到的,所以,我不能 100% 保证所有的内容都是正确的,不过我会尽量保证我所写下的内容的正确性,对于我还不太清楚的地方,我会在文中明确指出,也希望大家能够给予补充和完善。

2.概述

PHP 序列化后的内容是简单的文本格式,但是对字母大小写和空白(空格、回车、换行等)敏感,而且字符串是按照字节(或者说是 8 位的字符)计算的,因此,更合适的说法是 PHP 序列化后的内容是字节流格式。因此用其他语言实现时,如果所实现的语言中的字符串不是字节储存格式,而是 Unicode 储存格式的话,序列化后的内容不适合保存为字符串,而应保存为字节流对象或者字节数组,否则在与 PHP 进行数据交换时会产生错误。

PHP 对不同类型的数据用不同的字母进行标示,Yahoo 开发网站提供的 Using Serialized PHP with Yahoo! Web Services 一文中给出所有的字母标示及其含义:

  • a - array
  • b - boolean
  • d - double
  • i - integer
  • o - common object
  • r - reference
  • s - string
  • C - custom object
  • O - class
  • N - null
  • R - pointer reference
  • U - unicode string

N 表示的是 NULL,而 b、d、i、s 表示的是四种标量类型,目前其它语言所实现的 PHP 序列化程序基本上都实现了对这些类型的序列化和反序列化,不过有一些实现中对 s (字符串)的实现存在问题。

a、O 属于最常用的复合类型,大部分其他语言的实现都很好的实现了对 a 的序列化和反序列化,但对 O 只实现了 PHP4 中对象序列化格式,而没有提供对 PHP 5 中扩展的对象序列化格式的支持。

r、R 分别表示对象引用和指针引用,这两个也比较有用,在序列化比较复杂的数组和对象时就会产生带有这两个标示的数据,后面我们将详细讲解这两个标示,目前这两个标示尚没有发现有其他语言的实现。

C 是 PHP5 中引入的,它表示自定义的对象序列化方式,尽管这对于其它语言来说是没有必要实现的,因为很少会用到它,但是后面还是会对它进行详细讲解的。

U 是 PHP6 中才引入的,它表示 Unicode 编码的字符串。因为 PHP6 中提供了 Unicode 方式保存字符串的能力,因此它提供了这种序列化字符串的格式,不过这个类型 PHP5、PHP4 都不支持,而这两个版本目前是主流,因此在其它语言实现该类型时,不推荐用它来进行序列化,不过可以实现它的反序列化过程。在后面我也会对它的格式进行说 明。

最后还有一个 o,这也是我唯一还没弄清楚的一个数据类型标示。这个标示在 PHP3 中被引入用来序列化对象,但是到了 PHP4 以后就被 O 取代了。在 PHP3 的源代码中可以看到对 o 的序列化和反序列化与数组 a 基本上是一样的。但是在 PHP4、PHP5 和 PHP6 的源代码中序列化部分里都找不到它的影子,但是在这几个版本的反序列化程序源代码中却都有对它的处理,不过把它处理成什么我还没弄清楚。因此对它暂时不再 作更多说明了。

补充

最近的 PHP CVS 版本中,序列化的方式有所变化,基本类型的序列化仍然保持原来的格式,只是对 Unicode 支持上,有了新的进展。另外,对普通字符串的序列化也分成了 2 种。一种是 non-escaped 字符串,也就是我们上面说的那个小写 s 标识的字符串;另一种是 escaped 字符串,这种字符串格式用大写 S 标识。所以上面那个表现在应该改为:

  • a - array
  • b - boolean
  • d - double
  • i - integer
  • o - common object
  • r - reference
  • s - non-escaped binary string
  • S - escaped binary string
  • C - custom object
  • O - class
  • N - null
  • R - pointer reference
  • U - unicode string

3.NULL 和标量类型的序列化

NULL 和标量类型的序列化是最简单的,也是构成符合类型序列化的基础。这部分内容相信许多 PHP 开发者都已经熟知。如果您感觉已经掌握了这部分内容,可以直接跳过这一章。

3.1.NULL 的序列化

在 PHP 中,NULL 被序列化为:

N;

3.2.boolean 型数据的序列化

boolean 型数据被序列化为:

b:<digit>;

其中 <digit> 为 0 或 1,当 boolean 型数据为 false 时,<digit> 为 0,否则为 1。

3.3.integer 型数据的序列化

integer 型数据(整数)被序列化为:

i:<number>;

其中 <number> 为一个整型数,范围为:-2147483648 到 2147483647。数字前可以有正负号,如果被序列化的数字超过这个范围,则会被序列化为浮点数类型而不是整型。如果序列化后的数字超过这个范围 (PHP 本身序列化时不会发生这个问题),则反序列化时,将不会返回期望的数值。

3.4.double 型数据的序列化

double 型数据(浮点数)被序列化为:

d:<number>;

其中 <number> 为一个浮点数,其范围与 PHP 中浮点数的范围一样。可以表示成整数形式、浮点数形式和科学技术法形式。如果序列化无穷大数,则 <number> 为 INF,如果序列化负无穷大,则 <number> 为 -INF。序列化后的数字范围超过 PHP 能表示的最大值,则反序列化时返回无穷大(INF),如果序列化后的数字范围超过 PHP 所能表示的最小精度,则反序列化时返回 0。当浮点数为非数时,被序列化为 NAN,NAN 反序列化时返回 0。但其它语言可以将 NAN 反序列化为相应语言所支持的 NaN 表示。

3.5.string 型数据的序列化

string 型数据(字符串)被序列化为:

s:<length>:"<value>";

其中 <length> 是 <value> 的长度,<length> 是非负整数,数字前可以带有正号(+)。<value> 为字符串值,这里的每个字符都是单字节字符,其范围与 ASCII 码的 0 - 255 的字符相对应。每个字符都表示原字符含义,没有转义字符,<value> 两边的引号("")是必须的,但不计算在 <length> 当中。这里的 <value> 相当于一个字节流,而 <length> 是这个字节流的字节个数。

补充

在 PHP5 最新的 CVS 中(也就是将来要发布的 PHP6),上面对于 string 型数据的序列化方式已经被下面这种所取代,但是 PHP6 仍然支持上面那种序列化方式的反序列化。

新的序列化方式叫做 escaped binary string 方式,这是相对与上面那种 non-escaped binary string 方式来说的:

string 型数据(字符串)新的序列化格式为:

S:<length>:"<value>";

其中 <length> 是源字符串的长度,而非 <value> 的长度。<length> 是非负整数,数字前可以带有正号(+)。<value> 为经过转义之后的字符串。

它的转义编码很简单,对于 ASCII 码小于 128 的字符(但不包括 \),按照单个字节写入(与 s 标识的相同),对于 128~255 的字符和 \ 字符,则将其 ASCII 码值转化为 16 进制编码的字符串,以 \ 作为开头,后面两个字节分别是这个字符的 16 进制编码,顺序按照由高位到低位排列,也就是第 8-5 位所对应的16进制数字字符(abcdef 这几个字母是小写)作为第一个字节,第 4-1 位作为第二个字节。依次编码下来,得到的就是 <value> 的内容了。

个人认为这种大 S 方式的序列化没有小 s 方式好,对于反序列化来说,小 s 方式可以获得更快的速度。所以,我认为 PHP6 的这种修改是 PHP6 的一大败笔!如果要在其它语言实现 PHP 序列化,只要支持小 s 方式序列化就可以了,使用大 S 方式序列化,将与目前主流的 PHP4 和 PHP5 不兼容。但是反序列化应该实现大 S 方式的反序列化,这样可以兼容 PHP6 序列化的字符串内容。

4.简单复合类型的序列化

PHP 中的复合类型有数组(array)和对象(object)两种,本章主要介绍在简单情况下这两种类型数据的序列化格式。关于嵌套定义的复合类型和自定义序列化方式的对象的序列化格式将在后面的章节详细讨论。

4.1.数组的序列化

数组(array)通常被序列化为:

a:<n>:{<key 1><value 1><key 2><value 2>...<key n><value n>}

其中 <n> 表示数组元素的个数,<key 1>、<key 2>……<key n> 表示数组下标,<value 1>、<value 2>……<value n> 表示与下标相对应的数组元素的值。

下标的类型只能是整型或者字符串型(包括后面那种 Unicode 字符串型),序列化后的格式跟整型和字符串型数据序列化后的格式相同。

数组元素值可以是任意类型,其序列化后的格式与其所对应的类型序列化后的格式相同。

4.2.对象的序列化

对象(object)通常被序列化为:

O:<length>:"<class name>":<n>:{<field name 1><field value 1><field name 2><field value 2>...<field name n><field value n>}

其中 <length> 表示对象的类名 <class name> 的字符串长度。<n> 表示对象中的字段1个数。这些字段包括在对象所在类及其祖先类中用 var、public、protected 和 private 声明的字段,但是不包括 static 和 const 声明的静态字段。也就是说只有实例(instance)字段。

<filed name 1>、<filed name 2>……<filed name n>表示每个字段的字段名,而 <filed value 1>、<filed value 2>……<filed value n> 则表示与字段名所对应的字段值。

字段名是字符串型,序列化后格式与字符串型数据序列化后的格式相同。

字段值可以是任意类型,其序列化后的格式与其所对应的类型序列化后的格式相同。

但字段名的序列化与它们声明的可见性是有关的,下面重点讨论一下关于字段名的序列化。

4.3.对象字段名的序列化

var 和 public 声明的字段都是公共字段,因此它们的字段名的序列化格式是相同的。公共字段的字段名按照声明时的字段名进行序列化,但序列化后的字段名中不包括声明时的变量前缀符号 $。

protected 声明的字段为保护字段,在所声明的类和该类的子类中可见,但在该类的对象实例中不可见。因此保护字段的字段名在序列化时,字段名前面会加上

\0*\0

的前缀。这里的 \0 表示 ASCII 码为 0 的字符,而不是 \0 组合。

private 声明的字段为私有字段,只在所声明的类中可见,在该类的子类和该类的对象实例中均不可见。因此私有字段的字段名在序列化时,字段名前面会加上

\0<declared class name>\0

的前缀。这里 <declared class name> 表示的是声明该私有字段的类的类名,而不是被序列化的对象的类名。因为声明该私有字段的类不一定是被序列化的对象的类,而有可能是它的祖先类。

字段名被作为字符串序列化时,字符串值中包括根据其可见性所加的前缀。字符串长度也包括所加前缀的长度。其中 \0 字符也是计算长度的。


1注: 在 PHP 手册中,字段被称为属性,而实际上,在 PHP 5 中引入的用 __set、__get 来定义的对象成员更适合叫做属性。因为用 __set、__get 来定义的对象成员与其它语言中的属性的行为是一致,而 PHP 手册中所说的属性实际上在其他语言中(例如:C#)中被称为字段,为了避免混淆,这里也称为字段,而不是属性。

5.嵌套复合类型的序列化

上一章讨论了简单的复合类型的序列化,大家会发现对于简单的数组和对象其实也很容易。但是如果遇到自己包含自己或者 A 包含 B,B 又包含 A 这类的对象或数组时,PHP 又该如何序列化这种对象和数组呢?本章我们就来讨论这种情况下的序列化形式。

5.1.对象引用和指针引用

在 PHP 中,标量类型数据是值传递的,而复合类型数据(对象和数组)是引用传递的。但是复合类型数据的引用传递和用 & 符号明确指定的引用传递是有区别的,前者的引用传递是对象引用,而后者是指针引用。

在解释对象引用和指针引用之前,先让我们看几个例子。

<?php
echo "<pre>";
class SampleClass {
var $value;
}
$a = new SampleClass();
$a->value = $a;

$b = new SampleClass();
$b->value = &$b;

echo serialize($a);
echo "\n";
echo serialize($b);
echo "\n";
echo "</pre>";
?>

这个例子的输出结果是这样的:

O:11:"SampleClass":1:{s:5:"value";r:1;}
O:11:"SampleClass":1:{s:5:"value";R:1;}

大家会发现,这里变量 $a 的 value 字段的值被序列化成了 r:1,而 $b 的 value 字段的值被序列化成了 R:1。

但是对象引用和指针引用到底有什么区别呢?

大家可以看下面这个例子:

echo "<pre>";
class SampleClass {
var $value;
}
$a = new SampleClass();
$a->value = $a;

$b = new SampleClass();
$b->value = &$b;

$a->value = 1;
$b->value = 1;

var_dump($a);
var_dump($b);
echo "</pre>";

大家会发现,运行结果也许出乎你的预料:

object(SampleClass)#1 (1) {
["value"]=>
int(1)
}
int(1)

改变 $a->value 的值仅仅是改变了 $a->value 的值,而改变 $b->value 的值却改变了 $b 本身,这就是对象引用和指针引用的区别。

不过很不幸的是,PHP 对数组的序列化犯了一个错误,虽然数组本身在传递时也是对象引用传递,但是在序列化时,PHP 似乎忘记了这一点,看下面的例子:

echo "<pre>";
$a = array();
$a[1] = 1;
$a["value"] = $a;

echo $a["value"]["value"][1];
echo "\n";
$a = unserialize(serialize($a));
echo $a["value"]["value"][1];
echo "</pre>";

结果是:

1

大家会发现,将原数组序列化再反序列化后,数组结构变了。原本 $a["value"]["value"][1] 中的值 1,在反序列化之后丢失了。

原因是什么呢?让我们输出序列化之后的结果来看一看:

$a = array();
$a[1] = 1;
$a["value"] = $a;

echo serialize($a);

结果是:

a:2:{i:1;i:1;s:5:"value";a:2:{i:1;i:1;s:5:"value";N;}}

原来,序列化之后,$a["value"]["value"] 变成了 NULL,而不是一个对象引用。

也就是说,PHP 只对对象在序列化时才会生成对象引用标示(r)。对所有的标量类型和数组(也包括 NULL)序列化时都不会生成对象引用。但是如果明确使用了 & 符号作的引用,在序列化时,会被序列化为指针引用标示(R)。

5.2.引用标示后的数字

在上面的例子中大家可能已经看到了,对象引用(r)和指针引用(R)的格式为:

r:<number>;
R:<number>;

大家一定很奇怪后面这个 <number> 是什么吧?本节我们就来详细讨论这个问题。

这个 <number> 简单的说,就是所引用的对象在序列化串中第一次出现的位置,但是这个位置不是指字符的位置,而是指对象(这里的对象是泛指所有类型的量,而不仅限于对象类型)的位置。

我想大家可能还不是很明白,那么我来举例说明一下:

class ClassA {
var $int;
var $str;
var $bool;
var $obj;
var $pr;
}

$a = new ClassA();
$a->int = 1;
$a->str = "Hello";
$a->bool = false;
$a->obj = $a;
$a->pr = &$a->str;

echo serialize($a);

这个例子的结果是:

O:6:"ClassA":5:{s:3:"int";i:1;s:3:"str";s:5:"Hello";s:4:"bool";b:0;s:3:"obj";r:1;s:2:"pr";R:3;}

在这个例子中,首先序列化的对象是 ClassA 的一个对象,那么给它编号为 1,接下来要序列化的是这个对象的几个成员,第一个被序列化的成员是 int 字段,那它的编号就为 2,接下来被序列化的成员是 str,那它的编号就是 3,依此类推,到了 obj 成员时,它发现该成员已经被序列化了,并且编号为 1,因此它被序列化时,就被序列化成了 r:1; ,在接下来被序列化的是 pr 成员,它发现该成员实际上是指向 str 成员的一个引用,而 str 成员的编号为 3,因此,pr 就被序列化为 R:3; 了。

PHP 是如何来编号被序列化的对象的呢?实际上,PHP 在序列化时,首先建立一个空表,然后每个被序列化的对象在被序列化之前,都需要先计算该对象的 Hash 值,然后判断该 Hash 值是否已经出现在该表中了,如果没有出现,就把该 Hash 值添加到这个表的最后,返回添加成功。如果出现了,则返回添加失败,但是在返回失败前先判断该对象是否是一个引用(用 & 符号定义的引用),如果不是则也把 Hash 值添加到表后(尽管返回的是添加失败)。如果返回失败,则同时返回上一次出现的位置。

在添加 Hash 值到表中之后,如果添加失败,则判断添加的是一个引用还是一个对象,如果是引用,则返回 R 标示,如果是对象,则返回 r 标示。因为失败时,会同时返回上一次出现的位置,因此,R 和 r 标示后面的数字,就是这个位置。

5.3.对象引用的反序列化

PHP 在反序列化处理对象引用时很有意思,如果反序列化的字符串不是 PHP 的 serialize() 本身生成的,而是人为构造或者用其它语言生成的,即使对象引用指向的不是一个对象,它也能正确地按照对象引用所指向的数据进行反序列化。例如:

echo "<pre>";
class StrClass {
var $a;
var $b;
}

$a = unserialize('O:8:"StrClass":2:{s:1:"a";s:5:"Hello";s:1:"b";r:2;}');

var_dump($a);
echo "</pre>";

运行结果:

object(StrClass)#1 (2) {
["a"]=>
string(5) "Hello"
["b"]=>
string(5) "Hello"
}

大家会发现,上面的例子反序列化后,$a->b 的值与 $a->a 的值是一样的,尽管 $a->a 不是一个对象,而是一个字符串。因此如果大家用其它语言来实现序列化的话,不一定非要把 string 作为标量类型来处理,即使按照对象引用来序列化拥有相同字符串内容的复合类型,用 PHP 同样可以正确的反序列化。这样可以更节省序列化后的内容所占用的空间。

6.自定义对象序列化

6.1.PHP 4 中自定义对象序列化

PHP 4 中提供了 __sleep 和 __wakeup 这两个方法来自定义对象的序列化。不过这两个函数并不改变对象序列化的格式,影响的仅仅是被序列化字段的个数。关于它们的介绍,在 PHP 手册中写的还算比较详细。这里就不再多做介绍了。

6.2.PHP 5 中自定义对象序列化

PHP 5 中增加了接口(interface)功能。PHP 5 本身提供了一个 Serializable 接口,如果用户在自己定义的类中实现了这个接口,那么在该类的对象序列化时,就会被按照用户实现的方式去进行序列化,并且序列化后的标示不再是 O,而改为 C。C 标示的格式如下:

C:<name length>:"<class name>":<data length>:{<data>}

其中 <name length> 表示类名 <class name> 的长度,<data length> 表示自定义序列化数据 <data> 的长度,而自定义的序列化数据 <data> 是完全的用户自己定义的格式,与 PHP 序列化格式可以完全无关,这部分数据由用户自己实现的序列化和反序列化接口方法来管理。

Serializable 接口中定义了 2 个方法,serialize() 和 unserialize($data),这两个方法不会被直接调用,而是在调用 PHP 序列化函数时,被自动调用。其中 serialize 函数没有参数,它的返回值就是 <data> 的内容。而 unserialize($data) 有一个参数 $data,这个参数的值就是 <data> 的内容。这样大家应该就明白了,实际上接口中 serialize 方法就是让用户来自己序列化对象中的内容,序列化后的内容格式,PHP 并不关心,PHP 只负责把它充填到 <data> 中,等到反序列化时,PHP 只负责取出这部分内容,然后传给用户实现的 unserialize($data) 接口方法,让用户自己去反序列化这部分内容。

下面举个简单的例子,来说明 Serializable 接口的使用:

class MyClass implements Serializable
{
public $member;

function MyClass()
{
$this->member = 'member value';
}

public function serialize()
{
return wddx_serialize_value($this->member);
}

public function unserialize($data)
{
$this->member = wddx_deserialize($data);
}
}
$a = new MyClass();
echo serialize($a);
echo "\n";
print_r(unserialize(serialize($a)));

输出结果为(浏览器中的源代码):

C:7:"MyClass":90:{<wddxPacket version='1.0'><header/><data><string>member value</string></data></wddxPacket>}
MyClass Object
(
[member] => member value
)

因此如果想用其它语言来实现 PHP 序列化中的 C 标示的话,也需要提供一种这样的机制,让用户自定义类时,能够自己在反序列化时处理 <data> 内容,否则,这些内容就无法被反序列化了。

7.Unicode 字符串的序列化

好了,最后再谈谈 PHP 6 中关于 Unicode 字符串序列化的问题吧。

说实话,我不怎么喜欢把字符串搞成双字节 Unicode 这种编码的东西。JavaScript 中也是用这样的字符串,因此在处理字节流的东西时,反而非常的不方便。C# 虽然也是用这种方式来编码字符串,不过还好的是,它提供了全面的编码转换机制,而且提供这种字符串到字节流(实际上是到字节数组)的转换,所以处理起来还 算是可以。但是对于不熟悉这个的人来说,转来转去就是个麻烦。

PHP 6 之前一直是按字节来编码字符串的,到了 PHP 6 突然冒出个 Unicode 编码的字符串来,虽然是可选的,但仍然让人觉得非常不舒服,如果配置不当,老的程序兼容性都成问题。

当然加了这个东西以后,许多老的与字符串有关的函数都进行了修改。序列化函数也不例外。因此,PHP 6 中增加了专门的 Unicode 字符串序列化标示 U。PHP 6 中对 Unicode 字符串的序列化格式如下:

U:<length>:"<unicode string>";

这里 <length> 是指原 Unicode String 的长度,而不是 <unicode string> 的长度,因为 <unicode string> 是经过编码以后的字节流了。

但是还有一点要注意,<length> 尽管是原 Unicode String 的长度,但是也不是只它的字节数,当然也不完全是指它的字符数,确切的说是之它的字符单位数。因为 Unicode String 中采用的是 UTF16 编码,这种编码方式使用 16 位来表示一个字符的,但是并不是所有的都是可以用 16 位表示的,因此有些字符需要两个 16 位来表示一个字符。因此,在 UTF16 编码中,16 位字符算作一个字符单位,一个实际的字符可能就是一个字符单位,也有可能由两个字符单位组成。因此, Unicode String 中字符数并不总是等于字符单位数,而这里的 <length> 指的就是字符单位数,而不是字符数。

那 <unicode string> 又是怎样被编码的呢?实际上,它的编码也很简单,对于编码小于 128 的字符(但不包括 \),按照单个字节写入,对于大于 128 的字符和 \ 字符,则转化为 16 进制编码的字符串,以 \ 作为开头,后面四个字节分别是这个字符单位的 16 进制编码,顺序按照由高位到低位排列,也就是第 16-13 位所对应的16进制数字字符(abcdef 这几个字母是小写)作为第一个字节,第 12-9 位作为第二个字节,第 8-5 位作为第三个字节,最后的第 4-1 位作为第四个字节。依次编码下来,得到的就是 <uncode string> 的内容了。

我认为对于其他语言来说,没有必要实现这种序列化方式,因为用这种方式序列化的内容,对于目前的主流 PHP 服务器来说都是不支持的,不过倒是可以实现它的反序列化,这样将来即使跟 PHP 6 进行数据交换,也可以互相读懂了。

8.参考文献

PHP 3 中关于序列化和反序列化的源代码

PHP 4 中关于序列化的源代码

PHP 4 中关于反序列化的源代码

PHP 5 中关于序列化的源代码

PHP 5 中关于反序列化的源代码

PHP 6 中关于序列化的源代码

PHP 6 中关于反序列化的源代码

PHP 手册中关于序列化和反序列化的介绍

Using Serialized PHP with Yahoo! Web Services

Base64转换:AQAB=65537,你知道为什么吗?[转]

base64 encoder

原文来自博客园:http://www.cnblogs.com/midea0978/archive/2007/05/22/755826.html

作者为:在路上

先贴上BASE64的算法介绍:http://popscanner.icpcn.com/download/base64.doc,这是DOC文档,可以下载一看。

什么是Base64?

按照RFC2045的定义,Base64被定义为:Base64内容传送编码被设计用来把任意序列 的8位字节描述为一种不易被人直接识别的形式。(The Base64 Content-Transfer-Encoding is designed to represent arbitrary sequences of octets in a form that need not be humanly readable.)

为什么要使用Base64?

在设计这个编码的时候,我想设计人员最主要考虑了3个问题:
1.是否加密?
2.加密算法复杂程度和效率
3.如何处理传输?

加密是肯定的,但是加密的目的不是让用户发送非常安全的Email。这种加密方式主要就是“防君子不防小人”。即达到一眼望去完全看不出内容即可。
基 于这个目的加密算法的复杂程度和效率也就不能太大和太低。和上一个理由类似,MIME协议等用于发送Email的协议解决的是如何收发Email,而并不 是如何安全的收发Email。因此算法的复杂程度要小,效率要高,否则因为发送Email而大量占用资源,路就有点走歪了。

但 是,如果是基于以上两点,那么我们使用最简单的恺撒法即可,为什么Base64看起来要比恺撒法复杂呢?这是因为在Email的传送过程中,由于历史原 因,Email只被允许传送ASCII字符,即一个8位字节的低7位。因此,如果您发送了一封带有非ASCII字符(即字节的最高位是1)的Email通 过有“历史问题”的网关时就可能会出现问题。网关可能会把最高位置为0!很明显,问题就这样产生了!因此,为了能够正常的传送Email,这个问题就必须 考虑!所以,单单靠改变字母的位置的恺撒之类的方案也就不行了。关于这一点可以参考RFC2046。
基于以上的一些主要原因产生了Base64编码。


在路上作者的原文为:

在RSA加密算法中,RSA公钥的public exponent通常都是65537,用base64来表示就是AQAB,这个转换是怎么得来的呢?
通常的base64算法是实现byte[]与base64字符串之间的转换,如果你习惯下面的转换方式,那就错了:

Console.WriteLine(Convert.ToBase64String(Encoding.Default.GetBytes("65537")));

输出的结果:NjU1Mzc=,并不是AQAB。
那么对于数字转换为base64应该怎么转换呢?首先来回顾一下base64算法原理:

1、算法原理
Base64编码要求把3个8位字节(3*8=24)转化为4个6位的字节(4*6=24),之后在6位的前面补两个0,形成8位一个字节的形式。
具体转化形式如下图:

字符串 "张3"
byte[] 110101 011100 010100 110011
添加前缀00, byte[] 00110101 00011100 00010100 00110011
十进制 53 28 20 51
转码 1 c U z

可以这么考虑:把8位的字节连成一串110101011100010100110011
然后每次顺序选6个出来之后再把这6二进制数前面再添加两个0,就成了一个新的字节。之后再选出6个来,再添加0,依此类推,直到24个二进制数全部被选完。

那么,根据上面的对照表计算出字符串“张3”的base64编码就是"1cUz"
2、转码对照表

Value Encoding Value Encoding Value Encoding Value Encoding
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

3、AQAB与65537
根据上面的原理,实际上65537作为数字就不能直接转换字符串再转换为base64,这样只会得出错误的结果,应该直接将65537转换为2进制
Step 1: 1 0000 0000 0000 0001
由于转换需要3个8字节的数据,需要在左边加上前导0,结果就是
Step 2:0000 0001 0000 0000 0000 0001
然后依次取6位,加上00,变成下面的一窜数据
Step 3:0000 0000 0001 0000 0000 0000 0000 0001
每8转换为10进制
Step 4:0 16 0 1
对照转码表
Step 5:AQAB
这就是为什么AQAB=65537,按照上面的原理我们就可以将RSA加密的密钥大质数转换为byte[]或者base64字符串存储了,.net中就是按照base64格式存储的。
4、代码实现
如何用代码来实现上面的转换呢,在.net中有一个类BitConverter可以实现int32=>byte[]的转换,但是转换后高低位的存储位置颠倒了,需要转换过来
例如数字65520=1111 1111 1111 0000=〉BitConverter.GetBytes=〉byte[]={240,255,0,0}
实际我们需要转换为byte[]={0,0,255,240},接下来用Convert.ToBase64String就可以了,下面是演示代码[代码不具有通用性转换性,仅演示]:

public static void demo(){
byte[] bits=BitConverter.GetBytes(65537);
byte[] newbits=new byte[bits.Length-1]; //避免产生padding位,去掉高位0,仅取3位
for(int i=0;i<newbits.Length;i++){
newbits[i]=bits[bits.Length-2-i];//newbits[0]=bits[2],依次
}
String s=Convert.ToBase64String(newbits);
Console.WriteLine("Base64="+s);
newbits=Convert.FromBase64String(s);
for(int i=0;i<newbits.Length;i++){
bits[bits.Length-2-i]=newbits[i];//newbits[0]=bits[2],依次
}
bits[3]=0;//高位补0
Console.WriteLine("Num="+BitConverter.ToInt32(bits,0));

}

后来看到有人回复;

字节顺序取决于特定的计算机结构,所以不应该假定程序总运行在little-endian字节顺序的环境中:
byte[] bits=BitConverter.GetBytes((int)65537);
if(BitConverter.IsLittleEndian) Array.Reverse(bits);
int ofs = 0;
while(bits[ofs]==0 && ofs < bits.Length) ofs++;
String s=Convert.ToBase64String(bits,ofs,bits.Length - ofs);
//Write s
byte[] intBytes = new byte[4];
bits = Convert.FromBase64String(s);
bits.CopyTo(intBytes,intBytes.Length-bits.Length);
if(BitConverter.IsLittleEndian) Array.Reverse(intBytes);
int num = BitConverter.ToInt32(intBytes,0);
//Write num