2. 前端连接建立与认证
Created with Raphaël 2.1.0MySql连接建立以及认证过程clientclientMySqlMySql1.TCP连接请求2.接受TCP连接3.TCP连接建立4.握手包HandshakePacket5.认证包AuthPacket6.如果验证成功,则返回OkPacket7.默认会发送查询版本信息的包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和这些并没有关系。但是又涉及到缓存队列,所以设计上出现了一些如下的类模式:
这样应该是不推荐这么设计的,目前我还没想好如何去改善
Java 面试宝典是大明哥全力打造的 Java 精品面试题,它是一份靠谱、强大、详细、经典的 Java 后端面试宝典。它不仅仅只是一道道面试题,而是一套完整的 Java 知识体系,一套你 Java 知识点的扫盲贴。
它的内容包括:
- 大厂真题:Java 面试宝典里面的题目都是最近几年的高频的大厂面试真题。
- 原创内容:Java 面试宝典内容全部都是大明哥原创,内容全面且通俗易懂,回答部分可以直接作为面试回答内容。
- 持续更新:一次购买,永久有效。大明哥会持续更新 3+ 年,累计更新 1000+,宝典会不断迭代更新,保证最新、最全面。
- 覆盖全面:本宝典累计更新 1000+,从 Java 入门到 Java 架构的高频面试题,实现 360° 全覆盖。
- 不止面试:内容包含面试题解析、内容详解、知识扩展,它不仅仅只是一份面试题,更是一套完整的 Java 知识体系。
- 宝典详情:https://www.yuque.com/chenssy/sike-java/xvlo920axlp7sf4k
- 宝典总览:https://www.yuque.com/chenssy/sike-java/yogsehzntzgp4ly1
- 宝典进展:https://www.yuque.com/chenssy/sike-java/en9ned7loo47z5aw
目前 Java 面试宝典累计更新 400+ 道,总字数 42w+。大明哥还在持续更新中,下图是大明哥在 2024-12 月份的更新情况:
想了解详情的小伙伴,扫描下面二维码加大明哥微信【daming091】咨询
同时,大明哥也整理一套目前市面最常见的热点面试题。微信搜[大明哥聊 Java]或扫描下方二维码关注大明哥的原创公众号[大明哥聊 Java] ,回复【面试题】 即可免费领取。