警告:下面内容太干了,有条件的大佬自带润滑油

1. 业务场景

业务开发中经常需要根据一些数据变更实现相对应的操作。例如,用户登录P站,购买了一件商品,这时候需要去加积分,积分加完通知钱包扣款,扣款完成后通知短信服务发送短信给用户表示成功(后续的业务可能会进行用户的数据统计等等),如果这时候公司老板需要统计用户的消费信息,月度积分积累,以及用户年月日的消费情况,这时候怎么办?

mysql 关闭safemode mysql关闭slave_mysql 关闭safemode

2. 技术选型

2.1 业务系统

第一种方案直接通过业务系统进行调用,每个业务系统都通过接口进行调用统计中心,将自身的业务数据发送给统计服务,这种方案直接跟业务系统耦合了,而且随着产品的增长,代码量会越来越多,后续人员的维护会非常困难

mysql 关闭safemode mysql关闭slave_数据库_02

2.2 canal

mysql 关闭safemode mysql关闭slave_java_03

mysql 关闭safemode mysql关闭slave_java_04

2.2.1 简介

canal,译意为水道/管道/沟渠,主要用途是基于 MySQL 数据库增量日志解析,提供增量数据订阅和消费

2.2.2 工作原理

  • canal 模拟 MySQL slave 的交互协议,伪装自己为 MySQL slave ,向 MySQL master 发送dump 协议
  • MySQL master 收到 dump 请求,开始推送 binary log 给 slave (即 canal )
  • canal 解析 binary log 对象(原始为 byte 流)

2.2.3 优点

apache顶级开源框架,功能比较完备,提供了数据转发MQ的功能,以及客户端订阅功能

2.2.4 弊端

canal采用的使用 C/S 架构,会在服务器上单独部署服务端需要进行维护,如果出现服务端宕机就会导致所有的客户端都接收不了mysql的事件,而且比较重,对于后期的维护难度会非常高

2.3 mysql-binlog-connector

2.3.1 简介

github上面提供的一个开源mysql协议slave封装库,主要用于发起slave请求 https://github.com/osheroff/mysql-binlog-connector-java

2.3.2 工作原理

  • canal 模拟 MySQL slave 的交互协议,伪装自己为 MySQL slave ,向 MySQL master 发送dump 协议
  • 解析对应的事件

2.3.3 优点

非常轻量级,java开发直接可以嵌入到自己的微服务当中

2.3.4 弊端

太轻了,以至于这个组件就只有封装 slave 请求以及最基本的事件封装,其余什么都没有做,功能都需要自己进行开发

3. 协议扫盲

3.1 什么是协议?

协议,网络协议的简称,网络协议是通信计算机双方必须共同遵从的一组约定。如怎么样建立连接、怎么样互相识别等。只有遵守这个约定,计算机之间才能相互通信交流。它的三要素是:语法语义时序

3.2 mysql协议

客户端与服务端交互协议:https://dev.mysql.com/doc/internals/en/client-server-protocol.html

3.2.1 基本数据类型

在mysql协议中,会有以下三种类型用于定义协议的结构,根据对应的数据类型可以解析出对应协议的格式

  • Integer Types:
  • 定长整数类型:例如:int<3> 表示3个字节长度的整型,字节存储就为 01 00 00
  • 长度编码整数类型 int<lenenc>:例如:>= 251 && < 2^16,字节格式存储为 fc+2字节整数,如果要解码长度编码需要判断第一个字节的类型;如果小于 251 只需要 1个字节的整数
  • String Types:
  • 固定长度字符串 string(fx) :已经知道字符串的长度
  • 0结尾的字符串 string(NUL):从数据开始一直读取到 [00] 字节结尾
  • 可变长字符串 string(var) : 通过解析其它字段获取到当前字符串的长度
  • 以长度编码整数为前缀的字符串 string(lenenc):首位是长度编码整数类型,用于定义字符串的长度
  • 字符串数据包是最后的组成部分 string(EOF) :长度为整个数据包长度减去当前位置来计算
  • Describeing Packets:数据包描述协议,用于描述协议包的类型
3.2.2 mysql数据包

客户端和服务端以最大 16MByte 大小的数据包进行交换,下面是数据包头的结构:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bySo0Gmq-1661327746629)(images/1659585504785.png)]

COM_QUIT 断开连接包例子:

字节包

说明

01 00 00 00 01

长度:1

sequence_id:00

协议数据包:0x01

3.2.3 响应包
  • OK_Packet:标识命令执行成功的数据包
数据类型   				说明
int<1>    			  包类型:[00]代表OK包、[fe]代表EOF包
int<lenenc>		      受影响的行数
int<lenenc>           最后插入的id数

下面通过if else进行判断客户端是否支持的能力

if (capabilities & CLIENT_PROTOCOL_41) {
int<2>                状态标识
int<2>				  告警计数器

} else if(capabilities & CLIENT_TRANSACTIONS) {
int<2>                状态标识

}

if status_flags & SERVER_SESSION_STATE_CHANGED {
 string<lenenc>	      会话状态改变信息
   if  状态标识 & SERVER_SESSION_STATE_CHANGED {
   		string<lenenc>  会话状态信息
   }
} else {
string<EOF>           剩余数据就是状态信息
}
  • ERR_Packet:错误包
数据类型   				说明
int<1>    			  包类型:[ff]代表错误包
int<2>				  错误码
if capabilities & CLIENT_PROTOCOL_41 {
  string[1]			  sql状态标记
  string[5]		      sql标记
}
string<EOF>           剩下所有的错误信息
  • EOF_Packet:在MYSQL C/S 协议中,EOF和OK的作用是相同的
数据类型   				说明
int<1>    			  包类型:[fe]
if capabilities & CLIENT_PROTOCOL_41 {
  int[2]			  警告数量
  int[2]		      状态标记
}
string<EOF>           剩下所有的错误信息
  • Status Flags:https://dev.mysql.com/doc/internals/en/status-flags.html 状态值说明

3.3 连接生命周期

在mysql中连接生命周期有两个阶段,一个是连接阶段,一个是命令阶段

3.3.1 连接阶段(Connection Phase)
初始握手方式

初始握手方式在mysql中目前有两种

  • 普通握手(5点几的版本使用普通握手的方式进行)
  • 服务端发送初始握手包
  • 客户端响应握手包
  • SSL握手(目前mysql8以上的版本都使用的是TLS1.2进行的加密握手)
  • 服务端返回初始握手包
  • 客户端返回 SSL 连接请求包
  • SSL交换连接进行SSL连接
  • 客户端发送握手响应包
连接阶段数据包

https://dev.mysql.com/doc/internals/en/connection-phase-packets.html

客户端创建连接时服务端返回的版本号是:HandshakeV10

长度            说明
1              [0a] protocol version   			#协议版本号 V10
string[NUL]    server version          			#mysql服务端采用的版本号
4              connection id           			#当前连接的id
string[8]      auth-plugin-data-part-1			#加密数据
1              [00] filler						#填充物
2              capability flags (lower 2 bytes) #能力标识符低2位
  if more data in the packet:					#下面就是具体数据包数据集
