・アプリケーション作成
今回掲載するアプリケーションは1つのソリューションに
2つのプロジェクトを作成します。プロセス間通信ですから、当然2つのEXEファイルが必要です。
もちろん、1つのEXEでデータの送信/受信の両機能を盛り込むことも可能ですが、流れがわかりにくい
のであえてサーバー機能を持ったEXEとクライアント機能を持ったEXEの2つを作成します。
どういうアプリかというと、サーバ側EXEがSDIウィンドウで一定時間間隔でクライアント領域の
背景色が変化させ、同時に乱数を表示します。クライアント側EXEがダイアログベースでサーバ側の
現在の背景色と乱数値を取得して表示するだけのごく簡単なものです。
プログラムの中身はクライアント側から現在のサーバ側の背景色・乱数値のリクエストをすると、
サーバ側は共有メモリに書き込んで、それを取得してクライアント側のダイアログの
スタティックコントロールに表示するというものです。この中で同期オブジェクトは、イベント、
セマフォを用います。
クリティカルセクションに関しては説明はしますが、
実際にプログラムには使いません。ミューテックスは前頁の「二重起動の防止」の説明だけとします。
この後コードを掲載しますが、それだけではデータの流れ等よくわかないかもしれないので、
概略を先に図示します。
実線がデータの流れ(サーバのクライアント領域の背景色データ)、破線が命令系統の流れです。
では、早速作成していきます。
<サーバプログラム作成>
まず、サーバ側のプロジェクトから作成します。
VSのメニューからファイル(F)→新規作成(N)→プロジェクト(P)でダイアログが表示されるので
次のようなプロジェクトを作成してください。
プロジェクトの種類(P) : Visual C++ MFC テンプレート(T) : MFCアプリケーション プロジェクト名 : IpcSrv →OK
アプリケーションウィザードで以下のようにしてください。
アプリケーションの種類 : シングルドキュメント(S) ドキュメントビューアーキテクチャーサポート : チェックをはずす ユニコードライブラリを使用する : チェックする プロジェクト形式 : MFC標準 MFCの使用法 : 共有DLLでMFC使用 →次へ データベースサポート : なし →次へ メインフレームのスタイル : サイズ変更(T)、最小化ボタン(I)、 最大化ボタン(A)、システムメニュー(S)にチェック。 ツールバー : なし(O) →次へ 高度な機能 デフォルトのまま。 →次へ →完了。
以上でスケルトンができました。
<共有情報用のヘッダ作成>
サーバプログラムができた後、クライアントプログラムを作成しますが、通信するので データ共有のための情報が必要になるので先にそのヘッダファイルを作成します。このファイルを サーバ側・クライアント側でインクルードすることになります。
メニューの表示(V)→ソリューションエクスプローラ(P)でソリューションエクスプローラを
表示してください。
ヘッダーファイルフォルダを右クリックして追加(D)→新しい項目(W)でダイアログを表示して
以下のヘッダファイルを作成してください。
カテゴリ(C) : Visual C++ コード テンプレート(T) : ヘッダファイル(.h) ファイル名 : IpcShare.h
以下の記述をしてください
//プロセス間通信用ヘッダ //(同期オブジェクト名, 共有メモリ、ファイルマッピングオブジェクト名) #ifndef _IPCSHARE_H_ #define _IPCSHARE_H_ //サーバ側Window表題プレフィックス(サーバ側Window表題はプレフィックスにプロセスIDを付加) #define SRV_CAPTION "IpcSrv-" //ファイルマッピングオブジェクト名 #define IPC_FILEMAP_NAME "IpcFileMap" //同期用イベント名 //Clientよりデータ要求イベント名(Server, Client両者で利用) #define EVENT_REQUEST_NAME "EventIpcSrvRequest_" //Severから共有メモリ書換えイベント名(Server, Client両者で利用) #define EVENT_COMPLETE_NAME "EventIpcSrvComplete_" //Serverのセカンダリスレッド終了イベント名(Serverのみ) #define EVENT_TERMINATE_NAME "EventIpcSrvTerminate_" //データ要求セマフォ名(要求権) (Clientのみ) #define SEMA_REQUEST_NAME "SemaphoreIpcCliRequest" //共有メモリ定義 typedef struct { DWORD dwSize; //データサイズ BYTE data[1]; //可変長データ } SHARE_MEM; //共有最大データサイズ #define SHARED_DATA_SIZE ( sizeof(COLORREF) + sizeof(int) ) //必要に応じてサイズ変更 #endif //_IPCSHARE_H_ |
<メインフレームクラスの実装>
まず、フレームウィンドウクラスの実装から。MainFrm.cppに以下のコードを記述してください。
// MainFrm.cpp : CMainFrame クラスの実装 #include "IpcShare.h" //プロセス間通信用ヘッダ //・ //・ int CMainFrame::OnCreate(LPCREATESTRUCT lpCreateStruct) { if (CFrameWnd::OnCreate(lpCreateStruct) == -1) return -1; //ウィンドウ表題設定(クライアントからこのウィンドウの表題検査するためプレフィックス設定) CString strCaption( _T(SRV_CAPTION) ); CString strProcessId; strProcessId.Format( _T("%d"), ::GetCurrentProcessId() ); strCaption += strProcessId; SetWindowText(strCaption); // フレームのクライアント領域全体を占めるビューを作成します。 if (!m_wndView.Create(NULL, NULL, AFX_WS_DEFAULT_VIEW, CRect(0, 0, 0, 0), this, AFX_IDW_PANE_FIRST, NULL)) { TRACE0("ビューウィンドウを作成できませんでした。\n"); return -1; } return 0; } |
CMainFrame::OnCreateにウィンドウのタイトルを設定します。 クライアントからサーバを検索するとき に、このタイトル名(SRV_CAPTION + プロセスID)を使って行います。
<データエクスポート管理クラスの実装>
今回のサーバ側のメインになるクラスです。クライアントからの背景色要求リクエストに 対して、現在の背景色を共有メモリに書き込んで再びクライアントに通知します。
メニューのプロジェクト(P)→クラスの追加(C)で ダイアログボックスを表示して以下のようにしてください。
カテゴリ(C) : C++ テンプレート(T) : C++クラス →追加(A) クラス名 : CExport (他はデフォルト) →完了
CExportクラスができているので ヘッダファイル・実装ファイルを以下のように記述してください。
//Export.h : CExport クラスヘッダ //IpcSrvデータのプロセス外エクスポートクラス //(ワーカスレッドを作成し、共有メモリで他プロセス要求データをエクスポート) #pragma once #include <afxmt.h> //MFC同期オブジェクトヘッダ #include "IpcShare.h" //同期オブジェクト・共有メモリ用ヘッダ //親ウィンドウへのデータ取得要求メッセージ //(データ要求されたメッセージハンドラ内でデータ取得成功時は0, 失敗時は1を返却) #define WM_REQUEST_DATA (WM_USER + 11) class CExport { //メンバ変数//////////////////////////////////////////////////////////////////////////// protected: HANDLE m_hFileMap; //ファイルマッピングオブジェクトハンドル SHARE_MEM *m_pShareMem; //共有メモリへのポインタ CEvent *m_pEventRequest; //IpcSrvデータ要求イベント(トリガ) CEvent *m_pEventComplete; //共有メモリ書込み完了イベント CEvent *m_pEventTerminate; //セカンダリスレッド終了イベント CWinThread *m_pThread; //セカンダリスレッドへのポインタ CWnd *m_pParent; //このクラスの親ウィンドウ(ビューウィンドウ) //メンバ関数//////////////////////////////////////////////////////////////////////////// protected: void Terminate(); //終了処理 public: CExport(); //コンストラクタ ~CExport(); //デストラクタ int Initialize(CWnd *pParent); //初期化 BOOL CopyToSharedMemory(const void *pData, const DWORD dwSize); //データを共有メモリにコピー static UINT __cdecl ExportProc(LPVOID pParam); //セカンダリスレッド制御関数 }; |
続いて実装ファイルに以下を記述してください。
// Export.cpp : CExport クラスの実装 #include "StdAfx.h" #include "Export.h" //コンストラクタ CExport::CExport() : m_hFileMap(NULL) , m_pShareMem(NULL) , m_pEventRequest(NULL) , m_pEventComplete(NULL) , m_pEventTerminate(NULL) , m_pThread(NULL) , m_pParent(NULL) { } //デストラクタ CExport::~CExport() { Terminate(); } //終了処理 void CExport::Terminate() { //セカンダリスレッド終了 if (m_pEventTerminate && m_pThread) { m_pEventTerminate->SetEvent(); ::WaitForSingleObject(m_pThread->m_hThread, INFINITE); delete m_pThread; m_pThread = NULL; } //ファイルのビューのマッピング解除 if (m_pShareMem) {::UnmapViewOfFile(m_pShareMem); m_pShareMem = NULL;} //ファイルマップオブジェクトハンドルClose if (m_hFileMap) {::CloseHandle(m_hFileMap); m_hFileMap = NULL;} //イベント削除 if (m_pEventRequest) {delete m_pEventRequest; m_pEventRequest = NULL;} if (m_pEventComplete) {delete m_pEventComplete; m_pEventComplete = NULL;} if (m_pEventTerminate) {delete m_pEventTerminate m_pEventTerminate = NULL;} //親ウィンドウ登録解除 if (m_pParent) m_pParent = NULL; } /*初期化(データエクスポート用セカンダリスレッド開始 共有オブジェクト構築・マッピング 同期オブジェクト(イベント)作成) 引数 pParent 親ウィンドウへのポインタ(IpcSrvのビューウィンドウへのポインタ) 戻値 失敗-1 初期化済み0 成功1 */ int CExport::Initialize(CWnd *pParent) { if (! pParent) return -1; //初期化済みの時終了 if (m_pThread && m_hFileMap && m_pShareMem && m_pEventRequest && m_pEventComplete && m_pEventTerminate ) return 0; int nRet = -1; do { if (m_pThread || m_hFileMap || m_pShareMem || m_pEventRequest || m_pEventComplete || m_pEventTerminate ) break; //ファイルマッピングオブジェクト作成 m_hFileMap = ::CreateFileMapping(INVALID_HANDLE_VALUE, NULL, PAGE_READWRITE, 0, sizeof(DWORD) + SHARED_DATA_SIZE, _T(IPC_FILEMAP_NAME) ); if (! m_hFileMap) break; //ファイルビューのマッピング m_pShareMem = (SHARE_MEM *)::MapViewOfFile(m_hFileMap, FILE_MAP_WRITE, 0, 0, 0); if (! m_pShareMem) break; //各イベント作成(各イベントプレフィックスに //プロセスIDを付加したものをイベント名にする) CString strProcessId; strProcessId.Format( _T("%d"), ::GetCurrentProcessId() ); CString strRequest( _T(EVENT_REQUEST_NAME) ); strRequest += strProcessId; CString strComplete( _T(EVENT_COMPLETE_NAME) ); strComplete += strProcessId; CString strTerminate( _T(EVENT_TERMINATE_NAME) ); strTerminate += strProcessId; m_pEventRequest = new CEvent(FALSE, FALSE, strRequest, NULL); m_pEventComplete = new CEvent(FALSE, FALSE, strComplete, NULL); m_pEventTerminate = new CEvent(FALSE, FALSE, strTerminate, NULL); //セカンダリスレッド開始 m_pThread = AfxBeginThread(ExportProc, this, 0, 0, CREATE_SUSPENDED, NULL); m_pThread->m_bAutoDelete = FALSE; m_pThread->ResumeThread(); //親ウィンドウ登録 m_pParent = pParent; nRet = 1; } while(0); if (nRet < 0) //エラー時 Terminate(); return nRet; } /*親ウィンドウ内のデータを共有メモリにコピー 引数 pData 共有メモリに格納するデータ dwSize pDataのバイト数 戻値 失敗時は共有メモリは0でクリアされる*/ BOOL CExport::CopyToSharedMemory(const void *pData, const DWORD dwSize) { if (! m_pShareMem) return FALSE; BOOL bRet = FALSE; do { if (! pData || ! dwSize) break; //共有メモリのサイズがコピー元のサイズより小さい時はFALSE if ( dwSize > SHARED_DATA_SIZE ) break; m_pShareMem->dwSize = dwSize; //dataのサイズ ::RtlCopyMemory(m_pShareMem->data, pData, dwSize); //dataのコピー bRet = TRUE; } while(0); if (! bRet) //失敗時はで共有メモリ初期化 ::memset(m_pShareMem, 0, sizeof(DWORD) + SHARED_DATA_SIZE); return bRet; } /*セカンダリスレッド制御関数 引数pParam CExportへのポインタ(CExport::Initialize内のAfxBeginThreadを参照)*/ UINT CExport::ExportProc(LPVOID pParam) { CExport *pExport = static_cast<CExport *>(pParam); CSyncObject *pSync[] = {pExport->m_pEventRequest, pExport->m_pEventTerminate}; CMultiLock multilock(pSync, sizeof pSync / sizeof pSync[0], FALSE); //m_pEventRequestイベントがシグナルである間ループ //(m_pEventRequest, m_pEventTerminateのどちらかがシグナルになるまで待機) while( multilock.Lock(INFINITE, FALSE, 0) == WAIT_OBJECT_0 ) { //WM_REQUEST_DATAメッセージを親ウィンドウへ送り、共有メモリへデータ格納要求 if (pExport->m_pParent->SendMessage(WM_REQUEST_DATA, NULL, NULL) ) continue; //共有メモリにデータ書込み完了イベントシグナル化 pExport->m_pEventComplete->SetEvent(); } AfxEndThread(0, FALSE); return 0; } |
このクラスがサーバ側の中心的な部分で上図で機能の概略は
書いていますが、もう少し詳しく書いてみます。このクラスのインスタンスが作られ
(以下でビューウィンドウのOnCreateでインスタンス化されます)、Initializeを実行すると、
ファイルマッピングオブジェクトの作成、マッピングを行い、同時に各イベントの作成をして、
最後にセカンダリスレッドを起動してクライアントからのデータ要求を待機するようにします。
本来はセカンダリスレッド作成などを含むマルチスレッドのことを先に書かないといけないのですが、
さらにややこしくしたくないので、とりあえす「こう作る方法もある」程度で流してください。
いずれ書くかもしれません。
ということで、Initializeを呼ぶと上図のような状態でクライアントからの
要求を待機できるようになります。
セカンダリスレッドはCExport::ExportProc関数で制御され、 スレッド終了イベント(m_pEventTerminate)がシグナルになるまで無限ループになっていて データ要求に対応するようになっています。
クライアントから要求がきたら、ループ内に制御が解放され、親ウィンドウにデータの
要求が来たことを通知するメッセージ(WM_REQUEST_DATA)を送りますが、SendMessageで送っているので
親ウィンドウ側(ビュークラス)でメッセージハンドラの処理が済むまで制御が戻りません。
親ウィンドウのメッセージハンドラ内で処理されるのがCExport::CopyToShareMemoryです。
ここで、共有メモリにデータを書き込んでいます。
メッセージハンドラの処理が終わり、制御が返ると書き込み完了のイベント(m_pEventComplete)を
SetEventして、クライアント側で待機しているスレッドに通知します。
<ビュークラスの実装>
ビュークラスでは一定間隔で背景色を変更する機能と、セカンダリスレッドから届いた データ要求に対してデータエクスポート管理クラス(CExport)が、 共有メモリにデータを書き込む機能を加えます。
まず、ChildView.hヘッダファイルを次のように定義してください。
// ChildView.h : CChildView クラスのインターフェイス // #pragma once #include "Export.h" //プロセス外データエクスポート管理クラス用ヘッダ // CChildView ウィンドウ class CChildView : public CWnd { // ・ // ・ // ・ protected: CBrush m_brush; //Viewの背景色 int m_nValue; //ウィンドウ表示値 CArray<COLORREF, COLORREF> m_colpattern; //背景色パターン CExport m_export; //共有メモリ管理クラス public: //背景色変更用タイマ afx_msg void OnTimer(UINT_PTR nIDEvent); //ビュー構築時 afx_msg int OnCreate(LPCREATESTRUCT lpCreateStruct); //ビュー破壊時 afx_msg void OnDestroy(); //セカンダリスレッドからのデータ要求メッセージハンドラ afx_msg LRESULT OnRequestData(WPARAM wParam, LPARAM lParam); DECLARE_MESSAGE_MAP() }; |
次に実装ファイルに以下を記述してください。
// ChildView.cpp : CChildView クラスの実装 // #include "stdafx.h" #include "IpcSrv.h" #include "ChildView.h" // ・ // ・ // CChildViewコンストラクタ CChildView::CChildView() : m_brush( RGB(255, 255, 255) ) //背景色用ブラシ初期化 , m_nValue(0) //ウィンドウ表示値初期化 { //背景色パターン登録 m_colpattern.Add( RGB(255, 0, 0 ) ); m_colpattern.Add( RGB(255, 255, 0 ) ); m_colpattern.Add( RGB(0, 255, 0 ) ); m_colpattern.Add( RGB(0, 255, 255) ); m_colpattern.Add( RGB(0, 0, 255) ); m_colpattern.Add( RGB(255, 0, 255) ); m_colpattern.Add( RGB(255, 255, 255) ); ::srand( (unsigned)::time(NULL) ); //乱数初期化 } //デストラクタ CChildView::~CChildView() { m_brush.DeleteObject(); //m_brush GDIリソース解放 } BEGIN_MESSAGE_MAP(CChildView, CWnd) ON_WM_PAINT() ON_WM_TIMER() ON_WM_CREATE() ON_WM_DESTROY() //セカンダリスレッドからのデータ要求 ON_MESSAGE(WM_REQUEST_DATA, &CChildView::OnRequestData) END_MESSAGE_MAP() // ウィンドウ構築前 BOOL CChildView::PreCreateWindow(CREATESTRUCT& cs) { // ・ // ・ } //Window描画 void CChildView::OnPaint() { CPaintDC dc(this); // 描画のデバイスコンテキスト CRect rect; CString strValue; GetClientRect(&rect); dc.FillRect(&rect, &m_brush); //指定のブラシで描画 strValue.Format(_T("%d"), m_nValue); rect.DeflateRect(rect.Width() / 4, rect.Height() / 4); dc.SetBkMode(TRANSPARENT); dc.DrawText(strValue, &rect, DT_CENTER); //乱数値表示 } //Window構築時 int CChildView::OnCreate(LPCREATESTRUCT lpCreateStruct) { if (CWnd::OnCreate(lpCreateStruct) == -1) return -1; // 背景色切替え用タイマセット if (! SetTimer(1, 2000, NULL) ) return -1; //共有データ管理クラス構築 if (! m_export.Initialize(this) ) ::AfxMessageBox(_T("共有データ管理オブジェクト構築失敗 - 通信不能")); return 0; } //Window破壊時 void CChildView::OnDestroy() { CWnd::OnDestroy(); KillTimer(1); //タイマ終了 } //タイマ void CChildView::OnTimer(UINT_PTR nIDEvent) { static INT_PTR nIdx = 0; //背景色パターン切替 if ( ++nIdx > m_colpattern.GetUpperBound() ) nIdx = 0; //ブラシ作成 m_brush.DeleteObject(); m_brush.CreateSolidBrush( m_colpattern.GetAt(nIdx) ); //ウィンドウ表示値設定 m_nValue = ::rand(); //ビュー再描画 Invalidate(); } /*共有データ要求ハンドラ 戻値 共有データに正常に書き込めた時0、それ以外1*/ LRESULT CChildView::OnRequestData(WPARAM wParam, LPARAM lParam) { LRESULT nRet = 1; LOGBRUSH lb; BYTE *pBuf = new BYTE[SHARED_DATA_SIZE]; do { ::memset( &lb, 0, sizeof(LOGBRUSH) ); ::memset( pBuf, 0, SHARED_DATA_SIZE ); //m_brushの色のRGB値が格納されているLOGBRUSH構造体取得 if ( ! m_brush.GetLogBrush(&lb) ) break; *( (COLORREF *)pBuf ) = lb.lbColor; //背景色取得 *( (int *)(pBuf + sizeof(COLORREF)) ) = m_nValue; //乱数値取得 //背景色・乱数値を共有メモリにコピー if ( ! m_export.CopyToSharedMemory(pBuf, SHARED_DATA_SIZE) ) break; nRet = 0; } while(0); delete [] pBuf; return nRet; } |
(クリティカルセクションについて)
今回はセカンダリスレッドからメッセージを飛ばして、プライマリスレッドで
背景色データ(m_brush)・乱数値(m_nValue)にアクセスしているので、同期処理は必要ないですが
(メッセージ同期機構)、もし、セカンダリスレッド内で直接プライマリスレッドで作成された
CChildViewのメンバであるm_brush、m_nValueにアクセスする時は同期処理が必要になります。
プリマリスレッドのCChildView::OnTimerで一定間隔でm_brush、m_nValueが書き換えられ、
セカンダリスレッドでそのm_brush、m_nValueにアクセスされることになりますが、両スレッドは
各々単独で動いているので、例えばOnTimerでブラシがDeleteObjectされた瞬間にセカンダリスレッドから
m_brushをされると不味いからです。
この場合はクリティカルセクションを使います。その場合m_brush、m_nValueが書き換えられる
CChildView::OnTimerで次のような感じでCCriticalSectionを使わなければならないでしょう。
static INT_PTR nIdx = 0; CSingleLock slock(&m_critsec); //既にCCriticalSectionクラスの //m_critsecが構築されている slock.Lock(); if ( slock.IsLocked() ) { if ( ++nIdx > m_colpattern.GetUpperBound() ) nIdx = 0; m_brush.DeleteObject(); m_brush.CreateSolidBrush( m_colpattern.GetAt(nIdx) ); m_nValue = ::rand(); slock.Unlock(); //m_brushリソース解放 Invalidate(); //再描画 }
もちろん、セカンダリスレッド側も同じようにCCriticalSectionを使って同期をしなければなりません。
以上でサーバ側の実装ができたので、とりあえずビルド後実行してクライアント領域の背景色・乱数値が 変化していくかを確認してください。続いて、クライアントアプリケーションの作成に移ります。