Tomcat不安全字符的处理

做项目的时候碰到一个问题,就是Tomcat在处理含有|,{,}的字符的Url时候,发现请求没有到达指定的Controller上面,而在Access_log中写入了get null null 400的错误信息,从网上也翻了几个资料最终确定是tomcat的一个问题(个人觉得也是一个缺陷)

问题的由来

Tomcat根据rfc的规范Url中不能有类似|,{,}等不安全字符串,但在实际的操作中有时为了数据完整性和加密的方式都需要有|,{,}出现,这样的话Tomcat会直接告诉客户端Bad Request.

对于这个问题,很多人也提出很多不同的看法:https://bz.apache.org/bugzilla/show_bug.cgi?id=60594,经过修改,最终Tomcat把权限开放出来,通过tomcat.util.http.parser.HttpParser. requestTargetAllow这个配置选项,允许不安全字符的出现。Tomcat详细配置

解决方法

经过几次探索,有以下几个方法能够解决这个问题:

  1. 把请求的Url进行编码,这个对源头进行处理,来规避这个问题,如果是第三方来调用的url就无能无力。

  2. 修改Tomcat的配置文件(Tomcat\conf\catalina.properties),适用tomcat 7以上的版本

    1
    2
    3

    tomcat.util.http.parser.HttpParser.requestTargetAllow=|{}

  3. 使用其它服务器进行中转,比如IIS和Apache

Java枚举类型使用

在编程中,常常遇到多种类型、多种状态的情况,对于这种可以事先预知的业务我们常常有两种表示方法:

  • 使用DB管理,优点是方便在线维护,缺点是每次加载都要读取数据库,加载的性能取决于数据库的压力 。

  • 使用枚举类型,优点是加载速度依赖于应用服务器,缺点是修改比较麻烦,每次加类型都需要发布代码。

对于Java枚举类型的使用,我们可以总结为以下几个方面:整型值,字符串字段和字符串的相关描述,下面我们就讨论如何方便的在这几个类型中相关转换,对于所有的类型转换可以总结如下:

1
2
3
4
5
6
7
int --> Enum 
Enum--> int
String -->Enum
Enum-->String
Enum-->描述
int -->描述
String -->描述

下面我们对于上面的7种描述来做出对应的转换方法,首先定义一个枚举类型:

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
    public enum EnumDemo {
fail,
success,
unknow;
}
```

为了能够保证相关的value和值能够保存下来,我们需要对枚举类型改成如下:

```Java
public enum EnumDemo {
fail(0, "失败"),
success(1, "成功"),
unknow(2, "未知");

private int value;
private String name;

EnumDemo(int value, String name) {
this.value = value;
this.name = name;
}
public int getValue() {
return this.value;
}
public String getName() {
return this.name;
}
}
```


相关代码如下:

```Java
@Test
public void TestEnum(){
int val=1;
EnumDemo intEnum=EnumDemo.values()[val];//整型转Enum
String enumStr= intEnum.toString();//Enum转字符串
String enumAnno=intEnum.getName();//Enum转描述

System.out.println(enumStr);
System.out.println(enumAnno);


String str="success";
EnumDemo strEnum=EnumDemo.values()[val];//字符转Enum
int enumVal=strEnum.getValue();//字符转int
String enumValAnno=strEnum.getName();//Enum转描述

System.out.println(enumVal);
System.out.println(enumValAnno);
}

运行结果如下:

1
2
3
4
success
成功
1
成功

详解.net中IL语言

什么是IL语言

中间语言,又称(IL语言)。充当Clr与.net 平台的中间语言,比如用C#编写程序,编译器首先是把C#代码转译成IL语言,最终由Clr解释执行,下面我们学习下IL语言。

如何读懂IL语言

  • 写一个helloworld的.net 程序,编译运行完成。

    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
    		static void Main(string[] args)
    {
    Console.WriteLine("hello world");
    }
    ```

    - 使用ildasm.exe(C:\Program Files (x86)\Microsoft SDKs\Windows\v10.0A\bin\NETFX 4.6 Tools)反编译代码,得到IL代码如下:

    ```IL
    .method private hidebysig static void Main(string[] args) cil managed
    {
    .entrypoint
    // 代码大小 13 (0xd)
    .maxstack 8
    IL_0000: nop
    IL_0001: ldstr "hello world"
    IL_0006: call void [mscorlib]System.Console::WriteLine(string)
    IL_000b: nop
    IL_000c: ret
    } // end of method Program::Main
    ```

    - 查找对应的[指令表](http://blog.csdn.net/xiaouncle/article/details/71248830),来确定对应的含义


    <table class="table">
    <thead>
    <tr>
    <th>指令名称</th>
    <th>说明</th>
    </tr>
    </thead>
    <tbody>
    <tr>
    <td>Ldstr</td>
    <td>推送对元数据中存储的字符串的新对象引用。</td>
    </tr>
    <tr>
    <td>Nop</td>
    <td>如果修补操作码,则填充空间。尽管可能消耗处理周期,但未执行任何有意义的操作。</td>
    </tr>
    <tr>
    <td>Call</td>
    <td>调用由传递的方法说明符指示的方法。</td>
    </tr>
    <tr>
    <td>Ret</td>
    <td>从当前方法返回,并将返回值(如果存在)从调用方的计算堆栈推送到被调用方的计算堆栈上。</td>
    </tr>
    </tbody>
    </table>

    - 其它几个名词的的解释

    **hidebysig**: 与之对就的是hidebyname,这个是确定使用方法的签名还是使用方法的名称来确定调用哪个方法.


    - 整个的IL语言解释

    ```IL
    .method private hidebysig static void Main(string[] args) cil managed
    {
    .entrypoint //代码入口
    // 代码大小 13 (0xd)
    .maxstack 8 //整个程序的堆栈大小
    IL_0000: nop //无实在意义
    IL_0001: ldstr "hello world" //定义字符
    IL_0006: call void [mscorlib]System.Console::WriteLine(string) //调用WriteLine变量
    IL_000b: nop
    IL_000c: ret //返回
    } // end of method Program::Main
    ```


    ### 更复杂的Demo

    - 添加编写如下C#代码:

    ``` C#
    class Program
    {
    static void Main(string[] args)
    {
    var a = 0;
    var b = 1;
    var c = Add(a, b);
    Console.WriteLine(c.ToString());
    }

    public static int Add(int x,int y)
    {
    return x + y;
    }
    }

    ```

    - 生成相关的IL代码及解释

    ``` IL
    .method private hidebysig static void Main(string[] args) cil managed
    {
    .entrypoint
    // 代码大小 27 (0x1b)
    .maxstack 2
    .locals init ([0] int32 a,
    [1] int32 b,
    [2] int32 c) //定义3个变量
    IL_0000: nop
    IL_0001: ldc.i4.0 //将整数值 0 作为 int32 推送到计算堆栈上。
    IL_0002: stloc.0 //从计算堆栈的顶部弹出当前值并将其存储到索引 0 处的局部变量列表中。
    IL_0003: ldc.i4.1 //将整数值 1 作为 int32 推送到计算堆栈上。
    IL_0004: stloc.1 //从计算堆栈的顶部弹出当前值并将其存储到索引 1 处的局部变量列表中。
    IL_0005: ldloc.0 //将索引 0 处的局部变量加载到计算堆栈上,这里指a。
    IL_0006: ldloc.1 //将索引 1 处的局部变量加载到计算堆栈上,这里指b。
    IL_0007: call int32 ILTest.Program::Add(int32,
    int32) //调用Add方法
    IL_000c: stloc.2 //将索引 2 处的局部变量加载到计算堆栈上,这里指c。
    IL_000d: ldloca.s c //将位于特定索引处的局部变量的地址加载到计算堆栈上(短格式)。
    IL_000f: call instance string [mscorlib]System.Int32::ToString()
    IL_0014: call void [mscorlib]System.Console::WriteLine(string)
    IL_0019: nop
    IL_001a: ret
    } // end of method Program::Main

    ```

    Add方法:

    ``` IL
    .method public hidebysig static int32 Add(int32 x,int32 y) cil managed
    {
    // 代码大小 9 (0x9)
    .maxstack 2
    .locals init ([0] int32 V_0) //创建一个V_0的局部变量
    IL_0000: nop
    IL_0001: ldarg.0 //将索引为 0 的参数加载到计算堆栈上。
    IL_0002: ldarg.1 //将索引为 1 的参数加载到计算堆栈上。
    IL_0003: add //将两个值相加并将结果推送到计算堆栈上。
    IL_0004: stloc.0
    IL_0005: br.s IL_0007 //无条件地将控制转移到目标指令(短格式)
    IL_0007: ldloc.0 //将索引 0 处的局部变量加载到计算堆栈上。
    IL_0008: ret
    } // end of method Program::Add

