PHP(反)序列化学习

blog 417

前言

在很多语言中,将对象的状态信息转为可存储或可传输的过后才能就是序列化。序列化的逆向过程则是反序列化。主要是为了方便对象的传输,通过文件、网络等方式将序列化后的字符串进行传输,最终可以通过反序列化获取之前的对象。

正文

PHP反序列化后的基本类型表达:

布尔值(bool): a:value => a:0
整数型(int): i:value => i:1
字符串型(str): s:length:"value"; => s:4:"aaaa"
数组型(array): a:<length>:{key,value,pairs}; => a:1:{i:1,s:1:"a"}
对象型(object): O:<class_name_length>:
NULL类型: N

最终序列化数据格式如下:

<class_name>:<number_of_properties>:{<properties>};

接下来我们通过一个简单的例子来学习序列化之后得到的字符串以及反序列化后获取的对象

<?php 
    class Test{
      public $name = 'x1ong';
      public $age  = 18;
      public $sex = 'male';

      public function printData(){
        echo 'Your Name: '.$this->name . '<br>' . 'Your Age: ' . $this->age . '<br>' . "You Sex: " . $this->sex; 
      }
    }


$obj = new Test();  // 创建对象

$serialized = serialize($obj);  // 序列化一个对象   

var_dump($serialized  . "<br>");       // 将序列化之后的字符输出到页面并显示其数据类型

//序列化之后得到: O:4:"Test":3:{s:4:"name";s:5:"x1ong";s:3:"age";i:18;s:3:"sex";s:4:"male";}


// 对序列化后的内容进行反序列化 将字符串转为对象
$unserialized = unserialize('O:4:"Test":3:{s:4:"name";s:5:"x1ong";s:3:"age";i:18;s:3:"sex";s:4:"male";}');  


var_dump($unserialized);  // 打印这个对象
echo "<br>";  // 输出换行

// 上面将反序列后得到的对象赋值给$unserialized 因此我们直接引用$unserialized即可
$unserialized->printData();  // 执行Test类下面的printData方法

?>

以下为运行结果:

PHP(反)序列化学习

可以发现将序列化之后的内容O:4:"Test":3:{s:4:"name";s:5:"x1ong";s:3:"age";i:18;s:3:"sex";s:4:"male";}是一个字符串即String类型,将其进行反序列化后得到一个对象(object)。而我们将返回的对象赋值于$unserialize变量。则我们直接可以通过引用$unserialized调用其下的printData()方法

这也就可说明:

序列化:把复杂的数据类型压缩到一个字符串中 要序列化的数据类型可以是数组,字符串,对象等  函数 : serialize()

反序列化:通过序列化后的字符串获取之前的对象。函数:unserialize()

当对象被销毁的时候,则还是可以通过序列化之后的内容将其反序列化调用这个对象里面的方法:

<?php 
    class Test{
      public $name = 'x1ong';
      public $age  = 18;
      public $sex = 'male';

      public function printData(){
        echo 'Your Name: '.$this->name . '<br>' . 'Your Age: ' . $this->age . '<br>' . "You Sex: " . $this->sex; 
      }
    }

// 对序列化后的内容进行反序列化 将字符串转为对象
$unserialized = unserialize('O:4:"Test":3:{s:4:"name";s:5:"x1ong";s:3:"age";i:18;s:3:"sex";s:4:"male";}');  


var_dump($unserialized);
echo "<br>";
// 上面将反序列后得到的对象赋值给$unserialized 因此我们直接引用$unserialized即可
$unserialized->printData();  // 执行Test类下面的printData方法

?>

执行结果:

PHP(反)序列化学习

序列化格式解释

通过上述代码。我们将其序列化后得到以下字符串:


O:4:"Test":3:{s:4:"name";s:5:"x1ong";s:3:"age";i:18;s:3:"sex";s:4:"male";}


