ホームページ  >  記事  >  バックエンド開発  >  PHPのpack関数とunpack関数の詳しい紹介(コード付き)

PHPのpack関数とunpack関数の詳しい紹介(コード付き)

不言
不言転載
2019-02-25 09:58:113702ブラウズ

この記事では、PHP の Pack 関数と unpack 関数について詳しく紹介します (コード付き)。一定の参考値があります。必要な友人は参照してください。お役に立てば幸いです。

PHP には、packunpack という 2 つの重要で人気のない関数があります。ネットワークプログラミングやイメージファイルの読み書きなどのシナリオでは、これら 2 つの機能はほぼ必須です。ファイルの読み書き/ネットワークプログラミング、またはバイトストリーム処理の重要性を考慮すると、これら 2 つの機能を習得することが高度な PHP プログラミングの基礎となります。

この記事では、まず bytescharacters の違いを紹介し、2 つの関数の存在の必要性と重要性について説明します。次に、読者に一般的な理解を与え、実際に使用するための基礎を築くために、基本的な使用法と使用シナリオが紹介されます。

バイトと文字

PHP の利点は、シンプルで使いやすいことです。文字列および配列関連の関数を上手に使用すると、一般的なニーズを満たすことができます。文字列は日常業務で頻繁に使用されるため、PHP 開発者は文字に慣れており、少し経験がある人は文字エンコーディングを大まかに理解できます。しかし、多くの PHP 開発者は、それに付随する文字の概念であるバイトについて認識していません。

それは彼らのせいではありません。 PHP の世界では「バイト (ストリーム)」という概念がほとんど登場しません: byte キーワードはありません (もちろん char もありません)、公式ドキュメントにはバイトについて言及されていません; ネイティブ配列サポートはありません (一般的に使用される配列は次のとおりです)実際にはハッシュテーブル); もちろん、他の言語では文字列でバイト配列 (Byte Array、byte[]) を表現できます。

バイトと文字の関係と違いは何ですか?簡単に言うと、バイトはコンピュータのストレージと操作の最小単位であり、文字は人間が読み取ることができる最小単位です。バイトはストレージ (物理) 概念で、文字は論理概念です。バイトはデータ (内包と本質) を表し、文字は文字です。 ;文字はバイトで構成されます。

2 つの違いを説明するために、いくつかの例を示します。「中国」には 2 文字が含まれ、GBK エンコードには 4 バイトが必要で、UTF-8 エンコードには 6 バイトが必要です。数値「1234567890」には 10 文字が含まれ、表されます。 int32 型では、4 バイトしか必要としません。次の図は 42582 バイトを占め、「妻」という文字で表され、3 文字だけを占めます:

PHPのpack関数とunpack関数の詳しい紹介(コード付き)

一般的に使用される別の文字を与えます。文字とバイトの違いを説明する例。開発では、データのハッシュ値を取得するために md5 アルゴリズムを使用することが多く、このアルゴリズムは 128 ビット データ (16 バイト) を返します。値を見やすくするために、人々は従来 16 進数表現を使用しており、結果はよく知られた 32 ビット文字列 (大文字と小文字は区別されません) になります。 32 バイト長の文字列は md5 アルゴリズムの必然的な結果ではなく、16 バイトのデータがその本質です。必要に応じて、2^128 未満の数値を使用してハッシュ結果を表すことも、結果として 16 バイトを Base64 エンコードすることもできます。したがって、一般的に使用される 32 ビットのハッシュ値と md5 によって返される 16 バイトの関係は次のようになります。1 つは文字表現で、もう 1 つはその本質 (文字配列) です (PHP の md5 関数の 2 番目のパラメーター値は true です)。 16 文字のセクション データを取得するか、ハッシュ関数の 3 番目のパラメーターが true である場合)。

関連する概念には、バイト順序、文字エンコーディングなどが含まれますが、この記事では詳しく説明しません。

はじめに

PHP には、特に文字列を処理する関数が数十あります。通常、時刻、その他の関数を追加すると、文字列処理用の関数は 100 を下回ることはありません。 。対照的に、バイト処理は一般的ではなく、関連する機能もわずかです。一般的に使用される ord/chr に加えて、ハッシュ暗号化関数によって返される元のバイト、openssl_random_pseudo_bytes、openssl ライブラリ のその他の関数が実際に処理または返す バイト、最も重要な 2 つのバイト処理関数は、packunpack です。

