忍者ブログ

VB.NET-TIPS などプログラミングについて

VB.NETのTIPS(小技集)を中心に、Javascript、PHP その他のプログラミングについて少し役に立つ情報を発信します。いわゆる個人的な忘備録ですが、みなさんのお役に立てれば幸いです。

Remoting の IPC を使ったプロセス間通信について

同一PC上でプロセス(EXE)間で通信を行いたいことはよくあります。 例えば、1個のPGを何か受信専用として動作させておいて、別のPGではメインの処理を行いながら、たまに受信データの処理を行うなどの要求が出てきたりします。 別EXE同士でやり取りを行う場合には以下の様な方法があると思います。

  • WindowsAPIのメッセージ通信
  • WindowsAPIのPIPEでの通信
  • WindowsAPIのファイルマッピングでの通信
  • 通常のファイルを使った通信
  • 遅くてOKならば、データベースを介した通信

尚、今回は勉強もかねて RemotingIPC を使って、クライアントプロセス(EXE)からサーバープロセス(EXE)にメッセージを送信することをやってみます。
今回の例では、1個のソリューションの中に以下の3個のプロジェクトを生成します。

  • クライアントとサーバーで共有するクラスDLL
  • サーバープロセスの実行PG(EXE)
  • クライアントプロセスの実行PG(EXE)

先ず、プロセス間通信の準備として、クライアントとサーバーで共有するクラスの生成を行います。
以下のソースを見て下さい。MarshalByRefObject をクラスを継承したクラス ServiceClass を宣言しています。
内容的には多くのことをするわけでは無く、クライアントから呼出されるであろうメソッドで、 文字列データを引数としてイベントを raise しています。
このプロジェクトは IpcService と命名し、クラスライブラリ(DLL)としてビルドしておきます。
(後からこのDLLを、サーバープロセス及びクライアントプロセスのプロジェクトで参照設定します。)

クライアントとサーバーで共有するクラス

Public Class ServiceClass
    Inherits MarshalByRefObject

    'クライアントから呼び出しを受け、イベントを発生させる
    Public Sub RaiseServerEvent(ByVal message As String)
        'イベントを発生させる
        RaiseEvent RaiseClientEvent(message)
    End Sub

    Public Event RaiseClientEvent(ByVal messsage As String)

End Class

プロジェクトの定義は以下の様になります。



次に、サーバープロセスPGのプロジェクトを生成します。
1個のフォーム frmIpcServer をプロジェクトに追加し、そのフォーム上に1個のボタンとリッチエディットBOXを設置します。
フォームの静的変数として上記で説明した共有クラス(ServiceClass)の生成を行います。 また、ServiceClass から raise されるイベントの処理として、リッチエディットBOXに受信したメッセージを表示する為に、 デリゲート処理を宣言しています。
ボタンの押下によりIPCの受信開始を行うのですが、以下の様な手順で行います。

  • IpcServerChannel によりIPCチャネルを用意し、ChannelServices.RegisterChannel でチャネルを登録
  • RemotingConfiguration.RegisterWellKnownServiceType でサービスのタイプを宣言
  • RemotingServices.Marshal により共有クラスの参照許可
  • IpcChannel.StartListening によりIPCの受信を開始

サーバープロセスPG

Imports System.Runtime.Remoting
Imports System.Runtime.Remoting.Channels
Imports System.Runtime.Remoting.Channels.Ipc
Imports IpcService

Public Class frmIpcServer

    'IPC用クラスの生成
    Private WithEvents IpcServiceClass As New IpcService.ServiceClass

    '受信回数
    Private mintRcv As Integer = 0

    '別プロセスからのメッセージを処理するためデリゲートを利用
    Delegate Sub SetRichTextBox1Delegate(ByVal Value As String)

    Private RichTextBox1Delegate As New SetRichTextBox1Delegate(AddressOf AppendTextRichTextBox1)

    'リッチテキストボックスにメッセージを表示する
    Private Sub AppendTextRichTextBox1(ByVal message As String)
        RichTextBox1.AppendText(message)
    End Sub

    'IPC受信開始
    Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.Click
        'IPCチャネルを用意
        Dim IpcChannel As New IpcServerChannel("ipcPort")
        ChannelServices.RegisterChannel(IpcChannel, False)
        Dim strSClassName As String = GetType(ServiceClass).Name
        RemotingConfiguration.RegisterWellKnownServiceType(GetType(ServiceClass), strSClassName, WellKnownObjectMode.SingleCall)
        '「ServiceClass」を参照できるように設定
        Dim ref As ObjRef = RemotingServices.Marshal(IpcServiceClass, strSClassName)

        'IPC受信準備
        IpcChannel.StartListening(Nothing)
        RichTextBox1.AppendText("IPC channel is ready to receive." & vbNewLine)
    End Sub

    'IPC用クラスのイベント処理
    Private Sub IpcServiceClass_RaiseClientEvent(ByVal message As String) Handles IpcServiceClass.RaiseClientEvent
        'このイベント処理は、別プロセスのServiceClassからRaiseされるので、
        'このフォームのコントロールにアクセスするにはデリゲート処理を行う
        '(クライアントから受信したメッセージを処理)
        mintRcv += 1
        Me.Invoke(RichTextBox1Delegate, New Object() {mintRcv.ToString & ":" & message & vbNewLine})
    End Sub
   
