真累啊

本来就懒得跑,没消停几天就又得出差…人生真真是个杯具。

Comments

Web开发的若干安全问题小结

 万恶的项目终于要结题了…这时无论如何确该总结一下。实际上这次项目最大的收获有两部分,本文仅介绍其中之一—Web开发中的一些安全问题及其解决策略。

 项目的安全问题起源于首次测试版本提交。测试方使用了IBM Rational Appscan扫描提交的两部分应用程序,每个应用程序返回了将近二十个安全问题,具体列表这里先省略。Appscan是IBM旗下知名的web安全自动化测试工具,同类产品中较知名的还有HP的Webinspect。测试方回复的意思是说所有安全问题在项目验收前必须全部解决,而事实上本项目从设计阶段起就没有考虑过应用程序级的安全问题,对方也从未提出过此类要求。不过我们还是尝试先在逻辑上处理部分问题,自测后发现所有问题依然存在,包括众所周知且已有很多解决方案的Injection。

 纠结几日后大家终于决定重构,考虑在重构过程中再应用安全策略,于是又花了几周时间问题才得以基本解决。然而在解决web安全问题的过程中我们查阅了较多资料,由于缺乏经验,网上的许多解决方案基本都是无效的。当然也不能排除测试手段随着新的漏洞出现而加强的可能。这里我们仅就使用Appscan 7.8.0.2 Rules-Update 947的测试报告针对一些常见问题给出相应的解决办法。

 应当注意,目前Web安全领域拥有自己的国际性非盈利组织OWASP(Open Web Application Security Project),大部分Web安全问题的讨论都可以在https://www.owasp.org/index.php/Main_Page上找到相应的文章。如果读者对Web安全感兴趣,那么可以直接转向官网,本文接下来只考虑如何让应用程序通过Appscan 7.8的安全测试:)(如果仅仅是服务器或tomcat配置的问题,那么本文将不涉及)

 Injection

 SQL盲注问题十分普遍,一般考虑前端+后台验证。我们最初在后台对每个表单进行了关键词检测,然而测试依然不通过,而且仔细查阅资料后发现我们的过滤规则要比一般规则复杂得多,唯一问题在于处理策略不同:我们是直接给出错误提示,而大部分策略是将关键词截断后交付处理。郁闷的是改进后依然不能通过测试。于是开始求助,发现许多专业的JEE开发人员基本都直接使用一种servlet过滤器,项目地址http://securityfilter.sourceforge.net/,更神奇的是,配置该过滤器后问题的确不存在了,但是从表单直接进行关键词提交后台依然能收到完整语句,比较奇怪。此外,另一个问题Cross Site Scripting跨站点脚本攻击由于此过滤器的出现也不复存在了。

 Inadequate Account lockout

 不充分的账户封锁,这个问题看起来很简单,就是为了防止黑客暴力破解用户密码。尽管目前大多数站点使用的验证码技术已经足以克服此类问题,但Appscan测试对于验证码确实是无能为力的,因此测试方要求取消应用程序的验证码功能。后来使用了错误次数限制方案,如果某用户登录失败超过若干次,则直接禁止用户再尝试登录,直到申请管理员解封为止。

 Session Identifier Not Updated

 会话标识未更新,Appscan给出的描述是建议用户每次登录时需使用新的会话标识。网上给出了一种很简单的解决方案:

[c] session.invalidate();

 Cookie cookie = request.getCookies()[0];

 cookie.setMaxAge(0);[/c]

 上述代码是解决问题的核心,但不会使用照样无法解决任何问题。我们最终的解决方法是限制每次停留在登录页上的时间,如果超时提交登录则返回页面更新session后要求重新登录。另外,对于任何登录失败、用户注销、意外退出等情况都要彻底清除一遍sessionid。最后为了彻底解决这一问题,每次用户进入login页面后先检查sessionid是否更新,否则跳至过渡页清空sessionid后再返回login,问题解决。

 Unencrypted Login Request

 已解密的登录请求,要求就是数据加密传输。开始想当然地编写了一种简单的前端加密算法,再从后台进行解密,然而Appscan依然报此错(现在感觉是测试软件本身的问题,最后再谈)。于是决定转https,JEE平台转https的通行步骤如下:使用java keytool生成RSA密钥,打开tomcat的8443端口,并配置相应密钥即可,具体步骤网上很多,GOOGLE即可。更麻烦的问题随之而来,测试方认为尽管提供了8443的https访问,但仍然还可以通过8080使用普通的http访问,并要求禁止普通模式登录。

 但是由于要把8080端口提供给项目的WebService组件,不太可能完全禁掉。这里在项目web.xml里添加了一些配置解决:

[c]<security-constraint>

<!– Authorization setting for SSL –>

<web-resource-collection >

<web-resource-name >SSL</web-resource-name>

<url-pattern>*.jsp</url-pattern>

<url-pattern>*.action</url-pattern>

</web-resource-collection>

<user-data-constraint>

<transport-guarantee>CONFIDENTIAL</transport-guarantee>

</user-data-constraint>

</security-constraint>

<login-config>

<!– Authorization setting for SSL –>

<auth-method>CLIENT-CERT</auth-method>

<realm-name>Client Cert Users-only Area</realm-name>

</login-config>[/c]

 应注意,由于项目的一些组件无法通过https,因此url-pattern字段只对.jsp和.action进行了限制,如果不做特定限制,则系统默认是全部使用https传输。而且上述设置一旦在某个工程中出现,那么当前tomcat将全局采用这一配置。(这是实验结果,目前不评价其合理性)

 Cross-site request forgery

 跨站点请求伪造,网上研究文章一大堆,同时也是OWASP 2010 TOP 10级别。问题是很久都无法根本解决,经过两周研究后终于得以通过Appscan的CSRF检测。其实基本思路很简单,即在用户的提交的表单中加入一个隐藏域,存储一个唯一验证标识码:如sessionid。用户提交表单后由后台首先验证sessionid与提交标识是否一致,否则报错。

 然而要过Appscan的检测还是不太容易。首先是针对所有action的sessionid验证标识码检查,例如login,如果在session中始终保留一个标识,那么可能会报Session Identifier Not Updated错,解决方法是在登录成功后修改一下验证标识码。其实许多无法彻底解决的原因在于报错处理,起初我们将此类错误全部统一转向了一个安全报错页面,后来始终都无法解决这一问题。后来发现如果将错误情况转至首页则Appscan就不会报错,这可能是一个软件特征匹配的bug。

 除此之外,包括许多tomcat的配置问题这里就不再一一说明了。本次项目所引出的web安全问题确实需要得到重视,然而我们也应看到,过渡迷信流行的自动化测试工具可能会导致在某些问题上缘木求鱼,尤其是对于Appscan这种判断防御行为的复杂软件,仅靠有限的规则设置就当做是web安全的唯一标准——这也并非是聪明人的选择。

web
Comments

即日起开始同步分发blog数据

 本博即将迎来第300篇文章了。重建三年多来我做过多次较大的改动,这一版本无疑是维持较久的(已超过一年时间)。目前觉得对大家有所帮助的还是有关于技术方面的文章,但显然技术需要“交流”才能进步。不过有趣的是本博自诩为“交流频道”,却几乎没有相关交流的内容——这其实与我始终坚守这个独立的小站点有关,更何况百度根本不买我等穷弱儒的账。

 然而独立建站是我十余年的信念,岂能轻易废止。在300篇纪念之际我决定争取彻底改变多年来封闭写作的习惯,将原创文章第一时间推送至renren.com和qqzone,同时进一步向google提交新摘要数据并加强seo。另一方面,由于时间的原因,我能拥有的亲自撰写文章的时间越来越少,但潜在的阅读量仍十分可观。因此决定今后在三个不同的blog上提交侧重不同的数据:

 http://www.hanyi.name mp77的技术交流频道:主站点,注重原创文章,实际上目前已很少有转载内容了。

 http://8621316.qzone.qq.com QQ空间日志:重点转载文章,内容将多样化。

 http://blog.renren.com/micropotato 人人网日志:同步hanyi.name数据,并记录转载+评论的短篇文章。

 采取以上策略的原因在于,由于长时间逐渐减少,在短时间的条件下通过压缩原创篇幅来扩大信息量。如果有朝一日非主站点的数据量足够吸引人的话,我仍会选择将其中数据反导入到主站点中。

