mysql用户连接过程

连接方式

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/

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值