实现自己的数据库驱动——Packet的read(九)

前言

这篇主要用代码去实现,如何将Server端反馈回来的包,进行解析。如下图,完成三次握手之后,服务端发送了一个handshake packet。我们要做的是定义好handshake的类,以及实现将字节到类的转化。

你想输入的替代文字

Handshake类的实现

简单的看一下Handshake类的成员

1
2
3
4
5
6
7
8
9
10
11
public class HandshakePacket extends MysqlPacket {

public byte protocolVersion;//版本协议
public byte[] serverVersion;//版本号
public long threadId;//执行的线程号
public byte[] seed;//用于后期加密的salt1
public int serverCapabilities;//通信的协议
public byte serverCharsetIndex;//编码格式
public int serverStatus;//服务端的状态
public byte[] restOfScrambleBuff;//这个其实就是seed2
}

MysqlPacket是所有包的基础类,含有包的长度以及包的序列号。read和write是其两个抽象方法。这篇主要是讲解Packet如何根据数据类型来实现read。

1
2
3
4
5
6
7
public abstract class MysqlPacket {
public int packetLength;
public byte packetId;

public abstract void read(byte[] data);
public abstract void write(ByteBuffer buffer);
}

Packet Read实现的思路

首先来看packetLength是如何获取的。在前面的博客中我们得知,该数据是占用三个字节的。所以有下面的方法:

1
2
3
4
5
6
7
8
9
10
11
/**
* 为什么使用0xff,在前一篇博客中提到,当低位的数据byte向高位的数据进行转化的时候
* 要考虑到二进制的唯一性。
**/
public int readUB3() {
final byte[] b = this.data;//data是服务端发送给我们的handshark字节数组
int i = b[position++] & 0xff;//position表示当前字节的读取位置,初始值位0
i |= (b[position++] & 0xff) << 8;
i |= (b[position++] & 0xff) << 16;
return i;
}

同理像packetId、protocolVersion只用一个字节就可以存放的,就可以用下面的方法:

1
2
3
public byte read() {
return data[position++] ;
}

有一个要注意的是如果某个字段占用较大的字节数,如threadID该字段需要占用了四个字节。如果用int来存储就会有问题。因为已经表明了该字段需要四个字节,虽然int是四个字节,但是其最高位是表示符号的。所以int来放最多只能表示3个字节+7位。

所以为了溢出需要用long来存储。

1
2
3
4
5
6
7
8
public long readUB4() {
final byte[] b = this.data;
long l = (long) (b[position++] & 0xff);
l |= (long) (b[position++] & 0xff) << 8;
l |= (long) (b[position++] & 0xff) << 16;
l |= (long) (b[position++] & 0xff) << 24;
return l;
}

特殊的字段还有salt,根据它存储格式描述来看,它是以null为结尾的数据,所以实现如下:

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
public byte[] readBytesWithNull() {
final byte[] b = this.data;
int offset = -1;
for (int i = position; i < length; i++) {//用来存放找到的第一个null位置
if (b[i] == 0) {
offset = i;
break;
}
}
switch (offset) {//根据null的位置进行操作
case -1:
byte[] ab1 = new byte[length - position];
System.arraycopy(b, position, ab1, 0, ab1.length);
position = length;
return ab1;
case 0:
position++;
return EMPTY_BYTES;
default://通常是进入到这个方法体中
byte[] ab2 = new byte[offset - position];
System.arraycopy(b, position, ab2, 0, ab2.length);
position = offset + 1;
return ab2;
}
}

特殊的字段还有Auth Packet中的password,其数据格式为LengthEncodedString,即在password之前有一个表示该字段的长度。

使用LengthEncodedInteger编码的整数可能会使用1, 3, 4, 或者9 个字节,具体使用字节取决于数值的大小,下表是不同的数据长度的整数所使用的字节数:

最小值(包含) 最大值(不包含) 存储方式
0 251 1个字节
251 2^16 3个字节(0xFC + 2个字节具体数据)
2^16 2^24 4个字节(0xFD + 3个字节具体数据)
2^24 2^64 9个字节(0xFE + 8个字节具体数据)

其read方法如下:

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
public byte[] readBytesWithLength() {
int length = (int) readLength();//获取长度的方法
if (length == NULL_LENGTH) {
return null;
}
if (length <= 0) {
return EMPTY_BYTES;
}
byte[] ab = new byte[length];
System.arraycopy(data, position, ab, 0, ab.length);
position += length;
return ab;
}

public long readLength() {//根据读到的一个字节值,来判断实际的长度,
int length = data[position++] & 0xff;
switch (length) {
case 251:
return NULL_LENGTH;
case 252:
return readUB2();
case 253:
return readUB3();
case 254:
return readLong();
default:
return length;
}
}

还有如Auth packet中的Username是一个NullTerminatedString格式的数据,其read方法如下:

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
public String readStringWithNull() {//实现和readBytesWithNUll其实是类似的
final byte[] b = this.data;
if (position >= length) {
return null;
}
int offset = -1;
for (int i = position; i < length; i++) {
if (b[i] == 0) {
offset = i;
break;
}
}
if (offset == -1) {
String s = new String(b, position, length - position);
position = length;
return s;
}
if (offset > position) {
String s = new String(b, position, offset - position);
position = offset + 1;
return s;
} else {
position++;
return null;
}
}