.NETで名前付きパイプを試す(4) - 複数のクライアントに対応したサーバにする

もう少しNamedPipeClientStreamとNamedPipeServerStreamを試してみます。

前回のサーバは1つのクライアントの1つのリクエストを処理するだけでプログラムが終了しています。

ichiroku11.hatenablog.jp

これではさすがにサーバとは言えないと思うので、もう少しサーバっぽくしたいと思います。具体的には次のことに対応していきます。

  • 1つのサーバで複数のクライアントからのリクエストを処理できるようにする
  • 複数のリクエストを並列処理できるようにする

また前回までは「プロセス間」通信ということを確認したかったのでプロジェクトを分けましたが、今回は実行結果がわかりやすいように1つのプロジェクトにしてスレッド間で試しています。

複数のクライアントに対応したサーバ

まずはサーバのプログラムを終了しないようにして、複数のクライアントからのリクエストを処理できるようにします。

Serverメソッド内の処理全体をwhile文で囲んでいます。当然なのかもしれませんが、クライアントの NamedPipeClientStreamで接続が閉じられるとサーバのNamedPipeServerStreamも接続が閉じられるようで、NamedPipeServerStreamのインスタンスはwhile文の中で都度生成しています。ストリームのインスタンスは使い回すものじゃないのかなと。

serverIdは識別用です。後半でサーバを複数作るときに使います。

// サーバ
private static async Task Server(int serverId, Func<Message, Message> process) {
    while (true) {
        using (var stream = new NamedPipeServerStream("testpipe")) {
            // クライアントからの接続を待つ
            Console.WriteLine($"Server#{serverId} waiting");
            await stream.WaitForConnectionAsync();
            Console.WriteLine($"Server#{serverId} connected");

            // クライアントからリクエストを受信する
            var request = default(Message);
            using (var reader = new BinaryReader(stream, Encoding.UTF8, true)) {
                request = reader.ReadObject<Message>();
            }
            Console.WriteLine($"Server#{serverId} {nameof(request)}: {request}");

            // リクエストを処理してレスポンスを作る
            var response = process(request);

            // クライアントにレスポンスを送信する
            Console.WriteLine($"Server#{serverId} {nameof(response)}: {response}");
            using (var writer = new BinaryWriter(stream, Encoding.UTF8, true)) {
                writer.WriteObject(response);
            }
        }
    }
}

// サーバでの処理(Serverメソッドの引数に渡す)
// Contentの文字列を逆順にする処理
private static Message ServerProcess(Message message) {
    return new Message {
        Id = message.Id,
        Content = new string(message.Content.Reverse().ToArray()),
    };
}

複数のクライアントから接続

続いてクライアントです。clientIdは識別用です。

// クライアント
private static async Task<Message> Client(int clientId, Message request) {
    using (var stream = new NamedPipeClientStream("testpipe")) {
        // サーバに接続
        Console.WriteLine($"Client#{clientId} connecting");
        await stream.ConnectAsync();
        Console.WriteLine($"Client#{clientId} connected");

        // サーバにリクエストを送信する
        Console.WriteLine($"Client#{clientId} {nameof(request)}: {request}");
        using (var writer = new BinaryWriter(stream, Encoding.UTF8, true)) {
            writer.WriteObject(request);
        }

        // サーバからレスポンスを受信する
        var response = default(Message);
        using (var reader = new BinaryReader(stream, Encoding.UTF8, true)) {
            response = reader.ReadObject<Message>();
        }
        Console.WriteLine($"Client#{clientId} {nameof(response)}: {response}");

        return response;
    }
}

実行する

クライアントを2つとサーバを1つ動かしてみます。

サーバが1つなのでクライアントは同時に接続できていないことが確認できます。サーバではリクエストを1つずつ順に処理していることもわかります。

static void Main(string[] args) {
    Task.WaitAll(new[] {
        // クライアント
        Client(1, new Message { Id = 10, Content = "あいうえお", }),
        Client(2, new Message { Id = 20, Content = "かきくけこ", }),
        // サーバ
        Server(1, ServerProcess),
    });
}