2017年国庆出行

两个同学结婚小明和小六,分别在庐江和宿州,具体详细路途 花桥–苏州–庐江–宿州–蚌埠–花桥,具体路线如下:

road

一路不停的赶路,走了3天才走完这条路,出去走走想法也有了些许的变化。人生在世,唯富贵,学识难求,其余皆是过眼去烟。富贵是不期而遇,唯有学识自己努力才能得到。

这次相聚很短暂,下次又不知道何时才能见面,与老李相约走一趟青藏公路,五年为期,不知道五年后我的博客能不能更新一个青藏公路的文章。

小六结婚,寝室五人全到,老二带老婆夏青,我带老婆慧慧,老李和老苏各一个人,独缺老胡。

愿各位安好,日后当以富贵相见。

服务器CPU居高不下--解决问题历程

基本的概述

在一个服务器的集群上面,服务器的CPU长时间居高不下,响应的时间也一直很慢,即使扩容了服务器CPU的下降效果也不是很明显。

对于CPU过高的原因,可以总结到以下原因:

  • 太多的循环或者死循环

  • 加载了过多的数据,导致产生了很多的大对象

  • 产生了过多的对象,GC回收过于频繁(如:字符串拼接)

对于上面的情况,难点不是优化代码,难点在于定位到问题的所在,下面我们就用Dump抓包的方式来定位到问题的所在。介绍这个内容之前,我们要先回顾下.Net中垃圾回收的基础知识和一个工具的准备。


基础知识


垃圾回收触发条件

  • 代码显示调用System.GC的静态方法

  • windows报告低内存情况

  • CLR正在卸载AppDoamin

  • CLR正在关闭

大对象垃圾回收

CLR将对象分为大对象和小对象,认为大于85000字节或者更大的字节是大对象,CLR用不同的方式来对待大对象和小对象:

  • 大对象不是在小对象的地址空间分配,而是在进程地址空间和其他地方分配

  • GC不会压缩大对象,在内存中移动他们的代价过高,但这样会造成地址空间的碎片化,以至于会抛出OutOfMemeryException 异常。

  • 大对象总是在第二代回收。

工具准备

  1. 下载[windbg文件](dbg_amd64_6.12.2.633.msi)

  2. 相关DLL准备clr.dll和sos.dll,(都在对应.Net版本安装目录下面,我的安装目录在C:\Windows\Microsoft.NET\Framework64\v4.0.30319)

  3. 一个cpu运行的较高的时期的DUMP文件(下面会说如何获取)

  4. 准备测试代码,此处为了演示方便,简单了写了一个有潜在问题的代码:

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
public  class Common
{
public static List<string> GetList()
{
var list=new List<string>();
for (int i = 0; i < 10000; i++)
{
list.Add(i.ToString());
}
return list;
}


public static string GetString(List<string> list)
{
var str = "";
foreach (var l in list)
{
str += string.Format("'{0}',", l);
}
if (str.Length > 0)
{
str.Remove(str.Length - 1);
}
return str;
}
}

我们知道在字符串的拼接的时候,每一个字符串都是一个对象,拼接后又产生了一个新对象,所以在GetString这个方法中会有大量的GC操作,下面我们就调用下这个代码,看下CPU的情况,为了模拟并发情况,我们开多个标签,每个标签每1s秒中刷新一次。

CPU

抓取Dump

在任务管理器中选择应用程序池对应的w3wp.exe,右击–>创建转储文件。创建完成后,会提示出指定的路径

taskManger

taskManger

根据上面的步骤,我们准备我们分析的文件如下:

dumpfile

分析Dump

  • 打开windbg,加载对应的dump文件

    dumpOpen
    dumpOpen

  • 配置Sysmbol,添加”cachec:\mysymbol;srvhttp://msdl.microsoft.com/download/symbols
    dumpOpen
    dumpOpen

  • load sos.dll和clr.dll,命令如下:

    .load D:\windbg\sos.dll 
    .load D:\windbg\clr.dll
    
  • 运行命令!threadpool 显示有关托管线程池的信息,其它一些SOS 调试扩展命令.

    dumpOpen

  • 运行!runaway 查询cpu占用时长比较长的几个线程Id

    dumpOpen

  • 运行~22s (进入线程查看),kb(查看对应的调用)

    dumpOpen

  • 运行~* kb 查看所有线程的堆栈调用

    dumpOpen

  • 在上面搜索GC和大对象出现的线程 (ctrl+f搜索:GarbageCollectGeneration和allocate_large_object )

    dumpOpen

  • 可以看到定位触发GC的线程是31号线程

  • 运行命令~31s 进入31线程,再运行!clrstack查看堆栈调用,最终可以定位到出问题的代码,是由于字符串的拼接导致大量的对象产生,从而触发了GC。

    dumpOpen

Java反射和注解

反射

反射是指在运行的状态,对于任意一个类,都能够知道类里面的所有的属性和方法,并能够进行属性的赋值和方法的调用 。 在Java中使用Java.lang下面的Class来表示**类型的”类” ** ,在JDK中定义接口如下

