.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。知らなかったー。

T-SQL - DROP IF EXISTS

先週SQL Server 2016の提供が開始されました。

新機能を調べだしたところですが、まずはとりあえずDROP IF EXISTSを試してみました。

開発中、試行錯誤してたりとかでテーブル構造がなかなか固まらず、テーブルを作っては消してまた作るということを繰り返すことがありました。 私はGUIでテーブルを作らずクエリで作る方で、さらにGUIでテーブルを消すこともめんどくさくて、次のようなクエリをよく書いていました。

-- テーブルが存在していればテーブルを消す
if object_id(N't') is not null
    drop table t;

-- テーブルを作る
create table t(c int);

このテーブルが存在していれば消すということが、 SQL Server 2016からはすっきり書けるようになりました。それがDROP IF EXISTS。

-- テーブルが存在していればテーブルを消す
drop table if exists t;

ぶっちゃけ地味ですがわかりやすいですね。DROP文全般で使えるようですし、ALTER TABLEでカラムを削除するときも使えるみたいです。

参考

次はJSON関係を試してみようかなーと思います。

FlagsAttributeを指定したenumの文字列

ものすごく小ネタですが、FlagsAttributeを指定したenumの文字列表現はどうなるんだろ?とふと気になって試してみました。

// トッピング
[Flags]
public enum Toppings {
    None = 0x00,   // なし
    Nitamago = 0x01,   // 煮卵
    CharSiu = 0x02,    // チャーシュー
    Menma = 0x04,  // メンマ
    Moyashi = 0x08,    // もやし
}

出力してみます。フラグを組み合わせた場合はカンマ区切りの文字列となるようです。文字列の並びはenumの値順ですかね。

Console.WriteLine(Toppings.CharSiu);
// CharSiu

Console.WriteLine(Toppings.CharSiu | Toppings.Menma);
// CharSiu, Menma

Console.WriteLine(Toppings.CharSiu | Toppings.Nitamago | Toppings.Menma);
// Nitamago, CharSiu, Menma

Console.WriteLine(Toppings.None);
// None

もちろんカンマ区切りの文字列をパースできると。

var toppings = default(Toppings);
if (Enum.TryParse<Toppings>("CharSiu, Moyashi, Nitamago", out toppings)) {
    Console.WriteLine(toppings);
    // Nitamago, CharSiu, Moyashi
}

どうでもいいことですが、あぶりメンマが好きです。