O 表示对象类型
4 表示对象名的长度
Test 表示对象名
3 表示对象中有三个列
{} 里面表示类的属性
s 表示String类型
4 表示第一个属性名的长度
name 表示第一个属性名
s 表示第一个属性的值类型为String
5 表示第一个属性的值长度为5字符
x1ong 表示第一个属性的值为x1ong
s 表示第二个属性的值类型为String
3 表示第二个属性名的长度为3字符
age 表示第二个属性名
i 表示第二个属性的值类型为integer(整型)
18 表示第二个属性的值
s 表示第三个属性名为String类型
3 表示第三个属性名的长度为3个字符
sex 表示第三个属性名
s 表示第三个属性的值为String类型
4 表示第三个属性的值长度为4个字符
male 表示第三个属性的值

以上是对类进行序列化之后得到字符串。

下面我们介绍数组进行序列化之后得到的字符串

<?php 
  $data = ['x1ong','tai','shuai','le','ba'];

  echo serialize($data);
?>


运行得到:
a:5:{i:0;s:5:"x1ong";i:1;s:3:"tai";i:2;s:5:"shuai";i:3;s:2:"le";i:4;s:2:"ba";}

a 表示array即数组
5 表示数组元素的个数
{} 里面则代表数组元素部分信息
i 表示整型即integer(整型)
0 表示第一个数组元素的下标  
s 表示第一个数组元素的值类型即string
5 表示第一个数组元素的长度
x1ong 则表示第一个数组元素的值
i 表示整型 integer
1 表示第二个数组元素的下标
s 表示第二个数组元素值的数据类型即string
3 表示第二个数组值的长度
tai 表示第二数组值的值

后面的也是如此,这里不再分析。

下面则是针对关联数组序列化后得到的字符串分析

<?php 
  $data = [
      'name' => 'x1ong',
      'age' => 17,
      'addr' => 'HeNan'
  ];

  echo serialize($data);
?>


运行得到:
a:3:{s:4:"name";s:5:"x1ong";s:3:"age";i:17;s:4:"addr";s:5:"HeNan";}

a 表示 数组类型 array
3 表示 数组中元素的个数
{} 里面为数组元素的信息
s 表示键名的类型为string类型
4 表示第一个数组元素的键名的长度
name 表示第一个数组元素的键名
s 表示第一个数组元素值的数据类型为string类型
5 表示第一个数组元素的值长度
x1ong 表示第一个数组的值
s 表示string类型
3 表示第二个数组元素键名的长度
age 表示第二个数组元素的键名
i 表示第二数组元素的值为integer类型即整型
17表示第二个数组元素的值为17
s 表示string类型即字符串类型
4 表示第三个数组元素键名的长度
addr 表示第三个数组元素的键名
s 表示string类型
5 表示第三个数组元素值的长度
HeNan 表示第三个数组元素的值

接下来是字符串序列化后的格式:

<?php 

  $text = 'hello world';

  echo serialize($text);
?>

得到:
s:11:"hello world";  

s表示string对象
11 表示字符串的长度
hello world 表示字符串里面的值

反序列化之后的调用

类进行反序列化的调用:

<?php 
    class Test{
      public $name = 'x1ong';
      public $age = 17;
      public function printData(){
        echo 'Your Name: ' . $this->name . '<br>' . 'Your Age: ' . $this->age . '<br>';
      }
    }

$unserialized = unserialize('O:4:"Test":2:{s:4:"name";s:5:"x1ong";s:3:"age";i:18;}');

$unserialized->printData();
?>

unserialize()函数将其反序列化后得到一个对象,而我们直接通过该对象调用其printData()方法即可。调用属性也是如此。这里不再演示。

运行结果:

PHP(反)序列化学习

对数组进行反序列化后调用:

<?php 
  $info = [
    'name' => 'x1ong',
    'age' => 17,
    'addr' => 'Henan',
  ];

$unserialized = unserialize('a:3:{s:4:"name";s:5:"x1ong";s:3:"age";i:17;s:4:"addr";s:5:"Henan";}');


var_dump($unserialized);
?>

运行结果:

array(3) { ["name"]=> string(5) "x1ong" ["age"]=> int(17) ["addr"]=> string(5) "Henan" }