End Class

尚、このプロジェクトの参照設定で以下のものを追加します。

  • System.Runtime.Remoting
  • IpcService





最後に、クライアントプロセスPGのプロジェクトを生成します。
1個のフォーム frmIpcClient をプロジェクトに追加し、そのフォーム上に1個のボタンとディットBOXを設置します。
フォームロード時に ChannelServices.RegisterChannel でIPCチャネルの用意を行います。
ボタン押下時にメッセージ送信処理を行うのですが以下の様な手順で行います。

  • リモートIPCオブジェクトの URL を定義して Activator.GetObject 参照を取得
  • その参照を共有クラスのオブジェクト変数に代入
  • 共有参照クラスのイベント生成メソッド呼び出し

このプロジェクトの参照設定もサーバープロセスPGと同様です。

クライアントプロセスPG

Imports System.Runtime.Remoting
Imports System.Runtime.Remoting.Channels
Imports System.Runtime.Remoting.Channels.Ipc
Imports IpcService

Public Class frmIpcClient

    'IPC準備
    Private Sub Form1_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
        'IPCチャネルを用意する
        Dim ipcChannel As New IpcClientChannel
        ChannelServices.RegisterChannel(ipcChannel, False)
    End Sub

    'メッセージを送信する
    Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.Click
        Try
            '既存のリモートIPCオブジェクトへの参照を取得する
            Dim strURL As String = "ipc://ipcPort/" + GetType(ServiceClass).Name
            Dim objRemote As Object = Activator.GetObject(GetType(ServiceClass), strURL)

            'サーバとの共通クラスの参照(ServiceClass)
            Dim objServiceClass As ServiceClass = CType(objRemote, ServiceClass)
            '共有参照クラスのイベント生成メソッド呼び出し
            objServiceClass.RaiseServerEvent("Message From Client=>" & Me.TextBox1.Text)

        Catch ex As Exception
            MsgBox(ex.Message)
        End Try
    End Sub

End Class

このソリューションをデバッグ実行するために、ソリューションのプロパティを以下の図の様に設定します。

このソリューションをデバッグ実行すると以下の様になります。
サーバーPGの起動後、「Start IPC Channel」ボタンを押下して IPC 受信の準備を行います。 その後、クライアントPGのボタンを押下するごとに、サーバーPGにメッセージが表示されていきます。

今回のPGはまだまだ不備な点が多いので実用には供しないですが、ご参考になればと思います。

関連する記事

Remoting の IPC を使ったプロセス間通信についてその2(HTTPチャネル)
名前付きパイプを使ったプロセス間通信について
名前付きパイプを使ったプロセス間通信についてその2(複数クライアントとの通信)
名前付きパイプを使ったプロセス間通信についてその3(クライアントとの双方向通信)











PR

コメント

1. 色々と質問しますので教えて下さい

サーバー側のプログラムを起動し、「IPC channel is ready to receive.」が出力されるまでは正常に動作しますが、
クライアントからメッセージを送信すると以下のような例外が発生します。
型 'XXXXXX, XXXXXX, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null' を読み込めません。
(XXXXXXはこのプロジェクトで定義されたクラス名)
この例外を発生させなくする方法を教えて下さい。

また、サーバー側での
'IPC用クラスのイベント処理
' Me.Invoke(RichTextBox1Delegate, New Object() {mintRcv.ToString & ":" & message & vbNewLine})
Me.Invokeでコンパイルが通りません。この対処方法も教えて下さい。

2. 無題

「型 'XXXXXX, XXXXXX, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null' を読み込めません。 」
このエラーは何かDLLの参照がされていない様に思います。

以下のものは参照設定されていますでしょうか?
・System.Runtime.Remoting
・IpcService

以上、宜しくお願い致します。

