PHP 实现base64编码文件上传出现问题详解


Posted in PHP onSeptember 01, 2020

一、场景

领导:小A同学,我们要做一个样本上传进行分析的功能,你看下是否使用base64编码加进去,这样客户端的同学就不需要用form-data方式来上传了,直接使用json格式就可以上报,可以让格式上报统一。

小A:好的,领导,马上搞定!

咋看上面的对话没啥问题,很多公司团队内部为了一些标准化的问题,都会进行一些技术选型问题,但是噩梦也就从这个对话开始,功能实现当然都是很简单的,先来看简单流程图:

PHP 实现base64编码文件上传出现问题详解

本身的流程是一个很简单的文件转换成base64上传,再服务端decode保存,在开发联调过程中没有问题,非常完美的走下去了。

二、问题来了

突然有一天终端同学误操作将一个37M文件上传,nginx与php-fpm文件上传限制均为(60M),但是在界面出现500错误,进入docker 日志查看有一条数据:

Allowed memory size of 8388608 bytes exhausted (tried to allocate 1298358 bytes)

玩php的基本都知道这是啥意思,就是代码运行过程中使用内存超过 我们php.ini设置的memory_limit 的值,然后就屁颠屁颠进入php.ini找参数配置,很快找到:

memory_limit=128M

然后就转念一想,不应该出现这个问题,我们知道,php的内部变量使用cow(写时复制)机制来实现,那么内存申请只有在变量赋值变更才会进行

三、测验

接下来我们单独写一个程序来进行测试,将一个4.89M文件进行base64_encode 编码 与base64_decode解码,查看各自占用内存以及过程中占用峰值内存

<?php
$mid = memory_get_usage();
$apk_content = file_get_contents(__DIR__ . '/4bc1c8a05b8505662be778b6dad23b55.apk');
var_dump('文件加载到内存:' . round((memory_get_usage() - $mid) / 1024 / 1024, 2) . 'M');
var_dump('过程中峰值使用的内存:' . round(memory_get_peak_usage() / 1024 / 1024, 2) . 'M');

unset($mid);
$mid = memory_get_usage();
$base64_encode = base64_encode($apk_content);unset($apk_content);
var_dump('base64_encode占用内存:' . round((memory_get_usage() - $mid) / 1024 / 1024, 2) . 'M');
var_dump('过程中峰值使用的内存:' . round(memory_get_peak_usage() / 1024 / 1024, 2) . 'M');

unset($mid);
$mid = memory_get_usage();
base64_decode($base64_encode);
var_dump('base64_decode占用内存:' . round((memory_get_usage() - $mid) / 1024 / 1024, 2) . 'M');
var_dump('过程中峰值使用的内存:' . round(memory_get_peak_usage() / 1024 / 1024, 2) . 'M');
unset($mid);

执行结果:

string(29) "文件加载到内存:4.89M"
string(38) "过程中峰值使用的内存:5.25M"
string(33) "base64_encode占用内存:1.63M"
string(39) "过程中峰值使用的内存:11.76M"
string(30) "base64_decode占用内存:0M"
string(38) "过程中峰值使用的内存:13.4M"

通过上面结果可以看出

  • 加载文件使用内存没有太大问题,加载过程使用的峰值在5.25M,高出整体文件大小不多,这在文件加载过程有一些临时申请内存的问题
  • base64_encode占用内存,这个在使用的时候,就已经将内存差不多进行一个double,而这基本上也是在内核解析过程中,进行了内存申请,可以理解,文件本身占用内存+base64_encode 解析后的内存,两份内存同时存在的
  • base64_decode操作,这个操作就是解密了,解密过程中,这里直接就占用了3倍多的内存操作,问题就出在这里,在场景中出现的问题是一个37M的文件,为什么就把单个fpm的128M内存占满了呢

四、源码解析

base64_encode源码解析

首先找到对应的c文件 base64.c,找到里面php_base64_encode函数

PHPAPI zend_string *php_base64_encode(const unsigned char *str, size_t length) /* {{{ */
{
	const unsigned char *current = str;
	unsigned char *p;
	zend_string *result;

	result = zend_string_safe_alloc(((length + 2) / 3), 4 * sizeof(char), 0, 0);
	p = (unsigned char *)ZSTR_VAL(result);
    ...
}

我们先来分析这段代码,因为这里涉及到内存的问题,那么我们就看

result = zend_string_safe_alloc(((length + 2) / 3), 4 * sizeof(char), 0, 0);

这啥意思呢?

申请内存,最终调用的函数是:

safe_emalloc(size_t nmemb, size_t size, size_t offset)

在wiki上解释是:

void *safe_emalloc(size_t nmemb, size_t size, size_t offset)分配缓冲区来存放每块大小为 size 字节的 nmemb 块,并附加 offset 字节。类似于 emalloc(nmemb * size + offset),但增加了针对溢出的特殊保护。

那么我可以简单的认为,就是在encode过程中,重新申请了内存,申请的内存大小是文件本身的 4/3 大小,加上原来的文件本身大小,那么峰值大小可以理解为

峰值内存= 7/3 *4.89 = 11.41

那么与我们实验过程中峰值大小基本是相符。

base64_decode操作

同样我们进行源码分析

PHPAPI zend_string *php_base64_decode_ex(const unsigned char *str, size_t length, zend_bool strict) /* {{{ */
{
	const unsigned char *current = str;
	int ch, i = 0, j = 0, padding = 0;
	zend_string *result;

	result = zend_string_alloc(length, 0);
	...
}

