塩水の濃度をずぼらに求める計算テクニック

はじめに

重量パーセント濃度が低い溶液を作りたい場合は重量パーセント濃度を溶媒と溶質の比としていいという話です。 計算間違ってる気がするのでコメントなりで気づいた人は指摘してください。

料理ではn %の食塩水等の溶液をつくるシーンがしばしば登場します。しかし、重量パーセント濃度は溶液と溶質の重量比として定義されておりいささか不便です。もしこれが重量パーセント濃度を溶媒と溶質の比としていいならば話は簡単になります。これは許されるのでしょうか?今回はこれについて検証します。

随分まえに書こう書こうとおもって放置していた記事です。 下書きにあったので供養します。

証明

溶質の質量をa,溶媒の質量をbとします。 このとき,重量パーセント濃度 100n は定義から

 n = \frac{a}{a+b}

と表せます。これをa/bについて解けば

 \frac{a}{b} = \frac{n}{1-n}

となります。右辺をnについてマクローリン展開すると

 \frac{a}{b} = 0+\frac{2n}{2 \cdot 1}+O(n^{2})

n<< 1の時,2次以上の項を無視して

 \frac{a}{b} =n

を得ます。

実用性について

溶質の質量を計測する場合誤差 \delta aが生じます。両方の方法で求めたaの値の差 \Delta aが誤差以下なら十分実用的と考えられます(溶媒を測る際の誤差は今回は無視します)。あるbのときにどういったn の値なら許されるかを考えてみましょう。

 \Delta aは下記のようにかけます。

\Delta a= \frac{n^{2}}{1-n}  b

したがって条件式は下記となります。

 \frac{n^{2}}{1-n}  b \leq  \delta a

これを 0 \leq nという条件に注意して解の公式をつかって解けば

 0 \leq n \leq \frac{-c+\sqrt{{\delta a}^2+4b \delta a}}{2b}

という条件式を得ます。ためしに  1.0 \times 10^{3} gの水(つまり1 Lの水)を使うことを考えてみましょう。また家庭用の料理スケールの誤差は2.0 g程度らしいです。そうするとnの範囲は

 0.0 \leq n \leq 4.3 \times 10^{-2}

と 4.3 %程度の値になります。よくつくる海水の塩分濃度が3.4 %程度ですから、まあまあ実用に耐えると考えることができます。

まとめ

毎回、面倒だなと思っていた溶液の濃度計算も海水の濃度程度ぐらいなら実用的に溶媒と溶質の比としていいという話でした。日常生活でつかうことがないと思われていたマクローリン展開や解の公式なんかも案外役に立つものですね。

CCS制作発表会オンライン〜冷蔵庫の中身等を管理するWebAppの途中経過〜

はじめに

巷ではCOVID-19で外出"自粛"がなされていますね。サークル活動ももちろん影響を受けています。そのため千葉大学電子計算機研究会(以下CCS)のオンライン制作発表会の中間発表が行われる下りになった模様(企画してくれたゆんらべくんに感謝)。ということで作りかけですが、作品の説明を出そうと思います。

さてCCSではWeb系のことをやってる人がほとんどいないです。もっとみんなやってほしい。ということでWebアプリを作って布教しようとおもいなにか作ってみることにしました。

