Golang之封装Mysql Slave小例子

这个例子是在学习了mysql协议时,根据canal中源码采用 go 语言来写的 ,主要是用于学习 mysql slave 是如何注册到 mysql 上面,包括 mysql协议 如何发送数据包以及读取数据包;由于空闲时间不多,所以功能不是很完善,主要是实现了tls 握手以及 binlog 网路流的开启;只用于学习协议使用(代码很烂,大佬们轻喷);废话不多说,上代码

java版以及协议详细说明:https://blog.csdn.net/weixin_43915643/article/details/126506492?spm=1001.2014.3001.5501

1. 包结构

  • command:封装命令包实体
  • auth:插件验证包
  • common:通用常量
  • connector:连接器
  • event:事件读取包
    在这里插入图片描述

2. 代码实例

本次例子的目的是:通过 tls 开启 binlog 流就算成功

func main() {
	connector.NewConnection(connector.ConnectionConfig{
		Addr:     "localhost:3306",
		Username: "root",
		Password: "123456",
	})
}

我这里使用两个依赖:github上面的开源项目,组件已经封装的比较完全,我借用了 mysql.PacketIO 用于读取数据和写入数据,但是 binlog 流读取数据的格式跟 mysql.PacketIO 中提供的 api ReadPacket 读取的格式不一样所以,只能读第一个事件,后续mysql就会发送读取位置错误的包;这不影响,本次例子只在前置握手阶段,不做事件的解析,以后有时间的话会替换 mysql.PacketIO 对象自己封装tcp流

  • github.com/flike/kingshard/backend
  • github.com/flike/kingshard/mysql

在这里插入图片描述

3. 代码

注:mysql新版协议中,COM_BINLOG_DUMP 新增了一个 BINLOG_DUMP_NON_BLOCK 用来表示是否开启一个binlog阻塞流,有了这个功能之后,就不需要执行 COM_REGISTER_SLAVE 协议来注册slave了

3.1 连接器(connector)

最主要的方法 NewConnection() 开启一个 tcp 连接,根据连接 mysql 服务端返回的初始包,设置对应的客户端能力,并且将服务端升级为 tls 进行数据传输

package connector

import (
	"context"
	"crypto/tls"
	"errors"
	"fmt"
	_ "fmt"
	"gee/mysql_connector/auth"
	"gee/mysql_connector/command"
	"gee/mysql_connector/event"
	"gee/mysql_connector/pack"
	_ "github.com/flike/kingshard/backend"
	"github.com/flike/kingshard/mysql"
	"log"
	"net"
	"reflect"
	"strconv"
	"time"
)

type ConnectionConfig struct {
	//ip地址
	Addr string
	//用户名
	Username string
	//密码
	Password string
	//数据库
	Schema string
}

// Connector 连接实体
type Connector struct {
	//配置信息
	connectionConfig ConnectionConfig
	//获取到mysql连接数据流
	pkg *mysql.PacketIO
	//初始握手包
	initialHandshake pack.InitialHandshake
	//binlog 文件的名称
	binlogName string
	//binlog 文件的事件位置
	binlogPosition int
	//是否连接成功
	isSuccess bool
	//会为每一个事件写入一个校检值,用于检查每个事件的完整性
	checkSumType string
	//服务端id
	serverId int
}

// Execute 执行对应的sql
func (c *Connector) Execute(sql string) {

}

func (c *Connector) showMasterStatus() {
	queryCommand := command.QueryCommand{
		Sql: "show master status",
	}
	c.WriteCommand(queryCommand)
	resultSet := c.readResultSet()
	if len(resultSet) <= 0 {
		panic(errors.New("Failed to determine binlog filename/position"))
	}
	c.binlogName = resultSet[0].GetValue(0)
	c.binlogPosition, _ = strconv.Atoi(resultSet[0].GetValue(1))
	fmt.Printf("读取到Binlog文件名称:%s, Position位置:%d\n", c.binlogName, c.binlogPosition)
}

// binlogCheckSum 获取binglog_checksum属性
func (c *Connector) binlogCheckSum() {
	//默认采用 crc32 防止写入校检符 占4个字节
	queryCommand := command.QueryCommand{
		Sql: "show global variables like 'binlog_checksum'",
	}
	c.WriteCommand(queryCommand)
	resultSet := c.readResultSet()
	if len(resultSet) != 0 {
		c.checkSumType = resultSet[0].GetValue(1)
		if len(c.checkSumType) > 0 {
			queryCommand = command.QueryCommand{
				Sql: "set @master_binlog_checksum= @@global.binlog_checksum",
			}
			c.readResultSet()
		}
	}
}

