[Home] [Kuri] [Sysad] [Internet?] [Blog] [Java] [Windows] [Download] [Profile] [Flash] [+]

PostgreSQL とやりとりする Apache のモジュールを作ってみた

以前から、特に理由はないのですが、Apache のモジュールを作りたいと思っていました。 で、この間、発作的に、 Apacheモジュール プログラミングガイド という本を買ってしまいました。 まあ、せっかくなので、作ってみました。
あ、今回使用したソースは、 ここにあります。


目次


1. 環境

VineLinux 2.5 および Debian GNU/Linux woody 上で確認しました。
Apache のバージョンは、どちらも 1.3.26 です。


2. 作るもの

こんな感じのものを作ります。
ややこしいように見えますが、そんなにややこしくないです。

スクラッチで書くのは面倒ですので、 apxs が作ってくれる helloworld module をもとに作成します。
以下を実行しますと、mod_helloworld に必要なファイルを生成してくれます。

  % apxs -n helloworld -g
  Creating [DIR]  helloworld
  Creating [FILE] helloworld/Makefile
  Creating [FILE] helloworld/mod_helloworld.c


3. モジュールの内容

以下は、チェックなどを省いてだいぶ簡単にしてしまっています。
実際の
ソースには、 一応的チェックが入っていますので、コードが若干異なります。

3.1 基本なところ

まず必要なのが、module 構造体です。 いろんな段階で呼ばれる関数などを指定できます。
ただ、今回必要なのは、下のうち色の異なる部分だけです。

/* Dispatch list for API hooks */
module MODULE_VAR_EXPORT pgsample_module = {
    STANDARD_MODULE_STUFF, 
    NULL,                  /* module initializer                  */
    pgsample_cdir_cfg,     /* create per-dir    config structures */
    NULL,                  /* merge  per-dir    config structures */
    NULL,                  /* create per-server config structures */
    NULL,                  /* merge  per-server config structures */
    pgsample_cmds,         /* table of config file commands       */
    pgsample_handlers,     /* [#8] MIME-typed-dispatched handlers */
    NULL,                  /* [#1] URI to filename translation    */
    NULL,                  /* [#4] validate user id from request  */
    NULL,                  /* [#5] check if the user is ok _here_ */
    NULL,                  /* [#3] check access by host address   */
    NULL,                  /* [#6] determine MIME type            */
    NULL,                  /* [#7] pre-run fixups                 */
    NULL,                  /* [#9] log a transaction              */
    NULL,                  /* [#2] header parser                  */
    NULL,                  /* child_init                          */
    NULL,                  /* child_exit                          */
    NULL                   /* [#0] post read-request              */
#ifdef EAPI
    ,NULL,                 /* EAPI: add_module                    */
    NULL,                  /* EAPI: remove_module                 */
    NULL,                  /* EAPI: rewrite_command               */
    NULL                   /* EAPI: new_connection                */
#endif
};

このうち、実際にクライアントに返事を返すハンドラを、 handler_rec 構造体で定義します。

/* Dispatch list of content handlers */
static const handler_rec pgsample_handlers[] = { 
    "pgsample", pgsample_handler }, 
    { NULL, NULL }
};

SetHandler ディレクティブで、上記 pgsample ハンドラを指定すると、 pgsample_handler 関数が呼び出されます。
helloworld の場合、以下のように、単純なメッセージを返すだけになっています。

static int helloworld_handler(request_rec *r)
{
    /* Content-type を text/html に指定してヘッダを返す */
    r->content_type = "text/html";
    ap_send_http_header(r);
    /* もしヘッダだけを要求されているのでなければ、メッセージも返す */
    if (!r->header_only)
        ap_rputs("The sample page from mod_helloworld.c\n", r);
    return OK;
}

3.2 設定ファイルなところ

次に、設定ファイルのところを説明します。
module 構造体で pgsample_cmds を指定していますが、 これが、設定ファイルの情報です。中身はこうしています。

