在深入了解SMProxy之前,一直认为连接池是对mysql连接对象进行统一管理的处理,但是随之而来的问题是现有的php框架都没有自带mysql连接池,如何以最小的代价替代框架的数据库模块一直是一个难题。
在深入了解SMProxy之后,发现SMProxy的奇妙之处就在于你并不需要对框架的数据库模块进行任何的修改,即可使用SMProxy架构,它是基于mysql客户端与mysql服务端的中间件,通过swoole/server自己模拟与mysql报文交互并内部管理连接池对象来提升效率。
swoole:
运用到的知识点 swoole/server以及swoole/client, 不做更多的介绍
tcp 粘包问题: https://www.cnblogs.com/JsonM/articles/9283037.html
client -> tcp buffer(等待cpu指令, 如果buffer缓存达到上限,就会直接发送到server, 所有有可能一次性接受多个数据) -> server
mysql 协议分析
https://www.cnblogs.com/davygeek/p/5647175.html
// 位于SMProxy/src/Handler/Frontend/FrontendAuthticator
public function getHandshakePacket(int $server_id)
{
$rand1 = RandomUtil::randomBytes(8);
$rand2 = RandomUtil::randomBytes(12);
$this->seed = array_merge($rand1, $rand2);
$hs = new HandshakePacket();
$hs->packetId = 0;
// 以下根据握手报文
// 协议版本号
$hs->protocolVersion = Versions::PROTOCOL_VERSION;
// 服务器版本号信息
$hs->serverVersion = Versions::SERVER_VERSION;
// 服务器线程
$hs->threadId = $server_id;
// 随机数
$hs->seed = $rand1;
// 填充值,服务器权能标识,
$hs->serverCapabilities = $this->getServerCapabilities();
// 字符编码
$hs->serverCharsetIndex = (CharsetUtil::getIndex(CONFIG['server']['charset'] ?? 'utf8mb4') & 0xff);
// 服务器状态
$hs->serverStatus = 2;
// 服务器权能标识+填充值
$hs->restOfScrambleBuff = $rand2;
return getString($hs->write());
}
//位于 SMProxy/src/HandshakePacket
public function write()
{
// default init 256,so it can avoid buff extract
$buffer = [];
// 写入消息头长度
BufferUtil::writeUB3($buffer, $this->calcPacketSize());
// 写入序号 -- 消息头的
$buffer[] = $this->packetId;
// 写入协议版本号
$buffer[] = $this->protocolVersion;
// 写入服务器版本信息
BufferUtil::writeWithNull($buffer, getBytes($this->serverVersion));
// 写入服务器线程ID
BufferUtil::writeUB4($buffer, $this->threadId);
// 挑战随机数 9个字节 包含一个填充值
BufferUtil::writeWithNull($buffer, $this->seed);
// 服务器权能标识
BufferUtil::writeUB2($buffer, $this->serverCapabilities);
// 1字节 字符编码
$buffer[] = $this->serverCharsetIndex;
// 服务器状态
BufferUtil::writeUB2($buffer, $this->serverStatus);
if ($this ->serverCapabilities & Capabilities::CLIENT_PLUGIN_AUTH) {
// 服务器权能标志 16位
BufferUtil::writeUB2($buffer, $this->serverCapabilities);
// 挑战长度+填充值+挑战随机数
$buffer[] = max(13, count($this->seed) + count($this->restOfScrambleBuff) + 1);
$buffer = array_merge($buffer, [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]);
} else {
// 10字节填充数
$buffer = array_merge($buffer, self::$FILLER_13);
}
// +12字节挑战随机数
if ($this ->serverCapabilities & Capabilities::CLIENT_SECURE_CONNECTION) {
BufferUtil::writeWithNull($buffer, $this->restOfScrambleBuff);
}
if ($this ->serverCapabilities & Capabilities::CLIENT_PLUGIN_AUTH) {
BufferUtil::writeWithNull($buffer, getBytes($this->pluginName));
}
return $buffer;
}
// 位于SMProxy/src/SMProxyServer
private function auth(BinaryPacket $bin, \swoole_server $server, int $fd)
{
// 如果数据长度是20, -- 可能自定义的, 4-20是密码, 最后4位不知道干啥
if ($bin->data[0] == 20) {
// 密码长度是16 , 判断账号密码
$checkAccount = $this->checkAccount($server, $fd, $this->source[$fd]->user, array_copy($bin->data, 4, 20));
if (!$checkAccount) {
// 发送ERROR报文
$this ->accessDenied($server, $fd, 4);
} else {
if ($server->exist($fd)) {
// 发送OK报文
$server->send($fd, getString(OkPacket::$SWITCH_AUTH_OK));
}
// 认证标志设置为true
$this->source[$fd]->auth = true;
}
} elseif ($bin->data[4] == 14) {
// 序号等于14
if ($server->exist($fd)) {
// 无需认证即登录
$server->send($fd, getString(OkPacket::$OK));
}
} else {
$authPacket = new AuthPacket();
// 读取报文信息 登录认证报文
$authPacket->read($bin);
// 判断账号密码
$checkAccount = $this->checkAccount($server, $fd, $authPacket->user ?? '', $authPacket->password ?? []);
if (!$checkAccount) {
// 密码校验失败
if ($authPacket->pluginName == 'mysql_native_password') {
// 发送ERROR报文
$this ->accessDenied($server, $fd, 2);
} else {
// 记录用户数据
$this->source[$fd]->user = $authPacket ->user;
$this->source[$fd]->database = $authPacket->database;
// 填充数
$this->source[$fd]->seed = RandomUtil::randomBytes(20);
// 发送EOF报文
$authSwitchRequest = array_merge(
[254],
getBytes('mysql_native_password'),
[0],
$this->source[$fd]->seed,
[0]
);
if ($server->exist($fd)) {
$server->send($fd, getString(array_merge(getMysqlPackSize(count($authSwitchRequest)), [2], $authSwitchRequest)));
}
}
} else {
// 账号正确 发送OK报文, 并记录数据
if ($server->exist($fd)) {
$server->send($fd, getString(OkPacket::$AUTH_OK));
}
$this->source[$fd]->auth = true;
$this->source[$fd]->database = $authPacket->database;
}
}
}
https://github.com/linjinmin/SMProxy-
https://www.cnblogs.com/JsonM/articles/9283037.html // tcp粘包问题
https://www.cnblogs.com/davygeek/p/5647175.html // mysql协议