Laravel4+websocket 導入

Posted: 2014-03-01 19:07 |  laravel 
モダンPHPerにはおなじみlaravel4を使って新しい技術でもあるwebsocketを実装します。
laravel4のプラグインを探せばLatchetというものがあり、こちらを導入すれば何も苦労せずに実装できますが
あえてこちらではなく、
Latchetでも組み込まれているphp websocketクライアント

Ratchet

を自力で導入してみましょう。
まずはRatchetをcomposer経由で導入します。
composer.json
	"require": {
		"laravel/framework": "4.0.*",
		"cboden/Ratchet": "0.3.*"
	},
$ php composer.phar install
導入はいつも通り上記で終わりです。
Ratchetの実際の使用方法等はサイトで確認してください(後日このブログでも紹介しようと思います。)
という事でチュートリアル程度のものを実装します。
まずはなんでもいいんですが、extensionsとしてappの配下にディレクトリを作り、
そこにwebsocketを実装すると仮定します。
	"autoload": {
		"classmap": [
			"app/commands",
			"app/controllers",
			"app/models",
			"app/database/migrations",
			"app/database/seeds",
			"app/tests/TestCase.php",
			"app/extensions"
		]
	},
$ php composer.phar dump-autoload
サーバを実装します。
app/extensions/Wamp/Chat.php
namespace Wamp;
use Ratchet\MessageComponentInterface;
use Ratchet\ConnectionInterface;
use Guzzle\Http\Url;

/**
 * WebSocket Chat
 * @package Laravel4
 * @category extension
 * 
 */
class Chat implements MessageComponentInterface
{
    protected $clients;
	private $_room;

    public function __construct()
	{
        $this->clients = new \SplObjectStorage;
    }

	/**
	 * 接続開始
	 * @param \Ratchet\ConnectionInterface $conn
	 */
    public function onOpen(ConnectionInterface $conn)
	{
        // 
        $this->clients->attach($conn);
        echo "New connection! ({$conn->resourceId})\n";
    }
	
	/**
	 * メッセージ受信時の処理
	 * onMessage
	 * @param \Ratchet\ConnectionInterface $from
	 * @param type $msg
	 */
	public function onMessage(ConnectionInterface $from, $msg)
	{
        $numRecv = count($this->clients) - 1;
        echo sprintf('Connection %d sending message "%s" to %d other connection%s' 
		. "\n" , $from->resourceId, $msg, $numRecv, $numRecv == 1 ? '' : 's');

	// 接続しているすべてのクライアントに送信
        foreach ($this->clients as $client)
	{
		$client->send('hello');
        }
    }

	/**
	 * 切断
	 * @param \Ratchet\ConnectionInterface $conn
	 */
    public function onClose(ConnectionInterface $conn) {
        // The connection is closed, remove it, as we can no longer send it messages
        $this->clients->detach($conn);
        echo "Connection {$conn->resourceId} has disconnected\n";
    }

	/**
	 * socket Error
	 * @param \Ratchet\ConnectionInterface $conn
	 * @param \Exception $e
	 */
    public function onError(ConnectionInterface $conn, \Exception $e) {
        echo "An error has occurred: {$e->getMessage()}\n";
        $conn->close();
    }
}
サンプルそのまんまですね。
折角なのでwamp server bootをartisanコマンドにしてしまいましょう。
app/commands/WampBootCommand.php
use Illuminate\Console\Command;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Input\InputArgument;
use Ratchet\Server\IoServer;
use Ratchet\Http\HttpServer;
use Ratchet\WebSocket\WsServer;
use Wamp\Chat;


class WampBootCommand extends Command {
 
    /**
	 * コマンドライン実行名
     * @var string
     */
    protected $name = 'wamp:boot';
 
    /**
     * The console command description.
     * @var string
     */
    protected $description = "boot wamp server";
 
    /**
     * コマンド実行処理
     * @return void
     */
    public function fire()
    {
		$server = IoServer::factory(new HttpServer(new WsServer(new Chat())), 9990);
		$this->info('wamp boot');
		$this->info("av: " . implode(',', sys_getloadavg()));
		$this->info('Listening on port ' . 9990);
	    $server->run();
    }
}
portは9990としていますが、固定だとダサダサなので実際にはconfig等で指定する様にしましょう。
artisanに登録します。
app/start/artisan.php
$artisan->add(new WampBootCommand);
server起動方法は以下の様になり、起動するとメッセージがechoされます。
$ php artisan wamp:boot
wamp boot
av: 1.96044921875,1.7998046875,1.62841796875
Listening on port 9990
とりあえずそれっぽくロードアベレージなんかも返してみます。
簡単ですね。
サーバ側は基本的にこれだけです。
次にブラウザからアクセスしてみましょう。
適当なhtmlやテンプレートに下記のjsを書いてアクセスしてみてください(serverは起動しておく事)
var conn = new WebSocket('ws://localhost:9990');
conn.onopen = function(e) {
    console.log("Connection established!");
};