数组进行序列化的时候,返回的是一个字符串。而进行反序列化的时候,返回的是一个数组。直接输出即可。字符串反序列化调用与此基本相似。直接输出即可。但是字符串反序列化后返回一个字符串数组返回的则是数组

访问权限修饰符

有时候我们需要定义某个方法或属性,但是不希望被其他类或者对象访问,那么我们就可以在方法或属性的前面加上访问控制修饰符。

修饰符之public

<?php 

    class Test{  // 父类
      public $username = 'x1ong';

      public function printText(){  // 公有方法
        echo '我是printText打印出来的 <br>';
      }

    }

    class child extends Test{  // 子类
        function a(){
          parent::printText();  // 调用公有方法  打印 '我是printText打印出来的'
        }

    }


$obj = new Test();

echo $obj->username; // 从对象中调用公有属性username

echo "<br>"; // 输出换行

$obj->printText();  // 从对象中调用公有方法

$son = new child();  // 创建子类对象

$son->a(); // 从子类对象中调用父类的公有方法PrintText()方法

?>

运行结果:

PHP(反)序列化学习

可以发现,以public访问控制修饰符定义的属性或者方法都可以被实例对象或子类引用。

修饰符之private

<?php 

    class Test{  // 父类
      private $username = 'x1ong';

      private function printText(){  // 公有方法
        echo '我是printText打印出来的 <br>';
      }

    }

    class child extends Test{  // 子类
        function a(){
          parent::printText();  // 调用公有方法  打印 '我是printText打印出来的'
        }

    }



$obj = new Test();

echo $obj->username; // 从对象中调用公有属性username

echo "<br>"; // 输出换行

$obj->printText();  // 从对象中调用公有方法


$son = new child();  // 创建子类对象

$son->a(); // 从子类对象中调用父类的公有方法PrintText()方法


?>

运行结果:

PHP(反)序列化学习

通过结果发现,private定义的属性或者方法。是不能直接被实例对象或者子类调用的。

修饰符之protected

实例:

<?php 

    class Test{  // 父类
      protected $username = 'x1ong';

      protected function printText(){  // 公有方法
        echo '我是printText打印出来的 <br>';
      }

    }

    class child extends Test{  // 子类
        function a(){
          parent::printText();  // 调用公有方法  打印 '我是printText打印出来的'
        }

    }



$obj = new Test();

echo $obj->username; // 从对象中调用公有属性username

echo "<br>"; // 输出换行

$obj->printText();  // 从对象中调用公有方法


$son = new child();  // 创建子类对象

$son->a(); // 从子类对象中调用父类的公有方法PrintText()方法


?>

运行结果:

PHP(反)序列化学习

发现以protected定义的方法或者属性是不能被实例对象访问的。因此我们将实例对象访问的代码段去掉。再次测试:

<?php 

    class Test{  // 父类
      protected static $username = 'x1ong';

      protected function printText(){  // 公有方法
        echo '我是printText打印出来的 <br>';
      }

    }

    class child extends Test{  // 子类
        function a(){
          parent::printText();  // 调用公有方法  打印 '我是printText打印出来的'
        }
        function b(){
          echo parent::$username;  // 调用父类protected定义的属性
        }

    }
$son = new child();  // 创建子类对象

$son->a(); // 从子类对象中调用父类的公有方法PrintText()方法


$son->b();
?>

运行结果:

PHP(反)序列化学习

总结

public 表示全局的 类内部外部和实例对象都可以访问。

private表示私有的,只有本类内部可以使用,实例对象调用则直接报错。

protected表示受保护的,只有本类或子类或父类中可以访问。

权限控制修饰符序列化区别

public属性序列化:

<?php 

  class Test{
    public $username = 'x1ong';
    public $password = '*******';
  }


  $obj = new Test();

  $serialized = serialize($obj);

  echo $serialized;
 ?>

运行得到:

O:4:"Test":2:{s:8:"username";s:5:"x1ong";s:8:"password";s:7:"*******";}

private属性序列化:

<?php 

  class Test{
    private $username = 'x1ong';
    private $password = '*******';
  }


  $obj = new Test();

  $serialized = serialize($obj);

  echo $serialized;
 ?>

