『ゼロから創る暗号通貨』
第2章: P2Pネットワーク : 基盤作りから始めよう

濵津 誠/hamatz

第2章 P2Pネットワーク : 基盤作りから始めよう

本章では、SimpleBitcoinの基盤となるP2Pネットワークを実装します。ここで実装するP2Pネットワークでは、前章で登場したWalletとServerの機能を、EdgeノードおよびCoreノードという2種類のノードに割り当てます。2種類のノードの接続形態として考慮が必要になるのは、CoreノードとCoreノードの接続、および、CoreノードとEdgeノードの接続です。それぞれの接続を実現するプログラムを順番に書いていくので、ぜひ主人公たちと一緒にコードを書き、その動作をお手元のPC上で確認しながら読み進めてみてください。

本章を読み進める際には次のようなポイントを意識してみてください。

  1. CoreノードとEdgeノードの違い
  2. P2Pネットワークが拡大していくステップ
  3. SimpleBitcoinにおけるP2Pネットワークの役割範囲

[2pt_line]

「それでは、お待ちかねの開発に取り掛かろう。予告どおり、まずは暗号通貨に直接は関係しないP2Pネットワークを作ってもらうよ」

「なんでP2Pネットワークを作る必要があるんだっけ」

「暗号通貨の基盤となるのがブロックチェーンであり、そのブロックチェーンは中心が存在しない複数のサーバーが連携して、はじめてうまく機能する。そのようなサーバー間の連携は、P2Pネットワークにほかならない。全体像と照らし合わせると図2.1で示す箇所になる」

今回作る機能ブロックについて

図2.1: 今回作る機能ブロックについて

「いきなりP2Pネットワークを実装しろとかいわれても、あんまイメージわかないんだよなぁ」

「当然そうだろう。ここまでの説明では、ブロックチェーンにはWalletとServerという2つの登場人物が存在する、という話しかしていないからね。これから作りたいブロックチェーンを機能させるためのP2Pネットワークにおける両者の関係性や、連携方法については、まったく触れていない。だから、まずはそれらをはっきりさせる必要がある。それがこのカリキュラムのスタートポイントだ」

「よかった……。コードを書く前にもうちょっと説明があるんだ」

2.1 P2Pネットワーク

「念のために、P2Pの基本からおさらいしておこう。P2Pは、peer-to-peerの意味だ。peerには、『(能力などが)同等の人』みたいな意味がある。相互に接続されているpeer同士がそれぞれ通信したり各自でデータを保持したりする図2.2のような形態のネットワークだ」

P2Pネットワーク

図2.2: P2Pネットワーク

「わざわざP2Pなんて名前がついているってことは、他の形態のネットワークもあるっていうことだよね」

「ある。というか、中心となるサーバーに他のクライアントが接続する図2.3のような形態のほうが一般によく見られる。こういう形態は、クライアント-サーバー型のネットワークと呼ばれている」

中心となるサーバーを持つ一般的なクライアント-サーバー型のネットワーク

図2.3: 中心となるサーバーを持つ一般的なクライアント-サーバー型のネットワーク

「うん。それはさすがに知ってた。暗号通貨では、中央サーバーがないP2Pが選択されている、ってことだよね」

「そういうことになる。どこかのpeerが離脱したり、あるいは新しいpeerが増えたり、そういう変化に柔軟に対応できるようにすることで、特定のプレイヤーに依存することなく成立し続けるネットワークが暗号通貨には必要不可欠だからね」

「なんか、P2Pネットワークに繋がるpeerのことをサーバーって言っているけど、暗号通貨の登場人物はWalletとServerの2つだったよね。Walletはどういう位置づけになるの?」

「いい質問だ。P2Pネットワークにおいて、いわゆるサーバーにもクライアントにもなり得るものを『peer』と呼ぶことにする。その定義からするとWalletはクライアントの役割しか果たさないわけだから、『peer』ではない。Walletは、新しいTransactionを生み出したときにだけ、Server群にそれを伝えられれば十分。そう考えると、常時P2Pネットワークに接続しておく必要はない」

「なるほどー。Walletは『P2Pで繋がっているものではない』なら、しばらくは忘れていてもいいのかな」

