分类目录归档:计算机网络

HTTPS和SNI

问题:有两个站点架在同一台服务器上,并且强制HTTPS访问,地址分别为https://sub.domain.com和https://admin.sub.domain.com,HTTPS证书签名的域名分别为*.domain.com和*.sub.domain.com,使用HttpClient(4.1.1版本)去调用一个接口:

1
2
3
4
5
6
7
public class TestGet {
  public static void main(String args[]) throws Exception {
    HttpGet httpGet = new HttpGet("https://sub.domain.com/api/nodes");
    HttpClient httpClient = new DefaultHttpClient();
    httpClient.execute(httpGet);
  }
}

返回 javax.net.ssl.SSLException异常:

Exception in thread "main" javax.net.ssl.SSLException: hostname in certificate didn't match:  != <*.sub.domain.com> OR <*.sub.domain.com>
    at org.apache.http.conn.ssl.AbstractVerifier.verify(AbstractVerifier.java:231)
    at org.apache.http.conn.ssl.BrowserCompatHostnameVerifier.verify(BrowserCompatHostnameVerifier.java:54)
    at org.apache.http.conn.ssl.AbstractVerifier.verify(AbstractVerifier.java:152)
    at org.apache.http.conn.ssl.AbstractVerifier.verify(AbstractVerifier.java:133)
    at org.apache.http.conn.ssl.SSLSocketFactory.verifyHostname(SSLSocketFactory.java:559)
    at org.apache.http.conn.ssl.SSLSocketFactory.connectSocket(SSLSocketFactory.java:534)
    ……

问题分析:从异常信息来看,显然是SSL连接握手阶段出现证书不匹配的问题,客户端请求的是sub.domain.com域名,服务器返回了*.sub.domain.com的证书。但是浏览器访问又正常,这是为何?

解决方法:故事要从IPv4的设计开始说起,设计者认为32位够用了,后续的上层协议设计者也纷纷假设一个IP提供一个服务的前提,比如HTTP/1.0、SSL等,一个大坑就这样被埋下。从90年代中期开始,互联网发展太快,人们开始意识到IP资源越来越稀缺,于是着手设计IPv6,但是从IPv4到IPv6需要一个很长的过渡期,远远大于IPv4地址耗尽的时间。于是一些IP复用的方案出现了,CIDR和NAT技术能在一定程度上缓和IPv4资源稀缺的问题,互联网协议也适时进行了升级,比如HTTP,在HTTP/1.1版本中加入Host头,这个头部在虚拟主机(Virtual Host)中非常重要,它支持不同站点架在一个IP上,客户端通过Host头告知服务器需要访问的站点。但是Host头无法解决HTTPS场景中客户端与虚拟主机(多站点共享同一个IP)之间加密连接的建立,因为HTTPS依赖的SSL/TLS协议并没有同步跟进,SSL握手阶段客户端向服务器端发送的信息中未包含Host,所以服务器端也就没法返回正确的HTTPS证书了。这样的背景下,解决HTTPS虚拟主机的方法主要有:

1. 绑定不同的端口。为不同的虚拟主机绑定不同的HTTPS端口(默认端口443)。缺点显而易见,每次访问都需要显式指定端口号。
2. 绑定不同的IP。前面说过,IPv4资源越来越稀缺,而且这种方法也违背了虚拟主机的初衷。
3. 购买泛域名SSL证书(Wildcard Certificate)。
4. 购买多域名SSL证书(Multi Domain Certificate)。

这些方法都只是绕过限制,要根本解决问题还是得修改SSL/TLS协议,所以SNI(Server Name Indication)就适时被提了出来,它扩展了TLS协议(SSL 3.0不支持,在TLS 1.0以后支持,RFC4366RFC6606),在客户端请求的CLIENTHELLO阶段加入Host信息,告知服务器端要与哪个主机建立加密连接。
SNI

图片来源

实际上,SNI需要通信双方(服务端、客户端)都支持,根据Wiki上的总结,目前一些常见浏览器、服务器软件、类库的支持情况如下:

浏览器:

IE 7+
Mozilla Firefox 2.0+
Opera 8.0+
Google Chrome 5.0.342.1+
Safari 3.0+

注:这里的支持度还跟操作系统相关,比如Windows XP上所有IE均不支持,Chrome在不同操作系统下开始支持的版本也不同。

服务器软件:

Apache 2.2.12+
Nginx(依赖支持sni的openssl库)
Apache Traffic Server 3.2.0+
HAProxy 1.5+

类库:

OpenSSL: 0.9.8f(compiled in with config option '--enable-tlsext'), 0.9.8j+
libcurl / cURL 7.18.1+
Python 3.2
Qt 4.8
Oracle Java 7 JSSE
Apache HttpComponents 4.3.2
wget 1.14
Android 4.2 (Jellybean MR1)
Go (client and server)

Java是在JDK1.7里才支持SNI的,因此要在Java应用里使用,前提就是将OpenJDK或者Oracle JDK升到1.7,如果同时使用HttpClient进行HTTP接口调用,那么还必须将HttpClient版本升到4.3.2及以后,这个JIRA单描述了如何支持SNI(代码设计层面)的前因后果。

回到开头的异常问题,可以通过以下步骤修复:

1. 升级JDK至少到1.7。
2. 升级HttpClient至少到4.3.2。
3. 更新少量代码如下:

1
2
3
4
5
6
7
8
public class TestGet {
  public static void main(String args[]) throws Exception {
    HttpGet httpGet = new HttpGet("https://sub.domain.com/api/nodes");
    //HttpClient httpClient = new DefaultHttpClient();
    CloseableHttpClient httpClient = HttpClients.createDefault();
    httpClient.execute(httpGet);
  }
}

--EOF--

SSH和Puppet的身份认证机制

互联网上最成熟的身份验证方式就是非对称加密和数字证书技术,它是互联网安全的基石,以此为基础的SSH和SSL/TLS也是不可信网络下进行安全通信的标准协议,可以有效避免通信数据被窃听、篡改和伪造。

这里简单总结下SSH远程登录和Puppet是如何进行通信双方的身份识别。

1. SSH远程登录

SSH的一个主要应用就是安全可靠地登录远程机器,所有数据经过加密传输。客户端和远程主机(服务端)在通信之前需要握手,进行身份验证。首先,服务端任性地向客户端发送自己的公钥PubKey,客户端没得选择,只能线下确认PubKey是否准确,然后接受。如果不进行确认就接受公钥,容易发生中间人攻击,严重的话会导致密码泄露。有了服务端公钥PubKey之后,客户端就要向对方表明自己的身份了,SSH提供了两种方式:第一种是密码登陆,客户端用PubKey加密密码后发送给服务端,服务端用私钥解密后进行身份验证;第二种是密钥登陆,客户端公钥事先存储在服务端,当用户选择密钥登陆时,服务端向客户端发送一个随机数,客户端用用户私钥对随机数加密后发回服务端,服务端将数据解密后得到的随机数如果与之前的一样,表示身份验证通过。密钥登陆能避免中间人攻击。

2. Puppet

Puppet是一个C/S架构的配置管理和部署工具。采用SSL证书对客户端和服务端之间的通信进行身份认证和数据加密传输。Puppet自己提供本地CA,用于签发客户端和服务端的证书,通常情况下,服务端进程会兼任本地CA的职责。当服务端初始化时,首先创建一对本地CA的公私钥,用于签发证书,然后为服务端创建一对公私钥和一张数字证书,这张证书包含了服务端的公钥和公钥签名(用CA私钥签),最后将这张证书分发到所有的客户端;当客户端初始化时,它生成一对公私钥,并且把公钥发送到本地CA(服务端),申请为这个公钥签发一张证书。本地CA(服务端)可以根据不同策略对客户端的申请请求进行确认,这样服务端就有了一张客户端的数字证书。此时,客户端持有了自己的私钥和服务端数字证书,服务端持有了自己的私钥和客户端数字证书,之后的通信就是SSL协议的内容了,双方首先通过本地CA的公钥认证对方数字证书和公钥的合法性,确认双方身份,然后通过非对称加密算法进行随机数的加密传送,推导出双方的会话密钥,最后通过这个会话密钥进行传输内容的对称加解密。

References:
[1]. 『Secure Shell』.
[2]. 『SSH原理与运用(一):远程登录』.
[3]. 『数字签名是什么?』.
[4]. 『SSL』.
[5]. 『SSL/TLS协议运行机制的概述』.
[6]. 『What's the difference between SSH and SSL/TLS?』.
[7]. 『Puppet Labs: Certificates and Security』.

--EOF--