可爱的TSP

 有意思的是,我接触到的第一个算法问题就是TSP(旅行商问题),自此以后我开始热衷于计算机科学,遗憾的是至今也未有什么收获。但是很显然,TSP及其衍生问题始终是横亘面前的一座高峰,对每一个人—包括所有计算机科学家,都是那么的不可逾越。也许真要等超越阿兰图灵的大师出现—我等凡人恐怕是看不到这一天了。

 不过这里仍要说明的是,我们学习和体会TSP,并不是为了吃饱撑的自虐,也非为了搬弄别人整出的玩意来给自己凑文章。TSP的解法之多令人惊讶,各种方法—即使是一名高中生和一位大学教授所掌握的,在TSP面前似乎都一律平等。但它为我们巩固和发掘基本算法提供了确切的衡量准则,我想这才应该是凡人研究TSP的真正目的。

 TSP问题最初的形式来源于爱尔兰数学家W. R. Hamilton发明的智力迷绳游戏,即在一张无向图中找到一条Hamilton回路。也就是从无向图中任意起点处找到一条路径,该路径唯一地穿过图中的每个顶点,最终回到起点。通常Hamilton回路也被称为Hamilton圈。经过若干年演进,普林斯顿大学的Hassler Whitney首次将该系列问题命名为Travelling Salesman Problem,即TSP,此时问题不仅限于找到一条回路了—而是要求找到花费尽可能少的解。1972年,伯克利的Richard M. Karp证明了Hamilton回路问题是NP完备的,那么作为Hamilton回路的扩展—TSP问题也至少具有NP难的特性。截止目前,应用传统方法解决TSP问题的有动态规划法、贪心法、回溯法、分支限界法、线性规划方法以及采用混合策略;而对于在有限时间内寻找尽可能优解的大规模TSP问题,目前更多的倾向于采用随机组合优化搜索方法,如模拟退火、禁忌搜索、神经网络、蚁群算法、遗传算法以及混合策略等。

 应当注意的是,一个实用的TSP解决方案,通常包括了两个阶段的改进算法,即第一步的初始路径生成和第二步的优化算法。当然,由于本文并非是研究某种特定的算法及其改进,因此并不会突出介绍其中某一个阶段。下面我们就常用的解决方法进行介绍并做简单评价。

 动态规划

 DP思路清晰,易于实现。显然很方便就能找到TSP的最优子结构,从而给出基本的状态转移方程Mk(i,V) = min(Mk-1(j,V{j}),dij)。需要指出的是,DP解决这一类图论问题都具有较高的时间和空间复杂度,一般可用于求解问题规模较小的TSP问题。

 贪心思想

 Greedy为我们贡献了许多著名的图论算法,如Dijkstra的SSP、Prim和Kruskal的MST等等。然而贪心多用于求近似解,而非定格于最优值。目前贪心思想常用于辅助随机搜索算法,类似一种启发式机制,但初始解在许多情况下与最优解相距较远。

 回溯法

 一般的TSP问题可以简单归纳为排列树问题,那么应用回溯法的确可以得出最优解。问题在于,回溯法的解空间非常庞大,一般的剪枝方法并不十分有效,其时间复杂度上界甚至达到n!。然而通过DP思想的启发,我们可以尝试设计一种自顶向下的动态规划算法解决TSP问题,即备忘录方法。备忘录方法相当于搜索和DP的一种折衷,但也丝毫没有改善较高空间复杂度的问题。

 分支限界法

 BB也是一种解空间搜索方法,但和回溯法有明显区别。回溯法通常是一种DFS搜索方法,而BB实际上是一种BFS方法。其中采用队列式的BB方法本质上就是BFS,只不过添加了一定程度上的剪枝策略。而采用优先队列的BB倾向于寻找一个最优解,在许多最优化问题中,如果限界函数和优先队列设计得当,那么BB的性能要优秀许多。然而不可否认,BB的时间复杂度上界实际上并不比回溯法低。

 整数线性规划

 线性规划方法本身就要比TSP问题复杂得多,这里不得不提LP的原因在于,诞生以来计算机科学本应是崇尚应用的科学,尽管学科本身目前还比不上数学、物理学的历史悠久和博大精深,但后者如今基本上需要计算机来完成从理论到实践的跨越。我认为如果一位科学家只热衷于研究理论,那么他应被称作是数学家或者物理学家。

 这里所说的整数线性规划ILP解决TSP问题,实际上是由美国兰德公司的科学家在上世纪50年代提出的。如今ILP被用于许多学科,最直观的—如现代物流专业,TSP是其必须解决的基本问题之一,目前ILP已成为运筹学中的专门课题。ILP问题描述如下:

 试求min(cx),使其满足Ax=b,其中A为m*n矩阵,x为n维列向量,x>=0。

 根据数据特征的不同,ILP问题又可分为0-1ILP问题、混合ILP问题和纯ILP问题。一般的TSP问题可描述为典型的0-1ILP问题,故可用各种基于线性规划单纯形算法的割平面方法尝试解决。

 实际上,针对大规模TSP问题,现有的确定性方法已经很难在规定时间内得出较好的解—当然,如果不计耗费,那么上述算法恐怕还是得到最优解的截至目前的绝对保证,其花费甚至达到了数百CPU年。那么,许多智能算法的大行其道也就有其合理性了。唯一的问题在于,大部分智能算法来源于生物、物理学仿真和人工智能,学习和理解起来并不容易,更何况要用于解决实际问题。况且当前智能算法的“遍地开花”也表明,此类方法的稳定性和准确性还需得到进一步检验和完善。

 由于时间紧张(万恶的项目居然又延期…),我们这里无法再就智能算法做进一步展开了。下个月的文章里,我们将尝试介绍并实现一种应用确定性方法和智能算法解决类TSP(难度超过非对称TSP)问题的例子。