// 获取到服务端id
func (c *Connector) getMasterServerId() {
	queryCommand := command.QueryCommand{
		Sql: "select @@server_id",
	}
	c.WriteCommand(queryCommand)
	resultSet := c.readResultSet()
	if len(resultSet) >= 0 {
		c.serverId, _ = strconv.Atoi(resultSet[0].GetValue(0))
	}
}

// 发起binlog的dump请求
func (c *Connector) requestBinaryLogStream() {
	dumpBinaryLogCommand := command.DumpBinaryLogCommand{
		ServerId:       c.serverId,
		BinlogPosition: c.binlogPosition,
		BinlogFilename: c.binlogName,
	}
	c.WriteCommand(dumpBinaryLogCommand)
	for {
		packet, _ := c.pkg.ReadPacket()
		c.getReader()
		c.checkError(packet)
		e := new(event.Head)
		e.Read(packet)
		//读取了头部数据之后根据事件类型读取对应的数据
		fmt.Printf("事件类型:%d\n事件时间:%d\n事件长度:%d\n", e.EventId, e.Timestamp, e.EventLength)
	}
}

func (c *Connector) getReader() {
	value := reflect.TypeOf(c.pkg)
	if value.Kind() == reflect.Pointer {
		v := value.Elem().Field(0)
		fmt.Println(v)
	}
}

func (c *Connector) readResultSet() []command.ResultSet {
	//先读取第一个包是否是错误包
	if data, err := c.pkg.ReadPacket(); err == nil {
		c.checkError(data)
	}
	//循环读取数据包
	var rss []command.ResultSet
	//当读取到 E0F包时,后面的就是数据
	for {
		data, _ := c.pkg.ReadPacket()
		if data[0] == 0xFE {
			break
		}
	}
	//跳过0xFE的包
	for {
		data, _ := c.pkg.ReadPacket()
		if data[0] == 0xFE {
			break
		}
		c.checkError(data)
		rs := new(command.ResultSet)
		rs.ReadSet(data)
		rss = append(rss, *rs)
	}
	return rss
}

// checkError 检查错误信息
func (c *Connector) checkError(data []byte) {
	if data[0] == 0xFF {
		errorPacket := make([]byte, len(data)-1)
		copy(errorPacket[:], data[1:])
		e := new(command.ErrorPacket)
		e.Read(errorPacket)
		panic(errors.New(e.ToString()))
	}
}

func (c *Connector) WriteCommand(command command.Command) {
	bytes := command.ToByteArray()
	if c.isSuccess {
		c.pkg.Sequence = 0
	}
	if err := c.pkg.WritePacket(bytes); err != nil {
		panic(err)
	}
}

// readInitPacket 读取初始数据包
func (c *Connector) readInitPacket() {
	data, err := c.pkg.ReadPacket()
	if err != nil {
		panic(err)
	}
	//读取初始包
	initialHandPacket := pack.InitialHandshake{}
	initialHandPacket.ReadInitialHandshake(data)
	c.initialHandshake = initialHandPacket
}

// upgradeTls 升级为tls握手
func (c *Connector) upgradeTls(conn net.Conn) {
	//构建升级tls包
	upgradeTls := new(command.UpgradeTls)
	upgradeTls.InitPack = c.initialHandshake
	bytes := upgradeTls.ToByteArray()
	_ = c.pkg.WritePacket(bytes)
	//进行tls握手,InsecureSkipVerify 跳过证书的验证
	client := tls.Client(conn, &tls.Config{
		InsecureSkipVerify: true,
	})
	if err := client.Handshake(); err != nil {
		log.Fatalln(err.Error())
		return
	}
	//握手成功之后,将IO进行替换
	sequence := c.pkg.Sequence
	c.pkg = mysql.NewPacketIO(client)
	c.pkg.Sequence = sequence
	//获取到验证插件进行
	authenticator := auth.Authenticator{
		Username:    c.connectionConfig.Username,
		Password:    c.connectionConfig.Password,
		Schema:      c.connectionConfig.Schema,
		GreetPacket: c.initialHandshake,
		Pkg:         c.pkg,
	}
	c.isSuccess = authenticator.Authenticate()
}