// 実行結果(実行するごとに変わる部分もありますが)
/*
Client#1 connecting
Client#2 connecting
Server#1 waiting
Client#2 connected // Client#2が接続(Client#1はまだ接続待ち)
Server#1 connected
Client#2 request: { Id = 20, Content = "かきくけこ" }
Server#1 request: { Id = 20, Content = "かきくけこ" }
Server#1 response: { Id = 20, Content = "こけくきか" }
Client#2 response: { Id = 20, Content = "こけくきか" }
Server#1 waiting
Server#1 connected
Client#1 connected // ここでやっとClient#1が接続できる
Client#1 request: { Id = 10, Content = "あいうえお" }
Server#1 request: { Id = 10, Content = "あいうえお" }
Server#1 response: { Id = 10, Content = "おえういあ" }
Client#1 response: { Id = 10, Content = "おえういあ" }
Server#1 waiting
*/

複数のリクエストを並列に処理するサーバ

さらにサーバでは複数のリクエストを並列に処理できるようにしてみます。

NamedPipeServerStreamのコンストラクタにはサーバインスタンスの最大数を指定できるオーバーロードがあります。

このコンストラクタを使って複数のサーバインスタンスを用意できるように修正します。今回は最大数を2にしてみます。(ちなみに上記のサーバはサーバインスタンスの最大数は1でした。)

// サーバ
private static async Task Server(int serverId, Func<Message, Message> process) {
    while (true) {
        // サーバインスタンスを2に
        // using (var stream = new NamedPipeServerStream("testpipe")) {
        using (var stream = new NamedPipeServerStream("testpipe", PipeDirection.InOut, 2)) {
            // この部分は上記のServerメソッドと同じなので省略
        }
    }
}

実行する

クライアントとサーバを2つずつ動かしてみます。それぞれのサーバでリクエストが処理されていることを確認できます。

static void Main(string[] args) {
    Task.WaitAll(new[] {
        // クライアント
        Client(1, new Message { Id = 10, Content = "あいうえお", }),
        Client(2, new Message { Id = 20, Content = "かきくけこ", }),
        // サーバ
        Server(1, ServerProcess),
        Server(2, ServerProcess),
    });
}

// 実行結果(実行するごとに変わる部分もありますが)
/*
Client#1 connecting
Client#2 connecting
Server#1 waiting
Client#1 connected // Client#1が接続
Server#1 connected
Server#2 waiting
Server#2 connected
Client#2 connected // Client#2が接続
Client#2 request: { Id = 20, Content = "かきくけこ" }
Server#2 request: { Id = 20, Content = "かきくけこ" }
Server#2 response: { Id = 20, Content = "こけくきか" }
Client#2 response: { Id = 20, Content = "こけくきか" }
Server#2 waiting
Client#1 request: { Id = 10, Content = "あいうえお" }
Server#1 request: { Id = 10, Content = "あいうえお" }
Server#1 response: { Id = 10, Content = "おえういあ" }
Client#1 response: { Id = 10, Content = "おえういあ" }
Server#1 waiting
*/

これでNamedPipeClientStreamとNamedPipeServerStreamの基本的な使い方は把握できたかなーと思います。

ソース一式はこちらに。

.NETで名前付きパイプを試す · GitHub

おしまい。

.NETで名前付きパイプを試す(3) - クライアントサーバ間で送受信する

前回に続いてもう少しNamedPipeClientStreamとNamedPipeServerStreamを使った名前付きパイプを試します。

ichiroku11.hatenablog.jp

今回はクライアント側のプログラムからサーバ側のプログラムにリクエストを送ってレスポンスを受け取るようにしてみます。クライアント側、サーバ側それぞれのプログラムの動きは次のようにしたいと思います。

クライアント側のプログラムの動き

  1. サーバに接続
  2. サーバにリクエストを送信する
  3. サーバからレスポンスを受信する

