Mysql 5.5 的 UNIX Domain Socket 通信方式

疑问

我们看到用 mysql_secure_installation 安装配置的 mysql-server (mariadb-server),默认的 host 有 localhost, 127.0.0.1 和 ::1 .

那么问题来了,默认情况下,对于 IPv4, localhost 跟 127.0.0.1 应该是等同的。为什么在 mysql 这个数据库的认证记录中,会同时出现 localhost 和 127.0.0.1 两个不同的记录呢?

MariaDB [mysql]> select host,user,password from user;
+-----------+------+-------------------------------------------+
| host | user | password |
+-----------+------+-------------------------------------------+
| localhost | root | *84BB5DF4823DA319BBF86C99624479A198E6EEE9 |
| 127.0.0.1 | root | *84BB5DF4823DA319BBF86C99624479A198E6EEE9 |
| ::1 | root | *84BB5DF4823DA319BBF86C99624479A198E6EEE9 |
+-----------+------+-------------------------------------------+


mysql 建立本地通信的两种方式: localhost, 127.0.0.1

查阅相关手册,用 localhost 或者用 127.0.0.1 作为 host 的登录方式是不一样的。

如果没有指定或者指定了 localhost, mysql 将会以 UNIX domain socket 的方式与 mysqld 进行通信,这是一种无须经过网络栈的 IPC 通信方式。

如果要强制 mysql 使用网络栈与本机 mysqld 进行通信,则需要指定 host 为 127.0.0.1 .

由于使用 UNIX domain socket 不需要打包拆包、计算校验和、维护序号和应答等,只是将应用层数据从一个进程拷贝到另一个进程,所以它的性能通常会优于网络 socket 的通信方式。

man mysql_table

MYSQL PARAMETERS
hosts The hosts that Postfix will try to connect to and query from. Specify unix: for UNIX domain sock‐
ets, inet: for TCP connections (default). Example:
hosts = host1.some.domain host2.some.domain:port
hosts = unix:/file/name

The hosts are tried in random order, with all connections over UNIX domain sockets being tried
before those over TCP. The connections are automatically closed after being idle for about 1
minute, and are re-opened as necessary. Postfix versions 2.0 and earlier do not randomize the host
order.

NOTE: if you specify localhost as a hostname (even if you prefix it with inet:), MySQL will con‐
nect to the default UNIX domain socket. In order to instruct MySQL to connect to localhost over
TCP you have to specify
hosts = 127.0.0.1

https://dev.mysql.com/doc/refman/5.5/en/connecting.html

On Unix, MySQL programs treat the host name localhost specially, in a way that is likely different from what you expect compared to other network-based programs. For connections to localhost, MySQL programs attempt to connect to the local server by using a Unix socket file. This occurs even if a --port or -P option is given to specify a port number. To ensure that the client makes a TCP/IP connection to the local server, use --host or -h to specify a host name value of 127.0.0.1, or the IP address or name of the local server. You can also specify the connection protocol explicitly, even for localhost, by using the --protocol=TCP option. For example:

shell> mysql --host=127.0.0.1
shell> mysql --protocol=TCP