このセクションでは、質問の pack 関数の使用方法について説明します。

質問

簡単な質問を考えてみましょう。ユニバース 42 に対する究極の答えはメモリ内でどのように表現されるのでしょうか (またはそのバイト配列を取得する方法は何ですか)。

42 は整数であるため、ハードウェアによっては、42 が占めるバイト サイズは 1、2、4、8 などになる場合があります。ここでは、整数が 4 バイトを占めるように制限しているため、問題の同等の定式化は次のようになります。 整数をバイト配列 (ネイティブ順序、4 バイト) に変換するにはどうすればよいですか?

分析

マルチバイトなのでバイトオーダーの問題を考慮する必要があります。 42 は 255 を超えず、1 バイトのみを占めるため、他の 3 バイトはすべて 0 です。これに基づいて、次の結論が導き出されます: ビッグエンディアン (下位バイトが上位アドレスに格納される) の場合、4 バイトは次のとおりです: 0 0 0 42;リトルエンディアンの場合、結果は次のようになります: 42 0 0 0

マシンのバイトオーダーはどのようにしてわかりますか? PHP は関連する関数を提供しておらず、C 言語のようなアドレスにアクセスしてバイト データに直接アクセスすることもできません。強力な PHP はどのようにしてバイト順序を修正したり、データからバイトへの変換を完了したりできるのでしょうか?

解決策

PHP アプリケーション レベルでは、データのバイト (配列) への変換は pack の特別なセッションであり、バイト (配列) からバイト (配列) への変換はデータは unpack の特別番組です。この2つの関数を除いて、バイト配列(またはバイナリデータ)をデータに変換することはほぼ不可能です(できればアドバイスをお願いします)。

现在我们用pack函数获取42在内存中的字节数组。相关代码如下:

function intToBytes(int $num) : string {
    return pack("l", $num);
}

function outputBytes(string $bytes) {
    echo "bytes: ";
    for ($i = 0; $i < strlen($bytes); ++ $i) {
        echo ord($bytes[$i]), " ";
    }
    echo PHP_EOL;
}

outputBytes(intToBytes(42));

// 程序输出:
bytes: 42 0 0 0

本人计算机用的英特尔的CPU,x86架构是小端序,所以程序输出符合预期。

延伸一下,怎么判断机器的字节序?有了pack函数,答案非常简单:

function bigEndian() : bool {
    $data = 0x1200;
    $bytes = pack("s", $data);

    return ord($bytes[0]) === 0x12;
}

调用函数便返回本机是否大端序。

上述是pack函数简单的使用场景,接下来分别介绍pack和unpack函数。

pack和unpack

pack函数

pack是“打包/封包”的意思。如其名,pack函数的工作是将数据按照格式打包成字节数组。函数原型为:

pack ( string $format [, mixed $... ] ) : string

形式上与printf系列函数相同:第一个参数是格式字符串,其余参数是要格式化的参数。不同之处在于pack函数的格式中不能出现元字符和量词外的其他字符,所以不需要%符号。

上文的例子中使用了"l"和"s"两个格式化元字符,pack函数的元字符主要分为三类:

  1. 字符串:aA等;将数据转成字符串,功能上与sprintf类似,例如整数32转换成字符串"32";

  2. 字节:hH;对字节进行16进制编码,区别在于低位还是高位在前,功能上与dechex等函数类似;

  3. char/short/int/long/float/double六种基本类型:c/s/i/l等;将数据转换成对应类型的字节数组,除char类型外(暂)没有其他函数可替代;

注意:char和a/A等的区别是a/A等输入为字符(串),而's/S'的输入要求是小于256的整数,输入字符会得到0。

量词比较简单:数字和""两种。例如"i2"表示将两个参数按照整数转换,"c"表示后续都按照char类型转换。

unpack

unpack是pack的反向操作:将字节数组解析成有意义的数据。其函数原型为:

unpack ( string $format , string $data [, int $offset = 0 ] ) : array