Java反射

其中T 表示运行时类的类型,如果不知道类型可以使用Class<?>,Class表示的实例表示正在运行的 Java 应用程序中的类(包含枚举) 和接口 , 所有的反射出来的结果都共享一个基类Class。

获得类型、方法、属性和构造器

在Java中有三种方法可以反射定制类的Class(以String类型为例):

1
2
3
4
5
6
1. 通过Class.from("Java.lang.String")    

2. 通过String.class

3. 通过类的实例对像中的getClass()方法 :"abc".getClass()

为了演示反射的功能,我们首先定义一个类型:

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
class Person
{
private String userName;
private String userCode;
public String Sex;//字段

public String getUserName() {
return userName;
}

public void setUserName(String userName) {
this.userName = userName;
}

public String getUserCode() {
return userCode;
}

public void setUserCode(String userCode) {
this.userCode = userCode;
}

public String GetUserNameWithUserCode(){
return this.getUserName()+"_"+getUserCode();
}

public String GetUserNameWithUserCode(String prefix){
return prefix+"_"+ this.getUserName()+"_"+getUserCode();
}

public static String GetClassName(String prefix){
return prefix+"_Person";
}
}

获得Person类中的方法、属性和构造器,代码如下:

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
try {
Class<?> clazz = Class.forName("Person");

System.out.println("--------------Person的方法如下----------------");
Method[] methods = clazz.getMethods();
for (Method m : methods) {
System.out.println(m);
}


System.out.println("--------------Person字段如下----------------");
Field[] fields = clazz.getFields();
for (Field f : fields) {
System.out.println(f);
}

System.out.println("--------------Person构造函数如下----------------");
Constructor<?>[] constructors = clazz.getDeclaredConstructors();
for (Constructor c : constructors) {
System.out.println(c);
}
} catch (Exception e) {
e.printStackTrace();
}
```

运行结果:

```cte
--------------Person的方法如下----------------
public Java.lang.String Person.getUserName()
public Java.lang.String Person.GetUserNameWithUserCode()
public Java.lang.String Person.GetUserNameWithUserCode(Java.lang.String)
public void Person.setUserName(Java.lang.String)
public Java.lang.String Person.getUserCode()
public void Person.setUserCode(Java.lang.String)
public static Java.lang.String Person.GetClassName(Java.lang.String)
public final void Java.lang.Object.wait() throws Java.lang.InterruptedException
public final void Java.lang.Object.wait(long,int) throws Java.lang.InterruptedException
public final native void Java.lang.Object.wait(long) throws Java.lang.InterruptedException
public boolean Java.lang.Object.equals(Java.lang.Object)
public Java.lang.String Java.lang.Object.toString()
public native int Java.lang.Object.hashCode()
public final native Java.lang.Class Java.lang.Object.getClass()
public final native void Java.lang.Object.notify()
public final native void Java.lang.Object.notifyAll()
--------------Person字段如下----------------
public Java.lang.String Person.Sex
--------------Person构造函数如下----------------
Person()

```

#### 方法的调用和属性的赋值

上面的代码我们能够获得对应的属性和方法,下面就解决如何去调用当前方法和属性进行赋值:

```Java
try {
Class clazz =Class.forName("Person");
Object obj=clazz.newInstance();//创建对象

//set方法的获得
Method setUserName=clazz.getMethod("setUserName",String.class);
Method setUserCode=clazz.getMethod("setUserCode",String.class);
//set方法的调用
setUserName.invoke(obj,"fuwei");
setUserCode.invoke(obj,"F0001");

//一般方法的获得和调用
Method m1=clazz.getMethod("GetUserNameWithUserCode");
String s1= m1.invoke(obj,null).toString();

Method m2=clazz.getMethod("GetUserNameWithUserCode",String.class);
String s2= m2.invoke(obj,new Object[]{"Test"}).toString();

//静态方法的调用
Method m3=clazz.getMethod("GetClassName",String.class);
String s3= m3.invoke(null,new Object[]{"Test"}).toString();

System.out.println(s1);
System.out.println(s2);
System.out.println(s3);
}
catch (Exception e) {
e.printStackTrace();
}

```

输出结果:

```cte
fuwei_F0001
Test_fuwei_F0001
Test_Person

使用反射实现动态代理

为了更好的理解动态代理,首先要理解静态代理的逻辑,具体的实现示意图:

动态代理

具体实现类代码如下:

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
interface ProxyInterface {
public void doSomething();
}

//被代理者
class RealObj implements ProxyInterface {
@Override
public void doSomething() {
System.out.println("doSomething");
}

}

//代理者
class ProxyObj implements ProxyInterface {
@Override
public void doSomething() {
System.out.println("before Exector");
new RealObj().doSomething();
System.out.println("after Exector");
}

}


对应的调用代码:

1
2
3
4
5
6
7
8
9
10
11

@Test
public void TestProxy(){
CallMethod(new ProxyObj());

}

public void CallMethod(ProxyInterface pi){
pi.doSomething();
}

运行结果:

1
2
3
4
before Exector
doSomething
after Exector

在静态的代理中,可以看到是在代理类的内部使用了真实类的实例,来实现代理的功能,但这样就只能代理指定的类的类型,丧失了灵活性。如果我们能够在代理的时候把数据传入到代理类中,就可以动态的实现类的代理。 代码实现原理如下:

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
interface ProxyInterface {
public void doSomething();
}

//被代理者
class RealObj implements ProxyInterface {
@Override
public void doSomething() {
System.out.println("doSomething");
}

}

//代理者
class ProxyObj implements InvocationHandler {

private Object subject;
public ProxyObj(Object subject)
{
this.subject = subject;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

System.out.println("Before Exector");
Object obj = method.invoke(subject, args);
System.out.println("After Exector");

return obj;
}
}

调用的实现:

1
2
3
4
5
6
7
8
9
10
11

public void TestProxy()
{
RealObj real = new RealObj();
ProxyInterface proxy = (ProxyInterface) Proxy.newProxyInstance(
ProxyInterface.class.getClassLoader(),
new Class[]{ProxyInterface.class},
new ProxyObj(real));
proxy.doSomething();
}

对于这个实现的原理可以参考这篇博客细说JDK动态代理的实现原理 {:target=_blank}, 生成了一个继承自Proxy和实现了ProxyInterface方法的一个对象,具体的示意为:

1
2
3
4
class proxy extends Proxy implements HelloWorld{
....
}

注解

Java注解提供了关于代码的一些信息,但并不直接作用于它所注解的代码内容,常用的注解有可以参考:Java注释Override、Deprecated、SuppressWarnings详解

自定义注解

注解的大多使用情况都是结合反射,在Spring框架中也有很多都是使用反射+注解的方法来实现,下面为了更深入了解注解,我们可以自定义一个注解,注解在Java中的实现很简单:

1
2
3
4
5
6
public @interface MyAnno   
{

}


