转载请注明文章出处:https://tlanyan.me/php-pack-a...PHP有两个重要的冷门函数:pack和unpack。在网络编程、读写图像文件等场景中,这两个函数几乎是必不可少的。鉴于文件读写/网络编程,或者说字节流处理的重要性,掌握这两个功能是PHP高级编程的基础。本文首先介绍了字节和字符的区别,并说明了这两种功能存在的必要性和重要性。然后介绍基本的使用方法和使用场景,让读者对其有一个大概的了解,为实际使用打下基础。字节和字符PHP的优点是简单易用,熟练使用字符串和数组相关函数可以抵挡一般需求。日常工作中经常会用到字符串,所以PHP开发者对字符比较熟悉,稍微高级一点基本就能搞定字符编码。但是伴随而来的字符概念:bytes,很多PHP开发者并不了解/不熟悉。这不是他们的错。“byte(流)”这个概念在PHP世界中很少出现:没有byte关键字(当然也没有char),官方文档也没有提到byte;没有原生的数组支持(常用的数组其实就是哈希表);当然Strings(字符串)在其他语言中也可以表示字节数组(ByteArray,byte[])。字节和字符有什么联系和区别?简单地说,字节是计算机存储和运行的最小单位,字符是人们阅读的最小单位;字节是存储(物理)概念,字符是逻辑概念;一个字节代表数据(内涵和本质),字符代表它的意义;字符由字节组成。举几个例子说明两者的区别:“中国”包含2个字符,GBK编码需要4个字节,UTF-8编码需要6个字节;数字“1234567890”包含10个字符,用int32类型表示只需要4个字节;下图占用42582字节,用字符表示为“我老婆”,只占3个字符:再举一个常见的例子来说明字符和字节的区别。在开发中,我们经常使用md5算法来获取数据的哈希值,该算法返回一个128位(bit)的数据(16字节)。为了方便查看它的值,习惯上用16进制表示,结果就是大家熟知的32位字符串(不区分大小写)。32位长度的字符串并不是md5算法的必然结果,16字节的数据才是其本质。如果需要,可以使用小于2^128的数字来表示哈希结果,或者将16字节的base64编码为结果。因此,常用的32位哈希值与md5返回的16个字节的关系是:一是字符表示,二是其本质(字符数组)(PHPmd5函数第二个参数值为true得到16个字符的段数据,或者哈希函数的第三个参数为真)。相关概念包括字节序、字符编码等,本文不再展开。有兴趣的读者可以参考我之前的博客《文件与字符编码》或相关资料。介绍PHP中专门处理字符串的函数有几十个,加上正则化、时间等函数,处理字符串的函数不下百个。相比之下,字节处理就被忽略了,相关的函数也很少。除了常用的ord/chr、hash加密函数返回的原始字节,以及openssl库的openssl_random_pseudo_bytes函数实际处理或返回字节外,最重要的两个字节处理函数是pack和unpack。本节从问题引出pack函数的使用。问题考虑一个简单的问题:宇宙42的最终答案是如何在内存中表示的(或者说如何得到它的字节数组)?因为42是一个整数,根据硬件的不同,它占用的字节大小可能是1、2、4、8等。这里我们限制一个整数占用4个字节,所以问题的等价表达式是:如何将一个整数转换为字节数组(本机序列,4个字节)?分析因为是多字节的,所以必须考虑字节顺序的问题。42不超过255,只占一个字节,所以其他三个字节都为0。据此得出结论,如果是big-endian(低位字节存放在高位地址),这四个字节是:00042;如果是little-endian,结果就是:42000。那怎么知道机器的字节序呢?PHP不提供相关函数,也不能像C语言那样直接取地址访问字节数据。万能的PHP如何处理字节序,或者说完成数据到字节的转换呢?在方案的PHP应用层面,data到bytes(数组)的转换是pack的一个特殊session,bytes(arrays)到data的转换是unpack的一个特殊session。除了这两个函数,字节数组(或二进制数据)到数据的转换几乎是不可能的(有的话请告诉我)。现在我们使用pack函数获取内存中42的字节数组。相关代码如下:functionintToBytes(int$num):string{returnpack("l",$num);}functionoutputBytes(string$bytes){echo"bytes:";对于($i=0;$i<;strlen($bytes);++$i){echoord($bytes[$i]),"";}echoPHP_EOL;}outputBytes(intToBytes(42));//Programoutput:bytes:42000我电脑使用的IntelCPU,x86架构是littleendian,所以程序输出符合预期。延伸一下,如何判断机器的字节序?使用pack函数,答案非常简单:functionbigEndian():bool{$data=0x1200;$bytes=pack("s",$data);returnord($bytes[0])===0x12;}call函数返回机器是否是big-endian。以上是pack函数的简单使用场景,接下来分别介绍pack和unpack函数。pack和unpackpack功能pack的意思是“打包/打包”。顾名思义,pack函数的作用就是将数据按照格式打包成字节数组。函数原型为:pack(string$format[,mixed$...]):string形式与printf系列函数相同:第一个参数为格式字符串,其余参数为要格式化的参数。不同的是,除了元字符和量词之外的字符不能出现在pack函数的格式中,因此不需要%符号。在上面的示例中,使用了两个格式化元字符“l”和“s”。pack函数的元字符主要分为三类:字符串:a、A等;将数据转为字符串,功能上与sprintf类似,例如将整数32转为字符串“32”;字节:h和H;字节的十六进制编码,区别是低位在前还是高位在前,作用类似于dechex等函数;char/short/Int/long/float/double六种基本类型:c/s/i/l等;将数据转换成对应类型的byte数组,除了char类型(暂时)没有其他函数可以替代;注意:char和a/A等的区别在于a/A等的输入是字符(字符串),而's/S'的输入要求是小于256的整数,而输入字符将得到0。量词相对简单:数字和“”。例如“i2”表示将两个参数转换为整数,“c”表示将后面的参数按照char类型进行转换。unpackunpack是pack的逆操作:它将字节数组解析为有意义的数据。其函数原型为:unpack(string$format,string$data[,int$offset=0]):arrayunpack函数的第一个参数和返回值需要注意。返回值很容易理解。pack函数相当于把除了格式化参数之外的参数数组(想象成call_user_func_array的参数)变成字节数组;unpack则相反:释放数据并在输入时获取参数数组。返回一个数组,它的键是什么?这就是pack和unpack中格式参数($format)的区别:unpack要给发布的数据命名,每组数据用“/”隔开。由于格式化参数允许非元字符和量词以外的字符,为了区分数据,不同数据之间的“/”分隔符是必不可少的。一个例子:$bytes=pack("iaa*",42,":","生命、宇宙和一切的答案");outputBytes($bytes);$result=unpack("innumber/acolon/a*word",$bytes);print_r($result);//程序输出:字节:4200058841041013297110115119101114321161113210810510210144321161041071110105118101114115101329711010032101118101114121116104105110103Array([num]=>42[colon]=>:[word]=>andeverything)如果你不命名会发生什么发布的数据?比如上面例子中unpack的格式参数是:“i/a/a*”,结果是什么?结果是:Array([1]=>生命、宇宙和一切的答案)为什么?官方文件说:警告如果你不命名一个元素,使用从1开始的数字索引。请注意,如果您有多个未命名元素,一些数据将被覆盖,因为每个元素的编号从1重新开始。翻译事情是这样的:如果您不命名数据,默认的1、2、3...将用作键值。如果有多组数据,每组使用相同的下标会导致数据覆盖。所以你能理解为什么“i/a/a*”只剩下最后一组数据了吗?应用场景在读取图片、word/excel文件、解析binlog、二进制ip数据库文件等时,打包解包几乎是必不可少的。本文给出了在网络编程中使用pack和unpack进行协议分析的例子。假设我们的tcp数据包格式为:前四个字节代表数据包大小,其余字节为数据内容。所以client(发送)端的send函数可以是这样的:publicfunctionsend($data){//这里假设$data已经序列化、加密等,是一个字节数组//计算消息长度并封装消息$len=strlen($data);$header=pack("L",$len);//转换为网络(大端)序列$header=xxx//Packet$binary=$header.$数据;//调用fwrite/socket_send将数据写入内核缓冲区...}服务(接收)端根据协议解析接收到的数据流:publicfunctiondecodable($session,$buffer){$dataLen=strlen($缓冲);//非法数据包if($dataLen<4){//关闭连接,记录ip等.returnNOT_OK;}//获取前四个字节$header=substr($buffer,0,4);//转换为主机序列$header=xxx//解析数据长度$len=unpack("L",$header);//单个包不能超过8M,比如限制上传图片的大小if($len>8*1024*1024){//关闭连接等returnNOT_OK;}//检查数据包是否满足协议要求if($dataLen-4>=$len){returnOK;}//数据未全部到达,继续等待returnNEED_DATA;}通过pack和unpack,我们成功的处理了消息协议和二进制字节流的发送和解析。如果您使用\n作为消息分隔符,则可能无法使用pack和unpack。但是,网络通信中直接传输的字符很少(相当于明文传输),大多数情况下对二进制数据流的解析仍然依赖于pack和unpack。小结除了分配内存,最重要的系统调用是文件读写和网络连接,两者本质的操作对象都是字节流。打包和解包为PHP提供了操作低级字节的能力,这在二进制数据处理中非常有用。有志于跳出web编程的PHP开发者应该掌握这两个功能。参考文档和字符编码PHP手册:packPHP手册:unpackHandlingbinarydatainPHPwithpack()andunpack()PHP:In-depthpack/unpack
