MariaDB源码分析——VIO数据结构

VIO是一个数据结构,在include/violite.h中定义的说明中有一句话“This structure is for every connection on both sides.Note that it has a non-default move assignment operator, so if adding more members, you’ll need to update operator=.”,它的意思是说这个数据结构可以用来网络连接的服务端和客户端。后面就是说没有默认的操作符如果需要就自己搞一个。VIO主要是用在了网络通信Socket中的数据结构的封装。一般来说,封装的目的是为抽象,抽象的目的是为更有适应性,让程序在不同的平台或者说环境下的不修改或者改动最小。对具体动作的依赖降低,这也是设计原则里,依赖于抽象而不依赖于具体的行为动作。比如在处理不同的写入操作时就只考虑接口的实现,而不用考虑具体的细节的实现。
首先我们看一下实例化服务端的CONNECT类auto connect= new CONNECT(new_sock, sock.is_unix_domain_socket ? VIO_TYPE_SOCKET : VIO_TYPE_TCPIP, sock.is_extra_port ? extra_thread_scheduler : thread_scheduler),传入的形参有主线程accept的sock套接字、VIO的类型VIO_TYPE_SOCKET\VIO_TYPE_TCPIP、是否是通过extra port连接进来的(如果是就需要使用extra_thread_scheduler线程调度器,否则使用正常连接调度器thread_scheduler)。我们再看一下CONNECT类的定义,从构造函数可以看出VIO类型由enum_vio_type枚举类型定义,虽然CONNECT类中有VIO类型,但是还是没有根据VIO类型实例化出来相应的VIO结构体。

class CONNECT : public ilink {
public:
  MYSQL_SOCKET sock; // 主线程accept的sock套接字
  enum enum_vio_type vio_type; // VIO类型
  scheduler_functions *scheduler; // 调度器类型
  my_thread_id thread_id; // 主线程为该CONNECT分配的线程号
  /* Own variables */
  ulonglong    prior_thr_create_utime;
  static Atomic_counter<uint32_t> count;
  CONNECT(MYSQL_SOCKET sock_arg, enum enum_vio_type vio_type_arg, scheduler_functions *scheduler_arg): sock(sock_arg), vio_type(vio_type_arg), scheduler(scheduler_arg), thread_id(0), prior_thr_create_utime(0) {
    count++;
  }
  ~CONNECT() {
    count--;
    DBUG_ASSERT(vio_type == VIO_CLOSED);
  }
  void close_and_delete();
  void close_with_error(uint sql_errno, const char *message, uint close_error);
  THD *create_thd(THD *thd);
};

VIO类型和结构体

VIO类型包含VIO_CLOSED, VIO_TYPE_TCPIP, VIO_TYPE_SOCKET, VIO_TYPE_NAMEDPIPE, VIO_TYPE_SSL。如下为这些类型对应的通信方式。

VIO类型通信方式
VIO_TYPE_TCPIPTCP/IP通信方式
VIO_TYPE_SOCKETUNIX下的SOCKET通信方式
VIO_TYPE_NAMEDPIPE命名管道通信方式
VIO_TYPE_SSLSSL通信方式
VIO_TYPE_SHARED_MEMORY共享内存通信方式
enum enum_vio_type {
  VIO_CLOSED, VIO_TYPE_TCPIP, VIO_TYPE_SOCKET, VIO_TYPE_NAMEDPIPE, VIO_TYPE_SSL
};
static const LEX_CSTRING vio_type_names[] = {
  { STRING_WITH_LEN("Error") }, // cannot happen
  { STRING_WITH_LEN("TCP/IP") },
  { STRING_WITH_LEN("Socket") },
  { STRING_WITH_LEN("Named Pipe") },
  { STRING_WITH_LEN("SSL/TLS") },
  { STRING_WITH_LEN("Shared Memory") }
};

VIO结构体主要用于和数据库建立连接,不管是client还是server,建立连接的时候都需要用到此结构体。