「単にP2Pネットワークを実装することが目標なら、その認識で正しい。しかし、最終的に作りたいものはP2Pネットワークそのものではなく、暗号通貨の基盤となるブロックチェーンだ。WalletからServerに対してTransactionが送信されるというイベントがあって、はじめてブロックチェーンに更新が発生するのだから、相互接続するServerだけ作っても暗号通貨としては機能しない」

「たしかに」

「よって『peer』とはいえないWalletについてもP2Pネットワークの一部としてまとめて考えるほうがいいだろう。とはいえ、WalletとServerは同格の存在ではなく、役割が異なる。そのことを明確にしたほうがいいので、便宜上、WalletをEdgeノード、ServerをCoreノードと呼び分けることにしよう」

「あえて名前を変えて呼ぶのはなんで?」

「P2Pネットワークは暗号通貨以外の用途でも利用が可能だよね?WalletとServerは暗号通貨用途を実現するアプリケーションとしての名前であって、基盤としてのP2Pネットワークを実現するための呼び名としては適切ではない」

「ああ、なるほど。通信に特化した部分を切り出して別に名前を与える、ってことだね」

「そのとおり。次は、このCoreノードとEdgeノードについて詳しく見ていくことにしよう」

2.2 CoreノードとEdgeノード

「CoreノードとEdgeノードの関係を図にすると、図2.4のようになる」

CoreノードとEdgeノードの関係

図2.4: CoreノードとEdgeノードの関係

「Coreノード同士がそれぞれ相互に接続してて、各Coreノードにぶら下がるような形で複数のEdgeノード群が接続されている、って感じだね」

「そう。EdgeノードはP2Pネットワークに対し、必要なときにだけ新規のTransactionを送信する。自身に関連する値が変更されているかを定期的に確認するだけでよいので、いずれかのCoreノードを選んで接続するだけだ」

「この図を見るまで、Transactionってのは、Edgeノードから全部のCoreノードに対して送信されるものかと思ってた」

「そういう形にするのもアリだとは思う。けれど、Coreノードの数が3桁〜4桁と増えていった将来のことを考えると、全部のCoreノードに対してEdgeノードからTransactionを一括で送るのは効率が悪そうだよね」

「そりゃそうか」

「余談だけど、その意味では、CoreノードがP2Pネットワークに参加している全部のCoreノードを記憶している、というのも効率が悪い。そこで一般的には、図2.5のように、Coreノードのネットワーク同士を繋ぎ合うような、『リレーノード』などと呼ばれるものを用意する」

リレーノード

図2.5: リレーノード

「ふむふむ、なるほど。リレーノードか。また面倒そうなものが増えたな……」

「まあ、SimpleBitcoinでは話を単純化するために、今回はCoreノードとEdgeノードの2種類だけで話を進めていこう」

2.2.1 接続するCoreノードの選択

「とはいえ、いくら話を単純化するといっても、Edgeノードの接続先を1か所に絞るような構成にすれば、すべてのEdgeノードが1つのCoreノードに集中してしまうよね。それはさすがにあんまりだ。Edgeノードの接続先が1か所に集中することなくいい感じにバラけるようにしたいけど、それにはどうすればいいかなー」

「ちゃんとやるなら、Coreノード側というかP2Pネットワーク側で、接続されているEdgeノードの数といった何らかの基準に応じて、登録を要請してきたEdgeノードの接続先を割り振るような機能があるほうがいいんじゃないかな」

「たしかに。そっちのほうがちゃんとしてる。でも、けっこう複雑になりそうだし、ここは日和ってEdgeノード側で接続したいCoreノードを選択する程度にしておきたい……」

「それは悪くない判断だよ。では、ユーザーが自分で接続先のCoreノードを選択するとして、その候補はどうやって入手するとよいだろうか?」

「気持ち的には手動で、と言いたいところだけど、さすがにCoreノードの一覧をダウンロードしてきてユーザーに選ばせるくらいのことはしたほうがいいかなぁ」

「その機能を入れるにしても、肝心のCoreノードの一覧を取得する先は手動で入力するか、あるいは、あらかじめソースコードの中に固定値を入れておく必要はあるよね」

「あー、たしかに。だったら、もう最初からCoreノードの候補をいくつか仕込んどけばよくない?」

「SimpleBitcoinはゼロからの立ち上げだから、最初からがっつりCoreノードが揃ってる前提はないよ」

