Web系のプログラマーでもわかるP2Pアプリケーション

# はじめに

先月、映画『[Winny](https://www.telasa.jp/videos/217441)』を見ました  
Winnyというのは[Peer to Peer](https://ja.wikipedia.org/wiki/Peer_to_Peer)(P2P)型のファイル共有アプリケーションとなっています  

P2Pとは何か──  
Wikipediaからの引用では以下のようになっています 

> P2Pに対置される用語としてクライアント・サーバー方式がある。クライアント・サーバー方式ではネットワークに接続されたコンピューターに対しクライアントとサーバーに立場・機能を分離しており、一般的には多数のクライアントに対してサーバーが一つである。クライアントはサーバーとだけ通信でき、あるクライアントが他のクライアントと通信するにはサーバーを介する必要がある。 
> 
> P2Pではネットワークに接続されたコンピューター同士が対等の立場、機能で直接通信するものである。クライアント・サーバー方式ではクライアント数が非常に多くなると、サーバーおよびその回線に負荷が集中するのに対して、Peer to Peer方式はその構造上、コンピューター機器（以下機器）数が膨大になっても特定機器へのアクセス集中が発生しにくいという特徴がある。 

このような説明になってはいますが、やってる事としては要するに一つのノードがサーバーでありクライアントであるというイメージを持っていただければ良いかと思います  

ということでWeb系プログラマーにお馴染のPHPは単体でサーバーを起動することも出来るのでPHPを使ってhttpで実現するP2Pアプリケーションを作ってみましょう  
僕がLaravel初心者なのでLaravelの勉強もかねてLaravelを導入してみます  

# 成果物

https://github.com/m-m-n/p2p

使い方はREADMEにありますので、詳しい利用方法は省略します  
では中身を見ていきましょう  

## やってること

Dockerでsupervisordを立ち上げてサーバー機能を提供するコマンドと、今回ダウンロード処理は裏で勝手に動かすようにしたので、スケジューラーをキックするためのcronを実行するようにしています  
クライアント機能は別途 `artisan app:client-start` で起動します  

### クライアントについて

- 起動すると `p2p> ` というプロンプトが表示されますので、 `help` と入力すると使い方が表示されます  
    1. P2Pアプリケーションなので特定のサーバーに接続するものではないですが、まずP2Pネットワークに参加しなければならないので、稼働中のノードを登録する必要があります  
これを実現するコマンドが `node-add` となります  
`node-add example.com:8000` のような形で登録します  
ノードが増えると他のノードが所有してるファイルが検索できるようになっていきます  
    2. 自分が共有したいファイルは `share/` 直下に配置したあとに `refresh-share` を実行します  
これにより外部のノードからの検索対象となります  
    3. ファイルを検索する際は `search` コマンドを利用します  
`search サンプル` のように検索します  
検索結果が表示されますが、ハッシュ値がダウンロードする際のキーとなります  
これはデータとして同一のファイルがノードによって同じファイル名となっているとは限らないのでハッシュ値をキーとしています  
    4. ダウンロードする際はハッシュ値をダウンロードキューに追加します  
`download 0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef` のような形で登録します  
ハッシュ値はsha3-512とmd5を組み合わせて160文字としています  
実用性を考えるともうちょっと桁数を抑えられると良いのかもしれません  
もしくは内部でハッシュ値を持つにしてもユーザーが意識しないで使えると良いでしょう  

工夫したポイントとしては `php artisan app:refresh-share` のように単体でも呼び出せる形で作ったものを[ClientStart.php](https://github.com/m-m-n/p2p/blob/main/p2p-src/app/Console/Commands/ClientStart.php)から `Artisan::call()` で呼ぶようにしています  
便利ですねこれ👀  

#### GUIじゃないの？

ごもっともです  
Webサーバーとしても起動しているのでHTMLで画面を作るのも可能ではありますが、外部からの接続とポートを分けて起動しないと外部からクライアントの操作をされてしまうおそれがあります  
画面を実装する際はユーザーが操作する画面はlocalhostからしか見れないようにするかNginxなどのWebサーバーをちゃんと用意すると良いでしょう  
そもそもPHP+httpで実装しないのが良いかもしれません  

### ダウンロードキューについて

crontabがLaravelのスケジューラーをキックしてスケジューラーがダウンロードキューを参照してダウンロード処理を行っています  
スケジューラーは毎分起動するがダウンロード処理が動いている場合は重複で実行はされないようにしています  
こういうのを簡単に出来て便利ですねLaravelさん👀  

途中で失敗した際のレジューム機能も実装しようと思いましたが面倒なのでやってません  
多分[Range Request](https://developer.mozilla.org/ja/docs/Web/HTTP/Range_requests)を利用すれば実現できる  
できなかったらごめん  

ファイルは `share/tmp/` 直下にハッシュ値をファイル名としてダウンロードします  
ダウンロードが完了したらファイルのハッシュ値を算出し、ファイル名と比較してハッシュ値が一致していればダウンロード成功として `share/` 直下にファイル名を変更して移動します  

### サーバー機能について

サーバー機能としては以下の機能を実装しています  
- ノードの存在確認  
    - クライアントから見るとレスポンスが返ってくればノードが存在していると判断する  
    - クライアントのノードをサーバー側にも登録してもらうためにポート番号をパラメーターに与える  
- ノードリストの提供  
    - リクエストを受けたノードが把握してるノードの情報をランダムで10件返してやる  
- 検索結果の提供  
    - 検索リクエストの結果、自分が保有している、もしくは自分から見えるノードが保有している場合にファイルの情報を返す  
- ファイルの提供  
    - ハッシュ値に該当するファイルをクライアントに返す  
    - ファイルを持っていない場合には別なノードからダウンロードを行い中継する  

機能としてはシンプルな話かと思います  
これらをhttp通信でリクエストを受け、レスポンスを返します  
そのためクライアント側からもGuzzleを利用しリクエストを投げるというシンプルなhttpリクエストのコードで実装しています  

# まとめ

以上により、PHP(Laravel)とhttpでのP2Pアプリケーションによるファイル共有が実現できることがわかりました  

なおWinnyでは著作権の侵害に当たるファイルを共有していたユーザーが逮捕されています  
当アプリケーションを利用する場合においても著作権などに最大限留意し決して悪用しないようお願いいたします  
http通信なのでパケットの復元は容易です  

今回、Webの技術でP2Pアプリケーションを構築できることを実証するためのサンプルとしてプログラミングしてみましたが、実用性のあるP2Pアプリケーションを実装する場合は自前でソケットプログラミングして専用のプロトコルを設計したほうが良いとは思います  
もし今後、僕が本格的にGUIを用意したP2Pアプリケーションを実装するのであれば、現時点では[TAURI](https://tauri.app/)を利用する可能性が最有力です  

---

それでは良いP2Pライフを