Tag Archive APC

CodeIgniter: php-amqplib

ลองเขียน custom driver สำหรับติดต่อกับ RabbitMQ / AMQP ยังไม่สมบูรณ์แต่ก็ดีกว่าการที่ต้องมา connect ผ่าน php-amqplib ทุกครั้งที่จะใช้ตามปกติ

  1. config file ก่อนตามระเบียบ
    <?php
    defined('BASEPATH') or exit('No direct script access allowed');
    
    /*
    | -------------------------------------------------------------------
    | RABBITMQ CONNECTIVITY SETTINGS
    | -------------------------------------------------------------------
    | This file will contain the settings needed to access your RabbitMQ.
    |
    | For complete instructions please consult the 'RabbitMQ Connection'
    | page of the User Guide.
    |
    | -------------------------------------------------------------------
    | EXPLANATION OF VARIABLES
    | -------------------------------------------------------------------
    |
    |    ['connection_timeout'] => 3.0,
    |    ['context'] => null,
    |    ['heartbeat'] => 0
    |    ['host'],
    |    ['insist'] => false,
    |    ['keepalive'] => false,
    |    ['locale'] => 'en_US',
    |    ['login_method'] => 'AMQPLAIN',
    |    ['login_response'] => null,
    |    ['password'],
    |    ['port'],
    |    ['read_write_timeout'] => 3.0,
    |    ['user'],
    |    ['vhost'] => '/',
     */
    $config['RabbitMQ']['connects'] = [
        'default' => [
            'connection_timeout' => 3.0,
            'context' => null,
            'heartbeat' => 0,
            'host' => '127.0.0.1',
            'insist' => false,
            'keepalive' => false,
            'locale' => 'en_US',
            'login_method' => 'AMQPLAIN',
            'login_response' => null,
            'password' => 'guest',
            'port' => 5672,
            'read_write_timeout' => 3.0,
            'user' => 'guest',
            'vhost' => '/',
        ],
    ];
    
    /*
    | -------------------------------------------------------------------
    | RABBITMQ DEBUG SETTINGS
    | -------------------------------------------------------------------
    |
     */
    $config['RabbitMQ']['debug'] = false;
    
  2. ถึงคิวไฟล์ driver ที่จะเป็นตัวกลางระหว่าง codeigniter และ php-amqplib
    <?php
    use PhpAmqpLib\Connection\AMQPStreamConnection;
    use PhpAmqpLib\Message\AMQPMessage;
    
    class RabbitMQ extends AMQPStreamConnection
    {
    
        public function __construct($config = array())
        {
            $this->CI = &get_instance();
    
            $this->config = $config['RabbitMQ'];
    
            if ($this->config['debug'] == true) {
                define('AMQP_DEBUG', true);
            }
        }
    
        public function connect($dsnId = 'default') {
            $dsn = $this->config['connects'][$dsnId];
    
            $this->connection = new AMQPStreamConnection($dsn['host'], $dsn['port'], $dsn['user'], $dsn['password'], $dsn['vhost'], $dsn['insist'], $dsn['login_method'], $dsn['login_response'], $dsn['locale'], $dsn['connection_timeout'], $dsn['read_write_timeout'], $dsn['context'], $dsn['keepalive'], $dsn['heartbeat']);
    
            $this->channel = $this->connection->channel();
        }
    
        public function disconnect()
        {
            $this->channel->close();
            $this->connection->close();
        }
    
        public function setMessageJson($datas)
        {
            $msg_body = json_encode($datas);
    
            $properties = [
                'content_type' => 'application/json',
                'delivery_mode' => AMQPMessage::DELIVERY_MODE_PERSISTENT,
            ];
    
            return new AMQPMessage($msg_body, $properties);
        }
    
    }
  3. ที่นี้ก็ controller ที่จะทำงานให้
    <?php
    defined('BASEPATH') or exit('No direct script access allowed');
    
    class Messaging extends CI_Controller
    {
        public function __construct()
        {
            parent::__construct();
    
            $this->load->driver('RabbitMQ');
        }
    
        public function receive()
        {
            set_time_limit(0);
    
            $this->rabbitmq->connect();
    
            $exchange_name = 'customers';
            $queue_name = 'invoices';
    
    /**
     * Declares exchange
     *
     * @param string $exchange_name
     * @param string $type
     * @param bool $passive
     * @param bool $durable
     * @param bool $auto_delete
     * @param bool $internal
     * @param bool $nowait
     * @param array $arguments
     * @param int $ticket
     * @return mixed|null
     */
            $this->rabbitmq->channel->exchange_declare($exchange_name, 'fanout', false, true, false);
    
    /**
     * Declares queue, creates if needed
     *
     * @param string $queue
     * @param bool $passive
     * @param bool $durable
     * @param bool $exclusive
     * @param bool $auto_delete
     * @param bool $nowait
     * @param array $arguments
     * @param int $ticket
     * @return mixed|null
     */
            list($queueName, $message_count, $consumer_count) = $this->rabbitmq->channel->queue_declare('', false, false, true, false);
            $this->rabbitmq->channel->queue_bind($queue_name, $exchange_name);
    
            $callback = function ($msg) {
    
                $datas = json_decode($msg->body, true);
                fwrite(fopen('RabbitMQReceive.txt', 'a+'), print_r($datas, true));
    
                sleep(substr_count($msg->body, '.'));
    
                /* delete message */
                $msg->delivery_info['channel']->basic_ack($msg->delivery_info['delivery_tag']);
            };
    
    /**
     * Starts a queue consumer
     *
     * @param string $queue_name
     * @param string $consumer_tag
     * @param bool $no_local
     * @param bool $no_ack
     * @param bool $exclusive
     * @param bool $nowait
     * @param callback|null $callback
     * @param int|null $ticket
     * @param array $arguments
     * @return mixed|string
     */
            $this->rabbitmq->channel->basic_consume($queue_name, '', false, false, false, false, $callback);
    
            while (count($this->rabbitmq->channel->callbacks)) {
    /**
     * Wait for some expected AMQP methods and dispatch to them.
     * Unexpected methods are queued up for later calls to this PHP
     * method.
     *
     * @param array $allowed_methods
     * @param bool $non_blocking
     * @param int $timeout
     * @throws \PhpAmqpLib\Exception\AMQPOutOfBoundsException
     * @throws \PhpAmqpLib\Exception\AMQPRuntimeException
     * @return mixed
     */
                $this->rabbitmq->channel->wait();
            }
    
            $this->rabbitmq->channel->close();
            $connection->close();
        }
    
        public function send()
        {
            $this->rabbitmq->connect();
    
            $exchange_name = 'customers';
            $queue_name = 'invoices';
    
    /**
     * Declares exchange
     *
     * @param string $exchange_name
     * @param string $type
     * @param bool $passive
     * @param bool $durable
     * @param bool $auto_delete
     * @param bool $internal
     * @param bool $nowait
     * @param array $arguments
     * @param int $ticket
     * @return mixed|null
     */
            $this->rabbitmq->channel->exchange_declare($exchange_name, 'fanout', false, true, false);
    
    /**
     * Declares queue, creates if needed
     *
     * @param string $queue
     * @param bool $passive
     * @param bool $durable
     * @param bool $exclusive
     * @param bool $auto_delete
     * @param bool $nowait
     * @param array $arguments
     * @param int $ticket
     * @return mixed|null
     */
            list($queueName, $message_count, $consumer_count) = $this->rabbitmq->channel->queue_declare($queue_name, false, true, false, false);
    
            $datas = [
                'rand' => rand(0, 100),
                'time' => date('Y-m-d H:m:s'),
            ];
    
    /**
     * Declares message
     *
     * @param array $datas
     * @return string
     */
            $msg = $this->rabbitmq->setMessageJson($datas);
    
    /**
     * Publishes a message
     *
     * @param AMQPMessage $msg
     * @param string $exchange
     * @param string $routing_key
     * @param bool $mandatory
     * @param bool $immediate
     * @param int $ticket
     */
            $this->rabbitmq->channel->basic_publish($msg, $exchange_name, $queue_name);
    
            echo '<br><pre>' . print_r($datas, true), '</pre>';
    
            $this->rabbitmq->disconnect();
    
        }
    
    }
    
  4. ทดสอบโดยเรียก http://localhost/CodeIgniter-3.1.3/messaging/send และ http://localhost/CodeIgniter-3.1.3/messaging/receive

