当前最新版3.9.3已经可以支持Emoji 

----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

在为领航信息开发 eMessage 支持的时候,我们曾使用著名的开源 XMPP 服务器软件 Openfire。但在使用中遇到了几个问题,并通过修改源代码将这些问题解决掉了。接下来的几篇文章,我会介绍一下这些问题并讲述是如何解决掉的。

先介绍一下背景。XMPP 是一个开放的即时通讯协议,非常不错,有很多开源软件实现了 XMPP 协议,Openfire 算是实现得比较全的,而且安装配置比较容易。其他比较流行的开源 XMPP 服务器还有 Tigase 和 ejabberd。我们现在已经切换到了 ejabberd 上,毕竟 WhatsApp 最初也使用的是 ejabberd 嘛,呵呵。读者如果有兴趣,我也可以跟大家讲讲这几个 XMPP 服务器的区别和潜在问题。

问题描述

Emoji 现在基本已经成为一种工业事实标准,最早在日本流行,由日本的 DoKoMo 等运营商支持,后来苹果在 iOS 中支持了这一技术,最终变得全世界流行起来。Emoji 使用了一些 UNICODE 字符集中尚未定义的码位来表示一个个的表情图案。和一般的字体不同,应用程序在遇到这些字符的时候,需要使用位图来显示对应的表情图标,而不是直接使用字体中定义的字型来显示(当然,也可以用字体来显示 Emoji 字符,比如某些 Linux 控制台就可以显示部分 Emoji 字符)。前者可以是彩色的,而后者只能是某个特定的颜色。所以,要显示 Emoji 就要在应用程序中做扩展,在输出这些特定字符的时候做特殊处理,将其用对应的表情位图显示出来。让客户端应用支持 Emoji 并不是很难的工作,Android 的短信应用不支持 Emoji,但支持预先定义的特定字符序列来表示特定的表情,比如将“:)”显示成笑脸。处理这种字符序列的方法和处理 Emoji 表情的方法本质上一样的。
 
在使用 Openfire 作为 XMPP 服务器,将表示 Emoji 的 UNICODE 字符发送给其他用户的时候,就会出现问题。问题的原因在于 Emoji 使用的 UNICODE 字符集码位尚未被 UNICODE 标准化组织标准化,而 Openfire 会将 Emoji 字符看成是不符合标准的字符而直接忽略掉或者干脆断开客户端的连接。因此,要解决这个问题,其实相当容易,通过搜索引擎可以很快找到了解决方案。

解决方案