Comments

忙II

 果然是忙I的升级版,半夜偷偷跑回家逛逛,天亮就得回实验室了。下周的文章主题是古老与永恒—关于tsp问题。

Comments

样条插值的一般方法(下)

 前一篇文章中,我们通过基本的三次Bezier曲线引入了样条曲线插值的一般方法—catmull-rom及其改进算法Bessel—Overhauser。这里首先需要澄清的是,本文题名为“样条插值的一般方法”,实际上仅限于在计算机图形学中作基础应用性的讨论,而非是对数值计算方法中有关插值或曲线拟合的一般性总结,事实上在现阶段研究大量的理论方法对实际工作而言并无多大意义—当然,如果读者来自数学相关专业,本文的内容可能没有任何帮助,甚至起到误导的作用。

 同时,在继续这次的讨论之前,我们需要对上节遗留的问题做一些补充。因为我们愈加注意到,当前的基本造型技术所采用的样条插值方法实际上是改进后的catmull-rom,许多文献介绍了各种各样的改进算法,如果读者确实需要更加灵活可控的样条插值,除了上文文末的参考文献外,可能还需要阅读但不仅限于Kockanek和Bartels等人的论文。

 有了上述样条曲线插值的基础,就可以尝试进行曲面插值了。在这之前,我们应先回顾一下Bezier曲面的构造方法。

 三次Bezier曲面的控制点有4*4个,从基本公式来看,单独固定u值或v值,所得到的截面曲线实际上是普通的三次Bezier曲线。下面的影片演示了三次Bezier曲面的边界曲线构造过程,完整空间的情况比较复杂,待webGL成熟后我们会改进演示……

 

 类似基于分段Bezier曲线的样条曲线插值,同样可以使用局部Bezier曲面拼接的方法构建样条曲面插值。关键问题在于如何尽可能确保曲面拼接的连续性。由已知,两个Bezier曲面在边界处C1连续,实际上即其在边界处的偏导数非零且相等。现在给定m*n个控制点,要求构造通过每个控制点的C1连续的Bezier曲面。分析如下:

 1、按照catmull-rom的思路,易知可对相邻两个控制点插值构建Bezier曲线。

 2、又由前文的结论得知,三次Bezier曲面在边界处完全是一条只受边界点控制的三次Bezier曲线。

 由1、2,将mn个控制点划分为相邻的22角点集,每个角点集内部的4个角点即局部三次Bezier曲面片的顶点。通过Catmull-rom或Bessel-Overhauser算法在每条边界插值2个额外控制点,从而构建4条边界曲线。为了保证相邻曲面片C1连续,需要考虑局部面片中间的4个控制点。

 这里引入三次Bezier曲面在角点处的扭向量的概念:对于三次Bezier曲面,扭向量是指曲面在角点处的二阶混合偏导数。为了保证构建的曲面在边界处偏导数相等,可以将连续条件公式做简单变换,并使用扭向量表示。那么只需给定扭向量即可简单算出剩余的4个控制点值了。

 最简单的扭向量计算方法是直接令其为0,那么其等价于Ferguson法(1964)构造的曲面。其缺陷是曲面在插值点处较“平”,这种情况在真实感图形绘制时会变得尤为明显。还有其它一些扭向量的估计方法,如标准公式,但其仅限于在控制点等间距的情况下使用。当控制点间距不相等时,我们建议参考Buss的《3D computer graphics》中提到的方法。当然,作者推荐的《Curves and Surfaces for Computer-Aided Geometric Design: A Practical Guide》by Gerald Farin也绝对是几何造型领域的必备导论书了。

Comments

假如《宫》被改编成RPG…

 这周休息,看完了最后十集的《宫》。突然想到,如果本剧能改编为3D RPG for pc就更好了。

Comments