時間がなかったので中間発表ということで今回はフロントエンド(ブラウザで表示される側)を実装していきたいと思います。フロントエンドの言語としては色々ありますが、今回はElmを採用しました。Elmは公式サイト(https://elm-lang.org)では「A delightful language for reliable webapps.(堅牢なウェブアプリケーションのための楽しい言語)」と書かれています。書いてみるとほんとにそのとおりでまったくストレスがありません。特徴としてはできるかぎり言語をシンプルに保とうとしていること、そのために関数型言語のアプローチを採用していることがあげられます。そして、エラーメッセージが非常に丁寧です。そして多くのフロントエンドの言語と同様にJavaScriptに最終的にはコンパイルされます。

なにを作るかということですが、日常的に困ってることを解決できるものができればいいなと思い考え始めました。困っていることといえば買い物と冷蔵庫の中身の管理。買い物は下記の図のように延々とTwitterの@のうしろが増え続けます(しかもかいもののときみわすれて残り続ける)。

f:id:ikaro1192:20200420013522p:plain

そして冷蔵庫の中身の管理はおなじものを何個も買ってしまったり、逆にあるとおもってなかったり...これらが確認できるアプリがあれば便利ですね。ということで冷蔵庫管理アプリを作っていきましょう!

実装とか

ソースコードは下記で公開しています。src/Main.elmがelmの本体です。

github.com

動作動画は下記です。

動画投稿してから削除機能がないことに気づいたけどもういいや...こんな感じで動的なWebアプリケーションをつくることが可能です。制作時間は6時間ぐらい?

ソースコードを解説していこうと思ったのですが、そんなことは色んな人がたくさん記事をかかれているのでElmつかってここまでで思ったことをかいていきます。わたしの解説を読むより公式ドキュメントを読んだほうが114514倍勉強になると思います。 guide.elm-lang.org

英語が苦手な方は有志が日本語翻訳してくれています。

guide.elm-lang.jp

まずはエラーメッセージが丁寧であるということです。たとえば selectedRefrigeratorNumをselected_refrigerator_numしてしまった場合下記のようなエラーメッセージが表示されました。

-- TYPE MISMATCH -------------------------------------------------- src/Main.elm

The `model` record does not have a `selected_refrigerator_num` field:

77|             { model | selected_refrigerator_num = String.toInt selectedRefrigerator |> Maybe.withDefault 0 }
                          ^^^^^^^^^^^^^^^^^^^^^^^^^
This is usually a typo. Here are the `model` fields that are most similar:

    { selectedRefrigeratorNum : Int
    , refrigerators : List Refrigerator
    , userName : String
    }

So maybe selected_refrigerator_num should be selectedRefrigeratorNum?

約すと「modelはselected_refrigerator_numをもってないよ。これは普通typoであることがおおい。にてるフィールドは下記の通り。だからもしかしてselected_refrigerator_numはselectedRefrigeratorNumじゃないかな?」というような感じですね。よむだけでtypoだとわかるだけでなくどうすればいいかまで教えてくれてありがたいですね。  もう一つの例を下記に示します。

-- TYPE MISMATCH -------------------------------------------------- src/Main.elm

I cannot update the `selected_refrigerator_num` field like this:

77|             { model | selected_refrigerator_num = String.toInt selectedRefrigerator }
                                                      ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
This `toInt` call produces:

    Maybe.Maybe Int

But it should be:

    Int

Note: The record update syntax does not allow you to change the type of fields.
You can achieve that with record constructors or the record literal syntax.

Hint: Use Maybe.withDefault to handle possible errors. Longer term, it is
usually better to write out the full `case` though!

toIntはMaybe Intを返すけど、ここではIntが要求されているよという話ですね。Maybeは失敗するかもしれないことを示しています。 String.toIntはIntに変換する関数ですが、Intに変換できない可能性があるのでMaybe Intを返すのですね。他の言語だったらここで実行時に例外が発生したりnullが返ってきてプログラムがクラッシュするところですが、Elmの場合はそれをコンパイル時に防いでくれます。そしてどうすればいいかかかれているHintをよんでみると、「Maybe.withDefaultをつかえばエラーに対処できるけど、caseをつかって分岐したほうが長期的にはよりよい」といっていますね。ここでは考えてみた結果、デフォルト値をうまく設定できないと思ったので下記のようにcaseをつかったパターンマッチにして解決しました。

        AddFood food ->
            let
                getedValue =
                    MyUtil.getValue model.selectedRefrigeratorNum model.refrigerators
            in
            case getedValue of
                Nothing ->
                    model

                Just refrigerator ->
                    { model | refrigerators = MyUtil.changeValue model.selectedRefrigeratorNum { refrigerator | foods = food :: refrigerator.foods } model.refrigerators }

こんな感じで型をうまくつかうことで適切なエラーメッセージやプログラムの仕様の詰めが甘い部分を指摘してくれます。

あとは状態がModelに集中している、関数型言語の特徴で至るところで書き換わったりしないってのが嬉しいポイントかなーっておもいます。

だんだんねむくなってきた....

今後

現状永続化されてないのでバックエンドをどうにかしたいですね。こちらも勉強がてらGoLangで書きたいと思っていましたが、Pythonでかくようなきがします。あとはCSSをちゃんとつけてきれいにしていきたい。テストもちゃんとしたい...

まとめ

CCSのみんな軽率にWebアプリケーションつくってこうな!そしてフロントエンドやるときはElmつかうと楽しいよ!わたしも勉強中なので勉強会しましょう!(雑

お手軽スマートホームのすすめ

この記事はCCS †裏† Advent Calendar 2019の21日目の記事です。

adventar.org

前日のとしさんの記事はこちらです

docs.google.com

どうも千葉大電子計算機研究会(以下CCS)、老害㌠いかろちゃんです。はやいものでストレートでここまで来たはずなのにきづいたら9年も大学にいました。今年度で修了したいー!

はじめに

f:id:ikaro1192:20191220034413p:plain

ここ数年スマートホームが話題となっています。 ただ話題となるだけではなく最近はスマートスピーカーをはじめ様々なスマートホーム関連商品が一般向けにも多く発売されるようになりました。 その流れで比較的安価にスマートホームを実現する商品が多く出てきています。 特にスマート家電と銘打ったものではなく、今まであった家電をちょっとした工夫で(場合によっては工夫0で)スマート化できるという商品が現れ始めました。今回はそのような製品を使いお手軽にスマートホーム化する方法を紹介します。

実は私もスマートホーム化する前までは「ほんとに便利なの?」と思っていました。しかし、実際に使ってみると今はスマートホームなしでは生きていけない体になりました。まずうちでやっているスマートホームの構成と日々のルーチンを紹介をします。その後、スマートホーム化実装の一例としてGoogle HomeからのHDMI入力切り替えの実装を紹介します。これによってスマートホームの便利さや実は安価で簡単に実現できることを知ってもらえれば幸いです。 さあみなさんもじゃんじゃん家をスマートホーム化して幸せになりましょう!

ちなみに過去にもスマートホームで下記の記事を書いています。こちらでは今回紹介しないプログラミングをすることで便利になる一例を示しています。もしよかったらこちらもどうぞ。

hogespace.hatenablog.jp

構成

スマートホームを構成する要素は大きく分けて入力を担当する入力部、入力を受けて処理を行う処理部、処理の結果を使ってなにかを操作・出力する出力部、出力部によって制御される操作対象に別れます(ちなみにそれぞれ専門用語というわけではなくここで勝手に言っているだけです)。もちろん複数に属するものも存在します。入力部で入力をうけとり、処理部でなにかしら処理を行い、出力部で操作対象を操作します。入力、処理、出力とあるのはプログラムと一緒ですね。入力and/or出力が物理世界とリンクしているというのがスマートホームの特徴であり、通常のプログラムとの違いです。

わたしの家ではTVとChromecastが来る前までは下記構成で生活していました。

入力部として

  • Slack(サービス)
  • Backlog(サービス)
  • Google Home mini(ハードウェア)
  • Nature Remo(スマホアプリ)

処理部として

出力部として

  • Nature Remo mini(ハードウェア)
  • 照明リモコン化スイッチ(ハードウェア)
  • Google Home mini(ハードウェア)

操作対象として

  • エアコン(ハードウェア;当然!)
  • 電灯(ハードウェア;当然!)

があります。

次に以上の中でも肝になるスマートスピーカー(Google Home)、学習リモコン(Nature Remo mini)、そしてつなぎとしてつかっているIFTTTというサービスについて紹介します。

まずは色々なものごとの中心となるスマートスピーカーGoogle Homeについてです。スマートスピーカーは各社からGoogle Home以外にも例えばAlexaなど色々でてます。わたしはGoogleに情報収集され生活が便利にされてしまうのがすきなのでGoogle Homeにしました。スマートスピーカーは単純な受け答えはもちろん、他のスマート家電と連携することや制御が可能です。

Nature Remo (mini)とは学習リモコンと呼ばれるもので任意の赤外線リモコンを登録でき、そのリモコンとして振る舞えることができるようになるリモコンのことです。Nature Remoはスマホアプリ、Google Homeとの連携だけではなく、WebAPIも備えていますのでかなり自由度高く赤外線リモコンを操作できるようになります。ここらへんの細かい話は先程あげた記事を参照してください。Nature RemoにはNature RemoとNature Remo miniがあります。この2つの違いは値段と搭載しているセンサー、そして大きさの違いです。私の家ではNature Remo miniによって電灯とエアコンを制御しています。Nature Remo miniは7000円弱でかえる、たまにセールでさらに安く買えます。また、各種家電の赤外線化系のものは比較的安価に買える、そもそも赤外線リモコンがついていることが多いです。そのためNature Remoがあればわりと色々制御できるようになることが多いです。後半で紹介するHDMI切替器もNature Remoで制御します。

Nature スマートリモコン Nature Remo mini Remo-2W1

Nature スマートリモコン Nature Remo mini Remo-2W1

  • 出版社/メーカー: Nature 株式会社
  • メディア: エレクトロニクス

このあとに関係するものとしてIFTTTというサービスがあります。このサービスは複数のサービスをつなげることを目的とした無料のサービスでプログラミングをすることなくあるサービス(メール、Slack、 そしてもちろんGoogle Homeも!)でなにか特定のアクションがあったら別のサービスでなにかを実行させるというようなことができます。例えばTwitterで「さみしいにゃん」とつぶやいたらLINE botがどうしたの?と話しかけてくれるようにすることもできます。IFTTTでは一つ一つの「なにがあったら、なにをする」という塊をAppletと呼んでいます。このちいさな単位でやっていくため個々のAppletは簡単に、そして全体では色々できるということが実現できます。

具体的にスマートホームをどうつかっているのかわたしの一日(理想。現実よりだいぶ誇張してます)を見てみましょう。まず朝起きるとGoogle Homeからアラームが鳴り照明がつきます。アラームを声でとめたあと、「OK, google 音楽を流して」といい音楽を流しながら朝ごはんをたべます。食べ終わったら音楽をとめ、「OK goolge 今日の天気は?」と聞き天気と気温を把握し服をきます。そして、「OK Google いってきます」ということで家の照明とエアコンがoffになります。研究室に行き、「OK google...あっ...Google Homeないんだった」と恥ずかしい思いをした後、研究をし、家に帰ります。このときに日報をだしていればすでに電灯とエアコンがつきます。家に帰ったら「OK google ただいま」というとGoogle Homeが「おかえりなさい」といってくれるのでさみしいにゃんとなることもなくすみます。家に帰ってつかれたら「 OK, Google イッて」というとshimaidon.netで「んほぉぉ!イッぐぅぅ!!」とつぶやいてストレス解消ができます。これであれやこれやすませたあと、「OK google おやすみ」というと明日の予定、天気、そしてアラームの時間をセットしたあと、電気を消してくれます。もちろんそんなのめんどくさいー明日休みだー!ってふとんに倒れ込んでしまった日も「OK google、電気消して」といえば布団から一歩も出ずに電気が消えてくれます。 とまあ今感じでGoogleHomeに依存した生活を送っております。音声によって操作できることによって電灯とエアコンの操作をしなくてすむようになり格段にくらしやすくなりました。

スマートホーム化実装の一例

最近Google Chromecastとモニタを買いました。このモニタにはHDMI入力が1つしかありませんでした。しかし、ChromecastをつなぎたいときPCをつなぎたいときがあります。もちろんHDMI切替器を買えば解決します。ですがいちいち切り替え器を手で操作してって面倒です。リモコン付きHDMI切替器がありますが、リモコンってなくしがちです(わたしだけ?)。そこでGoogle Homeから声で操作できるようにしましょう。

Google Homeから直に制御できるHDMI切り替え装置はない(あっても多分高い)と考えられます。そこで赤外線リモコン付きHDMI切替器を学習リモコンを使って制御することを考えます。学習リモコンは家にあるNature Remo miniをそのまま流用します。またGoogle HomeとNature Remoの連携には今回はIFTTTを使います。

リモコン付きHDMI切替器は下記製品を使いました。

この製品は3口あること、ボタンがトグル式だけではなく、各ポートと番号が対応している点がよいです。というのも学習リモコンで制御するする場合はステートレスのほうが扱いやすい(操作の冪等性が保たれる;とりあえず実行しとけば望みの状態になる)からです。

以下の手順はめんどくさくなったのでUIがよく更新されるのであまり詳しく書きません。公式ドキュメントや他の記事を参考にしてください。 まずはHDMI切替器のリモコンをNature Remoに登録します。今回は1番ポートと2番ポートをそれぞれChromecastとPCに割り当てます。「+」アイコンを押すと新しい家電を追加する画面に推移します。今回は一般的器具ではないのでなんでもいいですがとりあえずAV機器にしました。Remoの指示通りに追加しましょう。

次にIFTTTの設定です。まずはIFTTTに登録しましょう。

ifttt.com

登録したら自分のAppletを追加します。this側はGoogleHome、then側はNature Remoにします。GoogleHomeは適当なキーワードで反応するようにして、Remo側は「Control home appliances」で先程登録したボタンを起動するようにしましょう。これを2回繰り返してPCとChromecastそれぞれに切り替えられるように登録します。 これで完成です。テストしてみましょう。

ちゃんとうごきましたね!これで好きなときに声で切り替えることができるようになりました。

このようにプログラミングができなくても結構色々カスタマイズできて楽しいです。もちろんプログラミングするとさらに幅が広がってもっと楽しいです。

まとめ

今回はスマートホームの概要を紹介しました。そして実際に使っている構成やルーチンを示すことでどのように生活に組み込むかを紹介しました。最後に新しく機器を追加する手順を簡単に紹介しました。今回示したようにNature Remoと既存の安価な赤外線リモコン、そしてIFTTTを組み合わせることで比較的安価・簡単に自宅をスマートホーム化することができます。またGoogle Home(Nust) miniは時々セールをやっていて0-3000円で手に入るイベントが多々あります。今もyoutubeプレミアム会員ならGoogle Nust miniが無料で手に入るキャンペーンをやっているようです。みなさんも高そう、難しそうと偏見を持たず安価にスマートホーム関連商品を手に入れて快適な生活を手に入れてみてはいかがでしょうか?

22日目は下記のとっちーの記事です。

yoooomaruuuu.hatenablog.com

なぜ弱火のレシピを時短のために強火にしてはいけないか1次元お肉を熱伝導方程式で焼いてみる

はじめに

料理の失敗談として「弱火で10分のところを強火ですれば時間短縮になる!」といって「外は黒焦げ、中が生焼け」となるという話をよく(?)聞きます。これはなぜでしょうか?今回はこれについて熱伝導方程式を解いてもとめようというのが今回の記事の趣旨です。 f:id:ikaro1192:20190603134316p:plain

理論と手法

物理において何が現象の主要な支配要因になってるかを把握するためにはパラメータを最小限に抑えて、現象をもっともらしく表現できる近似をしてあげることが大切です。今回は最低限、現象を説明できる条件の1次元の熱伝導を計算します。また、厳密に解けそうな気がしますが数値計算を用いて解いていきます。

ということで1次元のお肉を焼いていきましょう。1次元の熱伝導方程式は下記のように表現できます。

  \frac{\partial T}{\partial t} = \kappa  \frac{{\partial}^2 T}{{\partial} {x}^2}

ここでTは肉内部の温度分布で時間tと位置xの関数、κは熱拡散率です。肉の熱拡散率は(杉山,2013)*1にかかれている1.30×10^-7 m2/sを使います。境界条件としてフライパンに接している側は140度と200度(IHで弱火と強火でよく使われている値)とします。とりあえずあとで数式に登場させるためこれを T_hと置きます。また、空気と接している側の境界条件ですが断熱*2とします。これは料理をしている最中に肉と比べて部屋の温度がどんどん上がっていかないという物理的条件に対応します。また熱拡散率は本来温度に依存しますが簡単のため定数とします。焼くお肉の厚さですがここは庶民ですので L=1 cmのもの*3を用意します。そして、初期条件としてお肉の温度を一様に T_0=10度とします。 以上の条件を式にすると

  T(t,x=0) = T_h

   \frac{\partial T}{\partial x}  |_{x=L}= 0

  T(t=0,x \lt L) = T_0

次に数値計算できるよう離散化をします。今回は微分方程式の解き方としては陽解法を採用します。具体的には微分を下記の差分に置き換えます。

 \frac{\partial T}{\partial t} = \frac{T(t+\Delta t) - T(t)}{\Delta t}

 \frac{{\partial}^2 T}{\partial {x}^2} = \frac{T(x+\Delta x) - 2 T(x) + T(x-\Delta x)}{( {\Delta x}^2)}

ここで \Delta t \Delta xはそれぞれを表しています。

  \frac{T(t+\Delta t) - T(t)}{\Delta t} = \kappa  \frac{T(x+\Delta x) - 2 T(x) + T(x-\Delta x)}{( {\Delta x}^2)}

整理すれば

  T(t+\Delta t, x)  = T(t, x) + \frac{\kappa \Delta t}{{\Delta x}^2}  (T(t, x+\Delta x) - 2 T(t, x) + T(t, x-\Delta x))

を得ます。この調子で断熱の境界条件も離散化しましょう。

 \frac{\partial T}{\partial x} |_{x=L} = \frac{T(L) - T(L-\Delta x)}{\Delta x} =0

  T(L)  =T(L-\Delta x)

と最後とその手前の温度が同じという条件に帰着します。

以上によって求めた計算結果を1分、2分、3分での熱の分布を140度で加熱した場合、200度で加熱した場合で比較していきます。

実装

次に実装するだけです。今回はC++で実装しました。下にソースコードを示します。 また、いくつか式には出てこない部分があるので解説をしておきます。

#include<iostream>
#include<vector>

int main(){

    int input_temprature;
    int input_time;

    std::cin >> input_temprature >> input_time ;

    const double boundary_condition = input_temprature;//境界条件 [℃]
    const double temperature_init = 10; // 初期条件 [℃]
    const double thermal_diffusivity = 0.130; // [mm^2/s]
    const double x_max = 10; // お肉の厚さ [mm]
    const double dx = 0.1;
    const int Nx = x_max/dx;

    const double t_max = input_time;//加熱時間[s]
    const double dt = 0.8 * dx*dx/(2*thermal_diffusivity); //安定条件より。0.8は念の為。解説参照
    const int Nt = int(t_max/dt);

    std::vector<double> before_temperature;
    std::vector<double> now_temperature;

    //境界条件
    before_temperature.push_back(boundary_condition);

    //初期値を入れる
    for(int i =1; i<Nx;++i){
        before_temperature.push_back(temperature_init);
        now_temperature.push_back(temperature_init);
    }

    for(int j=0; j<Nt;++j){
        //境界条件
        now_temperature[0] = boundary_condition;
        for(int i =1; i < Nx-1; ++i){
            now_temperature[i] = before_temperature[i] + ((thermal_diffusivity* dt)/(dx*dx))*(before_temperature[i+1]-2*before_temperature[i]+before_temperature[i-1]);
        }

        //境界条件
        now_temperature[Nx-2] = now_temperature[Nx-3];
        before_temperature = now_temperature; // 本文参照
    }

    double x = 0;
    for(auto temperature : now_temperature){
        x+=dx;
        std::cout << x <<" "<< temperature << std::endl;
    }

    return 0;
}

まずは安定条件について。詳しくは数値計算の教科書等を参照してほしいのですが、今回採用した方法では \Delta t \Delta xが大きいと解が安定しません。具体的には

 \Delta t \leq \frac{{(\Delta x)}^2}{2\kappa}

を満たしている必要があります。そのためdtを下記式で求めています。

const double dt = 0.8 * dx*dx/(2*thermal_diffusivity); //安定条件より。0.8は念の為。解説参照

各経過時間を配列で確保することもできるのですが、それではメモリが多量に必要になります。そこで2ステップ分の配列を用意してそれを入れ替えていくことでメモリを節約します。下記部分です。

        before_temperature = now_temperature; // 本文参照

ほんとはこんなコピー発生させてコストがーとおもいますが、今回は計算を回してみてもそんなに時間がかからなかったのでこのままにしてあります。

境界条件は怪しいですのでもしバグを見つけたらおしえてください()

計算結果

記載の便宜上、「焼きすぎ」、「適温」、「生焼け」の3つを定義します*4。100度を超えた領域を「焼きすぎ」、50度を下回る領域を「生焼け」、そして100度以下、50度以上を「適温」とします。また適温部分の長さを「可食部」とすることにします。また、140度で3分やいたとき、200度で1分焼いたとき、200度で2分20秒焼いたときの温度分布をそれぞれパターンA、パターンB、パターンCとします。

計算結果のグラフは下記となります。

f:id:ikaro1192:20190611020718p:plain
フライパンからの距離と温度の関係。140度で3分やいたとき、200度で1分焼いたとき、200度で2分20秒焼いたときのそれぞれ温度分布を赤線、青線、黒線で示してある。破線は50度(生焼けの境界温度)と100度(焼きすぎの境界温度)を示している。

パターンAでは約2.9 mmにいったところで適温となっています。これには生焼け部はありませんので、したがってパターンAの可食部は7.1 mmとなります。パターンBは焼きすぎの領域がパターンAと一致しています。パターンBは5.0 mmのところで生焼けとなります。したがって、このときの可食部は2.1 mmとなります。残りは生焼けの部分で5.0 mmです。パターンCでは4.5 mmで適温となります。したがって可食部は5.5 mmとなります。表にまとめると下記のようになります。

パターン 焼きすぎ部の長さ(mm) 生焼け部の長さ(mm) 可食部部(mm)
A 2.9 0 7.1
B 2.9 5.0 2.1
C 4.5 0 5.5

パターンAとBを比べると2.9 mmを堺にしてプライパン側ではAよりBの温度が高くなり、外側ではAよりBの温度が低くなっています。パターンAとCを比べると常にCの温度がAの温度を上回っています。

考察

どう頑張っても140度の温度分布と200度の温度分布を一致させることはできませんでした。どんな時間加熱しても200度で加熱した場合は可食部は140度3分の場合の可食部以下となってしまいます。これは加熱する温度によって温度分布は異なるものになることを示します。温度分布が異なるのですから、弱火を強火で代用することができないことがわかります。

パターンAとBの比較からは表面の焼け具合を同様にすると中が生焼けになってしまうことがわかります。また、焼きすぎの部分でも200度のほうが同じ位置なら温度が高いです。これは「外は黒焦げ、中が生焼け」という状況に対応します。パターンCのグラフの意味することは外が黒焦げとなってしまった状況に対応します。つまり200度のほうがはやく表面は焼けますが、熱が中まではやく伝わっていくかは別問題ということを意味します。これは実は数値計算をするまでもなく数式でも理解することができます。離散化した式を見てみると次の瞬間の温度というのは周囲の温度差にある定数をかけたものとよむことができます。つまり、温度差があればそれだけ早く"平均的な"温度に落ち着こうとするものの、瞬時にその温度にいけるわけではないということを示しています。そういった時間ラグがあるため肉の中を一瞬で熱は伝わらず結局、表面には早く熱が伝わり焦げ、中の方は伝わらず生焼けとなってしまうのです。

まとめ

今回は1次元の豚肉を焼くとその温度分布はどうなるかを熱伝導方程式を数値計算を用いて解くことでもとめました。結果は加熱側の温度が違えば加熱時間をどう調整しようと同じ結果にすることはできないというものです。式を考察することにより熱は一瞬で伝わることができないため加熱温度が高ければ表面には早く熱が伝わり焦げ、中の方は伝わらず生焼けとなってしまうことを明らかにしました。ということでレシピの火加減と時間はまもるようにしましょう。

*1:https://www.jstage.jst.go.jp/article/cookeryscience/46/4/46_299/_pdf

*2:結論は大きく変わりませんが本当は少しづつ空気に熱は漏れ出すの熱抵抗を使ったほうがいいというコメント頂きました

*3:実は最初は3 cmでやったのですがうまく火が通らなかったのでこうしました

*4:実際に焦げたり美味しく安全に食べれる温度とは異なります

Nature RemoとGoogle Cloud Functionsをつかって日報を送ったら家の電気をつけるようにしてみた

はじめに

どうもこんにちはいかろちゃんです。最近自宅のスマートホーム化をはじめました。その第一弾として帰ることを検知して家の照明をつけようというのが今回の目的です。

個人用に研究室でBacklogをつかって日報という形で日々のまとめをその日の最後に書いています。また、家に帰ったら照明がついていてほしいですよね。日報が送って照明がつくようになればやる気も返った時のテンションも爆上がり間違いなしです。そこでBacklog→Google Cloud Functions(GCF)→Nature Remo→照明という流れで日報を送ったら家の照明をつける仕組みをつくってみました。

ハードウェアと使うサービスの概要

まずはNature Remoについて。これはいわゆる学習リモコンと呼ばれるもので、赤外線で動く電化製品のリモコンの信号を登録することによりそのリモコンとして振る舞えることができるようになるリモコンのことです。もちろん複数の電化製品の信号を学習させることも可能です。スマートフォンや、Google Home等のスマートスピーカーはもちろんリモコンとしてつかうことができます。そして一番嬉しい特徴がNature Remoはインターネット経由でアクセスできるWeb APIが公開されているという点です。このAPIをつかうことで任意のプログラムから赤外線信号を受け取る電化製品を操作することができるようになります。Nature RemoとNature Remo miniが存在しますが今回は安いRemo miniを利用しました(下記製品)。搭載しているセンサーと大きさ以外の機能は基本的に同一です。

Nature スマートリモコン Nature Remo mini Remo-2W1

Nature スマートリモコン Nature Remo mini Remo-2W1

  • 発売日: 2018/07/12
  • メディア: Tools & Hardware

次に照明のスマート化について。もともと自宅にあった照明はペンダント型照明(ようするに紐をひっぱるやつ)でした。紐を引くような機構をつくるのもありですが、よのなかには照明と電源の間に割って入って赤外線リモコンで操作可能にしてくれる製品も存在します。今回は「照明リモコンスイッチ OCR-CRS01W」を利用しました(下記製品)。この製品の嬉しいところは電灯に状態をもたせてボタンでトグルするのではなくonとoffボタンが存在することです。これにより面倒な状態管理をせずに操作できます。

次に日報管理です。これはBacklogを使っています。Backlogがどういうサービスかは今回とは関係ないので省きます。今回重要なのはコメントの追加をフックするWeb hockを登録できるという点です。これをNature Remoにつなげれば今回の目的は達成です。

Nature RemoのWeb APIはWeb HockではないのでBacklogと直接はつなげません。そこで今回はGoogle Cloud Functions(GCF)を仲介役としてはさみます。GCFはFunction as a Service(FaaS)の一つです。FaaSについての詳細な解説はここでははぶきますが、サーバレスであるイベントをうけてあるイベントを発火することが可能です。今回はBacklogからGCFを叩き、GCFがRemoのWeb APIを叩くという構成にします。

プログラムの流れ

Nature Remoをインターネット越しに操作するためにはアクセストークンを発行する必要があります。まずはhttps://home.nature.global/にアクセスしてログインします。その後、"Generate Access Token"でアクセストークンを発行しましょう。アクセストークンは後から確認できないようになっているのでメモを忘れずに。

Nature Remoに登録されている家電に対するそれぞれの操作はSignal IDとして管理されています。したがって、家電を操作するためにはSignal IDを特定する必要があります。下記コマンド(取得したアクセストークンの部分は適宜変更してください)で登録されている電化製品とその情報を得ることができます。nicknameを手がかりに対象の電化製品を特定しましょう。私の環境の場合は下記のような部分がありましたのでこれの電源onのSignal ID(dc1642ea-1d35-47bb-9ead-ab4aa8a51ac9")を使います。

curl -X GET "https://api.nature.global/1/appliances" -H "accept: application/json" -k --header "Authorization: Bearer 取得したアクセストークン"
(前略)
   "model": null,
    "type": "IR",
    "nickname": "照明",
    "image": "ico_light",
    "settings": null,
    "aircon": null,
    "signals": [
      {
        "id": "dc1642ea-1d35-47bb-9ead-ab4aa8a51ac9",
        "name": "電源on",
        "image": "ico_lightup"
      },
      {
        "id": "4ec42e50-b764-470a-963a-48a44e6ca364",
        "name": "消灯",
        "image": "ico_lightdown"
      }
(後略)

いよいよGCFをつかって連携部分を作っていきます。やることは単純で特定プロジェクトに投稿されたらNature RemoのAPIを叩くだけです。まずはGCFにログインし(この手順は省きます)、「関数を作成」をクリックします。関数名はdaily_report,メモリは128 MB,トリガーはHTTPとしました。表示されている「URL」という項目がBacklogに登録するWeb Hockとなります。ソースコードは下記のようにしました。下記のaccess_token, target_signal, project_keyを適切に設定すると動きます。project_keyはBacklogから調べてください。

import requests

access_token = '取得したアクセストークン'
target_signal = '取得したSignal ID'
project_key =  '日報投稿先のプロジェクトキー'

def daily_report(request):
    request_json = request.get_json()
    if  str(request_json["project"]["projectKey"]) == project_key:
        headers = {
            'accept': 'application/json',
            'Authorization': 'Bearer {}'.format(access_token),
        }
        response = requests.post('https://api.nature.global/1/signals/{}/send'.format(target_signal), headers=headers)


    return ""

最後にBacklogににWeb HockとしてGCPのURLを追加します。「プロジェクト設定」→「インテグレーション」→「Webhook」→「設定」をクリックしていきます。「WebHook URL」は先程GCFでえたURLとしましょう。また、通知するイベントは「課題にコメント」のみとしました。

これで完成です。投稿してみて実際に家の電気がつくか試して遊びましょう。

まとめと展望

今回はBacklogからRemoをGCFを使ってつなぐということを行いました。これによって日報を出し忘れると家が暗いままというかなしいことになり日報を出すモチベーションがアップです。今回はBacklogということでしたがWebhookに対応してれば同じパターンが使えますのでSlackやその他のものとも同様な手法でつなぐことができると思います。また、Remoは多様な電化製品に対応しており温度センサー等をそなえているので暑い時期ならついでにクーラーをつけるというようなことも可能です。いろいろアイディア次第で楽しいことができそうなのでたのしんでいきましょう!

管理しているMastodonインスタンスのRedisが攻撃を受けてしまった事件の技術的レポート

概要

12/29にクラウドの移行を実施した。 その際にdockerの設定ミスによりRedisへ外部から自由アクセスできるようになっていた。 後日、Redisへの攻撃の試行を検知した。 そのため、設定の修正を行った。

ユーザの皆様および関係各所申し訳ございません。

影響

  • 期間:2018 12/29-2019 01/05

  • ユーザへの影響: 12/29-01/05にフォローする/されるしたユーザのメールアドレスが、それぞれ短期間(各数秒程度)ではあるものの外部からアクセス可能な状態となっていた。

調査ログ

移行後、頻繁にDOWNアラートを検知。 ログを調査したところ下記出力。

Redis::CommandError: MISCONF Redis is configured to save RDB snapshots,

この時点でRedisまわりかDockerのメモリ関連の設定ミスだろうと疑う。 コンテナを再起動すれば治るのでアラートが起きたら発火する再起動スクリプトで暫定対応を実施。調査は後日することに。

後日調査。 障害時にRedisでsaveが発行できないことを発見。

$ /usr/local/bin/docker-compose exec redis redis-cli
127.0.0.1:6379> save
(error) ERR

Redisログ確認実施。

redis_1      | 1:M 05 Jan 12:23:55.004 * Background saving started by pid 1168
redis_1      | 1168:C 05 Jan 12:23:55.004 # Failed opening the RDB file root (in server root dir /etc/crontabs) for saving: Permission denied

なぜか"/etc/crontabs"に書き込もうとしているログ出力を確認。この時点で攻撃を受けた、あるいは使っているRedisイメージに悪意のあるコードが含まれていた可能性を疑う。

Redisのディレクトリ関連の設定を見てみると下記のように"/etc/crontabs"に書き込む設定にされている。真っ黒。

$ docker-compose exec redis redis-cli
127.0.0.1:6379> config get dir
1) "dir"
2) "/etc/crontabs"

もしやと思いホスト外からRedisにアクセス。普通にできてしまう...

ファイアウォール設定確認。

# firewall-cmd --list-all
public (active)
  target: default
  icmp-block-inversion: no
  interfaces: eth0
  sources:
  services: ssh dhcpv6-client https http
  ports:
  protocols:
  masquerade: no
  forward-ports:
  source-ports:
  icmp-blocks:
  rich rules:

redis関連のポートは開けていない。

Docker-Composeの設定を疑いportsを削除。 外部からつながらないことを確認。 Docker-Composeの設定により外部からアクセスできるようになっていたことを把握。

RedisのKeyとして"weaponX"等の不審なキーが存在することを確認。

対応

RedisのRDBファイルを削除。

docker-compose.ymlのportsで"IP:port:port"とすることでホストからのみアクセスできるようになる。この設定に直した。

監視拠点を別のConoHa VPSインスタンスとしてたて、プライベートネットワークで通信するようにした。 そして、ConoHaの接続許可ポートをしぼった。

DBへのアクセスは外部からできないことを確認。 また、ログ等を精査しRedis以外には侵入された形式がないことを確認。 sidekiqによるメール配送のためにsidekiqのキューに乗って消化されるまでの間、Redisにユーザのメールアドレスが存在することを確認。

なぜ設定漏れが発生したかの経緯

Sidekiqのキュー監視のためにRedisのポートを開放していた。 ただし、この時点ではIDCFクラウドをつかっており、IDCFクラウドでは上位LBでファイアウォールがあったため外部からRedisへはアクセスができなかった。 IDCF個人プラン廃止のため12/29にConoHaへ移行を実施。 ConoHaの接続許可ポートは未設定。 これは任意ポートを指定して開けることができないため監視を通す必要性から。 firewallの設定を実施。 しかし、「Dockerはfirewalldではなくiptablesに書き込むためfirewalldの設定は無視される」ということに気づいていなかった。

再発防止策

このRedisの攻撃手法についてもっと知りたい人のためのリンク

antirez.com

PythonとC++で雑に作るオンラインマルバツゲーム

この記事はCCS †裏† Advent Calendar 2018

adventar.org

の5日目の記事です。前日のとっちーの記事はこちら yoooomaruuuu.hatenablog.com

どうも千葉大電子計算機研究会(以下CCS)、老害㌠いかろちゃんです。おっさんではありません。

はじめに

現代のゲームはいわゆるソシャゲをはじめとして非常に多くのゲームがインターネットに接続されています。しかし、いざ個人でそういった開発しようと思っても情報が豊富にある状況とはいえません。そこで本記事ではオンラインマルバツゲームを題材に実際に動くサンプルを提供することで、インターネットを介したゲームをつくるヒントを提供できればと思います。とおもったが、途中でやる気が消失したので読む人はがんばってよみといてくだせぇ...

概要

通信する方法は複数考えられます。 速度を考えなくて良い場合ではプロトコルとしてHTTPを使用するのが良い選択です。 実際に多くのソシャゲでHTTPが利用されています*1。 HTTPというのはHypertext Transfer Protocolの略です。 もともとはwebサイトのhtmlなどのファイルをwebサーバ-ブラウザ間でやりとりするために考えられました。 しかし、HTTPプロトコルはどのようにデータをやりとりするかを規定しているだけです。 そのためhtml以外のデータをサーバ-クライアントとやりとりするために使っても問題ありません。 多くのソシャゲではサーバとクライアントでJSONという形式でやり取りすることで通信を成立させます。 クライアント-クライアント間の通信はサーバを仲介することによって実現します。

つまり、作る必要があるものとしては - HTTPプロトコルをつかってJSONをサーバとやりとりできるクライアント - HTTPプロトコルを使ってクライアントとJSONをやりとりできるWebアプリケーション があります。 ライブラリとしてHTTPプロトコルライブラリ及びJSONを扱えるライブラリがあればよさそうですね。 クライアント側のライブラリ、つまりC++のライブラリ、としてはcpprestsdkがあります。 cpprestsdkはMicrosoftが出しているクロスプラットフォームなHTTPライブラリでJSONも扱えます。 サーバぐぁのライブラリ、つまりPythonのライブラリ、としては今回はFlaskを利用します。

また、当然Webアプリケーションを実行するためのサーバ環境(=インフラ)も必要です。 こちらも時間があれば書きます。

APIの仕様

次はAPIの仕様を決定します。 書くのだるくなってきた... ソースコードみて察してください

ソースコード

めんどくさくなってきて途中まで...裏だしいいよね。 後ろに改善点等書きます。 また、勝敗判定の実装は読者への課題とします。 ユーザ登録もバグってる気がするのでDBに手動で書き込むかなおすかしてあげてください。

サーバ側

import json
from flask import Flask, jsonify, request
from flask_sqlalchemy import SQLAlchemy

app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///test.db'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
db = SQLAlchemy(app)


class User(db.Model):
    __tablename__ = 'users'
    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String(80), unique=True)
    victoryCount = db.Column(db.Integer, nullable=False)
    defeatCount = db.Column(db.Integer, nullable=False)

    def __init__(self, username):
        self.username = username
        self.victoryCount = 0
        self.defeatCount = 0

    def __repr__(self):
        return '<User %r>' % self.username


class Room(db.Model):
    __tablename__ = 'rooms'
    id = db.Column(db.Integer, primary_key=True)
    board = db.Column(db.Text)
    player1 = db.Column(db.Integer, db.ForeignKey('users.id'))
    player2 = db.Column(db.Integer, db.ForeignKey('users.id'))
    nowPlayer = db.Column(db.Integer)
    # 0 : waiting user, 1: battle, 2 end
    nowState = db.Column(db.Integer)
    player1_relationship = db.relationship("User", foreign_keys=[player1])
    player2_relationship = db.relationship("User", foreign_keys=[player2])

    def __init__(self, player1):
        self.board = '[[0,0,0],[0,0,0],[0,0,0]]'
        self.nowState = 0
        self.player1 = player1

    def __repr__(self):
        return '<Room %r>' % self.id


# ホントはGETも必要だけど...
@app.route("/api/v1.0/user", methods=['POST'])
def get_user():
    req_data = request.get_json()
    user = db.session.query(User).filter_by(username=req_data["user_name"]).first()
    if user is None:
        user = User(name)
        db.session.add(user)
        db.session.commit()
        result = {'result': {'user_id': int(user.id), 'user_name': user.username}}
        return jsonify(result)
    else:
        result = {'result': {'user_id': int(user.id), 'user_name': user.username}}
        return jsonify(result)


# POST(player_id)
# GET(room_id)
@app.route("/api/v1.0/lobby", methods=['GET', 'POST'])
def get_lobby():
    req_data = request.get_json()
    if request.method == 'GET':
        room_id = request.args.get("room_id", type=int)
        room = db.session.query(Room).filter_by(id=room_id).first()
        result = {'result': {'room_id': int(room.id), 'state': room.nowState}}
        return jsonify(result)
    elif request.method == 'POST':
        # 空きの検索
        room = db.session.query(Room).filter_by(nowState=0).first()
        if room is None:
            room = Room(req_data['player_id'])
            db.session.add(room)
            db.session.commit()
            result = {'result': {'room_id': int(room.id), 'state': room.nowState}}
            return jsonify(result)
        else:
            room.nowState = 1
            room.player2 = req_data['player_id']
            room.nowPlayer = 1
            db.session.add(room)
            db.session.commit()
            result = {'result': {'room_id': int(room.id), 'state': room.nowState}}
        return jsonify(result)


# POST(location_x,location_y,player_id)
@app.route("/api/v1.0/board/<room_id>", methods=['GET', 'POST'])
def get_board(room_id):
    room = db.session.query(Room).filter_by(id=int(room_id)).first()
    if request.method == 'GET':
        if room.nowPlayer == 1:
            now_player = room.player1
        elif room.nowPlayer == 2:
            now_player = room.player2
        result = {'result': {'room_id': int(room_id), 'now_player_id': now_player, 'board': json.loads(room.board)}}
        return jsonify(result)
    elif request.method == 'POST':

        req_data = request.get_json()

        if room.nowPlayer == 1:
            now_player = room.player1
        elif room.nowPlayer == 2:
            now_player = room.player2

        if req_data['player_id'] != now_player:
            result = {'status': "bad request"}
            # ステータスコードをかえしたい...
            return jsonify(result)

        board = json.loads(room.board)
        if board[req_data["location_y"]][req_data["location_x"]] == 0:
            board[req_data["location_y"]][req_data["location_x"]] = room.nowPlayer
            room.board = json.dumps(board)
            if room.nowPlayer == 1:
                room.nowPlayer = 2
            elif room.nowPlayer == 2:
                room.nowPlayer = 1
            db.session.add(room)
            db.session.commit()
            result = {'result': {'room_id': int(room_id), 'now_player_id': now_player, 'board': json.loads(room.board)}}
            return jsonify(result)
        else:
            result = {'result': "bad"}

            return jsonify(result)


@app.route("/api/v1.0/battle_status/<room_id>", methods=['GET'])
def get_battle_status(room_id):
    room = db.session.query(Room).filter_by(id=int(room_id)).first()
    result = {'result': {'now_user_id': room.nowPlayer, 'now_state': room.nowState, 'room_id': int(room_id)}}
    return jsonify(result)


if __name__ == "__main__":
    app.run()

requirements.txt

Click==7.0
Flask==1.0.2
Flask-SQLAlchemy==2.3.2
itsdangerous==1.1.0
Jinja2==2.10
MarkupSafe==1.1.0
SQLAlchemy==1.2.14
Werkzeug==0.14.1

クライアント側

#include <cpprest/http_client.h>
#include <cpprest/filestream.h>
#include<iostream>
#include<string>

using namespace utility;                    // Common utilities like string conversions
using namespace web;                        // Common features like URIs.
using namespace web::http;                  // Common HTTP functionality
using namespace web::http::client;          // HTTP client features
using namespace concurrency::streams;       // Asynchronous streams

int main(int argc, char* argv[])
{


    //ユーザ認証・登録処理
    std::string user_name;
    int user_id;
    std::cout << "user IDを入力してください" << std::endl;
    std::cin >> user_name;

    {
        json::value PostData;
        PostData["user_name"] = json::value::string(user_name);
        http_client client(("http://127.0.0.1:5000/api/v1.0/user"));


        auto post = client.request(methods::POST,"",  PostData.serialize(), "application/json").then(
                [&user_id](http_response response)
                {
                    if (response.status_code() == status_codes::OK){
                        return response.extract_json();
                    }
                }).then([&user_id](json::value json)
                {
                    user_id =  json["result"].as_object()["user_id"].as_integer() ;
                }
                );
        post.wait();
    }
    std::cout << "user id" << user_id << std::endl;
    int player_id = user_id;

    // ロビー処理
    int lobby_state =0;
    int room_id = 0;

    {
        json::value PostData;
        PostData["player_id"] = json::value::number(player_id);
        http_client client(("http://127.0.0.1:5000/api/v1.0/lobby"));


        auto lobby_post = client.request(methods::POST,"",  PostData.serialize(), "application/json").then(
                [&lobby_state, &room_id](http_response response)
                {
                    if (response.status_code() == status_codes::OK){
                        return response.extract_json();
                    }
                }).then([&lobby_state,&room_id](json::value json)
                {
                    room_id =  json["result"].as_object()["room_id"].as_integer() ;
                    lobby_state = json["result"].as_object()["state"].as_integer() ;
                }
                );
        lobby_post.wait();
    }

    std::cout << lobby_state << std::endl;


    std::cout << "対戦相手の選別中..." << std::endl;
    while(lobby_state == 0){
        http_client client(("http://127.0.0.1:5000/api/v1.0/lobby"));
        uri_builder builder;
        builder.append_query(U("room_id"), U(std::to_string(room_id)));

        auto lobby_get = client.request(methods::GET,builder.to_string()).then(
                [&lobby_state](http_response response)
                {
                    if (response.status_code() == status_codes::OK){
                        return response.extract_json();
                    }
                }).then([&lobby_state](json::value json)
                {

                    lobby_state = json["result"].as_object()["state"].as_integer() ;
                }
                );

        lobby_get.wait();
    }

    // ゲーム本体

    while(1){
        int now_player = -1;
        while(now_player != player_id){
            http_client client(("http://127.0.0.1:5000/api/v1.0/board"));
            uri_builder builder(U(std::string("/")+std::to_string(room_id)));

            auto board_get = client.request(methods::GET,builder.to_string()).then(
                    [&now_player](http_response response)
                    {
                        if (response.status_code() == status_codes::OK){
                            return response.extract_json();
                        }
                    }).then([&now_player](json::value json)
                    {

                        now_player = json["result"].as_object()["now_player_id"].as_integer() ;
                    }
                    );

            board_get.wait();
        }


        {
            http_client client(("http://127.0.0.1:5000/api/v1.0/board"));
            uri_builder builder(U(std::string("/")+std::to_string(room_id)));

            auto board_get = client.request(methods::GET,builder.to_string()).then(
                    [&now_player](http_response response)
                    {
                        if (response.status_code() == status_codes::OK){
                            return response.extract_json();
                        }
                    }).then([&now_player](json::value json)
                    {

                        std::cout << "現在の盤面" << std::endl;
                        for(auto col :json["result"].as_object()["board"].as_array()){
                            for(auto val:col.as_array()){
                                std::cout << val;
                            }
                            std::cout << std::endl;
                        }
                    }
                    );

            board_get.wait();
        }

        int x,y;
        std::cout << "石を置く位置を選択してください" << std::endl;
        std::cin >> x >> y;

        {
            json::value PostData;
            PostData["player_id"] = json::value::number(player_id);
            PostData["location_x"] = json::value::number(x);
            PostData["location_y"] = json::value::number(y);


            http_client client(("http://127.0.0.1:5000/api/v1.0/board"));
            uri_builder builder(U(std::string("/")+std::to_string(room_id)));

            auto board_get = client.request(methods::POST,builder.to_string(),  PostData.serialize(), "application/json").then(
                    [](http_response response)
                    {
                        if (response.status_code() == status_codes::OK){
                            return response.extract_json();
                        }
                    }).then([](json::value json)
                    {
                        std::cout << "現在の盤面" << std::endl;
                        for(auto col :json["result"].as_object()["board"].as_array()){
                            for(auto val:col.as_array()){
                                std::cout << val;
                            }
                            std::cout << std::endl;
                        }
                    }
                    );

            board_get.wait();
        }


    }
    return 0;
}

追加要素

エラーハンドリングをまったくと言っていいほどしていません。 悪いプログラムの見本です。実際には接続切れ、想定しない結果が返ってきたなどに備える必要があります。 ちゃんとしましょう。

また、データは暗号化しないとチートされる確率があがります。実際にはちゃんと暗号化しましょう。暗号化アルゴリズムとしてはAESを使うという記事をよく見かけます。 Roomの情報をDBで持ってますが、こういう情報はRedis等のKVSにもたせたほうがbetterだと思います。

今回は雑にGETとPOSTをつかう、ステータスコードも200しか返してませんが実際にはもっとちゃんとします。RestfulAPIとかGraphQLとかで調べてみるといいでしょう。

GUIを使う際はローディング画面なり出すと思います。その時はクライアント側はwaitじゃなくて非同期にできるものもあるのでそちらを利用しましょう。

また、すごい勢いでgetしてますが普通こういうデータはWebSocketとかつかってサーバ側からpushするようにしたほうがいいんじゃないかな。

おわりに

ほんとはもっとちゃんと書くつもりだったけど途中でやる気が無くなってしまいました。 気が向いたらなおすかもしれません。 会員の皆さんは本記事を参考に(反面教師に?)ソシャゲをつくってみましょう!

明日はkamewo氏のマイクラ話です。

*1:ただし、HTTPはセキュアではないのでSSL/TLSプロトコル上でHTTPを運用するHTTPSを使うのが普通です。