struct st_vio { // include/violite.h
  MYSQL_SOCKET  mysql_socket;       /* Instrumented socket */
  my_bool		localhost;	        /* Are we from localhost? 从本机连接过来的?*/
  int			fcntl_mode;	        /* Buffered fcntl(sd,F_GETFL) */
  struct sockaddr_storage local;	/* Local internet address */
  struct sockaddr_storage remote;	/* Remote internet address */
  enum enum_vio_type	type;		/* Type of connection VIO类型 */
  const char		*desc;		    /* String description */
  char                  *read_buffer;   /* buffer for vio_read_buff */
  char                  *read_pos;      /* start of unfetched data in the read buffer */
  char                  *read_end;      /* end of unfetched data */
  int                   read_timeout;   /* Timeout value (ms) for read ops. 读操作超时时间 */
  int                   write_timeout;  /* Timeout value (ms) for write ops. 写操作超时时间 */
  /* function pointers. They are similar for socket/SSL/whatever */
  void    (*viodelete)(Vio*);
  int     (*vioerrno)(Vio*);
  size_t  (*read)(Vio*, uchar *, size_t);
  size_t  (*write)(Vio*, const uchar *, size_t);
  int     (*timeout)(Vio*, uint, my_bool);
  int     (*vioblocking)(Vio*, my_bool, my_bool *);
  my_bool (*is_blocking)(Vio*);
  int     (*viokeepalive)(Vio*, my_bool);
  int     (*fastsend)(Vio*);
  my_bool (*peer_addr)(Vio*, char *, uint16*, size_t);
  void    (*in_addr)(Vio*, struct sockaddr_storage*);
  my_bool (*should_retry)(Vio*);
  my_bool (*was_timeout)(Vio*);
  int     (*vioclose)(Vio*);
  my_bool (*is_connected)(Vio*);
  int (*shutdown)(Vio *, int);
  my_bool (*has_data) (Vio*);
  int (*io_wait)(Vio*, enum enum_vio_io_event, int);
  my_bool (*connect)(Vio*, struct sockaddr *, socklen_t, int);
#ifdef HAVE_OPENSSL
  void	  *ssl_arg;
#endif
};

VIO抽象接口

因为数据库所有的操作都需要从客户端通过网络发送到数据库后台,网络是数据库开始的地方,所以这里先研究网络通信模块VIO。网络通信无非需要下面几个接口:建立连接、发送数据、读取数据、断开连接和释放资源。

vio.c文件提供的函数

vio_init函数static void vio_init(Vio *vio, enum enum_vio_type type, my_socket sd, uint flags)参数为vio(需要填充的Vio结构体指针)、type(VIO类型)、sd(SOCKET)、flags(一些标识)。给Vio结构体填充默认值。初始化一个vio结构,将其中的属性根据type和flags进行填充,并且将sd设置到vio结构上。

vio_reset函数重置Vio结构体,只有VIO_TYPE_TCPIP和VIO_TYPE_SOCKET两种Vio类型才支持此操作。

my_bool vio_reset(Vio* vio, enum enum_vio_type type, my_socket sd, void *ssl __attribute__((unused)), uint flags) {
  int ret= FALSE;
  Vio old_vio= *vio;
  /* Will be reinitialized depending on the flags. Nonetheless, already buffered inside the SSL layer. */
  my_free(vio->read_buffer);
  vio_init(vio, type, sd, flags);  
  vio->mysql_socket.m_psi= old_vio.mysql_socket.m_psi; /* Preserve perfschema info for this connection */
#ifdef HAVE_OPENSSL
  vio->ssl_arg= ssl;
#endif

  /* Propagate the timeout values. Necessary to also propagate the underlying proprieties associated with the timeout, such as the socket blocking mode.
    note: old_vio.read_timeout/old_vio.write_timeout is stored in ms but vio_timeout() takes seconds as argument, hence the / 1000 */
  if (old_vio.read_timeout >= 0) ret|= vio_timeout(vio, 0, old_vio.read_timeout / 1000);
  if (old_vio.write_timeout >= 0) ret|= vio_timeout(vio, 1, old_vio.write_timeout / 1000);
  DBUG_RETURN(MY_TEST(ret));
}

mysql_socket_vio_new函数申请一个Vio,并调用vio_init函数初始化以后返回其指针。(提供给外部函数的接口)

Vio *mysql_socket_vio_new(MYSQL_SOCKET mysql_socket, enum enum_vio_type type, uint flags) {
  Vio *vio;
  my_socket sd= mysql_socket_getfd(mysql_socket);
  if ((vio = (Vio*) my_malloc(key_memory_vio, sizeof(*vio), MYF(MY_WME)))) {
    vio_init(vio, type, sd, flags);
    vio->desc= (vio->type == VIO_TYPE_SOCKET ? "socket" : "TCP/IP");
    vio->mysql_socket= mysql_socket;
  }
  DBUG_RETURN(vio);
}