样条插值的一般方法(上)

  没有接触过计算机曲线、面生成技术的人可能会想到,如果我们给定一个点集序列,再根据该集合生成一条经过每个点的曲线,同时曲线的形态还可以依据点的相互位置而定。

 然而当我们初步接触了Bezier curve,敬佩之余还带有稍许失望。不过在这之前我们必须承认,“控制点”生成曲线的方法早已是老祖宗的专利,后来Citroen的de Casteljau和Renault的Bezier先后终于完成了样条曲线的数学定义。Bezier曲线的成功得益于其三个出色的特性:控制点简单、光滑度较好且可控以及允许Bezier反算。下面是直接根据三次Bezier公式生成带有四个控制点的Bezier曲线的过程。

 应当注意的是,工业标准的Bezier曲线生成往往采用基于de Casteljau算法的递归细分策略,其目的是为了确保进行计算机数值计算的稳定性,进一步提高曲线的精度。 现在我们希望解决文章开始的疑问,其实质是利用Bezier曲线进行插值,以构建能够通过“所有”控制点(实际上Bezier曲线往往通过首尾两个控制点)的曲线。

 通过对Bezier的学习我们知道,Bezier曲线必然通过首尾控制点的特性其实完全可以被用于插值算法。我们把有序点序列相邻两点作为首尾控制点,据此生成分段Bezier曲线,即可满足要求。这一过程需要解决两个问题:

 1、若采用分段三次Bezier曲线,如何确定除首尾控制点的另外两点;

 2、连接若干分段三次Bezier曲线的平滑性如何得以保证;

 早在1974年,Edwin Catmull和Raphael Rom发表《A class of local interpolating splines》一文,就解决了上述问题,而且至今仍是最重要的曲线插值方法。

 值得一提的是,Edwin Catmull是计算机图形学领域一名做出过许多突出贡献的科学家(如纹理映射、双三次Bezier曲面、反走样算法、曲面细分的优化算法,还独立发明了Z-Buffer技术),而且还是现代影视动画工业的奠基人之一。1979年,Catmull离开了纽约理工学院计算机图形学实验室,加盟Lucas的工业光魔并领导其计算机动画部门。1986年该部门被Steve Jobs收购,Catmull也成为新公司Pixar的CTO。2006年Disney收购Pixar,Catmull成为现任Disney Animation Studios&Pixar Animation Studios总裁。

 假设有m+1个插值点p0,…pm,现在要才用分段三次Bezier曲线,则Catmull-Rom样条曲线包含m-2条Bezier曲线,其中第i条Bezier曲线起始于pi终止于pi+1。设:

 li=½(pi+1 - pi-1)

 且

 pi+ = pi + 1/3li以及pi- = pi - 1/3li

 然后设qi(u)是pipi+pi+1-pi+1为控制点的Bezier曲线—曲线的定义域被变成了i<=u<=i+1。将这些Bezier曲线拼接在一起,就构成了完整的Catmull-Rom样条曲线q(u),使得对于任何i<=u<=i+1都有q(u)=qi(u)。

 其示例如图:

 Catmull-Rom存在一定的不足,当某相邻点间距明显小于其它点的平均相邻距离时,生成的曲线会发生“漂离现象”,也就是导致G1不连续。实际上Catmull-Rom的问题主要在于额外的控制点位置异常,采用被称作Bessel-Overhauser的样条函数即可解决这一问题,这就是不同于均匀参数化的弦长参数化方法,此外还有一种节点值参数化方法,即向心参数化方法。有兴趣的读者可以查阅其它相关文献。

 本文的参考资料来源于Samuel R.Buss的《3D computer graphics—a mathmatical introduction with OpenGL》相关章节,以及两篇分别来自T.Y.Kim和Christopher Twigg的论文。下次我们将讨论构造曲面的一般方法(当然具体日期要根据下周的空余时间而定了)。

Comments

Flowingdata:2010年的10个最佳数据可视化工程

 本周是开学第一周,事务繁多,故无暇进行相关专题讨论了。这里仅转载一篇来自flowingdata的有趣文章,值得一提的是,数据可视化、可视分析等都是最近几年才逐渐兴起的新兴科学,尽管下面这篇文章仅仅能代表原作者的个人观点,但我们似乎能从中一窥可视化技术在未来的科学和经济价值。

 10 Best Data Visualization Projects of the Year – 2010

 同时,这里给出译言网相应译文地址:

 http://article.yeeyan.org/view/191841/163084

 最后,在这里贴一张同样是来自flowingdata的图片(原作者是Moritz Resl)。

 

 对于这张过于“简单”的状态图,Moritz在swiss-miss上也给出了自己的解释:

 Let’s not forget though that sometimes doing stuff you like leads to sad, and more importantly, doing stuff you don’t like can lead to happy.

 我想,到目前为止这句话至少有半句是对的,因为你必须了解:

 What’s something you really like,more.

