前回の記事で MemoryMappedFile
を使ったプロセス間通信の基本的な使い方を紹介しました。今回は MessagePack for C# を使ってシリアライズしたデータのやり取りについてです。
MessagePack for C# の使い方についてはこちらの記事も参考にしてみてください。
目次
実装例
今回のサンプルは送信側も受信側もConsoleアプリのみです。
Enterキーを1回打つと、User
データが書き込まれます。2回目は IncrementAge
メソッドを呼ぶための Command
が書き込まれます。
先に送受信するデータ型を定義します。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 おきにポーリングしているだけなので、同じ値を何度も読み込んでしまいます。IDataProtocol
の Id
の値を保持しステートとして利用しています。
サンプルプロジェクト
ソースコード一式は以下においてあります。 ConsoleApp1とConsoleApp2に間借りして、データ型はConsoleApp1に定義してConsoleApp2からはプロジェクト参照しています。