Space of liukai93


  • 首页

  • 归档

链路劫持小记

发表于 2017-02-11

最近在处理了一些HTTP劫持的案例和拜读业内不少大牛的文章之后,觉得有必要把最近的一些关于劫持案例的分析和思考记录下来,以留作日后备忘。
简单谈一下目前遇到的一个案例:
​ 劫持者应该是利用在运营商内部的便利条件,在网关路由器上添加嗅探程序,嗅探明文HTTP请求数据包,拿到要劫持的数据包之后,马上给请求者返回HTTP response(302 到其他 url),并且立即关闭当前HTTP 请求。劫持者 302 的 url 是原网站的一个计费请求,类似淘宝推广链接。但是比较让人郁闷的是,劫持者返回的数据包是两个 TCP 数据包,偶尔会出现 TCP 报乱序(劫持技术不过关),导致客户端无法识别,从而导致页面白屏,严重影响用户体验!
下面介绍一下我是如何从数据包分析和定位劫持路由的:
案例背景:

山西移动部分地区,访问国内某中文导航网站出现白屏现象。

页面请求后得到奇怪数据返回:

​ 请求中 Connection 字段为 keep alive 且请求协议是 1.0 而返回的header 关闭了请求,返回一段奇怪的js,跳转到另外一个 url。
接下来观察 TCP flow:
​ 这次TCP 连接上有 2 个奇怪的现象,都可以证明这是一次处于链路中的劫持,之后如果遇到类似的情况也可以从这两个方面入手来处理:

  1. server 给 client 的 TCP 报的 TTL 不一致,且抖动很大。
  2. server 给 client 的 IP identification ,出现不符合 RFC 标准的情况
    TTL 不一致的情况:

    ​ 客户端接受的数据包TTL是 51 ,后面来自真实 server 的TTL 是 47,还有 1022 和 1024(本来应该在前面) 都是两个来自 劫持者的数据包,但是 fin 包在前,提前关闭连接,导致HTTP应用层拿不到正确的数据,导致浏览器白屏。

    这次 TCP 连接上的其他数据包,可以看到有部分数据包,被抛弃,而且被抛弃的数据包的 TTL 和 握手包的TTL 相等(一般握手包不会被劫持,说明被抛弃的包是来自 真实的服务器的)是 47 。
    2 . IP identification 异常现象:
    RFC定义:

    所以对于给定地址和协议的 ip 包来说,它的identification应该是公差为 1 的单调递增数列:
    但是在这次连接中,劫持者的 identification 等于被劫持的 identification:
    劫持者:

    被劫持者:

    仔细看,可以发现 993 和 1022 这两个包的identification 是一样的,多次抓包也是这样,所以这里可以判断,链路上肯定出了问题。
    从这以上两个特征,基本上可以得出结论:
    劫持者提前给浏览器返回了响应,且关闭了 HTTP 连接。导致正确的 数据包没有被接受,使得浏览器发生了异常跳转。而用户页面出现白屏的情况是劫持失败,劫持者的数据包乱序(程序错误),导致应用层无法获得争取 HTTP 响应。
    劫持过程应该类似于:

    结论已经获得,但是问题的解决就是要定位到相应的劫持路由,然后向有关部门反馈:
    定位的方法我推荐几种:
  1. 如果出现一定数量的用户反馈,可以多联系几位用户(不同网路环境下(wifi,手机热点),能复现劫持),抓包,然后获取 trace 截图,如果他们出现某几跳路由的重复,就可以缩小定位范围,或者直接定位路由IP。

  2. 根据劫持包的TTL反推,用scapy来构造自定义的,可以复现劫持的HTTP请求包,然后以TTL从1开始递增发包,出现劫持响应时,可以判断劫持者的位置。

    参考文章:
    http://www.freebuf.com/vuls/62561.html
    http://security.tencent.com/index.php/blog/msg/10
    谢谢这两篇文章的作者,指定迷津。

javascript-一些学习笔记

发表于 2016-04-11

JavaScript 事件

事件类型

按照事件添加的区别,分为DOM 0 级事件和DOM 2 级事件。

DOM 0 级事件类似于下面的这样添加:

1
2
3
4
<a onclick="alert(event.type)" >link</a>
<script type="text/javascript">
document.getElementByTag('a')[0].onclick = alert(event.type);
</script>

这样添加的优点是,几乎所有的浏览器都是兼容的。而且简单。缺点是一个元素的事件只能绑定一个事件处理程序。而且这个事件处理程序添加方式不能支持事件捕获,只能使用事件冒泡机制。

DOM 2 级事件添加方式:

1
2
3
4
5
6
e.addEventListener('click',function(){
alert('hello world');
},false);
e.addEventListener('click',function(){
alert('hello javascript');
},false)

这样做的优点是一个元素的一个事件可以绑定多个事件处理程序,事件执行的顺序与添加的顺许相关,例如上面的示例,依次输出 ‘hello world’ , ‘hello javascript’ . 方法的第三个参数表示处理程序是在冒泡阶段执行还是在捕获阶段执行。

但是这样的添加方式可能会在不同版本的浏览器存在兼容性问题,例如在IE中DOM 2 级的事件处理程序添加是这样来写的:

1
2
3
4
5
6
e.attachEvent('onclick',function(){
alert('hello world');
});
e.attachEvent('onclick',function(){
alert('hello javascript');
});

区别主要是第一个参数需要添加 ‘on’ ,其次还有主要是添加的事件处理程序的执行顺序不同,这次首先输出 ‘hello javascript ‘ , 然后再输出 ‘hello’,IE和opera 只支持事件冒泡。所以没有第三个参数。

各大浏览器之间的事件绑定程序添加方式不同,所以需要添加一个兼容的事件添加程序来保证兼容性。可以像这样一个简单的对象来处理事件添加。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
var Event = {
add: function(type, e, foo) {
if (e.addEventListener) {
e.addEventListener(type, foo, false);
} else if (e.attachEvent) {
e.attachEvent('on' + type, foo, false);
} else {
e['on' + type] = foo;
}

},
remove: function(type, e, foo) {
if (e.removeEventListener) {
e.removeEventListener(type, foo, false);
} else if (e.detachEvent) {
e.detachEvent('on' + type, foo);
} else {
e['on' + type] = null;
}

}

}

事件出发后,浏览器的处理方式也是不一样的,主要有冒泡和捕获方式。例如:IE 只支持冒泡。NetScape 只支持捕获,为了保持浏览器之间的兼容性,W3C制定了首先进入捕获阶段,然后进入冒泡阶段的事件处理机制。
img
由于浏览器之间的标准不统一,所以在事件绑定的机制上,一般建议使用冒泡的机制(IE不支持捕获)。

支持冒泡机制的浏览器可以使用 事件委托 :

事件委托是一种编程技巧,在父级元素监听事件,就可以处理子元素的事件活动,例如一个ul 列表 有 10 个 li 元素,如果给 li 元素都绑定 ‘click’ 事件处理程序的话,就会很麻烦,但是我们只要在 ul元素上绑定 ‘click’ 事件处理程序,使用冒泡方式,就能处理子元素的事件活动。

使用事件代理主要有两个优势:

  1. 减少事件绑定,提升性能。之前你需要绑定一堆子节点,而现在你只需要绑定一个父节点即可。减少了绑定事件监听函数的数量。
  2. 动态变化的 DOM 结构,仍然可以监听。当一个 DOM 动态创建之后,不会带有任何事件监听,除非你重新执行事件监听函数,而使用事件委托监听无须担忧这个问题。

javascript-闭包

今天在看 javascript 秘密花园 的时候,看到了之前 腾讯笔试的一个笔试的知识点,当时做题的时候没有想清楚,今天看到闭包,总算搞清楚了。

题目是:

1
2
3
4
5
for(var i = 0; i < 10; i++) {
setTimeout(function() {
console.log(i);
}, 1000);
}

问输出是什么?

当时认为是按照顺许输出 0~9,但是答案是 输出 10 次 10 。

输出 10 次 10 的原因 是, setTimeout 函数里的 匿名函数保持的是对 i 的引用,由于它是匿名调用,所以这个 i 相当与是 window.i (浏览器环境中),他是全局对象中的 i 的一个引用。由于 setTimeout 执行 10 次之后,相当与将一个匿名函数加入到任务队列中,在执行完循环之后,调用任务队列中的匿名函数。所以在循环之后,i的值变成了 10 ,所以任务队列中的匿名函数也就打印 10 次 10 。 要依次输出 0~9,就要避免对 i 的引用,可以使用以下代码:

1
2
3
4
5
6
for(var i = 0; i < 10; i++) {
setTimeout((function() {

console.log(i);
})(i), 1000);
}

为什么这样写就能输出 0 ~ 9 呢? 这里在每一次的循环中创建了一个匿名函数

pdo防止注入的原理

发表于 2016-01-25

当提到防止SQL注入的办法时,脑海中总是会想到使用PDO绑定参数的办法或者使用mysql_real_eascape_string()来处理(虽然古老的 mysql_XXX 这类的函数已经不建议使用)。但是PDO是如何防止注入的呢?

在手册中,有这样一段:

Prepared statements and stored procedures

Many of the more mature databases support the concept of prepared statements. What are they? They can be thought of as a kind of compiled template for the SQL that an application wants to run, that can be customized using variable parameters. Prepared statements offer two major benefits:

  • The query only needs to be parsed (or prepared) once, but can be executed multiple times with the same or different parameters. When the query is prepared, the database will analyze, compile and optimize it’s plan for executing the query. For complex queries this process can take up enough time that it will noticeably slow down an application if there is a need to repeat the same query many times with different parameters. By using a prepared statement the application avoids repeating the analyze/compile/optimize cycle. This means that prepared statements use fewer resources and thus run faster.
  • The parameters to prepared statements don’t need to be quoted; the driver automatically handles this. If an application exclusively uses prepared statements, the developer can be sure that no SQL injection will occur (however, if other portions of the query are being built up with unescaped input, SQL injection is still possible).

Prepared statements are so useful that they are the only feature that PDO will emulate for drivers that don’t support them. This ensures that an application will be able to use the same data access paradigm regardless of the capabilities of the database.

大概的翻译是:
很多更成熟的数据库都支持预处理语句的概念。这些是什么?它可以被认为是作为一种通过编译SQL语句模板来运行sql语句的机制。预处理语句可以带来两大好处:

    • 查询只需要被解析(或编译)一次,但可以执行多次通过相同或不同的参数。当查询处理好后,数据库将分析,编译和优化它的计划来执行查询。对于复杂的查询这个过程可能需要足够的时间,这将显著地使得应用程序变慢,如果有必要,可以多次使用不同的参数 重复相同的查询。通过使用处理好的语句的应用程序避免重复 【分析/编译/优化】 周期。这意味着,预处理语句使用更少的资源,而且运行得更快。
    • 绑定的参数不需要使用引号;该驱动程序会自动处理。如果应用程序使用预处理语句,开发人员可以确保不会发生SQL注入(但是,如果查询的其他部分使用了未转义的输入,SQL注入仍然是可能的)。*

预处理语句非常有用,PDO可以使用一种本地模拟的办法来为没有预处理功能的数据库系统提供这个功能。这保证了一个应用可以使用统一的访问方式来访问数据库。

这里讲了使用PDO可以带来两个很好的效果,预编译带来查询速度的提升,变量的绑定可以预防 sql injection,其实PDO的预防sql注入的机制也是类似于使用 mysql_real_escape_string 进行转义,PDO 有两种转义的机制,第一种是本地转义,这种转义的方式是使用单字节字符集(PHP < 5.3.6)来转义的(单字节与多字节),来对输入进行转义,但是这种转义方式有一些隐患。隐患主要是:在PHP版本小于5.3.6的时候,本地转义只能转换单字节的字符集,大于 5.3.6 的版本会根据 PDO 连接中指定的 charset 来转义。PHP官方手册这里有说明:

Warning

The method in the below example can only be used with character sets that share the same lower 7 bit representation as ASCII, such as ISO-8859-1 and UTF-8. Users using character sets that have different representations (such as UTF-16 or Big5) mustuse the charset option provided in PHP 5.3.6 and later versions.

所以就是说,不同的版本的PDO 在本地转义的行为上是有区别的。

第二种方式是PDO,首先将 sql 语句模板发送给Mysql Server,随后将绑定的字符变量再发送给Mysql server,这里的转义是在Mysql Server做的,它是根据你在连接PDO的时候,在charset里指定的编码格式来转换的。这样的转义方式更健全,同时还可以在又多次重复查询的业务场景下,通过复用模板,来提高程序的性能。如果要设置Mysql Server 来转义的话,就要首先执行:

1
$pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);

下面是通过 wireshark 抓到的数据包,来具体显示PDO 查询的过程:
img
绑定的变量:
img
如果不执行 $pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false); PDO 只是会将插入的参数使用本地转义之后和SQL模板拼装起来,然后一起发送给Mysql Server。这实际上与使用mysql_real_escape_string()过滤,然后拼装这种做法并没有什么不同。

要对数据库的安全做出更加全面的考量,以下两种方式任选其一:

A. 通过添加(php 5.3.6以前版本):$pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
B. 升级到php 5.3.6 (不用设置PDO::ATTR_EMULATE_PREPARES也可以)
为了程序移植性和统一安全性,建议使用 $pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false) 方法。

websocket解析和 php 实现

发表于 2016-01-11

Websocket 协议解析

​ WebSocket protocol 是HTML5一种新的协议。它是实现了浏览器与服务器全双工通信(full-duplex)。现很多网站为了实现即时通讯,所用的技术都是轮询(polling)。轮询是在特定的的时间间隔(如每1秒),由浏览器对服务器发出HTTP request,然后由服务器返回最新的数据给客服端的浏览器。这种传统的HTTP request 的模式带来很明显的缺点 – 浏览器需要不断的向服务器发出请求,然而HTTP request 的header是非常长的,里面包含的数据可能只是一个很小的值,这样会占用很多的带宽。

​ 在 WebSocket API,浏览器和服务器只需要要做一个握手的动作,然后,浏览器和服务器之间就形成了一条快速通道。两者之间就直接可以数据互相传送,改变了原有的B/S模式。

server

Websocket 数据:

websocket

浏览器端的websocket 请求一般是

1
2
3
4
5
6
7
8
9
10
11
// javacsript
var ws = new WebSocket("ws://127.0.0.1:4000");
ws.onopen = function(){
console.log("succeed");
};
ws.onerror = function(){
console.log(“error”);
};
ws.onmessage = function(e){
console.log(e);
}

当 new 一个 websocket 对象之后,就会向服务器发送一个 get 请求

request

这个请求是对摸个服务器的端口发送的,一般的话,会预先在服务器将一个socket 绑定到一个端口上,客户端和服务器端在这个预定的端口上通信(我这里绑定的就是 4000 端口,默认情况下,websocke 使用 80 端口)。

然后,在服务器端的socket监听到这个packet 之后就生成一个新的 socket,将发送过来的数据中的 Sec-WebSocket-Key 解析出来,然后按照把“Sec-WebSocket-Ke”加上一个魔幻字符串“258EAFA5-E914-47DA-95CA-C5AB0DC85B11”。使用SHA-1加密,之后进行BASE-64编码,将结果做为“Sec-WebSocket-Accept”头的值,返回给客户端。

客户端收到这个之后,就会将 通信协议 upgrade 到 websocket 协议。

response

然后就会在这个持久的通道下进行通信,包括浏览器的询问,服务器的push,双方是在一个全双工的状态下相互通信。 切换后的websocket 协议中的 数据传输帧的格式(此时不再使用html协议) 官方文档给出的说明:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
0                   1                   2                   3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-------+-+-------------+-------------------------------+
|F|R|R|R| opcode|M| Payload len | Extended payload length |
|I|S|S|S| (4) |A| (7) | (16/64) |
|N|V|V|V| |S| | (if payload len==126/127) |
| |1|2|3| |K| | |
+-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +
| Extended payload length continued, if payload len == 127 |
+ - - - - - - - - - - - - - - - +-------------------------------+
| |Masking-key, if MASK set to 1 |
+-------------------------------+-------------------------------+
| Masking-key (continued) | Payload Data |
+-------------------------------- - - - - - - - - - - - - - - - +
: Payload Data continued ... :
+ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
| Payload Data continued ... |
+---------------------------------------------------------------+

直接看这个,谁都会有点头大: 我花了一幅图来简单的解释这个 frame 的结构。

frame

各字段的解释:

FIN 1bit 表示信息的最后一帧,flag,也就是标记符