Comments

Qt与现有系统字符集的兼容问题研究

本文是假期完成的最后一篇文章,之后我将回到学校,届时更新频率将恢复为每周0-2篇左右。

众所周知,Qt在国际化方面的实现是非常方便的。但有时我们会面临一些问题,如在Windows系统中GB2312/GBK和unicode字符集的不兼容,以及在与现有网络程序实现通信的过程中发生的字符集不兼容问题。当然,此类问题可能比较罕见,而且事实上不只Qt面临这一棘手的情况。经过查阅一些资料并进行实践开发,我们相信Qt提供了目前最为便捷的字符集兼容解决方案。我们的最终应用是实现与Unix/Windows主机上的现有服务端程序进行通信,以实现如http、ftp等低级别应用层协议。

首先介绍GB2312-80、BIG-5、Unicode、GBK以及GB18030。

====================以下内容来自wikipedia====================

GB2312-80是中国国家标准简体中文字符集,全称《信息交换用汉字编码字符集-基本集》,又称GB0,由中国国家标准总局发布,1981年5月1日实施。GB2312中对所收汉字进行了“分区”处理,每区含有94个汉字/符号。这种表示方式也称为区位码。其中:01-09区为特殊符号;16-55区为一级汉字,按拼音排序;56-87区为二级汉字,按部首/笔画排序;10-15区及88-94区则未有编码。

在使用GB2312的程序通常采用EUC储存方法,以便兼容于ASCII。浏览器编码表上的“GB2312”,通常都是指“EUC-CN”表示法。每个汉字及符号以两个字节来表示。第一个字节称为“高位字节”,第二个字节称为“低位字节”。“高位字节”使用了0xA1-0xF7(把01-87区的区号加上0xA0),“低位字节”使用了0xA1-0xFE(把01-94加上0xA0)。由于一级汉字从16区起始,汉字区的“高位字节”的范围是0xB0-0xF7,“低位字节”的范围是0xA1-0xFE,占用的码位是72*94=6768。其中有5个空位是D7FA-D7FE。

Big5,又称为大五码或五大码,是使用繁体中文社区中最常用的电子计算机汉字字符集标准,共收录13,060个汉字。Big5码是一套双字节字符集,使用了双八码存储方法,以两个字节来安放一个字。第一个字节称为“高位字节”,第二个字节称为“低位字节”。“高位字节”使用了0x81-0xFE,“低位字节”使用了0x40-0x7E,及0xA1-0xFE。

Unicode是为了解决传统的字符编码方式的局限而产生的,例如ISO 8859所定义的字符虽然在不同的国家中广泛地使用,可是在不同国家间却经常出现不兼容的情况。很多传统的编码方式都具有一个共同的问题,即其容许电脑进行双语环境式的处理(通常使用拉丁字母以及其本地语言),但却无法同时支持多语言环境式的处理(指可同时处理混合多种语言的情况)。

大概来说,Unicode编码系统可分为编码方式和实现方式两个层次。

1、编码方式

Unicode的编码方式与ISO 10646的通用字符集(Universal Character Set,UCS)概念相对应,目前实际应用的Unicode版本对应于UCS-2,使用16位的编码空间。也就是每个字符占用2个字节。这样理论上一共最多可以表示216即65536个字符。基本满足各种语言的使用。实际上当前版本的Unicode尚未填充满这16位编码,保留了大量空间作为特殊使用或将来扩展。上述16位Unicode字符构成基本多文种平面(Basic Multilingual Plane,简称BMP)。最新(但未实际广泛使用)的Unicode版本定义了16个辅助平面,两者合起来至少需要占据21位的编码空间,比3字节略少。但事实上辅助平面字符仍然占用4字节编码空间,与UCS-4保持一致。未来版本会扩充到ISO 10646-1实现级别3,即涵盖UCS-4的所有字符。UCS-4是一个更大的尚未填充完全的31位字符集,加上恒为0的首位,共需占据32位,即4字节。理论上最多能表示231个字符,完全可以涵盖一切语言所用的符号。BMP字符的Unicode编码表示为U+hhhh,其中每个h 代表一个十六进制数位。与UCS-2编码完全相同。对应的4字节UCS-4编码后两个字节一致,前两个字节的所有位均为0。

2、实现方式

Unicode的实现方式不同于编码方式。一个字符的Unicode编码是确定的。但是在实际传输过程中,由于不同系统平台的设计不一定一致,以及出于节省空间的目的,对Unicode编码的实现方式有所不同。Unicode的实现方式称为Unicode转换格式(Unicode Translation Format,简称为UTF)。