运行得到

O:4:"Test":2:{s:14:"Testusername";s:5:"x1ong";s:14:"Testpassword";s:7:"*******";}

可以发现private属性序列化和public属性序列化得到的结果是不同的。private定义的属性只有本类可以访问。而他在序列化的时候,会在属性值中加入其类名。因此我们看到的是Testusername。Test则为类名,而username则为私有属性名。第二个Testpassword也是同理。还有就是他们的属性值长度不同。而Testusername明明是12个字符。为什么长度是14呢?

Testusername类名和属性之间看似没有间隔,实则是有间隔的。如下:

O:4:"Test":2:{s:14:"\0Test\0username";s:5:"x1ong";s:14:"\0Test\0password";s:7:"*******";}

private定义的属性在序列化的时候。会在属性的前面加上类名。而类名的开头和结尾则是加上\0。这个\0并不是真正的\0。而是ASCII为0的字符。即空字符NULL(不可见字符)。因此我们是看不到的。这也就解开了我们上个疑惑<Testusername明明是12个字符,为什么长度是14呢?其实这里的14包括类名前后的两个0。Testpassword也是如此>如果想要看到属性名中的类名前后两个不可见字符。我们需要使用urlencode()函数进行编码。

echo urlencode($serialized);


得到:
O%3A4%3A%22Test%22%3A2%3A%7Bs%3A14%3A%22%00Test%00username%22%3Bs%3A5%3A%22x1ong%22%3Bs%3A14%3A%22%00Test%00password%22%3Bs%3A7%3A%22%2A%2A%2A%2A%2A%2A%2A%22%3B%7D



而其中的%00则是经过url编码之后的\0即ASCII码为0的字符。

这里有一道关于private定义的属性反序列题目解析。[极客大挑战 2019]PHP

protected属性序列化:

protected属性序列化其结果基本与private属性序列化之后得到的结果一致。不同之处在与private表示属性名的时候,是类名加上属性名。而protected在表示属性名的时候,是使用*加上属性名表示。protected在序列化的时候*的前后同样会有\0。即ASCII为0的不可见字符

protected序列化后的结果:

O:4:"Test":2:{s:11:"*username";s:5:"x1ong";s:11:"*password";s:7:"*******";}


echo urlencode($serialized);

得到:
O%3A4%3A%22Test%22%3A2%3A%7Bs%3A11%3A%22%00%2A%00username%22%3Bs%3A5%3A%22x1ong%22%3Bs%3A11%3A%22%00%2A%00password%22%3Bs%3A7%3A%22%2A%2A%2A%2A%2A%2A%2A%22%3B%7D

其中%2A则表示*。而%00则表示ASCII码为0的不可显示字符。

PHP魔术方法

在PHP中其实是存在魔术方法的。该类方法在一定情况下会自动调用。不需要手动调用。但是存在一定的调用条件,比如__destruct是对象被销毁的时候进行调用。通常PHP在程序块执行结束时会进行垃圾回收。这时进行对象销毁,之后会自动调用__destruct魔术方法。

注意:所有的魔术方法必须声明为public

常见的魔术方法触发方式如下:

当对象被创建时: __construct

当对象被销毁时: __destruct

当对象调用某个方法。其方法不存在时调用:__call

当对象读取某个属性时,若读取的属性不存在时调用:__get

当对象设置一个属性时,若设置的属性不存在时调用: __set

当对象被作用字符串输出时调用:__toString

当对象被序列化之前调用:__sleep

当对象被反序列化之前调用:__wakeup

下面我们只介绍几个常用的:

实例:

<?php 
    class Test{
      public $username = 'x1ong';
      public $age = 17;
      public $addr = 'HenNan';


      // __construct
      public function __construct(){
        echo "construct....<br>";
      }

      // __destruct
      public function __destruct(){
        echo "dectruct....<br>";
      }

      // wakeup
      public function __wakeup(){
        echo "wakeup....<br>";
      }

      // sleep
      public function __sleep(){
        echo "sleep...<br>";
        return array('username','age');   // __sleep()方法返回值必须是一个数组
      }

      // __toString
      public function __toString(){
        return "toString....<br>";
      }

      // 打印类中的属性
      public function printData(){
        echo "*Your Name: " . $this->username . "<br>". " *Your Age:" . $this->age . "<br>". " *Your addr: " . $this->addr . "<br>";
      }

    }