static const command_rec pgsample_cmds[] = {
    {
        "PGsampleHost",                           /* name */
        ap_set_string_slot,                       /* func */
        XtOffsetOf(struct pgsample_cfgs, host),   /* cmd_data */
        OR_ALL,                                   /* req_override */
        TAKE1,                                    /* args_how */
        "Set Hostname for PostgreSQL server",     /* errmsg */
    },
    {
        "PGsampleDBname",                         /* name */
        ap_set_string_slot,                       /* func */
        XtOffsetOf(struct pgsample_cfgs, dbname), /* cmd_data */
        OR_ALL,                                   /* req_override */
        TAKE1,                                    /* args_how */
        "Set DB name for PostgreSQL server",      /* errmsg */
    },
    ...(後略)
    { NULL },  /* ターミネータ */
}

name には、コマンド名を指定します。

func には、 設定ファイルで指定された値を変数に格納するための関数を指定します。
自分で関数を作って指定することもできますし、Apache の関数を指定することもできます。 ここでは Apache の関数を指定しています。

cmd_data には、 func に渡す値を指定します。
Apache の関数を func に指定したときは、 設定情報を格納するための構造体のメンバのオフセットを、 XtOffsetOf マクロを使って指定します。
XtOffsetOf で指定している pgsample_cfgs 構造体は、以下のように定義しています。

struct pgsample_cfgs {
        char *host;    /* PGsampleHost */
        char *dbname;  /* PGsampleDBname */
        char *user;    /* PGsampleUser */
        char *table;   /* PGsampleTable */
        char *id;      /* PGsampleIdField */
        char *type;    /* GPsampleTypeField */
        char *object;  /* PGsampleObjectField */
};