CodeIgniter: Cache

เพื่อให้ผู้ใช้เข้าหน้าเพจด้วยความรวดเร็วจึงต้องทำ caching เก็บส่วนที่ไม่ได้เปลี่ยนแปลงบ่อย และสามารถใช้ซ่้ำๆ ได้

เริ่มจากสร้างไฟล์เก็บ config ขึ้นมาก่อน

<?php
defined('BASEPATH') or exit('No direct script access allowed');

$config['caching']['adapter'] = 'memcached';
$config['caching']['backup'] = 'file';

โดย

adapter
คือ จะเลือกใช้ driver ตัวไหนในการทำ cache ระหว่าง

Drivers Value
Alternative PHP Cache (APC) Caching apc
Dummy Cache dummy
File-based Caching file
Memcached Caching memcached
Redis Caching redis
WinCache Caching wincache
backup
เป็นตัวเลือกสำรองถ้าตัวแรกไม่สามารถทำงานได้ เช่น เครื่องที่เราใช้ไม่ได้ลง Memcached ไว้ ก็จะใช้ file แทน ( cache file ถูกเก็บไว้ที่ \application\cache หรือ ตาม config $config[‘cache_path’])

โหลด Driver ได้ 2 วิธีคือ

Auto-loading
เหมาะกับงานที่ต้องใช้ cache บ่อยๆ ทำได้โดยเพิ่ม ‘cache’ เข้าไป