AMQP 0-9-1协议笔记

0x00: 概览

AMQP(Version:0-9-1)协议定义了客户端和消息中间件(broker)的交互过程。它的目标是提供一个标准消息中间件技术,降低企业与系统的集成成本,向大众提供工业级的集成服务。

AMQP由AMQ Model和网络层协议两部分组成,具体来讲,前者由一系列服务器内的路由和消息存储以及一些规则组成,后者则是定义了协议的数据格式。AMQP协议是一个二进制协议,它的主要特征是:多Channel,协商式,异步,安全,可移植,高效。可分为功能层和传输层两个层次:

+------------------Functional Layer----------------+ 
|   Basic Transactions Exchanges Message queues    |
+--------------------------------------------------+
+------------------Transport Layer-----------------+
|      Framing  Content   Data representation      |
|     Error handling  Heart-beating   Channels     |
+--------------------------------------------------+

其中,功能层定义了一个命令集合,如图中划分的Basic、Transaction、Exchange、Queue等。传输层定义了实现上述命令所需的多Channel、心跳、帧格式、数据表示、错误处理等规范。

0x01: 基本架构

AMQ Model主要由exchange,queue, binding三大组件构成,三大组件的作用为:
exchange:从生产者程序接收消息,并路由到queue。exchange是一个匹配和路由引擎,只转发消息,不存储消息。
queue: 存储消息至内存或者磁盘,直到消息被消费者程序取走。
binding: 定义了exchange和queue的绑定关系,提供消息路由的标准。

一条AMQP消息由消息头加消息体构成,消息头是一系列属性的集合。生产者生产消息后,为消息加上routekey,发送到服务器端。exchange根据routekey将消息分发到queue,如果找不到相应queue,比如queue不存在,或者routekey未匹配到任何一个queue, 则exchange会将消息丢弃或者将其返回给生产者。一条消息可以存在于多个queue中,服务器端可以有多种实现方案,可以通过拷贝消息,可以通过引用计数的方式,等等。此时,对于每个queue来说,这条消息是唯一的。

生产者可以指定queue将消息存储在内存还是磁盘,还可以指定当queue没有消费者时,消息是否返回给生产者。消息在服务器端可以被持久化,可以设置优先级,同一队列中,高优先级消息会比低优先级的消息更早被投递。

服务器端不能删除消息头部信息,不能修改消息体内容,但是可以在消息头部添加信息。

vhost是服务器端一系列exchange、queue及相关概念的集合。每个连接都必须所属一个唯一的vhost,因此这个连接内的所有Channel都工作在同一个vhost里。客户端在连接握手的时候选择vhost,具体是在Connection.Open方法中指定,而Connection.Open方法是在认证之后,这意味着服务器端的认证是针对全局的,所有vhost共享。AMQP没有定义如何创建和配置vhost,这部分由实现方自己定义。

生产者不会将消息直接发送到queue, 否则会破坏AMQ model。但是AMQ Model会提供一个默认的exchange,当生产者不指定exchange时,消息都会发送到这个默认的exchange里,同时还提供一个默认的绑定关系,即当消息的routekey和queue名称一致时,exchange会将消息路由到这个queue里,这种模式将AMQP退化为普通的队列操作,用户无需理解exchange和binding的工作流程。

AMQP用“declare”声明exchange,它表示“如果不存在,则创建”,这个操作是幂等的。也提供删除exchange的操作,只是一般来说应用程序用不到。

exchange的类型有以下5种:
1. direct: bindkey为K,消息routekey为R,当R=K时转发。
2. fanout: 群发。
3. topic: bindkey为K,消息routekey为R,当R匹配K时转发。bindkey和routekey格式为由点号(.)分隔的一个或多个word,并且支持*和#通配符。每个word由A-Z,a-z,0-9之间的字符组成,*通配一个word,#通配0个或多个word。
4. header: binding为一个属性集合加x-match参数,当x-match为all时,消息头部必须匹配所有binding属性集合才转发;当x-match为any时,消息头部任意匹配binding属性集合中的一个就可以转发。关于属性匹配的规则是:
1) binding只包含name,不包含value时, 消息头部只需包含相同name,不管有没有value。
2) binding包含name-value,消息头部也要包含name-value,并且两者的name和value要分别一致。
5. system: 转发所有以"amq."开头的routingkey的消息给系统服务。