req_override には、 設定ファイルに書くべき場所を指定します。
http_config.h に、OR_ALL とかが定義されています。 コメントに簡単な説明がありますので、そちらをご覧ください。(^__^;
ちなみに、上記で指定した OR_ALL は、どこでもありです。

args_how には、 必要な引数の数を指定します。
同じく http_config.h の enum cmd_how に定義されていますので、 そちらのコメントなどを参照してください。(^__^;;
ちなみに、上記で指定した TAKE1 は、引数が1つという意味です。

errmsg には、そのコマンドの説明を指定します。
具体的には、エラー時の出力メッセージに使われます。 例えば、わざと PGsampleDBname の引数の数を多く設定してみた結果を、 以下に示します。

  # apachectl configtest
  Syntax error on line 1060 of /etc/httpd/conf/httpd.conf:
  PGsampleDBname takes one argument, Set DB name for PostgreSQL server

Vine の場合、apcahectl がないので、代わりに httpd -t を実行します。
ちなみに、ap_set_string_slot() は、 main/http_config.c で以下のように定義されています。

API_EXPORT_NONSTD(const char *)
ap_set_string_slot(cmd_parms *cmd, char *struct_ptr, char *arg)
{
    /* This one's pretty generic... */

    int offset = (int) (long) cmd->info;
    *(char **) (struct_ptr + offset) = arg;
    return NULL;
}

cmd_parms 構造体の info の値を offset としています。
main/http_config.c の invoke_cmd() で、 cmd_data の値をこの info に代入してから、func が呼び出されるようになっています。

3.3 pgsample_handler()

pgsample_handler() 関数は、以下のようにしました。

static int pgsample_handler(request_rec *r)
{
        struct pgsample_cfgs *cfg;
        PGconn *conn;
        Oid oid;
        char *type;

        cfg = ap_get_module_config(r->per_dir_config, &pgsample_module);
        if((conn = connect_db(r, cfg)) == NULL) {
                return FORBIDDEN;
        }
        if((type = get_data(r, cfg, conn, &oid)) == NULL) {
                disconnect_db(conn);
                return NOT_FOUND;
        }

        r->content_type = type;
        ap_send_http_header(r);
        if(!r->header_only) {
                output_image(r, conn, oid);
        }

        disconnect_db(conn);
        return OK;
}

まず、ap_get_module_config() で、pgsample_cfgs 構造体のデータを得ます。
この構造体は、<Directory> や <Location> 毎に作られます。 後述の pgsample_cdir_cfg 関数で領域が確保され、 pgsample_cmds の内容にしたがって、確保された領域に設定が格納されます。 それを、ap_get_module_config() で得ることができます。
ちなみに、ap_get_module_config() は、Apache の関数です。

次に、connect_db() で、PostgreSQL に接続します。
この関数は、あとで説明します。

そして、get_data() で、指定された ID のデータを得ます。
この関数も後で説明しますが、やっていることは、 select を用いて、指定された ID のレコードを得るだけです。
type には content-type が、 oid には画像データのラージオブジェクトの IDが格納されます。

ここでひとまず、content-type を指定してヘッダを返します。
このあたりは、helloworld と同じです。

そして、いよいよ画像データを返します。
output_image() で返しますが、これも詳細は後述します。

最後に、disconnect_db() で PostgreSQL との接続を断ちます。

pgsample_handler() の戻り値は、 OK か、 httpd.h の HTTP Status Codes で定義されている HTTP_* という値を用います。
HTTP_* な値にすると、それにみあった返事を勝手に返してくれます。
NOT_FOUND や FORBIDDEN などよく使われるものは、短い名前も定義されています。

3.4 pgsample_cdir_cfg()

この関数では、設定情報を格納する構造体の領域を確保し、 デフォルトの値を設定します。

static void *pgsample_cdir_cfg(pool *pool, char *arg)
{
        struct pgsample_cfgs *cfg;

        cfg = ap_pcalloc(pool, sizeof(struct pgsample_cfgs));
        cfg->host   = ap_pstrdup(pool, DEFAULT_HOST_NAME);
        cfg->table  = ap_pstrdup(pool, DEFAULT_TABLE_NAME);
        cfg->id     = ap_pstrdup(pool, DEFAULT_ID_NAME);
        cfg->type   = ap_pstrdup(pool, DEFAULT_TYPE_NAME);
        cfg->object = ap_pstrdup(pool, DEFAULT_OBJECT_NAME);
        return (void *)cfg;
}

ここでは、ap_pcalloc() 関数で pgsample_cfgs 構造体の領域を1つ確保し、 それぞれにデフォルトの値を格納しています。
# していないのもありますが、NULL のままで問題ないということです。
ap_p*() という関数は Apache の関数で、指定したプールに領域を確保してくれます。
これらの関数の便利なところは、不要になった時点で自動的に開放してくれるため、 自分で開放する必要がないことです。
これらの関数は、ap_alloc.h に宣言が、main/alloc.c に実体があります。
# つまり詳細はそちらを参照してくださいと…すみません。

3.5 PostgreSQL とやりとりする関数たち

まずは、PostgreSQL と接続・切断する関数です。

static PGconn *connect_db(request_rec *r, struct pgsample_cfgs *cfg)
{
        PGconn *conn;

        conn = PQsetdbLogin(cfg->host, NULL, NULL, NULL,
                                cfg->dbname, cfg->user, NULL);
        if(PQstatus(conn) == CONNECTION_BAD) {
                return NULL;
        }
        return conn;
}

static void disconnect_db(PGconn *conn)
{
        PQfinish(conn);
        conn = NULL;
}

PQsetdbLogin() で接続、PQfinish() で開放します。 これらは libpq の関数です。
# 他にも使っている PQ*() という関数も、libpq の関数です。
PQsetdbLogin() 関数では、ホスト名、データベース名、ユーザ名の他に、 ポート番号、オプション、パスワードも指定できます。 …が、今回は省略しました。

次に、select して特定のレコードを得る関数です。

static char *get_data(request_rec *r, struct pgsample_cfgs *cfg,
                        PGconn *conn, Oid *oidp)
{
        char *cmd;
        char *ret;
        PGresult *res;

        cmd = ap_psprintf(r->pool,
                          "SELECT %s,%s FROM %s WHERE %s = %s",
                          cfg->type, cfg->object, cfg->table,
                          cfg->id, get_id());
        res = PQexec(conn, cmd);
        if(PQresultStatus(res) != PGRES_TUPLES_OK) {
                PQclear(res);
                return NULL;
        }
        *oidp = strtol(PQgetvalue(res, 0, 0), NULL, 10);
        ret   = ap_pstrdup(r->pool, PQgetvalue(res, 0, 1));
        PQclear(res);
        return ret;
}

まず、実行する select 文を作成し、cmd に格納します。
get_id() 関数は、ユーザから指定された ID を得る関数です。 以下に示します。

static char *get_id(request_rec *r)
{
        char *args = r->args;
        char *p, *k, *v;

        p = ap_getword(r->pool, &args, '&');
        if(p) {
                k = ap_getword(r->pool, &p, '=');
                if(!strcmp(k, "id")) {
                        v = ap_getword(r->pool, &p, '=');
                        return v;
                }
        }
        return NULL;
}

GET リクエストで指定された文字列は、 request_rec 構造体の args フィールドに格納されています。
これから、ap_getword() 関数を用いて、必要な情報を抜き出します。
ap_getword() 関数は Apache の関数で、第2引数で指定した文字列に対して、 第3引数で指定した文字までの文字列を返します。 また、第2引数のポインタは、第3引数の文字の後まで進められます。

そして、次に PQexec() 関数で実際に select を実行し、 PQgetvalue() 関数で、結果を文字列で得ます。
PQexec() の結果は自動的に確保されて PQresult 構造体の中に格納されていますので、 必要なくなった時点で PQclear() を呼び、開放します。
# これを忘れると、メモリリークの原因になってしまいます。

最後に、ラージオブジェクトのデータを得て、そのまま返す関数です。

static int output_image(request_rec *r, PGconn *conn, Oid oid)
{
        char buf[65536];
        int fd;
        int l;

        PQclear(PQexec(conn, "begin"));
        fd = lo_open(conn, oid, INV_READ);
        if(fd < 0) {
                PQclear(PQexec(conn, "end"));
                return -1;
        }

        while(1) {
                l = lo_read(conn, fd, buf, sizeof(buf));
                if(l <= 0) {
                        break;
                }
                ap_rwrite(buf, l, r);
        }
        lo_close(conn, fd);
        PQclear(PQexec(conn, "end"));
        return 0;
}

lo_open() でラージオブジェクトを開き、 lo_read() で得たデータを ap_rwrite() で HTTP クライアントに返します。 そして最後に、lo_close() で閉めます。
また、一連の動作は、begin と end で囲む必要があります。


4. 動作確認

まずは、コンパイルしてモジュールを作成します。
実際には make を実行するだけなのですが、実行されるのは以下です。

  apxs -c -I`pg_config --includedir` -L`pg_config --libdir` -lpq mod_sample.c

pg_config コマンドでヘッダとライブラリの位置を得て、引数に指定しています。
コンパイルが通ったら、 make install を実行してちゃんとした場所にファイルを置いてもいいですし、 ちょこっと試すだけだったらこのままでも構いません。

次に、httpd.conf に設定を追加します。
例を以下に示します。

  LoadModule pgsample_module /some/where/mod_pgsample.so
  <Location /pgsample>
    SetHandler pgsample
    PGsampleDBname usu
    PGsampleUser usu
    PGsampleTable table_kuri
    PGsampleIdField db_id
    PGsampleTypeField ctype
    PGsampleObjectField imageobj
  </Location>

そして、設定を確認して、httpd を restart します。

  # apachectl configtest
  Syntax OK
  # apachectl restart

Vine の場合は、代わりに以下のように実行します。

  # httpd -t
  Syntax OK
  # kill -HUP `cat /var/run/httpd.pid`

あとは、ブラウザで確認するだけです。
実際に存在する ID で確かめてください。

  % w3m 'http://localhost/pgsample?id=1'


5. 最後に

最初に書きましたが、ソースは ここにあります。

栗日記では、 これに似たようなモジュールを使って、画像データを得るようにしました。
データベースとの接続をつなぎっぱなしにしていたり、 エラーチェックをもうちょっとちゃんとしたりなど手は加えてあります。 が、基本的には似たようなものです。

しかし、 Apacheモジュール プログラミングガイド - この本は、いいです。勉強になります。
ブラックボックスとか抽象化とか言われますが、 やっぱり中身を知ってると強いと思います。

Powered by Apache PostgreSQL Usupi Logo Kuri Logo
[Home] [Kuri] [Sysad] [Internet?] [Blog] [Java] [Windows] [Download] [Profile] [Flash] [-]
usu@usupi.org Last modified : Thu Mar 25 01:24:52 2004