unpack函数需要注意的是第一个参数和返回值。返回值好理解,pack函数相当于将除格式化参数外的参数数组(想象成call_user_func_array的参数)变成一个字节数组;unpack做相反的事情:释放数据,得到输入时的参数数组。

返回一个数组,其键分别是什么呢?这便是格式化参数($format)在pack和unpack的不同之处:unpack应该对释放出来的数据命名,用"/"分隔各组数据。由于格式化参数允许有非元字符和量词外的字符,为了区分数据,不同数据间的"/"分隔符必不可少。

一个例子:

$bytes = pack("iaa*", 42, ":", "The answer to life, the universe and everything");

outputBytes($bytes);


$result = unpack("inumber/acolon/a*word", $bytes);
print_r($result);

// 程序输出:
bytes: 42 0 0 0 58 84 104 101 32 97 110 115 119 101 114 32 116 111 32 108 105 102 101 44 32 116 104 101 32 117 110 105 118 101 114 115 101 32 97 110 100 32 101 118 101 114 121 116 104 105 110 103
Array
(
    [num] => 42
    [colon] => :
    [word] => The answer to life, the universe and everything
)

如果不对释放出来的数据命名会怎么样?例如上例中unpack的格式化参数为:"i/a/a*",结果是什么呢?其结果为:

Array
(
    [1] => The answer to life, the universe and everything
)

为何?官方文档上如是说:

Caution If you do not name an element, numeric indices starting from 1 are used. Be aware that if you have more than one unnamed element, some data is overwritten because the numbering restarts from 1 for each element.

翻译过来就是:如果你不对数据命名,默认的1, 2, 3...就用来当作键值。如果有多组数据,每组都用同样的下标,会导致数据覆盖。

所以能理解 "i/a/a*" 为何只剩最后一组数据了吧?

应用场景

读取图像、word/excel文件,解析binlog、二进制ip数据库文件等场合,packunpack几乎必不可少。本文举例说一下packunpack在网络编程时协议解析的用途。

假设我们的tcp包格式为:前四个字节表示包大小,其余字节为数据内容。于是客户(发送)端的send函数可以长这样:

public function send($data) {
  // 这里假设$data已经做了序列化、加密等操作,是字节数组
  // 计算报文长度,封装报文
  $len = strlen($data);
  $header = pack("L", $len);
  // 转换成网络(大端)序
  $header = xxx
  // 封包
  $binary = $header . $data;
  // 调用fwrite/socket_send等将数据写入内核缓冲区
  ...
}

服务(接收)端根据协议解析接收到的数据流:

public function decodable($session, $buffer) {
  $dataLen = strlen($buffer);
  // 非法数据包
  if ($dataLen < 4) {
    // 关闭连接、记录ip等
    ....
    return NOT_OK;
  }
  // 获取前四个字节
  $header = substr($buffer, 0, 4);
  // 转换成主机序
  $header = xxx
  // 解析数据长度
  $len = unpack("L", $header);
  // 单个报文不能超过8M,例如限制上传的图像大小
  if ($len > 8 * 1024 * 1024) {
    // 关闭连接等
    return NOT_OK;
  }

  // 检查数据包是否满足协议要求
  if ($dataLen - 4 >= $len) {
    return OK;
  }
  // 数据未全部到达,继续等待
  return NEED_DATA;
}

通过pack和unpack,我们顺利的处理报文协议和二进制字节流的发送和解析。

如果你用\n作为报文分隔符,pack和unpack也许用不到。但在网络通讯中直接传递字符毕竟少数(相当于明文传送),大多数情况下的二进制数据流的解析还是要靠pack和unpack。

总结

メモリの割り当てに加えて、最も重要なシステム コールはファイルの読み取りと書き込み、およびネットワーク接続であり、両方の重要な操作オブジェクトはバイト ストリームです。 packunpack は、バイナリ データ処理に非常に役立つ低レベルのバイト操作を実行する機能を PHP に提供します。 Web プログラミングから飛び出したいと考えている PHP 開発者は、これら 2 つの関数をマスターする必要があります。

以上がPHPのpack関数とunpack関数の詳しい紹介(コード付き)の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

声明:
この記事はsegmentfault.comで複製されています。侵害がある場合は、admin@php.cn までご連絡ください。