PHP
SQLServer

PHPでSQLServerの名前付きインスタンスに接続する

概要

  • PHP7.1のPDO/php_sqlsrvではポート番号指定が必要。
  • SQL Server Browserにポート番号を教えてもらって、指定すればOK。

経緯

  • PHP7.1で、SQLServerの名前付きインスタンスに接続できなかった。
  • PHP5.6の時は問題なく接続できていた。
  • PHP.netの説明を読んでも、名前付きインスタンスへの接続については書かれていない。
  • ググってみても、名前付きインスタンスをやめて固定ポートにすれば解決するという情報ばかり。
  • ほかのアプリでも使っているSQLServerなので、インスタンスの公開方法を変えるのは避けたい。

環境

PHPはremiリポジトリからyumでインストールしたもの。
* CentOS6 / PHP5.6 :php_pdo, php_mssql
* CentOS7 / PHP7.1 :php_pdo, php_sqlsrv

接続するのにFreeTDSを使っていたのが、microsoft製のドライバ経由に変わったらしい。

接続するコード

// PHP5.6で接続できたDSN
$dsn = "dblib:host={$ipaddr}\\SQLEXPRESS;dbname={$dbname};charset=UTF-8";

// 以前テストしたときにwindows上のphp_pdo_sqlsrvをつかって接続できたDSN
// 当時の環境は不詳。PHP5.3だったような。
$dsn = "sqlsrv:server={$ipaddr}\\SQLEXPRESS;database={$dbname}";

// PDOで接続。
$pdo = new PDO($dsn, 'dbuser', 'password');

PDOで接続する部分は一緒なので、以下はDSNのみ記載する。
windowsで成功していたのと同じように書いてみるが、つながらない。

//接続に失敗する
$dsn = "sqlsrv={$ipaddr}\\SQLEXPRESS;database={$dbname}";

失敗する様子をWiresharkでモニタしてみたところ、tcp/1433に接続しようとして応答がない状態。
インスタンスの動的ポート番号を調べたところ49174だったので、PDO_SQLSRV DSNの説明に従ってポート番号を入れてみると接続に成功した。

//つながった
$dsn = "sqlsrv={$ipaddr}\\SQLEXPRESS,49174;database={$dbname}";

動的ポートに対応する

SQLServerの名前付きインスタンスは動的ポート番号でListenしているため、ポート番号を調べる必要がある。
SQLServerのポート番号を解決するために、SQL Server Browserというサービスが動いているらしい。

SQL Server Browserに問い合わせる方法....

udp/1434でListenしていることは分かったものの、どうすればいいのかは調べきれず。

実際に問い合わせて観察してみた

PHP5.6ではポート番号を指定しなくても接続できたので、PHP5.6 (dblib) はSQL Server Browser に問い合わせているはず。
Wiresharkでパケットをモニタしながら接続してみたところ、

img.png

「SQLEXPRESS」という文字列の前に0x04、後に0x00をつけて送っていた。

手動で問い合わせてみると...

シェルから問い合わせてみた結果 (PC名は伏せ字に修正)。

$ echo -en "\04SQLEXPRESS\00" | nc -u 192.168.18.99 1434
?ServerName;*****;InstanceName;SQLEXPRESS;IsClustered;No;Version;9.00.4035.00;tcp;49174;np;\\*****\pipe\MSSQL$SQLEXPRESS\sql\query;;

セミコロン区切りで、項目名・内容の繰り返しで返ってくるようで、「tcp;<ポート番号>」を拾えばOKな印象。

PHPで接続する

クラスメソッドにして、コンストラクタから呼ぶ形にする。

class MyPDO_SQLSRV extends PDO {
    public function __construct($ipaddr, $dbuser, $password, $dbname) {
        $port = self::querySqlBrowser($ipaddr);
        if ($port === false) {
            throw new Exception('failed to find port number.');
        }
        $dsn = "sqlsrv:Server={$ipaddr}\\SQLEXPRESS,{$port};Database={$dbname}";
        parent::__construct($dsn, $dbuser, $password);
    }

    public static function querySqlBrowser($ipaddr) {
        $fp = fsockopen("udp://{$ipaddr}", 1434, $errno, $errstr);
        fwrite($fp, "\04SQLEXPRESS\00");
        $res = fread($fp, 1024);
        fclose($fp);
        if (!preg_match('/;tcp;([1-9][0-9]{0,4});/', $res, $matches)) {
            return false;
        }
        return $matches[1];
    }
}
//接続できた
$mypdo = new MyPDO_SQLSRV($ipaddr, 'dbuser', 'password', 'dbname');