3. 無題

やはり動きません。同じエラーが出ます。

こちらで動かしているソースを示しますので、どこが悪いか教えて下さい。
環境 VS2019 Community OS:Windows7
(サーバー側)
Imports System.Runtime.Remoting
Imports System.Runtime.Remoting.Channels
Imports System.Runtime.Remoting.Channels.Ipc



Class MainWindow
Private Sub MainWindow_Loaded(sender As Object, e As RoutedEventArgs) Handles Me.Loaded
Dim h1 As New frmIpcServer
h1.Button1_Click()

End Sub
End Class



Public Class frmIpcServer

'IPC用クラスの生成
Private WithEvents IpcServiceClass As New ServiceClass

'受信回数
Private mintRcv As Integer = 0

'別プロセスからのメッセージを処理するためデリゲートを利用
Delegate Sub SetRichTextBox1Delegate(ByVal Value As String)

Private RichTextBox1Delegate As New SetRichTextBox1Delegate(AddressOf AppendTextRichTextBox1)

'リッチテキストボックスにメッセージを表示する
Private Sub AppendTextRichTextBox1(ByVal message As String)
Debug.Print(message)
End Sub

'IPC受信開始
Public Sub Button1_Click()
'IPCチャネルを用意
Dim IpcChannel As New IpcServerChannel("ipcPort")
ChannelServices.RegisterChannel(IpcChannel, False)
Dim strSClassName As String = GetType(ServiceClass).Name
RemotingConfiguration.RegisterWellKnownServiceType(GetType(ServiceClass), strSClassName, WellKnownObjectMode.SingleCall)
'「ServiceClass」を参照できるように設定
Dim ref As ObjRef = RemotingServices.Marshal(IpcServiceClass, strSClassName)

'IPC受信準備
IpcChannel.StartListening(Nothing)
Debug.Print("IPC channel is ready to receive." & vbNewLine)
End Sub

'IPC用クラスのイベント処理
Private Sub IpcServiceClass_RaiseClientEvent(ByVal message As String) Handles IpcServiceClass.RaiseClientEvent
'このイベント処理は、別プロセスのServiceClassからRaiseされるので、
'このフォームのコントロールにアクセスするにはデリゲート処理を行う
'(クライアントから受信したメッセージを処理)
mintRcv += 1
Stop
'↓Me.Invoke でコンパイルが通りません。
' Me.Invoke(RichTextBox1Delegate, New Object() {mintRcv.ToString & ":" & message & vbNewLine})
End Sub

End Class


Public Class ServiceClass
Inherits MarshalByRefObject

'クライアントから呼び出しを受け、イベントを発生させる
Public Sub RaiseServerEvent(ByVal message As String)
'イベントを発生させる
RaiseEvent RaiseClientEvent(message)
End Sub

Public Event RaiseClientEvent(ByVal messsage As String)

End Class

(クライアント側)
Imports System.Runtime.Remoting.Channels
Imports System.Runtime.Remoting.Channels.Ipc

Class MainWindow
Private Sub MainWindow_Loaded(sender As Object, e As RoutedEventArgs) Handles Me.Loaded

'IPCチャネルを用意する
Dim ipcChannel As New IpcClientChannel
ChannelServices.RegisterChannel(ipcChannel, False)

'メッセージを送信する

Try
'既存のリモートIPCオブジェクトへの参照を取得する
Dim strURL As String = "ipc://ipcPort/" + GetType(ServiceClass).Name
Dim objRemote As Object = Activator.GetObject(GetType(ServiceClass), strURL)

'サーバとの共通クラスの参照(ServiceClass)
Dim objServiceClass As ServiceClass = CType(objRemote, ServiceClass)
'共有参照クラスのイベント生成メソッド呼び出し
objServiceClass.RaiseServerEvent("Message From Client=>")

Catch ex As Exception
MsgBox(ex.Message)
End Try


End Sub



End Class



Public Class ServiceClass
Inherits MarshalByRefObject

'クライアントから呼び出しを受け、イベントを発生させる
Public Sub RaiseServerEvent(ByVal message As String)
'イベントを発生させる
RaiseEvent RaiseClientEvent(message)
End Sub

Public Event RaiseClientEvent(ByVal messsage As String)

End Class

4. 無題

ソースを拝見しました。
「Me.Invoke」のところでエラーがでるのは、「Public Class frmIpcServer」が単なるユーザクラスとして宣言されているためと思います。
「System.Windows.Forms.Form」の派生クラスとして宣言されたものであれば「Invoke」メソッドは存在するのでエラーは出ないはずです。

