サーバーからクライアントに通知したい!っていうことはよくある話だけど、残念ながら.NET Remoting にはそういう機能はない。
Google先生にも聞いてみたところ、双方向やろうとしている人はいるけど、クライアントがサーバーになってるだけだったりしている。
なんとなく気に入らないので、ちょっと手間だけどクライアント側に通知する仕組みを作ってみた。手間だけど1回作っちゃえばね。
考え方は Comet と呼ばれるもの。
Web の世界では有名?なものだが、要するに応答で通知するというもの。
通常であればクライアントからの要求は即結果を返してあげるものだが、サーバー側で応答を返さずに通知したいタイミングまでひたすら待ち合わせる。
しかるべきタイミングが来たら応答を返す。クライアント側は応答が返ってきたらそれをトリガーに動き出す。
これを使う。
例として「状態変化を通知する」というお題で話を進める。
絵で書くとこんな感じ。
それぞれの役割についてざっくり説明する。
【サーバーアプリケーション】
- 状態管理クラスを生成し、リモートチャネルに登録しておく。
- 状態変化が起きたら生成している状態管理クラスの状変通知メソッドをCALL
【クライアントアプリケーション】
- 状変監視クラスを生成し、状変イベントを登録後、状変監視開始メソッドをCALL
【共通DLL】
■状態管理クラス
- 状変問合せメソッドが CALL されたら、スレッドをブロックする
- 状変通知メソッドが CALL されたら、ブロックされているスレッドを解放する
■状変監視クラス
- 状変監視開始メソッドが CALL されたら、状態管理クラスインスタンスを取得する
- 状変問合せメソッドを CALL し、結果に応じて状変イベントを発行するという処理を繰り返す
応答で通知してから次の要求を受け付けるまでに状変が発生してしまったら。たらたら;;;
まぁ、最終更新日時でも持ってやりとりするしかないね。
ほいじゃぁソースコードで表してみますか。
※結構長くて読みづらいかも。。。
※サーバーとクライアントのチャネル登録処理なんかはそれぞれのアプリケーションに実装すべきかもだけど、よく使うことになるので共通DLLに組み込んでます。
【共通DLL】
まずはサーバーとクライアントそれぞれのチャネル登録処理
/// <summary>
/// リモートサーバ
/// </summary>
public class RemoteServer
{
/// <summary>
/// TCPサーバーチャネル
/// </summary>
private static TcpServerChannel serverChannel = null;
/// <summary>
/// 要求ディクショナリ
/// </summary>
private static Dictionary<string, MarshalByRefObject> requestDictionary = new Dictionary<string, MarshalByRefObject>();
/// <summary>
/// チャネル登録
/// </summary>
/// <param name="ipAddress">IPアドレス</param>
/// <param name="port">ポート番号</param>
public static void RegisterChannel(string ipAddress, int port)
{
// 既に起動している場合は復帰する
if (serverChannel == null)
{
// セキュリティ設定
IDictionary props = new Hashtable();
props["useIpAddress"] = true;
props["port"] = port;
props["bindTo"] = new UriBuilder("", ipAddress).Host;
// TCP Channelを作成
var channel = new TcpServerChannel(props, null);
// TCPチャネル登録
ChannelServices.RegisterChannel(channel, false);
serverChannel = channel;
}
}
/// <summary>
/// リモートオブジェクト登録
/// </summary>
/// <returns>成否</returns>
public static void RegisterRemoteObject(MarshalByRefObject request, string path, Type type)
{
if (!requestDictionary.ContainsKey(path))
{
RemotingServices.Marshal(request, path, type);
requestDictionary.Add(path, request);
}
}
/// <summary>
/// チャネル登録解除
/// </summary>
public static void UnregisterChannel()
{
if (serverChannel != null)
{
string errors = "";
//==========================================================
// リモートオブジェクト登録解除
//==========================================================
try
{
foreach (MarshalByRefObject request in requestDictionary.Values)
{
RemotingServices.Disconnect(request);
}
}
catch (Exception e)
{
errors += e.Message + Environment.NewLine;
}
finally
{
requestDictionary.Clear();
}
//==========================================================
// チャネル登録解除
//==========================================================
try
{
ChannelServices.UnregisterChannel(serverChannel);
}
catch (Exception e)
{
errors += e.Message + Environment.NewLine;
}
finally
{
serverChannel = null;
GC.Collect();
}
if(!string.IsNullOrEmpty(errors))
{
throw new Exception(errors);
}
}
}
}
/// <summary>
/// リモートクライアント
/// </summary>
public class RemoteClient
{
/// <summary>
/// TCPクライアントチャネル
/// </summary>
private static TcpClientChannel clientChannel;
/// <summary>
/// IPアドレス
/// </summary>
private static string ipAddress;
/// <summary>
/// ポート番号
/// </summary>
private static int portNo;
/// <summary>
/// チャネル登録
/// </summary>
/// <returns></returns>
public static void RegisterChannel(string ip, int port)
{
if (clientChannel == null)
{
// TCP Channel を作成
var channel = new TcpClientChannel();
// リモートオブジェクトを登録
ChannelServices.RegisterChannel(channel, false);
clientChannel = channel;
ipAddress = ip;
portNo = port;
}
}
/// <summary>
/// チャネル削除
/// </summary>
public static void UnregisterChannel()
{
if (clientChannel != null)
{
ChannelServices.UnregisterChannel(clientChannel);
clientChannel = null;
ipAddress = null;
portNo = 0;
}
}
/// <summary>
/// リモートオブジェクト取得
/// </summary>
/// <param name="path">パス</param>
/// <param name="type">タイプ</param>
/// <returns>リモートオブジェクト</returns>
public static object GetRemoteObject(string path, Type type)
{
// URI生成
UriBuilder uri = new UriBuilder();
uri.Scheme = "TCP" + Uri.SchemeDelimiter;
uri.Host = ipAddress;
uri.Port = portNo;
uri.Path = path;
// オブジェクトを作成
return Activator.GetObject(type, uri.ToString());
}
}
次、状態管理
/// <summary>
/// 状態管理
/// </summary>
public class Status : MarshalByRefObject
{
/// <summary>
/// 待ち合わせ
/// </summary>
private List<AutoResetEvent> autoResetList = new List<AutoResetEvent>();
/// <summary>
/// 最終更新日時
/// </summary>
private DateTime latestChangeDateTime = DateTime.Now;
/// <summary>
/// 状態変化問い合わせ要求(For Client)
/// </summary>
/// <param name="startTime">前回更新日時</param>
/// <returns>状態更新日時</returns>
public DateTime InquiryStatusChangeRequest(DateTime startTime)
{
try
{
AutoResetEvent autoReset;
lock (this)
{
if (startTime < this.latestChangeDateTime)
{
return this.latestChangeDateTime;
}
autoReset = new AutoResetEvent(false);
autoResetList.Add(autoReset);
}
autoReset.WaitOne();
}
catch
{
}
return this.latestChangeDateTime;
}
/// <summary>
/// 状態変化通知(For Server)
/// </summary>
public void NoticeStatusChange()
{
lock (this)
{
this.latestChangeDateTime = DateTime.Now;
foreach (var autoReset in autoResetList)
{
autoReset.Set();
}
autoResetList.Clear();
}
}
/// <summary>
/// 対象のインスタンスの有効期間ポリシーを制御する
/// </summary>
/// <returns>有効期間サービス オブジェクト</returns>
public override object InitializeLifetimeService()
{
return null;
}
}
次、状変監視
/// <summary>
/// 状変チェック
/// </summary>
public class StatusCheck
{
#region インスタンスフィールド
/// <summary>
/// 起動状態
/// </summary>
private bool isStarted = false;
/// <summary>
/// 状態チェックスレッド
/// </summary>
private Thread statusCheckThread;
#endregion
#region イベント
/// <summary>
/// 状態変化イベントハンドラ
/// </summary>
public delegate void StatusChangeEventHandler();
/// <summary>
/// 状態変化イベント
/// </summary>
private event StatusChangeEventHandler statusChangeEvent;
/// <summary>
///状態変化イベントを登録または削除します。
/// </summary>
public event StatusChangeEventHandler StatusChangeEvent
{
add { statusChangeEvent += value; }
remove { statusChangeEvent -= value; }
}
#endregion
/// <summary>
/// コンストラクタ
/// </summary>
/// <param name="statusChangeCallBack"></param>
public StatusCheck(StatusChangeEventHandler statusChangeCallBack)
{
try
{
statusChangeEvent = statusChangeCallBack;
startStatusCheck();
}
catch (Exception ex)
{
throw ex;
}
}
/// <summary>
/// 状態チェックリスタート
/// </summary>
public void RestartStatusCheck()
{
try
{
if (this.isStarted)
{
throw new Exception("既に起動中です。");
}
startStatusCheck();
}
catch (Exception e)
{
throw e;
}
}
/// <summary>
/// 状態チェック停止
/// </summary>
public void StopStatusCheck()
{
this.isStarted = false;
if (this.statusCheckThread != null)
{
this.statusCheckThread.Abort();
}
}
/// <summary>
/// 状変監視開始
/// </summary>
private void startStatusCheck()
{
if (this.isStarted)
{
// 開始中の場合は復帰
return;
}
var status = (Status)RemoteClient.GetRemoteObject(RemotingConstant.PATH_STATUS, typeof(Status));
this.isStarted = true;
ThreadStart start = delegate()
{
DateTime latestDate = DateTime.MinValue;
while (this.isStarted)
{
try
{
// 状態変化問い合わせ要求
latestDate = status.InquiryStatusChangeRequest(latestDate);
// イベント通知
if (statusChangeEvent != null)
{
statusChangeEvent.BeginInvoke(null, null);
}
}
catch (Exception e)
{
break;
}
}
this.isStarted = false;
};
this.statusCheckThread = new Thread(start);
this.statusCheckThread.IsBackground = true;
this.statusCheckThread.Start();
}
}
/// <summary>
/// リモートアクセス用定数
/// </summary>
public class RemotingConstant
{
/// <summary>
/// URIパス定義(状態)
/// </summary>
public const string PATH_STATUS = "Status";
}
【サーバーアプリケーション】
初期化ね。
// チャネル登録
RemoteServer.RegisterChannel("0.0.0.0", 50000);
//状態管理
var status = new Status();
RemoteServer.RegisterRemoteObject(status, RemotingConstant.PATH_STATUS, typeof(Status));
通知処理は
// 通知
status.NoticeStatusChange();
【クライアントアプリケーション】
// チャネル登録
RemoteClient.RegisterChannel("0.0.0.0", 50000);
// 状態監視開始
var statusCheck = new StatusCheck(statusChangeEvent);
/// <summary>
/// 状態変化イベント
/// </summary>
private static void statusChangeEvent()
{
}
通知したい内容がたくさんあって、時系列で各情報を通知したければ、状態変化クラスにあたるやつが、時刻+通知情報を管理して前回通知した時刻以降に発生したものを全て通知するという処理を書くことになるが、それはサンプルを書くまでもないので省略。
0 件のコメント:
コメントを投稿