vio_new函数调用mysql_socket_vio_new函数申请一个Vio。

Vio *vio_new(my_socket sd, enum enum_vio_type type, uint flags) {
  Vio *vio;
  MYSQL_SOCKET mysql_socket = MYSQL_INVALID_SOCKET;
  DBUG_TRACE;
  mysql_socket_setfd(&mysql_socket, sd);
  vio = mysql_socket_vio_new(mysql_socket, type, flags);
  return vio;
}

vio_timeout函数int vio_timeout(Vio *vio, uint which, int timeout_sec)给Vio设置超时。which用于指定设置send还是receive。Whether timeout is for send (1) or receive (0)

vio_delete函数删除一个Vio连接,必须等到其完成以后再释放掉资源。
vio_end函数清除内存环境,当一个连接结束的时候

viopipe.c文件提供函数

viopipe.c给windows环境增加管道通信方式。管道其实是一个只能顺序写和顺序读的文件,起主要调用Windows的API中WriteFile和ReadFile实现,由于Windows上的File相关API操作都是异步的,所以还需要一个等到异步完成的操作

  • vio_is_connected_pipe建立一个管道连接
  • vio_is_write _pipe往管道中写数据
  • vio_is_read _pipe从管道中读数据
  • vio_shutdown_pipe关闭一个管道通信
  • wait_overlapped_result等到一个异步IO的完成

vioshm.c文件提供函数

从名字可以看出来,该文件中实现vio share memory的通信方式,简写方式是单词开头的辅音的组合。其中设计了五个Event,分别用于:等待建立连接、等待服务器数据写完、等待服务器数据读完、等待客户端数据写完、等待客户端数据读完。

  • vio_is_connected_shared_memory建立shm的连接,这里其实就是简单的等到一个信号,等带其它进程Signal次信号,从而建立连接。
  • vio_read_shared_memory等待读数据的时候,要同时监听两个Event,分别是服务器端wrote和close事件。读完数据以后,要通过Event通知服务器端,数据已经读完。
  • vio_write_shared_memory写完数据以后,也要同时监听两个Event,分别是服务器端read和close事件。
  • vio_shutdown_shared_memory发送关闭信号,关闭连接
  • vio_delete_shared_memory释放资源,释放五个Event

viosocket.c文件提供函数

viosocket.c文件提供从socket上发送数据接收数据,设置socket属性的函数接口。
vio_socket_io_wait函数尝试在socket上等待I/O事件。
vio_read函数size_t vio_read(Vio *vio, uchar *buf, size_t size)用于从socket上读取数据。

size_t vio_read(Vio *vio, uchar *buf, size_t size) {
  ssize_t ret;
  int flags = 0;  
  if (vio->read_timeout >= 0) flags = VIO_DONTWAIT; /* If timeout is enabled, do not block if data is unavailable. */
  while ((ret = mysql_socket_recv(vio->mysql_socket, (SOCKBUF_T *)buf, size, flags)) == -1) {
    int error = socket_errno;
    /* Error encountered that is unrelated to blocking; percolate it up. */
#if SOCKET_EAGAIN == SOCKET_EWOULDBLOCK
    if (error != SOCKET_EAGAIN)
#else
    if (error != SOCKET_EAGAIN && error != SOCKET_EWOULDBLOCK)
#endif
      break;    
    if (!vio_is_blocking(vio)) { /* Nonblocking with either EAGAIN or EWOULDBLOCK. Don't call io_wait. 0 bytes are available. */
      DBUG_PRINT("info", ("vio_read on nonblocking socket read no bytes"));
      return -1;
    }    
    if ((ret = vio_socket_io_wait(vio, VIO_IO_EVENT_READ))) break; /* Wait for input data to become available. */
  }
  return ret;
}

vio_socket_connect函数用于连接数据库。