在 Openfire 3.8.2 版本源代码术中,修改 openfire_src/src/java/org/jivesoftware/openfire/net 目录下的 MXParser.java 文件的最后一个函数:
  1. /**
  2. * Makes sure that each individual character is a valid XML character.
  3. *
  4. * Note that when MXParser is being modified to handle multibyte chars correctly, this method needs to change (as
  5. * then, there are more codepoints to check).
  6. */
  7. @Override
  8. protected char more() throws IOException, XmlPullParserException {
  9. final char codePoint  = super.more(); // note - this does NOT return a codepoint now, but simply a (single byte) char
  10. er!
  11. if ((codePoint == 0x0) ||  // 0x0 is not allowed, but flash clients insist on sending this as the very first
  12. racter of a stream. We should stop allowing this codepoint after the first byte has been parsed.
  13. (codePoint == 0x9) ||
  14. (codePoint == 0xA) ||
  15. (codePoint == 0xD) ||
  16. ((codePoint >= 0x20) && (codePoint <= 0xD7FF)) ||
  17. ((codePoint >= 0xE000) && (codePoint <= 0xFFFD))) {
  18. return codePoint;
  19. throw new XmlPullParserException("Illegal XML character: " + Integer.parseInt(codePoint+"", 16));
  20. }

先看看这个函数的作用。如注释所言,这个函数用来判定特定字符是否是一个合法的 XML 字符。XML 一般要求按照 UTF8 编码的方式存储和传输字符,在程序处理时,会直接转换成 UNICODE 的 UCS 形式,这样便于程序做处理。

 
在 Linux 控制台上运行 $ man utf8 命令,你可以迅速知悉 UNICODE 码位范围以及和 UTF-8 编码之间的对应关系:
  1. The following byte sequences are used to represent a character.  The sequence to be used depends on the  UCS  code
  2. number of the character:
  3. 0x00000000 - 0x0000007F:
  4. 0xxxxxxx
  5. 0x00000080 - 0x000007FF:
  6. 110xxxxx 10xxxxxx
  7. 0x00000800 - 0x0000FFFF:
  8. 1110xxxx 10xxxxxx 10xxxxxx
  9. 0x00010000 - 0x001FFFFF:
  10. 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
  11. 0x00200000 - 0x03FFFFFF:
  12. 111110xx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx
  13. 0x04000000 - 0x7FFFFFFF:
  14. 1111110x 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx

从上面的对应关系就可以知悉,UNICODE 可能的码位(code point 或者 code number)最大可以到 0x7FFFFFFF!而 Openfire 判断是否为合法 XML 字符的范围只到 0xFFFD!显然,不能显示正确处理 Emoji 字符的原因是 Openfire 将用来表示 Emoji 字符的那些码位范围给当成非法 XML 字符了。一种比较简单粗暴的修改办法是:

  1. /**
  2. * Makes sure that each individual character is a valid XML character.
  3. *
  4. * Note that when MXParser is being modified to handle multibyte chars correctly, this method needs to change (as
  5. * then, there are more codepoints to check).
  6. */
  7. @Override
  8. protected char more() throws IOException, XmlPullParserException {
  9. final char codePoint  = super.more(); // note - this does NOT return a codepoint now, but simply a (single byte) char
  10. r!
  11. if ((codePoint == 0x0) ||  // 0x0 is not allowed, but flash clients insist on sending this as the very first
  12. acter of a stream. We should stop allowing this codepoint after the first byte has been parsed.
  13. (codePoint == 0x9) ||
  14. (codePoint == 0xA) ||
  15. (codePoint == 0xD) ||
  16. ((codePoint >= 0x20) && (codePoint <= 0xFFFD)) ||
  17. ((codePoint >= 0x10000) && (codePoint <= 0x10FFFF))) {
  18. return codePoint;
  19. }
  20. throw new XmlPullParserException("Illegal XML character: " + Integer.parseInt(codePoint+"", 16));
  21. }

但马上有高手指出,这个方法太粗暴了,可能带来一些安全隐患(参见:http://community.igniterealtime.org/thread/48846),所以更加正确的方法是:

  1. @Override
  2. protected char more() throws IOException, XmlPullParserException {
  3. final char codePoint = super.more(); // note - this does NOT return a codepoint now, but simply a (double byte) character!
  4. boolean validCodepoint = false;
  5. boolean isLowSurrogate = Character.isLowSurrogate(codePoint);
  6. if ((codePoint == 0x0)|| // 0x0 is not allowed, but flash clients insist on sending this as the very first character of a stream. We should stop allowing this codepoint after the first byte has been parsed.
  7. (codePoint == 0x9) ||
  8. (codePoint == 0xA) ||
  9. (codePoint == 0xD)||
  10. ((codePoint >= 0x20) && (codePoint <= 0xD7FF)) ||
  11. ((codePoint >= 0xE000) && (codePoint <= 0xFFFD))) {
  12. validCodepoint = true;
  13. }
  14. else if (highSurrogateSeen) {
  15. if (isLowSurrogate) {
  16. validCodepoint = true;
  17. } else {
  18. throw new XmlPullParserException(
  19. "High surrogate followed by non low surrogate '0x"
  20. + String.format("%x", (int) codePoint) + "'");
  21. }
  22. }
  23. else if (isLowSurrogate) {
  24. throw new XmlPullParserException("Low surrogate '0x "+ String.format("%x", (int) codePoint)+ " without preceeding high surrogate");
  25. }
  26. else if (Character.isHighSurrogate(codePoint)) {
  27. highSurrogateSeen = true;// Return here so that highSurrogateSeen is not reset
  28. return codePoint;
  29. }
  30. // Always reset high surrogate seen
  31. highSurrogateSeen = false;
  32. if (validCodepoint)
  33. return codePoint;
  34. throw new XmlPullParserException("Illegal XML character '0x"+ String.format("%x", (int) codePoint) + "'");
  35. }
有了上述修改,你的 Openfire 服务器就可以正确处理 Emoji 了。Openfire 最新的版本是 3.9.1,估计已经修改掉这个问题了吧,但本人未确认。

在 MySQL 中存储 Emoji 字符

没想到,为了在 MySQL 数据库中保存 Emoji 字符,需要使用 MySQL 5.5 以上版本引入的 utf8mb4 的字符集。原来 MySQL 的 utf8 字符集只支持编码为一个、两个字节或者三个字节的情形,也就是 UNICODE  UCS 编码范围为 0 到 0xFFFD 这种情形。要支持超过这个范围的 UTF8 编码字符,就需要使用 MySQL 5.5 中引入的 utf8mb4 字符集。从名字中可以看出,这个字符集专门用来支持单个 UNICODE 的 UTF8 编码长度达到四个字节的情形。当然,超过也许也是可以的。至于为什么不能直接用 utf8 编码来兼容这些字符,我就不知道了,也许是历史原因吧。
 
大家可以用“mysql utf8mb4”为关键词搜索一下就知道如何设置/配置 mysql 来支持这个字符集了。但是,要让 openfire 能够和 mysql 正确打交道,还需要升级一下 openfire 使用的 JAVA mysql 数据库连接器(connnector),要升级到最新的版本。否则,如果使用老的 mysql 数据库连接器,会出现无法理解 utf8mb4 字符集的情形。
 
吐槽一下,我实在是不能理解为什么 MySQL 要引入 utf8mb4 这个字符集,难不成将来还需要引入 utf8mb6、utf8mb8 这样的字符集不成?哪位大侠可以帮我解答这个疑惑?

后记

俺是不太喜欢 JAVA 语言的,至今未能使用 JAVA 语言完整编写过一个程序,这实在是本人二十多年码农生涯的一大遗憾,但打打补丁这事儿还是可以做做的。下一篇文章给大家介绍一个针对 Openfire 服务器在集群环境下处理 SOCKS5 代理的问题,就修改了几行代码,但解决了一个大问题。
 
http://blog.csdn.net/ldwtill/article/details/23210835

XMPP 服务器 Openfire 的 Emoji 支持问题(进行部分修改)的更多相关文章

  1. Strophe.js连接XMPP服务器Openfire、Tigase实现Web私聊、群聊(MUC)

    XMPP(Extensible Messaging and Presence Protocol)是一种网络即时通讯协议,它基于XML,具有很强的扩展性,被广泛使用在即时通讯软件.网络游戏聊天.Web聊 ...

  2. xmpp和OpenFire示例,即时聊天室,支持离线消息

    让我说说为什么写这个博客,这是因为我在上周末的研究XMPP和OpenFire,从互联网上下载Demo,但跑不起来.它花了很长的时间.它被改造.抬高.篇博文也是希望后边学习XMPP和OpenFire的同 ...

  3. 技术笔记:XMPP之openfire+spark+smack

    在即时通信这个领域目前只找到一个XMPP协议,在其协议基础上还是有许多成熟的产品,而且是开源的.所以还是想在这个领域多多了解一下. XMPP协议:具体的概念我就不写了,毕竟这东西网上到处是.简单的说就 ...

  4. IOS Socket 05-XMPP开始&安装服务器openfire&安装配置客户端

    1. 即时通讯技术简介(IM) 即时通讯技术(IM-Instant Messageing)支持用户在线实时交谈.如果要发送一条信息,用户需要打开一个小窗口,以便让用户及其朋友在其中输入信息并让交谈双方 ...

  5. 常用的XMPP服务器

    1. Openfire (Wildfire) 3.x 底层通讯采用的mina框架,minak框架其实性能一般,netty早已经超越它,虽然最初都是Doug Lea写的.3.4版本之后支持集群,单台服务 ...

  6. XMPP 和 OpenFire

    XMPP XMPP(可扩展消息处理现场协议)是基于可扩展标记语言(XML)的协议,它用于即时消息(IM)以及在线现场探测.是一种数据传输协议. XMPP的前身是Jabber,一个开源形式组织产生的网络 ...

  7. 基于MAC10.12+MYSQL5.7.17搭建XMPP服务器【黑苹果系统】

    在以前的公司中了解到XMPP可以搭建即时通讯APP.出于好奇自己在空余时间也学了一下搭建XMPP服务器,其中遇到了许多问题,经过坎坷的路程终于搭建成功[这些坎坷的经历主要是由于自己的无知造成的] 下面 ...

  8. 在MAC中安装XMPP服务器

    一.安装MySQL 1.下载安装包

  9. Android基于XMPP Smack openfire 开发的聊天室

    Android基于XMPP Smack openfire 开发的聊天室(一)[会议服务.聊天室列表.加入] http://blog.csdn.net/lnb333666/article/details ...

随机推荐

  1. (Problem 62)Cubic permutations(待续)

    The cube, 41063625 (3453), can be permuted to produce two other cubes: 56623104 (3843) and 66430125 ...

  2. File,FileInputStream,FileReader,InputStreamReader,BufferedReader 的使用和区别

    1 ) File 类介绍 File 类封装了对用户机器的文件系统进行操作的功能.例如,可以用 File 类获得文件上次修改的时间移动, 或者对文件进行删除.重命名.换句话说,流类关注的是文件内容,而 ...

  3. Effective C++ 第二版 10) 写operator delete

    条款10 写了operator new就要同时写operator delete 写operator new和operator delete是为了提高效率; default的operator new和o ...

  4. android TDD平台插入双卡时,查看允许返回发送报告的选项,去掉勾选,不起作用

    请在MultiSimPreferenceActivity.java 下修改 修改1: 函数 isChecked()     private boolean isChecked(String prefe ...

  5. [Leetcode]-containsNearbyDuplicate

    //题目: //给定一个整数数组与一个整数k,当且存在两个不同的下标i和j满足nums[i] = nums[j]而且| i - j | <= k时返回true.否则返回false. #inclu ...

  6. 解决外贸电商难题,PayPal中国外贸电商大会圆满礼成

        在全球经济一体化的背景下,越来越多的中国企业将目光转移到了海外.对中国的企业而言,要想将生意做到海外大致有两种方法可供选择,一是到海外设立分支机构或者分公司,二是通过外贸电子商务平台实现交易. ...

  7. 飘逸的python - 一个最简单的服务器

    python拥有这种单独起一个服务器监听端口的能力,用标准库的wsgiref就行. from wsgiref.simple_server import make_server def simple_a ...

  8. PDO获取数据的方法fetch()、fetchAll()、setFetchMode()、bindColumn()

    PDO的数据获取方法与其他数据库扩展都非常类似,只要成功执行SELECT查询,都会有结果集对象产生.不管是使用PDO对象中的query()方法,还是使用prepare()和execute()等方法结合 ...

  9. (zz)Linux下Gcc生成和使用静态库和动态库详解

    http://blog.chinaunix.net/uid-23592843-id-223539.html

  10. WebRTC Demo - getUserMedia()

    WebRTC介绍 WebRTC提供三类API: MediaStream,即getUserMedia RTCPeerConnection RTCDataChannel getUserMedia已经由Ch ...