本文将尝试解析 mysql.js源码
什么是Mysql
到底什么是mysql? 官方的介绍是 MySQL is the world's most popular open source database
如果让你用自己的语言来回答这个问题,你会怎么回答?
这是之前我在公司的一次分享会上提出的问题,看似简单的问题却没人回应,最后是leader回答了我:mysql是一种服务
。我觉得我挺认可这个答案的,完善一下就是 mysql是一种基于mysql协议的网络服务
。也许你会觉得这是一个非常片面的回答,因为mysql
的很多功能都没有体现出来,但是我觉得这是对mysql
最形象的描述。
到底要如何操作数据库
就我的经历来说,操作数据库一般就分为两种:工具(Navicat
、cli
)或者业务代码。业务代码中不同语言一般也会采用不同的方式,例如java
中可以使用jdbc
、或者spring注解
(实习时使用过,不太确定),node
可以使用mysqljs
。不论使用什么方式,最终都是为了查出数据,所以我就一直很好奇到底最原生的数据长什么样子:
例如你在cli
中查出来的数据长这样
但是使用mysqljs
你能得到的数据只能是对象的形式
从本质上来说:不同的工具、工具库都是通过解析mysql数据包、然后封装成某种数据结构再返回给我们。
Mysql协议
我不知道你们一般是怎么去学习一种协议,比如http协议,tcp协议。我觉得靠背他们的定义很难真正去理解,后来我发现最好的办法是用代码实现这种协议,只有实现了这种协议你才能真正理解它。
我有专门去查阅mysql协议
的介绍,发现都有些无法理解。简单总结下我个人的理解:mysql协议是一种基于tcp的有状态的应用层协议
。
mysql
底层都是socket
编程,通过socket
发送和接收一些二进制数据,它不像http
协议是纯文本的。
我举个例子,如果你能将下面代码交互过程发送的包全部手动解析出来(利用MysqlJS
执行上图中select * from demo1
),那你应该就理解了mysql协议。
receive: 74,0,0,0,10,56,46,48,46,50,56,0,10,0,0,0,52,98,3,117,1,60,112,125,0,255,255,255,2,0,255,223,21,0,0,0,0,0,0,0,0,0,0,46,93,40,123,32,30,122,95,114,17,33,28,0,99,97,99,104,105,110,103,95,115,104,97,50,95,112,97,115,115,119,111,114,100,0
...send: 66,0,0,1,207,243,6,0,0,0,0,0,33,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,114,111,111,116,0,20,187,226,180,160,90,88,116,96,100,1,118,223,174,254,246,59,21,19,104,204,115,104,101,112,101,110,103,0
receive: 7,0,0,2,0,0,0,2,0,0,0
...send: 20,0,0,0,3,115,101,108,101,99,116,32,42,32,102,114,111,109,32,100,101,109,111,49
receive: 1,0,0,1,5,47,0,0,2,3,100,101,102,7,115,104,101,112,101,110,103,5,100,101,109,111,49,5,100,101,109,111,49,4,78,65,77,69,4,78,65,77,69,12,33,0,44,1,0,0,253,0,0,0,0,0,45,0,0,3,3,100,101,102,7,115,104,101,112,101,110,103,5,100,101,109,111,49,5,100,101,109,111,49,3,65,71,69,3,65,71,69,12,63,0,11,0,0,0,3,0,0,0,0,0,45,0,0,4,3,100,101,102,7,115,104,101,112,101,110,103,5,100,101,109,111,49,5,100,101,109,111,49,3,83,69,88,3,83,69,88,12,33,0,44,1,0,0,254,0,0,0,0,0,43,0,0,5,3,100,101,102,7,115,104,101,112,101,110,103,5,100,101,109,111,49,5,100,101,109,111,49,2,105,100,2,105,100,12,63,0,11,0,0,0,3,3,80,0,0,0,53,0,0,6,3,100,101,102,7,115,104,101,112,101,110,103,5,100,101,109,111,49,5,100,101,109,111,49,7,99,111,117,110,116,114,121,7,99,111,117,110,116,114,121,12,33,0,30,0,0,0,253,0,0,0,0,0,5,0,0,7,254,0,0,34,0,16,0,0,8,5,108,105,115,105,51,2,49,56,3,231,148,183,1,50,251,18,0,0,9,7,119,97,110,103,119,117,50,2,49,56,3,229,165,179,1,51,251,17,0,0,10,8,122,104,97,110,103,115,97,110,2,49,54,1,54,1,52,251,12,0,0,11,6,229,188,160,228,184,137,251,251,1,53,251,14,0,0,12,6,231,142,139,228,186,148,2,50,48,0,1,54,251,5,0,0,13,254,0,0,34,0
这是我在mysqljs
中打断点截取的,是一次完整的查询过程中抓到的包。receive是接收的包,send是发送的包,数据格式都是Buffer
,由于显示不下,这里都转成了字符串展示。
简单说下解析规则:
首先肯定是不能简单的采用暴力toString
方法解析的,因为255
这种二进制数据toString
会乱码。
每个包都分为多个小包,包头固定4
个长度、例如第一个包74,0,0,0
就是包头的内容,前面三个就是包实体占得长度,解析方式为:
Parser.prototype.parseUnsignedNumber = function parseUnsignedNumber(bytes) {
if (bytes === 1) {
return this._buffer[this._offset++];
}
var buffer = this._buffer;
var offset = this._offset + bytes - 1;
var value = 0;
if (bytes > 4) {
var err = new Error('parseUnsignedNumber: Supports only up to 4 bytes');
err.offset = (this._offset - this._packetOffset - 1);
err.code = 'PARSER_UNSIGNED_TOO_LONG';
throw err;
}
while (offset >= this._offset) {
value = ((value << 8) | buffer[offset]) >>> 0;
offset--;
}
this._offset += bytes;
return value;
};
这是
mysqljs
的解析源码,参数bytes
就是包头长度,这里固定为3
,参数_offset
就是该数据包当前解析的位置,参数_buffer
是Buffer对象、记录当前数据包。包头的最后一个二进制数据是用来记录该包是当前会话的第几个包,从0开始记录
。
顺便介绍下发送时包头的解析方式:比如你要发送一个包给mysql,你的包实体有1000个二进制数据,那么你包头应该怎么写呢
PacketWriter.prototype.writeUnsignedNumber = function(bytes, value) {
this._allocate(bytes);
for (var i = 0; i < bytes; i++) {
this._buffer[this._offset++] = (value >> (i * 8)) & 0xff;
}
};
bytes
这里也固定是3
,value
就是包头的长度,我们可以先不管第一行代码,是用来保证_buffer
不越界的。所以你可以试试任意一个数字加密之后是否能解密成功。 接下来就开始解析包里面的内容了。通常第一个字符是用来表示下一次读取的长度,也是用来表示当前包是什么类型的。
Protocol.prototype._determinePacket = function(sequence) {
var firstByte = this._parser.peak();
if (sequence.determinePacket) {
var Packet = sequence.determinePacket(firstByte, this._parser);
if (Packet) {
return Packet;
}
}
switch (firstByte) {
case 0x00: return Packets.OkPacket;
case 0xfe: return Packets.EofPacket;
case 0xff: return Packets.ErrorPacket;
}
throw new Error('Could not determine packet, firstByte = ' + firstByte);
};
peak
方法就是用来读取当前解析位置的二进制数据,我们先不管sequence,它表示你发送的sql对象,每个sequence都会先尝试去解析返回的包,如果没有解析成功,就交由下面的switch兜底:OkPacket表示当前会话结束了、退出当前会话,执行下一个会话。EofPacket表示当前是结束包,统计一些信息的,ErrorPacket表示当前会话报错了,常见的有会话超时。
我们就拿最后一个
select
的返回结果举个例子
1,0,0,1,5,47,0,0,2,3,100,101,102
先读取到5
,表示查询的结果一共有5
列,然后读取下一个包一共47个字符,接下来读取到3
,正常数据包(非错误包),表示下一次读取三个字符:100, 101, 102
Parser.prototype.parseString = function (length) {
var offset = this._offset;
var end = offset + length;
var value = this._buffer.toString(this._encoding, offset, end);
this._offset = end;
return value;
};
直接使用
parseString
方法解析出字符串def
。
这里我只是举个例子,mysql底层的各种数据结构还是很多,比如日期,对应的解析方式也不同,我也没有全部研究,但是我觉得最终都是要解析成文本。
MysqlJs源码解读
mysql的解析规则大概是这样,但是具体的细节还有很多,本文只介绍部分数据包的解析方式。
var mysql = require('mysql');
var connection = mysql.createConnection({
host: 'localhost',
user: 'root',
password: 'root',
database: 'shepeng'
});
// connection.connect();
var query = 'zhangsan';
connection.query('select * from demo1 where name = ?', query, function (error, results, fields) {
if (error) throw error;
console.log(results);
// console.log('The fields is: ', fields);
});
最终只介绍这几行代码背后发生的事情,其他的情况请大家自行研究。由于源码过于冗长,本文只介绍重要的源码部分。
引入了MysqlJs
后、createConnection
方法创建了一个链接,实际上这时候并没有连接数据库,而是在下面执行的connect
方法才开始连接数据库。代码中进行了注释,是因为query
方法中会先判断是否连接再进行查询。
Connection.prototype.connect = function connect(options, callback) {
if (!callback && typeof options === 'function') {
callback = options;
options = {};
}
if (!this._connectCalled) {
this._connectCalled = true;
// Connect either via a UNIX domain socket or a TCP socket.
this._socket = (this.config.socketPath)
? Net.createConnection(this.config.socketPath)
: Net.createConnection(this.config.port, this.config.host);
// Connect socket to connection domain
if (Events.usingDomains) {
this._socket.domain = this.domain;
}
var connection = this;
this._protocol.on('data', function(data) {
console.log('...send:', data.join(','));
connection._socket.write(data);
});
this._socket.on('data', wrapToDomain(connection, function (data) {
console.log('receive:', data.join(','));
connection._protocol.write(data);
}));
this._protocol.on('end', function() {
connection._socket.end();
});
this._socket.on('end', wrapToDomain(connection, function () {
connection._protocol.end();
}));
this._socket.on('error', this._handleNetworkError.bind(this));
this._socket.on('connect', this._handleProtocolConnect.bind(this));
this._protocol.on('handshake', this._handleProtocolHandshake.bind(this));
this._protocol.on('initialize', this._handleProtocolInitialize.bind(this));
this._protocol.on('unhandledError', this._handleProtocolError.bind(this));
this._protocol.on('drain', this._handleProtocolDrain.bind(this));
this._protocol.on('end', this._handleProtocolEnd.bind(this));
this._protocol.on('enqueue', this._handleProtocolEnqueue.bind(this));
if (this.config.connectTimeout) {
var handleConnectTimeout = this._handleConnectTimeout.bind(this);
this._socket.setTimeout(this.config.connectTimeout, handleConnectTimeout);
this._socket.once('connect', function() {
this.setTimeout(0, handleConnectTimeout);
});
}
}
this._protocol.handshake(options, wrapCallbackInDomain(this, callback));
};
可以发现,Mysql.js
的本质就是Socket
连接,上图中抓到的包就是这在这里打印出来的。
然后我们看看query
方法
Protocol.prototype.query = function query(options, callback) {
return this._enqueue(new Sequences.Query(options, callback));
};
很简单的一行代码。本质上就是内部维护了一个队列,用来收集你要执行的操作,那么如何区分你要执行的操作呢?这里就引入了Sequences
对象。我们打开_enqueue
方法你会发现只是往你要执行的操作对象上注入一些回调事件、并没有触发_socket.write
。这是因为连接还没有建立,所有的操作必须先认证,建立连接后,再依次从队列中取出执行。
那么连接到底什么时候建立的呢,代码中并没有任何其他触发建立连接的操作了。我们仔细观察上图中收发到的包,会发现第一个包是先收到的,也就是说我们执行Net.createConnection
创建socket
后,是mysql
先联系我们的,而不需要我们主动发送连接请求。
实际上在你执行任何操作之前,Mysql.js
都会先注册一个Handshake
类型的Sequences
,目的就是用来处理mysql
发送过来的第一个包,包内容每次都不一样,里面包含了很多关于mysql
的基本信息,主要还有个用来加密密码的秘钥,第一个接收包的主要字段如下:
HandshakeInitializationPacket.prototype.parse = function(parser) {
this.protocolVersion = parser.parseUnsignedNumber(1);
this.serverVersion = parser.parseNullTerminatedString();
this.threadId = parser.parseUnsignedNumber(4);
this.scrambleBuff1 = parser.parseBuffer(8);
this.filler1 = parser.parseFiller(1);
this.serverCapabilities1 = parser.parseUnsignedNumber(2);
this.serverLanguage = parser.parseUnsignedNumber(1);
this.serverStatus = parser.parseUnsignedNumber(2);
this.protocol41 = (this.serverCapabilities1 & (1 << 9)) > 0;
if (this.protocol41) {
this.serverCapabilities2 = parser.parseUnsignedNumber(2);
this.scrambleLength = parser.parseUnsignedNumber(1);
this.filler2 = parser.parseFiller(10);
// scrambleBuff2 should be 0x00 terminated, but sphinx does not do this
// so we assume scrambleBuff2 to be 12 byte and treat the next byte as a
// filler byte.
this.scrambleBuff2 = parser.parseBuffer(12);
this.filler3 = parser.parseFiller(1);
} else {
this.filler2 = parser.parseFiller(13);
}
if (parser.reachedPacketEnd()) {
return;
}
// According to the docs this should be 0x00 terminated, but MariaDB does
// not do this, so we assume this string to be packet terminated.
this.pluginData = parser.parsePacketTerminatedString();
// However, if there is a trailing '\0', strip it
var lastChar = this.pluginData.length - 1;
if (this.pluginData[lastChar] === '\0') {
this.pluginData = this.pluginData.substr(0, lastChar);
}
};
其中scrambleBuff1
就是用来加密密码的秘钥, 加密过程不在本文讨论范围。
解析完成后将一些列数据(包括database
等信息)再返回给mysql
,mysql
会给我们返回个OkPacket
,这个时候就完成了握手,所有的会话就可以在断开之前畅通无阻了。
Protocol.prototype._emitPacket = function(packet) {
var packetWriter = new PacketWriter();
packet.write(packetWriter);
this.emit('data', packetWriter.toBuffer(this._parser));
if (this._config.debug) {
this._debugPacket(false, packet);
}
};
发送包的代码也就简短几行,本质上也很简单,把你要发的数据按顺序塞到一个Buffer
中,然后通过Socket
发送出去就完事了。但是需要注意的是一个大包里面可能有多个小包,每个小包要按包头(length
+number
)+包body的形式来拼接,就和你收到的包一样的格式。
下面看看如何处理接收到的包,主要是通过模块Parser.js
来进行解析
Parser.prototype.write = function write(chunk) {
this._nextBuffers.push(chunk);
while (!this._paused) {
var packetHeader = this._tryReadPacketHeader();
if (!packetHeader) {
break;
}
if (!this._combineNextBuffers(packetHeader.length)) {
break;
}
this._parsePacket(packetHeader);
}
};
while
循环不停的解析包头、然后通过包头来解析包内容,一直到整个包解析完成。
具体的解析过程这里就不赘述了,由于包的类型太多,大家可以自行研究。
最后
非常建议大家打个断点调试一下,对网络协议这一块会有一个比较深的认识。
也希望大家思考下如何借鉴mysql
协议实现http
协议。
如果还有什么疑问或者建议,可以多多交流,原创文章,文笔有限,才疏学浅,文中若有不正之处,万望告知。