AWS SDK for PHP から AWS SQSを使ってみた
はじめに
最近話題のマストドンのStreaming APIを叩いて、特定のトゥートに含まれる文字列を別な何かに提供したい!!
という要望を叶えるために AWS SQS を使うことになった。
ここでは、メモも含めてここにまとめていく。
意外と文献がなかったので、実践では標準キュー、FIFOキューの両方についてコードを書く。
AWS Simple Queue Service とは
SQS とは、これ。
メッセージキューをクラウド化したもの、キューをいくつも作る事ができてそれぞれ独立して管理できる。
一時的にデータを蓄えて置くのにはかなり最適で、非同期サービス対サービスのような今回の状況にかなり適している。
またキュー自体にも2種類あり、標準のキューとFIFOキューがある。
多分キューと聞いて思い浮かべるのは後者だと思う。
スタンダードキュー
スタンダードキューには大きく3つの特徴がある。
メッセージの順序
スタンダードキューにおけるメッセージの順序は特徴的な部分がある。通常キューと言われるとFIFO(First-In First-Out)を考えると思うがこのキューでは順番がなるべく保持されるだけで必ずしもFIFOを実現できるものではないと言うこと。
そのため、各メッセージに順序をつけてDBなどに突っ込むなどしてメッセージを受け取ったときにソートする必要がある
一回以上の配信
ドキュメントにもあるが、SQSは冗長性と可用性を確保するために、メッセージが複数のサーバに保存される。
それが影響して、メッセージの受信と削除のときにまれに同一のメッセージのコピーを処理してしまい想定される挙動と異なる挙動を取る場合がある。
そのため、それらを考慮して処理を設計する必要がある。
ショートポーリングを使用したメッセージ処理
SQSはデフォルトでショートポーリングを使用する。ショートポーリングとはポーリングされたキューが空であろうとなかろうとすぐに応答を返すことを指す。
反対にロングポーリングはキューに達するかロングポーリングがタイムアウトになるまで応答を返すことがない。
ショートポーリングを使用した場合、サーバのサブセットがサンプリングされるためそれらのサーバにあるメッセージだけ返される。
そのため、受信リクエストによっては全てのメッセージを受け取ることができない、というかそもそもサーバがわが持っていいない場合がある。
これは、サーバにデータをコピーしていることとそれらのサーバが分散システムとなっていることが要因となっている。
AWS SDK for PHP による実践
まず共通する環境について。
PHPは以下のバージョンを使う。
$ php -v PHP 7.2.8-1+ubuntu16.04.1+deb.sury.org+1 (cli) (built: Jul 25 2018 10:51:50) ( NTS ) Copyright (c) 1997-2018 The PHP Group Zend Engine v3.2.0, Copyright (c) 1998-2018 Zend Technologies with Zend OPcache v7.2.8-1+ubuntu16.04.1+deb.sury.org+1, Copyright (c) 1999-2018, by Zend Technologies with Xdebug v2.6.0, Copyright (c) 2002-2018, by Derick Rethans
composer を使って、以下のパッケージを持ってくる。
$ composer require aws/aws-sdk-php
また以下のディレクトリにcredential、つまり認証情報を以下の形式でいれる必要がある。
このあたりは、次回のIAM関連についての記事と同じタイミングで書く。
$ cat $HOME/.aws/credential [default] aws_access_key_id = ACCESS_KEY aws_secret_access_key = SECRET_KEY
標準キューとFIFOキューの作り方に関してはコンソールから作るだけなので省略。
標準キュー(スタンダードキューの実践)
メッセージの送信
<?php require_once 'vendor/autoload.php'; use Aws\Sqs\SqsClient; use Aws\Exception\AwsException; define('QUEUE_URL', 'url'); try { $client = new SqsClient([ 'profile' => 'default', 'region' => 'us-east-1', 'version' => 'latest', ]); $post_params = [ 'DelaySeconds' => 0, //ここに入れた秒数だけキューから値を取り出せない 'MessageAttributes' => [ 'Title' => [ 'DataType' => 'String', //binaryを指定するとbase64 encodeでバイナリを与える 'StringValue' => 'Amazon SQS Test Message', ] ], 'MessageBody' => 'This is AWS SDK for PHP.', //メッセージ本文 'QueueUrl' => QUEUE_URL, ]; $result = $client->sendMessage($post_params); } catch (SqsException $error) { $e->getMessage(); }
メッセージの受信と削除
<?php require_once 'vendor/autoload.php'; use Aws\Sqs\SqsClient; use Aws\Exception\AwsException; define('QUEUE_URL', 'url'); try { $client = new SqsClient([ 'profile' => 'default', 'region' => 'us-east-1', 'version' => 'latest', ]); $get_params = [ 'AttributeNames' => ['All'], 'MessageAttributeNames' => ['All'], //All指定部分は取得したい属性を設定する 'MaxNumberOfMessages' => 5, //一度に受け取るメッセージの最大数 'QueueUrl' => QUEUE_URL, 'WaitTimeSeconds' => 20, 'VisibilityTimeout' => 60, ]; $result = $client->receiveMessage($get_params); var_dump($result); $data = $result->get('Messages'); if ($data) { foreach($data as $item) { echo $item['Body']."\n"; //受け取ったら削除しないとデータは残る $client->deleteMessage([ 'QueueUrl' => QUEUE_URL, 'ReceiptHandle' => $item['ReceiptHandle'], ]); } } } catch (SqsException $error) { $e->getMessage(); }
FIFO キュー
FIFOキューはイベントの順番が重要になる場合や重複不可な時に使うと良いらしい・
というのもメッセージ順序が大きく関係している。
メッセージの順序
FIFOキューでは配信は一回だけの処理とされる点が標準キューと大きな違いがある。
メッセージの送信、受信の順序は厳密に守られコンシューマが削除するまで使用可能状態で残り続ける。
また、キューの状態で重複は導入されていない・
重要な用語
いくつか、SQS FIFOを使ううえで重要な用語がある。(メッセージグループなど)
それは以下のサイトで詳解されていたので省略。
Amazon SQS FIFO (先入れ先出し) キュー - Amazon Simple Queue Service
メッセージの送信
<?php require_once 'vendor/autoload.php'; use Aws\Sqs\SqsClient; use Aws\Exception\AwsException; define('QUEUE_URL', 'url'); try { $client = new SqsClient([ 'profile' => 'default', 'region' => 'us-east-1', 'version' => 'latest', ]); $client->sendMessage([ 'QueueUrl' => QUEUE_URL, 'MessageBody' => 'hello,SQS!!', 'MessageGroupId' => 'test', 'MessageDeduplicationId' => time(), ]); } catch (SqsException $error) { $e->getMessage(); }
メッセージの受信と削除
<?php require_once 'vendor/autoload.php'; use Aws\Sqs\SqsClient; use Aws\Exception\AwsException; define('QUEUE_URL', 'url'); try { $client = new SqsClient([ 'profile' => 'default', 'region' => 'us-east-1', 'version' => 'latest', ]); $result = $client->receiveMessage([ 'MaxNumberOfMessages' => 1, 'QueueUrl' => QUEUE_URL, ]); foreach ($result->search('Messages[]') as $message) { $queueHandle = $message['ReceiptHandle']; echo $message['Body']; $client->deleteMessage([ 'QueueUrl' => QUEUE_URL, 'ReceiptHandle' => $queueHandle, ]); } } catch (SqsException $error) { $e->getMessage(); }
おわりに
SQSはシンプルなメッセージをアプリケーション間で共有する際に便利で
特にFIFOを様々な配信サーバの一つの機能として利用できる。
Laravelの動く環境をdocker-compose(PHP 7.2 + nginx + MySQL)でいい感じにする
はじめに
Laravelを
$ php artisan serve
で動かすのにすごくモヤモヤしていたのでdocker-compose をつかってLaravelのプロジェクトが動くインフラ(PHP 7.2 + nginx + MySQL)を構築してみたからまとめる。
ディレクトリ構成は以下の感じになった。
$ tree -L 1 . ├── LICENSE ├── README.md ├── app ├── artisan ├── bootstrap ├── composer.json ├── composer.lock ├── config ├── database ├── docker ├── docker-compose.yml ├── package.json ├── phpunit.xml ├── public ├── readme.md ├── resources ├── routes ├── server.php ├── storage ├── tests └── webpack.mix.js
完全に好みである。
docker-compose
例のファイル(docker-compose.yml)を書く。
version: '3' services: nginx: build: context: ./docker/nginx depends_on: - php ports: - 80:80 volumes: - ./:/src php: build: context: ./docker/php environment: DB_HOST: mysql volumes: - ./:/src mysql: image: mysql:5.7 volumes: - ./docker/mysql:/var/lib/mysql environment: - MYSQL_ROOT_PASSWORD=root - MYSQL_USER=sample - MYSQL_PASSWORD=sample - MYSQL_DATABASE=sample ports: - 3306:3306
見ればわかるようにプロジェクトルートを/srcとして設定している。
コンテナ側では基本的に/srcに入るようになっていてそれを公開する形になっている。
またこの設定ファイルにあるように、docker-composeで使用するコンテナの設定ファイル群は./dockerディレクトリ以下にある。
PHPのDockerfileは以下の通り
FROM php:7.2.8-fpm RUN docker-php-ext-install pdo_mysql mysqli mbstring WORKDIR /src
PHP 7.2を持ってきている。
そしてこれがnginxのDockerfile (./docker/nginx/Dockerfile)
FROM nginx:latest COPY ./default.conf /etc/nginx/conf.d/default.conf
こっちが一番重要なnginxの設定ファイル (./docker/nginx/default.conf)。
今回は、https化まではしていないから結構シンプルになっている。
server { listen 80; server_name _; root /src/public; index index.php; access_log /src/docker/nginx/logs/access.log; error_log /src/docker/nginx/logs/error.log; location / { try_files $uri $uri/ /index.php$is_args$args; } location ~ \.php$ { fastcgi_split_path_info ^(.+\.php)(/.+)$; fastcgi_pass php:9000; fastcgi_index index.php; include fastcgi_params; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; fastcgi_param PATH_INFO $fastcgi_path_info; } }
諸々のパスはコンテナ側に準拠している。
そして一番重要なのがrootを設定すること。
環境を立ち上げる
ログファイルを格納するディレクトリを作る $ mkdir ./docker/nginx/logs MySQLで使用するディレクトリを作る、このディレクトリはignoreしないとgit add できなくなる $ mkdir ./docker/mysql githubで管理した時にignoreされたファイルをなかったら作る $ echo '' >> ./storage/logs/laravel.log 権限を与える $ sudo chmod 777 -R ./storage/ ./bootstrap/ $ composer install Laravelのおまじない $ cp .env.example .env $ php artisan key:generate $ docker-compose up -d
これでLaravelが動く最低限の環境ができた
yarn + webpack を感覚で使っていたので勉強がてら手元に環境を構築してみる
はじめに
今までJavaやScala、Cなど型が割としっかりしている言語を使って開発を続けてきた。
しかし、その経験がweb系の開発を行うときに思わぬ障害になった。
PHPとJSがわからない、あいつら抽象的過ぎてわからないという現象にぶち当たった。
そしてなんだ、こいつらwebpackとyarnってなんや!!ってなったからしっかりやっていく。
yarnとは
yarn はパッケージマネージャの npm の上位互換のようなツールと言われている。
だったら npm でいいじゃん!と思うかも知れないけど npm は色々な問題を抱えているため、 yarn が使われることが多いみたい。
yarn ではダウンロードしたパッケージをキャッシュしているので、一度使ったパッケージはすぐに再利用することができる。
このあたりは composer に似ている。
yarn は npm で管理されているけど、その方法でUbuntuにインストールするのは推奨されていない。
また package.json に対応しているから npm で package.json を使っている場合はすぐに移行できるらしい。
webpackとは
webpack はWebアプリケーションで使用しているリソースの依存関係を解決し、js や css などのアセットを生成するビルドツール。
つまり、1つ以上のモジュールをひとつにまとめたファイルを吐ける。
yarn の導入
Ubuntu 16.04に突っ込むことを前提にする。
しかし apt では入らないので、公式にかいてあるようにコマンドを叩いて突っ込む
$ curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | sudo apt-key add - $ echo "deb https://dl.yarnpkg.com/debian/ stable main" | sudo tee /etc/apt/sources.list.d/yarn.list $ sudo apt-get update $ sudo apt-get install yarn
version 1.7.0の安定版を入れるので
$ yarn --version 1.7.0
と、でればok
webpack の導入
をやろうとしたら、nodejsのバージョンが低いっぽいので先に nvm を入れる。
nvm の導入
次のコマンドを叩いて導入する
$ git clone https://github.com/creationix/nvm.git ~/.nvm $ source ~/.nvm/nvm.sh
それが終わったら、雑に node のバージョンを指定して突っ込む。
$ nvm install 8.11.3 Downloading and installing node v8.11.3... Downloading https://nodejs.org/dist/v8.11.3/node-v8.11.3-linux-x64.tar.xz... ######################################################################## 100.0% Computing checksum with sha256sum Checksums matched! Now using node v8.11.3 (npm v5.6.0) Creating default alias: default -> 8.11.3 (-> v8.11.3)
webpackの導入をやる
適当にディレクトリを作ってそれをプロジェクトとする。
その前に、yarn を使って以下のことをする必要がある。
$ yarn init
これで例の package.json が生成される。
{ "name": "SampleWebpack", "version": "1.0.0", "main": "index.js", "author": "lrf141", "license": "MIT" }
ここまできたら、いよいよ webpack を導入する。
$ yarn add webpack webpack-dev-server --dev yarn add v1.7.0 [1/4] Resolving packages... [2/4] Fetching packages... info fsevents@1.2.4: The platform "linux" is incompatible with this module. info "fsevents@1.2.4" is an optional dependency and failed compatibility check. Excluding it from installation. [3/4] Linking dependencies... [4/4] Building fresh packages... success Saved 2 new dependencies. info Direct dependencies ├─ webpack-dev-server@3.1.4 └─ webpack@4.12.0 info All dependencies ├─ webpack-dev-server@3.1.4 └─ webpack@4.12.0 Done in 3.31s.
これで導入は完了。
{ "name": "SampleWebpack", "version": "1.0.0", "main": "index.js", "author": "lrf141", "license": "MIT", "devDependencies": { "webpack": "^4.12.0", "webpack-dev-server": "^3.1.4" } }
package.json も上記の様に変化している。
次に webpack.config.js という webpack の設定ファイルを書く。
module.exports = { entry: "./src/index.js", output: { filename: "index.bundle.js", path: __dirname + "/build" } }
サンプルのjsファイルを作る。
window.onload = function() { var elem = document.getElementById('sample'); elem.innerHTML = 'I love Jason Statham.'; }
<script src="index.bundle.js"></script> <body> <div id="sample"></div> </body>
実際にビルドしてみる。
$ yarn run webpack-dev-server -- --inline
と、失敗するので yarn を使って webpack-cli を導入する
$ yarn add webpack-cli
{ "name": "SampleWebpack", "version": "1.0.0", "main": "index.js", "author": "lrf141", "license": "MIT", "devDependencies": { "webpack": "^4.12.0", "webpack-dev-server": "^3.1.4" }, "dependencies": { "webpack-cli": "^3.0.8" } }
もう一回やってみる。
$ yarn run webpack-dev-server -- --inline yarn run v1.7.0 warning From Yarn 1.0 onwards, scripts don't require "--" for options to be forwarded. In a future version, any explicit "--" will be forwarded as-is to the scripts. $ /home/rabbitfoot/webProject/SampleWebpack/node_modules/.bin/webpack-dev-server --inline ℹ 「wds」: Project is running at http://localhost:8080/ ℹ 「wds」: webpack output is served from / ⚠ 「wdm」: Hash: 485e1d2511df65a321f5 Version: webpack 4.12.0 Time: 427ms Built at: 2018-06-24 01:12:22 Asset Size Chunks Chunk Names index.bundle.js 139 KiB 0 [emitted] main Entrypoint main = index.bundle.js [2] ./src/index.js 127 bytes {0} [built] [4] (webpack)/hot/emitter.js 77 bytes {0} [built] [6] (webpack)/hot sync nonrecursive ^\.\/log$ 170 bytes {0} [built] [9] ./node_modules/html-entities/index.js 231 bytes {0} [built] [10] ./node_modules/ansi-html/index.js 4.16 KiB {0} [built] [11] (webpack)-dev-server/client/overlay.js 3.58 KiB {0} [built] [12] ./node_modules/sockjs-client/dist/sockjs.js 176 KiB {0} [built] [13] (webpack)-dev-server/client/socket.js 1.05 KiB {0} [built] [14] ./node_modules/loglevel/lib/loglevel.js 7.68 KiB {0} [built] [15] ./node_modules/ansi-regex/index.js 135 bytes {0} [built] [16] ./node_modules/strip-ansi/index.js 161 bytes {0} [built] [19] ./node_modules/querystring-es3/index.js 127 bytes {0} [built] [23] ./node_modules/url/url.js 22.8 KiB {0} [built] [24] (webpack)-dev-server/client?http://localhost:8080 7.75 KiB {0} [built] [25] multi (webpack)-dev-server/client?http://localhost:8080 ./src/index.js 40 bytes {0} [built] + 11 hidden modules WARNING in configuration The 'mode' option has not been set, webpack will fallback to 'production' for this value. Set 'mode' option to 'development' or 'production' to enable defaults for each environment. You can also set it to 'none' to disable any default behavior. Learn more: https://webpack.js.org/concepts/mode/ ℹ 「wdm」: Compiled with warnings.
これで画面に文字列が無事に表示された。
このあと、webpack の設定をいじってビルド周りのコマンドを登録したりして
効率化するべきなんだろうけど、そこら辺は必要になったときに追ってやっていく
Laravelを使いたかったので雑に入門した
はじめに
なぜか急にwebシステムを作る必要がでてきたため、Laravelを使ってみようとおもった。
だが使ったことないので備忘録も兼ねてまとめていこうと思う。
対象
などなど、何が言いたいかというと初心者に向けた記事じゃなくて
自分用のメモ兼ある程度のサーバサイド経験者に共有したい内容となっている
Laravelとは
Laravel は、MVCのWebアプリケーション開発用の無料・オープンソースのPHPで書かれたWebアプリケーションフレームワークのこと。
PHPのwebアプリケーションフレームワークといえば、CakePHPやFuelPHPあたりを想像するだろうが、最近はLaravelが熱いらしい。
Laravelの環境を構築する
composerが入っていることを前提にする。
その上で最初にComposerを使用し、Laravelインストーラをダウンロードする。
composer global require "laravel/installer"
またはcomposerを使ってプロジェクトを作る
composer create-project --prefer-dist laravel/laravel project-name
プロジェクトを作成したらそのディレクトリに移動して、ローカル開発サーバを起動する。
その前にドキュメントによるとアプリケーションキーの設定と設定ファイルのあれこれを行う。
設定ファイルをリネームする
cp -p .env.example .env
そしてアプリケーションキーを作成する。
php artisan key:generate
ここまで完了した段階で、以下のコマンドを実行することで最初のHello,World的なページが表示される。
$ php artisan serve Laravel development server started: <http://127.0.0.1:8000>
ブラウザからlocalhost:8000にアクセスすると…
ここまででとりあえず最低限起動するところまでは完了した。
ルーティング
webアプリケーションを作る上でまずやらないといけないのがルーティング
普段はLeague\RouteをつかってやっているがLaravelではどうおこなうかわからなかったのでまとめてみる。
まずルーティングがどこにあるかというとroutesディレクトリにおいてある。
そうするといくつかのファイルがあると思うが、 web.php がwebインターフェースのルーティングを定義する。
Laravelではルーティング内で毎回App\Http\Controllerを指定しなくて済むようにデフォルトでRouteServiceProviderが設定している。
Laravelで利用できるルート定義メソッドは以下の通り
Route::get($uri, $callback); Route::post($uri, $callback); Route::put($uri, $callback); Route::patch($uri, $callback); Route::delete($uri, $callback); Route::options($uri, $callback);
HTTPのリクエストメソッドに対応している。
引数のタイプは、URIとクロージャーになっている。
複数のHTTPリクエストメソッドを利用したい場合はmatchメソッドを使用して
<?php Route::match(['get', 'post'], '/', function () { // }); Route::any('foo', function () { // });
のように書くこともできる
anyはすべてのHTTPリクエストメソッドに対してルーティングを登録する場合にもちいられる。
また一般的なGETリクエストを使用するルーティングは次のように書かれる
<?php Route::get('/hello', 'HogeController@index');
第一引数については言う必要はないと思うが、問題は第二引数。
呼び出すコントローラのメソッドを@で指定している。
また、単純にViewを表示するだけなら以下のようにも書ける。
<?php Route::view('/welcome', 'welcome'); Route::view('/welcome', 'welcome', ['name' => 'Taylor']);
シンプルなルーティングはこれでかけるがもう少し詳しく書きたい場合がある。
そういう時にルートパラメータは次のように書ける。
<?php Route::get('user/{id}', function ($id) { return 'User '.$id; }); Route::get('posts/{post}/comments/{comment}', function ($postId, $commentId) { // });
パラメータ名の後ろに?をつけると任意パラメータになる。
<?php Route::get('user/{name?}', function ($name = null) { return $name; }); Route::get('user/{name?}', function ($name = 'John') { return $name; });
さらに詳しいことは公式ドキュメントに書いてある。
ルーティング 5.6 Laravel
リクエスト周り
LaravelでHTTPリクエストのインスタンスを利用するには
Illuminate\Http\Requestクラスをコントローラのメソッドに指定する必要がある
ひとまず、以下のクラスとルーティングを前提に説明していく
<?php namespace App\Http\Controllers; use Illuminate\Foundation\Bus\DispatchesJobs; use Illuminate\Routing\Controller as BaseController; use Illuminate\Foundation\Validation\ValidatesRequests; use Illuminate\Foundation\Auth\Access\AuthorizesRequests; use Illuminate\Http\Request; class SampleController extends BaseController { public function index() { return 'hello,world'; } public function sampleRequest(Request $request) { $name = $request->name; return $name; } public function sampleRequestWithParam(Request $request, $val) { return $id; } }
<?php /* |-------------------------------------------------------------------------- | Web Routes |-------------------------------------------------------------------------- | | Here is where you can register web routes for your application. These | routes are loaded by the RouteServiceProvider within a group which | contains the "web" middleware group. Now create something great! | */ Route::get('/', function () { return view('welcome'); }); Route::get('hello', function() { return 'Hello,World'; }); Route::get('/sample', 'SampleController@index'); Route::get('/sample2/{name}', 'SampleController@sampleRequest'); Route::get('/sample3/{val}', 'SampleController@sampleRequestWithParam');
このコードにおいて、 パラメータの値を取得する方法は2つある。
ひとつ目が、
<?php $name = $request->name;
上記の部分。
こちらは動的プロパティを参照する方法となる。
ふたつ目が
<?php public function sampleRequestWithParam(Request $request, $val) { return $id; }
の引数部分。
プロパティ名を変数として宣言するとそちらに直接代入される。
プロパティだけでなく、URL全体を取得するのは次のように行う。
<?php // クエリストリングなし $url = $request->url(); // クエリストリング付き $url = $request->fullUrl();
クエリストリングから値を取り出したい場合は以下のようにする
<?php $name = $request->query('name');
またこのRequestクラスは、PSR7を満たしている以下のパッケージで置き換えることができる
composer require symfony/psr-http-message-bridge composer require zendframework/zend-diactoros
その他、リクエストまわりは先程と同様に公式に全てまとまっている
HTTPリクエスト 5.6 Laravel
ビュー
LaravelのViewはBladeを使用している。
Bladeは .blade.php という拡張子でテンプレートを用意し
サーバサイドから呼び出しとデータを渡して表示する。という手順で利用する
//sample_view.blade.php <html> <body> <h1>Hello, {{ $name }}</h1> </body> </html>
ルーティングでは以下のように直接viewを呼び出す方式を今回は取ってみる。
<?php Route::get('/sample4', function (){ return view('sample_view', ['name' => 'scala']); //view('sample_view')->with('name', 'scala');でも同様に行える });
雑感だけど、League/Plateと似ている
諸々のテンプレートエンジンとほとんど使い方自体は同じ
コントローラ
コントローラはapp/Http/Controllers以下のディレクトリに配置する。
namespaceはPSR準拠のものを設定する。
ドキュメントに乗っているサンプルコントローラは以下の通り
<?php namespace App\Http\Controllers; use App\User; use App\Http\Controllers\Controller; class UserController extends Controller { /** * 指定ユーザーのプロフィール表示 * * @param int $id * @return Response */ public function show($id) { return view('user.profile', ['user' => User::findOrFail($id)]); } }
このように、コントローラクラスを継承し拡張することでコントローラは作成できる
メソッドではURLパラメータを受け取ることも、PSR7準拠のリクエストを受け取ることも出来る。
ルーティングと関連するところはコントローラの名前空間で、ルーティングではApp\Http\Controllerまでは自動で設定されているので
それより深いnamespaceを持つ場合は適宜指定しないと行けない。
シングルアクションコントローラでは__invokeメソッドを使用することができる。
この場合のルーティングは クラス名@アクションメソッド ではなく、 クラス名 となる。
モデル
モデル関連は公式ドキュメントに情報が少ない。
それもそれで何故かわかった。
そもそも、MVCのモデルは開発者やベンダーで割と作り方がバラバラでそれに対応するためにLaravelでも自由に作れるようになっているとあった。
たしかに、コントローラやビューほどある一定の形式を保つ必要が無いからその理由に同意できる。
Laravelではモデルはartisanコマンドをつかって作る
それらは以下のサイトが参考になった。
udemy.benesse.co.jp
PHPでheaders already sentを解消するためだけに雑にテンプレートエンジンを作った話
はじめに
どうもよく訓練されたJavaer、けんつです。
PHPの勉強がてらオレオレTwitterクライアントを作ろうとしていたら次のエラーにぶち当たった。
Warning: Cannot modify header information - headers already sent by (output started at /some/file.php:12) in /some/file.php on line 23
調べると
このエラーは HTTP ヘッダーを変更する関数(後述)の呼び出しより前に、すでに何かがアウトプットされているために発生します
とのことで、ならMVCモデルのViewでレンダリングするときはどうすれば良いのだろうかと疑問に思った。
今回はそれを理解するためだけに、最小構成オレオレテンプレートエンジンを雑に作ってみたのでそれについて書いて見ようと思う。
2018/05/13の段階でとりあえずレンダリングはできる状態にあるが、細かい所や設計面を作りこめていないので今後改良していく。
composerをつかっている環境であれば以下のコマンドで導入できる。
$ composer require lrf141/rook
エラーの原因と解決策
まず原因はHTTPヘッダーを変更する関数をコールするまえに、何かしらを出力していることである。
なのでよくある解決策としてはレンダリングしたいものを
<? ob_start();
などのように内部バッファに溜め込んで最終的にヘッダーに追加することが挙げられる。
もしくは、サーバ側の設定でアウトプットバッファリングを有効にすることでも回避できる。
今回は前者でやってみる。
テンプレートエンジンの実装
今回自作したテンプレートエンジンのソースは次のような構成になっている。
./src/ ├── Directory.php ├── Engine.php ├── FileExtension.php ├── Name.php └── Template.php
何をやったかというと、雑に説明するなら
テンプレート内で使用したいデータ群を配列として受け取ったらそれを変数として展開し
テンプレートとして利用するファイル(.php)をテンプレートエンジン内でincludeしてレンダリングするというもの。
その過程で、 バッファを利用している。
まず基礎となるEngineクラスは次のように実装した。
<?php namespace Lrf141\Rook; class Engine { /** * Default template directory * @var Directory */ private $directory; /** * Template file extension * @var FileExtension */ private $fileExtension; /** * create new Engine Instance * @param string $base_dir * @param string $ext file extension */ public function __construct(string $base_dir, string $ext = 'php') { $this->directory = new Directory($base_dir); $this->fileExtension = new FileExtension($ext); } /** * generate Template * * @param string $name template name * @return Template * */ public function make(string $name): Template { return new Template($this, $name); } /** * render based on template * * @param string $template template name * @param array $data wanna use in template * @return string rendering result */ public function render(string $template, array $data = []): string { return $this->make($template)->render($data); } /** * get path to templates Directory * @return Directory */ public function getDirectory(): Directory { return $this->directory; } /** * get templates file extension * @return FileExtension */ public function getFileExtension(): FileExtension { return $this->fileExtension; } }
このクラスのインスタンスを生成した時点で基準となるディレクトリと拡張子(ここではphp)を指定してそれらを管理するクラスのインスタンスを保持している。
Engineクラスのインスタンスからrenderメソッドをコールするとテンプレートが生成されて、レンダリング結果のhtmlがstringで返ってくる仕組みとなっている。
諸々の関連クラスのあれこれは省略するとして、一番重要なのがTemplateクラス。
<?php namespace Lrf141\Rook; use LogicException; use Throwable; use Exception; class Template { /** * @var Engine */ private $engine; /** * @var Name */ private $name; /** * @var array */ private $sections = array(); /** * @var array */ private $data = array(); /** * generate new Template Instance * @param Engine $engine * @param string $name */ public function __construct(Engine $engine, string $name) { $this->engine = $engine; $this->name = new Name($name); } /** * rendering based on template * @param array $data * @return string */ public function render(array $data = []): string { //expands array as var $this->data($data); unset($data); extract($this->data); try { $level = ob_get_level(); //dump buffer ob_start(); include $this->path(); $content = ob_get_clean(); return $content; } catch (Throwable $e) { while (ob_get_level() > $level) { ob_end_clean(); } throw $e; } catch (Exception $e) { while (ob_get_level() > $level) { ob_end_clean(); } throw $e; } } /** * add data * @param array $data * @return array|none */ public function data(array $data = null) { if (is_null($data)) { return $this->data; } $this->data = array_merge($this->data, $data); } /** * generate path * @return string */ public function path(): string { $dir = $this->engine->getDirectory()->get(); $ext = $this->engine->getFileExtension()->get(); $name = $this->name->get(); $path = $dir . '/' . $name . '.' . $ext; if (!$this->isExist($path)) { throw LogicException( `the template path "` . $path . '" does not exist.' ); } return $path; } /** * check the path is true * @param string $path * @return bool */ public function isExist(string $path): bool { return file_exists($path); } }
この中で重要なのがrenderメソッドで、次の部分で配列を変数として展開している。
<?php //expands array as var $this->data($data); unset($data); extract($this->data);
特にextract関数がそれをやっている。
その後に
<?php ob_start();
で、出力で内部バッファの利用を開始する。
その後に
<?php include $this->path(); $content = ob_get_clean();
指定したパスにあるphpファイルをincludeして
ob_get_clean();でバッファの内容をstringで取得して、クリアしている。
これによって、直接出力しなくてもバッファに溜め込んで取得できるため上記のエラーが起きなくなる。
ただ最終的には取得した内容を吐き出す必要が出てくるのでその段階で工夫がまた必要にはなってくる。
それはPSR-7で制定されているHTTPメッセージ辺りの扱いを上手くやっていく必要があるのだと思う。
ただ出力を乱立させないで、テンプレートエンジンを利用することである程度まとめられるのでエラーは起こしにくくなるだろう。
おわりに
エラーを潰すためにこれだけやったがやったことをざっくりとまとめると内部バッファを使っただけなので
その辺り、PSR-7辺りを上手く使って行こうと思う。
PHPでLeague/Routeを使ってルーティングを構成する
はじめに
どうもよく訓練されたJVM教のけんつです。
JVM環境でしか使い物にならないと言われてきた自分がWebサーバサイドのコードをPHPで書く機会がここ半年ほどで多くなってきた。
それでも、Webやったことないしわからんぞ!という感じなので素のPHPでオレオレTwitterクライアントを書く事にした。
とりあえず、イカしたdocker-composeを適当に引っ張ってきてイマドキのなうでヤングな環境を構築するところまではやった。
結構順調だったので、このまま最小構成を作ってしまおうとした矢先に自分を苦しめる存在が出てきた。
そうそれこそが
「素のPHPでルーティングどうやんの?」
という疑問である。
とりあえず、「PHP ルーティング」でぐぐってみると
- apache 環境では…。いや使ってるのnginxだし!!
- Cakeでルーティングはこうする…。 いや素のPHPで書きたいんだ!!
- Laravelではこうする…。 だからフレームワークは使わんと言った!!
- なかったから自作したお。 わけわからん!!
といった調子になった。
それでもいくつか、FastRouteのような使えそうなライブラリを見つけた。
しかし、以前Play Frameworkで色々やっていた自分に取ってみれば「書く事が多すぎてだるい!!」に尽きる。
それでも探し続けて、やっと「League/Route」なるものを見つけた。
色々とサンプルをみて、「これええやん!」となったが問題はPHPに浸かっている人間でなかったのでわかりにくいというか何をやっているかわからないという個人的な点にあった。
なので、ここでは自分なりに頑張って色々調べてみて「League/Route」を使うための公式にあるサンプルが何をやっているのかまとめてみようと思う。
環境
まずPHPのバージョンだが、Routeは5.4以降に対応しているので大抵の環境で動作するだろう。
Routeを使うにはcomposerかautoloadで持ってくるのを前提にしている。
自分はPHPを書くときはcomposerを使うのでそちらを参考にする。
$ composer require league/route
またこのライブラリはPSR-7を前提にしているのでそちらも解決する。
composer require zendframework/zend-diactoros
あとの文章に出てくるLeague\Containerでは2.x系のものを使用している。
こちらはバージョンによって仕様がかなり違うので注意が必要。
と環境はここまでで構築できるはず。
ルーティングを構成する基本要素
環境を構築したら、次のようにコードを書けばルーティングが実装できるよ。と公式には書いているが
<?php use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; $container = new League\Container\Container; $container->share('response', Zend\Diactoros\Response::class); $container->share('request', function () { return Zend\Diactoros\ServerRequestFactory::fromGlobals( $_SERVER, $_GET, $_POST, $_COOKIE, $_FILES ); }); $container->share('emitter', Zend\Diactoros\Response\SapiEmitter::class); $route = new League\Route\RouteCollection($container); $route->map('GET', '/', function (ServerRequestInterface $request, ResponseInterface $response) { $response->getBody()->write('<h1>Hello, World!</h1>'); return $response; }); $response = $route->dispatch($container->get('request'), $container->get('response')); $container->get('emitter')->emit($response);
ここらへんを全てフレームワークに投げていた人にはよく分からない。
なので、該当するPSRなどを読んで理解していく。
まず、先頭にあるこのコード
$container = new League\Container\Container;
League\Containerを呼び出しているのだが、ここで指すコンテナってなんぞ?とおもったので調べてみた。
Dependency Injection Container (PSR-11)
DIコンテナと言われるものでPSR-11内で決定されている。
DIコンテナと言われても分からない。なので調べた。
PSR-11については以下のページが
qiita.com
DIについては以下のページが個人的に分かりやすかった。
qiita.com
そもそもDIとは
つまりこれは何かと言われると、例えば以下の処理をしようとする。
<?php class Sample { private $engine; public function __construct() { $this->engine = new Engine(); } }
ここではnew Engineとしたがこれはテンプレートエンジンでもいいし、エンジンでなくてもいい。
依存性を直接コンストラクタやメソッドのスコープ内で生成してしまうコードを指す。
これらは上記のqiita記事でも書かれているが
上のコードだと何が嬉しくないというと
- Engineクラスを別のクラスに変えたりするとSampleクラスを直接変更しなければならない。
- Engineが上手く動作してくれないとそもそも、このクラスのテストが出来ない。
という点にある。
それを次のようにすると嬉しいことがある。
<?php class Car { /** * @var EngineInterface */ private $engine; /** * @param EngineInterface $engine */ public function __construct(EngineInterface $engine) { $this->engine = $engine; } }
何が嬉しいかというと、依存性が外部で完結しこちらのクラスではそれを受け取っているだけなので変更も容易でテストもしやすい。
しかしこれも残念な点を抱えている。
何かというと色んな依存関係を解消しようとすると引数がめちゃくちゃ長くなりやすいという点。
それと結局依存を注入する手続きは手動で行っており,抽象ではなく具象に対して依存するコードを書くことになるという点。
それらの問題を解決するためにDIコンテナが存在する。
DIコンテナとは
DIコンテナについては以下のサイトがわかりやすくまとまっていた。
qiita.com
つまり、今まで抽象っぽくなってたけど結局具象な上に手動で行っていたものを別な場所(DIコンテナ)に突っ込んで管理しよう。というのを実現するためにある。(らしい)
今回の環境ではLeague/Containerを使っているので公式から使用例を引用してくる。
container.thephpleague.com
まずは前提として以下のようなコードがあるとする。
<?php namespace Acme\Service; class SomeService { // ... }
これに対してDIコンテナを次のように使うことができる
<?php $container = new League\Container\Container; // register the service as a prototype against an alias $container->add('service', 'Acme\Service\SomeService'); // now to retrieve this service we can just retrieve the alias // each time we `get` the service it will be a new instance $service1 = $container->get('service'); $service2 = $container->get('service'); var_dump($service1 instanceof Acme\Service\SomeService); // true var_dump($service1 === $service2); // false
使い方としてはコンテナにDIしたいサービスのクラスを渡すこと。
それをgetすることで実際にそれらを利用することができる。
しかし、add/getではgetするたびに新しいインスタンスになるため
$service1 === $service2
という、評価式がfalseを返す。この点には注意したい。
実際にルーティングを組むときにはgetではなくshareメソッドが呼ばれているが実装は以下のようになっている。
<?php /** * {@inheritdoc} */ public function share($alias, $concrete = null) { return $this->add($alias, $concrete, true); }
addメソッドに$shareというフラグをtrueにして呼び出しているだけである。
公式ドキュメントでは$shareがtrueになっているもの関しては先程の評価式がtrueになる。
つまり同一のインスタンスが返ることになる。
同一インスタンスが返る場合としては、addメソッドをコールした場合でもクラスインスタンスを直接渡す場合もなると公式に書いていた。
ここまでの内容を理解すれば、ルーティング設定時のこの部分を理解できる。
<?php $container = new League\Container\Container; $container->share('response', Zend\Diactoros\Response::class); $container->share('request', function () { return Zend\Diactoros\ServerRequestFactory::fromGlobals( $_SERVER, $_GET, $_POST, $_COOKIE, $_FILES ); }); $container->share('emitter', Zend\Diactoros\Response\SapiEmitter::class);
まずDIコンテナを作り、そこにサービスをshareメソッドを使って追加している。
具体的にはPSR-7に準拠したサービスである、ResponseとRequestにEmitterを追加している。
が、しかしPSR-7とは何者か知らなかったので次はそれをまとめる。
HTTP message interfaces (PSR-7)
PSR-7は何を決めているかというと、題名の通りPHPでHTTP通信を扱うときのリクエストとレスポンスの標準となる仕様を決定している。
これに関しては公式と以下のサイトが役に立った。
全てのHTTPリクエストには特定の形式が存在しRequestは以下のような
POST /path HTTP/1.1 Host: example.com foo=bar&baz=bat
Responseは次のような形式をとる。
HTTP/1.1 200 OK Content-Type: text/plain This is the response body
これをPSR-7では次の2つのインターフェースを使って宣言している
Psr\Http\Message\RequestInterface Psr\Http\Message\ResponseInterface
これらを使うことで、自分でああだこうだしないでHTTPリクエストをPHPで扱うことができる。
ここをまとめようと思うと、DIコンテナの比にならないくらい文章量が多くなってしまうのでここでは割愛。後日別記事にする。
しかし、何故これが必要なのかというとルーティングを構成する次の部分で使っているからである。
<?php $route->map('GET', '/', function (ServerRequestInterface $request, ResponseInterface $response) { $response->getBody()->write('<h1>Hello, World!</h1>'); return $response; });
そう、この部分をみてわかるようにルーティングを構成して呼び出すメソッドはPSR-7準拠のインターフェースを引数に持ち$responseを返すことになっているからだ。
今上記のコードだと、HTTPレスポンスのボディにHTMLを書き込みそれを返しているのでHello,World!が表示されるといった状況になっている。
これはこの後、クラスを指定する場合でも同様に呼び出すメソッドには関係してくる。
Dispatchから先
残りはこの部分。
<?php $response = $route->dispatch($container->get('request'), $container->get('response')); $container->get('emitter')->emit($response);
ここではdispatchに先ほど設定したDIコンテナの値を使っている。
それをdispatchメソッドに渡している。dispatchメソッドの実装は次の通り。
<?php /** * Dispatch the route based on the request. * * @param \Psr\Http\Message\ServerRequestInterface $request * @param \Psr\Http\Message\ResponseInterface $response * * @return \Psr\Http\Message\ResponseInterface */ public function dispatch(ServerRequestInterface $request, ResponseInterface $response) { $dispatcher = $this->getDispatcher($request); $execChain = $dispatcher->handle($request); foreach ($this->getMiddlewareStack() as $middleware) { $execChain->middleware($middleware); } try { return $execChain->execute($request, $response); } catch (Exception $exception) { $middleware = $this->getStrategy()->getExceptionDecorator($exception); return (new ExecutionChain)->middleware($middleware)->execute($request, $response); } }
dispatcherを取得した後にDispatcherクラスのhandleメソッドをコールしてミドルウェアを適用しそれらを実行している。
最後に今までのあれこれをemit(うまく表現できる日本語がわからなかった)することでルーティングで設定したあれこれが実行される。
ルーティング周りをもう少し頑張ってみる
これまでまとめたところが実は公式で言うところのHello Worldでしかない。
PHPをやったこともない人間が雑に触ろうとした結果PSR-7,11を調べたりしないと行けないのですごく大変だった。
ただ、ここまでやった段階だけでは無名関数を毎回書いて呼び出す必要があったり、もしルーティング内でワイルドカードを使用したい場合などどうするんだとなるのでそのあたりを公式サイトをみてまとめていく。
ワイルドカードを使いたい場合
これが自分自身このライブラリを使いたかった理由のひとつでもあるが、ルーティングとしてpathを設定するときにワイルドカードは次のように使える。
<?php $router = new League\Route\RouteCollection; $router->map('GET', '/user/{id}/{name}', function (ServerRequestInterface $request, ResponseInterface $response, array $args) { // $args = [ // 'id' => {id}, // the actual value of {id} // 'name' => {name} // the actual value of {name} // ]; return $response; });
もっと値を限定して使うなら次のようにする。
<?php $router = new League\Route\RouteCollection; // this route will only match if {id} is numeric and {name} is a alpha $router->map('GET', '/user/{id:number}/{name:word}', function (ServerRequestInterface $request, ResponseInterface $response, array $args) { // $args = [ // 'id' => {id}, // the actual value of {id} // 'name' => {name} // the actual value of {name} // ]; return $response; });
ここで指定できる種別には以下のものがある。
- number
- word
- alphanum_dash
- slug
- uuid
ここまででも十分便利なのに正規表現を適用することもできる。
<?php $router = new League\Route\RouteCollection; $router->addPatternMatcher('wordStartsWithM', '(m|M)[a-zA-Z]+'); $router->map('GET', 'user/mTeam/{name:wordStartsWithM}', function (ServerRequestInterface $request, ResponseInterface $response, array $args) { // $args = [ // 'id' => {id}, // the actual value of {id} // 'name' => {name} // the actual value of {name} // ]; return $response; });
HTTPメソッドでルーティングを設定
今まではmapを使った例が多かったがもう少し便利なものがある。
<?php $route = new League\Route\RouteCollection; $route->get('/acme/route', 'Acme\Controller::getMethod'); $route->post('/acme/route', 'Acme\Controller::postMethod'); $route->put('/acme/route', 'Acme\Controller::putMethod'); $route->patch('/acme/route', 'Acme\Controller::patchMethod'); $route->delete('/acme/route', 'Acme\Controller::deleteMethod'); $route->head('/acme/route', 'Acme\Controller::headMethod'); $route->options('/acme/route', 'Acme\Controller::optionsMethod');
直接指定することもできるし、HTTPメソッドごとに処理を分けることもできる。
ホストやスキームを指定する
なんてこともできる。
<?php $route = new League\Route\RouteCollection; // this route will respond to http://example.com/acme/route // or https://example.com/acme/route $route->map('GET', '/acme/route', 'AcmeController::method')->setHost('example.com'); // this route will only respond to https://example.com/acme/route $route->map('GET', '/acme/route', 'AcmeController::method')->setScheme('https')->setHost('example.com');
ルーティングをグループとしてまとめる
これが一番使いたかった機能だと思う。
ルーティングをグループ化して追加できるのはスゴク便利。
<?php $route = new League\Route\RouteCollection; $route->group('/admin', function ($route) { $route->map('GET', '/acme/route1', 'AcmeController::actionOne'); $route->map('GET', '/acme/route2', 'AcmeController::actionTwo'); $route->map('GET', '/acme/route3', 'AcmeController::actionThree'); });
もちろんこれらも個別にホストやスキームを設定できる。
<?php $route = new League\Route\RouteCollection; $route->group('/admin', function ($route) { $route->map('GET', '/acme/route1', 'AcmeController::actionOne'); $route->map('GET', '/acme/route2', 'AcmeController::actionTwo')->setScheme('https'); $route->map('GET', '/acme/route3', 'AcmeController::actionThree'); }) ->setScheme('http') ->setHost('example.com') ;
そして実際のリクエストは次のようになる
GET http://example.com/admin/acme/route1 GET https://example.com/admin/acme/route2 GET http://example.com/admin/acme/route3
これスゴク便利。
さいごに
とりあえず、ここまでが自分の使いたかった機能を一通り網羅した。
ルーティングについて雑に書くはずがDIコンテナやPSRについてまで触れるとは思わなかったけどかなり理解が深まった。
関連するPSRやその他についてもこの後まとめていきたい。
あと自分はWebあたりの知識が割と不足していてわからないことだらけだったので、単純に語句とか調べてなるほどなとなるより
コードをみてこういうふうになっているのかと理解する方がよいということがわかったのは収穫だった
KLabの技術系インターンに参加してきた話
- はじめに
- インターンに行くまで
- 目標
- micropsとは
- インターン(0日目)
- インターン(1日目)
- インターン(2日目)
- インターン(3日目)
- インターン(4日目)
- インターン(5日目)
- インターン(6日目)
- さいごに
はじめに
人生初のインターンということでKLabさんの技術系インターンに参加してきました。
www.klab.com
今回、期間中お世話になったメンターは山本雅也さんです。
twitter.com
進捗払いに関してはどこのインターンに行った方も載せていますが、これに関しては割愛します。
詳しくは僕のツイッターを漁ってください。
インターンに行くまで
まず、何があったかというと昨年の9月頃にこんなことがありました。
というかけんつくんネットワーク周りに興味があるんだっけ?
— chikuwait (@chikuwa_IT) 2017年9月20日
TCP/IP のプロトコルスタックをゼロから作るインターンを知っているんだけど
— chikuwait (@chikuwa_IT) 2017年9月20日
pandaxさんネットワークおにいさんなのでけんつくんをつなげてみた
— chikuwait (@chikuwa_IT) 2017年9月20日
けんつくんもきっと煽りがいがあるはずなので
— chikuwait (@chikuwa_IT) 2017年9月20日
はい、つまりはそういうことです。
この前に、chikuwait君と話していて
「ネットワーク、興味あるから勉強したいんだよねぇ」
と、雑に話した途端にこれです。
というわけで、この後に山本さんに直接連絡を取りリモートで面談を行い
すごくあっさりと決まってしまいました。
ただ、ネットワーク周りを本当にやったことがなかったのでインターンまでに
「Linux カーネルモジュールで最速のEchoサーバを作る」
や
「DHCPの仕様をざっくりと理解してくる」
といったことを事前に行いました。
進捗払いに関してはどこのインターンに行った方も載せていますが、これに関しては割愛します。
詳しくは僕のツイッターを漁ってください。
目標
今回のインターンでの目標はメンターである山本さんが作っているユーザ空間で動作するTCP/IPプロトコル・スタックである「microps」にDHCPクライアント機能を追加実装する。
ただ、それだけです。
micropsとは
micropsは前述の通り、ユーザ空間で動作するTCP/IPプロトコル・スタックで、Ethernetフレームの送受信を行います。
加えてカーネル内のプロトコル・スタックを使用していません。
僕が機能追加する前の段階でSocketライクなAPIを提供する機能もありました。
インターン(0日目)
飛行機代を抑えるために早朝の便をとり、なんと前日の昼前に東京着という意味不明なスケジュールで行動していました。
初の東京だったのでまずどこに行こうかと思い、やっぱりアキバにいってしまいました。
その後、紆余曲折を経てホテルに到着。初日からの戦闘に向けて美味しいものも食べてきました。
インターン(1日目)
この日は、10:30ごろにKLabさんが入っている六本木ヒルズ森タワーに行きました。
気合が入りすぎて5時ごろには起床していたので、何故か江東区から歩いていくという強行に出ました。
この日はまず
インターンのガイダンス説明から始まり
メンターさんの紹介やその他諸々の手続きを行い
その後すぐに会議室に移りmicropsの仕様とDHCPに関する説明を30分ほど受けました。
それからはひたすらに開発環境の構築やmicropsのビルドなどを行い、その日は終了。
インターン(2日目)
この日はDISCOVERパケットを構築すること、またそれを送信し返ってくるOFFERパケットを解析することが目標だった。
DHCPクライアントがどのようにIP等を手に入れているかというと、ざっくりと以下の4プロセスを踏む
- DISCOVER パケットをクライアントがブロードキャストで送信
- OFFER パケットが返ってくるので情報を解析
- 必要な情報を取り出しREQUEST パケットに組み込み送信
- ACK or NACKが返ってきて終了
2日目はこの前半2つを行った。
まずはRFCや事前資料からDISCOVERパケットを構築するところから始めた。
パケット自体は固定形式部分が234byte、可変形式のオプションが64byteという構成になっている。
またIPを取得していない状態でsocket等を使用するため、送受信は全てブロードキャストを使用する。
そのため、ブロードキャストフラグも使用し明示的にブロードキャストを指定。
オプションは4byteのマジックコードに始まり、パケットの種別を示すものが3byte続く。
その後はクライアントIDやMACアドレス、サーバ固有情報などが含まれ最終的には0xFFをStopperとしている。
Stopperから末尾まではデータが入っているはずがないので0x00で埋めてある。
ここまでがパケット構成となる。
その結果を以下に示す。
========== DHCP Message Debug Print ========== op: 1 htype: 1 hlen: 6 hops: 0 xid: 5abda168 (1522377064) secs: 0 flags: 8000 ciaddr: 0.0.0.0 yiaddr: 0.0.0.0 siaddr: 0.0.0.0 giaddr: 0.0.0.0 chaddr: 08:00:27:0c:a1:27 sname: file: magic: 63 53 82 63 option[35] 01 option[3d] 01 08 00 27 0c a1 27 option:[ff] total 300 bytes (padding 47 bytes)
次に構築したパケットを送信、レスポンスを受信する処理を追加した。
ここで使用したものはmicropsに始めから搭載されていたudp関連のsocketライクなAPI。
レスポンスデータは形式こそオプション以外では変化が無いもののオペコードは必ず2になって(送信時は1を指定)返ってくる。
またオプション部分でマジックコードの次もOFFERパケットを示すものに変わっている。
以下が受信したOFFERパケットの情報となる。
========== DHCP Message Debug Print ========== op: 2 htype: 1 hlen: 6 hops: 0 xid: 5abda168 (1522377064) secs: 0 flags: 8000 ciaddr: 0.0.0.0 yiaddr: 192.168.0.102 siaddr: 192.168.0.1 giaddr: 0.0.0.0 chaddr: 08:00:27:0c:a1:27 sname: file: magic: 63 53 82 63 option[35] 02 option[36] c0 a8 00 01 option[33] 00 00 02 58 option[01] ff ff ff 00 option[03] c0 a8 00 01 option[06] c0 a8 00 01 option[0f] 55 62 75 6e 74 75 53 65 72 76 65 72 44 48 43 50 53 65 72 76 65 72 2e 6c 6f 63 61 6c option:[ff] total 304 bytes (padding 0 bytes)
二日目はここで終了。
インターン(3日目)
3日目はOFFERパケットから必要な情報を取り出し、REQUESTパケットを構築し送信
返ってくるACK(or NACK)を解析して、micropsを使用したEchoサーバの設定に必要な情報を組み込むところまでが目標。
まずは、yiaddrから使えるIPを取り出してREQUESTパケットとして構築。
構築したパケット自体は2日目に行ったものとほとんど同様なので概要は省略。
========== DHCP Message Debug Print ========== op: 1 htype: 1 hlen: 6 hops: 0 xid: 5abda168 (1522377064) secs: 0 flags: 8000 ciaddr: 0.0.0.0 yiaddr: 192.168.0.102 siaddr: 192.168.0.1 giaddr: 0.0.0.0 chaddr: 08:00:27:0c:a1:27 sname: file: magic: 63 53 82 63 option[35] 03 option[3d] 01 08 00 27 0c a1 27 option[32] c0 a8 00 66 option:[ff] total 300 bytes (padding 41 bytes)
次にACK(or NACK)を受け取る。
これもOFFERパケットの受信と同様に行った。
========== DHCP Message Debug Print ========== op: 2 htype: 1 hlen: 6 hops: 0 xid: 5abda168 (1522377064) secs: 0 flags: 8000 ciaddr: 0.0.0.0 yiaddr: 192.168.0.102 siaddr: 192.168.0.1 giaddr: 0.0.0.0 chaddr: 08:00:27:0c:a1:27 sname: file: magic: 63 53 82 63 option[35] 05 option[36] c0 a8 00 01 option[33] 00 00 02 58 option[01] ff ff ff 00 option[03] c0 a8 00 01 option[06] c0 a8 00 01 option[0f] 55 62 75 6e 74 75 53 65 72 76 65 72 44 48 43 50 53 65 72 76 65 72 2e 6c 6f 63 61 6c option:[ff] total 304 bytes (padding 0 bytes)
ここまでは前日に行っていたこととあまり大差なかったのですんなりいった。
問題はこのあと、実際にACKから必要なIPアドレス、サブネットマスク、デフォルトゲートウェイを取り出して設定として組み込む点だった。
IPアドレスなどはすぐに見つかり、RFCなどにオプション種別も記載されていたので比較的容易に発見できた。
それを組み込む際にip_init 関数が存在するがこいつを単に呼び出すとethernetフレームにipプロトコルが二重に追加されてしまう問題があった。
またルーティングテーブル周りの初期化や、このmicropsを利用する人が任意でDHCPクライアント機能を利用できるようにするなど
やることが山積みだった。
このあたりはメンターの山本さんの力を借りて(もちろんここまで来るのに絶大な支援があった)、実装することができた。
ここで3日目が終了。
インターン(4日目)
この日は、最低限の処理しか追加していなかったコードにオプション解析などを組み込みデータがちゃんと正しいものか、こちらが想定しているものかチェックする処理などを追加
その後はひたすら鬼のペアプロコードリファクタリングを行った。(多分これが一番きつかった)
インターン(5日目)
この日は最終成果発表に向けて資料の準備とPRの作成を行った。
期間中、多大なご迷惑をお掛けした分プルリクがマージされた時はすごく嬉しかったというか、やったぞという気持ちしかなかった。
インターン学生の @lrf141 くんが僕の作ってる TCP/IP プロトコルスタック「microps」に DHCP クライアント機能を追加してくれました!実質三日で仕上げてくれたので優勝です🎉https://t.co/BnUoBgYmwf
— YAMAMOTO Masaya (@pandax381) 2018年3月30日
その後の成果発表も無事に終わり、Klabの方々とご飯を食べに行きこの日は終了。
そしてインターンも終了。
インターン(6日目)
帰りの飛行機は例によって飛行機代を抑えるためにほぼ最終便をとっていた。
そのため、馬鹿みたいに時間があったので大門->スカイツリー->浅草->上野、アキバと観光してきた。
余談ではあるが、人生経験の一環としてメイドカフェに行こうか本気で迷った。
さいごに
というわけで長かったようで短く感じた怒涛の初インターンは終了した。
インターンでやったことあったことを雑にまとめてみたが、実際にはスケジュールがかつかつで8時間はコードを書いている日の連続だった。
技術的な面でも未熟であったことがわかったが、その他にも色々と自分には知らないことが多かった。
成果発表で、色々なエンジニアの方に今回何をやったかということを報告し「わからない事にわからないなりに挑戦し、完成させたという点は評価できる」との評価を頂いたが
やはり実際に作っている時などに、「これはきいたことがあるぞ」「これは知っているかもしれない」と思っていたこともあったがそれをいざ実行しようとなると「お、なんか上手くいかないぞ」といったことが多くあり
わかっている事とできる事は全く別物であると痛感した。
また、自分は今までコードばかりを書いていてそれが楽しかったからというのもあるが
もっと自分が作るものの基盤的な知識を身につけなければいけないということも実感した。
そういった、自身の弱点のようなものを見続けたので今回のインターンはすごく辛く感じる面が多かった。
しかし、それ以上にものすごいレベルのエンジニアの方たちに囲まれて
今回のようにコードを書いて、何かを実現してという環境に身を置けたのはすごく楽しかった。
辛いと感じることもあったが、それでもわからないことをわからないと認めてその上で色々と試行錯誤を繰り返し一つの問題を解決していく。そういった時間を過ごせたことはこの上なく幸せな時間だった。
学ぶべきことが多く分かった反面で、学んだことも非常に多かった。
一週間ではあったが自分自身が以前に比べて成長できたことを感じることができたのでインターンに参加して本当によかったと思った。
最後に、今自分がいる大学ではあまり情報系に熱心な同期がいない。
そして家でもコードを書いて、それを楽しいと5年も言い続けている自分はそんな普通な人からみるとキモいという対象らしい。
そんな人たちに流されて普通の大学生活を送ろうとしていたが、今回のインターンでお昼休みなどに山本さんとキャリアについて話していて
自分のやりたいことがはっきりと見えたので周りを気にしないで残りの学生生活を自分なりに有効活用しようと思う。
けんつくんは僕が紹介したインターンで邪念を捨てたらしいです
— chikuwait (@chikuwa_IT) 2018年4月10日
本当に今回インターンに参加できてよかったとおもう。
今回、お世話になったKLabの皆さん、そしてなによりメンターとして1週間指導してくださった山本雅也さん
本当にありがとうございました。
無限インターン行きたい!!