queue的声明有一些关键3个关键属性:
1. name: 队列名称,如果不填,则服务器会默认给一个。
2. exclusive: 标记当前队列是否是某条连接独享,当连接断开时,队列删除。
3. durable:标记此队列是否需要持久化。如果设置为false,当服务器重启时,队列删除。

queue是一个FIFO的缓冲区,但是在某些情况下,比如有多个消费者,消息设置优先级或者投递策略优化等因素都会破坏队列的FIFO特性(顺序性),唯一能保证队列FIFO特性的方法就是保证只有一个消费者,并且设置消息的优先级相同。queue中的一条消息只会投递给一个消费者,除非这个消费者投递失败(连接断开)或者消费者显式拒绝(Basic.Reject)。对于queue来说,一个Channel就是一个消费者。

Channel.Flow方法可以用于控制服务器端或者客户端的流量,它是能控制生产者发送过多的消息。pre-fetch机制可以控制消费者在ack消息之前服务器最多向其投递的消息数量,以保证服务质量(Qos),对消费者来说,pre-fetch机制比Channel.Flow方法更加优雅。

消息ack有两种方式:
1. 自动:服务器在把消息投递到客户端后(Basic.Deliver或Basic.Get-Ok)后直接将消息删除。
2. 显式:消费者显式发送消息ack方法(单条或者批量),服务器端收到ack方法后删除消息。

AMQP的错误处理有两个层次:
1. Channel异常:一个具体操作级别的错误(operational error)会抛出Channel异常,它不会影响到客户端的整体运行,只需关闭Channel即可,比如队列不存在、未获得操作权限等。
2. Connection异常:一个结构或者协议级别的错误(structural error)会抛出Connection异常,它会影响程序继续往下执行,需关闭整个连接,比如参数配置错误、方法乱序等。

当出现异常时,服务器端会给出3位数字的错误码和一个简单的错误描述文本,这和HTTP类似。

0x02: 命令

AMQP是一种wire-level协议。所谓的wire-level协议,我理解的是它类似API,但是抽象层次更低,API是以“类名.方法名”的方式去调用,而wire-level是直接以字节流的方式在网络层传输,类似HTTP,可以直接在TCP连接上发送"GET ..."字节流来实现。AMQP命令将接口与实现分离,接口是程序员友好的“类名+方法名”格式,而实现则是以帧的形式存在,AMQP协议定义了各个命令的帧格式。

AMQP的方法可以分为两类:
1. 同步请求-响应。通信一端发送一个请求命令(类名+方法名+参数),另一端返回一个响应命令(类名+方法名+响应值)。为简单起见,每个请求命令都对应一个唯一的一个响应命令。
2. 异步通知。通信一端发送一个请求命令,另一端无需返回响应。这种方法一般用在性能优先的场景,比如消息投递。

所有的AMQP命令都是上述的同步请求、同步响应和异步通知三种之一。

各个层次命令的对应关系表述如下(以Queue.Declare为例):

Queue.Declare
  queue=my.queue
  auto-delete=TRUE
  exclusive=FALSE

相应的帧格式为:

+--------+---------+----------+-----------+-----------+ 
|  Queue | Declare | my.queue |     1     |     0     |
+--------+---------+----------+-----------+-----------+ 
   class    method     name    auto-delete   exclusive

更高层次的API表述为:

queue_declare (session, "my.queue", TRUE, FALSE);

伪代码可表述为如下,它的含义是发出去一个同步请求方法后,一直等到与之匹配的响应方法为止,这期间如果收到一个异步方法,则会优先进行处理:

send request method to server 
repeat
  wait for response from server
  if response is an asynchronous method
    process method (usually, delivered or returned content) 
  else
    assert that method is a valid response for request
    exit repeat 
  end-if
end-repeat