$autoload['drivers'] = [..., 'cache', ...];
load driver
โหลด driver เฉพาะ controller ที่ต้องใช้ cache เท่านั้น

    public function __construct()
    {
        parent::__construct();

        $this->load->driver('cache');
    }

การใช้งาน

<?php
defined('BASEPATH') or exit('No direct script access allowed');

class Welcome extends CI_Controller
{
    /**
     * Index Page for this controller.
     *
     * Maps to the following URL
     *         http://example.com/index.php/welcome
     *    - or -
     *         http://example.com/index.php/welcome/index
     *    - or -
     * Since this controller is set as the default controller in
     * config/routes.php, it's displayed at http://example.com/
     *
     * So any other public methods not prefixed with an underscore will
     * map to /index.php/welcome/<method_name>
     * @see https://codeigniter.com/user_guide/general/urls.html
     */
    public function index()
    {
        $cacheKey = 'home' . date('Y-m-d'); /* unque id */
        $cacheKey = mb_ereg_replace('([^\w\s\d\-_~,;\[\]\(\).])', '', $cacheKey);

        if (!$datas = $this->cache->get($cacheKey)) {

            $datas = $this->load->view('welcome_message', '', true);

            $this->cache->save($cacheKey, $datas, 300);
        }

        echo $datas;
    }

}

เพราะว่า file driver เป็นการเขียนไฟล์ลงบน hard disk จริงๆ จึงควรแน่ใจว่าไฟล์ถูกรูปแบบที่ os สามารถเขียนไฟล์ได้ โดนอาจจะใช้ md5 เข้ารหัส $cacheKey ก่อน

$cacheKey = md5($cacheKey);

หรือใช้

$cacheKey = mb_ereg_replace('([^\w\s\d\-_~,;\[\]\(\).])', '', $cacheKey);

อ่านเพิ่มเติม

ใช้ cache ใน YII 2