サーバ側のプログラムの動き

  1. クライアントからの接続を待つ
  2. クライアントからリクエストを受信する
  3. リクエストを処理してレスポンスを作る
  4. クライアントにレスポンスを送信する

これでちょっとはクライアントサーバっぽくなってくるかなと。

クライアント側のプログラム

まずはクライアント側からです。ReadObjectとWriteObject、Messageは前回のエントリで使ったものと同じです。またBinaryWriter、BinaryReaderのusingの部分でパイプのストリームを閉じないようにしています。

// クライアント側
class Program {
    private static async Task<Message> Client(Message request) {
        using (var stream = new NamedPipeClientStream("testpipe")) {
            // 1. サーバに接続
            await stream.ConnectAsync();

            // 2. サーバにリクエストを送信する
            Console.WriteLine($"{nameof(request)}: {request}");
            using (var writer = new BinaryWriter(stream, Encoding.UTF8, true)) {
                writer.WriteObject(request);
            }

            // 3. サーバからレスポンスを受信する
            var response = default(Message);
            using(var reader = new BinaryReader(stream, Encoding.UTF8, true)) {
                response = reader.ReadObject<Message>();
            }
            Console.WriteLine($"{nameof(response)}: {response}");

            return response;
        }
    }

    static void Main(string[] args) {
        Console.WriteLine("Client");

        Client(new Message {
            Id = 1,
            Content = "あいうえお",
        }).Wait();
    }
}

サーバ側のプログラム

続いてサーバ側です。リクエストからレスポンスを作る部分は、受け取ったリクエストの文字列をそのまま返すのも寂しいので文字列を逆順にしてレスポンスを作っています。

// サーバ側
class Program {
    private static async Task Server(Func<Message, Message> process) {
        using (var stream = new NamedPipeServerStream("testpipe")) {
            // 1. クライアントからの接続を待つ
            await stream.WaitForConnectionAsync();

            // 2. クライアントからリクエストを受信する
            var request = default(Message);
            using(var reader = new BinaryReader(stream, Encoding.UTF8, true)) {
                request = reader.ReadObject<Message>();
            }
            Console.WriteLine($"{nameof(request)}: {request}");

            // 3. リクエストを処理してレスポンスを作る
            var response = process(request);

            // 4. クライアントにレスポンスを送信する
            Console.WriteLine($"{nameof(response)}: {response}");
            using (var writer = new BinaryWriter(stream, Encoding.UTF8, true)) {
                writer.WriteObject(response);
            }
        }
    }

    // Contentの文字列を逆順にする処理
    private static Message Process(Message message) {
        return new Message {
            Id = message.Id,
            Content = new string(message.Content.Reverse().ToArray()),
        };
    }

    static void Main(string[] args) {
        Console.WriteLine("Server");

        Server(Process).Wait();
    }
}

実行する

2つのプログラムを実行すると送受信できることを確認できます。

// クライアント側の実行結果
Client
request: { Id = 1, Content = "あいうえお" }
response: { Id = 1, Content = "おえういあ" }

// サーバ側の実行結果
Server
request: { Id = 1, Content = "あいうえお" }
response: { Id = 1, Content = "おえういあ" }

今回も目的をクリアできたのでこのへんで。

.NETで名前付きパイプを試す(2) - クライアントからサーバにオブジェクトを送る

前回はクライアントからサーバに文字列を送ってみましたが、文字列だけでは少し寂しいので今回はオブジェクトを送信したいと思います。それでもまだ寂しいけど。

ichiroku11.hatenablog.jp

下準備

目的をクリアするために必要なクラスを準備していきます。

オブジェクトはバイト配列で送信することにして、まずはオブジェクトとバイト配列を変換するクラスを用意します。といってもヘルパー的なメソッドを持っている程度です。

// オブジェクトとバイト配列の変換
public class ObjectConverter<TObject> {
    private readonly IFormatter _formatter;