bool vio_socket_connect(Vio *vio, struct sockaddr *addr, socklen_t len, bool nonblocking, int timeout, bool *connect_done) {
  int ret, wait;
  int retry_count = 0;

  /* If timeout is not infinite, set socket to non-blocking mode. */
  if (((timeout > -1) || nonblocking) && vio_set_blocking(vio, false)) return true;  
  do { /* Initiate the connection. */
    ret = mysql_socket_connect(vio->mysql_socket, addr, len);
  } while (ret < 0 && vio_should_retry(vio) &&
           (retry_count++ < vio->retry_count));
  if (connect_done) *connect_done = (ret == 0);
  wait = (ret == -1) && (errno == EINPROGRESS || errno == EALREADY);
  /*The connection is in progress. The vio_io_wait() call can be used to wait up to a specified period of time for the connection to succeed. If vio_io_wait() returns 0 (after waiting however many seconds), the socket never became writable (host is probably unreachable.) Otherwise, if vio_io_wait() returns 1, then one of two conditions exist:
    1. An error occurred. Use getsockopt() to check for this.
    2. The connection was set up successfully: getsockopt() will return 0 as an error.
  */
  if (!nonblocking && wait && (vio_io_wait(vio, VIO_IO_EVENT_CONNECT, timeout) == 1)) {
    int error;
    IF_WIN(int, socklen_t) optlen = sizeof(error);
    IF_WIN(char, void) *optval = (IF_WIN(char, void) *)&error;
    /* At this point, we know that something happened on the socket. But this does not means that everything is alright. The connect might have failed. We need to retrieve the error code from the socket layer. We must return success only if we are sure that it was really a success. Otherwise we might prevent the caller from trying another address to connect to. */
    if (connect_done) *connect_done = true;
    if (!(ret = mysql_socket_getsockopt(vio->mysql_socket, SOL_SOCKET, SO_ERROR, optval, &optlen))) {
      errno = error;
      ret = (error != 0);
    }
  }
  /* If necessary, restore the blocking mode, but only if connect succeeded. */
  if (!nonblocking && (timeout > -1) && (ret == 0)) { if (vio_set_blocking(vio, true)) return true;
  }
  if (nonblocking && wait) {
    if (connect_done) *connect_done = false; return false;
  } else { return (ret != 0);
  }
}

原始的Shutdown其实就弄个参数是关闭一方(接或者收)或者全关闭。这就是所谓的优雅的关闭Socket,可是这个Shutdown里发现还处理了线程和队列 等的相关内容,最后才Close了Socket句柄。

int vio_shutdown(Vio *vio) {
  int r = 0;
  if (vio->inactive == false) {
    if (mysql_socket_shutdown(vio->mysql_socket, SHUT_RDWR)) r = -1;
#ifdef USE_PPOLL_IN_VIO
    if (vio->thread_id != 0 && vio->poll_shutdown_flag.test_and_set()) { // Send signal to wake up from poll.
      if (pthread_kill(vio->thread_id, SIGALRM) == 0) vio_wait_until_woken(vio);
      else perror("Error in pthread_kill");
    }
#elif defined HAVE_KQUEUE
    if (vio->kq_fd != -1 && vio->kevent_wakeup_flag.test_and_set()) vio_wait_until_woken(vio);
#endif
    if (mysql_socket_close(vio->mysql_socket)) r = -1;
#ifdef HAVE_KQUEUE
    if (vio->kq_fd == -1 || close(vio->kq_fd)) r = -1;
    vio->kq_fd = -1;
#endif
  }
  if (r) { DBUG_PRINT("vio_error", ("close() failed, error: %d", socket_errno)); /* FIXME: error handling (not critical for MySQL) */
  }
  vio->inactive = true;
  vio->mysql_socket = MYSQL_INVALID_SOCKET;
  return r;
}

使用

先看一下怎么产生个一VIO:利用mysql_socket_vio_new函数生成VIO,或直接通过vio_new函数生成VIO。sql/sql_connect.c文件的THD *CONNECT::create_thd(THD *thd)函数会调用mysql_socket_vio_new函数生成VIO,主要用于服务端使用。vio_new函数主要用于客户端生成VIO结构体连接数据库。至于SetBlock这些具体的属性,懂Socket的一眼就明白,不懂得就好好看看网络编程,不然说也说不清楚。说得直白一点就是设置Socket的属性是阻塞还是非阻塞的。
下来自然就是连接了:vio_socket_connect函数。这是多么熟悉的味道啊,熟悉到真得不想再解释,不过还是简单说一下,不同的平台对Socket的处理略有不同,但基本上是设置协议族,相关端口转换,阻塞非阻塞的设置,如果把相关的异常去除,就会看得更加清楚。然后是读和写,这里只看读的:vio_read函数size_t vio_read(Vio *vio, uchar *buf, size_t size)用于从socket上读取数据。最后就是关闭了:vio_shutdown函数。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值