1              character set					#采用的编码集
2              status flags						#状态标识符
2              capability flags (upper 2 bytes) #扩展服务端能力
if capabilities & CLIENT_PLUGIN_AUTH {
1              length of auth-plugin-data       #验证插件的长度
} else {
1              [00]
}
string[10]     reserved (all [00])              #保留位全部是 [00]
 if capabilities & CLIENT_SECURE_CONNECTION {
string[$len]   auth-plugin-data-part-2 ($len=MAX(13, length of auth-plugin-data - 8))
  
  if capabilities & CLIENT_PLUGIN_AUTH {
string[NUL]    auth-plugin name					#验证插件的名称插件名称
  }

注意点:capability flags整个由4个字节组成,上面包中将能力标识分成高低位进行传输,下面是详细的客户端能力标识位,协议中涉及到的标识位

  • CLIENT_PLUGIN_AUTH:0x80000
  • CLIENT_SECURE_CONNECTION:0x8000

https://dev.mysql.com/doc/internals/en/capability-flags.html#packet-Protocol::CapabilityFlags

下面是创建mysql连接时,服务端返回的初始数据包

mysql 关闭safemode mysql关闭slave_mysql_05

SSLRequest

上面在采用初始握手之后,如果采用SSL进行连接,客户端需要按照下面的包进行返回,目的是将普通的握手升级为SSL

4              capability flags, CLIENT_SSL always set
4              max-packet size
1              character set
string[23]     reserved (all [0])

上面包的目的是将普通连接,升级为SSL连接,下面的包需要在上面发送之后再发送认证包

4              capability flags
4              max-packet size
1              character set
string[23]     reserved (all [0])
string<NUL>    username用户名设置后的0结尾
string<lenenc> password通过对应的插件加密密码
string<NUL>    schema数据库名称,如果不为空的话(数据库为空,可以不写这个值)
string<NUL>    plugin name 插件名称

能力标识:

  • CLIENT_SSL:0x0800
握手返回数据包

在mysql中为了兼容新老协议的客户端握手响应,使用了两种协议进行区分

  • HandshakeResponse41
长度				说明
4              capability flags, CLIENT_PROTOCOL_41 always set  #客户端 的能力标识
4              max-packet size                                  #包最大的长度
1              character set                                    #编码方式
string[23]     reserved (all [0])                               #填充符
string[NUL]    username                                         #用户名称以0结尾

  if capabilities & CLIENT_PLUGIN_AUTH_LENENC_CLIENT_DATA {     #根据客户端能力进行写入
lenenc-int     length of auth-response                          
string[n]      auth-response
  } else if capabilities & CLIENT_SECURE_CONNECTION {
1              length of auth-response                          #数据长度
string[n]      auth-response								    #数据值
  } else {
string[NUL]    auth-response
  }
  
  if capabilities & CLIENT_CONNECT_WITH_DB {                    #是否直接连接数据库
string[NUL]    database											#数据库名称
  }
  if capabilities & CLIENT_PLUGIN_AUTH {
string[NUL]    auth plugin name                                 #插件名称
  }
  if capabilities & CLIENT_CONNECT_ATTRS {                      #写入扩展字段
lenenc-int     length of all key-values                         #key和values的长度
lenenc-str     key
lenenc-str     value
   if-more data in 'length of all key-values', more keys and value pairs
  }
  • HandshakeResponse320
长度				说明
2              capability flags, CLIENT_PROTOCOL_41 never set   #客户端 的能力标识
3              max-packet size									#包最大的长度
string[NUL]    username
  if capabilities & CLIENT_CONNECT_WITH_DB {
string[NUL]    auth-response                                    #数据
string[NUL]    database											#数据库名称
  } else {
string[EOF]    auth-response                                    #数据
  }
3.3.2 命令阶段(Command Phase)

在命令阶段客户端需要发送一个,序列号为 [00] 用来标识开启了命令阶段,例如:

13 00 00 00 03 53 ...
01 00 00 00 01
            ^^- command-byte (命令类型)
         ^^---- sequence-id == 0  (序列号)

3.4 常用协议

在mysql协议中一共分为20种子协议进行区分以下列举部分例子:

3.4.1 COM_QUIT
请求包

断开连接命令

1              [01] COM_QUIT

例如:01 00 00 00 01   #三个字节的长度,1个字节的序列号,01就代表类型
响应包

OK_Packet

3.4.2 COM_QUERY
请求包
1              [03] 协议类型
string[EOF]    执行的sql语句
响应包

响应包分为4种类型:

  • ERR_Packet
  • OK_Packet
  • LOCAL_INFILE_Reuqest
  • Resultset

mysql 关闭safemode mysql关闭slave_数据库_06

当发起一个查询请求时,会先返回一个 int<lenenc> 类型的整数,表示结果集的数量

payload
    lenenc-int     number of columns in the resultset
  • 如果返回的数据为 0:说明这是一个 OK_Packet 包
  • 如果不是一个有效的 int<lenenc> 类型的数据:说明时一个 ERR_Packet 或者是 INFILE 类型的包

Resultset:结果集由一个定义列数的数据包开始,然后就是一个同样数量的列定义数据包,以一个 EOF 包进行分割(如果客户端没有设置 CLIENT_DEPRECATE_EOF 则为数据包)

  • 包含一个 LengthEncodeInteger 数据包:用来表示列数
  • ColumnDefinition数据包:列定义数据包
  • 根据客户端设置:CLIENT_DEPRECATE_EOF 表示,包含一个 EOF_Packet 包
  • 一个或者多个 ResultsetRow,真正存数据的数据包,每列的数据
  • 如果为空发送 0xfb
  • 其他的所有内容通过 string<lenenc> 进行数据的编码
  • ERR_Packet或者 EOF_Packet 进行结尾

Java示例:上面进行读取时可以通过 EOF_Packet 进行分割读取

private ResultSetRowPacket[] readResult(PacketChannel channel) throws IOException {
    //先读取出列数的数据包,可以不使用,channel.read();读取整个数据包
    byte[] statementResults = channel.read();
    //跳过不是 0xFE 的包,如果是 0xFE数据分割,这里跳过的是列定义的数据包
    while (channel.read()[0] != (byte) 0xFE) { }
    List<ResultSetRowPacket> resultSet = new LinkedList();
    byte[] bytes;
    //读取 ResultSetRow 的数据包,如果读取到 0xFE或者0xFF 就退出
    while ((bytes = channel.read())[0] != (byte) 0xFE) {
        //需要验证是否是0xFF包,错误包
        resultSet.add(new ResultSetRowPacket(bytes));
    }
    return resultSet.toArray(new ResultSetRowPacket[0]);
}
3.4.3 COM_REGISTER_SLAVE

注册从服务协议

请求包
1              [15] 协议id
4              服务id,不能重复
1              从协议的ip地址长度
string[$len]   ip地址
1              用户名长度
string[$len]   从服务用户名
1              密码长度
string[$len]   从服务密码
2              从服务mysql端口
4              可以忽略,副本的排名
4              主mysql的id,通常为o
响应包

OK_Pakcet 或者 ERR_Packet

3.4.4 COM_BINLOG_DUMP

binlog监听协议:

  • 在使用此协议时,8.0之前需要先注册slave用于进行监听
  • 在8.0之后的版本,通过当前协议的标识符,可以直接开启一个网络流,就不需要再注册slave了
请求包
1              [12] 协议id
4              binlog的写位置
2              标识符,在8.0之后可以写入 0x01,表示不阻塞
4              服务id
string[EOF]    binlog文件名称
响应包
  • 一个 binlog 的网络流
  • ERR_Packet
  • 或者设置表示符为0x01后,返回的EOF_Packet
3.4.5 COM_BINLOG_DUMP_GTID

在mysql5.6版本开始的新特性,保证事务id在集群中的唯一性用于故障回复以及容错能力,需要在mysql配置中开启,可以在mysql中通过 select @@gtid_mode; 查询是否开启

请求包
1              [1e] 协议类型
2              标识符,有三个 0x01、0x02、0x04
4              服务id
4              binlog名称长度
string[len]    binlog文件名称
8              binlog写入的位置
  if flags & BINLOG_THROUGH_GTID {
4              数据长度
string[len]    数据
  }

3.5 Binlog Event

3.5.1 头部协议
4              时间戳
1              事件类型:https://dev.mysql.com/doc/internals/en/binlog-event-type.html
4              服务id
4              事件数据长度
   if binlog-version > 1:
4              下一个事件的位置
2              binlog的标识符:https://dev.mysql.com/doc/internals/en/binlog-event-flag.html
3.5.2 事件类型

二进制协议将 NULL 值作为位图进行发送,而不是像前面 ResultsetRow 发送完整字节(查询),如果发送的数据 NULL值比较多,使用 null-bitmap发送更加节省空间

mysql 关闭safemode mysql关闭slave_mysql 关闭safemode_07

mysql 关闭safemode mysql关闭slave_mysql 关闭safemode_08

QUERY_TYPE_EVENT
4              线程id
4              执行的事件
1              数据库名称的长度
2              错误码
  if binlog-version ≥ 4:
2              状态变量长度

string[$len]   状态值变量
string[$len]   数据库名称,通过上面的数据库名称长度决定
1              [00]
string[EOF]    剩下所有的数据为查询语句
TABLE_MAP_EVENT

在进行表写入或者修改时,会发送当前时间用于通知当前表的字段结构以及类型

post-header:
  6              table_id                            #表id
  2              flags

payload:
  1              schema name length                  #数据库名称的长度
  string         schema name                         #数据库名称
  1              [00]
  1              table name length                   #表名长度
  string         table name                          #表名
  1              [00]
  lenenc-int     column-count                        #列的数量
  string.var_len [length=$column-count] column-def   #列定义的数组,每个字段类型一个字节
  lenenc-str     column-meta-def                     #元信息数组,长度是元信息数组的总长度,每个元信息的长度取决于列字段的类型
  n              NULL-bitmask, length: (column-count + 8) / 7 #位图掩码,包含每一列的位图
ROWS_EVENT
  • UPDATE_ROWS_EVENT:修改事件
  • WRITE_ROWS_EVENT:写事件
  • DELETE_ROWS_EVENT:删除事件

以上三个事件,在mysql5.6.x以后的版本使用v2的版本,5.1.15到5.6.x使用v1版本,5.1.10到5.1.15使用v0版本,

这里我使用的是8.0.18以后的版本使用的是v1版本进行解析;

说明: 在mysql每次进行表数据修改时,都会发布一个 TABLE_MAP_EVENT 时间用于通知当前表的结构,其中包含每个字段的类型

header:
6  				     table_id               #6个字节为表id

2                    flags   				#标识符
  if version == 2 {                 		#版本号
2                    extra-data-length		#额外数据的长度
string.var_len       extra-data
  }

body:
lenenc_int           number of columns      #列的数量
string.var_len       columns-present-bitmap1, length: (num of columns+7)/8  #根据列的长度加上7然后除以8获取到列的长度

  if UPDATE_ROWS_EVENTv1 or v2 {
string.var_len       columns-present-bitmap2, length: (num of columns+7)/8
  }

rows:
string.var_len       nul-bitmap, length (bits set in 'columns-present-bitmap1'+7)/8 #返回可能为null的位图(如果是修改,这个字段是修改前的数据)
string.var_len       value的类型值定义在table-map事件中
  if UPDATE_ROWS_EVENTv1 or v2 {
string.var_len       nul-bitmap, length (bits set in 'columns-present-bitmap2'+7)/8  #这个值是修改后的数据索引(标识哪个字段是为null)
string.var_len       value的类型值定义在table-map事件中
  }

在 ROW_EVENT事件中,mysql对null的值的发送采用的是 null-bitmap 进行发送数据,相对于全值发送更加的节省空间,下面是计算的方式

NULL-bitmap-byte = ((field-pos + offset) / 8) #计算字节位

NULL-bitmap-bit = ((field-pos + offset) % 8) #计算位的位

Resultset Row, 9 fields, 9th field is a NULL (9th field -> field-index == 8, offset == 2)

nulls -> [00] [00]

byte_pos = (10 / 8) = 1
bit_pos  = (10 % 8) = 2 #一个字节8位,通过字段跟8进行取莫,可以分布在 0 - 7之间

nulls[byte_pos] |= 1 << bit_pos
nulls[1] |= 1 << 2;  #通过左移异取值

nulls -> [00] [04]

取值的算法:

public void test_bitmap() {
    Integer[] values = {1,2,3,null}; //设置初始索引数据值
    byte[] bytes = new byte[(values.length + 7) >> 3];
    for (int i = 0; i < values.length; i++) {
        if (values[i] != null) { //通过算法设置对应位的数据
            bytes[(i + 2) >> 3] |= (1 << ((i + 2) % 8));
        }
    }
    //通过BitSet获取到对应索引数据
    BitSet set = new BitSet();
    for (int i = 0; i < values.length; i++) {
        if ((bytes[(i + 2) >> 3] & (1 << ((i + 2) % 8))) != 0) {
            set.set(i);
        }
    }
    System.out.println(set.toString());
}

3.6 代码实战

下面是根据 mysql-binlog-connector 进行的一个简单的通过握手然后监听 binlog 的流程实战

3.6.1 connector
  • 开启Socket连接mysql,获取到 mysql server端的响应
  • 解析mysql initial packet
  • 通过SSL发送请求,将socket升级为 TLS 请求
  • 根据返回的加密插件,通过插件对密码进行加密后进行发送
  • 判断是否登录成功
  • 通过 show master status 查询当前 mysql server的 binlog 状态
  • 查询 binlog_checksum 通过4个额外字节做校检应用
  • 查询服务id
  • 发送 binlog dump的消息
  • 监听binlog网络流
public void test_connector() throws IOException {
        Socket socket = new Socket();
        socket.connect(new InetSocketAddress("localhost", 3306));
        //对包连接进行封装一层,初始化连接时,服务端会返回初始的握手包
        PacketChannel channel = new PacketChannel(socket);

        //读取首次发送请求返回的数据信息
        byte[] initialHandshakePacket = channel.read();
        GreetingPacket greetingPacket = new GreetingPacket(initialHandshakePacket);
        System.out.println(greetingPacket);

        SSLRequestCommand sslRequestCommand = new SSLRequestCommand();
        //通过将mysql返回的服务端的支持的各种能力参数设置到 ssl请求的实体中,再调用 toByteArray 会进行计算,详细参数可以查看mysql文档
        sslRequestCommand.setCollation(greetingPacket.getServerCollation());
        //发送ssl请求的参数
        channel.write(sslRequestCommand);
        //将管道连接升级为 SSL进行连接
        channel.upgradeToSSL(SocketFactory.SSL_SOCKET_FACTORY);

        //进行密码的发送,服务端返回的加密插件进行加密
        Command authenticateCommand;
        authenticateCommand = getAuthenticateCommand(greetingPacket);
        channel.write(authenticateCommand);
        //读取响应体
        byte[] authenticationResult = channel.read();
        //判断第1个包是否为0x00
        switch (authenticationResult[0]) {
            case (byte) 0x00:
                // success
                break;
            case (byte) 0xFF:
                // error
                byte[] bytes = Arrays.copyOfRange(authenticationResult, 1, authenticationResult.length);
                ErrorPacket errorPacket = new ErrorPacket(bytes);
                throw new AuthenticationException(errorPacket.getErrorMessage(), errorPacket.getErrorCode(),
                                                  errorPacket.getSqlState());
            case (byte) 0xFE:
                break;
        }
        //验证完成,需要将 packetNumber 置为0开始新一轮的登录
        channel.authenticationComplete();
        //拉取binlog文件,获取到binlog的名称以及position
        channel.write(new QueryCommand("show master status"));
        ResultSetRowPacket[] resultSetRowPackets = readResult(channel);
        ResultSetRowPacket resultSetRow = resultSetRowPackets[0];
        String binlogFilename = resultSetRow.getValue(0);
        long binlogPosition = Long.parseLong(resultSetRow.getValue(1));
        System.out.println("binlog文件名:" + binlogFilename + "binlog位置信息:" + binlogPosition);

        //查询到 binlog_checksum
        channel.write(new QueryCommand("show global variables like 'binlog_checksum'"));
        ResultSetRowPacket[] rowPackets = readResult(channel);
        String upperCase = rowPackets[0].getValue(1).toUpperCase();
        if ("CRC32".equals(upperCase)) {
            channel.write(new QueryCommand("set @master_binlog_checksum= @@global.binlog_checksum"));
            byte[] statementResult = channel.read();
        }
        //设置服务的id
        channel.write(new QueryCommand("select @@server_id"));
        ResultSetRowPacket[] readResult = readResult(channel);
        long serverId = Long.parseLong(readResult[0].getValue(0));

        //发起 binlog的请求
        DumpBinaryLogCommand command = new DumpBinaryLogCommand(serverId, binlogFilename, binlogPosition);
        channel.write(command);

        //获取到对应的输入流进行读取 binlog 的二进制日志流
        ByteArrayInputStream inputStream = channel.getInputStream();
        try {
            while (inputStream.peek() != -1) {
                //读取包的长度
                int packetLength = inputStream.readInteger(3);
                //跳过sequence_id
                inputStream.skip(1L);
                int marker = inputStream.read();
                //兼容数据包是否大于16m
                ByteArrayInputStream byteArrayInputStream = packetLength == 16777215 ?
                                                            new ByteArrayInputStream(readPacketSplitInChunks(inputStream, packetLength))
                                                                                     : inputStream;
                EventHeader header = new EventHeader();
                header.setTimestamp(byteArrayInputStream.readLong(4) * 1000L);
                header.setEventType(byteArrayInputStream.readInteger(1));
                header.setServerId(byteArrayInputStream.readLong(4));
                header.setEventLength(byteArrayInputStream.readLong(4));
                header.setNextPosition(byteArrayInputStream.readLong(4));
                header.setFlags(byteArrayInputStream.readInteger(2));
            }
        } catch (Exception v) {
            System.out.println(v.getMessage());
        }
    }
3.6.2 channel

只是将 socket 进行封装,并且封装了ssl升级的方法以及写入和读取的操作

public class PacketChannel implements Channel {
    //发送包数
    private int packetNumber;
    //是否验证完成
    private boolean authenticationComplete;
    //是否是ssl
    private boolean isSSL;
    //套接字
    private Socket socket;
    //输入流
    private ByteArrayInputStream inputStream;
    //输出流
    private ByteArrayOutputStream outputStream;

    public PacketChannel(String hostname, int port) throws IOException {
        this(new Socket(hostname, port));
    }

    public PacketChannel(Socket socket) throws IOException {
        this.packetNumber = 0;
        this.isSSL = false;
        this.socket = socket;
        this.inputStream = new ByteArrayInputStream(new BufferedSocketInputStream(socket.getInputStream()));
        this.outputStream = new ByteArrayOutputStream(socket.getOutputStream());
    }

    public ByteArrayInputStream getInputStream() {
        return this.inputStream;
    }

    public ByteArrayOutputStream getOutputStream() {
        return this.outputStream;
    }

    public void authenticationComplete() {
        this.authenticationComplete = true;
    }

    public byte[] read() throws IOException {
        //通过读取头部的3个字节确定包的长度,一次性读一个整包
        int length = this.inputStream.readInteger(3);
        int sequence = this.inputStream.read();
        if (sequence != this.packetNumber++) {
            throw new IOException("unexpected sequence #" + sequence);
        } else {
            return this.inputStream.read(length);
        }
    }

    public void write(Command command) throws IOException {
        //发送协议数据
        byte[] body = command.toByteArray();
        ByteArrayOutputStream buffer = new ByteArrayOutputStream();
        //先写包的长度
        buffer.writeInteger(body.length, 3);
        if (this.authenticationComplete) {
            this.packetNumber = 0;
        }
        //写入序列id
        buffer.writeInteger(this.packetNumber++, 1);
        //写入消息
        buffer.write(body, 0, body.length);
        this.outputStream.write(buffer.toByteArray());
        this.outputStream.flush();
    }

    public void upgradeToSSL(SSLSocketFactory sslSocketFactory) throws IOException {
        //通过 SSL工厂,将当前socket套接字升级为 SSL
        SSLSocket sslSocket = sslSocketFactory.createSocket(this.socket);
        sslSocket.startHandshake();
        this.socket = sslSocket;
        //替换原有的输入输出流
        this.inputStream = new ByteArrayInputStream(sslSocket.getInputStream());
        this.outputStream = new ByteArrayOutputStream(sslSocket.getOutputStream());
        this.isSSL = true;
    }

    public boolean isSSL() {
        return this.isSSL;
    }

    public boolean isOpen() {
        return !this.socket.isClosed();
    }

    public void close() throws IOException {
        try {
            this.socket.shutdownInput();
        } catch (Exception var3) {
        }

        try {
            this.socket.shutdownOutput();
        } catch (Exception var2) {
        }

        this.socket.close();
    }
}
3.6.3 握手包
Command

封装协议消息体顶级接口

public interface Command {
    byte[] toByteArray() throws IOException;
}
GreetingPacket

用户读取初始连接mysql服务端后返回的数据包

public class GreetingPacket {
    private int protocolVersion;
    private String serverVersion;
    private long threadId;
    private String scramble;
    private int serverCapabilities;
    private int serverCollation;
    private int serverStatus;
    private int pluginDataLength;
    private String pluginProvidedData;

    public GreetingPacket(byte[] bytes) throws IOException {
        ByteArrayInputStream buffer = new ByteArrayInputStream(bytes);
        //读取协议号,是否采用的 HandshakeV10
        this.protocolVersion = buffer.readInteger(1);
        //读取服务端的版本号,是否以 [00] 结尾
        this.serverVersion = buffer.readZeroTerminatedString();
        //读取4个字节的连接id
        this.threadId = buffer.readLong(4);
        //插件加密数据第一部分,一直读取到 [00]
        String scramblePrefix = buffer.readZeroTerminatedString();
        //服务端能力标识
        this.serverCapabilities = buffer.readInteger(2);
        //服务端采用的编码集
        this.serverCollation = buffer.readInteger(1);
        //服务器状态
        this.serverStatus = buffer.readInteger(2);
        int extendServerCapabilities = buffer.readInteger(2);
        boolean match = (extendServerCapabilities & 0x08) >> 3 == 1;
        if (match) {
            //插件的长度
            this.pluginDataLength = buffer.readInteger(1);
        }
        buffer.skip(10L);
        //读取到加密字符
        if ((this.serverCapabilities & 0x8000) >> 15 == 1) {
            //判断是加密插件的数据是否超过了 13,如果超过了13就直接返回13,减1的目的是去掉后面 [00] 分隔符,因为插件长度只有12个字节
            byte[] bytes1 = buffer.read(Integer.max(13, this.pluginDataLength - 8) - 1);
            String s = new String(bytes1);
            this.scramble = scramblePrefix + s;
            buffer.skip(1L);
        }
        if (match) {
            //插件名称
            this.pluginProvidedData = buffer.readZeroTerminatedString();
        }
    }

    public String toString() {
        return String.format("服务端返回协议版本:[%s]\n" +
                      "Mysql版本号:[%s]\n" +
                      "连接Id:[%d]\n" +
                      "服务端能力标识:[%d]\n" +
                      "编码集Id:[%d]\n" +
                      "服务端状态:[%d]\n" +
                      "Scramble:[%s]\n" +
                      "采用的插件:[%s]",
                      this.protocolVersion,
                      this.serverVersion,
                      this.threadId,
                      this.serverCapabilities,
                      this.serverCollation,
                      this.serverStatus,
                      this.scramble,
                      this.pluginProvidedData);
    }
}
SSLRequestCommand

升级SSL请求协议实体

public class SSLRequestCommand implements Command {
    private int clientCapabilities;
    private int collation;

    public SSLRequestCommand() {
    }

    public void setClientCapabilities(int clientCapabilities) {
        this.clientCapabilities = clientCapabilities;
    }

    public void setCollation(int collation) {
        this.collation = collation;
    }

    public byte[] toByteArray() throws IOException {
        ByteArrayOutputStream buffer = new ByteArrayOutputStream();
        int clientCapabilities = this.clientCapabilities;
        if (clientCapabilities == 0) {
            /**
             * 采用插件的方式进行连接、
             * 使用CLIENT_PROTOCOL_41协议
             * 支持本地插件的验证方式
             * 期望更长的标志
             */
            //翻译成二进制位 10001000001000000100
            clientCapabilities = 557572;
        }
        //采用SSLRequest方式进行连接
        clientCapabilities |= 2048;
        //写入4个字节
        buffer.writeInteger(clientCapabilities, 4);
        buffer.writeInteger(0, 4);
        //客户端的字符集,根据服务端的返回进行选择
        buffer.writeInteger(this.collation, 1);

        //写入23个填充符
        for(int i = 0; i < 23; ++i) {
            buffer.write(0);
        }

        return buffer.toByteArray();
    }
}
QueryCommand

查询协议封装实体

public class QueryCommand implements Command {
    private String sql;

    public QueryCommand(String sql) {
        this.sql = sql;
    }

    public byte[] toByteArray() throws IOException {
        ByteArrayOutputStream buffer = new ByteArrayOutputStream();
        buffer.writeInteger(3, 1);
        buffer.writeString(this.sql);
        return buffer.toByteArray();
    }
}
DumpBinaryLogCommand

开启 binlog 网络流协议实体

public class DumpBinaryLogCommand implements Command {
    private long serverId;
    private String binlogFilename;
    private long binlogPosition;

    public DumpBinaryLogCommand(long serverId, String binlogFilename, long binlogPosition) {
        this.serverId = serverId;
        this.binlogFilename = binlogFilename;
        this.binlogPosition = binlogPosition;
    }

    public byte[] toByteArray() throws IOException {
        ByteArrayOutputStream buffer = new ByteArrayOutputStream();
        buffer.writeInteger(18, 1);
        buffer.writeLong(this.binlogPosition, 4);
        buffer.writeInteger(0, 2);
        buffer.writeLong(this.serverId, 4);
        buffer.writeString(this.binlogFilename);
        return buffer.toByteArray();
    }
}
3.6.4 ResultSetRowPacket

用于读取 ResultSetRow 类型的协议包

public class ResultSetRowPacket {
    private String[] values;

    public ResultSetRowPacket(byte[] bytes) throws IOException {
        ByteArrayInputStream buffer = new ByteArrayInputStream(bytes);
        LinkedList values = new LinkedList();

        while(buffer.available() > 0) {
            values.add(buffer.readLengthEncodedString());
        }

        this.values = (String[])values.toArray(new String[values.size()]);
    }

    public String[] getValues() {
        return this.values;
    }

    public String getValue(int index) {
        return this.values[index];
    }

    public int size() {
        return this.values.length;
    }
}
3.6.5 SSLSocketFactory

ssl工厂接口

public interface SSLSocketFactory {
    SSLSocket createSocket(Socket var1) throws SocketException;
}
3.6.6 SocketFactory

用于创建 ssl 工厂类

public class SocketFactory {

    public static final SSLSocketFactory SSL_SOCKET_FACTORY = new DefaultSSLSocketFactory() {
        protected void initSSLContext(SSLContext sc) throws GeneralSecurityException {
            sc.init((KeyManager[])null, new TrustManager[]{new X509TrustManager() {
                public void checkClientTrusted(X509Certificate[] x509Certificates, String s) throws CertificateException {
                }

                public void checkServerTrusted(X509Certificate[] x509Certificates, String s) throws CertificateException {
                }

                public X509Certificate[] getAcceptedIssuers() {
                    return new X509Certificate[0];
                }
            }}, (SecureRandom)null);
        }
    };

    public static Socket createSocket() {
        return new Socket();
    }

    public static SSLSocket createSslSocket(Socket socket) throws SocketException {
        return SSL_SOCKET_FACTORY.createSocket(socket);
    }


    /**
     * 实现了 SSLSocketFactory
     */
    public static class DefaultSSLSocketFactory implements SSLSocketFactory {
        private final String protocol;

        public DefaultSSLSocketFactory() {
            //设置使用 TLSv1 的版本进行连接
            this("TLSv1");
        }

        public DefaultSSLSocketFactory(String protocol) {
            this.protocol = protocol;
        }

        public SSLSocket createSocket(Socket socket) throws SocketException {
            SSLContext sc;
            try {
                sc = SSLContext.getInstance(this.protocol);
                this.initSSLContext(sc);
            } catch (GeneralSecurityException var5) {
                throw new SocketException(var5.getMessage());
            }

            try {
                return (SSLSocket)sc.getSocketFactory().createSocket(socket, socket.getInetAddress().getHostAddress(), socket.getPort(), true);
            } catch (IOException var4) {
                throw new SocketException(var4.getMessage());
            }
        }

        protected void initSSLContext(SSLContext sc) throws GeneralSecurityException {
            sc.init((KeyManager[])null, (TrustManager[])null, (SecureRandom)null);
        }
    }

}
3.6.7 加密插件类
AuthenticateSecurityPasswordCommand
public class AuthenticateSecurityPasswordCommand implements Command {
    //数据库
    private String schema;
    //用户名
    private String username;
    //密码
    private String password;
    //加密字符串
    private String salt;
    //客户端能力标识符
    private int clientCapabilities;
    //采用的字符集
    private int collation;

    public AuthenticateSecurityPasswordCommand(String schema, String username, String password, String salt, int collation) {
        this.schema = schema;
        this.username = username;
        this.password = password;
        this.salt = salt;
        this.collation = collation;
    }

    public void setClientCapabilities(int clientCapabilities) {
        this.clientCapabilities = clientCapabilities;
    }

    public void setCollation(int collation) {
        this.collation = collation;
    }

    @Override
    public byte[] toByteArray() throws IOException {
        ByteArrayOutputStream buffer = new ByteArrayOutputStream();
        int clientCapabilities = this.clientCapabilities;
        if (clientCapabilities == 0) {
            clientCapabilities = ClientCapabilities.LONG_FLAG |
                                 ClientCapabilities.PROTOCOL_41 |
                                 ClientCapabilities.SECURE_CONNECTION |
                                 ClientCapabilities.PLUGIN_AUTH;

            if (schema != null) {
                clientCapabilities |= ClientCapabilities.CONNECT_WITH_DB;
            }
        }
        buffer.writeInteger(clientCapabilities, 4);
        buffer.writeInteger(0, 4); // maximum packet length
        buffer.writeInteger(collation, 1);
        for (int i = 0; i < 23; i++) {
            buffer.write(0);
        }
        buffer.writeZeroTerminatedString(username);
        byte[] passwordSHA1 = passwordCompatibleWithMySQL411(password, salt);
        buffer.writeInteger(passwordSHA1.length, 1);
        buffer.write(passwordSHA1);
        if (schema != null) {
            buffer.writeZeroTerminatedString(schema);
        }
        buffer.writeZeroTerminatedString("mysql_native_password");
        return buffer.toByteArray();
    }

    /**
     * see mysql/sql/password.c scramble(...)
	 * @param password the password
	 * @param salt salt received from server
	 * @return hashed password
     */
    public static byte[] passwordCompatibleWithMySQL411(String password, String salt) {
        if ("".equals(password) || password == null) {
            return new byte[0];
        }

        MessageDigest sha;
        try {
            sha = MessageDigest.getInstance("SHA-1");
        } catch (NoSuchAlgorithmException e) {
            throw new RuntimeException(e);
        }
        byte[] passwordHash = sha.digest(password.getBytes());
        return CommandUtils.xor(passwordHash, sha.digest(union(salt.getBytes(), sha.digest(passwordHash))));
    }

    private static byte[] union(byte[] a, byte[] b) {
        byte[] r = new byte[a.length + b.length];
        System.arraycopy(a, 0, r, 0, a.length);
        System.arraycopy(b, 0, r, a.length, b.length);
        return r;
    }
}
AuthenticateSHA2Command
public class AuthenticateSHA2Command implements Command {
    //数据库
    private String schema;
    //用户名
    private String username;
    //密码
    private String password;
    //加密字符串
    private String scramble;
    //客户端能力标识符
    private int clientCapabilities;
    //采用的字符集
    private int collation;
    private boolean rawPassword = false;

    public AuthenticateSHA2Command(String schema, String username, String password, String scramble, int collation) {
        this.schema = schema;
        this.username = username;
        this.password = password;
        this.scramble = scramble;
        this.collation = collation;
    }

    public AuthenticateSHA2Command(String scramble, String password) {
        this.rawPassword = true;
        this.password = password;
        this.scramble = scramble;
    }

    public void setClientCapabilities(int clientCapabilities) {
        this.clientCapabilities = clientCapabilities;
    }

    public byte[] toByteArray() throws IOException {
        ByteArrayOutputStream buffer = new ByteArrayOutputStream();
        if (this.rawPassword) {
            //对密码进行编码
            byte[] passwordSHA1 = this.encodePassword();
            buffer.write(passwordSHA1);
            return buffer.toByteArray();
        } else {
            //设置对应的客户端能力位
            int clientCapabilities = this.clientCapabilities;
            if (clientCapabilities == 0) {
                clientCapabilities |= 4;
                clientCapabilities |= 512;
                clientCapabilities |= 32768;
                clientCapabilities |= 524288;
                clientCapabilities |= 2097152;
                if (this.schema != null) {
                    clientCapabilities |= 8;
                }
            }

            buffer.writeInteger(clientCapabilities, 4);
            buffer.writeInteger(0, 4);
            buffer.writeInteger(this.collation, 1);

            for(int i = 0; i < 23; ++i) {
                buffer.write(0);
            }

            buffer.writeZeroTerminatedString(this.username);
            byte[] passwordSHA1 = this.encodePassword();
            buffer.writeInteger(passwordSHA1.length, 1);
            buffer.write(passwordSHA1);
            if (this.schema != null) {
                buffer.writeZeroTerminatedString(this.schema);
            }

            buffer.writeZeroTerminatedString("caching_sha2_password");
            return buffer.toByteArray();
        }
    }

    private byte[] encodePassword() {
        if (this.password != null && !"".equals(this.password)) {
            try {
                MessageDigest md = MessageDigest.getInstance("SHA-256");
                int CACHING_SHA2_DIGEST_LENGTH = 32;
                byte[] dig1 = new byte[CACHING_SHA2_DIGEST_LENGTH];
                byte[] dig2 = new byte[CACHING_SHA2_DIGEST_LENGTH];
                byte[] scramble1 = new byte[CACHING_SHA2_DIGEST_LENGTH];
                md.update(this.password.getBytes(), 0, this.password.getBytes().length);
                md.digest(dig1, 0, CACHING_SHA2_DIGEST_LENGTH);
                md.reset();
                md.update(dig1, 0, dig1.length);
                md.digest(dig2, 0, CACHING_SHA2_DIGEST_LENGTH);
                md.reset();
                md.update(dig2, 0, dig1.length);
                md.update(this.scramble.getBytes(), 0, this.scramble.getBytes().length);
                md.digest(scramble1, 0, CACHING_SHA2_DIGEST_LENGTH);
                return CommandUtils.xor(dig1, scramble1);
            } catch (NoSuchAlgorithmException var6) {
                throw new RuntimeException(var6);
            } catch (DigestException var7) {
                throw new RuntimeException(var7);
            }
        } else {
            return new byte[0];
        }
    }
}
3.6.8 数据流

对 input、output一些常用的操作进行封装

BufferedSocketInputStream
public class BufferedSocketInputStream extends FilterInputStream {

    private byte[] buffer;
    private int offset;
    private int limit;

    public BufferedSocketInputStream(InputStream in) {
        this(in, 524288);
    }

    public BufferedSocketInputStream(InputStream in, int bufferSize) {
        super(in);
        this.buffer = new byte[bufferSize];
    }

    public int available() throws IOException {
        return this.limit - this.offset + this.in.available();
    }

    public int read() throws IOException {
        if (this.offset < this.limit) {
            return this.buffer[this.offset++] & 255;
        } else {
            this.offset = 0;
            this.limit = this.in.read(this.buffer, 0, this.buffer.length);
            return this.limit != -1 ? this.buffer[this.offset++] & 255 : -1;
        }
    }

    public int read(byte[] b, int off, int len) throws IOException {
        if (this.offset >= this.limit) {
            if (len >= this.buffer.length) {
                return this.in.read(b, off, len);
            }

            this.offset = 0;
            this.limit = this.in.read(this.buffer, 0, this.buffer.length);
        }

        int bytesRemainingInBuffer = Math.min(len, this.limit - this.offset);
        System.arraycopy(this.buffer, this.offset, b, off, bytesRemainingInBuffer);
        this.offset += bytesRemainingInBuffer;
        return bytesRemainingInBuffer;
    }
}
ByteArrayInputStream
public class ByteArrayInputStream extends InputStream {
    private InputStream inputStream;
    private Integer peek;
    private Integer pos;
    private Integer markPosition;
    private int blockLength;

    public ByteArrayInputStream(InputStream inputStream) {
        this.blockLength = -1;
        this.inputStream = inputStream;
        this.pos = 0;
    }

    public ByteArrayInputStream(byte[] bytes) {
        this((InputStream)(new java.io.ByteArrayInputStream(bytes)));
    }

    public int readInteger(int length) throws IOException {
        int result = 0;

        for(int i = 0; i < length; ++i) {
            result |= this.read() << (i << 3);
        }

        return result;
    }

    public long readLong(int length) throws IOException {
        long result = 0L;

        for(int i = 0; i < length; ++i) {
            result |= (long)this.read() << (i << 3);
        }

        return result;
    }

    public String readString(int length) throws IOException {
        return new String(this.read(length));
    }

    public String readLengthEncodedString() throws IOException {
        return this.readString(this.readPackedInteger());
    }

    public String readZeroTerminatedString() throws IOException {
        ByteArrayOutputStream s = new ByteArrayOutputStream();

        int b;
        while((b = this.read()) != 0) {
            s.writeInteger(b, 1);
        }

        return new String(s.toByteArray());
    }

    public byte[] read(int length) throws IOException {
        byte[] bytes = new byte[length];
        this.fill(bytes, 0, length);
        return bytes;
    }

    public void fill(byte[] bytes, int offset, int length) throws IOException {
        int read;
        for(int remaining = length; remaining != 0; remaining -= read) {
            read = this.read(bytes, offset + length - remaining, remaining);
            if (read == -1) {
                throw new EOFException();
            }
        }

    }

    public BitSet readBitSet(int length, boolean bigEndian) throws IOException {
        byte[] bytes = this.read(length + 7 >> 3);
        bytes = bigEndian ? bytes : this.reverse(bytes);
        BitSet result = new BitSet();

        for(int i = 0; i < length; ++i) {
            if ((bytes[i >> 3] & 1 << i % 8) != 0) {
                result.set(i);
            }
        }

        return result;
    }

    private byte[] reverse(byte[] bytes) {
        int i = 0;

        for(int length = bytes.length >> 1; i < length; ++i) {
            int j = bytes.length - 1 - i;
            byte t = bytes[i];
            bytes[i] = bytes[j];
            bytes[j] = t;
        }

        return bytes;
    }

    public int readPackedInteger() throws IOException {
        Number number = this.readPackedNumber();
        if (number == null) {
            throw new IOException("Unexpected NULL where int should have been");
        } else if (number.longValue() > 2147483647L) {
            throw new IOException("Stumbled upon long even though int expected");
        } else {
            return number.intValue();
        }
    }

    public Number readPackedNumber() throws IOException {
        int b = this.read();
        if (b < 251) {
            return b;
        } else if (b == 251) {
            return null;
        } else if (b == 252) {
            return (long)this.readInteger(2);
        } else if (b == 253) {
            return (long)this.readInteger(3);
        } else if (b == 254) {
            return this.readLong(8);
        } else {
            throw new IOException("Unexpected packed number byte " + b);
        }
    }

    public int available() throws IOException {
        return this.blockLength != -1 ? this.blockLength : this.inputStream.available();
    }

    public int peek() throws IOException {
        if (this.peek == null) {
            this.peek = this.readWithinBlockBoundaries();
        }

        return this.peek;
    }

    public int read() throws IOException {
        int result;
        if (this.peek == null) {
            result = this.readWithinBlockBoundaries();
        } else {
            result = this.peek;
            this.peek = null;
        }

        if (result == -1) {
            throw new EOFException();
        } else {
            this.pos = this.pos + 1;
            return result;
        }
    }

    private int readWithinBlockBoundaries() throws IOException {
        if (this.blockLength != -1) {
            if (this.blockLength == 0) {
                return -1;
            }

            --this.blockLength;
        }

        return this.inputStream.read();
    }

    public void close() throws IOException {
        this.inputStream.close();
    }

    public void enterBlock(int length) {
        this.blockLength = length < -1 ? -1 : length;
    }

    public void skipToTheEndOfTheBlock() throws IOException {
        if (this.blockLength != -1) {
            this.skip((long)this.blockLength);
            this.blockLength = -1;
        }

    }

    public int getPosition() {
        return this.pos;
    }

    public synchronized void mark(int readlimit) {
        this.markPosition = this.pos;
        this.inputStream.mark(readlimit);
    }

    public boolean markSupported() {
        return this.inputStream.markSupported();
    }

    public synchronized void reset() throws IOException {
        this.pos = this.markPosition;
        this.inputStream.reset();
    }

    public synchronized long fastSkip(long n) throws IOException {
        long skipOf = n;
        if (this.blockLength != -1) {
            skipOf = Math.min((long)this.blockLength, n);
            this.blockLength = (int)((long)this.blockLength - skipOf);
            if (this.blockLength == 0) {
                this.blockLength = -1;
            }
        }

        this.pos = this.pos + (int)skipOf;
        return this.inputStream.skip(skipOf);
    }
}
ByteArrayOutputStream
public class ByteArrayOutputStream extends OutputStream {
    private OutputStream outputStream;

    public ByteArrayOutputStream() {
        this(new java.io.ByteArrayOutputStream());
    }

    public ByteArrayOutputStream(OutputStream outputStream) {
        this.outputStream = outputStream;
    }

    public void writeInteger(int value, int length) throws IOException {
        for(int i = 0; i < length; ++i) {
            this.write(255 & value >>> (i << 3));
        }

    }

    public void writeLong(long value, int length) throws IOException {
        for(int i = 0; i < length; ++i) {
            this.write((int)(255L & value >>> (i << 3)));
        }

    }

    public void writeString(String value) throws IOException {
        this.write(value.getBytes());
    }

    public void writeZeroTerminatedString(String value) throws IOException {
        if (value != null) {
            this.write(value.getBytes());
        }

        this.write(0);
    }

    public void write(int b) throws IOException {
        this.outputStream.write(b);
    }

    public void write(byte[] bytes) throws IOException {
        this.outputStream.write(bytes);
    }

    public byte[] toByteArray() {
        return this.outputStream instanceof java.io.ByteArrayOutputStream ? ((java.io.ByteArrayOutputStream)this.outputStream).toByteArray() : new byte[0];
    }

    public void flush() throws IOException {
        this.outputStream.flush();
    }

    public void close() throws IOException {
        this.outputStream.close();
    }
}
3.6.9 EventHeader

事件头部实体

public class EventHeader {
    private long timestamp;
    private int eventType;
    private long serverId;
    private long eventLength;
    private long nextPosition;
    private int flags;
    
    public EventHeader readHeader(ByteArrayInputStream byteArrayInputStream) throws IOException {
        this.setTimestamp(byteArrayInputStream.readLong(4) * 1000L);
        this.setEventType(byteArrayInputStream.readInteger(1));
        this.setServerId(byteArrayInputStream.readLong(4));
        this.setEventLength(byteArrayInputStream.readLong(4));
        //下一个事件开始的位置
        this.setNextPosition(byteArrayInputStream.readLong(4));
        this.setFlags(byteArrayInputStream.readInteger(2));
        return this;
    }
}

3.7 canal实战

3.7.1 服务端
配置mysql
server-id=1
log-bin=mysql-bin 
binlog_format=row
binlog-do-db=test
配置canal
canal.id = 1  #设置id,用于集群部署
canal.ip = 192.168.8.106   #ip地址,默认用本机
canal.port = 11111         #默认端口
canal.metrics.pull.port = 11112  #设置监控数据的拉取端口
canal.zkServers = 
#flush data to zk
canal.zookeeper.flush.period = 1000 
canal.withoutNetty = false
#tcp, kafka, RocketMQ
canal.serverMode = tcp
#flush meta cursor/parse position to file
canal.destinations = example     #
canal.instance.mysql.slaveId=2   #设置从数据的id
#enable gtid use true/false 
canal.instance.gtidon=false
#position info
canal.instance.master.address=127.0.0.1:3306 #设置数据库的ip地址
canal.instance.dbUsername=root   #设置用户名
canal.instance.dbPassword=123456   #设置密码
canal.instance.connectionCharset = UTF-8
canal.instance.defaultDatabaseName =设置默认数据库 
#enable druid Decrypt database password 
canal.instance.enableDruid=false
3.7.2 客户端
public class CanalClient implements InitializingBean {

    Logger logger = LoggerFactory.getLogger(CanalClient.class);

    @Override
    public void afterPropertiesSet() {
        CanalConnector connector = CanalConnectors.newSingleConnector(new InetSocketAddress("192.168.8.190", 11111), "example", "", "");
        try {
            connector.connect();
            connector.subscribe(".*\\..*");
            connector.rollback();
            while (true) {
                Message message = connector.getWithoutAck(1000);
                long batchId = message.getId();
                List<CanalEntry.Entry> entries = message.getEntries();
                int size = entries.size();
                if (batchId == -1 || size == 0) {
                    logger.info("休眠。。。。");
                    Thread.sleep(2000);
                } else {
                    entries.forEach(item -> {
                        CanalEntry.EntryType entryType = item.getEntryType();
                        if (!(entryType == CanalEntry.EntryType.TRANSACTIONBEGIN || entryType == CanalEntry.EntryType.TRANSACTIONEND)) {
                            try {
                                CanalEntry.RowChange rowChange = CanalEntry.RowChange.parseFrom(item.getStoreValue());
                                CanalEntry.EventType eventType = rowChange.getEventType();
                                logger.info("事件类型:" + eventType);
                                logger.info("数据信息:binlog[%s:%s],name[%s,%s],envent[%s]",
                                            item.getHeader().getLogfileName(),item.getHeader().getLogfileOffset()
                                           ,item.getHeader().getSchemaName(),item.getHeader().getTableName(),eventType);
                                List<CanalEntry.RowData> rowDatasList = rowChange.getRowDatasList();
                                rowDatasList.forEach(data -> {
                                    if (eventType == CanalEntry.EventType.DELETE) {
                                        List<CanalEntry.Column> beforeColumnsList = data.getBeforeColumnsList();
                                        System.out.println("=======================【删除事件】======================");
                                        beforeColumnsList.forEach(column -> {
                                            logger.info("字段[%s],value[%s]",column.getName(),column.getValue());
                                        });
                                        //如果是新增语句
                                    } else if (eventType == CanalEntry.EventType.INSERT) {
                                        System.out.println("=======================【插入事件】======================");
                                        List<CanalEntry.Column> afterColumnsList = data.getAfterColumnsList();
                                        afterColumnsList.forEach(column -> {
                                            logger.info("字段[%s],value[%s]",column.getName(),column.getValue());
                                        });
                                    } else {
                                        //变更前的数据
                                        System.out.println("=======================【更新事件】======================");
                                        List<CanalEntry.Column> beforeColumnsList = data.getBeforeColumnsList();
                                        System.out.println("------->; before");
                                        beforeColumnsList.forEach(column -> {
                                            logger.info("字段[%s],value[%s]",column.getName(),column.getValue());
                                        });
                                        //变更后的数据
                                        List<CanalEntry.Column> afterColumnsList = data.getAfterColumnsList();
                                        System.out.println("------->; after");
                                        afterColumnsList.forEach(column -> {
                                            logger.info("字段[%s],value[%s]",column.getName(),column.getValue());
                                        });
                                    }
                                });
                            } catch (InvalidProtocolBufferException e) {
                                e.printStackTrace();
                            }
                        }
                    });
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }

    }
}

3.8 mysql-binlog-connector

根据组件 https://github.com/osheroff/mysql-binlog-connector-java 实战demo;

实战例子非常简单,只需要通过 BinaryLogClient 类创建一个客户端用于监听数据,后续通过 Listener 进行数据库的监听就可以了

public class MyLogBinClient {

    public static void main(String[] args) {
        BinaryLogClient logClient = new BinaryLogClient("192.168.8.106", 3306, "root", "root");
        EventDeserializer eventDeserializer = new EventDeserializer();
        eventDeserializer.setCompatibilityMode(EventDeserializer.CompatibilityMode.CHAR_AND_BINARY_AS_BYTE_ARRAY,
                                               EventDeserializer.CompatibilityMode.DATE_AND_TIME_AS_LONG);
logClient.registerLifecycleListener(new BinaryLogClient.LifecycleListener() {
            @Override
            public void onConnect(BinaryLogClient client) {
                System.out.println("启动完成");
            }

            @Override
            public void onCommunicationFailure(BinaryLogClient client, Exception ex) {
                System.out.println("监听binlog异常");
            }

            @Override
            public void onEventDeserializationFailure(BinaryLogClient client, Exception ex) {
                System.out.println("事件序列化失败");
            }

            @Override
            public void onDisconnect(BinaryLogClient client) {
                System.out.println("客户端断开连接");
            }
        });
        logClient.setEventDeserializer(eventDeserializer);
        logClient.registerEventListener(event -> {
            EventData eventData = event.getData();
            if (eventData instanceof TableMapEventData) {
                TableMapEventData tableMapEventData = (TableMapEventData) eventData;
                System.out.println(String.format("表Id:%s,数据库:%s,表名:%s", tableMapEventData.getTableId(), tableMapEventData.getDatabase(),
                                                 tableMapEventData.getTable()));
            }
            if (eventData instanceof UpdateRowsEventData) {
                System.out.println("修改数据:" + eventData);
                UpdateRowsEventData updateRowsEventData = (UpdateRowsEventData) eventData;
                for (Map.Entry<Serializable[], Serializable[]> row : updateRowsEventData.getRows()) {
                    Serializable[] rowKey = row.getKey();
                    for (Serializable serializable : rowKey) {
                        if (serializable instanceof byte[]) {
                            System.out.println(new String((byte[]) serializable).trim());
                        } else {
                            System.out.println(serializable);
                        }
                    }
                }
            }
            if (eventData instanceof WriteRowsEventData) {
                System.out.println("插入数据:" + eventData);
            }
            if (eventData instanceof DeleteRowsEventData) {
                System.out.println("删除:" + eventData);
            }
        });

        try {

            logClient.connect();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

3.8 spring-mysql-connector-start

通过 mysql-binlog-connector 跟springboot框架进行整合实现自动装配以及通过注解进行处理

  • 注解式启动客户端
  • 封装查询了数据库列名
  • 数据库表结构的缓存构建
  • 注解式注册生命周期
  • 自定义事件序列化器
  • 自定义数据库表自定义匹配模式
3.8.1 Start

用于开启springboot服务

@SpringBootApplication
@EnableMysqlSlave
public class Start {

    public static void main(String[] args) {
        SpringApplication.run(Start.class);
    }

}
3.8.2 LifecycleListener

生命周期监听函数

@LifecycleListener(serverId = "test")
public class LifecycleListener {

    @OnConnect
    public void onConnect(String name, BinaryLogClient client) {
        System.out.println("启动完成");
    }

    @OnDisconnect
    public void OnDisconnect(BinaryLogClient client) {
        System.out.println("客户端断开连接");
    }
    
    @OnCommunicationFailure
    public void onCommunicationFailure(BinaryLogClient client, Exception ex) {
        System.out.println("监听binlog异常");
    }
    
    @OnEventDeserializationFailure
    public void onEventDeserializationFailure(BinaryLogClient client, Exception ex) {
        System.out.println("事件序列化失败");
    }

}
3.8.3 EventListner

支持多客户端进行数据库订阅,以及监听通过表达式进行订阅

@Slf4j
@EventListener(subscribe = "test.user") // test.* 监听test库下所有的事件
public class CommonEventListener extends AbstractApplicationEventListener {
    @Override
    protected void dispose(Entity entity) {
        log.info("接收到修改事件:{}", entity);
    }
}
3.8.4 代码结构

mysql 关闭safemode mysql关闭slave_mysql_09