    public ObjectConverter(IFormatter formatter = null) {
        _formatter = formatter ?? new BinaryFormatter();
    }

    // オブジェクト=>バイト配列
    public byte[] ToByteArray(TObject obj) {
        using (var stream = new MemoryStream()) {
            _formatter.Serialize(stream, obj);
            return stream.ToArray();
        }
    }

    // バイト配列=>オブジェクト
    public TObject FromByteArray(byte[] bytes) {
        using (var stream = new MemoryStream(bytes)) {
            return (TObject)_formatter.Deserialize(stream);
        }
    }
}

バイト配列をストリームに読み書きするためにBinaryWriterとBinaryReaderを使いますが、ちょっとした拡張メソッドも用意しておきます。以下の方法がベストなのかわかりませんが、バイト配列の長さとバイト配列の順で書き込み、バイト配列の長さとバイト配列の順で読み込むようにしています。

// BinaryWriterの拡張メソッドを定義
public static class BinaryWriterExtensions {
    // オブジェクトの書き込み
    public static void WriteObject<TObject>(this BinaryWriter writer, TObject obj) {
        // オブジェクトをバイト配列に変換
        var converter = new ObjectConverter<TObject>();
        var bytes = converter.ToByteArray(obj);

        // 長さを書き込んでからバイト配列を書き込む
        writer.Write(bytes.Length);
        writer.Write(bytes);
    }
}

// BinaryReaderの拡張メソッドを定義
public static class BinaryReaderExtensions {
    // オブジェクトの読み込み
    public static TObject ReadObject<TObject>(this BinaryReader reader) {
        // 長さを読み込んでから、その長さ分のバイト配列を読み込む
        var length = reader.ReadInt32();
        var bytes = reader.ReadBytes(length);

        // バイト配列をオブジェクトに変換
        var converter = new ObjectConverter<TObject>();
        return converter.FromByteArray(bytes);
    }
}

送信する適当なオブジェクトを用意しておきます。結局は文字列という・・・サンプル力が弱いなと。

// メッセージ
[Serializable]
public class Message {
    public int Id { get; set; }
    public string Content { get; set; }

    public override string ToString() {
        return $@"{{ {nameof(Id)} = {Id}, {nameof(Content)} = ""{Content}"" }}";
    }
}

クライアント側

クライアントです。前回のサンプルで内側のusingの部分でStreamを閉じてしまっていることに気づきました。今回は閉じないようにしています。

// クライアント側
class Program {
    private static async Task Client() {
        using (var stream = new NamedPipeClientStream("testpipe")) {
            // サーバに接続
            await stream.ConnectAsync();

            // オブジェクトを送信(書き込み)
            // このusingでStreamを閉じないようにしておく
            using (var writer = new BinaryWriter(stream, Encoding.UTF8, true)) {
                Console.WriteLine($"Send");
                var message = new Message {
                    Id = 16,
                    Content = "あいうえお",
                };
                writer.WriteObject(message);
                Console.WriteLine($"Sent: {message}");
            }
        }
    }

    static void Main(string[] args) {
        Console.WriteLine("Client");

        Client().Wait();
    }
}

// 実行結果
/*
Client
Send
Sent: { Id = 16, Content = "あいうえお" }
*/

サーバ側

サーバです。

// サーバ側
class Program {
    private static async Task Server() {
        using (var stream = new NamedPipeServerStream("testpipe")) {
            // クライアントからの接続を待つ
            await stream.WaitForConnectionAsync();

            // オブジェクトを受信(読み込み)
            using (var reader = new BinaryReader(stream, Encoding.UTF8, true)) {
                Console.WriteLine("Receive");
                var message = reader.ReadObject<Message>();
                Console.WriteLine($"Received: {message}");
            }
        }
    }

    static void Main(string[] args) {
        Console.WriteLine("Server");

        Server().Wait();
    }
}

// 実行結果
/*
Server
Receive
Received: { Id = 16, Content = "あいうえお" }
*/

