数据库路由中间件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 ...
随机推荐
- Sundy_Android开发深入浅出和高级开发视频教程
Sundy_Android开发深入浅出和高级开发视频教程 放于播音员的网盘中又名:android零基础到高级软件开发工程师培训课程全集(400多讲) 1.课程介绍 2.java重点难点 3.版本控制- ...
- C++ new new[]详解
精髓: operator new()完成的操作一般只是分配内存:而构造函数的调用(如果需要)是在new运算符中完成的. operator new和new 运算符是不同的,operator new只分配 ...
- 有权并查集,Poj(1988)
题目链接:http://poj.org/problem?id=1988 题目大意: 有n个从1到n编号的箱子,将每个箱子当做一个栈,对这些箱子进行p次操作,每次操作分别为以下两种之一: 输入 M x ...
- 数长方形有多少个?POJ(1693)
题目链接:http://poj.org/problem?id=1693 解题报告: 随机选两根横的,再找一下与这两根横线相交的竖线有多少根,m,那么就有(m-1)*m/2个长方形. #include ...
- 问题 C: B 统计程序设计基础课程学生的平均成绩
题目描述 程序设计基础课程的学生成绩出来了,老师需要统计出学生个数和平均成绩.学生信息的输入如下: 学号(num) 学生姓名(name) ...
- Max answer(The Preliminary Contest for ICPC China Nanchang National Invitational)
Alice has a magic array. She suggests that the value of a interval is equal to the sum of the values ...
- 配置伪静态(URL重写)
本篇借鉴了很多文章,这里做个记录. 有时我们的导航栏出现xx.aspx?id=x&name=xx 等等这样,会显得不好看,我们可以利用伪静态来美化我们的导航栏,伪静态的形式可以自己定义,本质还 ...
- How good are detection proposals, really?
How good are detection proposals, really? J. Hosang, R. Benenson, B. Schiele Oral at BMVC 2014 http: ...
- ImportError : cannot import name main
当我们有时候安装不成功插件或者其他模块时候,会有pip报错hu@hu-VirtualBox:~/下载/MySQL-python-1.2.4b4$ pip install pymysqlTracebac ...
- Hive[6] HiveQL 查询
6.1 SELECT ... FROM 语句 hive> SELECT name,salary FROM employees; --普通查询 hive>SELECT e.n ...