// NewConnection 创建一个tcp的连接
func NewConnection(connectionConfig ConnectionConfig) (*Connector, error) {
	connector := new(Connector)
	connector.connectionConfig = connectionConfig
	ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
	defer cancel()
	dialer := &net.Dialer{}
	conn, err := dialer.DialContext(ctx, "tcp", connectionConfig.Addr)
	if err != nil {
		log.Fatalln("连接Mysql服务器失败:" + err.Error())
		return nil, err
	}
	//转换为tcp类型
	tcpConn := conn.(*net.TCPConn)
	//设置不需要延时
	tcpConn.SetNoDelay(false)
	//设置保持长连接
	tcpConn.SetKeepAlive(true)
	//将tcp连接包装成 PacketIO实体
	connector.pkg = mysql.NewPacketIO(tcpConn)
	//读取初始实体
	connector.readInitPacket()
	//升级tls连接
	connector.upgradeTls(conn)
	//读取binlog文件的名称
	connector.showMasterStatus()
	//设置服务id
	connector.getMasterServerId()
	//发起日志
	connector.requestBinaryLogStream()
	return connector, nil
}

3.2 command

命令包,主要用于封装发送的协议包以及读取错误和结果包

  • Command:定义的接口
  • InitialHandshake:初始握手包
  • UpgradeTls:发送请求升级为tls
  • AuthenticateSecurityPasswordCommand:对密码做完加密之后发送mysql
  • QueryCommand:封装查询sql的实体
  • ResultSet:读取查询完之后返回的数据
  • ErrorPacket:错误包信息
  • DumpBinaryLogCommand:发起binlog请求

Command

type Command interface {

	//ToByteArray 将数据转换为字节数组
	ToByteArray() []byte
}

InitialHandshake

type InitialHandshake struct {
	//协议版本号
	ProtocolVersion int
	//服务版本号
	ServerVersion string
	//连接id
	ConnectionId uint32
	//加密字符串
	Salt string
	//服务端能力标识
	ServerCapability uint32
	//采用的字符集
	CharacterSet byte
	//状态标识符
	StatusFlags uint16
	//插件名称
	AuthPluginName string
}

// ReadInitialHandshake 读取初始包
func (s *InitialHandshake) ReadInitialHandshake(data []byte) {
	buffer := bytes.NewBuffer(data)
	//判断数据包的类型是否是错误数据包
	protocolVersion, _ := buffer.ReadByte()
	if protocolVersion == mysql.ERR_HEADER {
		panic(errors.New("read initial handshake error"))
	}

	//判断mysql服务端的版本号
	if protocolVersion < mysql.MinProtocolVersion {
		panic(fmt.Errorf("invalid protocol version %d, must >= 10", data[0]))
	}
	s.ProtocolVersion = int(protocolVersion)
	//读取服务版本号
	s.ServerVersion, _ = buffer.ReadString(0x00)
	//读取连接id
	s.ConnectionId = binary.LittleEndian.Uint32(buffer.Next(4))
	//读取加密字符串
	s.Salt = string(buffer.Next(8))
	buffer.Next(1)
	//读取低位
	s.ServerCapability = uint32(binary.LittleEndian.Uint16(buffer.Next(2)))
	characterSetByte, _ := buffer.ReadByte()
	//读取字符集
	s.CharacterSet = characterSetByte
	//读取状态标识
	s.StatusFlags = binary.LittleEndian.Uint16(buffer.Next(2))
	//读取高位
	s.ServerCapability = uint32(binary.LittleEndian.Uint16(buffer.Next(2)))<<16 | s.ServerCapability

	//根据客户端能力是否支持对应的能力
	if s.ServerCapability&mysql.CLIENT_PLUGIN_AUTH != 0 {
		pluginLength, _ := buffer.ReadByte()
		//跳过10字节的填充位
		buffer.Next(10)
		if s.ServerCapability&mysql.CLIENT_SECURE_CONNECTION != 0 {
			s.Salt += string(buffer.Next(int(math.Max(float64(13), float64(pluginLength-8))) - 1))
			buffer.Next(1)
		}
		s.AuthPluginName = string(buffer.Next(buffer.Len() - 1))
		buffer.Next(1)
	}
}

UpgradeTls

type UpgradeTls struct {
	//客户端能力标识
	CapabilityFlags uint32
	//初始包
	InitPack pack.InitialHandshake
}

