第12回 実践 クライアント/サーバーデータベースソリューション 第11回 第13回


マルチスレッドSocketサーバーによる,データベースサーバーの実現

Access,VB5,Jetデータベースエンジンを利用した
C/Sシステム構築に関する考察,実験および実践


秋月巌ソリューション事務所
秋月 巌 AKIZUKI,Iwao


倦怠感

 最近,プログラミングに関する話題がとぼしい.Javaの影はうすくなったし,新しい開発ツールにもインパクトがない.コントロールは画期的な製品が出にくい状況だし,新しいパラダイムを提唱する製品も,そのメリットを理解してもらうことはむずかしい.
 なぜだろう? 革新的なテクノロジーが多すぎて,それらを理解するのに開発者たちが忙しいからだろうか? そればかりが理由だとはどうも思えない.新しいテクノロジーはどれも未完成で,明日の納期を心配する開発者がそれらの理解に労力を割いているわけではないだろう.
 オブジェクト思考が巷間を席捲したときのような,華々しさが今のプログラミング業界には欠けている.こうして執筆している原稿も過去を懐かしむトーンに傾きがちである.もちろん,これは前向きではない.

何か,新しいことを…

 何か,積極的に新しいものにチャレンジしたいと思うのだが,何にチャレンジするかの選択が難しい.まず最大の原因は,現行の開発ツールに対する不満が以前よりもはるかに少なくなったことである.以前は何か新しいものを作るには新しいツールやコンポーネントを理解する必要があったが,現在では大抵のことはVisual Basicでまかなえてしまう.
 今回解説するマルチスレッドサーバーもそうである.マルチスレッドSocketサーバープログラムを開発するために,私は市販のマルチスレッド関連の本を読んでいた.それらの書籍は,MFCを使ったC++で記述するものと,C言語を使ったものとに大別される.もちろん,W.Stealride氏が本誌に掲載したVisual Basicでマルチスレッドアプリーケーションを作成する記事(本誌97年7月号「Visual Basic 5.0とマルチスレッド」,98年1月号「ActiveXサーバーを使ったマルチスレッド化技法」)にも目を通したが,実用的で安定稼動するプログラムを開発するには,あまり適していないと思ったのである.
 もちろん,Service Pack 2以降のパッチをあてたVisual Basic 5.0でマルチスレッドActiveXサーバーを開発できることは知っていた.しかしマルチスレッドActiveXクライアントを作成できない以上,アプリケーションはマルチスレッドの恩恵を受けることはできない.つまりActive Server Pages等のVisual Basic以外で作成されたプログラムから呼び出した場合に,マルチスレッドで動作するだけである.

マルチスレッドアプリケーション

 Visual Basicで作成したActiveXコントロールに関する当時の私の認識は,このレベルまでは間違っていない.マルチスレッドで動作するのはApertment modelとして作成されたActiveX DLLであり,ActiveX EXEは別プロセスを起動するため,当然マルチスレッドでは動作しないと思っていたのである.
 私がVisual Basicによるマルチスレッドアプリケーション開発の障壁を超えるためにC言語を使おうと考えていたのは,この不見識が理由である.プロジェクトプロパティの「マルチスレッド」チェックボタンがチェックできない事実に引きずられて,マニュアルを読み違えていたのである.しかし本稿でも何度か述べたように,ActiveX EXEはInstancingプロパティを「MultiUse」に設定すれば,別プロセスでマルチスレッドとして動作する.

Visual Basicによるマルチスレッドプログラミング

 このことをStealride氏から聞いたとき,私たちは安全なマルチスレッドアプリケーション開発方法について話し合っていた.彼は例のごとく制御不可能な原子炉的な方法で問題を解決することを考えていた.何が一番安全にこの問題を解決する方法だろうか,という私の問いに彼は「VBかもしれないな」と答えた.彼のこの返事と,ActiveX EXEをマルチスレッドで動作させることの間に関連性はない.結局,私は,「VBでできるかもしれない,安全なマルチスレッドアプリケーション開発方法」について聞きそびれてしまった.ActiveX EXEを使ってマルチスレッドアプリケーションを開発するには,一瞬とはいえども,2つのプロセスを起動しなければならない.彼の性格では,この方法はすでに候補からはずれていたのかもしれない.