「そうだった。となると、どこか特定の場所にCoreノードを登録してもらうことにして、そこから一覧をもらって接続先のCoreノードを選ぶような方式にするか……」

「せっかく『特定のプレイヤーに依存しない』ようなブロックチェーンを作ろうとしてるのに、いきなり特定の何かに依存する前提にするのは、ちょっと悲しくないかい?」

「うわー、たしかにそのとおりだー。あー、どうすりゃいいんだー」

「あんまり考えすぎるとドツボにハマるし、導入初期はシンプルにしておいて、それなりにネットワークが立ち上がった後でCoreノードの選択方法を変える、くらいの気持ちでもいいんじゃないかな」

「つまりどういうこと?」

「接続先の入力は、とりあえず手動でいいんじゃないか、ってこと。ユーザーが接続のために必要な情報を事前に入手する感じだよ」

「えっ?そんなんでいいの?」

「まぁ、完全なビットコインではなく、SimpleBitcoinだしね。Edgeノードが利用可能な接続先情報を管理するための中央サーバーを前提にして、そのサーバーを運用し続ける負担を背負うことに比べれば、ずっとマシなんじゃないかな?」

「いわれてみれば、そりゃそうか」

2.2.2 接続先のCoreノードを信用できるか

「でも、Edgeノードの接続先って、本当に1つのCoreノードでいいのかな?」

「なんでそう思うんだい?」

「前に『トラストレス』って話があったよね。トラストレスという観点で考えると、接続先のCoreノードを無条件に信頼するのはダメなんじゃないかって気がするんだけど」

「いい視点だ。セキュリティの話だね。間違いなくいい視点ではあるんだが……」

「……だが、なに?」

「それを真面目に解決しようとすると、まったくシンプルではなくなってしまうんだよね。たとえば、Edgeノードが接続先のCoreノードから取得できるブロックチェーンの情報に、嘘が混じっている可能性について心配するとしよう。君が疑問に思ったとおり、接続先が1つのCoreノードしかないと、この可能性を排除できない。複数のCoreノードから情報をもらう形にして、それらを比較できるようにすれば、ひとつの解決策になる。でも、その複数のCoreノードの情報ってのは、どうやって入手するんだろう?」

「Coreノードが信頼できない前提なら、そのCoreノードからもらうP2Pネットワーク上の他のノードの情報だって、やはり信用しちゃいけないわけか。かといって、たとえばどこかの掲示板で複数のCoreノードの情報を探してきたとして、それが信用できるかといえば、そんなわけもない、と」

「そのとおり」

「だったら、そもそも、CoreノードにEdgeノードが接続するっていう方式を取らなければいいのでは。参加者が自分でCoreノードを用意する前提にしてしまうのがいいんじゃない?」

「一見すると、そう思えるよね」

「その言い方だと、それじゃダメってこと?」

「さすがに、自分で立てているCoreノードなら信頼できる。それは正しい。ただ、自分が立てるCoreノードは、どこか他のCoreノードに接続することで、はじめてP2Pネットワークに参加できる。じゃあ、そのときに接続する先のCoreノードの情報は、どうやって信用するに値すると判断できるだろう。自分が立てるCoreノードにしたって、結局はP2Pネットワークに接続するにあたって同じような問題に直面するだろう?」

「たしかに、Coreノードを自前にしたからといって、何も解決してないな」

「そう。いまはとりあえず、『Coreノードとして信頼可能なものの一覧が、コミュニティなど何らかの方法で定期的にメンテナンスされている』といった前提を置くことにしよう。何らかの方法でメンテナンスされた情報を信頼してるという時点で、厳密にいうとこれは『トラストレス』じゃない。しかし、SimpleBitcoinではシンプルさを優先しよう。実をいうと、オリジナルのビットコインにしたって、やっていることはこれと大差ない」

「そうだね。Coreノード同士で認証し合うみたいな話になると、どんどん複雑化していって、いつまでたっても暗号通貨の話にまで進まなそうだ」

「Coreノードが別のCoreノードを何らかの方法で信頼できるという前提にしておけば、Edgeノードが信頼できるCoreノードがないから自分でCoreノードを立てる、といった議論にも意味がなくなる」

「そりゃそうだね」

「念のため強調しておくと、各参加者が自分でCoreノードを運用すること自体に意味がないわけではない。セキュリティを理由にCoreノードを立てることに意味がないといっているだけだから、そこは間違えないように」

