MySQLにおけるlocalhostと127.0.0.1
先日、会社のインターンの人にこんなことを聞かれた。
「localhostと127.0.0.1ってどう違うんですか?」
/etc/hosts
に書いてあるとか、そういう月並みな説明はできるけど、よく考えたらうまく説明できない。
どのようなしくみで動いているのかなどをきちんと理解しているわけではないことがわかってしまったのだ。
この記事では libc
の gethostbyname
などの UNIX
の話というよりは、Web開発で MySQL
の DATABASE_HOST
の localhost
を 127.0.0.1
に変えたら動いた! について少し深掘りして考えてみる。
libc
や /etc/nsswitch.conf
に関しては、別途調べて別記事にまとめて書く。
MySQL
に接続する方法は2つある。
- unix domain socket
- tcp
unix domain socket
は同一マシン内でのプロセス間通信のことだ(今はこれくらいの理解)
tcp
でもつなぐことができるのだが、 unix domain socket
のほうがはるかに高速なので基本的にはこちら使うのが一般的らしい。
参考記事はこちら。
今回は以前あった、「laravelからMySQLにつながらない」という問題を再考した。
まずphp自体は名前解決どうしているんだろうか、と思い php/php-srcの対象のコードを読んだ。
https://github.com/php/php-src/blob/master/main/network.c#L1307-L1309
PHPAPI struct hostent* php_network_gethostbyname(char *name) {
#if !defined(HAVE_GETHOSTBYNAME_R)
return gethostbyname(name);
#else
if (FG(tmp_host_buf)) {
free(FG(tmp_host_buf));
}
FG(tmp_host_buf) = NULL;
FG(tmp_host_buf_len) = 0;
memset(&FG(tmp_host_info), 0, sizeof(struct hostent));
return gethostname_re(name, &FG(tmp_host_info), &FG(tmp_host_buf), &FG(tmp_host_buf_len));
#endif
}
gethostbyname
ちゃんと使っている….無問題。なんでだろう。
そしたら詳しいおっさんが、「MySQLとかはlocalhostで指定するとunix domain socketを優先しようとする」などと教えてくれたので、 mysql-server
を読みに行った。
repoは mysql/mysql-serverだ。
対象のコードは以下。 "localhost"
という文字列をgrepすると見つかった。
https://github.com/mysql/mysql-server/blob/8.0/sql-common/client.cc#L4275-L4322
#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);
DBUG_PRINT("info", ("Using socket"));
if (sock == SOCKET_ERROR) {
set_mysql_extended_error(mysql, CR_SOCKET_CREATE_ERROR, unknown_sqlstate,
ER_CLIENT(CR_SOCKET_CREATE_ERROR), socket_errno);
goto error;
}
net->vio =
vio_new(sock, VIO_TYPE_SOCKET, VIO_LOCALHOST | VIO_BUFFERED_READ);
if (!net->vio) {
DBUG_PRINT("error", ("Unknow protocol %d ", mysql->options.protocol));
set_mysql_error(mysql, CR_CONN_UNKNOW_PROTOCOL, unknown_sqlstate);
closesocket(sock);
goto error;
}
host = LOCAL_HOST;
if (!unix_socket) unix_socket = mysql_unix_port;
host_info = (char *)ER_CLIENT(CR_LOCALHOST_CONNECTION);
DBUG_PRINT("info", ("Using UNIX sock '%s'", unix_socket));
memset(&UNIXaddr, 0, sizeof(UNIXaddr));
UNIXaddr.sun_family = AF_UNIX;
strmake(UNIXaddr.sun_path, unix_socket, sizeof(UNIXaddr.sun_path) - 1);
if (mysql->options.extension && mysql->options.extension->retry_count)
my_net_set_retry_count(net, mysql->options.extension->retry_count);
if (vio_socket_connect(net->vio, (struct sockaddr *)&UNIXaddr,
sizeof(UNIXaddr), get_vio_connect_timeout(mysql))) {
DBUG_PRINT("error",
("Got error %d on connect to local server", socket_errno));
set_mysql_extended_error(mysql, CR_CONNECTION_ERROR, unknown_sqlstate,
ER_CLIENT(CR_CONNECTION_ERROR), unix_socket,
socket_errno);
vio_delete(net->vio);
net->vio = 0;
goto error;
}
mysql->options.protocol = MYSQL_PROTOCOL_SOCKET;
}
#elif defined(_WIN32)
以下の部分が該当箇所だ。パフォーマンスのためか、 LOCAL_HOST
の場合は socket
でつないでるっぽい(if文が分かりづらい….)
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); /* ... */}
試しに、 /etc/hosts
で以下のようにしたらどうなるだろうか。
MySQLにつないでみる。
~ (*´ω`*) < mysql -u root --host="hoge" -P 3306
Welcome to the MySQL monitor. Commands end with ; or \g.
[省略]
netstatの情報はこんな感じだ。
~ (*´ω`*) < netstat -an | grep 3306
tcp4 0 0 127.0.0.1.3306 127.0.0.1.50816 ESTABLISHED
tcp4 0 0 127.0.0.1.50816 127.0.0.1.3306 ESTABLISHED
tcp4 0 0 127.0.0.1.3306 *.* LISTEN
tcp4 0 0 127.0.0.1.50765 127.0.0.1.3306 TIME_WAIT
localhostでつなぐとこんな感じ。
~ 。+゚(∩´﹏`∩)゚+。 < mysql -u root -h localhost
Welcome to the MySQL monitor. Commands end with ; or \g.
[省略]
netstatの情報はこんな感じだ。
~ (*´ω`*) < netstat -an | grep 3306
tcp4 0 0 127.0.0.1.3306 *.* LISTEN
tcp4 0 0 127.0.0.1.50816 127.0.0.1.3306 TIME_WAIT
tcpで動いていないことが確認できた。
正しく説明するにはC言語読む必要が出てくるし、詳しいおっさんが近くにいないと厳しい。
先駆者はいたけどまぁ書いてもよいよね。