เวลาทำเว็บยุคปัจจุบันที่ปรับข้อมูลสำหรับ user แต่ละคน แต่ความจริงๆ แล้วแต่ละคนไม่ได้รับข้อมูลที่ต่างกันทั้งหมด บางส่วนก็เหมือนกับคนอื่นๆ ตามเงื่อนไข แทนที่จะ render ใหม่ทุกครั้ง ในระบบขนาดใหญ่ก็นิยมแยกเป็นส่วนๆแล้วใช้ระบบแคช จำส่วนนั้นๆ แทนที่การคำนวณใหม่ สำหรับทุกคน สำหรับทุกครั้งที่มีการเรียกหน้าเว็บ

ใน YII 2 มี cache ให้เลือกทำ Data Caching หลายตัว เช่น APC, file, memcache, memcached, WinCache, XCache, YII2, Zend แต่หลักการเหมือนๆ กันคือ

  1. สร้าง Cache Keys ขึ้นมาให้แยกต่างจากตัวอื่นๆ ในกลุ่ม นิยมใช้ ตัวแปรที่ส่งเข้าฟังก์ชั่นมาเป็น unique ให้ต่างจากตัวอื่นๆ
  2. เช็กดูว่า Cache Keys ตัวนี้มีอยู่รึเปล่า ถ้ามีก็ให้เอา cache มาใช้โดยไม่คำนวณใหม่ ถ้าไม่มีก็สร้างขึ้นใหม่
  3. เพราะว่าข้อมูลบางส่วนอาจจะเก่า หรือใช้ไม่ได้ เช่น มีการเพิ่มข้อมูลเข้ามาใหม่ หรือมีการแสดงวันที่อยู่ข้างใน ระบบแคช จึงมีทางเลือกให้ลบออกตามระยะเวลา expired / invalidated ตามช่วงเวลา yii ใช้หน่วยเวลาเป็น วินาที ตามตัวอย่างใช้ 86400 คือ 1 วัน แสดงว่าทุกๆ วันไฟล์นี้จะโดนลบออกไป
	public function actionIndex($id)
	{

		/* ใช้ cache ตัวไหนเลือกจาก http://www.yiiframework.com/doc-2.0/guide-caching-data.html Supported Cache Storage */

		$cache = Yii::$app->cache;

		/* สร้าง Cache Keys ชื่อ saveName ต่อด้วยตัวแปรที่ส่งเข้ามา มีหลายตัวก็ต่อไปเรื่อยๆ */

		$cacheKeys = 'saveName'.$id;

		/* ดูว่ามีแคชชื่อนี้รึยัง */

		$data = $cache->get($cacheKeys);

		/* ถ้าไม่มีก็สร้างใหม่ใส่ตัวแปร data  */

		if ($data === false) {

		/*
		ถ้าไม่ต้องการ layout ใช้
			$data = $this->renderPartial('index', [
		*/
			$data = $this->render('index', [
				'id' => $id,
			]);

		/* เก็บข้อมูลเข้า cache เวลาถ้าไม่ใส่จะไม่ลบออกตามเวลา */

			$cache->set($cacheKeys, $data, 86400);

		}

		/* ส่ง output ออกไปทั้งจาก cache หรือจากการ render ใหม่ */
		return $data;
	}

ทดสอบดูโดย

  • cacheKey ถ้่าหากตรวจพบว่าไม่ใช้ตัวอักษร alphanumeric โดย function alphanumeric() หรือ ยาวกว่า 32 ตัวอักษร จะถูก hash เป็น string ยาว 32 ตัวอักษร โดยใช้ md5()
  • เมื่อเปิด folder cache เช่น \backend\runtime\cache จะเห็นว่าไฟล์จะถูกสร้างมาใหม่ตามรูปแบบ อักษร 2 ตัวแรกของ cacheKey\cacheKey.bin เปิดดูข้างในจะเป็นข้อมูลที่ถูก เปลี่ยนโดย function serialize() เก็บเอาไว้
  • เปลี่ยนตัวแปรที่ส่งเข้า function ดูว่าถ้าตัวแปรเหมือนเดิมมันจะไม่สร้างไฟล์ใหม่ เว็บเราก็แสดงผลเร็วกว่า ถ้าตัวแปรไม่เหมือนกันก็จะมีไฟล์ถูกสร้างมาใหม่