「Coreノードを立てる意味がない、なんて話になったら、Serverを提供する人にとって価値が見出せるという、SimpleBitcoinのそもそもの前提が成立しなくなるものね。ちゃんと覚えてるよ」

2.2.3 CoreノードとEdgeノードの仕様まとめ

この節では、本書で実装する暗号通貨SimpleBitcoinの基盤となるP2Pネットワークについて、次のような仕様を確認しました。

  1. EdgeノードはCoreノードにぶら下がる形で存在する
  2. 1つのCoreノードに対して複数のEdgeノードが接続可能とする
  3. Edgeノードが接続する先のCoreノードは、あらかじめ何らかの方法で用意されたCoreノードの一覧から、Edgeノードのユーザーが自分で選択して入力する
  4. Coreノードを新規にP2Pネットワークに接続したい場合も、3と同じCoreノードの一覧から、Coreノードのユーザーが自分で選択して入力する

次節では、これらの仕様を前提として、CoreノードとEdgeノードを実装していきます。

2.3 P2Pネットワークの実装

2.3.1 プロトコルを考える

「実装にあたり、まずは、CoreノードとEdgeノードが接続されて図2.4のようなP2Pネットワークの形になるまでに、どういうステップを踏めばいいかを考えてみよう」

「えっと、とりあえず、拠り所となるCoreノードが存在していることを前提とするね。そのCoreノードに、別のCoreノードが接続する。そういう接続が次々に起こって、Coreノード同士のネットワークができる。その中で信用可能と思われるCoreノードをEdgeノードが選択し、そこに接続する」

「そうだね。じゃあ、それを絵に描いてみよう」

図2.6のような流れになると思う」

P2P Networkの成長過程

図2.6: P2P Networkの成長過程

  1. 拠り所となるCoreノードを起動する
  2. 1で起動したCoreノードに対し、別に起動したCoreノードが接続していくことで、Coreノードのネットワークが生まれる
  3. 2で形成されたネットワークから、Coreノードを1つ選択し、Edgeノードが接続する
  4. 2および3を繰り返すことで、CoreノードおよびEdgeノードからなるネットワークがどんどん大きくなっていく

「いいんじゃないかな。では、この図2.6を見ながら、CoreノードとEdgeノードがそれぞれどんな機能を持っていればいいかを考えてみよう。まず、2つめのステップで必要になる機能は何かな?」

「すでに起動済みのCoreノードに対して接続を依頼し、それを受け付けてもらう必要があると思う」

「それだけ?」

「えっと、Coreノードは接続する相手がどんどん増えていくから、最初のCoreノードに接続が完了したタイミングで、そこに接続されている既存の他のCoreノード群の情報を教えてもらう必要があるかな」

「厳密にいえば、初回接続時だけじゃなく、Coreノードが追加されるタイミングで常に通知してもらう必要があるけどね」

「なるほどたしかに」

「ほかにはどうだろう?」

「あとは、接続依頼と対になる形で、ネットワークから離脱したい場合にそれをネットワークに伝える機能も必要になると思う。離脱するときにお行儀よく離脱メッセージをくれる相手ばかりとも限らないだろうから、定期的に接続状況の確認ができるような機能も必要かなぁ。Ping的なやつ」

「Pingに返事がなかったらP2Pネットワークから離脱したものとして扱うことにしよう、というわけか」

「そうそう」

「じゃあ、次は3つめのステップ、Edgeノードの追加について考えてみようか」

「これは簡単だね。CoreとEdgeの区別をつける必要はあるけど、基本的にはCoreノードとほぼ同じで、接続要求と離脱要求が処理できればいい。他のCoreノードに接続されているEdgeノードについては情報を知る必要ないから、それくらいかな。念のため、Edgeノード自身が接続しているCoreノードの生存確認と、死んでしまっているときに他のCoreノードにぶら下がり先を引っ越せるようにCoreノードの一覧は持てるようにしておいたほうがいいかも」

「Coreノードの生存確認の方法と、Coreノードの一覧をもらう方法については、CoreとEdgeで区別をつける必要はないだろうね」

「うん。以上をまとめると、CoreノードとEdgeノードに必要な機能はこんな感じになると思う」

Coreノードに必要な機能

図: Coreノードに必要な機能

Edgeノードに必要な機能