我们可以用 strace 跟踪服务端和客户端的 socket 建立过程。
实验环境:
Red Hat Enterprise Linux Server release 7.1 (Maipo)
mariadb-5.5.41-2.el7_0.x86_64
(可参考 http://feichashao.com/mariadb/ 进行配置)

localhost (AF_UNIX)

1. 启动 mysqld 服务后,查看 mysqld 对应的 pid:

# ss -tulnp | grep mysql
tcp LISTEN 0 50 *:3306 *:* users:(("mysqld",22013,13))

pid=22013.

2. 在一个终端窗口中,用 strace 追踪该进程(server):

# strace -Tttfv -s 8192 -o /tmp/myserver_localhost.out -p 22013

3. 在另一个终端窗口,用 strace 追踪 mysql 命令(client):

# strace -Tttfv -s 8192 -o /tmp/myclient_localhost.out mysql -h localhost --user=root --password=redhat mysql

4. 建立连接后, Ctrl-C 关闭两个终端的 strace.

5. 从 server 端的输出(/tmp/myserver_localhost.out) 可以看到 AF_UNIX 的建立过程:
(有省略)

22013 20:03:14.244657 accept(15, {sa_family=AF_LOCAL, NULL}, [2]) = 34 <0.000031>
22013 20:03:14.244716 fcntl(15, F_SETFL, O_RDWR) = 0 <0.000008>
22013 20:03:14.244744 getsockname(34, {sa_family=AF_LOCAL, sun_path="/var/lib/mysql/mysql.sock"}, [28]) = 0 <0.000023>
22769 20:03:16.626385 shutdown(34, SHUT_RDWR) = 0 <0.000018>
22769 20:03:16.626441 close(34) = 0 <0.000028>

6. 从 client 端的输出(/tmp/myclient_localhost.out) 可以看到相应的 AF_UNIX 连接:
(有省略)

22767 20:03:14.244093 socket(PF_LOCAL, SOCK_STREAM, 0) = 3 <0.000019>
22767 20:03:14.244198 connect(3, {sa_family=AF_LOCAL, sun_path="/var/lib/mysql/mysql.sock"}, 110) = 0 <0.000933>
22767 20:03:14.245284 read(3, "R\0\0\0\n5.5.41-MariaDB\0\20\0\0\0kXV*ld0a\0\377\367\10\2\0\17\240\25\0\0\0\0\0\0\0\0\0\0y_LA@]616Y?m\0mysql_native_password\0", 16384) = 86 <0.000354>
22767 20:03:16.623663 close(3) = 0 <0.000028>

7. 另外我们可以看到,socket 文件是在 my.cnf 里定义的:

# cat /etc/my.cnf | grep socket
socket=/var/lib/mysql/mysql.sock

Note: AF_UNIX also known as AF_LOCAL.

127.0.0.1 (AF_INET)

同理,使用 127.0.0.1 作为 host, 我们能看到不同的输出。

1. 启动 mysqld 服务后,查看 mysqld 对应的 pid:

# ss -tulnp | grep mysql
tcp LISTEN 0 50 *:3306 *:* users:(("mysqld",22013,13))

pid=22013.

2. strace server:

# strace -Tttfv -s 8192 -o /tmp/myserver_127.out -p 22013

3. strace client:

# strace -Tttfv -s 8192 -o /tmp/myclient_127.out mysql -h 127.0.0.1 --user=root --password=redhat mysql

4. 从 Server 端可以看到:

22013 20:03:52.755466 accept(13, {sa_family=AF_INET, sin_port=htons(56912), sin_addr=inet_addr("127.0.0.1")}, [16]) = 34 <0.000013>
22013 20:03:52.755513 fcntl(13, F_SETFL, O_RDWR) = 0 <0.000009>
22013 20:03:52.755541 getsockname(34, {sa_family=AF_INET, sin_port=htons(3306), sin_addr=inet_addr("127.0.0.1")}, [16]) = 0 <0.000009>
22790 20:03:54.093376 shutdown(34, SHUT_RDWR) = 0 <0.000074>
22790 20:03:54.093493 close(34) = 0 <0.000028>

5. 从 Client 端可以看到:

22788 20:03:52.755147 socket(PF_INET, SOCK_STREAM, IPPROTO_TCP) = 3 <0.000025>
22788 20:03:52.755203 fcntl(3, F_SETFL, O_RDONLY) = 0 <0.000006>
22788 20:03:52.755229 fcntl(3, F_GETFL) = 0x2 (flags O_RDWR) <0.000006>
22788 20:03:52.755253 connect(3, {sa_family=AF_INET, sin_port=htons(3306), sin_addr=inet_addr("127.0.0.1")}, 16) = 0 <0.000677>
22788 20:03:54.090230 close(3) = 0 <0.000044>


实例代码

我们可以在 Linux 下简单实现 Unix domain socket 通信。

在同一目录下,分别建立 server.c 和 client.c 文件。

server 在当前目录下建立 server_socket 文件,绑定并监听该 socket.

client 则连接到 server_socket 文件,与 server 进行通信。

// server.c
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>

int main()
{
	/* delete the socket file */
	unlink("server_socket");
	
	/* create a socket */
	int server_sockfd = socket(AF_UNIX, SOCK_STREAM, 0);

	struct sockaddr_un server_addr;
	server_addr.sun_family = AF_UNIX;
	strcpy(server_addr.sun_path, "server_socket");

	/* bind with the local file */
	bind(server_sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr));

	/* listen */
	listen(server_sockfd, 5);

	char ch;
	int client_sockfd;
	struct sockaddr_un client_addr;
	socklen_t len = sizeof(client_addr);
	while(1)
	{
		printf("server waiting:\n");

		/* accept a connection */
		client_sockfd = accept(server_sockfd, (struct sockaddr *)&client_addr, &len);

		/* exchange data */
		read(client_sockfd, &ch, 1);
		printf("get char from client: %c\n", ch);
		++ch;
		write(client_sockfd, &ch, 1);

		/* close the socket */
		close(client_sockfd);
	}

	return 0;
}
// client.c
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>

int main()
{
	/* create a socket */
	int sockfd = socket(AF_UNIX, SOCK_STREAM, 0);

	struct sockaddr_un address;
	address.sun_family = AF_UNIX;
	strcpy(address.sun_path, "server_socket");

	/* connect to the server */
	int result = connect(sockfd, (struct sockaddr *)&address, sizeof(address));
	if(result == -1)
	{
		perror("connect failed: ");
		exit(1);
	}

	/* exchange data */
	char ch = 'A';
	write(sockfd, &ch, 1);
	read(sockfd,&ch, 1);
	printf("get char from server: %c\n", ch);

	/* close the socket */
	close(sockfd);

	return 0;
}

先运行 server, 后运行 client,可见效果:

[root@host1 unix_socket]# ./server
server waiting:
get char from client: A

[root@host1 unix_socket]# ./client
get char from server: B

在 mariadb 的源码中,我们也能看到类似部分:

// mariadb-5.5.44/sql-common/client.c
/*  snip */
#if defined(HAVE_SYS_UN_H)
  if (!net->vio &&
      (!mysql->options.protocol ||
       mysql->options.protocol == MYSQL_PROTOCOL_SOCKET) &&
      (unix_socket || mysql_unix_port) &&
      (!host || !strcmp(host,LOCAL_HOST)))
  {
    my_socket sock= socket(AF_UNIX, SOCK_STREAM, 0);
/* snip */

参考资料

1. unix(7) http://man7.org/linux/man-pages/man7/unix.7.html
2. 本地socket unix domain socket http://blog.csdn.net/bingqingsuimeng/article/details/8470029