それが僕には楽しかったんです。

僕と MySQL と時々 MariaDB

Laravelを使いたかったので雑に入門した

はじめに

なぜか急にwebシステムを作る必要がでてきたため、Laravelを使ってみようとおもった。
だが使ったことないので備忘録も兼ねてまとめていこうと思う。

対象

  • webアプリケーションを構築したことがある
  • 他のフレームワークを使用した経験がある
  • PHPを書いたことがある
  • PHPでなくてもなんかコードは書いたことある

などなど、何が言いたいかというと初心者に向けた記事じゃなくて
自分用のメモ兼ある程度のサーバサイド経験者に共有したい内容となっている

Laravelとは

Laravel は、MVCのWebアプリケーション開発用の無料・オープンソースPHPで書かれたWebアプリケーションフレームワークのこと。
PHPのwebアプリケーションフレームワークといえば、CakePHPFuelPHPあたりを想像するだろうが、最近は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にアクセスすると…
f:id:RabbitFoot141:20180607104430p:plain
ここまででとりあえず最低限起動するところまでは完了した。


ちなみにLaravelではpublicディレクトリにあるindex.phpがフロントコントローラとなっている。

ルーティング

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

おわりに

題名にあるように相当雑な入門記事が出来上がった。
特にモデルなんかはひどい。

しかし、思った以上にLaravelのドキュメントが整っていて
入門記事とうたって機能の大半を紹介するには分量が最高に多くなってしまうため
MVCとルーティング、リクエストというWebアプリを構築する上でこれだけわかっておけばどうにかなりそうなあれこれを最高に雑にまとめてみた

このあと、気合で勉強してその都度、機能を深く掘り下げていきたい

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でレンダリングするときはどうすれば良いのだろうかと疑問に思った。
今回はそれを理解するためだけに、最小構成オレオレテンプレートエンジンを雑に作ってみたのでそれについて書いて見ようと思う。

github.com

2018/05/13の段階でとりあえずレンダリングはできる状態にあるが、細かい所や設計面を作りこめていないので今後改良していく。
composerをつかっている環境であれば以下のコマンドで導入できる。

$ composer require lrf141/rook

packagist.org

エラーの原因と解決策

まず原因は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」を使うための公式にあるサンプルが何をやっているのかまとめてみようと思う。

route.thephpleague.com

環境

まず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通信を扱うときのリクエストとレスポンスの標準となる仕様を決定している。
これに関しては公式と以下のサイトが役に立った。

www.php-fig.org
qiita.com

全ての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の技術系インターンに参加してきた話

はじめに

人生初のインターンということでKLabさんの技術系インターンに参加してきました。
www.klab.com

今回、期間中お世話になったメンターは山本雅也さんです。
twitter.com


進捗払いに関してはどこのインターンに行った方も載せていますが、これに関しては割愛します。
詳しくは僕のツイッターを漁ってください。

インターンに行くまで

まず、何があったかというと昨年の9月頃にこんなことがありました。




はい、つまりはそういうことです。

この前に、chikuwait君と話していて
「ネットワーク、興味あるから勉強したいんだよねぇ」
と、雑に話した途端にこれです。

というわけで、この後に山本さんに直接連絡を取りリモートで面談を行い
すごくあっさりと決まってしまいました。

ただ、ネットワーク周りを本当にやったことがなかったのでインターンまでに
Linux カーネルモジュールで最速のEchoサーバを作る」

DHCPの仕様をざっくりと理解してくる」
といったことを事前に行いました。

進捗払いに関してはどこのインターンに行った方も載せていますが、これに関しては割愛します。
詳しくは僕のツイッターを漁ってください。

目標

今回のインターンでの目標はメンターである山本さんが作っているユーザ空間で動作するTCP/IPプロトコル・スタックである「microps」にDHCPクライアント機能を追加実装する。
ただ、それだけです。

micropsとは

micropsは前述の通り、ユーザ空間で動作するTCP/IPプロトコル・スタックで、Ethernetフレームの送受信を行います。
加えてカーネル内のプロトコル・スタックを使用していません。
僕が機能追加する前の段階でSocketライクなAPIを提供する機能もありました。