図: Edgeノードに必要な機能

「いいね。もちろん、接続した後でメッセージを送信しあうためにP2Pネットワークを構成するわけだから、このほかにメッセージ送受信機能が必要になる。とはいえ、接続管理に関するものとしては、ここにまとめた機能で十分だろう。次は、これらの機能を使ってノードをどう実装すればいいか、少し詳しく考えてみよう」

2.3.2 Coreノードの動作

「Coreノードの動作から考えるね。まずは、始原となるCoreノードに自分の接続先情報を送信し、それを登録してもらうことによって、他のノードからも接続可能なCoreノードとなる。それから、EdgeノードなりCoreノードなりが接続してくるのを待って、そのときがきたら送られてきた情報に応じて処理を実行して返す、という感じかな」

「だいたいあってる。Coreノードの動作を絵としてまとめると、図2.7のようになる」

Coreノード同士が接続するまでの流れ

図2.7: Coreノード同士が接続するまでの流れ

「ん?ソケット?」

「そう。各ノード間のデータのやり取りにはソケット通信を使うことにする。Server SocketClient Socket、この2つのイメージを掴むために、まずは簡単な通信プログラムを書いてみるところから始めてごらん」

図2.7によると、Server Socketは、いったん開いたら他のノードからのデータを受け取るためにずっと口を開けておくんだね。一方、Client Socketは、送信が必要なデータがあるときだけ暫定的に開き、送信が終わって役目を果たしたら使い捨てられる。そんなイメージであってる?」

「そのとおり。というわけで、まずはServer Socketを開いて待ち受ける側の簡易サーバーと、その待ち受けているServer SocketにClient Socketを使って接続してデータを送信する簡易クライアントを作ってみよう」

「開いているソケットにデータを送信すればいいだけだから、クライアント側は簡単だね!せいぜい気をつけることがあるとすると、文字列の場合そのまま送信はできないからUTF-8でエンコードすることくらいかな……」

リスト2.1: client.py

    import socket

    my_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    # ここは環境に合わせた接続先を入れる
    my_socket.connect(('10.1.1.27', 50030))
    my_text = "Hello! This is test message from my sample client!"
    my_socket.sendall(my_text.encode('utf-8'))

「次はサーバーのほうを考えてみよう」

「うーん、クライアントから接続するのだから、クライアントにわかる接続先の名前が必要だよな。ソケットプログラムの解説にあるgethostname()を使えばいいのかな」

「利用するユーザー側の環境制限が少ないほうがいいから、IPアドレスで接続できるようにしたほうがいいんじゃないかな。ちなみに、自分で自分のIPアドレスの確認ができる人には無用の機能だが、Googleの提供しているパブリックDNSサーバーを指定してs.connect(("8.8.8.8", 80))のようにすると、s.getsockname()[0]でIPアドレスが取得できる」

「なるほど。これから先、複数のクライアントが接続してくることも考慮して作っとくか」

「実験目的で会社の中といった閉じられた環境で使いたい場合など、Googleのサーバーに接続できない場合も考慮して、IPアドレスの手動入力も可能にはしておくべきだろうね」

「たしかにそれはそうかも」

リスト2.2: server.py

  from concurrent.futures import ThreadPoolExecutor
  import socket
  import os


  def __handle_message(args_tuple):

      conn, addr, data_sum = args_tuple
      while True:
          data = conn.recv(1024)
          data_sum = data_sum + data.decode('utf-8')

          if not data:
              break

      if data_sum != '':
          print(data_sum)


  def __get_myip():
      s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
      s.connect(('8.8.8.8', 80))
      return s.getsockname()[0]


  def main():

      # AF_INET : IPv4 ベースのアドレス体系を使うということ
      # SOCK_STREAM : TCP/IPを使うということ
      my_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

      # 多重接続になってもいいようにスレッドで処理するようにする
      executor = ThreadPoolExecutor(max_workers=os.cpu_count())

      # 開くポート番号は適当に選んだだけ。
      myhost = __get_myip()
      print('my ip address is now  ...', myhost)
      my_socket.bind((myhost, 50030))
      # 同時に接続してくる相手の数。今回はテストなのでとりあえず1
      my_socket.listen(1)

      while True:
          # 接続があるまで待機
          print('Waiting for the connection ...')
          conn, addr = my_socket.accept()
          print('Connected by .. ', addr)
          data_sum = ''
          executor.submit(__handle_message, (conn, addr, data_sum))


  if __name__ == '__main__':
      main()