ActiveX EXEは安全か?

 私の知識と経験で判断する限り,ActiveX EXEを使ってマルチスレッドアプリケーションを実現することは,とくにリスクの多い開発方法ではない.起動時に別プロセスをいったん起動しなければいけないという制約も,現在のマシン環境では大きな負荷ではないし,ましてやサーバーアプリケーションを配置するようなマシンならば問題になることはない.
 結局,C言語やC++言語を使用してマルチスレッドアプリケーションを開発する方法の調査は投げ出すことになった.本当に向学心のある者ならば,手をつけたついでに追求するのかもしれない.しかし,安全で効率的な解決方法があるときに,わざわざ,困難の道を選ばないのが普通の人間である.
 Visual Basicの標準EXEでマルチスレッドアプリケーションの開発ができない理由は,おそらく開発環境上での実行が難しいからだということが考えられる.Visual Basicで作成したActiveXコンポーネントがマルチスレッドで動作する以上,実行に関しては一応問題がないことになっているはずである.
 しかし開発環境上でマルチスレッドアプリケーションを実行するときには,開発環境が動作しているプロセスでスレッドが生成されることになるので,干渉する可能性がある.もっともVisual InterDevなどでは開発環境とは別にプロセスを起動しながらアプリケーションを実行してデバッグする方法を実現しているのだから,将来的にはVisual Basicも対応することだろう.もっとも,やはり別プロセスのデバッグは技術的にも難しいせいか,Visual InterDevのデバッガも安定しているとはいいがたい.

Type8 DB Server

 さて,今回紹介するType8は,前回までに作成したType7をマルチスレッド対応に改変している.単にInstancingプロパティを変更するだけでなく,接続待ちをしているアプリケーションと同じプロセスにデータアクセス用のコンポーネントが起動することで,コールバック式の接続方式を廃止した.一般的なSocketサーバーアプリケーションのように,サーバー側は単独のポートを使用するためセキュリティ上有利になっている他,DB Serverの最大のウィークポイントであった1接続あたりのメモリ消費が激減している.一方,以前のようにマルチスレッドとマルチプロセスを切り替えることはできなくなった.
 当初,私は同じプロセスで接続要求を受け付けるための接続ブローカとデータアクセスコンポーネントを同じプロセスで起動し,WinsockコントロールのRequestIDをデータアクセスコンポーネントに渡せば,簡単に接続できるものだと思っていた.しかし接続ブローカ側のWinsockコントロールが意味不明の切断を行なうため,接続ブローカのSocket部分をWinsock APIで記述しなおさなければならなかった.Visual Basicから非同期でWinsock APIを利用する方法については,本誌98年2月号に掲載された福岡寿和氏の記事「WinSock32ネットワークプログラミング入門」のサンプルを参考にした.

