连接方式
TCP/IP(linux)
这一般意义上也可以称为socket,使用tcp/ip协议来连接。一般的远程连接都是这种方式。
Unix Socket(linux)
这边的指的是unix socket,
UNIX Socket连接方式其实不是一个网络协议,所以只能在MySQL客户端和数据库实例在同一台服务器上的情况下使用
比如mysql登入使用 socket=/mysql/tmp/mysql3006.sock
name pipe(windows)
named pipe方式对于远程访问是无效的,要想达到远程访问的目的,即Win本地SQL客户端到Linux远程SQL服务器
share memory(windows)
原理
建立连接的过程
1.server 监听端口
2.client 向server建立TCP连接
3.server 向client发送挑战码报文(报文详细内容在下文中有分析)
4.client 使用挑战码加密密码,将加密后的密码包含在回包中,发送给server
5.server 根据client的回包,验证密码的有效性,给client发送ok包或error包
这边图中的第2步这边实线是由server发起,原因就是进过tcp的三次握手后,先由服务端发送一个是通知client需要的发送模板的协议,报文格式。所以是server发起。
这里的挑战码是指:mysql认证采用的是挑战/应答的方式,挑战码用于加密client输入的密码,从而避免了网络传输明文密码,服务器生成该挑战码并发送给客户端,由客户端进行处理并返回相应结果,然后服务器检查是否与预期的结果相同,从而完成用户认证的过程
代码解析
网络初始化的入口在network_init
在network_init打断点。
进入
//循环监控端口
void connection_event_loop()
{
Connection_handler_manager *mgr= Connection_handler_manager::get_instance();
while (!abort_loop)
{
Channel_info *channel_info= m_listener->listen_for_connection_event();
if (channel_info != NULL)
mgr->process_new_connection(channel_info);
}
}
进入到listen_for_connection_event()
获取tcp连接
connect_sock= mysql_socket_accept(key_socket_client_connection, listen_sock,
(struct sockaddr *)(&cAddr), &length);
Channel_info* channel_info= NULL;
if (is_unix_socket) //判断是什么连接方式
channel_info= new (std::nothrow) Channel_info_local_socket(connect_sock); //通过socket
else
channel_info= new (std::nothrow) Channel_info_tcpip_socket(connect_sock);//通过tcp
if (channel_info == NULL)
{
(void) mysql_socket_shutdown(connect_sock, SHUT_RDWR);
(void) mysql_socket_close(connect_sock);
connection_errors_internal++;
return NULL;
}
return channel_info;
}
我第一次是直接[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ss8AIXJd-1616428222407)(en-resource://database/7592:1)]
(gdb) p is_unix_socket
$8 = true
第二次使用-h ip来登入
channel_info= new (std::nothrow) Channel_info_tcpip_socket(connect_sock);
(gdb) p channel_info
$2 = (Channel_info *) 0x45d1660
返回连接后调用process_new_connection 来创建新的连接。
Connection_handler_manager::process_new_connection (this=0x3a57b50, channel_info=0x45d1660) at /home/mysql/mysql-5.7.29/sql/conn_handler/connection_handler_manager.cc:268
Per_thread_connection_handler::add_connection (this=0x3a4e510, channel_info=0x45d1660) at /home/mysql/mysql-5.7.29/sql/conn_handler/connection_handler_per_thread.cc:401
**进入判断 判断是否超过max_connections,里面每次去判断都会加1**
如果报错则返回错误
bool Connection_handler_manager::check_and_incr_conn_count()
{
bool connection_accepted= true;
mysql_mutex_lock(&LOCK_connection_count);
/*
Here we allow max_connections + 1 clients to connect
(by checking before we increment by 1).
The last connection is reserved for SUPER users. This is
checked later during authentication where valid_connection_count()
is called for non-SUPER users only.
*/
if (connection_count > max_connections)
{
connection_accepted= false;
m_connection_errors_max_connection++;
}
else
{
++connection_count;
if (connection_count > max_used_connections)
{
max_used_connections= connection_count;
max_used_connections_time= (ulong)my_time(0);
}
}
mysql_mutex_unlock(&LOCK_connection_count);
return connection_accepted;
}
这边可以打印看下,默认的是:
(gdb) p max_connections
$5 = 151
(gdb) p connection_count
$6 = 1
151个连接,
如果没有超过,则进入add_connection
{
int error= 0;
my_thread_handle id;
DBUG_ENTER("Per_thread_connection_handler::add_connection");
// Simulate thread creation for test case before we check thread cache
DBUG_EXECUTE_IF("fail_thread_create", error= 1; goto handle_error;);
if (!check_idle_thread_and_enqueue_connection(channel_info)) //是否有thread cache
DBUG_RETURN(false);
/*
There are no idle threads avaliable to take up the new
connection. Create a new thread to handle the connection
*/
channel_info->set_prior_thr_create_utime();
error= mysql_thread_create(key_thread_one_connection, &id,
&connection_attrib,
handle_connection,
(void*) channel_info);
#ifndef DBUG_OFF
handle_error:
#endif // !DBUG_OFF
if (error)
{
connection_errors_internal++;
if (!create_thd_err_log_throttle.log())
sql_print_error("Can't create thread to handle new connection(errno= %d)",
error);
channel_info->send_error_and_close_channel(ER_CANT_CREATE_THREAD,
error, true);
Connection_handler_manager::dec_connection_count();
DBUG_RETURN(true);
}
Global_THD_manager::get_instance()->inc_thread_created();
DBUG_PRINT("info",("Thread created"));
DBUG_RETURN(false);
}
在check_idle_thread_and_enqueue_connection 有一个参数是空闲的thread缓存线程
如果有就直接将channelinfo插入队列并唤醒阻塞的线程
其中
if (Per_thread_connection_handler::blocked_pthread_count > wake_pthread)
我的理解应该是blocked_pthread_count缓存的连接数,而wake_pthread目前需要申请的线程数,不知道理解的对不对。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qt2oWfV9-1616428222410)(en-resource://database/7594:1)]
我这边是有连接的,不需要创建
waiting_channel_info_list->push_back(channel_info);把channel_info加入到队列中,并向空闲线程发送信号
如果是没有缓存线程,比如重新退出后:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WAcjOTMk-1616428222414)(en-resource://database/7596:1)]
两个值都是0
则需要创建新的线程,进入mysql_thread_create
#ifdef HAVE_PSI_THREAD_INTERFACE
#define mysql_thread_create(K, P1, P2, P3, P4) \
inline_mysql_thread_create(K, P1, P2, P3, P4)
#else
#define mysql_thread_create(K, P1, P2, P3, P4) \
my_thread_create(P1, P2, P3, P4)
进入
static inline int inline_mysql_thread_create(
PSI_thread_key key,
my_thread_handle *thread, const my_thread_attr_t *attr,
my_start_routine start_routine, void *arg)
{
int result;
result= PSI_THREAD_CALL(spawn_thread)(key, thread, attr, start_routine, arg);
return result;
}
后面具体的入口是:
extern "C" void *handle_connection(void *arg)
my_thread_init
init_new_thd //初始化进程
到这里创建连接其实已经结束了。
下面就是等待用户的操作。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bBkZiw4e-1616428222419)(en-resource://database/7598:1)]
最后当进入连接后,会继续在
73 Channel_info *channel_info= m_listener->listen_for_connection_event();
at /home/mysql/mysql-5.7.29/sql/conn_handler/connection_acceptor.h:74
等待
check_and_incr_conn_count
第二阶段
第一次循环执行prepare,后面跳过
300 if (thd_prepare_connection(thd))
这里面有
初始化lex词法分析器
891 lex_start(thd);
rc= login_connection(thd); //身形身份认证
thd->get_protocol_classic()->set_read_timeout(connect_timeout); //这里的connect_timeout,就是获取连接的超时时间,可以设置。
进入 check_connection(thd)
执行到do_command(thd)是具体的执行sql
handle_connection (arg=0x551cfa0) at /home/mysql/mysql-5.7.29/sql/conn_handler/connection_handler_per_thread.cc:313
313 if (do_command(thd))
这一层的循环用来处理读取请求,每当处理完一条命令,就会循环一次。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HamxXiiH-1616428222421)(en-resource://database/7604:1)]
当执行一条sql的时候
往下可以看到我们常见设置超时时间
939 my_net_set_read_timeout(net, thd->variables.net_wait_timeout);
(gdb) p thd->variables.net_wait_timeout
$3 = 28800
再下去就是
return_value= dispatch_command(thd, &com_data, command);
就是真正的执行sql
到这边连接基本上大体的流程走完了,看了好久。
最后看下连接权限这块的内容
主要是在
check_connection (thd=0x7f58e0012190) at /home/mysql/mysql-5.7.29/sql/sql_connect.cc:503
503 uint connect_errors= 0;
到
auth_rc= acl_authenticate(thd, COM_CONNECT);
到
int check_for_max_user_connections(THD *thd, const USER_CONN *uc)
这里顺表说一下USER_CONN的结构体
在sql_connect.h
typedef struct user_conn {
/*
Pointer to user+host key (pair separated by '\0') defining the entity
for which resources are counted (By default it is user account thus
priv_user/priv_host pair is used. If --old-style-user-limits option
is enabled, resources are counted for each user+host separately).
*/
char *user;
/* Pointer to host part of the key. */
char *host;
/**
The moment of time when per hour counters were reset last time
(i.e. start of "hour" for conn_per_hour, updates, questions counters).
*/
ulonglong reset_utime;
/* Total length of the key. */
size_t len;
/* Current amount of concurrent connections for this account. */
uint connections;
/*
Current number of connections per hour, number of updating statements
per hour and total number of statements per hour for this account.
*/
uint conn_per_hour, updates, questions;
/* Maximum amount of resources which account is allowed to consume. */
USER_RESOURCES user_resources;
} USER_CONN;
主要的资源限制在这里:
int check_for_max_user_connections(THD *thd, const USER_CONN *uc)
{
int error=0;
Host_errors errors;
DBUG_ENTER("check_for_max_user_connections");
mysql_mutex_lock(&LOCK_user_conn);
if (global_system_variables.max_user_connections &&
!uc->user_resources.user_conn &&
global_system_variables.max_user_connections < (uint) uc->connections)
{
my_error(ER_TOO_MANY_USER_CONNECTIONS, MYF(0), uc->user);
error=1;
errors.m_max_user_connection= 1;
goto end;
}
thd->time_out_user_resource_limits();
if (uc->user_resources.user_conn &&
uc->user_resources.user_conn < uc->connections)
{
my_error(ER_USER_LIMIT_REACHED, MYF(0), uc->user,
"max_user_connections",
(long) uc->user_resources.user_conn);
error= 1;
errors.m_max_user_connection= 1;
goto end;
}
if (uc->user_resources.conn_per_hour &&
uc->user_resources.conn_per_hour <= uc->conn_per_hour)
{
my_error(ER_USER_LIMIT_REACHED, MYF(0), uc->user,
"max_connections_per_hour",
(long) uc->user_resources.conn_per_hour);
error=1;
errors.m_max_user_connection_per_hour= 1;
goto end;
}
thd->increment_con_per_hour_counter();
end:
if (error)
{
thd->decrement_user_connections_counter();
/*
The thread may returned back to the pool and assigned to a user
that doesn't have a limit. Ensure the user is not using resources
of someone else.
*/
thd->set_user_connect(NULL);
}
mysql_mutex_unlock(&LOCK_user_conn);
if (error)
{
inc_host_errors(thd->m_main_security_ctx.ip().str, &errors);
}
DBUG_RETURN(error);
}
对应上面提到的建立连接的过程
1.server 监听端口
2.client 向server建立TCP连接
3.server 向client发送挑战码报文(报文详细内容在下文中有分析)
4.client 使用挑战码加密密码,将加密后的密码包含在回包中,发送给server
5.server 根据client的回包,验证密码的有效性,给client发送ok包或error包
从
2227 res= do_auth_once(thd, auth_plugin_name, &mpvio);
(gdb) s
do_auth_once (thd=0x7fa64c000b70, auth_plugin_name=…, mpvio=0x7fa6c01203b0)
at /home/mysql/mysql-5.7.29/sql/auth/sql_authentication.cc:1930
1930 DBUG_ENTER(“do_auth_once”);
进入:
1952 st_mysql_auth *auth= (st_mysql_auth *) plugin_decl(plugin)->info;
(gdb) n
1953 res= auth->authenticate_user(mpvio, &mpvio->auth_info);
(gdb) s
native_password_authenticate (vio=0x7fa6c01203b0, info=0x7fa6c01203c8) at /home/mysql/mysql-5.7.29/sql/auth/sql_authentication.cc:2713
2713 MPVIO_EXT *mpvio= (MPVIO_EXT *) vio;
(gdb) n
2715 DBUG_ENTER("native_password_authenticate");
具体看native_password_authenticate函数
static int native_password_authenticate(MYSQL_PLUGIN_VIO *vio,
MYSQL_SERVER_AUTH_INFO *info)
{
uchar *pkt;
int pkt_len;
MPVIO_EXT *mpvio= (MPVIO_EXT *) vio;
DBUG_ENTER("native_password_authenticate");
/* generate the scramble, or reuse the old one */
//生成挑战码
if (mpvio->scramble[SCRAMBLE_LENGTH])
generate_user_salt(mpvio->scramble, SCRAMBLE_LENGTH + 1);
/* send it to the client */
//发送给客户端
if (mpvio->write_packet(mpvio, (uchar*) mpvio->scramble, SCRAMBLE_LENGTH + 1))
DBUG_RETURN(CR_AUTH_HANDSHAKE);
/* reply and authenticate */
/* read the reply with the encrypted password */
//等待客户端返回使用挑战码加密后的密码。
if ((pkt_len= mpvio->read_packet(mpvio, &pkt)) < 0)
DBUG_RETURN(CR_AUTH_HANDSHAKE);
DBUG_PRINT("info", ("reply read : pkt_len=%d", pkt_len));
#ifdef NO_EMBEDDED_ACCESS_CHECKS
DBUG_RETURN(CR_OK);
#endif /* NO_EMBEDDED_ACCESS_CHECKS */
DBUG_EXECUTE_IF("native_password_bad_reply",
{
/* This should cause a HANDSHAKE ERROR */
pkt_len= 12;
}
);
if (mysql_native_password_proxy_users)
{
*info->authenticated_as= PROXY_FLAG;
DBUG_PRINT("info", ("mysql_native_authentication_proxy_users is enabled, setting authenticated_as to NULL"));
}
if (pkt_len == 0) //有没有设置密码
DBUG_RETURN(mpvio->acl_user->salt_len != 0 ?
CR_AUTH_USER_CREDENTIALS : CR_OK);
info->password_used= PASSWORD_USED_YES;
if (pkt_len == SCRAMBLE_LENGTH)
{
if (!mpvio->acl_user->salt_len)
DBUG_RETURN(CR_AUTH_USER_CREDENTIALS);
DBUG_RETURN(check_scramble(pkt, mpvio->scramble, mpvio->acl_user->salt) ?
CR_AUTH_USER_CREDENTIALS : CR_OK);
}
my_error(ER_HANDSHAKE_ERROR, MYF(0));
DBUG_RETURN(CR_AUTH_HANDSHAKE);
}
mpvio中环境信息更新到线程信息THD中, successful login
在mpvio->read_packet会去读取加密串
check_scramble(pkt, mpvio->scramble, mpvio->acl_user->salt) 是真正去检查密码
server_mpvio_update_thd(thd, &mpvio);更新thd中的信息
从上面的逻辑看
一个用户成功建立连接后,即使你用管理员账号对这个用户的权限做了修改,也不会影响已经存在连接的权限。修改完成后,只有再新建的连接才会使用新的权限设置
參考:
http://mysql.taobao.org/monthly/2018/02/07/