「さっそく、サーバーを動かしてクライアントから接続してみよう」

  python3 server.py
  my ip address is now ... 10.1.1.27  ← ※①
  Waiting for the connection ...
  Connected by ..  ('10.1.1.27', 55017)
  Waiting for the connection ...
  Hello! This is test message from my sample client!
  • ①:(著者註)あえてイントラっぽいアドレスに変えてあります

「いいんじゃないかな。次は、このサーバーとクライアントで実際にやり取りするメッセージのフォーマットを考えてみようか」

2.3.3 メッセージを定義しよう

「メッセージのフォーマットかー。単に文字列をやり取りするのじゃだめかな」

「それだと必要な情報を取り出すのが大変になる。とはいえ、複雑な情報を扱うわけではないので、シンプルにJSONとかでいいだろう」

「最低でも、このメッセージがどんな種類のメッセージなのかを伝えられないとだめだよね。それと、プロトコルの名前とバージョンくらいが宣言されてれば、受け取ったときに困らないかな。あとは、メッセージの種類に応じて必要になる情報を格納するフィールドが必要だよね」

ノード間でやり取りされるメッセージの構成

{
  'protocol' : 'プロトコル名。今回は simple_bitcoin_protocol',
  'version' : 'バージョンを宣言する。とりあえず0.1.0',
  'msg_type' : 'メッセージの種別を宣言するための識別子を格納する',
  'payload' : 'Coreノードのリスト等、なんらか補助的に値を渡す必要がある場合はココに格納する'
}

「悪くないね。それじゃあ、これまでの話を統合して、まずはメッセージを取り扱うためのクラスを書いてみよう。メッセージの生成と、パースくらいできればいい」

「クラスの名前はMessageManagerでいいかな。必要になりそうなメッセージをすべてMSG_***みたいな名前で定義しておくとして、メソッドとしては、とりあえず、メッセージを組み立てるbuildと、パースするparseだけ書けばいいってことだよね。バージョンとかプロトコル名の不一致みたいな最低限のエラー処理だけは入れておこう」

リスト2.3: MessageManager.py

  from distutils.version import StrictVersion
  import json


  PROTOCOL_NAME = 'simple_bitcoin_protocol'
  MY_VERSION = '0.1.0'

  MSG_ADD = 0
  MSG_REMOVE = 1
  MSG_CORE_LIST = 2
  MSG_REQUEST_CORE_LIST = 3
  MSG_PING = 4
  MSG_ADD_AS_EDGE = 5
  MSG_REMOVE_EDGE = 6

  ERR_PROTOCOL_UNMATCH = 0
  ERR_VERSION_UNMATCH = 1
  OK_WITH_PAYLOAD = 2
  OK_WITHOUT_PAYLOAD = 3


  class MessageManager:
      def __init__(self):
          print('Initializing MessageManager...')

      def build(self, msg_type, payload=None):

          message = {
              'protocol': PROTOCOL_NAME,
              'version': MY_VERSION,
              'msg_type': msg_type,
          }

          if payload is not None:
              message['payload'] = payload

          return json.dumps(message)

      def parse(self, msg):

          msg = json.loads(msg)
          msg_ver = StrictVersion(msg['version'])

          cmd = msg.get('msg_type')
          payload = msg.get('payload')

          if msg['protocol'] != PROTOCOL_NAME:
              return ('error', ERR_PROTOCOL_UNMATCH, None, None)
          elif msg_ver > StrictVersion(MY_VERSION):
              return ('error', ERR_VERSION_UNMATCH, None, None)
          elif cmd == MSG_CORE_LIST:
              return ('ok', OK_WITH_PAYLOAD, cmd, payload)
          else:
              return ('ok', OK_WITHOUT_PAYLOAD, cmd, None)

「いいね。これをベースにして、実際にCoreノードとEdgeノードを作って繋いでみよう」

  • この書籍は無料で各章の半分まで読めます。
  • クラウドファンディングでのご購入、電子版をご購入のお客様は、全文をお読みいただけます。
  • 閲覧にはご購入されたアカウントでのログイン が必要です。
  • 製本版をご購入の方は別途電子版のご購入 が必要となります。