Coreで簡易サーバーアプリを作って複数の機器を連携させる

# はじめに

設置する機器が複数ある場合など、システムがある程度の規模になった場合、複数のアプリケーションを連携させたくなることがあります。
＜例＞
・画像をスキャンするアプリケーション
・送信された画像を演出に用いるアプリケーション

同じPCだけでなく、違うPCで連携させたいことがあります。
＜例＞
・画像撮影用PC
・演出用PC

シンプルなやり方をとりたいので、
.Net Coreでサーバーアプリケーションを作り、通信はhttp(あるいはhttps)を使い、アプリケーション間で連携することにします。




# 必要なもの
開発環境・実行環境用 共にWindows PCを使用し、
サーバー側PCの実行環境にIIS(Internet Information Services)を使用します。
クライアント機器は、http通信が出来るならばなんでも利用出来そうです。

※多数（具体的には20台より多く）の機器から接続する場合、
サーバーにデスクトップ用のWindowsを使用するとライセンス違反となる場合があるので、その場合はLinux等のOS上に実行環境を構築します。


## サーバー側PC
サーバーアプリケーションを配置します。
.Net Coreのランタイム等が必要になります。
https://dotnet.microsoft.com/download/dotnet-core
・開発用に .NET Core SDK
・実行環境用にASP.NET Core Hosting Bundle
をダウンロードし、実行用にIISをインストールします。

## クライアント側PC
クライアント側のアプリケーションに、サーバーアプリケーションと通信する機能を実装します。
サーバーアプリを配置しているPCと同じPCでも構いません。



# サーバーアプリの作成
Visual Studio上で新しいプロジェクトを作成し、
プロジェクトテンプレートに「ASP .Net Core アプリケーション(C#)」を、
追加のプロジェクトテンプレートに「API」を選択してみます。



以下のクラスを追加します。
<details><summary>SampleItem.cs</summary><div>

```cs
    public class SampleItem
    {
        /// <summary>
        /// ID(連番)
        /// </summary>
        [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
        [Key]
        public uint ID { get; set; }

        /// <summary>
        /// 名前
        /// </summary>
        [MaxLength(128)]
        public string Name { get; set; }

	（略）
    }
```
</div>
</details>

<details><summary>SampleDbContext.cs</summary><div>

    public class SampleDbContext: DbContext
    {
        public DbSet<SampleItem> Samples { get; set; }

        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
            optionsBuilder.UseSqlite("Filename=ImageDatabase.sqlite");
            base.OnConfiguring(optionsBuilder);
        }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            modelBuilder.Entity<SampleItem>().ToTable("SampleItem", "test");
            base.OnModelCreating(modelBuilder);
        }
</div>
</details>

また、Startupクラスを以下のように編集します。
<details><summary>Startup.cs</summary><div>

```cs
    public class Startup
    {
        /// <summary>
        /// appsettings.jsonなどから読み込んだこのアプリの設定情報
        /// </summary>
        public IConfiguration Configuration { get; }

        public Startup(IConfiguration configuration)
        {
            // appsettings.jsonなどから読み込んだ設定
            Configuration = configuration;
        }

        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {
            services.Configure<AppSetting>(Configuration);

            // DBコンテキストの作成
            services.AddEntityFrameworkSqlite().AddDbContext<SampleDbContext>();

            // DBのテーブルが無ければ作る。既にある場合はそのまま
            using (var client = new SampleDbContext())
            {
                client.Database.EnsureCreated();
            }
            services.AddControllersWithViews();
        }

        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            （※省略）
        }
    }
```
</div>
</details>


# 機能の実装
投稿した画像を保存する機能、取得する機能を実装します。

<details><summary>APIController.cs</summary><div>