RSV 1-3 1bit each 以后备用的 默认都为 0

Opcode 4bit 帧类型,

Mask 1bit 掩码,是否加密数据,默认必须置为1

Payload len 7bit 数据的长度,当这个7 bit的数据 == 126 时,后面的2 个字节也是表示数 据长度,当它 == 127 时,后面的 8 个字节表示数据长度Masking-key 1 or 4 bit 掩码Payload data playload len bytes 数据

所以我们这里的代码,通过判断 Playload len的值,来用 substr 截取 Masking-key 和 PlayloadData。

根据掩码解析数据的方法是:

1
2
3
4
5
6

for( i = 0; i < data.length ; i++){

orginalData += data[i] ^ maskingKey[i mod 4];

}

在PHP中,当我们收到数据之后,按照这里的格式截取数据,并将其按照这里的方法解析后就得到了浏览器发送过来的数据。 当我们想把数据发送给浏览器时,也要按照这个格式组装frame。 这里是我的方法:

1
2
3
4
5
6
7
8
9
10
11
function frame($s){
$a = str_split($s, 125);
if (count($a) == 1){
return "\x81" . chr(strlen($a[0])) . $a[0];
}
$ns = "";
foreach ($a as $o){
$ns .= "\x81" . chr(strlen($o)) . $o;
}
return $ns;
}

强行将要发送的数据分割成 125 Byte / frame,这样 playload len 只需要 7 bits。也就是直接将数据的长度的ascall码拼接上去,然后后面跟上要发送的数据。 每一个 frame 前面加的 ‘\x81’ 用二进制就是: 1000 0001 1000 :

1 是 FIN

000 是三个备用的bit

0001 : 指的是 opcode 官方的解释:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
  |Opcode  | Meaning                             | Reference |
-+--------+-------------------------------------+-----------|
| 0 | Continuation Frame | RFC 6455 |
-+--------+-------------------------------------+-----------|
| 1 | Text Frame | RFC 6455 |
-+--------+-------------------------------------+-----------|
| 2 | Binary Frame | RFC 6455 |
-+--------+-------------------------------------+-----------|
| 8 | Connection Close Frame | RFC 6455 |
-+--------+-------------------------------------+-----------|
| 9 | Ping Frame | RFC 6455 |
-+--------+-------------------------------------+-----------|
| 10 | Pong Frame | RFC 6455 |
-+--------+-------------------------------------+-----------|

可以设置上图中 opcode 的值,来告诉浏览器这个frame的数据属性。

php 实现简单的 websocket 服务端

自十月底,html5 宣布定稿之后,新一轮的关于html的讨论便开始了,现在这里,我也为大家介绍一种html5标准中提到的新技术 websocket,以及他的 php 实现范例。

WebSocket是HTML5开始提供的一种浏览器与服务器间进行全双工通讯的网络技术。WebSocket通信协议于2011年被IETF定为标准RFC 6455,WebSocketAPI被W3C定为标准。

在WebSocket API中,浏览器和服务器只需要做一个握手的动作,然后,浏览器和服务器之间就形成了一条快速通道。两者之间就直接可以数据互相传送。

传统的web程序,都遵循这样的执行方式,浏览器发送一个请求,然后服务器端接收这个请求,处理完成浏览器的请求之后,再将处理完成的结果返回给浏览器,然后浏览器处理返回的html,将其呈现给用户。如下图所示:

server

即使后来出现了ajax这样的新的技术,它实际也是使用javascript的api来向服务器发送请求,然后等待相应的数据。也就是说,在浏览器和服务器的交互中,我们每想得到一个得到新的数据(更新页面,获得服务器端的最新状态)就必须要发起一个http请求,建立一条TCP/IP链接,当这次请求的数据被浏览器接收到之后,就断开这条TCP/IP连接。

新的websocket技术,在浏览器端发起一条请求之后,服务器端与浏览器端的请求进行握手应答之后,就建立起一条长久的,双工的长连接,基于这条连接,我们不必做一些轮询就能随时获得服务器端的状态,服务器也不必等待浏览器端的请求才能向用户推送数据,可以在这条连接上随时向以建立websocket连接的用户 push 数据。

这里是 websocket 协议的 RFC 文档。

我这里的实现是基于 php sockets的实现,调用了来自 php.net 的 socket-api php socket .

代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
<?php
class WsServer{
public $socket;
public $socketArray;
public $activatedSocketArray;
public function __construct($address,$port){
$this->socket = $this->init($address,$port);
if($this->socket == null)
exit(1);

}
private function init($address,$port){
$wssocket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
socket_set_option($wssocket, SOL_SOCKET, SO_REUSEADDR, 1);
socket_bind($wssocket,$address,$port);
if(socket_listen($wssocket)){
$this->p("Socket init success");
return $wssocket;
}else{
$this->p("Socket init failure");
exit();
}

}

public function run(){
$this->socketArray[] = $this->socket;
$write = NULL;
$except = NULL;
while (true){
$this->activatedSocketArray = $this->socketArray;
socket_select($this->activatedSocketArray, $write, $except, null);

foreach($this->activatedSocketArray as $s){
if($s == $this->socket){
$client = socket_accept($this->socket);
socket_recv($client, $buffer, 2048, 0);

// response a handshake response
if(socket_write($client, $this->handshakeResponse($buffer))!== false){
$this->p('add a socket into the queue');
array_push($this->socketArray, $client);
}else{
$this->p('error on handshake');
$this->errorLog(socket_last_error());
}
}else{
socket_recv($s, $buffer, 2048, 0);

$frame = $this->parseFrame($buffer);
var_dump($frame);
$str = $this->decode($buffer);
$str = $this->frame($str);
socket_write($s,$str,strlen($str));
}
}

}
}
/**
* parse the frame
* */
private function parseFrame($d){
$firstByte = ord(substr($d,0,1));
$result =array();
$result['FIN'] = $this->getBit($firstByte,1);
$result['opcode'] = $firstByte & 15;
$result['length'] = ord(substr($d,1,1)) & 127;
return $result;
}
private function getBit($m,$n){
return ($n >> ($m-1)) & 1;
}
/*
* build the data frame sent to client by server socket.
*
* **/
function frame($s){
$a = str_split($s, 125);
if (count($a) == 1){
return "\x81" . chr(strlen($a[0])) . $a[0];
}
$ns = "";
foreach ($a as $o){
$ns .= "\x81" . chr(strlen($o)) . $o;
}
return $ns;
}
/*
* decode the client socket input
*
* **/
function decode($buffer) {
$len = $masks = $data = $decoded = null;
$len = ord($buffer[1]) & 127;

if ($len === 126) {
$masks = substr($buffer, 4, 4);
$data = substr($buffer, 8);
}
else if ($len === 127) {
$masks = substr($buffer, 10, 4);
$data = substr($buffer, 14);
}
else {
$masks = substr($buffer, 2, 4);
$data = substr($buffer, 6);
}

for ($index = 0; $index < strlen($data); $index++) {
$decoded .= $data[$index] ^ $masks[$index % 4];
}
return $decoded;
}
/*
* params @requestHeaders : read from request socket
* return an array of request
*
* */
function parseHeaders($requsetHeaders){
$resule =array();
if (preg_match("/GET (.*) HTTP/" ,$requsetHeaders,$match)) { $resule['reuqest'] = $match[1]; }
if (preg_match("/Host: (.*)\r\n/" ,$requsetHeaders,$match)) { $result['host'] = $match[1]; }
if (preg_match("/Origin: (.*)\r\n/" ,$requsetHeaders,$match)) { $result['origin'] = $match[1]; }
if (preg_match("/Sec-WebSocket-Key: (.*)\r\n/",$requsetHeaders,$match)) { $result['key'] = $match[1]; }
return $result;

}
/*
* protocol version : 13
* generting the key of handshaking
* return encrypted key
* */
function getKey($requestKey){
return base64_encode(sha1($requestKey . '258EAFA5-E914-47DA-95CA-C5AB0DC85B11', true));

}
/**params @parseRequest : request written in socket
* return handshakResponse witten in response socket
* */
function handshakeResponse($request){
$parsedRequest = $this->parseHeaders($request);
$encryptedKey = $this->getKey($parsedRequest['key']);
$response = "HTTP/1.1 101 Switching Protocol\r\n" .
"Upgrade: websocket\r\n" .
"Connection: Upgrade\r\n" .
"Sec-WebSocket-Accept: " .$encryptedKey. "\r\n\r\n";
return $response;

}
/*
* report last_error in socket
*
* */
function errorLog($ms){
echo 'Error:\n'.$ms;
}
/*
* print the log
*
* */
private function p($e){
echo $e."\n";
}
function test(){
$read[] = $this->socket;
$write = null;
$except = null;
socket_select($read, $write, $except,null);
var_dump($read);
}
}
$w = new WsServer('localhost',4000);
$w->run();
?>

