数据库路由中间件MyCat - 源代码篇(2)
此文已由作者张镐薪授权网易云社区发布。
欢迎访问网易云社区,了解更多网易技术产品运营经验。
2. 前端连接建立与认证
Title:MySql连接建立以及认证过程client->MySql:1.TCP连接请求
MySql->client:2.接受TCP连接client->MySql:3.TCP连接建立MySql->client:4.握手包HandshakePacketclient->MySql:5.认证包AuthPacketMySql->client:6.如果验证成功,则返回OkPacketclient->MySql:7.默认会发送查询版本信息的包MySql->client:8.返回结果包
2.2 (4)握手包HandshakePacket
NIOReactor其实就是一个网络事件反应转发器。 很多地方会用到NIOReactor,这里先讲FrontendConnection和NIOReactor绑定这一部分。上一节说到,NIOAcceptor的accept()最后将FrontendConnection交给了NIOReactor池其中的一个NIOReactor。调用的是 postRegister(AbstractConnection c)方法。
final void postRegister(AbstractConnection c) {
reactorR.registerQueue.offer(c);
reactorR.selector.wakeup();
}
postRegister将刚才传入的FrontendConnection放入RW线程的注册队列。之后,唤醒RW线程的selector。 为什么放入RW线程的注册队列,而不是直接注册呢?如果是直接注册,那么就是NIOAcceptor这个线程负责注册,这里就会有锁竞争,因为NIOAcceptor这个线程和每个RW线程会去竞争selector的锁。这样NIOAcceptor就不能高效的处理连接。所以,更好的方式是将FrontendConnection放入RW线程的注册队列,之后让RW线程自己完成注册工作。 RW线程的源代码:
private final class RW implements Runnable { private final Selector selector; private final ConcurrentLinkedQueue<AbstractConnection> registerQueue; private long reactCount; private RW() throws IOException { this.selector = Selector.open(); this.registerQueue = new ConcurrentLinkedQueue<AbstractConnection>();
} @Override
public void run() { final Selector selector = this.selector;
Set<SelectionKey> keys = null; for (;;) {
++reactCount; try {
selector.select(500L); //从注册队列中取出AbstractConnection之后注册读事件
//之后做一些列操作,请参考下面注释
register(selector);
keys = selector.selectedKeys(); for (SelectionKey key : keys) {
AbstractConnection con = null; try {
Object att = key.attachment(); if (att != null) {
con = (AbstractConnection) att; if (key.isValid() && key.isReadable()) { try { //异步读取数据并处理数据
con.asynRead();
} catch (IOException e) {
con.close("program err:" + e.toString()); continue;
} catch (Exception e) {
LOGGER.debug("caught err:", e);
con.close("program err:" + e.toString()); continue;
}
} if (key.isValid() && key.isWritable()) { //异步写数据
con.doNextWriteCheck();
}
} else {
key.cancel();
}
} catch (CancelledKeyException e) { if (LOGGER.isDebugEnabled()) {
LOGGER.debug(con + " socket key canceled");
}
} catch (Exception e) {
LOGGER.warn(con + " " + e);
}
}
} catch (Exception e) {
LOGGER.warn(name, e);
} finally { if (keys != null) {
keys.clear();
}
}
}
} private void register(Selector selector) {
AbstractConnection c = null; if (registerQueue.isEmpty()) { return;
} while ((c = registerQueue.poll()) != null) { try { //注册读事件
((NIOSocketWR) c.getSocketWR()).register(selector); //连接注册,对于FrontendConnection是发送HandshakePacket并异步读取响应
//响应为AuthPacket,读取其中的信息,验证用户名密码等信息,如果符合条件
//则发送OkPacket
c.register();
} catch (Exception e) {
c.close("register err" + e.toString());
}
}
}
}
因为NIOAcceptor线程和RW线程这两个都会操作RW线程的注册队列,所以要用ConcurrentLinkedQueue RW线程不断检查selector中需要响应的事件,并如果注册队列不为空,就不断注册其中的AbstractConnection,在这里就是FrontendConnection。 之后执行FrontendConnection的register()方法:
@Override
public void register() throws IOException { if (!isClosed.get()) { // 生成认证数据
byte[] rand1 = RandomUtil.randomBytes(8); byte[] rand2 = RandomUtil.randomBytes(12); // 保存认证数据
byte[] seed = new byte[rand1.length + rand2.length];
System.arraycopy(rand1, 0, seed, 0, rand1.length);
System.arraycopy(rand2, 0, seed, rand1.length, rand2.length); this.seed = seed; // 发送握手数据包
HandshakePacket hs = new HandshakePacket();
hs.packetId = 0;
hs.protocolVersion = Versions.PROTOCOL_VERSION;
hs.serverVersion = Versions.SERVER_VERSION;
hs.threadId = id;
hs.seed = rand1;
hs.serverCapabilities = getServerCapabilities();
hs.serverCharsetIndex = (byte) (charsetIndex & 0xff);
hs.serverStatus = 2;
hs.restOfScrambleBuff = rand2; // 异步写,本节就讲到这里
hs.write(this); // 异步读取并处理,这个与RW线程中的asynRead()相同,之后客户端收到握手包返回AuthPacket(就是下一节)就是从这里开始看。
this.asynRead();
}
}
这个方法就是生成HandshakePacket并发送出去,之后异步读取响应。 之前的示例中MySql的HandshakePacket结构: 可以总结出: HandshakePacket:
packet length(3 bytes)
packet number (1)
protocol version (1)
version (null terminated string)
thread id (4)
salt (8)
server capabilities (2)
server charset (1)
server status (2)
unused (13)
salt (12)
0x00 --- 结束
这里我们看下MyCat中的实现这一部分MySql协议栈的packet类结构: 这里可以看出,每个包都实现了自己的包长度和信息方法,并且针对前段后端连接都有读写方法实现,所以,之后读写数据都会根据场景不同调用这些类中的方法。这些包就是整个MySql协议栈除逻辑外的内容实现。 HandshakePacket.write(FrontendConnection c)方法将上面传入的数据封装成ByteBuffer,并传入给FrontendConnection c的write(ByteBuffer buffer),这个方法直接继承自AbstractConnection:
public final void write(ByteBuffer buffer) { //首先判断是否为压缩协议
if(isSupportCompress())
{ //CompressUtil为压缩协议辅助工具类
ByteBuffer newBuffer= CompressUtil.compressMysqlPacket(buffer,this,compressUnfinishedDataQueue); //将要写的数据先放入写缓存队列
writeQueue.offer(newBuffer);
} else
{ //将要写的数据先放入写缓存队列
writeQueue.offer(buffer);
} try { //处理写事件,这个方法比较复杂,需要重点分析其思路
this.socketWR.doNextWriteCheck();
} catch (Exception e) {
LOGGER.warn("write err:", e); this.close("write err:" + e);
}
}
如代码注释中所述,先将要写的数据放入写缓冲队列,之后调用NIOSocketWR.doNextWriteCheck()处理写事件。
public void doNextWriteCheck() { //检查是否正在写,看CAS更新writing值是否成功
if (!writing.compareAndSet(false, true)) { return;
} try { //利用缓存队列和写缓冲记录保证写的可靠性,返回true则为全部写入成功
boolean noMoreData = write0(); //因为只有一个线程可以成功CAS更新writing值,所以这里不用再CAS
writing.set(false); //如果全部写入成功而且写入队列为空(有可能在写入过程中又有新的Bytebuffer加入到队列),则取消注册写事件
//否则,继续注册写事件
if (noMoreData && con.writeQueue.isEmpty()) { if ((processKey.isValid() && (processKey.interestOps() & SelectionKey.OP_WRITE) != 0)) {
disableWrite();
}
} else { if ((processKey.isValid() && (processKey.interestOps() & SelectionKey.OP_WRITE) == 0)) {
enableWrite(false);
}
}
} catch (IOException e) { if (AbstractConnection.LOGGER.isDebugEnabled()) {
AbstractConnection.LOGGER.debug("caught err:", e);
}
con.close("err:" + e);
}
} private boolean write0() throws IOException { int written = 0;
ByteBuffer buffer = con.writeBuffer; if (buffer != null) { //只要写缓冲记录中还有数据就不停写入,但如果写入字节为0,证明网络繁忙,则退出
while (buffer.hasRemaining()) {
written = channel.write(buffer); if (written > 0) {
con.netOutBytes += written;
con.processor.addNetOutBytes(written);
con.lastWriteTime = TimeUtil.currentTimeMillis();
} else { break;
}
} //如果写缓冲中还有数据证明网络繁忙,计数并退出,否则清空缓冲
if (buffer.hasRemaining()) {
con.writeAttempts++; return false;
} else {
con.writeBuffer = null;
con.recycle(buffer);
}
} //读取缓存队列并写channel
while ((buffer = con.writeQueue.poll()) != null) { if (buffer.limit() == 0) {
con.recycle(buffer);
con.close("quit send"); return true;
}
buffer.flip(); while (buffer.hasRemaining()) {
written = channel.write(buffer); if (written > 0) {
con.lastWriteTime = TimeUtil.currentTimeMillis();
con.netOutBytes += written;
con.processor.addNetOutBytes(written);
con.lastWriteTime = TimeUtil.currentTimeMillis();
} else { break;
}
} //如果写缓冲中还有数据证明网络繁忙,计数,记录下这次未写完的数据到写缓冲记录并退出,否则回收缓冲
if (buffer.hasRemaining()) {
con.writeBuffer = buffer;
con.writeAttempts++; return false;
} else {
con.recycle(buffer);
}
} return true;
} private void disableWrite() { try {
SelectionKey key = this.processKey;
key.interestOps(key.interestOps() & OP_NOT_WRITE);
} catch (Exception e) {
AbstractConnection.LOGGER.warn("can't disable write " + e + " con "
+ con);
}
} private void enableWrite(boolean wakeup) { boolean needWakeup = false; try {
SelectionKey key = this.processKey;
key.interestOps(key.interestOps() | SelectionKey.OP_WRITE);
needWakeup = true;
} catch (Exception e) {
AbstractConnection.LOGGER.warn("can't enable write " + e);
} if (needWakeup && wakeup) {
processKey.selector().wakeup();
}
}
注释已经很详细,如此执行完,便成功将握手包发送给了客户端。 在这里稍微吐槽下,由于MyCat在网络通信上同时做了AIO和NIO,但是在设计上AbstractionConnection和这些并没有关系。但是又涉及到缓存队列,所以设计上出现了一些如下的类模式: 这样应该是不推荐这么设计的,目前我还没想好如何去改善
更多网易技术、产品、运营经验分享请点击。
相关文章:
【推荐】 质量报告之我见
【推荐】 揭秘:网上抽奖系统如何防止刷奖
【推荐】 BRVAH(让RecyclerView变得更高效) (2)
数据库路由中间件MyCat - 源代码篇(2)的更多相关文章
- 数据库路由中间件MyCat - 源代码篇(1)
此文已由作者张镐薪授权网易云社区发布. 欢迎访问网易云社区,了解更多网易技术产品运营经验. 进入了源代码篇,我们先从整体入手,之后拿一个简单流程前端连接建立与认证作为例子,理清代码思路和设计模式.然后 ...
- 数据库路由中间件MyCat - 源代码篇(13)
此文已由作者张镐薪授权网易云社区发布. 欢迎访问网易云社区,了解更多网易技术产品运营经验. 4.配置模块 4.2 schema.xml 接上一篇,接下来载入每个schema的配置(也就是每个MyCat ...
- 数据库路由中间件MyCat - 源代码篇(7)
此文已由作者张镐薪授权网易云社区发布. 欢迎访问网易云社区,了解更多网易技术产品运营经验. 3. 连接模块 3.4 FrontendConnection前端连接 构造方法: public Fronte ...
- 数据库路由中间件MyCat - 源代码篇(15)
此文已由作者张镐薪授权网易云社区发布. 欢迎访问网易云社区,了解更多网易技术产品运营经验. public static void handle(String stmt, ServerConnectio ...
- 数据库路由中间件MyCat - 源代码篇(17)
此文已由作者张镐薪授权网易云社区发布. 欢迎访问网易云社区,了解更多网易技术产品运营经验. 调用processInsert(sc,schema,sqlType,origSQL,tableName,pr ...
- 数据库路由中间件MyCat - 源代码篇(14)
此文已由作者张镐薪授权网易云社区发布. 欢迎访问网易云社区,了解更多网易技术产品运营经验. 对于表的dataNode对应关系,有个特殊配置即类似dataNode="distributed(d ...
- 数据库路由中间件MyCat - 源代码篇(4)
此文已由作者易国强授权网易云社区发布. 欢迎访问网易云社区,了解更多网易技术产品运营经验. 2. 前端连接建立与认证 Title:MySql连接建立以及认证过程client->MySql:1.T ...
- 数据库路由中间件MyCat - 源代码篇(16)
此文已由作者张镐薪授权网易云社区发布. 欢迎访问网易云社区,了解更多网易技术产品运营经验. 5. 路由模块 真正取得RouteResultset的步骤:AbstractRouteStrategy的ro ...
- 数据库路由中间件MyCat - 源代码篇(10)
此文已由作者张镐薪授权网易云社区发布. 欢迎访问网易云社区,了解更多网易技术产品运营经验. 3. 连接模块 3.5 后端连接 3.5.2 后端连接获取与维护管理 还是那之前的流程, st=>st ...
随机推荐
- Hive建模
Hive建模 1.介绍 Hive作为数据仓库,同关系型数据库开发过程类似,都需要先进行建模,所谓建模,就是对表之间指定关系方式.建模在hive中大致分为星型.雪花型和星座型.要对建模深入理解,首先需要 ...
- java设置随机数教程
java作为程序猿开发人员都在使用的一款编程语言,许多入门的朋友都陷入了一个简单的问题就是,使用java开发时随机数要怎么设置?java怎么设置随机数?经常会有地方需要用到随机数,不用着急,一起来看看 ...
- 笨办法学Python(八)
习题 8: 打印,打印 formatter = "%r %r %r %r" print formatter % (1, 2, 3, 4) print formatter % (&q ...
- Metasploitable渗透测试实战——生成木马
攻击机:kali 目标机:windows 1.生成木马 wincap发送至本机 2.进入msf (命令:msfconsole)启动监听 3.当目标点击test.exe(可伪装)时,触发后门,实现入 ...
- Kubernetes解决了Docker使用中的哪些问题?
kubernetes是谷歌开源的容器集群管理系统,是Google多年大规模容器管理技术Borg的开源版本 (1)基于容器的应用部署.维护和滚动升级 (2)网络,建立容器之间的通信子网如隧道.路由等,解 ...
- Spring 上下文操作工具类 ContextUtils
ContextUtils.java package com.java.config; import org.springframework.beans.BeansException; import o ...
- Python 3 collections.defaultdict() 与 dict的使用和区别
综述: 这里的defaultdict(function_factory)构建的是一个类似dictionary的对象,其中keys的值,自行确定赋值,但是values的类型,是function_fact ...
- quartz的持久化任务调度使用应用的dataSource
Quartz提供两种基本作业存储类型--->第一种类型叫做RAMJobStore: 最佳的性能,因为内存中数据访问最快 不足之处是缺乏数据的持久性,当程序路途停止或系统崩溃时,所 ...
- SQL基础语句汇总
连接数据库 1 mysql -h10.20.66.32 -uroot -p123456 -h后面是mysqlServer所在地址,-u后面是用户名,-p后面是密码 查看数据库 1 show datab ...
- mac安装mysql及workbench
安装mysql 登录MySQL网站 打开网站Download MySQL Community Server,选择下方的dmg文件下载 点击download,此处为8.0.11版本 然后选择no tha ...