github.com

インターン(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の作成を行った。
期間中、多大なご迷惑をお掛けした分プルリクがマージされた時はすごく嬉しかったというか、やったぞという気持ちしかなかった。

その後の成果発表も無事に終わり、Klabの方々とご飯を食べに行きこの日は終了。
そしてインターンも終了。

インターン(6日目)

帰りの飛行機は例によって飛行機代を抑えるためにほぼ最終便をとっていた。
そのため、馬鹿みたいに時間があったので大門->スカイツリー->浅草->上野、アキバと観光してきた。
余談ではあるが、人生経験の一環としてメイドカフェに行こうか本気で迷った。

さいごに

というわけで長かったようで短く感じた怒涛の初インターンは終了した。
インターンでやったことあったことを雑にまとめてみたが、実際にはスケジュールがかつかつで8時間はコードを書いている日の連続だった。
技術的な面でも未熟であったことがわかったが、その他にも色々と自分には知らないことが多かった。
成果発表で、色々なエンジニアの方に今回何をやったかということを報告し「わからない事にわからないなりに挑戦し、完成させたという点は評価できる」との評価を頂いたが
やはり実際に作っている時などに、「これはきいたことがあるぞ」「これは知っているかもしれない」と思っていたこともあったがそれをいざ実行しようとなると「お、なんか上手くいかないぞ」といったことが多くあり
わかっている事とできる事は全く別物であると痛感した。
また、自分は今までコードばかりを書いていてそれが楽しかったからというのもあるが
もっと自分が作るものの基盤的な知識を身につけなければいけないということも実感した。

そういった、自身の弱点のようなものを見続けたので今回のインターンはすごく辛く感じる面が多かった。

しかし、それ以上にものすごいレベルのエンジニアの方たちに囲まれて
今回のようにコードを書いて、何かを実現してという環境に身を置けたのはすごく楽しかった。
辛いと感じることもあったが、それでもわからないことをわからないと認めてその上で色々と試行錯誤を繰り返し一つの問題を解決していく。そういった時間を過ごせたことはこの上なく幸せな時間だった。

学ぶべきことが多く分かった反面で、学んだことも非常に多かった。
一週間ではあったが自分自身が以前に比べて成長できたことを感じることができたのでインターンに参加して本当によかったと思った。


最後に、今自分がいる大学ではあまり情報系に熱心な同期がいない。
そして家でもコードを書いて、それを楽しいと5年も言い続けている自分はそんな普通な人からみるとキモいという対象らしい。
そんな人たちに流されて普通の大学生活を送ろうとしていたが、今回のインターンでお昼休みなどに山本さんとキャリアについて話していて
自分のやりたいことがはっきりと見えたので周りを気にしないで残りの学生生活を自分なりに有効活用しようと思う。

本当に今回インターンに参加できてよかったとおもう。
今回、お世話になったKLabの皆さん、そしてなによりメンターとして1週間指導してくださった山本雅也さん
本当にありがとうございました。

無限インターン行きたい!!

プログラマのためのSQL第4版を読んで。〜 データベース VS ファイルシステム 〜

はじめに

訳あって今、カーネルからWebまでという非常に広い範囲を日常的に触っている。
しかし、一日の時間は24時間と決まっており自分にはコードを書く事以外にもしなければならないことがある。
だが、コードを書かないという日はなるべく作りたくない。
我儘を言うなら、限られた時間でC言語で低レイヤーをやったりPHPを使って高レイヤーをいじったり、ScalaJavaを使ってその中間も色々やってみたい。

そう思った時に、その全てを叶えるのがデータベースだと思った。

NoSQLであるRedisはC言語ベースで作られていてソースコードGithub上で公開されている。
PHPではサーバサイドとして実際にDBを操作する時がある。
Scala/Javaにはjdbcなどがある。

つまり、先ほどの我儘を全て叶えてくれる非常に魅力的な領域だった。
というわけで、そんなふわふわした理由のもとにデータベースを勉強してみようと思った。

単にデータベースを勉強するといっても、実際にDBを使うことに始まり
それらを利用するためのドライバやDB自体をソースコードレベルで理解することまでを目標にする。

その上でまずDBの使い方、特にSQLについて深く勉強したいと思ったので「プログラマのためのSQL」を購入した。
ここではその書籍を読んで勉強したことをまとめていく。

データベース VS ファイルシステム

データベース、特にRDBMSにおいては普段利用するプログラミング言語が操作するファイルシステムとはかなり勝手が違う。
データベースを操作するのにはSQLを使用し、SQLプログラミング言語と異なり独自のI/Oシステムを持たずデータの宣言、操作、制御のみを行う。

しかし、プログラミング言語にもSQLにもモデルは存在する。
そのモデルを理解することで対象を理解することが容易になる。
プログラミング言語におけるモデルとは、あまり詳しくないのではっきりとは言えないが「いくつかのプログラミング言語は数学をベースとしているため数学で実際に使用する記号群や構成を使用することができる。」といったことを指す。
SQLにおけるモデルは数学でいうところの集合(Set)としてのデータである。
数学でいう集合は、それに含まれる要素は特定のタイプに属していて順序をもたない。そして、集合に対する操作は一度に全ての要素に対して行われる。

わかりやすい例が参考文献に記述してあった。

正の整数の集合から奇数の部分集合を求める場合、答えはすべての奇数を含む単一の集合として得られる。奇数を1つずつ調べて順番に集合を作っていくようなことはしない。ただ、奇数を「2で割ったときの余りが1になる」という条件を満たす数として定義するだけだ。

このような背景があるために、分類の条件を変更したとしてもテスト可能で、かつ分類そのものも可能である。
それ故、集合を対象とする集合指向モデルは並列処理に向いている。


SQLは3つのサブ言語から構成されている

  • DDL: データ宣言言語(Data Declaration Language)
  • DML: データ操作言語(Data Manipulation Language)
  • DCL: データ制御言語(Data Control Language)

DDLはデータベースに含まれる中身を定義し、そこに含まれるデータの整合性を確保する。
前述のファイルシステムでは整合性やデフォルト値、他のテーブルとの関連性は一切定義されない上に厳密にデータの論理的一貫性を保つ機能はない。
しかしデータベースにおいてはDDLがそれらの役割持ち、DMLとDCLと共に動作するためSQLは統合された全体として扱われファイルの様に分離された一部分とは扱われない。

DMLSQLを一度でも書いたことがある人ならわかる。
SQLにおける次の操作を示す。

  • SELECT
  • INSERT
  • UPDATE
  • DELETE

DCLはここではまとめない。かなり深い概念らしくあまり解説されていなかったから。


ここまでのまとめ

  • SQLは集合をモデルにしている
  • 3つのサブ言語から構成されている
  • データベースにおけるスキーマはファイルの集合体ではなく、関連性を持つ。
  • テーブルはファイルではなく、スキーマの一部である。

エンティティとしてのテーブル

データベース、特にリレーショナルデータベースにおいてエンティティとは属性によって定義される。そしてそのインスタンスはテーブルにおける1行1行を指す。
属性とはテーブルでいうところの列であり、値はそれ以上分解不能な原始的な値であるスカラ値をもつ。
よってエンティティとしての役割をデータベースのテーブルは持つことになる。

完全に同じ構成のテーブルが2つあるとき集合的な観点からみるとそれらは同じ種類の要素を持つ集合でしかない。
これはファイルであれば許されることである。なぜならファイルはそれら自体は物理的に分類された単位であるために同一情報を異なるファイルとして保持することが許容されている。
しかし、前述の内容よりSQLにおいては集合であり区別が付かないのでそのような物は一つにまとめてしまうのがよい。

関連としてのテーブル

これは非常に簡単なことで、テーブルにおける関連とは列が一つ以上のエンティティテーブルを参照することによって成立している。
これはファイルとフィールドにはない特性になっている。

行 VS レコード

行はレコードではない。レコードはそれを読み込むアプリケーション側で定義されるものであって、それ自体はスキーマで定義される。
フィールドの名前はアプリケーション側で定義されるのに対して、行はデータベーススキーマ中で定義されるということになる。


ここで空のファイルを考える。
空のファイルは0バイト長という状態であるが、空のテーブルに関しては行は空としても列を持っていて理論的制約などを保持している。
つまり数学的意味合いの集合における空集合とは少し異なり、上記の構造をとるためたとえ空であっても別の集合として扱うべきである。

次の行にある特徴としてはテーブル内の全ての行は構造上同一な形式を取る点であり、これはファイルにはない。
ファイルであれば内部に含まれるデータの構造などは全て一致しているわけでもなく文字列であったり数値であったり情報もサイズもバラバラにしてしまうこともできるからだ。

列 VS フィールド

レコード内のフィールドはそれを読み込むアプリケーション側で定義されるが行における列はデータベーススキーマで定義される。
そして列が保持するデータ型は前述の通りスカラ値を持つ。

そして、SQLでは列は列名によってのみアクセスされる。
厳密に列名によってのみというわけではなく

SELECT *
INSERT INTO <table name>

の様な省略形は存在する。しかしこれは列名のリストを単にテーブル定義で列が定義された物理順序で展開しているだけにすぎないので内部的には列名でのみアクセスされる。
SQLにおけるNULLの使用もこれに近いものとなっている。

列があるが故に、ファイルとの違いがある。
ファイルは受け入れる内容や吐き出す内容に特に制約は無いがSQLにはその制約が存在する。
それにより、SQLが扱うものには一定の整合性が保証される。

つまりはそれが指す関係の整合性を示すことになる。
これはファイルごとに独立しているファイルシステムには無い特徴となっている。

おわりに

SQLとしての基盤的な事象をファイルシステムと比較してまとめてあったのはSQLを理解する上で非常に大事な情報であると感じた。
次はDBMSが内部的に行っているトランザクションと同時実行制御に関する章を読んでまとめていきたい。

参考文献

最速のEchoサーバーを目指して、LinuxKernelモジュールを作っていく part4

はじめに

前回、実装したはずの機能で特にacceptでコケる問題がなかなか解決出来なかったので

毎回おなじみになりつつあるこのサイト
linux/include - Elixir - Free Electrons
さらに今回は、この本も参考にして書いていく
www.oreilly.co.jp

まず色々、調べたりしたことをまとめてその後に本題に入ろうと思う

色々やる前のメモ

実際に動いたであろうサンプルソースを参考にああだこうだ考えながら書いていたがacceptでコケる。
省略していた待ち行列が関係してるのではと思っている。

そもそも、エラーコード返ってきてるのにそれを見てなかったから見てみる。
見てみた。

$dmesg
....
....
[524524.095520] Fastest Echo Server Start!!
[524524.095544] accept error no:-11
$

なんか、acceptでエラーが返ってきている。
それを上記のサイトで調べる。
まずはacceptの実装から。
acceptの実装は次のようになっていた。

static int accept(struct socket *sock, struct socket *new_sock, int flags)
{
	struct sock *sk = sock->sk;
	struct sk_buff *buf;
	int res;

	lock_sock(sk);

	if (sock->state != SS_LISTENING) {
		res = -EINVAL;
		goto exit;
	}

	while (skb_queue_empty(&sk->sk_receive_queue)) {
		if (flags & O_NONBLOCK) {
			res = -EWOULDBLOCK;
			goto exit;
		}
		release_sock(sk);
		res = wait_event_interruptible(*sk_sleep(sk),
				(!skb_queue_empty(&sk->sk_receive_queue)));
		lock_sock(sk);
		if (res)
			goto exit;
	}

	buf = skb_peek(&sk->sk_receive_queue);

	res = tipc_create(sock_net(sock->sk), new_sock, 0, 0);
	if (!res) {
		struct sock *new_sk = new_sock->sk;
		struct tipc_sock *new_tsock = tipc_sk(new_sk);
		struct tipc_port *new_tport = new_tsock->p;
		u32 new_ref = new_tport->ref;
		struct tipc_msg *msg = buf_msg(buf);

		lock_sock(new_sk);

		/*
		 * Reject any stray messages received by new socket
		 * before the socket lock was taken (very, very unlikely)
		 */

		reject_rx_queue(new_sk);

		/* Connect new socket to it's peer */

		new_tsock->peer_name.ref = msg_origport(msg);
		new_tsock->peer_name.node = msg_orignode(msg);
		tipc_connect2port(new_ref, &new_tsock->peer_name);
		new_sock->state = SS_CONNECTED;

		tipc_set_portimportance(new_ref, msg_importance(msg));
		if (msg_named(msg)) {
			new_tport->conn_type = msg_nametype(msg);
			new_tport->conn_instance = msg_nameinst(msg);
		}

		/*
		 * Respond to 'SYN-' by discarding it & returning 'ACK'-.
		 * Respond to 'SYN+' by queuing it on new socket.
		 */

		if (!msg_data_sz(msg)) {
			struct msghdr m = {NULL,};

			advance_rx_queue(sk);
			send_packet(NULL, new_sock, &m, 0);
		} else {
			__skb_dequeue(&sk->sk_receive_queue);
			__skb_queue_head(&new_sk->sk_receive_queue, buf);
		}
		release_sock(new_sk);
	}
exit:
	release_sock(sk);
	return res;
}

この中で、一通り関係しそうなものをerror.hとかerror-base.hから探してみる。
そうすると

#define EWOULDBLOCK EAGAIN

が返っていることがわかり今度はこのEAGAINを調べた。
すると、こいつが例の11番のエラーを返していた。
ただ

#define EAGAIN 11 //try again

とあり、何をtry againすればいいのかわからなかった。
そこでaccept関数の実装をみて、どこでこのエラーが返ってきてるか調べてみることにした。

するとここの処理が影響していることがわかった。

while (skb_queue_empty(&sk->sk_receive_queue)) {
		if (flags & O_NONBLOCK) {
			res = -EWOULDBLOCK;
			goto exit;
		}
		release_sock(sk);
		res = wait_event_interruptible(*sk_sleep(sk),
				(!skb_queue_empty(&sk->sk_receive_queue)));
		lock_sock(sk);
		if (res)
			goto exit;
	}

skb_queue_emptyが返す値が真であるならO_NONBLOCKをflagsとして渡しているので
今回のgotoで該当の処理がスキップされエラーとなってしまうためskb_queue_emptyが返す値が偽である必要がある。

skb_queue_empty関数に渡しているものは

&sk->sk_receive_queue

という構造体の値である。
そもそもこのskが何かというとaccept関数の先頭で宣言、初期化されている次のものである。

struct sock *sk = sock->sk;

右辺のsockはaccept関数に渡している第一引数である。
そして問題のskb_queue_empty関数は次のようになっている。

static inline int skb_queue_empty(const struct sk_buff_head *list)
{
	return list->next == (struct sk_buff *)list;
}

これはソケットバッファに関するキューの様でリストの次の要素と先頭要素が等価であることを調べている(っぽい)。

これとはまた別にO_NONBLOCK、ノンブロッキングIOについてもよくわかっていなかったのでそれも別途記述していく。

ノンブロッキングI/O

カーネルモジュールで、TCPソケットを使おうとしたらほとんどのソースにO_NONBLOCKが出てきてなんだこれはと調べると
次のサイトに色々まとまっていた。

blog.takanabe.tokyo

ノンブロッキングI/OではI/O対象のファイルディスクリプタの準備完が了していないことをアプリケーション側に伝えるため即座にエラーが返る(errnoにEGAINが格納されて返ってくる)。一般に、O_NONBLOCKフラグを利用してノンブロッキングモードを宣言するが、この時プロセスはブロック状態にならず、CPUを他の処理に回すことができるためI/O待ち時間を有効活用できる。

ノンブロッキングI/Oはソケットなどのネットワークに用いられるI/OモデルでディスクI/Oには使わない。

これに関連していたのがO_NONBLOCKを使用していたacceptであった。
次のManPageを参照すると色々わかった。
Man page of ACCEPT

キューに保留となっている接続要求がなく、 かつソケットが非停止になっていないときは、 accept() は接続が発生するまで呼び出し元を停止 (block) する。 ソケットが非停止になっていて、 待ち状態の接続要求がキューに無いときは、 accept() はエラー EAGAIN か EWOULDBLOCK で失敗する。

これが最も自分を苦しめたあれである。
そもそも今回は、このキューを使っていなかった。
そのため、待ち状態の接続要求がキューになく(そもそもキューを用意していなく)、acceptがEAGAIN,EWOULDBLOCKを返していた・

おわりに

そんなこんなで、tcpクライアントからメッセージを受け取り表示するところまではできた。
コードは以下のレポジトリにある。
github.com
使ったtcpクライアントはpythonで書いたものを使用した。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import socket

target = "localhost"
port = 8888

#using IPv4 and TCP/IP
client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

#connecting
client_socket.connect( (target, port) )

#data send
client_socket.send("hello,world")

#get data
response_data = client_socket.recv(1024) 

print response_data

ただ、データの送信が出来ないことに加えて、時折メッセージを受信出来なかったりするのでそのあたりのバグとリファクタリングは今後も行っていく必要がある

最速のEchoサーバーを目指して、LinuxKernelモジュールを作っていく part3

はじめに

前回まで
rabbitfoot141.hatenablog.com
rabbitfoot141.hatenablog.com

この二回でカーネルモジュールで標準出力や、カーネルスレッドを動かすところまでやった。
今回はこれをTCPサーバー化するのを目標にやっていく。

ネットワーク周り

さくっと実装したいが、カーネルモジュールでのネットワーク周りがわからないので
そこあたりから色々調べていく。

ソケット周り

これが使えないとネットワーク通信ができないのでこれがどういう仕様で実装されているか調べてみる。


見つかった。

http://elixir.free-electrons.com/linux/v3.4/source/include/linux/net.h#L138

これは

#include <linux/net.h>

というヘッダーにあることがわかった。

これの138行目にsocketという名前の構造体があり。
こいつがBSDのソケットとして使えるようなので使うことにする。

/**
 *  struct socket - general BSD socket
 *  @state: socket state (%SS_CONNECTED, etc)
 *  @type: socket type (%SOCK_STREAM, etc)
 *  @flags: socket flags (%SOCK_ASYNC_NOSPACE, etc)
 *  @ops: protocol specific socket operations
 *  @file: File back pointer for gc
 *  @sk: internal networking protocol agnostic socket representation
 *  @wq: wait queue for several uses
 */
struct socket {
	socket_state		state;

	kmemcheck_bitfield_begin(type);
	short			type;
	kmemcheck_bitfield_end(type);

	unsigned long		flags;

	struct socket_wq __rcu	*wq;

	struct file		*file;
	struct sock		*sk;
	const struct proto_ops	*ops;
};

色々なコードや、文献を漁っているとこの中でも次に重要になるのが

const struct proto_ops	*ops;

という構造体。
この構造体の実装は次のようになっている。

struct proto_ops {
	int		family;
	struct module	*owner;
	int		(*release)   (struct socket *sock);
	int		(*bind)	     (struct socket *sock,
				      struct sockaddr *myaddr,
				      int sockaddr_len);
	int		(*connect)   (struct socket *sock,
				      struct sockaddr *vaddr,
				      int sockaddr_len, int flags);
	int		(*socketpair)(struct socket *sock1,
				      struct socket *sock2);
	int		(*accept)    (struct socket *sock,
				      struct socket *newsock, int flags);
	int		(*getname)   (struct socket *sock,
				      struct sockaddr *addr,
				      int *sockaddr_len, int peer);
	unsigned int	(*poll)	     (struct file *file, struct socket *sock,
				      struct poll_table_struct *wait);
	int		(*ioctl)     (struct socket *sock, unsigned int cmd,
				      unsigned long arg);
#ifdef CONFIG_COMPAT
	int	 	(*compat_ioctl) (struct socket *sock, unsigned int cmd,
				      unsigned long arg);
#endif
	int		(*listen)    (struct socket *sock, int len);
	int		(*shutdown)  (struct socket *sock, int flags);
	int		(*setsockopt)(struct socket *sock, int level,
				      int optname, char __user *optval, unsigned int optlen);
	int		(*getsockopt)(struct socket *sock, int level,
				      int optname, char __user *optval, int __user *optlen);
#ifdef CONFIG_COMPAT
	int		(*compat_setsockopt)(struct socket *sock, int level,
				      int optname, char __user *optval, unsigned int optlen);
	int		(*compat_getsockopt)(struct socket *sock, int level,
				      int optname, char __user *optval, int __user *optlen);
#endif
	int		(*sendmsg)   (struct kiocb *iocb, struct socket *sock,
				      struct msghdr *m, size_t total_len);
	int		(*recvmsg)   (struct kiocb *iocb, struct socket *sock,
				      struct msghdr *m, size_t total_len,
				      int flags);
	int		(*mmap)	     (struct file *file, struct socket *sock,
				      struct vm_area_struct * vma);
	ssize_t		(*sendpage)  (struct socket *sock, struct page *page,
				      int offset, size_t size, int flags);
	ssize_t 	(*splice_read)(struct socket *sock,  loff_t *ppos,
				       struct pipe_inode_info *pipe, size_t len, unsigned int flags);
	void		(*set_peek_off)(struct sock *sk, int val);
};

bind,listen,acceptなどなど、どこかで見たことのあるような単語が並んでいる。
実際にtcpサーバを構築するときは、これらの関数に必要な情報を渡して呼び出す必要がある。


なんやかんや、C言語でネットワークプログラミングをする時と同じ様なあれこれはあるので
それと似たような感じでコードを書けそう。


この後で実装するコードは、カーネルスレッドを多用するので
kernel_lock関数を使った方がいいとかはまた別途考える必要がある。

文字列周り

文字列周りの扱いはどうしようかと調べていたら、linux/socket.hにちょうどいい構造体があった。

/*
 *	As we do 4.4BSD message passing we use a 4.4BSD message passing
 *	system, not 4.3. Thus msg_accrights(len) are now missing. They
 *	belong in an obscure libc emulation or the bin.
 */
 
struct msghdr {
	void	*	msg_name;	/* Socket name			*/
	int		msg_namelen;	/* Length of name		*/
	struct iovec *	msg_iov;	/* Data blocks			*/
	__kernel_size_t	msg_iovlen;	/* Number of blocks		*/
	void 	*	msg_control;	/* Per protocol magic (eg BSD file descriptor passing) */
	__kernel_size_t	msg_controllen;	/* Length of cmsg list */
	unsigned	msg_flags;
};

kernelでネットワークのレスポンスを扱うにはなにやらこれがいいらしい。

実装の前に

全体の流れを整理する。

  • 必要なsocket類を構造体などで宣言する。
  • listenをするスレッドを作る
  • 上記のスレッド内でsocketを作成、bind、listenなどを行う
  • それが完了したら更にacceptを行うスレッドを作る
  • 文字列が送信されてきたら、それを送り返す

実装する

実装してみた、結果はこちら。

github.com


ヘッダーファイルに直書きしているのは後で直す。

ただし、実行してみるとaccept errorと表示されている。

[  689.884212] accept_thread the csock is :474605440,474606080
[  689.884216] accept start
[  689.884220] accept error

なるはやで解決しないといけないものは
accept errorと表示されるので、上記opsを見なおしてなぜそこでコケたのか調べる。
あとrmmodを実行した際に

rmmod: ERROR: ../libkmod/libkmod-module.c:793 kmod_module_remove_module() could not remove 'fastecho': Device or resource busy
rmmod: ERROR: could not remove module fastecho.ko: Device or resource busy

といったエラーが出てinsmodするために再起動しないといけない。
それはだるいのでどうにかしたい。

以上、学生アドベントカレンダー25日目の記事でした。