$obj = new Test();  // 新建一个对象  此时调用了__construct

$obj->printData();  // 调用对象中的printData()方法


echo $obj;  // 打印一个对象。此时会调用__toString()方法。

// serialize() 函数会检查类中是否存在一个魔术方法 __sleep()。如果存在,该方法会先被调用,然后才执行序列化操作。
$serialized = serialize($obj);    // 序列化前先执行__sleep()

echo $serialized;

// unserialize() 函数会检查类中是否存在一个魔术方法 __wakeup()。如果存在,该方法会先被调用,然后再unserialize() 函数会检查类中是否存在一个魔术方法 __sleep()。如果存在,该方法会先被调用,然后才执行序列化操作。执行序列化操作。
$unserialized = unserialize('O:4:"Test":2:{s:8:"username";s:5:"x1ong";s:3:"age";i:17;}');


$unserialized->printData();  // 利用反序列后返回的对象调用printData()方法

echo "<br>"

?>

运行结果:

construct....
*Your Name: x1ong
*Your Age:17
*Your addr: HenNan
toString....
sleep...
O:4:"Test":2:{s:8:"username";s:5:"x1ong";s:3:"age";i:17;}
wakeup....
*Your Name: x1ong
*Your Age:17
*Your addr: HenNan

dectruct....
dectruct....

可以发现,construct__魔术方法在第一个被执行了。我们并没有调用它。而我们只是先new了一个对象。可知,该方法会在创建对象时调用。而接着我们调用了对象中的printData()方法。因此打印出了三行内容。即Nameage以及addr

后来我们使用echo尝试输出$obj这个对象。发现它并没有报错。而是打印出了toString....。这里要注意:__toString方法必须要使用return 返回一个字符串。否则会报如下错误。

Catchable fatal error: Method Test::__toString() must return a string value in /private/var/www/html/index.php on line 47

我们使用serialize()方法的时候。会先去调用__sleep()方法。这里的__sleep()方法return的返回值必须是一个数组。其元素必须为我们的属性名。否则会如下错误:

Notice: serialize(): "test" returned as member variable from __sleep() but does not exist in /private/var/www/html/index.php on line 50

Notice: serialize(): "ae" returned as member variable from __sleep() but does not exist in /private/var/www/html/index.php on line 50

_sleep()返回的数组元素,取决于我们序列化返回字符串中的元素

当我们的return返回的元素不是属性名。则会提示性错误但是程序依旧会执行。

 public function __sleep(){
        echo "sleep...<br>";
        return array('test','ae');   // __sleep()方法返回值必须是一个数组
      }


序列化后的结果:
O:4:"Test":2:{s:4:"test";N;s:2:"ae";N;}

可以发现,我们序列化后的属性名变成了testae

注意:所有对象被创建的时候、serialize()执行的时候、unserialize()执行的时候等等都会先去看类中是否有__construct和__sleep()以及__wakeup()等等,如果他们存在,则先调动他们。

unserialize()执行的时候 函数会检查类中是否存在一个魔术方法 __wakeup()。如果存在,该方法会先被调用,然后才执行序列化操作。__wakeup方法对返回的数据类型没有要求,甚至可以不返回值。直接echo

之后我们通过反序列化后重建的对象调用printData()方法。发现可以调用。而__destruct是在对象被销毁的时候调用。

那么他为什么会被调用两次呢?是因为我们实例化对象一次。其销毁的时候会执行__destruct。而我们反序列化后重建对象被销毁的时候,也会执行__destruct方法。

在 PHP 中有一种垃圾回收机制,当对象不能被访问时就会自动启动垃圾回收机制,收回对象占用的内存空间。而析构函数__destruct正是在垃圾回收机制回收对象之前调用的。程序执行完成后也会自动执行__destruct方法

分享