例如,如果一个仅包含基本7位ASCII字符的Unicode文件,如果每个字符都使用2字节的原Unicode编码传输,其第一字节的8位始终为0。这就造成了比较大的浪费。对于这种情况,可以使用UTF-8编码,这是一种变长编码,它将基本7位ASCII字符仍用7位编码表示,占用一个字节(首位补0)。而遇到与其他Unicode字符混合的情况,将按一定算法转换,每个字符使用1-3个字节编码,并利用首位为0或1进行识别。这样对以7位ASCII字符为主的西文文档就大大节省了编码长度(具体方案参见UTF-8)。类似的,对未来会出现的需要4个字节的辅助平面字符和其他UCS-4扩充字符,2字节编码的UTF-16也需要通过一定的算法进行转换。再如,如果直接使用与Unicode编码一致(仅限于BMP字符)的UTF-16编码,由于每个字符占用了两个字节,在Macintosh (Mac)机和PC机上,对字节顺序的理解是不一致的。这时同一字节流可能会被解释为不同内容,如某字符为十六进制编码4E59,按两个字节拆分为4E和59,在Mac上读取时是从低字节开始,那么在Mac OS会认为此4E59编码为594E,找到的字符为“奎”,而在Windows上从高字节开始读取,则编码为U+4E59的字符为“乙”。就是说在Windows下以UTF-16编码保存一个字符“乙”,在Mac OS里打开会显示成“奎”。此类情况说明UTF-16的编码顺序若不加以人为定义就可能发生混淆,于是在UTF-16编码实现方式中使用了大端序(Big-Endian, 简写为UTF-16 BE)、小端序(Little-Endian,简写为UTF-16 LE)的概念,以及可附加的字节顺序记号解决方案,目前在PC机上的Windows系统和Linux系统对于UTF-16编码默认使用UTF-16 LE。

此外Unicode的实现方式还包括UTF-7、Punycode、CESU-8、SCSU、UTF-32等,这些实现方式有些仅在一定的国家和地区使用,有些则属于未来的规划方式。目前通用的实现方式是UTF-16小尾序(LE)、UTF-16大尾序(BE)和UTF-8。在微软公司Windows XP操作系统附带的记事本(Notepad)中,“另存为”对话框可以选择的四种编码方式除去非Unicode编码的ANSI(对于英文系统即ASCII编码,中文系统则为GB2312或Big5编码) 外,其余三种为“Unicode”(对应UTF-16 LE)、“Unicode big endian”(对应UTF-16 BE)和“UTF-8”。

目前辅助平面的工作主要集中在第二和第三平面的中日韩统一表意文字中,因此包括GBK、GB18030、Big5等简体中文、繁体中文、日文、韩文以及越南喃字的各种编码与Unicode的协调性被重点关注。考虑到Unicode最终要涵盖所有的字符,从某种意义而言,这些编码方式也可视作Unicode的出现于其之前的既成事实的实现方式,如同ASCII及其扩展Latin-1一样,后两者的字符在16位Unicode编码空间中的编码第一字节各位全为0,第二字节编码与原编码完全一致。但上述东亚语言编码与Unicode编码的对应关系要复杂得多。

GBK即汉字内码扩展规范,K为汉语拼音 Kuo Zhan(扩展)中“扩”字的声母。英文全称Chinese Internal Code Specification。字符有一字节和双字节编码,00–7F范围内是一位,和ASCII保持一致,此范围内严格上说有96个文字和32个控制符号。之后的双字节中,前一字节是双字节的第一位。总体上说第一字节的范围是81–FE(也就是不含80和FF),第二字节的一部分领域在40–FE,其他领域在80–FE。

GB18030,全称:国家标准GB 18030-2005《信息技术中文编码字符集》,是中华人民共和国现时最新的内码字集,是GB 18030-2000《信息技术信息交换用汉字编码字符集 基本集的扩充》的修订版。与GB 2312-1980完全兼容,与GBK基本兼容,支持GB 13000及Unicode的全部统一汉字,共收录汉字70244个。

GB 18030主要有以下特点:与 UTF-8 相同,采用多字节编码,每个字可以由1个、2个或4个字节组成。编码空间庞大,最多可定义161万个字符。支持中国国内少数民族的文字,不需要动用造字区。汉字收录范围包含繁体汉字以及日韩汉字。

GB 18030的字节结构如下:单字节,其值从0到0x7F。双字节,第一个字节的值从0x81到0xFE,第二个字节的值从0x40到0xFE(不包括0x7F)。四字节,第一个字节的值从0x81到0xFE,第二个字节的值从0x30到0x39,第三个字节从0x81到0xFE,第四个字节从0x30到0x39。

