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 Memcached应用实现代码
Feb 08 PHP
五款常用mysql slow log分析工具的比较分析
May 22 PHP
PHP 图片上传代码
Sep 13 PHP
APACHE的AcceptPathInfo指令使用介绍
Jan 18 PHP
利用php+mcDropdown实现文件路径可在下拉框选择
Aug 07 PHP
php实现图片文件与下载文件防盗链的方法
Nov 03 PHP
Yii2验证器(Validator)用法分析
Jul 23 PHP
php异步:在php中使用fsockopen curl实现类似异步处理的功能方法
Dec 10 PHP
PHP随机获取未被微信屏蔽的域名(微信域名检测)
Mar 19 PHP
解决form中action属性后面?传递参数 获取不到的问题
Jul 21 PHP
支持汉转拼和拼音分词的PHP中文工具类ChineseUtil
Feb 23 PHP
PHP微商城开源代码实例
Mar 27 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
PHP中限制IP段访问、禁止IP提交表单的代码
2011/04/23 PHP
PHP 搜索查询功能实现
2016/11/29 PHP
BOOM vs RR BO5 第一场 2.14
2021/03/10 DOTA
JavaScript Base64编码和解码,实现URL参数传递。
2006/09/18 Javascript
Ext.MessageBox工具类简介
2009/12/10 Javascript
js中reverse函数的用法详解
2013/12/26 Javascript
原生JS实现LOADING效果
2015/03/16 Javascript
JavaScript中使用Math.PI圆周率属性的方法
2015/06/14 Javascript
jQuery通过deferred对象管理ajax异步
2016/05/20 Javascript
JS判断鼠标进入容器的方向与window.open新窗口被拦截的问题
2016/12/23 Javascript
node学习记录之搭建web服务器教程
2017/02/16 Javascript
JavaScript限制在客户区可见范围的拖拽(解决scrollLeft和scrollTop的问题)(2)
2017/05/17 Javascript
浅析JS中什么是自定义react数据验证组件
2018/10/19 Javascript
JS判断两个数组或对象是否相同的方法示例
2019/02/28 Javascript
JQuery事件委托原理与用法实例分析
2019/05/13 jQuery
vue3.0生命周期的示例代码
2020/09/24 Javascript
vue监听键盘事件的相关总结
2021/01/29 Vue.js
Python中for循环详解
2014/01/17 Python
利用Django框架中select_related和prefetch_related函数对数据库查询优化
2015/04/01 Python
Python打造出适合自己的定制化Eclipse IDE
2016/03/02 Python
pandas系列之DataFrame 行列数据筛选实例
2018/04/12 Python
Python的argparse库使用详解
2018/10/09 Python
Python tensorflow实现mnist手写数字识别示例【非卷积与卷积实现】
2019/12/19 Python
python的slice notation的特殊用法详解
2019/12/27 Python
Python基础之函数原理与应用实例详解
2020/01/03 Python
Tensorflow设置显存自适应,显存比例的操作
2020/02/03 Python
执行Python程序时模块报错问题
2020/03/26 Python
HTML5注册表单的自动聚焦与占位文本示例代码
2013/07/19 HTML / CSS
Oracle快照(snapshot)
2015/03/13 面试题
大四毕业生学习总结的自我评价
2013/10/31 职场文书
2014两会学习心得:时代的发展
2014/03/17 职场文书
作风年建设汇报材料
2014/08/14 职场文书
学校办公室主任岗位职责
2015/04/01 职场文书
Python 把两层列表展开平铺成一层(5种实现方式)
2021/04/07 Python
「睡美人」爱洛公主粘土人开订
2022/03/22 日漫
navicat 连接Ubuntu虚拟机的mysql的操作方法
2022/04/02 MySQL