func (u UpgradeTls) ToByteArray() []byte {
	//升级ssl请求数据的长度为32
	data := make([]byte, 36)
	// 设置当前客户端的能力标识符
	capability := mysql.CLIENT_PROTOCOL_41 |
		mysql.CLIENT_SECURE_CONNECTION |
		mysql.CLIENT_LONG_FLAG |
		mysql.CLIENT_SSL |
		mysql.CLIENT_PLUGIN_AUTH

	u.CapabilityFlags = capability

	data[4] = byte(u.CapabilityFlags)
	data[5] = byte(u.CapabilityFlags >> 8)
	data[6] = byte(u.CapabilityFlags >> 16)
	data[7] = byte(u.CapabilityFlags >> 24)
	//MaxPacket 直接默认为0

	//设置字符集
	data[12] = u.InitPack.CharacterSet

	return data
}

AuthenticateSecurityPasswordCommand

type AuthenticateSecurityPasswordCommand struct {
	//用户名
	Username string
	//密码
	Password string
	//数据库名称
	Schema string
	//加密字符串
	Salt string
	//采用的字符集
	Collation uint8
	//插件名称
	PluginName string
}

func (a AuthenticateSecurityPasswordCommand) ToByteArray() []byte {
	length := 4
	//能力标识符
	capability := mysql.CLIENT_PROTOCOL_41 |
		mysql.CLIENT_SECURE_CONNECTION |
		mysql.CLIENT_LONG_PASSWORD |
		mysql.CLIENT_TRANSACTIONS |
		mysql.CLIENT_LONG_FLAG |
		mysql.CLIENT_SSL |
		mysql.CLIENT_PLUGIN_AUTH
	//长度:能力标识符、最大包长度、字符集、23个0的填充位
	length += 4 + 4 + 1 + 23

	//用户名的长度 0 结尾
	length += len(a.Username) + 1
	//计算出的密码
	calcPassword := mysql.CalcPassword([]byte(a.Salt), []byte(a.Password))
	//加上密码的长度
	length += 1 + len(calcPassword)

	//判断是否有指定的数据库
	if len(a.Schema) > 0 {
		capability |= mysql.CLIENT_CONNECT_WITH_DB
		length += len(a.Schema) + 1
	}
	length += len(common.MYSQL_NATIVE) + 1
	data := make([]byte, length)
	data[4] = byte(capability)
	data[5] = byte(capability >> 8)
	data[6] = byte(capability >> 16)
	data[7] = byte(capability >> 24)
	//跳过最大包长度 maxSize
	pos := 4 + 4 + 4 + 1 + 23
	//直接写入字符集
	data[12] = a.Collation
	//写入用户名
	pos += copy(data[pos:], a.Username)
	// 0x00 分割
	pos++
	data[pos] = byte(len(calcPassword))
	pos++
	//写入密码
	pos += copy(data[pos:], calcPassword)
	if len(a.Schema) > 0 {
		pos += copy(data[pos:], a.Schema)
		pos++
	}
	pos += copy(data[pos:], common.MYSQL_NATIVE)
	return data
}

QueryCommand

type QueryCommand struct {
	//查询sql
	Sql string
}

func (q QueryCommand) ToByteArray() []byte {
	b := new(bytes.Buffer)
	//保留头出来,让后面写的时候可以写入头包
	b.Write([]byte{0, 0, 0, 0})
	//写入协议号
	b.WriteByte(3)
	//写入sql
	b.WriteString(q.Sql)
	//返回数组
	return b.Bytes()
}

ResultSet

type ResultSet struct {
	values []string
}

func (r *ResultSet) ReadSet(data []byte) {
	buffer := bytes.NewBuffer(data)
	var values []string
	for {
		if buffer.Len() <= 0 {
			break
		}
		lengthByte, _ := buffer.ReadByte()
		//读取字段的长度
		length := int(lengthByte)
		value := string(buffer.Next(length))
		values = append(values, value)
	}
	r.values = values
}

func (r ResultSet) GetValue(index int) string {
	if index < 0 || len(r.values) < 0 {
		panic(errors.New("索引索引必须大于或等于0"))
	}
	return r.values[index]
}

DumpBinaryLogCommand

type DumpBinaryLogCommand struct {
	ServerId       int
	BinlogFilename string
	BinlogPosition int
}

func (d DumpBinaryLogCommand) ToByteArray() []byte {
	buffer := bytes.NewBuffer([]byte{})
	buffer.Write([]byte{0, 0, 0, 0})
	buffer.WriteByte(18)
	//写入binlog的位置
	buffer.Write([]byte{
		byte(d.BinlogPosition),
		byte((d.BinlogPosition >> 8) & 0x000000FF),
		byte((d.BinlogPosition >> 16) & 0x000000FF),
		byte((d.BinlogPosition >> 24) & 0x000000FF)})
	buffer.Write([]byte{0, 0})
	//写入服务id
	buffer.Write([]byte{
		byte(d.ServerId),
		byte((d.ServerId >> 8) & 0x000000FF),
		byte((d.ServerId >> 16) & 0x000000FF),
		byte((d.ServerId >> 24) & 0x000000FF)})
	//写入binlog名称
	buffer.WriteString(d.BinlogFilename)
	return buffer.Bytes()
}