Winsock APIとWinsockコントロールの連携

 最終的にはWinsock APIアプリケーションとVisual BasicのWinsockコントロールが連携する形になった.ここではデータベースサーバーとして利用しているが,汎用的なマルチスレッドSocketサーバーアプリケーションのテンプレートとして利用できるはずである.福岡寿和氏の記事「WinSock32ネットワークプログラミング入門 」は,Socketクライアントの作成方法を解説しているので,併せて読むことでサーバーとクライアントのコンプリートな開発方法が理解できることになる.Visual Basic Magazineのバックナンバーを持たない方は,PCDN(PCデベロッパーネットワーク)の「翔泳社VB Magazineライブラリ」(http://pcdn.int21.co.jp/pcdn/vb/noriolib/)で,この記事とサンプルを参照できる.

 サーバーのバージョンアップにあわせて,Type7クライアントも変更されている(図1).Type7クライアントではコールバックを受信するために2つのWinsockコントロールを配置していたが,それがひとつになっている.2つのコントロールで行なっていた処理がマージされているだけで,クライアント側が行なう処理には変更がない.そのため,クライアントアプリケーションの名称はType8たが,内部的に使用されているコンポーネントの名前はType7のままである.

図1:クライアントアプリケーション for Type8 DB Server
図1:クライアントアプリケーション for Type8 DB Server

Type8 DB Serverの構成

 Type8 DB Serverは2つのプロジェクトによって構成されている(図1・2).従来も2つのプロジェクトによって成り立っていたが,既存の2つのプロジェクトは,ひとつのプロジェクト(DBsvrMT)にマージされ,新たにDBsvrStartプロジェクトが追加されている.DBsvrStartプロジェクトは,標準EXEであり,DBsvrMTプロジェクトはActiveX EXEである.
 マルチスレッドで単独のSocketポートを利用するために,リクエストブローカとデータアクセスコンポーネントはひとつのActiveX EXEコンポーネントとしてコンパイルされる必要がある.

図2:DBsvrStartプロジェクト
図2:DBsvrStartプロジェクト

図3:DBsvrMTプロジェクト
図3:DBsvrMTプロジェクト

図4:プロセスとスレッドの起動
図4:プロセスとスレッドの起動

Type8 DB Serverのプロセスとスレッドモデル

 アプリケーションの実行時に,プロセスとスレッドは図4のように生成される.実行プロセスを起動するためのスタートアッププログラムであるDB SvrStart.VBPは,ActiveX EXEを呼び出すことで実行プロセスを生成する.その処理を行なっているのがフォームのロードイベントに記述されている以下のコードである.

Private Sub Form_Load()
    Set DBsvrRBObj = CreateObject( _
                     "DBsvrMT.clsDBsvrRB")
    DBsvrRBObj.ConnRB
    Set DBsvrRBObj = Nothing
    End
End Sub
 このプロジェクトでは,これ以外の処理を一切行なっていない.このプログラムはフォームのLoadイベントでこの処理を実行し,最終行にあるEndステートメントでアプリケーションを終了する.
 CreateObject関数を使用してActiveX EXEを実体化し,呼び出したオブジェクトのConnRBメソッドを呼び出している.次の行ではオブジェクト変数にNothingキーワードを代入し,オブジェクトを破棄する.この時点でオブジェクトのライフサイクルは終了し,オブジェクトのインスタンスは破棄されるはずである.しかし,実際にはこの処理によって呼び出された接続リクエストブローカは,サービスを終了しない.それは呼び出されたConnRBメソッドで,フォームの表示処理を行なっているからである.
 以下のコードは,フォームを開くために,ConnRBメソッドとfrmDBsvrRBフォームに記述されているコードである.

Public Sub ConnRB()
    frmDBsvrRB.PConnect
End Sub
Public Sub PConnect()
    Me.Show
End Sub
 オブジェクトの寿命はつきていても,インスタンスの破棄時に,表示されたフォームが自動的に閉じられることはない.コンテナの制御から開放され,結果的にActiveX EXEコンポーネントである接続リクエストブローカは,普通の標準EXE同様に実行されている状態になる.このアプリケーションを終了させるには,オートメーションを利用するのではなく,フォームを閉じればよい.ただ,この実行状態が標準EXEの実行状態と根本的に違うのは,同じプロジェクトに格納されているクラスをインスタンス化したときに,マルチスレッドで実行することが可能な点である.標準EXEでは特殊な方法を使わない限り同じプロセスにスレッドを生成することはできない.

Winsock APIによる接続リクエスト待ち状態

 リスト1は,スタートアッププログラムが起動した接続リクエストブローカのフォームのロード時に実行される処理である.つまり,スタートアッププログラムが実行されると,ここまでの処理を一気行なうことになる.
 フォームのLoadイベントプロシージャでは,フォームにスレッドIDを表示したあとConnectionRequestサブプロシージャを呼び出している.スレッドIDの表示は,あとでDBアクセスサーバーと違うスレッドで実行されているかを確認するために利用する.
 ConnectionRequestサブプロシージャには,接続リクエストを受け付けるための処理が記述されている.以下のコードは初期化から,接続待機状態にいたるまでの一連の流れである.

lngRet = WSAStartup(&H101, musrStartup)
mlngSock = socket(AF_INET, SOCK_STREAM, 0)
   |(中略)
lngRet = bind(mlngSock, musrSockBuf, _
              Len(musrSockBuf))
lngRet = listen(mlngSock, 5)
 Winsock APIに関する解説は,福岡氏の記事を参考にしていただきたいが,これらの処理はWinsock APIでサーバー側の処理を記述する定型的なコードである.引数の値を変更し,エラーハンドラを追加することで大抵のプログラムに対応できるはずである.
 次の1行に関しては,解説を追加しておく必要があるだろう.

lngRet = WSAAsyncSelect( _
 mlngSock, txtSockIn.hwnd, &H100, FD_ACCEPT)
 ここでは,接続要求を受け付けるにあたって,非同期処理を行なっている.Winsock APIのAccept関数を使って同期処理を行ないながら,接続の受け付けを行なうことも処理としては可能だが,その場合このプログラムを呼び出したスタートアッププログラムに制御がもどらなくなってしまう.結果としてスタートアッププログラムは表示されたまま操作できなくなる.
 ここではWSAAsyncSelect関数の4番目の引数にFD_ACCEPTを指定することで,Accept処理を非同期的に実現している.ポイントになるのは,2番目と3番目の引数である.2番目の引数のtxtSockIn.hwndのtxtSockInは,フォームに配置されたテキストボックスコントロールの名称であり,H100はKeyDownイベントに対応するWindowsメッセージである.
 結果として接続リクエストがあったときにはtxtSockInテキストボックスコントロールのKeyDownイベントが発生し,対応するイベントプロシージャ(リスト2)が実行される.もちろんtxtSockInテキストボックスコントロールは,このイベントを受信するためだけにフォームに配置されているのであり,他の用途には利用されない.
 KeyDownイベントプロシージャではAccept関数が実行され,新しいSocketが生成される.戻り値にかえるのは,生成されたSocketのIDである.この値はWinsockコントロールのConnectionRequestイベントプロシージャの引数RequestIDに対応する.

リスト1:ユーザーの接続リクエストを待つための処理
Private Sub Form_Load()
    Me.Show
    Text1 = App.ThreadID
    Call ConnectionRequest
End Sub
Private Sub ConnectionRequest()
    lngRet = WSAStartup(&H101, musrStartup)
    mlngSock = socket(AF_INET, SOCK_STREAM, 0)

    musrSockBuf.sin_family = AF_INET
    musrSockBuf.sin_port = htons(CLng("1010"))
    musrSockBuf.sin_addr = htonl(INADDR_ANY)
    musrSockBuf.sin_zero = String$(8, 0)
    
    lngRet = bind(mlngSock, musrSockBuf, Len(musrSockBuf))
    lngRet = listen(mlngSock, 5)
    lngRet = WSAAsyncSelect(mlngSock, txtSockIn.hwnd, &H100, FD_ACCEPT)
    DoEvents

End Sub

リスト2:接続要求を受け入れ,DBアクセスサーバーを起動する
Private Sub txtSockIn_KeyDown(KeyCode As Integer, Shift As Integer)
    mlngSock2 = accept(mlngSock, musrSockBuf, Len(musrSockBuf))
    Dim DBaccessObj As clsDBaccess
    Set DBaccessObj = CreateObject("DBsvrMT.clsDBaccess")
    DBaccessObj.ConnCli mlngSock2
    Set DBaccessObj = Nothing
End Sub

新規スレッドの作成

 次の2行では,同じActiveX EXE内にあるDBアクセスサーバークラスを起動し,先ほど生成したSocketのIDを引数にConnCliメソッドを実行する.

Set DBaccessObj = CreateObject( _
 "DBsvrMT.clsDBaccess")
DBaccessObj.ConnCli mlngSock2
 ここでクラスのインスタンス化にCreateObject関数を使用して,レイトバインディングを行なっているのには理由がある.この状況でSet Newキーワードを使いアーリーバインディングを行なうと,新しいクラスのインスタンスのために新規スレッドが生成されず,シングルスレッドとして動作してしまう.これは単に実証主義的な結論ではなく,マニュアルにもそのように記述されている.これがアーキテクチャに依存するものなのか,COMの実装に依存するものなのかはわからないが,そのように動作することは確かである.
 次のコードでオブジェクトを破棄しているのは接続リクエストブローカを呼び出す場合と同様である.

Set DBaccessObj = Nothing
 ActiveXクライアントがオブジェクトをリリースしても,フォームがオープンされたオブジェクトは実行を継続する.これ以降DBアクセスサーバーは,接続リクエストブローカとは無関係にクライアント処理を継続する.もちろんDBアクセスサーバーのひとつのインスタンスが応対するのは,ひとつのクライアントだけである.5つのクライアントが同時に接続しているならば,5つのDBアクセスサーバーが起動することになる.これらのインスタンスは,それぞれ別のスレッドで実行されるため,1人のユーザーのクエリー実行が終了するまで,別のユーザーが待たされることはない.もっとも,あるユーザーが重い処理を実行していれば,サーバーの処理全体が遅くなるのは避けられない.


接続の完了

 ConnCliメソッド(リスト3)では,WinsockコントロールのAcceptメソッドを実行している.引数に指定されているのは,Winsock APIのAccept関数が返したSocketのIDである.このようにWinsockコントロールのAcceptメソッドはWinsock APIのAccept関数に対応しているわけではないことに注意してほしい.
 この結論は実証主義的に導いたものであり,ドキュメントに記載されたものではないが,とにかくこれでWinsockコントロールによる通信が可能な状態になる.

リスト3:接続要求の受け入れ
Public Sub ConnCli(ByRef SocketHDL As Long)
   frm_WinsockDummy.PConnectCli SocketHDL
End Sub
Public Sub PConnectCli(ByRef SocketHDL As Long)
   Winsock1.accept SocketHDL
   Text1 = App.ThreadID 
End Sub

接続情報の受信

 クライアントが送信したデータを受信したときには,WinsockコントロールのDataArrivalイベントプロシージャが呼び出される.このプロシージャでは,今までSQL文の受信とデータアクセス,それに結果の返信を行なってきた.それはデータベースへの接続情報の受信は,マルチユーザーマネージメントサーバーが行なってきたからである.マルチユーザーマネージメントサーバーは,Type8から接続リクエストブローカと名称が変更され,純粋に接続の仲介だけを行なうことになった.
 それにともない,データベースへの接続情報もDataArrivalイベントプロシージャで受信することが必要になった.リスト4では,クライアントのホスト名とODBCデータソース名を受信しているが,コールバックをしないType8では,クライアントのホスト名には何の意味もない.大切なのはODBCデータソース名である.
 DBアクセスサーバーは,ODBCデータソース名をもとにデータベースへの接続を行なう.WinsockコントロールのGetDataメソッドで取得した受信内容から,ODBCのデータソース名に該当する部分を切り出し,RDOのOpenConnectionメソッドの引数に設定している.


リスト4:ユーザーが送信した接続情報を受信するためのイベントプロシージャ
Private Sub Winsock1_DataArrival(ByVal bytesTotal As Long)
   |
   |(中略:変数の宣言)
   |
    If connectFlag = 0 Then
        ' データの取得
        Winsock1.GetData strData, vbString
        intstr = InStr(1, Trim(strData), ",")
        If intstr > 0 Then
            ' 接続クライアント名
            txt_RemoteHost.Text = Left(Trim(strData), intstr - 1)
            ' ODBCデータソース名
            txt_dsn.Text = Mid(Trim(strData), intstr + 1, Len(Trim(strData)))
        End If
        
        Set en = rdoEnvironments(0)
        Set cn = en.OpenConnection(DSName:=txt_dsn.Text, _
                 Prompt:=rdDriverCompleteRequired)
        txt_LocalHost.Text = LOCAL_HOST
        connectFlag = 1
        Exit Sub
    End If
   |
   |(中略:データアクセスの処理)
   |
End Sub

使用可能なデータベースエンジン

 サンプルプログラムはJetデータベースエンジンを使用することを前提につくられているが,標準的なSQL文だけをサポートしているので,当然他のデータベースエンジンを使用することもできる.しかしType7までのマルチプロセスサーバーと違い,Type8はマルチスレッドで動作するため,データベースエンジン自体がマルチスレットに対応している必要がある.この場合,使用するエンジンがデータベースサーバーか,あるいはファイル共有型のデータベースエンジンかで意味がかわってくる.
 接続データベースがデータベースサーバーの場合はミドルウェアがマルチスレッドに対応していればよく,ファイル共有型の場合はデータベースエンジン自体がマルチスレッドに対応している必要がある.
 もっとも,データベースエンジンにデータベースサーバーを使用するのは,コストの点からいってメリットが少なく,他社製のファイル共有型のデータベースエンジンを使うこともすすめにくいので,やはりJetデータベースエンジン用と考えた方がよいのかもしれない.あるいはType8 DB Serverはインターネットを経由したデータベースアクセスをサポートするので,その用途に使用する方法もある.
 もちろん,Jetデータベースエンジンを使用してもインターネット経由でのアクセスが可能なので,社内にインターネットサーバーを公開する環境を持っているかたは,ぜひ試してみてほしい.ブラウザに頼ることなく,インターネット経由でサーバーへのデータアクセスができる.

データアクセス方法に関して

 データベースへのアクセスに関しては,現時点ではRDOを使用しているが,JetデータベースエンジンへのアクセスにおいてRDOがベストな方法なわけではない.現時点でのADOのメリットは大きくないが,今後のJetデータベースエンジンやMicrosoft SQL ServerがOLEDBネイティブとして実装されてくることを考えれば,ADOに変換するメリットはあるだろう.
 また,クライアント側のデータベースアクセスをコンポーネント化することも,早急に必要だろう.その場合,単にSocketクライアントとしてクライアントのコンポーネントを実装するか,あるいはOLEDBのデータプロパイダとして実装するかといった,アーキテクチャ上の選択が必要になる.もちろん,データの更新と追加といった処理にも,レコードセットレベルで対応する必要がある.楽しみである.


VB Magazine ライブラリ | Visual Basic WorkGroup
int21 ホームページ | PCDN ホームページ


Copyright (c) 1998 int21 Corporation All Rights Reserved.
For questions or comments, please send mail to: pcdn@int21.co.jp