yotiky Tech Blog

とあるエンジニアの備忘録

.NET - MemoryMappedFile を使ったプロセス間通信 (MessagePack for C# 編)

前回の記事MemoryMappedFile を使ったプロセス間通信の基本的な使い方を紹介しました。今回は MessagePack for C# を使ってシリアライズしたデータのやり取りについてです。

MessagePack for C# の使い方についてはこちらの記事も参考にしてみてください。

yotiky.hatenablog.com

目次

実装例

今回のサンプルは送信側も受信側もConsoleアプリのみです。 Enterキーを1回打つと、User データが書き込まれます。2回目は IncrementAge メソッドを呼ぶための Command が書き込まれます。

f:id:yotiky:20200806162528g:plain

先に送受信するデータ型を定義します。MessagePack for C#Union を使っています。Union を使うと interface でシリアライズができるため、共通項目をもたせたり、inteface で一次変換を挟めるので型の判別が楽になります。

[Union(0, typeof(User))]
[Union(1, typeof(Command))]
public interface IDataProtocol
{
    [Key(0)]
    int Id { get; set; }
}

[MessagePackObject]
public class User : IDataProtocol
{
    [Key(0)]
    public int Id { get; set; }
    [Key(1)]
    public int UserId { get; set; }
    [Key(2)]
    public string Name { get; set; }
    [Key(3)]
    public int Age { get; set; }

    public void IncrementAge()
    {
        Age++;
    }
}
[MessagePackObject]
public class Command : IDataProtocol
{
    [Key(0)]
    public int Id { get; set; }
    [Key(1)]
    public string Name { get; set; }
    [Key(2)]
    public int UserId { get; set; }
}

User は単にデータです。メソッドが呼び出せないので Command を定義してName を使って呼び分けるような簡単の仕組みを実装します。

IDataProtocol ではシーケンシャルなIDをイメージした Id プロパティを持たせています。受信側はポーリングしてメモリを読み込むので書き込まれた内容が更新されているかどうかわからないため、この Id を更新材料に使っています。

続いて、送信側の Console アプリのサンプルコードです。

static void Main(string[] args)
{
    using (var sharedMemory = MemoryMappedFile.CreateNew("SharedMemory", 1024))
    {
        {
            Console.WriteLine("Please press Enter.");
            Console.ReadLine();

            var data = new User
            {
                Id = 1,
                UserId = 1,
                Name = "Shion",
                Age = 17,
            };
            var serialized = MessagePackSerializer.Serialize<IDataProtocol>(data);
            using (var accessor = sharedMemory.CreateViewAccessor())
            {
                accessor.Write(0, serialized.Length);
                accessor.WriteArray(sizeof(int), serialized, 0, serialized.Length);
            }
        }
        {
            Console.WriteLine("Please press Enter.");
            Console.ReadLine();

            var data = new Command
            {
                Id = 2,
                Name = "IncrementAge",
                UserId = 1,
            };
            var serialized = MessagePackSerializer.Serialize<IDataProtocol>(data);
            using (var accessor = sharedMemory.CreateViewAccessor())
            {
                accessor.Write(0, serialized.Length);
                accessor.WriteArray(sizeof(int), serialized, 0, serialized.Length);
            }
        }
    }
    Console.ReadLine();
}

それぞれのデータは IDataProtocol としてシリアライズします。メモリの頭にデータの長さを書き込んでいます。一見 MemoryMappedViewStream が使えそうですが、デシリアライズしてみないと中身がわからないため、空の状態(初期状態)ではデシリアライズでコケてしまいます。データの長さが入っていればシリアライズされたデータが入っていると判断できます。

今度は、受信側の Console アプリです。

static void Main(string[] args)
{
    User user = null;
    using (var sharedMemory = MemoryMappedFile.OpenExisting("SharedMemory"))
    {
        var state = 0;
        while (state < 2)
        {
            using (var accessor = sharedMemory.CreateViewAccessor())
            {
                var size = accessor.ReadInt32(0);
                if (0 < size)
                {
                    var data = new byte[size];
                    accessor.ReadArray<byte>(sizeof(int), data, 0, data.Length);
                    var deserialized = MessagePackSerializer.Deserialize<IDataProtocol>(data);
                    switch (deserialized)
                    {
                        case User x:
                            if (state < x.Id)
                            {
                                Console.WriteLine(MessagePackSerializer.SerializeToJson(x));
                                user = x;
                                state = x.Id;
                            }
                            break;
                        case Command x:
                            if (state < x.Id)
                            {
                                Console.WriteLine(MessagePackSerializer.SerializeToJson(x));
                                if (x.Name == "IncrementAge")
                                    if (user?.UserId == x.UserId)
                                        user.IncrementAge();
                                Console.WriteLine(MessagePackSerializer.SerializeToJson(user));
                                state = x.Id;
                            }
                            break;
                        default:
                            break;
                    }
                }
            }

            Thread.Sleep(100);
        }
    }
}

最初のデータでデータのサイズを取得しています。サイズ0より大きければ IDataProtocol でデシリアライズです。switch で型の判定と代入ができるようになったのは便利ですね。 Command が送られてきた場合は対応する処理を呼び出しています。

先に触れたように、100ms おきにポーリングしているだけなので、同じ値を何度も読み込んでしまいます。IDataProtocolId の値を保持しステートとして利用しています。

サンプルプロジェクト

ソースコード一式は以下においてあります。 ConsoleApp1とConsoleApp2に間借りして、データ型はConsoleApp1に定義してConsoleApp2からはプロジェクト参照しています。

github.com