数据库路由中间件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 ...
随机推荐
- 原生Js在各大浏览器上、火狐、ie、谷歌、360等出现的不兼容问题。
1 document.getElementsByName("name") 在Ie低版本,360普通版本,以及火狐低版本不支持. 2 element.innerText 在低版本的 ...
- Python用户交互与流程控制
1. 用户交互 python3通过input实现用户交互,与python2的raw_input一样,接收的值都转换成字符串格式.python2中也有一个input,而python2中的input接收的 ...
- April 26 2017 Week 17 Wednesday
We read the world wrong and say that it deceives us. 我们把世界看错了,反而说它欺骗了我们. It is not a cakewalk to see ...
- C4C销售订单中业务伙伴的自动决定功能Partner determination procedure
例子:我新建一个Sales Order,account 字段选择ID为1001的Account:Porter LLC 创建成功后,观察这个Sales Order的Involved Party里,Bil ...
- centos6.x yum 安装 mysql5.6 mysql5.7
先卸载低版本MYSQL yum remove mysql* rpm -ivh http://repo.mysql.com/mysql-community-release-el6.rpm yum ins ...
- 2017.10.29 C/C++/C#程序如何打成DLL动态库
C/C++程序如何打成DLL动态库: **1.在VS中新建main.h,添加如下内容:** extern "C" _declspec(dllexport) int onLoad() ...
- caffe RandomBrightness和RandomContrast
1. void RandomBrightness(const cv::Mat& in_img, cv::Mat* out_img, const float brightness_prob, c ...
- EF 状态版 增删改
1.利用 状态版 实现 增加 . 2.利用状态版 进行 删除 3. 利用状态版 实现 修改 ,修改 方法 有3种 第1种修改方法 第2种修改 方法 第3种修改方法
- System.Threading.Tasks
前言: 我们之前介绍了两种构建多线程软件的编程技术(使用异步委托或通过System.Threading的成员).这两个可以在任何版本的.NET平台工作. 关于System.Threading 的介绍 ...
- 批量删除xml文件中的<?xml version="1.0" ?>
#!/bin/shcd 'home/usrname/'ls cd '/home/usrname/VOC2007/Annotations/' for file in `ls /home/usrname/ ...