====================以上内容来自wikipedia====================

事实上,上述wiki内容是没有必要细究的:),下面进入正题。

Qt中的内置字符串类为QString,它是以16位unicode编码(unicode 4.0/unicode-16)存储的。QString被应用于所有涉及字符串操作的API中,也就是说,如果你要用Qt构建应用程序,那么研究QString是不可避免的。

下面是一种比较自由的字符集转换方式:

[code] QByteArray encodedString = "mp77的技术交流频道"; //为什么要用QByteArray? QTextCodec *codec = QTextCodec::codecForName("gb2312"); QString string = codec->toUnicode(encodedString); [/code]

QTextCodec为我们提供了针对超过30种常用字符集间互相转换的机制,但这种简单实现代码量较大,不适合字符串操作较多的情况。那么下面这种方案则受到广大程序员的欢迎:

[code] QTextCodec::setCodecForTr(QTextCodec::codecForName("gb2312")); //beginning of code …… QString string = QObject::tr("mp77的技术交流频道"); [/code]

QTextCodec提供了一种类似国际化接口的操作,所有的字符集规定在代码行首部完成,之后只需要借助国际化的方法初始化任意字符串即可(尽管这与真正的国际化功能有所区别)。现在我们解释首段代码中为何使用QByteArray接收中文字符串,如果不做任何处理直接运行:

[code] QString string = "mp77的技术交流频道"; [/code]

由于系统采用ANSI编码(包括Multi-Byte Chactacter System,MBCS字符集),那么上述语句执行后,字符串string在内存中的内容实际上是以ANSI编码。由于对ansi的操作与unicode完全不兼容,那么此后QT中所有的字符串操作都会受到影响,最终输出时就会出现所谓的“乱码”。因此,类似的直接赋值形式应当尽量避免。

我们注意到在vc6时代经常采用以下赋值方式:

[code] const char* string = "mp77的技术交流频道"; [/code]

这一句包括了申请内存、写入内容并以“\0”结尾等操作,其在内存中的字符编码形态自然也是ANSI的。Qt特别提供的QByteArray类就是为了兼容这种情况,而且官方手册声称它比const char *的声明初始化方式要好很多(如采用implicit sharing,简单地说就是COW技术),而且使用上也方便许多。

这里介绍一个我们在实际应用中遇到的例子。题目是实现FTP协议,当然,要想实现一个稳定的FTP协议难度并不小,这主要是因为相关RFC文档并没有提供足够的协议细节,导致目前几乎没有绝对通用的FTP服务端或客户端。当然,目前仍有能够兼容大多数平台的客户端/服务端程序,且在日常使用中似乎没有发现什么问题。

Qt实现FTP客户端很容易——只要使用现成的QFtp即可,但关键的服务端需要手动完成了。我们在使用cuteFTP和FlashFXP这种常用客户端软件进行测试时立即发现了一个严重问题,即服务端返回的目录list中出现汉字乱码。我们检查了服务端生成目录list的函数,并未对任何字符串进行编码操作。于是直接在服务端socket的数据信息中添加了toUtf8的操作。事实表明这一修改没有解决任何问题。

继续检查后我们注意到类似如下一行关键代码:

[code]

QString string;

string = "…";

string += local.toString(fileinfo.lastModified()," MMM dd yyyy ") + filename+"\r\n";

[/code]

上面实际上利用了QString对+/+=的重载进行字符串的简单构建,其中filename是出现乱码的地方——也就是唯一存在中文可能的文件或目录名称。由于字符串常量的存在,我们对string进行直接赋值实际上就导致内存信息为ansi编码,而filename来自于fileinfo.completeBaseName(),是Qt有关目录操作函数返回的文件名信息,其编码自然是utf-16。查阅QString对+/+=的重载说明知,Qt为了提升字符串操作的性能,只是在目标字符串的尾部申请了一块额外的内存空间,并直接将源字符串所在的内存块复制到新空间中。由此可见,string中实际上是ansi和utf-16两种编码的字符串混合,无论我们今后采用何种编码转换方式,都无法实现完整内容的正确转换。

一种解决方案是:

[code]

QString string;

string = "…";

string += local.toString(fileinfo.lastModified()," MMM dd yyyy ") + filename.toLocal8Bit()+"\r\n";

[/code]

toLocal8Bit函数返回的是QByteArray类型的本地字符集串,如此就能保证string保持唯一的字符集了。

近期的文章中我们将介绍如何使用Qt构建网络应用程序,并提供一个代码示例。

Comments