只需要这样定义就可以直接使用这个注解 ,但这个是没有任何的实际意义。在这个注解中,可以添加对应的内部方法:

1
2
3
4
5
6
7
8

public @interface MyAnno
{

String value();
}


在使用的时候,我们就可以对value进行赋值:

1
2
3
4
5
6
@MyAnno(value = "test")
public void TestReflect()
{

}

对于上面的代码,注解没有干涉内部代码的逻辑,但也没有起任何的作用。对于注解的使用,我们可以根据上面说到反射来实现我们的目的。比如,我们想实现一个注解为test的方法调用,可以先获得一个方法的所有注解,然后根据注解的value值来判断是否调用 ,下面是获得所有注解的方法:

1
2
3
4
5
6
7
8
9
10

Method[] methods = clazz.getMethods();
for (Method m : methods) {
Annotation[] ans= m.getDeclaredAnnotations();
for (Annotation an : ans)
{
System.out.println(an);
}
}

如何使用正则表达式

说到正则,可能很多人会很头疼这个东西,除了计算机好像很难快速的读懂这个东西,更不用说如果使用了。下面我们由浅入深来探索下正则表达式:

ps:此文适用于还有没有入门正则表达基础的读者

正则表达式可以简的定义成为一种字符串的匹配方式,至于来源可以参考:正则表达式{:target=_blank}

简单的使用

有这么一段字符串ABC12345ABC1234AB12C,对于这个字符串,如果想提取其中的字母,应该怎么办呢?

1
2
3
4
5
6
1. 可以找出所有的字母列表组成一个数组,[A,B,C...Z]     

2. 把字符串转成字符的数组,进行遍历

3. 如果是字母则继续,如果不是则直接继续下一个匹配

以上的分析过程则大概的讲述了不用正则表达式的过程,如果使用正则,怎么去写呢?

首先,我们是要匹配字母,那我要知道正则中用什么来表式字母呢?

1
2
3
4
5
6
7
8

[a-z] //匹配所有的小写字母
[A-Z] //匹配所有的大写字母
[a-zA-Z] //匹配所有的字母
[0-9] //匹配所有的数字
[0-9\.\-] //匹配所有的数字,句号和减号
[ \f\r\t\n] //匹配所有的白字符

根据上面的内容,可以看到用[A-Z]来表示A-Z的字母,当我们使用表达式[A-Z]去做测试的时候,发现可以匹配到所有的字母

正则表达式


基于对上面简单的描述,我们再继续深入,如果我想匹配里面所有的字符串呢?这就用到我们的其它的表达式了,把这种能够表达一个上下文的表达式称为元字符

下面我们看下我们需要的元字符:

字符 描述
{n}

n 是一个非负整数。匹配确定的 n 次。例如,'o{2}' 不能匹配 "Bob" 中的 'o',但是能匹配 "food" 中的两个 o。

{n,}

n 是一个非负整数。至少匹配n 次。例如,'o{2,}' 不能匹配 "Bob" 中的 'o',但能匹配 "foooood" 中的所有 o。'o{1,}' 等价于 'o+'。'o{0,}' 则等价于 'o*'。

{n,m}

m 和 n 均为非负整数,其中n <= m。最少匹配 n 次且最多匹配 m 次。例如,"o{1,3}" 将匹配 "fooooood" 中的前三个 o。'o{0,1}' 等价于 'o?'。请注意在逗号和两个数之间不能有空格。

+

匹配前面的子表达式一次或多次。例如,'zo+' 能匹配 "zo" 以及 "zoo",但不能匹配 "z"。+ 等价于 {1,}。

?

匹配前面的子表达式零次或一次。例如,"do(es)?" 可以匹配 "do" 或 "does" 。? 等价于 {0,1}。

.

匹配除 "\n" 之外的任何单个字符。要匹配包括 '\n' 在内的任何字符,请使用像"(.|\n)"的模式。

基于对上面的表格,可以找到我们想要的元字符{n,}和+,所以我们的表达式可以写成**[A-Z]{1,}** 或 [A-Z]+

正则表达式

到时目前为止我们已经可以写出一个简单的正则表达式了,上面的匹配结果为:

1
2
3
4
ABC
ABC
AB
C

在这个结果中,我们可以看出ABC和AB都是满足当前的正则表达式的,但为什么第一个不返回AB或者A呢?也就是下面的结果也满足当前的表达式:

1
2
3
4
5
AB
C
ABC
AB
C

贪婪模式

正则表达式一般趋向于最大长度匹配,也就是所谓的贪婪匹配。

上面的意思就是如果有多个可能都能匹配,则我尽可能的匹配更多的字符串。

实战–去除html中的html标签

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
 <ul class="dropdown-menu">
<li><a href="#" class="dropdown-header">业务功能</a>
</li>
<li><a href="#">信息建立</a>
</li>
<li><a href="#">信息查询</a>
</li>
<li><a href="#">信息管理</a>
</li>
<li role="separator" class="divider"></li>
<li><a href="#" class="dropdown-header">系统功能</a>
</li>
<li><a href="#">设置</a>
</li>
</ul>

对于上面的html标签,我们去除html标签,第一步是能够匹配到对应的标签,我们知道html标签是以“<”开始,并以“>”结束。

  1. 确定“<[**一堆任意的正则]**>”的正则形势

  2. 在“**一堆任意的正则”**正则中,可以是任意的字符,比如

    1
    2
    3

    <a href="blog.laofu.online">付威的网络博客</a>

  3. 根据2的分析,我们找到正则中可以代表所有字符的元字符“.”,则字符可以表示成<.+>

  4. 对于<.+> 这个正则来说,可以找到对应的html标签,但我们同样遇到了意想不到的结果,因为正则贪婪的模式的原因,匹配的结果也包含了我们想要的汉字

  5. 对于以上的问题,我们可以考虑把 “一堆任意的正则” 换成 “一堆不包含Html标签的正则”

  6. 针对上面的分析 我们可以把正则改成 <[^<>]+>

最终的匹配结果:

正则表达式

相关资料


表达式全集