ErrorPacket

type ErrorPacket struct {
	code      int
	sqlStatus string
	sqlFlag   string
	errorMsg  string
}

func (e *ErrorPacket) Read(data []byte) {
	buffer := bytes.NewBuffer(data)
	codeByte, _ := buffer.ReadByte()
	e.code = int(codeByte)
	sqlStatusByte, _ := buffer.ReadByte()
	e.sqlStatus = string(sqlStatusByte)
	sqlFlagByte := buffer.Next(5)
	e.sqlFlag = string(sqlFlagByte)
	buffer.Next(1)
	e.errorMsg = string(buffer.Next(buffer.Len()))
}

func (e *ErrorPacket) ToString() string {
	return fmt.Sprintf("错误信息:%s\n", e.errorMsg)
}

3.3 Authenticator

根据mysql中密码的加密插件进行对应的插件选择

type Authenticator struct {
	//用户名
	Username string
	//密码
	Password string
	//数据库名称
	Schema string
	//初始化包
	GreetPacket pack.InitialHandshake
	//管道数据流
	Pkg *mysql.PacketIO
}

// Authenticate 根据对应的插件名称创建对应插件验证器
func (a *Authenticator) Authenticate() bool {
	pluginName := strings.TrimSpace(a.GreetPacket.AuthPluginName)
	var c command.Command
	if pluginName == common.MYSQL_NATIVE {
		c = &command.AuthenticateSecurityPasswordCommand{
			Username:   a.Username,
			Password:   a.Password,
			Schema:     a.Schema,
			Salt:       a.GreetPacket.Salt,
			PluginName: a.GreetPacket.AuthPluginName,
			Collation:  a.GreetPacket.CharacterSet,
		}
	} else {

	}
	//获取到字节数组信息
	bytes := c.ToByteArray()
	//写入流中
	_ = a.Pkg.WritePacket(bytes)
	//读取对应的数据包
	result, _ := a.Pkg.ReadPacket()
	switch int(result[0]) {
	case -2:
	case -1:
		log.Fatalln("login failed")
		return false
	case 0:
		log.Printf("login success..........")
		return true
	}
	return false
}

3.4 common

const (
	SHA2_PASSWORD string = "caching_sha2_password"

	MYSQL_NATIVE string = "mysql_native_password"
)

3.5 事件

对于 binlog 的事件解析,这里只解析了头部,需要自己解析详细事件的大佬可以看官网的协议说明

type Head struct {
	//时间戳
	Timestamp int32

	//事件id
	EventId byte

	//服务id
	ServerId int32

	//事件长度
	EventLength int32

	//下一个事件在binlog文件中的位置
	NextEventPosition int32

	//标识符
	Flag int16
}

func (h *Head) Read(data []byte) {
	buffer := bytes.NewBuffer(data)
	//将包首付包类型读跳过
	buffer.Next(1)
	timestampBytes := buffer.Next(4)
	//读取时间戳
	h.Timestamp |= int32(timestampBytes[0]) | int32(timestampBytes[1])<<8 | int32(timestampBytes[2])<<16 | int32(timestampBytes[3]<<24)
	h.EventId, _ = buffer.ReadByte()

	serverIdBytes := buffer.Next(4)
	h.ServerId |= int32(serverIdBytes[0]) | int32(serverIdBytes[1])<<8 | int32(serverIdBytes[2])<<16 | int32(serverIdBytes[3]<<24)

	eventLengthBytes := buffer.Next(4)
	h.EventLength |= int32(eventLengthBytes[0]) | int32(eventLengthBytes[1])<<8 | int32(eventLengthBytes[2])<<16 | int32(eventLengthBytes[3]<<24)

	nextEventPositionBytes := buffer.Next(4)
	h.NextEventPosition |= int32(nextEventPositionBytes[0]) | int32(nextEventPositionBytes[1])<<8 | int32(nextEventPositionBytes[2])<<16 | int32(nextEventPositionBytes[3]<<24)

	flagBytes := buffer.Next(2)
	h.Flag |= int16(flagBytes[0]) | int16(flagBytes[1]<<8)
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值