```cs
        [Serializable]
        [DataContract]
        public class ScanImageGetInfo
        {
            [DataMember(Name = "id")]
            public uint ID { get; set; }
            （略）
        }

        [HttpPost]
        public IActionResult PostImage(IFormFile Targetfile, int MachineID, int ImageCodeID)
        {
            if (Targetfile == null)
            {
                return new BadRequestResult(); // HTTPステータスコード 400
            }
            
            string savePath = SaveFileToWorkFolder(Targetfile, MachineID, ImageCodeID);

            //ファイルをダウンロード
            return new EmptyResult(); // HTTPステータスコード 200
        }

        /// <summary>
        /// 指定したIDの画像データを取得
        /// </summary>
        /// <param name="Id">PopImageで取得したレコードのID</param>
        /// <returns></returns>
        public IActionResult GetImage(int ID)
        {
            using (var context = new SampleDbContext())
            {
                var item = context.ScanImages.FirstOrDefault(x => x.ID == ID);
                if (item == null) return new BadRequestResult();
                filePath = folder + "\\" + item.FilePath;
                fileName = item.Name;
            }

            return File(GetStream(filePath), "image/png", Path.GetFileName(fileName));
        }

        // ファイルをこのPC上に保存する関数
        string SaveFileToWorkFolder(){
                （略）
        }
        
        // FileStreamを全て読み込み、MemoryStreamとして返す
        private Stream GetStream(String filepath)
        {
            var memory = new MemoryStream();
            using (var stream = new FileStream(filepath, FileMode.Open))
            {
                stream.CopyTo(memory);
            }
            memory.Position = 0;
            return memory;
        }
```
</div>
</details>


これで/api/postimageにアクセスするとファイルの保存、/api/getimageにアクセスするとファイルの取得が出来るようになりました。

# ファイルの投稿（クライアントスキャナアプリ側）
.Net Frameworkで作成したアプリケーションですが、
以下のように引数を与えてHttpのPostを送信しています。

<details><summary>画像の投稿を行うHttpPostImage()関数</summary><div>

```cs
        async public Task<string> HttpPostImage(string imageFilePath, int imageCodeID)
        {
            //File.WriteAllText(System.Environment.GetFolderPath(Environment.SpecialFolder.DesktopDirectory) + "\\log2.txt",
            //    "postimage" + "\n");

            // 送信を行います。
            // POSTする情報
            //WWWForm form = new WWWForm();
            //form.AddField("userId", userId);

            string APIURL = "http://"
                + Properties.Settings.Default.ServerURL
                + ":"
                + Properties.Settings.Default.ServerPort
                + "/API/PostImage";

            using (var stream = File.OpenRead(imageFilePath))
            using (var fileContent = new StreamContent(stream))
            using (var content = new MultipartFormDataContent())
            using (var client = new HttpClient())
            {
                fileContent.Headers.ContentDisposition = new System.Net.Http.Headers.ContentDispositionHeaderValue("form-data")
                {
                    Name = "Targetfile",
                    FileName = Path.GetFileName(imageFilePath),
                };
                fileContent.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("application/octet-stream");

                content.Add(fileContent);
                content.Add(new StringContent(Properties.Settings.Default.MachineID.ToString()), "MachineID");
                content.Add(new StringContent(imageCodeID.ToString()), "ImageCodeID");

                using (var response = await client.PostAsync(APIURL, content))
                {
                    return await response.Content.ReadAsStringAsync();
                }
            }
        }
```
</div>
</details>

# ファイルの取得（クライアント演出アプリ側）
Unityのアプリケーションですが、以下のようにしてGetを送信し、テクスチャ画像を取得することが出来ます。

<details><summary>画像の取得を行うコルーチン関数</summary><div>

```cs
        IEnumerator AccessCoroutine()
        {
            // Request
            string getImageUri = "http://" + _serverURL + ":" + _serverPort + ServerGetImageAPI;

            // 処理終わるまでループ
            while (isRunning)
            {
                
                      （略 idに取得したい画像のidを入れる）
                        string queryString = "?id=" + id;
                        // Texture2Dを作成
                        using (UnityWebRequest webGetImageRequest = UnityWebRequestTexture.GetTexture(getImageUri + queryString, true))
                        {
                            yield return webGetImageRequest.SendWebRequest();
                            if (webGetImageRequest.isNetworkError || webGetImageRequest.isHttpError)
                            {
                                Debug.Log(webGetImageRequest.error);
                            }
                            else
                            {
                                // Get downloaded asset bundle
                                Texture2D texture = DownloadHandlerTexture.GetContent(webGetImageRequest);
                                (略 テクスチャー画像の処理)
                            }
                        }

                    }
                }
                yield return new WaitForSeconds(1.0f);
            }
        }

```
</div>
</details>

これらのプログラムを組み合わせて、PC間で連携したひとつのシステムを作ることが出来ます。