AMQP命令可以分成6类,分别为:
1. Connection类。AMQP是面向连接的协议,连接可以多Channel复用。客户端和服务器端的时序为:
C:AMPQ0091 -> S:Start -> C:Start-OK -> S:Secure -> C:Secure-OK -> S:Tune -> C:Tune-OK -> C:Open -> S:Open-OK -> using connection -> Close -> Close-Ok
Close可由任意一端发起,由另一端响应。
2. Channel类。AMQP支持多Channel复用连接,多Channel可以将一条物理连接划分为多个逻辑连接,提升性能,并且实现“防火墙友好”。Channel之间互相独立,且可以同时处理不同的请求,所有Channel共享连接带宽。建议多线程语言环境下采用“channel-per-thread”编程模型。
3. Exchange类。
4. Queue类。
5. Basic类。封装了消息相关的方法。例如生产者发送消息(Basic.Publish)、开始/停止消费消息(Basic.Consume/Basic.Cancel)、服务器发送消息(Basic.Deliver、Basic.Return)、消息ack(Basic.Ack、Basic.Reject)、获取消息(Basic.Get)。
6. Transaction类。AMQP支持两种事务类型:
1)自动事务。服务器端每条生产者发送的消息和消费者发送的ack作为独立的事务,自动提交。
2)本地事务。服务器端缓存生产者发送的消息和消费者发送的ack,由客户端显式发送指令提交。
AMQP事务只有覆盖到生产者发送消息和消费者ack消息,并没有覆盖到服务器端向消费者投递消息,因此,客户端的rollback操作不会重新造成消息入队(requeue)或者投递消息(redeliver)。

0x03: 帧格式

TCP/IP是个流协议,没有内建机制为帧定界,常用的帧定界有三种方式:
1. 每条连接只发送一个帧。简单但是效率低下。
2. 每个帧结尾加特殊标记。简单但是解析效率低下。
3. 将帧的大小放置在每一帧头部。AMQP采用这种方式实现帧定界。

所有AMQP命令都能映射为帧。帧的格式如下:

0      1         3             7            size+7        size+8 
+------+---------+-------------+ +------------+ +-----------+ 
| type | channel |    size     | |   payload  | | frame-end | 
+------+---------+-------------+ +------------+ +-----------+
 octet    short      long size        octets         octet

所有帧都是由一个7字节长的帧头部(frame header),任意字节长度的payload,和一个字节长的帧尾(frame-end)构成。构成帧的数据类型包括位(bit),无符号整数(unsign integer),字符串(string)以及field table(类似map)。

AMQP有四种类型的帧:
type=1: method类型。
type=2: content header类型。
type=3: content body类型。
type=4: 心跳。
当任一端收到的type非上述4种之一,则可以判定协议非法,断开连接。

每个帧都携带一个Channel号,Channel号的可表示范围为0-65535,其中,0表示此帧应用于当前连接,1-65535表示此帧属于某个具体Channel。AMQP通过这种方式来实现多Channel连接复用。从逻辑上看,Channel与连接的关系如下:

    frames      frames      frames      frames
+-----------+-----------+-----------+-----------+ 
|  channel  |  channel  |  channel  |  channel  | 
+-----------+-----------+-----------+-----------+ 
|               socket connection               | 
+-----------------------------------------------+

帧头部的size字段表示payload的长度,这个长度不包含frame-end这个字节。

frame-end在AMQP帧中为固定值0xCE,它的作用是可进行简单的错误检测,说它“简单”是因为此处没有像数据链路层帧那样通过CRC来检测错误。任意一段在解析帧之前,需判断frame-end的有效性。

不同类型的帧有不同的payload:
1. method帧。method帧的payload格式如下:

 0          2           4 
 +----------+-----------+-------------- - - 
 | class-id | method-id | arguments... 
 +----------+-----------+-------------- - -
    short       short       ...

其中,class-id为类id,method-id为方法id。AMQP类和方法的具体定义可以通过查阅amqp-xml-doc0-9.pdf文档

2. content header帧。content帧是一些携带消息应用数据的帧,比如Basic.Publish或者Basic.Deliver方法后面必须紧跟着content帧。从宏观看来,此类帧的发送顺序为:

[method] [content header] [content body] [content body]...

method帧后面跟着一个content header帧,content header帧后面跟着0个或多个content body帧。AMQP之所以将数据放在content帧,而没有包含在method帧里,其设计目的是为了支持类似“zero copy”、sendfile()的功能,即,不用解开method帧取出content数据就能得到content内容。而将content的属性(封装在header里)和内容分离,也是为了方便接收端只需根据content header就能决定是否丢弃不需要的content body,提升效率。

content header帧的payload格式如下:

0          2        4           12               14 
+----------+--------+-----------+----------------+------------- - - 
| class-id | weight | body size | property flags | property list... 
+----------+--------+-----------+----------------+------------- - -
    short     short   long long        short         remainder...