conn.onmessage = function(e) {
    console.log(e.data);
};
firefoxであればコンソールに文字列が表示されているはずです。
serverを立ち上げたコマンドの下にもechoされていると思います。
どうでしたでしょうか?
ここからは更にlaravel4らしくrouteやモデルを使い、より実務的な実装を行います。
Route::get('/socket/test/{id?}', 'TestController@pageSocketTest');
Routeに上記のような感じで追加しましょう。(dump-autoload忘れずに)
app/controllers/TestController.php
	/**
	 * 
	 * @param type $id
	 */
	public function pageSocketTest($id = null)
	{
                if(is_null($id)){
                    return 1;
                }
		return $id;
	}
そのまま引数を返却するだけのシンプルなものとします。
ブラウザからのアクセスももっと実用性ある方法にしましょう。
var conn = new WebSocket('ws://localhost:9990/socket/test/33');
conn.onopen = function(e) {
    console.log("Connection established!");
};

conn.onmessage = function(e) {
    console.log(e.data);
};
といってもパスでアクセスする様にしただけですね。
最初に実装したサーバを下記の様にしてみます。
app/extensions/Wamp/Chat.php
namespace Wamp;
use Ratchet\MessageComponentInterface;
use Ratchet\ConnectionInterface;
use Guzzle\Http\Url;
use Illuminate\Container\Container;
use Symfony\Component\HttpFoundation\Request;
use Illuminate\Routing\Router;

/**
 * WebSocket Chat
 * @package Laravel4
 * @category extension
 * 
 */
class Chat implements MessageComponentInterface
{
    protected $clients;
	private $_room;

    public function __construct()
	{
        $this->clients = new \SplObjectStorage;
    }

	/**
	 * 接続開始
	 * @param \Ratchet\ConnectionInterface $conn
	 */
    public function onOpen(ConnectionInterface $conn)
	{
        // 接続したクライアントを割当
        $this->clients->attach($conn);
        
    }
	
	/**
	 * メッセージ受信時の処理
	 * onMessage
	 * @param \Ratchet\ConnectionInterface $from
	 * @param type $msg
	 */
	public function onMessage(ConnectionInterface $from, $msg)
	{
        $numRecv = count($this->clients) - 1;
        echo sprintf('Connection %d sending message "%s" to %d other connection%s' 
		. "\n" , $from->resourceId, $msg, $numRecv, $numRecv == 1 ? '' : 's');
		$senderPath = $from->WebSocket->request->getPath();
		$request = \Request::create($senderPath, 'POST', ['data' => $msg]);
		\Request::replace($request->input());
		$element = \Route::dispatch($request)->getContent();
		// 接続しているクライアントあてに送信r
        foreach ($this->clients as $client)
		{
			$client->send($element);
        }
    }

	/**
	 * 切断
	 * @param \Ratchet\ConnectionInterface $conn
	 */
    public function onClose(ConnectionInterface $conn) {
        // The connection is closed, remove it, as we can no longer send it messages
        $this->clients->detach($conn);
        echo "Connection {$conn->resourceId} has disconnected\n";
    }

	/**
	 * socket Error
	 * @param \Ratchet\ConnectionInterface $conn
	 * @param \Exception $e
	 */
    public function onError(ConnectionInterface $conn, \Exception $e) {
        echo "An error has occurred: {$e->getMessage()}\n";
        $conn->close();
    }
}
もうちょっと楽な実装方法があるかもしれませんが、下記の様に
websocketサーバでパスを取得します。
内部的にはguzzleを使用しています。
		$senderPath = $from->WebSocket->request->getPath();
これでどのURIにアクセスしたいのかが判定できます。
つぎにrouterに実際にリクエストを送信しましょう
		$request = \Request::create($senderPath, 'POST', ['data' => $msg]);
		\Request::replace($request->input());
		$element = \Route::dispatch($request)->getContent();
laravel4で使用しているsymfonyコンポーネントを使って、送信します。
先ほどrouteに登録したURIに対してのアクセスとなります。
値を一緒に送信したい場合は、\Request::createの第3引数に配列で追加していきましょう。
この例は、ブラウザから送ったメッセージ(conn.send('hogefugapiyo'))を$_POST['data']で送信します。
こうする事で、普段のlaravel4の実装方法を大きく変わる事無く使用する事ができます。
\Route::dispatch($request)->getContent();
返却値は上記の様にして取得する事ができます。
今回はアクセス時に指定した引数の33がそのまま返却されるはずです。
あとはいつも通りモデル等を介してDBやRedisなどとやり取りが可能になります。
session情報は、websocket側は取得できない為Authやログインチェックは行えませんが、
Illuminate\Auth\GuardにあるgetNameなどを使用しながら取得等、
比較的苦労も少なく、様々な実装が可能ですので
モダンPHPだけではなくhtml5などの新しいものも融合して実装してみましょう。

ちなみに他のフレームワークでも同じような感じで実装はできますが、laravel4より簡単に実装できるものは無いような気がします

about ytake

執筆に参加しています


Laravel お役立ち情報

share



このエントリーをはてなブックマークに追加

Categories

laravel 45

DTM 0

music 0

PHP全般 31

0

JAPAN 1

WORLD 1

javascript 4

RDBMS 1

NoSQL 1

NewSQL 1

Recent Posts

Ad

comments powered by Disqus

GitHub

Social Links

Author


クリエイティブ・コモンズ・ライセンス
Yuuki Takezawa 作『Ytake Blog』はクリエイティブ・コモンズ 表示 - 非営利 4.0 国際 ライセンス で提供されています。

© ytake/comnect All Rights Reserved. 2014