ブログの記事のクラス「Public Class frmIpcServer」は、最初にVS上でWindowsフォームを作成したうえで、
そこにソースを追加していますので、「System.Windows.Forms.Form」の派生クラスとなり「invoke」でエラーは出ません。
(私の方ではVS2012のProfessionalの環境です)

投稿されたソースではサーバ側のウインドウは「MainWindow」の様ですのでそちらに「frmIpcServer」の内容を全て移行すればできると思います。

5. 無題

御回答ありがとうございます。
Me.Invoke(RichTextBox1Delegate,~ は画面表示関連の処理なので実行しなくても構わないので、深く考えないことにしますが、
クライアント側の
'共有参照クラスのイベント生成メソッド呼び出し
objServiceClass.RaiseServerEvent("Message From Client=>")

この行で例外が発生します。

型 'frmIpcClient.ServiceClass, frmIpcClient, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null' を読み込めません。

↑この解決方法を教えて下さい。
よろしくお願い致します。

6. 無題

あと、
>このソリューションをデバッグ実行するために、ソリューションのプロパティを以下の図の様に設定します。
この下の画像で、マルチスタートアッププロジェクト の欄に3行が表示されていますが、私の開発環境ではFrmIpcClientの1行しか表示されません。これも例外発生の原因でしょうか。
3行を表示させるにはどうすれば良いのですか。

7. 無題

少し気になったのですが、クライアント側に「Imports IpcService」の宣言がないのですが、これを入れてみてはどうでしょうか?

8. 無題

PublicKeyToken=null' を読み込めません。の原因がDLLファイルの有無に関係があるらしいので、現在調査中です。
現在IpcServiceに当たるDLLは作っておりませんし、参照も不要だと思っています。サーバー、クライアント共通で Public Class ServiceClass ~ として宣言しているからです。



9. 無題

今回のエラーの件は、サーバー側、クライアント側で別々に「ServiceClass」を宣言していることにある様です。
クライアント側で送信したクラスは「IpcClient.exe」側のクラスであり、それをサーバー側で受信しても、自分自身の「ServiceClass」として処理する為です。

そのため、クライアント側の「ServiceClass」はそのままにして、サーバー側ではクライアント側の「ServiceClass」を参照する様にします。

・サーバー側の参照設定で、クライアント側の「IpcClient.exe」を設定します。
・サーバー側の「ServiceClass」の宣言を以下の様にします。
'IPC用クラスの生成
Private WithEvents IpcServiceClass As New IpcClient.ServiceClass

以上で行けると思うのですが、如何でしょうか?

10. 無題

ブログ主さんへ 動作しましたのでソースをつけます。

Me.Invoke(RichTextBox1Delegate, New Object() ~ の解決方法は、WPFとWindows Formの仕様の違いで全く想像が付かず、諦めました。
私がIPC通信を組み込むアプリケーションは、速度は重視しないのでサーバー側はディスパッチャータイマーでポーリングすることにしました。
「ServiceClass」共通クラスをDLLにせずにサーバーに置き、クライアントから参照する方法を採択させていただきました。

共通プロパティーのマルチスタートアッププロジェクトは、設定しなくても動作しました。

一日掛かりましたが、IPC通信の組み込みは何としても実現したかったので満足です。朝から何度も質問させえていただきましたが、有用なヒントをいただき、また親切な御対応にも感謝致します。非常に助かりました。
同じ技術の解説サイトとしては、最高に分かり易い内容でした。


VS2019 Community .Net4.7 WPF使用
(サーバー側)

Imports System.Runtime.Remoting
Imports System.Runtime.Remoting.Channels
Imports System.Runtime.Remoting.Channels.Ipc

Class MainWindow

Private Property Timer1 As New System.Windows.Threading.DispatcherTimer()

Public Property counter1 As Integer = 0
Private WithEvents IpcServiceClass As New ServiceClass


Private Sub MainWindow_Loaded(sender As Object, e As RoutedEventArgs) Handles Me.Loaded

Timer1.Interval = New TimeSpan(0, 0, 0, 0, 100) '100msに1回タイマーイベント発生
Timer1.Start()
AddHandler Timer1.tick, AddressOf dispatcherTimer_tick

'IPCチャンネルを用意
Dim IpcChannel As New IpcServerChannel("ipcPort")
ChannelServices.RegisterChannel(IpcChannel, False)
Dim strSClassName As String = GetType(ServiceClass).Name
RemotingConfiguration.RegisterWellKnownServiceType(GetType(ServiceClass), strSClassName, WellKnownObjectMode.SingleCall)
RemotingServices.Marshal(IpcServiceClass, strSClassName)

'IPC受信準備
IpcChannel.StartListening(Nothing)
Debug.Print("IPC channel is ready to receive." & vbNewLine)
End Sub

'IPC用クラスのイベント処理
Private Sub IpcServiceClass_RaiseClientEvent(ByVal message As String) Handles IpcServiceClass.RaiseClientEvent
counter1 += 1
Debug.Print("受信:" & message)
End Sub

Public Sub dispatcherTimer_tick()
If counter1 > 0 Then
Me.label1.Content = Now().ToLongTimeString
counter1 = 0
End If
End Sub


End Class

Public Class ServiceClass 'このクラスはクライアントから参照される
Inherits MarshalByRefObject

'クライアントから呼び出しを受け、イベントを発生させる
Public Sub RaiseServerEvent(ByVal message As String)
'イベントを発生させる
RaiseEvent RaiseClientEvent(message)
End Sub

Public Event RaiseClientEvent(ByVal message As String)

End Class

Imports System.Runtime.Remoting
Imports System.Runtime.Remoting.Channels
Imports System.Runtime.Remoting.Channels.Ipc

Class MainWindow

Private Property Timer1 As New System.Windows.Threading.DispatcherTimer()

Public Property counter1 As Integer = 0
Private WithEvents IpcServiceClass As New ServiceClass


Private Sub MainWindow_Loaded(sender As Object, e As RoutedEventArgs) Handles Me.Loaded

Timer1.Interval = New TimeSpan(0, 0, 0, 0, 100) '100msに1回タイマーイベント発生
Timer1.Start()
AddHandler Timer1.tick, AddressOf dispatcherTimer_tick

'IPCチャンネルを用意
Dim IpcChannel As New IpcServerChannel("ipcPort")
ChannelServices.RegisterChannel(IpcChannel, False)
Dim strSClassName As String = GetType(ServiceClass).Name
RemotingConfiguration.RegisterWellKnownServiceType(GetType(ServiceClass), strSClassName, WellKnownObjectMode.SingleCall)
RemotingServices.Marshal(IpcServiceClass, strSClassName)

'IPC受信準備
IpcChannel.StartListening(Nothing)
Debug.Print("IPC channel is ready to receive." & vbNewLine)
End Sub

'IPC用クラスのイベント処理
Private Sub IpcServiceClass_RaiseClientEvent(ByVal message As String) Handles IpcServiceClass.RaiseClientEvent
counter1 += 1
Debug.Print("受信:" & message)
End Sub

Public Sub dispatcherTimer_tick()
If counter1 > 0 Then
Me.label1.Content = Now().ToLongTimeString
counter1 = 0
End If
End Sub


End Class

Public Class ServiceClass 'このクラスはクライアントから参照される
Inherits MarshalByRefObject

'クライアントから呼び出しを受け、イベントを発生させる
Public Sub RaiseServerEvent(ByVal message As String)
'イベントを発生させる
RaiseEvent RaiseClientEvent(message)
End Sub

Public Event RaiseClientEvent(ByVal message As String)

End Class


(クライアント側)
Imports System.Runtime.Remoting.Channels
Imports System.Runtime.Remoting.Channels.Ipc
'プロジェクトの参照先としてfrmIpcServer(サーバー側プログラム)を設定しておく
Class MainWindow
Private Sub MainWindow_Loaded(sender As Object, e As RoutedEventArgs) Handles Me.Loaded

'IPCチャネルを用意する
Dim ipcChannel As New IpcClientChannel
ChannelServices.RegisterChannel(ipcChannel, False)

End Sub

Private Sub Button_Click(sender As Object, e As RoutedEventArgs)
'メッセージを送信する
Try
'既存のリモートIPCオブジェクトへの参照を取得する
Dim strURL As String = "ipc://ipcPort/" + GetType(frmIpcServer.ServiceClass).Name
Dim objRemote As frmIpcServer.ServiceClass = Activator.GetObject(GetType(frmIpcServer.ServiceClass), strURL)

objRemote.RaiseServerEvent("Message From Client=>" & Me.TB1.Text)

Catch ex As System.Runtime.Remoting.RemotingException
Debug.Print(ex.Message) 'サーバー側未起動の場合、ここで検出
Catch ex As Exception
MessageBox.Show(ex.Message)
End Try
End Sub

End Class


11. 無題

解決されたとのことで、何よりです。
また、何かありましたら宜しくお願い致します。
コメントを書く