一、创建连接 - TCP连接怎么创建
com.mysql.jdbc.ConnectionImpl#coreConnect方法:
JDBC源码如下:
private void coreConnect(Properties mergedProps) throws SQLException, IOException {
int newPort = 3306;
String newHost = "localhost";
String protocol = mergedProps.getProperty(NonRegisteringDriver.PROTOCOL_PROPERTY_KEY);
if (protocol != null) {
// "new" style URL
if ("tcp".equalsIgnoreCase(protocol)) {
newHost = normalizeHost(mergedProps.getProperty(NonRegisteringDriver.HOST_PROPERTY_KEY));
newPort = parsePortNumber(mergedProps.getProperty(NonRegisteringDriver.PORT_PROPERTY_KEY, "3306"));
} else if ("pipe".equalsIgnoreCase(protocol)) {
setSocketFactoryClassName(NamedPipeSocketFactory.class.getName());
String path = mergedProps.getProperty(NonRegisteringDriver.PATH_PROPERTY_KEY);
if (path != null) {
mergedProps.setProperty(NamedPipeSocketFactory.NAMED_PIPE_PROP_NAME, path);
}
} else {
// normalize for all unknown protocols
newHost = normalizeHost(mergedProps.getProperty(NonRegisteringDriver.HOST_PROPERTY_KEY));
newPort = parsePortNumber(mergedProps.getProperty(NonRegisteringDriver.PORT_PROPERTY_KEY, "3306"));
}
} else {
String[] parsedHostPortPair = NonRegisteringDriver.parseHostPortPair(this.hostPortPair);
newHost = parsedHostPortPair[NonRegisteringDriver.HOST_NAME_INDEX];
newHost = normalizeHost(newHost);
if (parsedHostPortPair[NonRegisteringDriver.PORT_NUMBER_INDEX] != null) {
newPort = parsePortNumber(parsedHostPortPair[NonRegisteringDriver.PORT_NUMBER_INDEX]);
}
}
this.port = newPort;
this.host = newHost;
// reset max-rows to default value
this.sessionMaxRows = -1;
this.io = new MysqlIO(newHost, newPort, mergedProps, getSocketFactoryClassName(), getProxy(), getSocketTimeout(),
this.largeRowSizeThreshold.getValueAsInt());
this.io.doHandshake(this.user, this.password, this.database);
if (versionMeetsMinimum(5, 5, 0)) {
// error messages are returned according to character_set_results which, at this point, is set from the response packet
this.errorMessageEncoding = this.io.getEncodingForHandshake();
}
}
看到
this.io = new MysqlIO(newHost, newPort, mergedProps, getSocketFactoryClassName(), getProxy(), getSocketTimeout(),
this.largeRowSizeThreshold.getValueAsInt());
这个MysqlIO的构造方法我们看下:
public MysqlIO(String host, int port, Properties props, String socketFactoryClassName, MySQLConnection conn, int socketTimeout,
int useBufferRowSizeThreshold) throws IOException, SQLException {
this.connection = conn;
if (this.connection.getEnablePacketDebug()) {
this.packetDebugRingBuffer = new LinkedList<StringBuilder>();
}
this.traceProtocol = this.connection.getTraceProtocol();
this.useAutoSlowLog = this.connection.getAutoSlowLog();
this.useBufferRowSizeThreshold = useBufferRowSizeThreshold;
this.useDirectRowUnpack = this.connection.getUseDirectRowUnpack();
this.logSlowQueries = this.connection.getLogSlowQueries();
this.reusablePacket = new Buffer(INITIAL_PACKET_SIZE);
this.sendPacket = new Buffer(INITIAL_PACKET_SIZE);
this.port = port;
this.host = host;
this.socketFactoryClassName = socketFactoryClassName;
this.socketFactory = createSocketFactory();
this.exceptionInterceptor = this.connection.getExceptionInterceptor();
try {
this.mysqlConnection = this.socketFactory.connect(this.host, this.port, props);
if (socketTimeout != 0) {
try {
this.mysqlConnection.setSoTimeout(socketTimeout);
} catch (Exception ex) {
/* Ignore if the platform does not support it */
}
}
this.mysqlConnection = this.socketFactory.beforeHandshake();
if (this.connection.getUseReadAheadInput()) {
this.mysqlInput = new ReadAheadInputStream(this.mysqlConnection.getInputStream(), 16384, this.connection.getTraceProtocol(),
this.connection.getLog());
} else if (this.connection.useUnbufferedInput()) {
this.mysqlInput = this.mysqlConnection.getInputStream();
} else {
this.mysqlInput = new BufferedInputStream(this.mysqlConnection.getInputStream(), 16384);
}
this.mysqlOutput = new BufferedOutputStream(this.mysqlConnection.getOutputStream(), 16384);
this.isInteractiveClient = this.connection.getInteractiveClient();
this.profileSql = this.connection.getProfileSql();
this.autoGenerateTestcaseScript = this.connection.getAutoGenerateTestcaseScript();
this.needToGrabQueryFromPacket = (this.profileSql || this.logSlowQueries || this.autoGenerateTestcaseScript);
if (this.connection.getUseNanosForElapsedTime() && TimeUtil.nanoTimeAvailable()) {
this.useNanosForElapsedTime = true;
this.queryTimingUnits = Messages.getString("Nanoseconds");
} else {
this.queryTimingUnits = Messages.getString("Milliseconds");
}
if (this.connection.getLogSlowQueries()) {
calculateSlowQueryThreshold();
}
} catch (IOException ioEx) {
throw SQLError.createCommunicationsException(this.connection, 0, 0, ioEx, getExceptionInterceptor());
}
}
核心在this.socketFactory.connect(this.host, this.port, props);方法里面。
这里我们就不说了。我这里最感兴趣的是:tcp连接创建后根据mysql应用层协议,数据库服务器收到socket建立时是要验证用户名和密码的,那么这里是发生在哪里呢?
上面已经解释了传输层的tcp三次握手已经建立,下一步就应该进行mysql应用层报文的验证了。那么我们猜想:客户端和数据库服务器建立tcp连接后,肯定要把用户名和密码组装成mysql协议约定的连接报文,通过tcp连接传递给服务器。我们继续向后看源码验证下。
二、JDBC创建连接后,如何组装mysql请求报文,然后发送给服务器
上面在执行coreConnect方法里面,创建完 new MysqlIO()后,紧接着执行了:
this.io.doHandshake(this.user, this.password, this.database);
我们进去看一下,这个方法太大了,我们只截取感兴趣的代码:
{
// Passwords can be 16 chars long
packet = new Buffer(packLength);
if ((this.clientParam & CLIENT_RESERVED) != 0) {
if ((versionMeetsMinimum(4, 1, 1) || ((this.protocolVersion > 9) && (this.serverCapabilities & CLIENT_PROTOCOL_41) != 0))) {
packet.writeLong(this.clientParam);
packet.writeLong(this.maxThreeBytes);
// charset, JDBC will connect as 'latin1', and use 'SET NAMES' to change to the desired charset after the connection is established.
packet.writeByte((byte) 8);
// Set of bytes reserved for future use.
packet.writeBytesNoNull(new byte[23]);
} else {
packet.writeLong(this.clientParam);
packet.writeLong(this.maxThreeBytes);
}
} else {
packet.writeInt((int) this.clientParam);
packet.writeLongInt(this.maxThreeBytes);
}
// User/Password data
packet.writeString(user, CODE_PAGE_1252, this.connection);
if (this.protocolVersion > 9) {
packet.writeString(Util.newCrypt(password, this.seed, this.connection.getPasswordCharacterEncoding()), CODE_PAGE_1252, this.connection);
} else {
packet.writeString(Util.oldCrypt(password, this.seed), CODE_PAGE_1252, this.connection);
}
if (this.useConnectWithDb) {
packet.writeString(database, CODE_PAGE_1252, this.connection);
}
send(packet, packet.getPosition());
}
看到没,packet包就是mysql验证密码,这个过程的报文(请求sql的报文可不长这样)。我们看下这个报文:packet先写8字节的clientParam,在写8字节的maxThreeBytes,在写1字节的8,在写23字节空数据。最后再写String类型的user,然后写加密后的密码,最后在写database。
最后一句send出去,给到服务器。服务器接收到这种类型的报文,就知道要验证用户名和密码,以及数据库名称了。
但是debug乱点的时候,验证执行的是下面代码
// switch to pluggable authentication if available
//
if ((this.serverCapabilities & CLIENT_PLUGIN_AUTH) != 0) {
proceedHandshakeWithPluggableAuthentication(user, password, database, buf);
return;
}
proceedHandshakeWithPluggableAuthentication这个方法,进去也是很复杂。但是可以看到,默认验证次数循环100次,如果100次还没得到结果,就报错。
源码省略了,反正只要知道jdbc在创建tcp之后,做了mysql应用层协议的一些事情。所以,创建连接很耗时,不仅要tcp三次握手,还要进行一次验证。3 + 2 = 5次网络来回,所以连接很宝贵,需要用连接池来管理,尽量减少连接的创建。
网友评论