其中,class-id表示此帧所属的method帧的class-id。weight值固定为0。body size是其后跟随的所有content body帧的总大小,当它为0时表示未携带任何数据(比如Basic.Publish了一个空字符串)。

3. content body帧。content body帧的payload整个就是一个二进制流,前跟frame header,后跟frame-end。

4. heartbeat帧。心跳帧目的是保证长时间没有数据流通的tcp连接不被关闭。心跳机制工作在AMQP的传输层即可,因此它独立成帧,没有对应着功能层的某个类和方法。心跳帧的间隔由双方在协议协商阶段商定。在具体实现方面,协议规定了:
1) 所有心跳帧的Channel号为0。
2) 当某一端不支持心跳机制但是收到心跳帧时,直接丢弃不能抛异常。
3) 客户端在收到服务器端的Connection.Tune方法后开始发送心跳帧(to server),在发送Connection.Open方法后开始监控心跳帧(from server)。服务器端在收到Connection.Tune-OK方法后开始发送和监控心跳帧(to/from client)。
4) 任何帧都可以当成心跳使用,因此心跳帧只需在连接上没有数据传递期间发送即可。任一端检测到2个心跳周期未收到数据(数据帧或者心跳帧)时,关闭连接。

Summarized from 『AMQP Protocol Specification

--EOF--

MySQL无法插入Emoji表情问题

测试发现iPhone提交的Emoji表情无法插入MySQL数据库,Java层抛出的异常显示为:

1
2
java.sql.SQLException: Incorrect string value: '\xF0\x9F\x98\x84...' 
    for column 'remark' at row 1

解决这个问题的方法是将MySQL(5.5.3 or later)的字符集从utf8改为utf8mb4,utf8mb4表示支持4个字节表示的UTF-8编码。要理解这个方法,需要知道以下前提知识:

1. UTF-8编码
UTF-8编码是Unicode字符集的一种可变长编码方式,也是目前国际上最通用的一种编码方式,它的好处在于完全兼容ASCII码和ISO 8859-1(Latin-1)编码,最多4个字节就能表示Unicode字符集中的所有字符(U+000000 - U+10FFFF, 共1114112个code points,即码位),用它来编码Unicode字符集时的可编码区间和所需字节数如下:

1
2
3
4
5
6
7
8
9
+-----------------------+--------+
| U+000000 - U+00007F   | 1个字节 |
+-----------------------+--------+
| U+000080 - U+0007FF   | 2个字节 |
+-----------------------+--------+
| U+000800 - U+00FFFF   | 3个字节 |
+-----------------------+--------+
| U+010000 - U+10FFFF   | 4个字节 |
+-----------------------+--------+

作为近亲,顺便也了解下UTF-16和UTF-32编码。UTF-16也是可变长编码方式,用它来编码Unicode字符集时的可编码区间和所需字节数如下:

1
2
3
4
5
+-----------------------+--------+
| U+000000 - U+00FFFF   | 1个字节 |
+-----------------------+--------+
| U+010000 - U+10FFFF   | 2个字节 |
+-----------------------+--------+

因此,单从存储空间的角度上看,如果存储的内容大部分为英、意、法等Latin-1字符,那么选择UTF-8编码较为合适,如果存储的内容大部分为CJK(东亚文字,Chinese, Japanese, Korean),那么选择UTF-16编码更合适。

至于UTF-32,它是一个定长的编码方式,对所有的Unicode字符采用统一的4字节长度编码方式,它的好处是计算机处理方便,坏处显而易见,太费存储空间,实际中较少使用。

2. MySQL对utf8的处理方式
MySQL在建表时可以指定字符编码为utf8,但是奇葩的是,MySQL的CHARSET=utf8只能表示Unicode字符集中一部分,通过查看属性可知它最多只能编码所有可以3个字节表示的Unicode字符:

1
2
3
4
5
6
mysql> show character set where charset = 'utf8';
+----------+-----------------------------+---------------------+--------+  
| Charset  | Description                 | Default collation   | Maxlen |  
+----------+-----------------------------+---------------------+--------+  
| utf8     | UTF-8 Unicode               | utf8_general_ci     |      3 |  
+----------+-----------------------------+---------------------+--------+