字符 描述
\ 将下一个字符标记为一个特殊字符、或一个原义字符、或一个向后引用、或一个八进制转义符。例如,“n”匹配字符“n”。“\n”匹配一个换行符。串行“\\”匹配“\”而“\(”则匹配“(”。
^ 匹配输入字符串的开始位置。如果设置了RegExp对象的Multiline属性,^也匹配“\n”或“\r”之后的位置。
$ 匹配输入字符串的结束位置。如果设置了RegExp对象的Multiline属性,$也匹配“\n”或“\r”之前的位置。
* 匹配前面的子表达式零次或多次。例如,zo*能匹配“z”以及“zoo”。*等价于{0,}。
+ 匹配前面的子表达式一次或多次。例如,“zo+”能匹配“zo”以及“zoo”,但不能匹配“z”。+等价于{1,}。
? 匹配前面的子表达式零次或一次。例如,“do(es)?”可以匹配“does”或“does”中的“do”。?等价于{0,1}。
{n} n是一个非负整数。匹配确定的n次。例如,“o{2}”不能匹配“Bob”中的“o”,但是能匹配“food”中的两个o。
{n,} n是一个非负整数。至少匹配n次。例如,“o{2,}”不能匹配“Bob”中的“o”,但能匹配“foooood”中的所有o。“o{1,}”等价于“o+”。“o{0,}”则等价于“o*”。
{n,m} mn均为非负整数,其中n<=m。最少匹配n次且最多匹配m次。例如,“o{1,3}”将匹配“fooooood”中的前三个o。“o{0,1}”等价于“o?”。请注意在逗号和两个数之间不能有空格。
? 当该字符紧跟在任何一个其他限制符(*,+,?,{n},{n,},{n,m})后面时,匹配模式是非贪婪的。非贪婪模式尽可能少的匹配所搜索的字符串,而默认的贪婪模式则尽可能多的匹配所搜索的字符串。例如,对于字符串“oooo”,“o+?”将匹配单个“o”,而“o+”将匹配所有“o”。
. 匹配除“\n”之外的任何单个字符。要匹配包括“\n”在内的任何字符,请使用像“(.|\n)”的模式。
(pattern) 匹配pattern并获取这一匹配。所获取的匹配可以从产生的Matches集合得到,在VBScript中使用SubMatches集合,在JScript中则使用$0…$9属性。要匹配圆括号字符,请使用“\(”或“\)”。
(?:pattern) 匹配pattern但不获取匹配结果,也就是说这是一个非获取匹配,不进行存储供以后使用。这在使用或字符“(|)”来组合一个模式的各个部分是很有用。例如“industr(?:y|ies)”就是一个比“industry|industries”更简略的表达式。
(?=pattern) 正向肯定预查,在任何匹配pattern的字符串开始处匹配查找字符串。这是一个非获取匹配,也就是说,该匹配不需要获取供以后使用。例如,“Windows(?=95|98|NT|2000)”能匹配“Windows2000”中的“Windows”,但不能匹配“Windows3.1”中的“Windows”。预查不消耗字符,也就是说,在一个匹配发生后,在最后一次匹配之后立即开始下一次匹配的搜索,而不是从包含预查的字符之后开始。
(?!pattern) 正向否定预查,在任何不匹配pattern的字符串开始处匹配查找字符串。这是一个非获取匹配,也就是说,该匹配不需要获取供以后使用。例如“Windows(?!95|98|NT|2000)”能匹配“Windows3.1”中的“Windows”,但不能匹配“Windows2000”中的“Windows”。预查不消耗字符,也就是说,在一个匹配发生后,在最后一次匹配之后立即开始下一次匹配的搜索,而不是从包含预查的字符之后开始
(?<=pattern) 反向肯定预查,与正向肯定预查类拟,只是方向相反。例如,“(?<=95|98|NT|2000)Windows”能匹配“2000Windows”中的“Windows”,但不能匹配“3.1Windows”中的“Windows”。
(?<!pattern) 反向否定预查,与正向否定预查类拟,只是方向相反。例如“(?<!95|98|NT|2000)Windows”能匹配“3.1Windows”中的“Windows”,但不能匹配“2000Windows”中的“Windows”。
x|y 匹配x或y。例如,“z|food”能匹配“z”或“food”。“(z|f)ood”则匹配“zood”或“food”。
[xyz] 字符集合。匹配所包含的任意一个字符。例如,“[abc]”可以匹配“plain”中的“a”。
[^xyz] 负值字符集合。匹配未包含的任意字符。例如,“[^abc]”可以匹配“plain”中的“p”。
[a-z] 字符范围。匹配指定范围内的任意字符。例如,“[a-z]”可以匹配“a”到“z”范围内的任意小写字母字符。
[^a-z] 负值字符范围。匹配任何不在指定范围内的任意字符。例如,“[^a-z]”可以匹配任何不在“a”到“z”范围内的任意字符。
\b 匹配一个单词边界,也就是指单词和空格间的位置。例如,“er\b”可以匹配“never”中的“er”,但不能匹配“verb”中的“er”。
\B 匹配非单词边界。“er\B”能匹配“verb”中的“er”,但不能匹配“never”中的“er”。
\cx 匹配由x指明的控制字符。例如,\cM匹配一个Control-M或回车符。x的值必须为A-Z或a-z之一。否则,将c视为一个原义的“c”字符。
\d 匹配一个数字字符。等价于[0-9]。
\D 匹配一个非数字字符。等价于[^0-9]。
\f 匹配一个换页符。等价于\x0c和\cL。
\n 匹配一个换行符。等价于\x0a和\cJ。
\r 匹配一个回车符。等价于\x0d和\cM。
\s 匹配任何空白字符,包括空格、制表符、换页符等等。等价于[ \f\n\r\t\v]。
\S 匹配任何非空白字符。等价于[^ \f\n\r\t\v]。
\t 匹配一个制表符。等价于\x09和\cI。
\v 匹配一个垂直制表符。等价于\x0b和\cK。
\w 匹配包括下划线的任何单词字符。等价于“[A-Za-z0-9_]”。
\W 匹配任何非单词字符。等价于“[^A-Za-z0-9_]”。
\xn 匹配n,其中n为十六进制转义值。十六进制转义值必须为确定的两个数字长。例如,“\x41”匹配“A”。“\x041”则等价于“\x04&1”。正则表达式中可以使用ASCII编码。.
\num 匹配num,其中num是一个正整数。对所获取的匹配的引用。例如,“(.)\1”匹配两个连续的相同字符。
\n 标识一个八进制转义值或一个向后引用。如果\n之前至少n个获取的子表达式,则n为向后引用。否则,如果n为八进制数字(0-7),则n为一个八进制转义值。
\nm 标识一个八进制转义值或一个向后引用。如果\nm之前至少有nm个获得子表达式,则nm为向后引用。如果\nm之前至少有n个获取,则n为一个后跟文字m的向后引用。如果前面的条件都不满足,若nm均为八进制数字(0-7),则\nm将匹配八进制转义值nm
\nml 如果n为八进制数字(0-3),且m和l均为八进制数字(0-7),则匹配八进制转义值nml。
\un 匹配n,其中n是一个用四个十六进制数字表示的Unicode字符。例如,\u00A9匹配版权符号(©)。

常用正则表达式

用户名 /^[a-z0-9_-]{3,16}$/
密码 /^[a-z0-9_-]{6,18}$/
十六进制值 /^#?([a-f0-9]{6}|[a-f0-9]{3})$/
电子邮箱 /^([a-z0-9_\.-]+)@([\da-z\.-]+)\.([a-z\.]{2,6})$/
/^[a-z\d]+(\.[a-z\d]+)*@([\da-z](-[\da-z])?)+(\.{1,2}[a-z]+)+$/
URL /^(https?:\/\/)?([\da-z\.-]+)\.([a-z\.]{2,6})([\/\w \.-]*)*\/?$/
IP 地址 /((2[0-4]\d|25[0-5]|[01]?\d\d?)\.){3}(2[0-4]\d|25[0-5]|[01]?\d\d?)/
/^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/
HTML 标签 /^<([a-z]+)([^<]+)*(?:>(.*)<\/\1>|\s+\/>)$/
删除代码\\注释 (?<!http:|\S)//.*$
Unicode编码中的汉字范围 /^[\u2E80-\u9FFF]+$/

参考资料:

  1. 正则表达式文档

  2. 正则表达式 - 教程

SpringMVC 教程

SpringMVC 概述

   Spring 是目前比较流行的MVC框架,让POJO处理起来变的容易,也支持Rest的Url请求。采用松散的耦合可插拔的接口,比其它MVC接口更具有扩展性和灵活性

maven+spring+Idea 实现helloworld

下面就让我们用maven+Spring+Idea 实现一个 helloWorld的程序(至于环境的搭建可以直接到网上找个教程)

添加Maven项目

  1. 选择maven-archetype-webapp 这个项目类型

    maven创建

  2. 填写GroupId和ArtifactId后直接下一步直到创建完成

    Group

  3. Maven生成的目录如下:

MVN_Index

添加SpringMVC引用

对于MVC的使用,我们首先需要添加对SpringMVC的引用,使用Maven可以方便的实现对jar包的引用和版本的管理。

  1. 添加SpringMVC的引用

    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
        <dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-webmvc</artifactId>
    <version>4.3.2.RELEASE</version>
    </dependency>
    ```



    2. 添加对jsp的页面解析 jstl的引用

    ``` xml
    <dependency>
    <groupId>Javax.servlet</groupId>
    <artifactId>Javax.servlet-api</artifactId>
    <version>3.1.0</version>
    <scope>provided</scope>
    </dependency>
    <dependency>
    <groupId>Javax.servlet</groupId>
    <artifactId>jstl</artifactId>
    <version>1.2</version>
    </dependency>
    ```

    #### 添加SpringMVC配置

    1. 添加Spring的配置文件,修改WEB-INF下面的web.config,添加如下内容

    ``` xml

    <!-- 配置DispatcherServlet -->
    <servlet>
    <servlet-name>spring-mvc</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    <!-- 配置DispatcherServlet ,配置SpringfMVC配置文件的位置和名称-->
    <!--这里可以不用通过contextConfigLocation来配置SpringMVC的配置文件,可以使用默认的配置文件的目录:/WEB-INF/<servlet-name>-servlet.xml-->
    <init-param>
    <param-name>contextConfigLocation</param-name>
    <param-value>classpath:spring-mvc.xml</param-value>
    </init-param>
    <load-on-startup>1</load-on-startup>
    </servlet>
    <!--对应的Mapping-->
    <servlet-mapping>
    <servlet-name>spring-mvc</servlet-name>
    <url-pattern>/</url-pattern>
    </servlet-mapping>
  2. Spring文件配置MVC,在resources文件夹下面添加对应的spring-mvc.xml,添加如下内容:

    <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
        <property name="redirectContextRelative" value="true"></property>
        <property name="prefix" value="/WEB-INF/views/"></property>
        <property name="suffix" value=".jsp"></property>
    </bean>
    <!--配置扫描的包-->
    <context:component-scan base-package="Controller"></context:component-scan>
    

添加Controller和views

  1. 在main文件夹下添加Java目录,并标记为SourceRoot

    SPRING_MarkAs

  2. 添加Controller包,添加一个Controller代码:

    @Controller
    public class HelloController {  
        
        @RequestMapping("/Hello")
        public String Hello(){
            return "index";
        }
        
    }
    
  3. 添加views文件夹,新建一个index.jsp页面

    <html>
    <body>
    <h2>Hello World!</h2>
    </body>
    </html>
    

整体的项目的目录结构如下:

Contact

配置Tomcat

Tomcat

运行 Hello World

Run

HelloWorld运行的过程

当我们在浏览器中发送一个Hello的请求,会被servlet-mapping所拦截,根据url的匹配格式跳转到指定的Controller,返回对应的值index值.

返回的值,会被指定的视图解析器解析为指定的物理的视图。对于 InternalResourceViewResolver 视图解析器,会做如下的的解析:

prefix+returnVal+suffix

这样的方式解析到指定的物理视图.


RequestMapping修饰方法


RequestMapping修饰方法

在上面的Demo中,我们用RequestMapping来修饰对应Controller中对应的方法,来说明当前的方法是这了响应Hello的请求。

RequestMapping的Value支持Ant通配符

在**@RequestMapping(“/Hello”)**映射中,我们让其匹配的是/Hello的url地址,RequestMapping也支持Ant通配符,具体的内容如下:

Ant 风格资源地址支持 3 种匹配符:

  • ?:匹配文件名中的一个字符
  • *:匹配文件名中的任意字符
  • 匹配多层路径

@RequestMapping 还支持 Ant 风格的 URL:

  • /user/*/createUser: 匹配
  • /user/aaa/createUser、/user/bbb/createUser 等 URL
  • /user/**/createUser: 匹配 –
  • /user/createUser、/user/aaa/bbb/createUser 等 URL
  • /user/createUser??: 匹配 –
  • /user/createUseraa、/user/createUserbb 等 URL

如果我们修改上述代码为:

  @RequestMapping("/Hello/*/123")
    public String Hello(){
        return "index";
    }

则使用:
http://localhost:8080/SpringMVC/Hello/myMvc/123
http://localhost:8080/SpringMVC/Hello/myMvc1231/123
….
都可以访问到Hello方法

RequestMapping修饰类

对于上面的Demo我们可以在HelloController上面添加RequestMapping来指定访问url的前缀的路径:

@RequestMapping("/SpringMVC")
@Controller
public class HelloController {

    @RequestMapping("/Hello")
    public String Hello(){
        return "index";
    }

}

如果Controller没有修复Request的修饰,则代表的是web的根目录。

RequestMapping的请求方式

RequestMapping可以指定请求的方式,demo如下:

@RequestMapping(value = "GetName",method = RequestMethod.GET)
public String GetName(){
    return "success";
}

@RequestMapping(value = "PostName",method = RequestMethod.POST)
public String PostName(){
    return "success";
}

修改Index的页面和添加一个success页面

<html>
<body>
<a href="/SpringMVC/GetName">GetName</a>
<br>
<a href="/SpringMVC/PostName">PostName</a>
</body>
</html>   

Post页面的请求结果 :

测试Post

RequestMapping 指定Header和Params

RequestMapping支持对参数和Header的定义,可以支持简单的表达式:

  • param1: 表示请求必须包含名为 param1 的请求参数
  • !param1: 表示请求不能包含名为 param1 的请求参数
  • param1 != value1: 表示请求包含名为 param1 的请求参数,但其值
    不能为 value1
  • {“param1=value1”, “param2”}: 请求必须包含名为 param1 和param2
    的两个请求参数,且 param1 参数的值必须为 value1
@RequestMapping(value ="TestParamsAndHeaders",method = RequestMethod.GET,params = {"userName","age!=10"},headers = {"Accept-Language:zh-CN,zh;q=0.8,en;q=0.6"})
    public String TestParamsAndHeaders(){
        return "success";
}
<a href="/SpringMVC/TestParamsAndHeaders?userName=fuwei&age=11">TestParamsAndHeaders1</a><!--可以访问 -->
<br>
<a href="/SpringMVC/TestParamsAndHeaders?userName=fuwei&age=10">TestParamsAndHeaders2</a><!--不可以访问 -->
<br>
<a href="/SpringMVC/TestParamsAndHeaders?loginName=fuwei&age=10">TestParamsAndHeaders3</a><!--不可以访问 -->

上面的方法的映射要求是:必须要有userName参数,age!=10,且只接受zh-CN的语言的请求,如果修改上面的header中的accept的语言,则都无法请求。使用params和header可以更加精确的映射请求。

PathVariable注解

通过 @PathVariable 可以将 URL 中占位符参数绑定到控制器处理方法的入参中:URL 中的 {xxx} 占位符可以通过@PathVariable(“xxx”) 绑定到操作方法的入参中。这个能使得SpringMVC可以支持REST风格(关于Rest)。

@RequestMapping("/GetNameById/{id}")
public String GetNameById(@PathVariable("id") Integer id){
    System.out.println(id);
    return "success";
}

在浏览器中访问:

http://localhost:8080/SpringMVC/GetNameById/123123

可以在控制台打印出:123123


SpringMVC 获得请求参数方式


使用 @RequestParam

RequestParam来映射对应的参数,它具有3个属性:

  • value : 当前参数的值

  • require: 是否必须,默认是true

  • defalutValue: 默认值

@RequestMapping("/TestRequestParam")
public String TestRequestParam(@RequestParam(value = "userId",defaultValue = 0,required = true) int uid){
    System.out.println(uid);
        return "success";
}

访问:http://localhost:8080/SpringMVC/TestRequestParam?userId=123 会在控制台打印出123

POJO 参数传递

对于表单提交来说,可能会有多字段,如果都使用@RequestParam则会比较麻烦。
针对这个问题我们可以使用POJO的方法进行传递 ,
Spring MVC 会按请求参数名和 POJO 属性名进行自动匹配,自动为该对象填充属性值。也可以使用级联属性。
如:userEx.dept.deptId、dept.address.tel 等

@RequestHeader 与@RequestCookie 注解

(其中的属性值与RequestParam 相同,不再赘述~~)


@RequestMapping("/TestRequestHeader")
public String TestRequestHeader(@RequestHeader(value = "Accept-Language") String lan){
    System.out.println(lan);
    return "success";
}

访问:http://localhost:8080/SpringMVC/TestRequestHeader 打印出指定的语言版本:

zh-CN,zh;q=0.8,en;q=0.6

@RequestMapping("/TestRequestCookie")
public String TestRequestCookie(@CookieValue(value = "JSESSIONID") String sid){
    System.out.println(sid);
    return "success";
}

访问:http://localhost:8080/SpringMVC/TestRequestCookie 打印出Cookie中的JSESSIONID。


@RequestMapping(value = "/TestPojo",method = RequestMethod.POST)
public String TestPojo(User user){
    ObjectMapper map=new ObjectMapper();
    try {
        System.out.println(map.writeValueAsString(user));
    } catch (IOException e) {
        e.printStackTrace();
    }
    return "success";
}

对应的html代码:

<form action="/SpringMVC/TestPojo" method="post">
    <input name="UserName"/><br><br>
    <input name="UserMail"/><br><br>
    <input name="Dept.DeptId"/><br><br>
    <input name="Dept.Addr.Povince"/><br><br>
    <input name="Dept.Addr.City"/><br><br>
<input type="submit" value="Submit">
</form>

填写信息打,在后台印出:

{
    "userName": "username",
    "dept": {
        "addr": {
            "povince": "shanghai",
            "city": "changning"
        },
        "deptId": 10
    },
    "userMail": "userMail"
}

Servlet原生的API参数

SpringMVC支持以下类型Servlet参数 :

  • HttpServletRequest
  • HttpServletResponse
  • HttpSession
  • Java.security.Principal
  • Locale
  • InputStream
  • OutputStream
  • Reader
  • Writer
@RequestMapping(value = "/TestServletApi")
public String TestServletApi(HttpServletRequest request, HttpServletResponse response){
    try {
        System.out.println("TestServletApi HttpServletRequest:"+request.getRequestURL());
        response.getWriter().write("<h1>Hello Servlet<h1/>");
        response.getWriter().close();
    } catch (IOException e) {
        e.printStackTrace();
    }
    return  "success";

}

访问:http://localhost:8080/SpringMVC/TestServletApi

控制台打印: http://localhost:8080/SpringMVC/TestServletApi

浏览器返回:Hello Servlet

helloServlet

博客添加浮动目录

一直都想给自己的博客添加一个浮动的目录,在网上也找也几个,从易用性方面都不是太理想,所以今天才有了想法自己去写一个插件 。

需求


1. 当打开博客的时候在右下角自动生成对应的目录  

2. 支持拖拽移动  

3. 可以点击展开和收缩  (目前未实现)

易用性方面,希望能够直接引用 js后,来执行一句代码来完成对应的动作 。

实现逻辑


  1. 读取页面的所有h1,h2,h3,h4,h5

  2. 根据对应的元素和排序,生成对应的数据,格式如下:

     [
       {
           text: "目录",
           level: 2,
           achorName: "目录",
           order: 1,
           chapterIndex: "1"
       }, {
           text: "UTF8的出现",
           level: 3,
           achorName: "utf8的出现",
           order: 6,
           chapterIndex: "1.5"
       }
    ]
    
  3. 根据数据生成对应的html

CataLog

相关使用

代码地址:ICatalogJs

使用时候只需要引用js后,执行init方法:

<script type="text/Javascript">
	catalog.init();
</script>

本篇对应的效果可以看右下角

字符编码探密--ASCII,UTF8,GBK,Unicode

目录

ASCII 的由来

在计算机的“原始社会”,有人想把日常的使用的语言使用计算机来表示, 我们知道在计算机的世界里面,只有0和1,为了解决尽量多的去表示字符,最终他们决定用8位0和1(一个字节)来表示字符,并且规定当机器读到这几个数据的时候,做出动作或者打印出指定的字符:

遇上0001 0000, 终端就换行;				

遇上0000 0111, 终端就向人们嘟嘟叫;		

遇上‭‭0001 1011‬, 打印机就打印反白的字,或者终端就用彩色显示字母。

这样就形成了最早期的ASCII码表{:target=”_blank”},并把小于32的字符称为**“控制字符”**,剩下的继续进行编写,一直到127个字符,这样一套完整的字符方案就完成了,终于可以把文字搬到计算机中了。

  当大家都在兴奋的可以在电脑的阅读的文章的时候,新的问题又出现了,随着计算机的普及,很多国家都使用了计算机,原来的ASCII码在使用英语的国家可以无障碍的使用,但到了其它国家就无法满足要求了,所以他们决定对后面没有用到的编码(128-255)表示自己国家的语言,并加上了其它的相关的符号,直到编码空间被全部用完,从128-255的字符集称为“ASCII的扩展字符集”。

汉字怎么办?

  等到中国人使用电脑的时候,发现已经没有编码供我们来存储对应的汉字了,连扩展的空间都已经被全部占满了。所以我们的前辈们用自己的智慧创造性提出了新的编码格式:


 1. 去除127之外的乱七八糟的字符串和符号    

 2. 如果一个字节且小于127号的字符,与原ASCII码意义相同    

 3. 如果有两个同时大于127的字符则表示一个汉字,所以就会有一个字符会占用两个字节的说法   

  由于汉字占用两个字节,为了方便区别,我们把前面的字节称为“高位”(从161-247),后面的为“低位”(161-254),这样的一个搭配能表示出大概7000多个数字, 这个编码把所有用到的数字,符号,标点等等都编写进去,其中也有包括对ASCII码的重写,这样就会有我们常说的全角和半角的符号之分。这样的编码格式就是我们常说的“GB2312

GB2312对照表{:target=”_blank”}

博大精深的文化带来的编码麻烦

  当我们庆幸我们的汉字可以在计算中进行传递时,却发现GB2312编码小小的内存空间无法表示“博大精深文化” 需求,很多生僻字无法打印。所以不得不对现有的编码格式就行扩容,在剩余高位和低位的空间中寻找还没有使用的编码,但后来发现还是不够用,因为我们还有繁体字。

  经过几次折腾,最终决定放弃对低位的限制,不再要求低位是大于127的值,一旦发现高位是大于127的就认为是汉字,这样又多了大概20000个汉字的表示,这种扩展的编码格式称为:GBK2312

   再后来我们发现光有汉字是无法把“中国的文字” 全部表示出来,我们还要表示其它民族的文字,所以又对原来的编码格式进行扩展,最终形成编码:GB18030

Unicode的诞生

随着计算机的普及,世界很多国家和很多民族都使用了计算机,各种编码格式也越来越多。随着相互之间的文字交流也越来越频繁,编码格式是成为了主要的障碍,是时候是时候需要一个更为兼容性强大的的编码格式来一统天下,这个编码就是Unicode{:target=”_blank”},创建它的是就是ISO(国际标谁化组织)。ISO当然解决这个问题也比较简单粗暴:


 1. 废除所有地区性的编码    

 2. 重新制定一个能够包含地球上所有的编码     

  在Unicode的制定的时候,硬件已经不再那么的昂贵,所以就大气的把原来的使用一个字节表示(8位),改用成两个字符(16位)表示,其它的字符统一编码,这样理论上一共最多可以表示2^16(即65536)个字符,基本满足各种语言的使用。实际上当前版本的统一码并未完全使用这16位编码,而是保留了大量空间以作为特殊使用或将来扩展。这种大气的方式,虽然统一的编码方式,但也同样带来了问题,由于扩展的字节数,使用原来能用一个字节表示的字母和半角字符的高位都是0,存储空间比原来多了一倍。同样Unincode中,所有的字符都是都是两个字节 而汉字也由原来的两个字符转成了一个字符 –(更多的实现方式不属于此文的讨论之内)


这里要注意字符与字节的区别,在ASCII码的代码,一个字节是8位,用一个字节来表示一个字符,而汉字用两个字节来表示,所以有汉字可以作为两个字符。

而在Unicode时代,一个字节还是8位,但是所有的字符表示都是用两个字节,所以汉字也就算成一个字符     
  上述16位统一码字符构成基本多文种平面。最新(但未实际广泛使用)的统一码版本定义了16个辅助平面,两者合起来至少需要占据21位的编码空间,比3字节略少。但事实上辅助平面字符仍然占用4字节编码空间,与UCS-4保持一致。未来版本会扩充到ISO 10646-1实现级别3,即涵盖UCS-4的所有字符。UCS-4是一个更大的尚未填充完全的31位字符集,加上恒为0的首位,共需占据32位,即4字节。理论上最多能表示2^31个字符,完全可以涵盖一切语言所用的符号。

相关资料:Unicode字符平面映射{:target=”_blank”}


半角字符“A”的表示方法:       

ASCII码: 01000001    

Unicode编码:00000000 01000001       

汉字“付”的表示方法    

GBK编码:‭10111000‬ 10110110‬    

Unicode编码:01001110‬ ‭11011000‬

UTF8的出现

伴随着Unicode的缺点的出现,特别是到互联网的出现,为解决unicode如何在网络上传输的问题,可谓各种实现方法都出现了,具有代表性的就是UTF8和UTF16。这里强调:他们的关系是UTF8和UTF16是Unicode的一种实现方式。

那UTF8又是如何表示字符的呢? UTF8的最大的特点是可伸缩性,具体的编码规则:

1. 对于一个单字节字符,而且首位为0 ,其余的用Unicode补齐。所以对于ASCII内的字符,ASCII的编码与UTF8相同。     

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

还是以“付”为例:


汉字“付”的表示方法    

GBK编码:‭10111000‬ 10110110‬    

Unicode编码:01001110‬ ‭11011000‬   

UTF-8编码:11100100 10111000 10100101

在读取UTF编码的时候,我们只需要读取前面几位有几个1 ,就可以清楚的知道,当前字符占用了几个字节。

至于编码的转换,如果理解了原理就很简单了,如果有机会我会写出如何手动去转换字符编码 –(如果我还有动力的写下篇的话…)

.net代码实现一个字符编码转换,这里转出来是二进制的数据,其它格式可以自己扩展:

  public static void Main(string[] args)
  {
      ConvertEncode("Unicode", "GBK", "付");
      Console.Read();
  }

  private static void ConvertEncode(string tarEncode, string desEncode, string content, string defaultEncode = "Unicode")
  {
      var byteArr = Encoding.GetEncoding(defaultEncode).GetBytes(content);
      var str = Encoding.Convert(Encoding.GetEncoding(tarEncode), Encoding.GetEncoding(desEncode), byteArr);
      foreach (var b in str)
      {
          Console.Write(Convert.ToString(b, 2));
      }
  }

【参考资料】

  1. 维基百科-Unicode{:target=”_blank”}

  2. 阮一峰的网络日志-字符编码笔记:ASCII,Unicode和UTF-8{:target=”_blank”}

  3. ASCII、Unicode、GBK和UTF-8字符编码的区别联系{:target=”_blank”}

Your browser is out-of-date!

Update your browser to view this website correctly.&npsb;Update my browser now

×