2つとも実行するとクライアント側からサーバ側にオブジェクトを送信できていることが確認できます。

名前付きパイプというよりオブジェクトのストリーム読み書きのサンプルみたいになった気もしますが、今回はこんなところで。

.NETで名前付きパイプを試す(1) - クライアントからサーバにメッセージを送る

.NET Frameworkを使って名前付きパイプでプロセス間通信を実装する方法が気になったので調べています。このエントリはその勉強の記録です。

名前付きパイプを実装するには、NamedPipeServerStreamとNamedPaipeClientStreamを使います。

クラス名にもありますが、名前付きパイプはクライアントサーバ型の通信です。通信するといってもあまり特別なことはなくてFileStreamやMemoryStreamと同じように扱えばOKのようです。Streamに書き込む(=送信)、Streamから読み込む(=受信)といった操作で通信できるようです。

ということでまずはすごく単純なクライアント側のプログラムとサーバ側のプログラムを作ってみます。それぞれの動きは次のようにします。

クライアント側のプログラムの動き

  • サーバに接続
  • メッセージ(文字列)を送信する

サーバ側のプログラムの動き

  • クライアントからの接続を待つ
  • メッセージ(文字列)を受信する

実用にはほど遠いですが一旦これを目指します。文字列以外も送る、送受信するなどやりたいことはたくさんありますが、最初は高望みしないことにします。

クライアント側のプログラム

まずはクライアント側。サーバに接続してメッセージを送信します。

// クライアント側
class Program {
    private static async Task Client() {
        // ローカルの"testpipe"という名前のパイプのクライアント
        using (var stream = new NamedPipeClientStream("testpipe")) {
            // サーバに接続
            await stream.ConnectAsync();

            // メッセージを送信(書き込み)
            using (var writer = new StreamWriter(stream)) {
                Console.WriteLine($"Send");
                var message = "あいうえお";
                await writer.WriteLineAsync(message);
                Console.WriteLine($"Sent: {message}");
            }
        }
    }

    static void Main(string[] args) {
        Console.WriteLine("Client");

        Client().Wait();
    }
}

// 実行結果
/*
Client
Send
Sent: あいうえお
*/

コメントとして実行結果ものせていますがサーバ側も動かした場合の実行結果です。

また試していませんが、NamedPipeClientStreamを作るときにサーバ名を指定できるようです。指定しない場合はローカル上のパイプになります。

サーバ側のプログラム

次にサーバ。クライアントからの接続を待ってメッセージを受信します。

// サーバ側
class Program {
    private static async Task Server() {
        // "testpipe"という名前のパイプのサーバ
        using (var stream = new NamedPipeServerStream("testpipe")) {
            // クライアントからの接続を待つ
            await stream.WaitForConnectionAsync();

            // メッセージを受信(読み込み)
            using (var reader = new StreamReader(stream)) {
                Console.WriteLine("Receive");
                var message = await reader.ReadLineAsync();
                Console.WriteLine($"Received: {message}");
            }
        }
    }

    static void Main(string[] args) {
        Console.WriteLine("Server");

        Server().Wait();
    }
}

// 実行結果
/*
Server
Receive
Received: あいうえお
*/

この2つのプログラムを実行するとクライアントからサーバにメッセージを送信できることが確認できます。

とりあえず送信できたので今回はこのあたりで。

ちなみにソリューションにプロジェクトを2つ入れて同時にデバッグ実行すると楽かもです。

ichiroku11.hatenablog.jp

参考

Visual Studioでソリューション内の複数のプロジェクトをデバッグ実行する

実行可能なプロジェクトが複数あるソリューションで、複数のプロジェクトをデバッグ実行する方法です。

今日知りました。

ソリューションのプロパティからスタートアッププロジェクトで「マルチスタートアッププロジェクト」を選びます。そして実行したいプロジェクトのアクションを「開始」にします。

詳しくはこのあたりに。

さすがVisual Studio。知らなかったー。