也就是说,Unicode码位U+010000-U+10FFFF之间的字符在MySQL中是无法用CHARSET=utf8来表示的,CHARSET=utf8只能表示约5.88% 的Unicode字符。注:(U+00FFFF + 1) / (U+10FFFF + 1) = 5.88%。

MySQL 5.5.3以后加了一种utf8mb4的编码类型,它的Maxlen为4,支持4字节长度的UTF-8编码,如下:

1
2
3
4
5
6
mysql> show character set where charset = 'utf8mb4';
+----------+-----------------------------+---------------------+--------+  
| Charset  | Description                 | Default collation   | Maxlen |  
+----------+-----------------------------+---------------------+--------+  
| utf8mb4  | UTF-8 Unicode               | utf8mb4_general_ci  |      4 |  
+----------+-----------------------------+---------------------+--------+

根据最新的Unicode 6.1版本,Emoji表情所在码位为U+1F300 - U+1F64F,因此需要用4个字节编码,所以,出现了本文开头出现的SQLException异常。

Emoji表情无法插入只是MySQL utf8字符集无法处理U+010000以上Unicode字符的一个特例,有了utf8mb4之后,为了今后少踩些这方面的坑,应尽量采用CHARSET=utf8mb4来指定数据表字符集。

--EOF--

利用iptables实现基于端口的网络流量统计

如何统计某个应用的网络流量(包括网络流入量和网络流出量)问题,可以转换成如何基于端口号进行网络流量统计的问题。大部分网络应用程序都是传输层及以上的协议,因此基于端口号(tcp, udp)统计网络流量基本能覆盖到此类需求。

利用iptables实现基于端口的流量统计是一种比较简单可行的方案。它可以对流经每一条规则的包数量和流量进行计数。例如要对常规的Web服务器进行流量统计,可以设置如下规则:

1
2
root@debian:~# iptables -A INPUT -p tcp --dport 80
root@debian:~# iptables -A OUTPUT -p tcp --sport 80

第一条规则表示,在INPUT链上添加一条规则,该条规则对所有来自外部网络的、与本机80端口通信的请求有效,即网络流入量,第二条规则则相反,它用于统计从本机80端口发出的网络流量,即网络流出量。因为我们的目的是统计流量,故此处可以省略ACCEPT或DROP之类的动作。

查看流量计数时,只要加上-nvx参数即可:

1
2
3
4
5
6
7
8
9
10
11
root@debian:~# iptables -nvx -L
Chain INPUT (policy ACCEPT 320 packets, 33045 bytes)
pkts     bytes target     prot opt in     out     source  destination         
0        785      tcp  --  *      *   0.0.0.0/0   0.0.0.0/0 tcp dpt:80
 
Chain FORWARD (policy ACCEPT 0 packets, 0 bytes)
pkts     bytes target     prot opt in     out     source  destination         
 
Chain OUTPUT (policy ACCEPT 311 packets, 33711 bytes)
pkts     bytes target     prot opt in     out     source  destination         
0        1252      tcp  --  *      *   0.0.0.0/0   0.0.0.0/0 tcp spt:80

iptables的-vx参数表示详细信息,可以以字节为单位显示包数量和网络流量,-n参数表示以数字方式表示ip地址和端口号等,不加的话iptables会默认将ip反解为主机名,例如127.0.0.1用localhost表示,端口号显示为协议号,例如80显示成http, 5672显示成amqp。

利用iptables规则统计网络流量可以保证应用在关闭重启后统计数据不丢失,但是对于主机重启的情况,它就无能为力了,因为默认情况下主机重启iptables规则会清空,即使使用开机任务等方式重建iptables规则,流量计数器也会清零。解决此类问题一个变通的方法是定期将网络流量值保存到文件,并清0重新计数,定期更新的时间越短,机器重启造成的流量计数丢失问题影响越小(特别涉及计费相关的业务)。

创建一个crontab任务,定期将流量统计追加至文件,例如:

1
root@debian:~# iptables -vxn -L >> file

也可以选择直接将流量值追加到文件:

1
root@debian:~# iptables -vxn -L | awk '{print $2}' >> file

当写入文件成功后,记得将流量值清0:

1
root@debian:~# iptables -Z

-Z参数支持指定链名称和规则索引号,例如下列命令表示清空INPUT链的第一条规则的流量计数器。

1
root@debian:~# iptables -Z INPUT 1

Reference:
[1] iptables man page: http://ipset.netfilter.org/iptables.man.html.

--EOF--