这里使用的zend_string_alloc来进行申请内存,那么底层使用的函数就是emalloc函数,来看下wiki的解释

void *emalloc(size_t size)分配 size 字节的内存。

这个就比较好理解了,传入参数内存再进行一个double拷贝就可以,

那么我们进行一个decode的内存峰值的计算:

峰值内存=(4/3+4/3) *4.89 =13.04

基本与我们测试的结果相差不多,因为精度关系,我们进行四舍五入的计算,测试代码是精准计算,所以会有小数点偏差。

五、总结

那这就可以理解为什么一个为什么在我们一个37M的文件,不能再128M内存进行base64_encode与base64_decode操作,当然这里有一些临时变量没有及时释放内存的情况,但是通过源码分析可以知道,要做一次这样场景来进行文件上传,单纯文件的内存损耗是2.6倍左右,所以为了节省内存,我们不要再用这个方式来进行操作了,很费内存的

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

PHP 相关文章推荐
PHP中的CMS的涵义
Mar 11 PHP
无刷新动态加载数据 滚动条加载适合评论等页面
Oct 16 PHP
json的键名为数字时的调用方式(示例代码)
Nov 15 PHP
php保存任意网络图片到服务器的方法
Apr 14 PHP
php使用ob_flush不能每隔一秒输出原理分析
Jun 02 PHP
php使用MySQL保存session会话的方法
Jun 26 PHP
php实现文章置顶功能的方法
Oct 20 PHP
php获取指定数量随机字符串的方法
Feb 06 PHP
PHP实现类似于C语言的文件读取及解析功能
Sep 01 PHP
php实现socket推送技术的示例
Dec 20 PHP
php爬取天猫和淘宝商品数据
Feb 23 PHP
yii2.0框架数据库操作简单示例【添加,修改,删除,查询,打印等】
Apr 13 PHP
PHP copy函数使用案例代码解析
Sep 01 #PHP
PHP超全局变量实现原理及代码解析
Sep 01 #PHP
PHP终止脚本运行三种实现方法详解
Sep 01 #PHP
PHP如何使用array_unshift()在数组开头插入元素
Sep 01 #PHP
PHP数组Key强制类型转换实现原理解析
Sep 01 #PHP
Laravel中GraphQL接口请求频率实战记录
Sep 01 #PHP
PHP实现Snowflake生成分布式唯一ID的方法示例
Aug 30 #PHP
You might like
Yii净化器CHtmlPurifier用法示例(过滤不良代码)
2016/07/15 PHP
Laravel框架路由和控制器的绑定操作方法
2018/06/12 PHP
PHP5.5基于mysqli连接MySQL数据库和读取数据操作实例详解
2019/02/16 PHP
JS比较2个日期间隔的示例代码
2014/04/15 Javascript
HTML页面登录时的JS验证方法
2014/05/28 Javascript
javascript设计模式之解释器模式详解
2014/06/05 Javascript
JavaScript中Function详解
2015/02/27 Javascript
js实现当复选框选择匿名登录时隐藏登录框效果
2015/08/14 Javascript
深入浅析JavaScript中数据共享和数据传递
2016/04/25 Javascript
JavaScript对象数组如何按指定属性和排序方向进行排序
2016/06/15 Javascript
AJAX和jQuery动态加载数据的实现方法
2016/12/05 Javascript
nodejs的压缩文件模块archiver用法示例
2017/01/18 NodeJs
Javascript中字符串和数字的操作方法整理
2017/01/22 Javascript
jquery仿京东侧边栏导航效果
2017/03/02 Javascript
微信小程序开发实现消息推送
2020/11/18 Javascript
maptalks+three.js+vue webpack实现二维地图上贴三维模型操作
2020/08/10 Javascript
Js跳出两级循环方法代码实例
2020/09/22 Javascript
python实现的文件同步服务器实例
2015/06/02 Python
Django中使用Whoosh进行全文检索的方法
2019/03/31 Python
python 实现将小图片放到另一个较大的白色或黑色背景图片中
2019/12/12 Python
使用Python的Turtle库绘制森林的实例
2019/12/18 Python
Python&amp;&amp;GDAL实现NDVI的计算方式
2020/01/09 Python
手把手教你从PyCharm安装到激活(最新激活码),亲测有效可激活至2089年
2020/11/25 Python
优秀的计算机专业求职信范文
2013/12/27 职场文书
六十大寿答谢词
2014/01/12 职场文书
银行员工辞职信范文
2014/01/20 职场文书
美容院经理岗位职责
2014/04/03 职场文书
2014党委书记四风对照检查材料思想汇报
2014/09/21 职场文书
党的群众路线教育实践活动个人对照检查材料(公安)
2014/11/05 职场文书
2015年乡镇信访工作总结
2015/04/07 职场文书
公司财务经理岗位职责
2015/04/08 职场文书
大学三好学生主要事迹范文
2015/11/03 职场文书
2016年区委书记抓基层党建工作公开承诺书
2016/03/25 职场文书
新的CSS 伪类函数 :is() 和 :where()示例详解
2022/08/05 HTML / CSS
ubuntu端向日葵键盘输入卡顿问题及解决
2022/12/24 Servers
华为HarmonyOS3.0强在哪? 看看鸿蒙3.0这7个小功能
2023/01/09 数码科技