这个类主要实现了对websocket的握手回应,将每一个连接成功的websocket加入到一个数组中之后,就能够在服务器端对多个websocket 客户端进行处理。

对websocket的握手请求,在接收到的报文中 会得到一个 Sec-WebSocket-Key 字段,要把“Sec-WebSocket-Key”加上一个魔幻字符串“258EAFA5-E914-47DA-95CA-C5AB0DC85B11”。使用SHA-1加密,之后进行BASE-64编码,将结果做为“Sec-WebSocket-Accept”头的值,返回给客户端。这样就完成了与客户端之间的握手。

之后,就能在服务器端监听客户端发来的请求了,同时也可以操作在服务端的socket句柄,来向浏览器推送消息。

php-字符串搜索函数调研

发表于 2016-01-06

将一个字符串插入到另外一个字符串中间,是php中常常出现的一个操作,具体的实现方案也有替换和直接插入两种,涉及到的相关函数也不少,下面就是我在一个实际工作当中处理字符串插入的效率的调研。

场景:

在一个 1MB左右的文本文件(content)中的 “hello” 前插入一个10个字节大小的字符串(string)。

采取的方案:(机器性能不同,用相对的比较方式对比性能)

编号 方式 性能(此处是相对值,仅表示各函数的相对速度大小) 备注
A 【替换】str_replace(‘#hello#’,$string,$content) 3 备注
B 【替换】strtr($content,array(”=>$script)); 42 备注
C 【正则替换】preg_replace(‘##hello##’,$script,$content); 4.4 备注
D 【Substr_replace +strops 插入】substr_replace($content,$script,strpos($content,”),0) 1 备注

A 方案 str_replace() 函数

函数签名:

mixed str_replace ( mixed $search , mixed $replace , mixed $subject [, int &$count ] )

参数
如果 search 和 replace 为数组,那么 str_replace() 将对 subject 做二者的映射替换。如果 replace 的值的个数少于 search 的个数,多余的替换将使用空字符串来进行。如果 search 是一个数组而 replace 是一个字符串,那么 search 中每个元素的替换将始终使用这个字符串。该转换不会改变大小写。

如果 search 和 replace 都是数组,它们的值将会被依次处理。

search
查找的目标值,也就是 needle。一个数组可以指定多个目标。
replace
search 的替换值。一个数组可以被用来指定多重替换。
subject
执行替换的数组或者字符串。也就是 haystack。

如果 subject 是一个数组,替换操作将遍历整个 subject,返回值也将是一个数组。
count
如果被指定,它的值将被设置为替换发生的次数。
返回值 ¶
该函数返回替换后的数组或者字符串。

输入的类型可以是字符串或者数组,先对字符串进行分析

情况1.输入的 search 和 replace 均为字符串

  1. 替换的过程首先要查找 search ,在一个字符串中查找另外一个字符串,就变成了经典的 ‘find needls in haystack’ 问题了。
    在源码中,调用情况是 str_replace->php_str_to_str_ex[字符串替换]->zend_memstr php_str_to_str_ex在 ext/standerd/string.c 的php_str_to_str_ex 顾名思义,他就是主要负责处理字符串替换,而 zend_memstr 主要是查找 needles

zend_memstr 源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
static inline char *
zend_memnstr(char *haystack, char *needle, int needle_len, char *end)
{
char *p = haystack;
char ne = needle[needle_len-1];

if (needle_len == 1) {
return (char *)memchr(p, *needle, (end-p));
}

if (needle_len > end-haystack) {
return NULL;
}

end -= needle_len;

while (p <= end) {
// 如果在 p 指向的前(end - p +1)个空间查找到 needle 的首字符,便比较needle的最后一个字符和 p 当前指向的位置 + needle_len 个字符;
// 这个算法在 needle的第一个字符不会密集出现在 haystack 中时,会比较高效,memchr 会带来较大的 移动步长,相比于 KMP,Horspol 算法,简单,易懂
if ((p = (char *)memchr(p, *needle, (end-p+1))) && ne == p[needle_len-1]) {
if (!memcmp(needle, p, needle_len-1)) {
return p;
}
}

if (p == NULL) {
return NULL;
}

p++;
}

return NULL;
}

在使用 zend_memstr 查找到 search 的内存地址之后,php_str_to_str_ex 就会使用memcpy ,用 to 来替换 from。

【值得注意的是,在php_str_to_str_ex 中,会一直查找 subject 中的 search ,知道把他们全部替换为 replace】也就是说,这个过程在发生一次之后不会退出,直到循环,将所有 content 里的 to 替换成 from 才会退出。

当 str_replace 输入为数组时,使用的算法和 输入为 string 是一样的,只不过相当于循环的处理输入的数组,将他们单个取出,像对待字符串一样去处理。

方案B strtr() 函数

说明 ¶

string strtr ( string $str , string $from , string $to )

string strtr ( string $str , array $replace_pairs )

该函数返回 str 的一个副本,并将在 from 中指定的字符转换为 to 中相应的字符。 比如, $from[$n]中每次的出现都会被替换为 $to[$n],其中 $n 是两个参数都有效的位移(offset)。

如果 from 与 to 长度不相等,那么多余的字符部分将被忽略。 str 的长度将会和返回的值一样。

参数 ¶

  • str

    待转换的字符串

  • from

    字符串中与将要被转换的目的字符 to 相对应的源字符

  • to

    字符串中与将要被转换的字符 from 相对应的目的字符

  • replace_pairs

    参数 replace_pairs 可以用来取代 to 和 from 参数,因为它是以 array(‘from’ => ‘to’, …) 格式出现的数组。

返回值 ¶

返回转换后的字符串。

如果 replace_pairs 中包含一个空字符串(“”)键,那么将返回 FALSE。 If the str is not a scalar then it is not typecasted into a string, instead a warning is raised and NULLis returned.

strtr 有两种输入情况,源码中也是将它分成了两个子过程来处理:

输入为字符串:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
PHPAPI char *php_strtr(char *str, int len, char *str_from, char *str_to, int trlen)
{
int i;
unsigned char xlat[256];

if ((trlen < 1) || (len < 1)) {
return str;
}
// 一个 256 大小的数组,代表 ascll 码
for (i = 0; i < 256; xlat[i] = i, i++);
// 用 to 来替换指定的from字符的 ascll
for (i = 0; i < trlen; i++) {
xlat[(unsigned char) str_from[i]] = str_to[i];
}
// 重新赋值
for (i = 0; i < len; i++) {
str[i] = xlat[(unsigned char) str[i]];
}

return str;
}

输入为数组:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
/* {{{ php_strtr_array
*/
static void php_strtr_array(zval *return_value, char *str, int slen, HashTable *hash)
{
zval **entry;
char *string_key;
uint string_key_len;
zval **trans;
zval ctmp;
ulong num_key;
int minlen = 128*1024;
int maxlen = 0, pos, len, found;
char *key;
HashPosition hpos;
smart_str result = {0};
HashTable tmp_hash;

zend_hash_init(&tmp_hash, zend_hash_num_elements(hash), NULL, NULL, 0);
zend_hash_internal_pointer_reset_ex(hash, &hpos);
while (zend_hash_get_current_data_ex(hash, (void **)&entry, &hpos) == SUCCESS) {
switch (zend_hash_get_current_key_ex(hash, &string_key, &string_key_len, &num_key, 0, &hpos)) {
case HASH_KEY_IS_STRING:
len = string_key_len-1;
if (len < 1) {
zend_hash_destroy(&tmp_hash);
RETURN_FALSE;
}
zend_hash_add(&tmp_hash, string_key, string_key_len, entry, sizeof(zval*), NULL);
if (len > maxlen) {
maxlen = len;
}
if (len < minlen) {
minlen = len;
}
break;

case HASH_KEY_IS_LONG:
Z_TYPE(ctmp) = IS_LONG;
Z_LVAL(ctmp) = num_key;

convert_to_string(&ctmp);
len = Z_STRLEN(ctmp);
zend_hash_add(&tmp_hash, Z_STRVAL(ctmp), len+1, entry, sizeof(zval*), NULL);
zval_dtor(&ctmp);

if (len > maxlen) {
maxlen = len;
}
if (len < minlen) {
minlen = len;
}
break;
}
zend_hash_move_forward_ex(hash, &hpos);
}

key = emalloc(maxlen+1);
pos = 0;
// 替换的步骤在这里
while (pos < slen) {
if ((pos + maxlen) > slen) {
maxlen = slen - pos;
}

found = 0;
// key 是每一次需要被替换的字符串
memcpy(key, str+pos, maxlen);

for (len = maxlen; len >= minlen; len--) {
key[len] = 0;
// trans 被赋值为新的字符串
if (zend_hash_find(&tmp_hash, key, len+1, (void**)&trans) == SUCCESS) {
char *tval;
int tlen;
zval tmp;

if (Z_TYPE_PP(trans) != IS_STRING) {
tmp = **trans;
zval_copy_ctor(&tmp);
convert_to_string(&tmp);
tval = Z_STRVAL(tmp);
tlen = Z_STRLEN(tmp);
} else {
tval = Z_STRVAL_PP(trans);
tlen = Z_STRLEN_PP(trans);
}
// 将 trans 附加到一个新的字符串,最终用来被返回
smart_str_appendl(&result, tval, tlen);
pos += len;
found = 1;

if (Z_TYPE_PP(trans) != IS_STRING) {
zval_dtor(&tmp);
}
break;
}
}

if (! found) {
smart_str_appendc(&result, str[pos++]);
}
}

efree(key);
zend_hash_destroy(&tmp_hash);
smart_str_0(&result);
RETVAL_STRINGL(result.c, result.len, 0);
}

从 strtr 的执行过程来看,strtr 也是需要遍历整个 subject 来替换 to -> from,这个也是它在我们的场景中他的效率比较慢的原因。

方案C 正则替换

正则替换涉及到正则解析引擎,这个应该也是需要遍历 subject 来替换 to -> from。

方案D substr_replace +strops 插入

substr_replace 函数签名:

mixed substr_replace ( mixed $string , mixed $replacement , mixed $start [, mixed$length ] )

substr_replace() 在字符串 string 的副本中将由 start 和可选的 length 参数限定的子字符串使用 replacement进行替换。

strpos 函数签名:

说明 ¶

mixed strpos ( string $haystack , mixed $needle [, int $offset = 0 ] )

返回 needle 在 haystack 中首次出现的数字位置。

使用 strpos 定位目标串位置,然后 substr_replace 快速替换。strpos 使用上面提到的 zend_memstr 查找 needle,然后 substr_replace 替换,替换的核心操作就是涉及到内存的分配。这个方法,只是会在目标串中查找一次needle,相比之前的方法,减少了目标串的遍历,由于我们的目标串较大,所以这个方法在相对的比较中,占据了较大的优势.

总结:

在网上搜索字符串替换方案时,会有各种五花八门的解释和分析,但是都缺少原理上的分析和具体场景。在这次调研过程中,我也发现php在一个问题的解决上,有较多的解决办法,看似相同,却又有区别,这个就要对源码较清晰的了解方法的执行过程,才能写出较好性能的程序。

mysql使用中的积累

发表于 2015-11-25

mysql-外键

mysql 外键在我个人的理解中是一个 DB 级别的提供数据一致性保障的工具。

  1. 实体完整性:

实体完整性(Entity integrity)是指关系的主关键字不能重复也不能取”空值”。一个关系对应现实世界中一个实体集。现实世界中的实体是可以相互区分、识别的,也即它们应具有某种惟一性标识。在关系模式中,以主关键字作为惟一性标识,而主关键字中的属性(称为主属性)不能取空值,否则,表明关系模式中存在着不可标识的实体(因空值是”不确定\”的),这与现实世界的实际情况相矛盾,这样的实体就不是一个完整实体。按实体完整性规则要求,主属性不得取空值,如主关键字是多个属性的组合,则所有主属性均不得取空值。

  1. 参照完整性:

参照完整性(Referential Iintigrity)是定义建立关系之间联系的主关键字与外部关键字引用的约束条件。关系数据库中通常都包含多个存在相互联系的关系,关系与关系之间的联系是通过公共属性来实现的。所谓公共属性,它是一个关系R(称为被参照关系或目标关系)的主关键字,同时又是另一关系K(称为参照关系)的外部关键字。如果参照关系K中外部关键字的取值,要么与被参照关系R中某元组主关键字的值相同,要么取空值,那么,在这两个关系间建立关联的主关键字和外部关键字引用,符合参照完整性规则要求。如果参照关系K的外部关键字也是其主关键字,根据实体完整性要求,主关键字不得取空值,因此,参照关系K外部关键字的取值实际上只能取相应被参照关系R中已经存在的主关键字值。

  1. 用户定义完整性:

实体完整性和参照完整性适用于任何关系型数据库系统,它主要是针对关系的主关键字和外部关键字取值必须有效而做出的约束。用户定义完整性(user defined integrity)则是根据应用环境的要求和实际的需要,对某一具体应用所涉及的数据提出约束性条件。这一约束机制一般不应由应用程序提供,而应有由关系模型提供定义并检验,用户定义完整性主要包括字段有效性约束和记录有效性。

外键就是在数据库中参照完整性的具体实现。

外键的定义:一个属性不是他所在关系的主键,但却是另外一个关系的主键。

mysql 中建立外键

在mysql数据库中有innodb engine 支持的不同表中是可以建立外键的,建立外键的表必须满足这几个条件:

  1. 两张表必须都是InnoDB表,并且它们没有临时表。
  2. 建立外键关系的对应列必须具有相似的InnoDB内部数据类型。
  3. 建立外键关系的对应列必须建立了索引。(在可视化工具会自动建立)

现在有两个表:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
CREATE TABLE `order` (
`user_id` int(11) NOT NULL,
`order_id` int(11) NOT NULL,
`name` varchar(100) CHARACTER SET utf8 NOT NULL,
PRIMARY KEY (`order_id`),
KEY `order_user` (`user_id`),
CONSTRAINT `order_user` FOREIGN KEY (`user_id`) REFERENCES `user` (`user_id`) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=latin1;

CREATE TABLE `user` (
`user_id` int(11) NOT NULL,
`name` varchar(50) CHARACTER SET utf8 NOT NULL,
PRIMARY KEY (`user_id`),
KEY `name` (`name`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;

在order 表中有一个外键,此时 user 表是主表,order 是子表。(主表:外键中主键所在的表。子表:外建中非主键的属性所在的表)

建立外键:

1
CONSTRAINT `order_user` FOREIGN KEY (`user_id`) REFERENCES `user` (`user_id`) ON DELETE CASCADE ON UPDATE CASCADE

执行上面的语句后,此时在 user 和 order 建立了一种关系,当你没有把 order 中的 user_id 的属性设置为NULL时 此时 order 中的 每一行数据中的 user_id 的值只能是 user 表的 user_id的
子集 。例如:如果 order.user_id = {1,2,3} 则 order.user_id 在插入数据的时候只能选择 1,2,3 这三个值。当子表试图插入一个在主表中对应的外键不存在的值时,子表会拒绝插入。

新建外键是可以选择这5个选线 DELETE CASCADE ON UPDATE CASCADE 当主表更新或删除存在于外键关系中的主键时,子表应该采取的动作,当然也可以不添加。此时共有5*5+1 种动作

  1. CASCADE: 从父表中删除或更新对应的行,同时自动的删除或更新自表中匹配的行。ON DELETE CANSCADE和ON UPDATE CANSCADE都被InnoDB所支持。
  2. SET NULL: 从父表中删除或更新对应的行,同时将子表中的外键列设为空。注意,这些在外键列没有被设为NOT NULL时才有效。ON DELETE SET NULL和ON UPDATE SET SET NULL都被InnoDB所支持。
  3. NO ACTION: InnoDB拒绝删除或者更新父表。
  4. RESTRICT: 拒绝删除或者更新父表。指定RESTRICT(或者NO ACTION)和忽略ON DELETE或者ON UPDATE选项的效果是一样的。
  5. SET DEFAULT: InnoDB目前不支持。

这里所指的更新或删除子表指的是只对外键关系中的属性起作用。

mysql 外键使用场景

外键约束是满足 数据库参照完整性的

参照完整性(Referential Iintigrity)是定义建立关系之间联系的主关键字与外部关键字引用的约束条件。

来假设一个情况:一个购物网站的数据库有这两张表(当然不止两张) user 和 order (订单)

mysql 建表语句如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
CREATE TABLE `order` (
`user_id` int(11) NOT NULL,
`order_id` int(11) NOT NULL,
`name` varchar(100) CHARACTER SET utf8 NOT NULL,
PRIMARY KEY (`order_id`),
KEY `order_user` (`user_id`),
CONSTRAINT `order_user` FOREIGN KEY (`user_id`) REFERENCES `user` (`user_id`) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=latin1;

CREATE TABLE `user` (
`user_id` int(11) NOT NULL,
`name` varchar(50) CHARACTER SET utf8 NOT NULL,
PRIMARY KEY (`user_id`),
KEY `name` (`name`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;

在order 中每一条订单都属于一位用户 ,用户在order 表中拥有多条记录,此时在order 中表中不可能出现一条记录的user_id 不在user的 user_id 的记录,如果这条记录的拥有者没有

存在与我的user 表中,它就不是我的用户,所以order 中不可能存在他的记录。这是一个很简单逻辑。在应用程序层次可以用一些不简单的代码实现。但是 有没有想过 如果你有数十个表每个表与其他表都有上述 user和order 这样的关系,你还能够驾驭他们在代码间的关系吗?当你的网站的程序员跳槽后,你能保证你新聘请的程序员不会对着这复杂和晦涩的逻辑嘶吼吗?

在这种情况下,在数据库层次添加外键约束,用一个统一的标准来表达上述的关系可以更加的有利于代码的维护和更新。

在 order 表中 我们可以添加:

1
2

CONSTRAINT `order_user` FOREIGN KEY (`user_id`) REFERENCES `user` (`user_id`) ONDELETE CASCADE ON UPDATE CASCADE

这样,在order 中和就不能添加user 中 user_id 不存在的值了,也就保证了order 和 user的 参照完整性
ON DELETE CASCADE ON UPDATE CASCADE 指的是在主表也就是 user 表的user_id 发生update 或delete 时,子表order 的 user_id 发生update user_id那一行数据 delete,这里还有其他的一些动作。

使用外键约束,不但简化了业务逻辑 还是一层对数据的完整性的保护 ,你一定不会想在你的order表中出现一条莫名其妙的订单吧,外键约束也在某种程度上保护了数据。在一些对数据一致性要求比较高的场景中,我们可以借助 DB 的外键来避免业务逻辑上的疏漏。

mysql-索引

索引的目的

​ 在我个人的理解中,mysql 的索引在实际应用中是一种通过空间换取查询时间的方式,它是通过对选定列在持久化存储上建立一个单独的专门用来查询的数据结构来达到提升查询速度的目的。一般这个数据结构是 BTree,在 where 语句中对已经建立好索引的列查询时,mysql 的查询引擎会在索引上进行数据的查询操作,而不是在原有的列上扫描,从而提升了查询的速度。

索引的实现

​ 索引解决的是查找的问题,一些数据结构如有序list ,二叉查找树,红黑树,BTree。mysql 使用了 Btree 作为索引的数据结构。在初始化索引时,调用引擎新建索引,每当该列有新的数据插入时,再更新索引,所以由此得出,索引比较适合一些读多写少的场景中,大量的写会对索引的更新产生压力。

索引的使用方式

​ 流行的 Mysql 引擎有 MyISAM和InnoDB数据存储引擎,笔者主要对 InnoDB 有部分使用经验,对索引的建立,主要遵守最左前缀匹配规则即可,具体解释就是:

​ 有一个表(stu),有三列 : px , uid , score ,在业务流程中可能会对 uid 和 score 同时使用where 查询时,可以考虑建立一个联合索引,索引包含两列 <uid,score> ,这个联合索引有最左匹配的性质,在以下两条 sql 中都可以起到加速查询的作用:

1
2
SELECT uid , score FROM stu WHERE  score  = 123 AND uid = 123 ; #mysql 会自己调整 where 的条件以达到使用索引的目的
SELECT uid , score FROM stu WHERE uid=123;

但是下面这条语句就无法使用索引

1
SELECT uid , score FROM stu WHERE score=123;

需要专门新建一个 的索引

字符集和字符编码学习笔记

发表于 2015-01-11

在web开发中我们总会遇到这样那样的字符编码问题,例如,当我们在代码编辑器里可以好好显示的html文档在浏览器里却变成了乱码,有时候为了能让我们的页面正常显示我们可能要忙上一天都无法解决(我可是深有体会)。为了搞清楚字符编码的问题,今天我也花了很长时间去百度。这里我和大家分享一下我的感想,不对之处,欢迎指正。

​ 首先要了解下什么是字符编码和字符集。

字符集(Charset):是一个系统支持的所有抽象字符的集合。字符是各种文字和符号的总称,包括各国家文字、标点符号、图形符号、数字等。

字符集的概念就是指一个集合,例如:26个英文字母就构成了一个英文字母字符集,还有繁体汉字字符集、日文汉字字符集等。

字符编码(Character Encoding):是一套法则,使用该法则能够对自然语言的字符的一个集合(如字母表或音节表),与其他东西的一个集合(如号码或电脉冲)进行配对。即在符号集合与数字系统之间建立对应关系,它是信息处理的一项基本技术。通常人们用符号集合(一般情况下就是文字)来表达信息。而以计算机为基础的信息处理系统则是利用元件(硬件)不同状态的组合来存储和处理信息的。元件不同状态的组合能代表数字系统的数字,因此字符编码就是将符号转换为计算机可以接受的数字系统的数,称为数字代码。

字符编码指一套规则,计算机只能处理由0,1构成的数据,计算机不能处理符号和字符,他只会处理0,1数据。为了能让计算机处理这些非数值数据,我们就把非数值数据根据一定的规则与一些二进制数据对应起来,例如在ASCLL编码中,我们用’0110 0001’来表示’a’, ASCLL对应表 , 大家可以去看看。总而言之,这些将字符集里的字符与二进制数据对应的法则的集合就是字符编码。

所以,一个字符集必然对应着一个一个字符编码。

常见的字符集&字符编码。

ASCLL字符集&字符编码。

ASCII(American Standard Code for Information Interchange,美国信息交换标准代码))是基于拉丁字母的一套电脑编码系统。它主要用于显示现代英语,而其扩展版本EASCII则可以勉强显示其他西欧语言。它是现今最通用的单字节编码系统(但是有被Unicode追上的迹象),并等同于国际标准ISO/IEC 646。ASCII**字符集**:主要包括控制字符(回车键、退格、换行键等);可显示字符(英文大小写字符、阿拉伯数字和西文符号)。

ASCII**编码**:将ASCII字符集转换为计算机可以接受的数字系统的数的规则。使用7位二进制(一个字节)表示一个字符,共128字符。后来,随这计算机的普及128个字符不够用了,于是就动用了剩下的哪一个二进制,形成了扩展的ASCLL编码,共有256个。

128的ASCLL编码表

img

256的ASCLL编码表。

img

ASCLL相对来说是一种比较古老的字符集&字符编码,因为他表示的字符太少了,渐渐就产生了其他包含更多字符的字符集&字符编码。

GBXXXX字符集&编码

BG—-国标

GB 2312 **字符集&编码**

GB 2312 **字符集:**

GB 2312 或 GB 2312-80 是中国国家标准简体中文字符集,全称《信息交换用汉字编码字符集·基本集》,又称GB0,由中国国家标准总局发布,1981年5月1日实施。GB2312编码通行于中国大陆;新加坡等地也采用此编码。中国大陆几乎所有的中文系统和国际化的软件都支持GB 2312。

GB 2312字符编码:

一个小于127的字符的意义与原来相同,但两个大于127的字符连在一起时,就表示一个汉字,前面的一个字节(他称之为高字节)从0xA1用到 0xF7(176–247),后面一个字节(低字节)从0xA1到0xFE(160–254),这样我们就可以组合出大约6654多个简体汉字了。在这些编码里,还把数学符号、罗马希腊的 字母、日文的假名们都编进去了,连在ASCII里本来就有的数字、标点、字母都统统重新编了两个字节长的编码,这就是常说的”全角”字符,而原来在127号以下的那些就叫”半角”字符了。

GB2312字符集中删掉了扩展后的那部分ASCLL字符,也就是说一个字符的二进制大于127是没有在GB2312中定义的。GB2312 收录了%99的常用汉字,但对于一些偏僻的姓,少数名族的字符没有收录,这时候就有必要出现另一种扩充的编码方式,这就是GBK。

GBK**字符集&编码**

GBK**字符集**: gbk字符集是GB2312的超集,同 GB2312一样,GBK也支持希腊字母、日文假名字母、俄语字母等字符,但不支持韩语中的表音字符(非汉字字符)。GBK还收录了GB2312不包含的 汉字部首符号、竖排标点符号等字符。

GBK字符编码:为了能够表示更多的字符,GBK的制定者们扩充了表示字符的字节范围,规定:一个小于127的字符的意义与原来相同,当有一个大于127的字符出现时且这个大于127的字符的后面跟着一个字节范围是0x40-7E和0x80-0xFE(十进制:64—127和128—256)时,他就表示一个汉字,也就是说高字节范围是0×81-0xFE,低字节范围是0x40-7E和0x80-0xFE。从这一点可以看出,GBK完全兼容了GB2312,而他又进行了扩展,但这个扩展对程序员们造成了一些影响。在GBK中,当我们遍历一个字符串时,我们无法通过判断他的二进制码是否小于127来判断他是有特殊含义的ASCLL字符,因为他有可能是一个汉字的一半。所以,当我们想要在字符串中添加标识符时,我们最好选择小于64的字符来做,例如(!,@,#,$,%,^).

有些系统中用0x40-0x7E中的字符(如”|”)做特殊符号,在定位这些符号时又没有判断这些符号是不是属于某个 GBK字符的低字节,这样就会造成错误判断。在支持GB2312的环境下就不存在这个问题。需要注意的是支持GBK的环境中小于0x80的某个字节未必就 是ASCII符号;另外就是最好选用小于0×40的ASCII符号做一些特殊符号,这样就可以快速定位,且不用担心是某个汉字的另一半。Big5编码中也存在相应问题。

GB13080**字符集&编码**

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

GB13080字符编码:GBK和GB2312都是双字节等宽编码,如果算上和ASCII兼容所支持的单字节,也可以理解为是单字节和双字节混合的变长编码。GB18030编码是变长编码,有单字节、双字节和四字节三种方式。GB18030的单字节编码范围是0x00-0x7F,完全等同与ASCII;双字节编码的范围和GBK相同,高字节是0x81-0xFE,低字节 的编码范围是0x40-0x7E和0x80-FE;四字节编码中第一、三字节的编码范围是0x81-0xFE,二、四字节是0x30-0x39。

BIG5字符集&编码

BIG5字符集:由于GB系列的字符集&字符编码只收录了简体汉字而没有考虑台湾人民的感受,所以他们也推出了自己的字符集&字符编码那就是大五码—BIG5,Big5收录的汉字只包括繁体汉字,不包括简体汉字,一些生僻的汉字也没有收录。GBK收录的日文假名字符、俄文字符BIG5也没有收录。Big5编码对应的字符集是GBK字符集的子集,也就是说Big5收录的字符是GBK收录字符的一部分,但相同字符的编码不同。

BIG5字符编码:Big5是双字节编码,高字节编码范围是0x81-0xFE(128–255),低字节编码范围是0x40-0x7E和0xA1-0xFE。和GBK相比,少了低字节是0x80-0xA0的组合。0x8140-0xA0FE是保留区域,用于用户造字区。

因为Big5也占用了ASCII的编码空间(低字节所使用的0x40-0x7E),所以Big5编码在一些环境下存在和GBK编码相同的问题,即低字节范围为0x40-0x7E的字符有可能会被误处理,尤其是低字节是0x5C(”/”)和0x7C(”|”)的字符。可以参考GBK一节相应说明。

小结:在上述所例举的GB系列,BIG5,这些字符集他们都用一个小于127的字节来表示英文字符,用两个字节且第一个字节必须大于127的双字节来表示汉字。于是他们有了一个共同的名字—- “DBCS”(Double Byte Charecter Set 双字节字符集)。在DBCS系列标准里,最大的特点是两字节长的汉字字符和一字节长的英文字符并存于同一套编码方案里,因此他们写的程序为了支持中文处理,必须要注意字串里的每一个字节的值,如果这个值是大于127的,那么就认为一个双字节字符集里的字符出现了。

伟大的UNICODE

 因为当时各个国家都像中国这样搞出一套自己的编码标准,结果互相之间谁也不懂谁的编码,谁也不支持别人的编码,连大陆和台湾这样只相隔了150海里,使用 着同一种语言的兄弟地区,也分别采用了不同的 DBCS 编码方案。导致文件在不同的地区的传输出现了很大的问题,这时候一个伟大的创想产生了——Unicode(”Universal Multiple-Octet Coded Character Set”,简称 UCS, 俗称 “UNICODE”)。这个由一个叫 ISO(国际标谁化组织)的国际组织编制的字符集可以表示世界上所有的字符,它规定用4个字节来表示字符,每个数字代表唯一的至少在某种语言中使用的符号(并不是所有的数字都用上了,但是总数已经超过了65535,所以2个字节的数字是不够用的。)在UNICODE中,每个字符对应一个数字,每个数字对应一个字符。即不存在二义性。不再需要记录”模式”了。U+0041总是代表’A’,即使这种语言没有’A’这个字符。

在计算机科学领域中,Unicode(统一码、万国码、单一码、标准万国码)是业界的一种标准,它可以使电脑得以体现世界上数十种文字的系统。Unicode 是基于通用字符集(Universal Character Set)的标准来发展,并且同时也以书本的形式[1]对外发表。直至目前为止的第六版,Unicode 就已经包含了超过十万个字符(在2005年,Unicode 的第十万个字符被采纳且认可成为标准之一)、一组可用以作为视觉参考的代码图表、一套编码方法与一组标准字符编码、一套包含了上标字、下标字等字符特性的枚举等。Unicode 组织(The Unicode Consortium)是由一个非营利性的机构所运作,并主导 Unicode 的后续发展,其目标在于:将既有的字符编码方案以Unicode 编码方案来加以取代,特别是既有的方案在多语环境下,皆仅有有限的空间以及不兼容的问题。

UNICODE这种编码方式虽然形成了一种统一的字符集,世界上的人们终于能在同一种字符集下工作了。但是他也带来了一些问题。

例如汉字”严”的unicode是十六进制数4E25,转换成二进制数足足有15位(100111000100101),也就是说这个符号的表示至少需要2个字节。表示其他更大的符号,可能需要3个字节或者4个字节,甚至更多。而计算机不知道这两个字节是一个汉字,还是两个ASCLL字符。

用跟多的字节表示英文字符,这样浪费了大量的空间,老外们非常不高兴。

传输时的问题。

于是这个时候就有必要出现一种可以解决这个问题的全新的编码方式。于是就出现了UTF系列(utf-8,utf-16,utf-32)

(可以这样理解:**Unicode是字符集,UTF-32/ UTF-16/ UTF-8**是三种字符编码方案。)

UTF-32编码方式:

上述使用4字节的数字来表达每个字母、符号,或者表意文字(ideograph),每个数字代表唯一的至少在某种语言中使用的符号的编码方案,称为UTF-32。UTF-32又称UCS-4是一种将Unicode字符编码的协定,对每个字符都使用4字节。就空间而言,是非常没有效率的。这种方法有其优点,最重要的一点就是可以在常数时间内定位字符串里的第N个字符,因为第N个字符从第4×Nth个字节开始。虽然每一个码位使用固定长定的字节看似方便,它并不如其它Unicode编码使用得广泛。

UTF-16 编码方式

​ 尽管有Unicode字符非常多,但是实际上大多数人不会用到超过前65535个以外的字符。因此,就有了另外一种Unicode编码方式,叫做UTF-16(因为16位 = 2字节)。UTF-16将0–65535范围内的字符编码成2个字节,如果真的需要表达那些很少使用的”星芒层(astral plane)”内超过这65535范围的Unicode字符,则需要使用一些诡异的技巧来实现。这些诡异的技巧请自行百度。

小结:

对于UTF-32和UTF-16编码方式还有一些其他不明显的缺点。不同的计算机系统会以不同的顺序保存字节。这意味着字符U+4E2D在UTF-16编码方式下可能被保存为4E 2D或者2D 4E,这取决于该系统使用的是大尾端(big-endian)还是小尾端(little-endian)。这时候数据的传输就出了问题,为了解决这个问题,多字节的Unicode编码方式定义了一个”字节顺序标记(Byte Order Mark)”,它是一个特殊的非打印字符,你可以把它包含在文档的开头来指示你所使用的字节顺序。对于UTF-16,字节顺序标记是U+FEFF。如果收到一个以字节FF FE开头的UTF-16编码的文档,你就能确定它的字节顺序是单向的(one way)的了;如果它以FE FF开头,则可以确定字节顺序反向了。这就是我们在notepad++的格式选项里看到的UCS-2 BIG endian & UCS-2 Little endian 的含义了。

UTF-8 编码方式

互联网的普及,强烈要求出现一种统一的编码方式。UTF-8就是在互联网上使用最广的一种Unicode的实现方式,也是最重要的一种编码方式。

UTF-8(8-bit Unicode Transformation Format)是一种针对Unicode的可变长度字符编码(定长码),也是一种前缀码。它可以用来表示Unicode标准中的任何字符,且其编码中的第一个字节仍与ASCII兼容,这使得原来处理ASCII字符的软件无须或只须做少部份修改,即可继续使用。因此,它逐渐成为电子邮件、网页及其他存储或传送文字的应用中,优先采用的编码。互联网工程工作小组(IETF)要求所有互联网协议都必须支持UTF-8编码。UTF-8最大的一个特点,就是它是一种变长的编码方式。它可以使用1~4个字节表示一个符号,根据不同的符号而变化字节长度。

UTF-8的编码规则很简单,只有二条:

1)对于单字节的符号,字节的第一位设为0,后面7位为这个符号的unicode码。因此对于英语字母,UTF-8编码和ASCII码是相同的。

2)对于n字节的符号(n>1),第一个字节的前n位都设为1,第n+1位设为0,后面字节的前两位一律设为10。剩下的没有提及的二进制位,全部为这个符号的unicode码。

下表总结了编码规则,字母x表示可用编码的位。

Unicode符号范围 | UTF-8编码方式

(十六进制) | (二进制)

——————–+———————————————

0000 0000-0000 007F | 0xxxxxxx

0000 0080-0000 07FF | 110xxxxx 10xxxxxx

0000 0800-0000 FFFF | 1110xxxx 10xxxxxx 10xxxxxx

0001 0000-0010 FFFF | 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx

下面,还是以汉字”严”为例,演示如何实现UTF-8编码。

已知”严”的unicode是4E25(100111000100101),根据上表,可以发现4E25处在第三行的范围内(0000 0800-0000 FFFF),因此”严”的UTF-8编码需要三个字节,即格式是”1110xxxx 10xxxxxx 10xxxxxx”。然后,从”严”的最后一个二进制位开始,依次从后向前填入格式中的x,多出的位补0。这样就得到了,”严”的UTF-8编码是”11100100 10111000 10100101″,转换成十六进制就是E4B8A5。

从上面的过程中我们可以看出在utf-8中 汉字大多是三个字节的而英文字母都是1个字节的,这样原来的英文文件就可以在不用修改的情况下来适应utf-8,也不用多余的占用空间。

UTF-8优点:

  1. UTF-8是ASCII的一个超集。因为一个纯ASCII字符串也是一个合法的UTF-8字符串,所以现存的ASCII文本不需要转换。为传统的扩展ASCII字符集设计的软件通常可以不经修改或很少修改就能与UTF-8一起使用。
  2. UTF-8字符串可以由一个简单的算法可靠地识别出来。就是,一个字符串在任何其它编码中表现为合法的UTF-8的可能性很低,并随字符串长度增长而减小。

Utf-8 BOM 问题:

前面提到过”字节顺序标记(Byte Order Mark)”—-BOM 这是用来表示UTF-16和UTF-32的字节顺许的,但UTF-8并不需要他,他在utf-8文件中被编译为” EF BB BF “,占三个字节。这就是我们用记事本创建一个utf-8文件时,它的起始大小为3字节,这三个字节来标识UTF-8编码。但这三个字节会常常造成一些问题。

例如:

  1. 当一个文件流是utf-8 BOM 形式编码的话,其在除IE10之外的IE中都会输出一个空行。这会导致一些页面的错乱。
  2. 受COOKIE送出机制的限制,在这些文件开头已经有BOM的文件中,COOKIE无法送出(因为在COOKIE送出前PHP已经送出了文件头),所以登入和登出功能失效。一切依赖COOKIE、SESSION实现的功能全部无效。

解决办法:

使用notepad++编辑器打开文件,在”格式”选项中选择”转为UTF-8无BOM格式”。

ANSI 是神马??

在notepad++中的”格式”选项中,我们可以看到”以ANSI编码”这个选项,那么,ANSI究竟是什么呢???

为使计算机支持更多语言,通常使用 0x80~0xFF 范围的 2 个字节来表示 1 个字符。比如:汉字 ‘中’ 在中文操作系统中,使用 [0xD6,0xD0] 这两个字节存储。不同的国家和地区制定了不同的标准,由此产生了 GB2312, BIG5, JIS 等各自的编码标准。这些使用 2 个字节来代表一个字符的各种汉字延伸编码方式,称为 ANSI 编码。在简体中文系统下,ANSI 编码代表 GB2312 编码,在日文操作系统下,ANSI 编码代表 JIS 编码。不同 ANSI 编码之间互不兼容,当信息在国际间交流时,无法将属于两种语言的文字,存储在同一段 ANSI 编码的文本中。

ANSI 就是一种对双字节编码方式的总称,我们可以把它看做一个变量,在不同的地域,他的值是不同的。而这个地域的判断又是依赖于操作系统。在中国,ANSI=GB2312,在日本,ANSI=JIS。有的时候ANSI也被称为’本地编码’。事实上,ANSI的值取决于windows的’codepage’,你可以在命令行下输入”chcp”来查看你的codepage,一般为936,然后查看cmd的属性时,你会发现codepage时GBK。

说了半天,也就是在中国,你的ANSI就是GB2312。文本以ANSI的形式存储或处理时,就是以GB2312处理的。

举个例子:

比 如 一个用户在简体中文Windows下面用记事本输入一些汉字后保存,然后copy到另一台英文Windows上,尝试用记事本打开,就会发现出现的是一 些乱码。并不是copy的过程中出现了错误,而是因为在英文的Windows上打开文件时默认的ANSI编码是Wenstern European(Windows) – Codepage 1252,而在简体中文的Windows上保存打开时默认的编码格式是Chinese Simplified(GB2312) – Codepage 936。

实验课

新建5个txt文件分别命名为”utf-8.txt”,”gb2312.txt”,”ANSI.txt”,”Unicode big endian,txt”,”Unicode little endian.txt”,然后用notepad++打开,将其改变为名称对应的编码方式。然后在里面都输入’严’

然后,使用Ultraedit 打开,按下’ctrl+H’查看他们的十六进制码。

  • ANSI:文件的编码就是两个字节”D1 CF”,这正是”严”的GB2312编码。:

  • Gb2312:文件的编码就是两个字节”D1 CF”,和ANSI一样。

  • Unicode litte endian:编码是四个字节”FF FE 25 4E”,其中”FF FE”表明是小头方式存储,真正的编码是4E25。

  • Unicode big endian:编码是四个字节”FE FF 4E 25″,其中”FE FF”表明是大头方式存储。

  • UTF-8:编码是六个字节”EF BB BF E4 B8 A5″,前三个字节”EF BB BF”表示这是UTF-8编码,后三个”E4B8A5″就是”严”的具体编码,它的存储顺序与编码顺序是一致的。

    ​

    参考资料及延伸阅读:

    http://blog.csdn.net/stilling2006/article/details/4129700

    http://www.pconline.com.cn/pcedu/empolder/gj/other/0505/616631.html

    http://www.joelonsoftware.com/articles/Unicode.html

    http://www.crifan.com/files/doc/docbook/char_encoding/release/webhelp/content/

    http://huaichang.blogbus.com/logs/19602597.html

liukai93

7 日志
4 标签
GitHub Twitter ZhiHu
© 2018 liukai93
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4