最近开了一节课,《支持10万人同时在线 Go语言打造高并发web即时聊天(IM)应用》
课程播出后,很多读者问到图片处理相关的东西,如怎么进行前端压缩,和异步上传等,有鉴于此,笔者系统性地整理了图片处理相关技术细节。老规矩,文章末尾给有源代码地址。
从 2017 年开始,我们持续为某企业支撑多场 HTML5 晒单赢红包活动,活动规则(套路)如下:
用户在商超里面购买产品 P,获得小票 T;
用户关注公众号,从菜单进入小票上传页面内,用手机拍摄小票,并上传;
上传成功后,后端通过图像识别,分辨小票内容是否包含产品 P,借此判断用户是否有抽奖的机会;
小票上传成功后,将在列表页面 L 中按照上传先后顺序显示。
整个活动持续运维 2 年多,整个过程遇到了各种奇葩问题,举例如下:
因为有些手机像素太高,拍照图片达到 2M 左右,上传太慢,上传出错;
部分手机上传后相片旋转了 90 度;
列表页面加载速度越来越慢,甚至卡顿;
图片铺满了硬盘空间,导致应用日志写入失败,系统报错;
用户反应页面打开慢,白屏;
多用户同时上传图片,有用户上传失败;
有用户反应手机无牌照弹窗;
经过分析反应页面打开慢的大部分是北方用户。
...... 本场 chat 的目的也就在此,希望从如何解决这些问题出发,举一反三,触类旁通,最后达到系统化地梳理图片应用常用方法和技巧的目的。
图片异步上传
2.1 异步上传的好处
异步上传图片的好处显而易见。首先用户能获得更好的用户体验,异步上传页面不需要要刷新,在上传过程中,应用可以通过进度条,loading 等效果,达到友好提醒的目的。其次,能将图片业务抽象,业务逻辑和图像上传解耦合。
2.2 用插件实现前端异步上传
常用插件 uploadify、webuploader、ajaxfileupload.js、jquery.form.js 等,这里不做重点讨论。
2.3 用 JS+H5 实现图片上传
2.3.1 JavaScript 代码段
//异步上传核心函数//filedom 通过dom query方法返回的dom如document.getElementByID("test")//onsuccess上传成功回//onerror上传失败回调function uploadfile(filedom, onsuccess, onerror, onprogress ){//使用H5的formdata进行上传//formdata 是H5新增加内容var formdata = new FormData(); formdata.append(filedom.name,filedom.files[0]);//还可以同时上传参数formdata.append("clientID","客户端的唯一标识"); //在AndroID/IOS 系统中webview都支持XMLHttpRequest//无需考虑IEvar xhr= new XMLHttpRequest(); xhr.upload.onprogress=console.log;//如果定义了进度函数if(typeof onprogress=="function"){ xhr.upload.onprogress = onprogress; }//如果定义了上传成功回调if(typeof onsuccess!="function"){ onsuccess = console.log; }//如果定义了上传失败回调if(typeof onerror!="function"){ onerror = console.log; }//第二个参数是后端服务地址,将做重点说明xhr.open("POST", "/attach/upload");//ajax发送数据xhr.send(formdata);//时间成功回调xhr.onreadystatechange = function(){ //OnReadyStateChange事件 if(xhr.readyState == 4){ //4为完成 if(xhr.status == 200){ //200为成功 onsuccess(JSON.parse(xhr.responseText)) }else{ onerror(xhr) } } }; }//上传成功后回调处理function ajaxuploadsuccess(res){}//上传失败回调处理function ajaxuploaderror(res){}//上传失败回调处理function ajaxuploadprogress(ev) { if(ev.lengthComputable) { var percent = 100 * ev.loaded/ev.total; console.log({"文件总大小":ev.total,"已上传":ev.loaded,"上传进度":percent+"%"}); } }
2.3.2 Html 代码段
<input type="file" name="file" accept="image/*" capture="filesystem" onchange="uploadfile(this,ajaxuploadsuccess,ajaxuploaderror)">
特别需要注意
name 属性即为后端接收文件参数的名字,不能缺失。
accept 属性标识了该控件只能检索图片类型文件。
capture=filesystem 属性标识了系统只能通过摄像头拍照。
2.3.3 让界面更漂亮
为了让界面变得更加美观,比如要实现如下按钮,点击头像即可上传效果
我们通常需要对 DOM 进行特殊的处理
<li class="mui-table-view-cell mui-media"> <a class="mui-navigate-right"> <input onchange="uploadfile(this,ajaxuploadsuccess)" accept="image/png,image/jpeg" type="file"placeholder="请输入群名称" class="mui-input-clear mui-input" style="wIDth: 100%; height: 48px; position: absolute; opacity: 0;"> <img ID="head-img" class="lazyload" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsQAAA7EAZUrDhsAAAANSURBVBhXYzh8+PB/AAffA0nNPuCLAAAAAElFTkSuQmCC" data-original="https://images.gitbook.cn/5489db20-3466-11e9-93c1-37d3989d4cb2" class="mui-media-object mui-pull-right head-img" style="border-radius: 50%;"> <div class="mui-media-body"> 头像 <p class="mui-ellipsis">点击右侧上传头像</p> </div> </a> </li>
其中
input 标签 opacity 属性设置为 0,意味着这个 input 是透明的,所以能看到下面的图片。
input 标签 position 为 absolute,宽度和高度适当则刚好可以让这个 dom 遮住 image ,用户点击 image 附近任意位置都可以实现上传。
为了头像变成圆形,设置 img 标签 border-radius: 50%。
在上面的例子中,ajaxuploadsuccess 函数中实现了图片的 src 更新功能。
上传成功回调说明:
/*内容格式说明 {"code":0,"data":"https://images.gitbook.cn/5489db20-3466-11e9-93c1-37d3989d4cb2","msg":"ok"} */function ajaxuploadsuccess(res){ //上传成功后返回上传成功的图片地址,并更新用户头像地址 document.getElementByID("head-img").src=res.data ; }
图片压缩
3.1 为什么要对图片进行处理
3.1.1 可以快速上传增强用户体验
目前手机质量越来越好,相机拍照质量也越来越高,导致直接后果是手机端图片变大,随便一张图 2-3M,那么如果上传这样的图片,用户所需时间将会增加,以 1 秒钟 300kb 计算,一张图所需时间越为 10 秒钟,用户耗费大量的时间在等待当中,以至于失去耐性,这对用户是极大的伤害。如果对图片进行压缩,比如压缩至 300kb,用户所需时间将缩短为 1 秒,可以显著提高用户体验。
3.1.2 可以降低流量
主要有俩个方面,一方面是用户上传的流量,1 张 3M 的和一张 300kb 的,显而易见前者消耗的流量大得多。另一方面是降低用户浏览图片所耗费的流量。
3.1.3 可以降低存储所用空间
经过压缩处理的图片,存储空间大大降低。
3.1.4 可以让应用更流畅
如果我们未对图片的显示效果做处理,当每个图片较大时,浏览器将超负荷渲染图片,该行为直接导致页面加载速度变慢,刷新响应变慢,甚至出现卡顿现象。对图片进行压缩处理后,应用将反应快,更加流畅
3.2 实现前端压缩
3.2.1 关于 Html5 操作文件的基本知识
3.2.1.1 关于文件对象 File
如下是 console.log 打印出来的 file 对象内容。file 对象描述了文件的基本信息,但是没有文件内容。通过 input 标签可以获得 files 数组,遍历该数组可以获得具体每一个 file 对象。
var files = document.getElementByID("filedomID").files;for(var file in files){ console.log(file); }
在 Chrome 引擎浏览器中效果
{ lastModified:1511236246031,//最近一次更新时间戳lastModifiedDate:"Tue Nov 21 2017 11:50:46 GMT+0800 (中国标准时间) ",//最近一次更新时间name:"0.首页 – 副本.png",//文件名称size:2552293,//文件大小,单位Bytetype:"image/png",//文件类型webkitRelativePath:""//input上加上webkitdirectory属性时,用户可选择文件夹,此时weblitRelativePath表示文件夹中文件的相对路径}
Firefox下对象信息如下,缺少了 lastModifiedDate 信息
{ lastModified:1511236246031,//最近一次更新时间戳name:"0.首页 – 副本.png",//文件名称size:2552293,//文件大小,单位Bytetype:"image/png",//文件类型webkitRelativePath:""}
IE 下打印信息如下
{constructor: File {...}, lastModifiedDate: "Tue Nov 21 2017 11:50:46 GMT+0800 (中国标准时间) ",//最近一次更新name: "0.首页 – 副本.png", size: 75605, type: "image/jpeg"}
可见无论何种浏览器,size、name、type 属性都会存在。我们将利用 file 的 size 属性做大小判断,name 属性做类型校验。
3.2.1.2 FileReader 对象简介
FileReader 对象提供了操作文件内容的接口
方法名称 | 描述 |
---|---|
readAsArrayBuffer(file) | 按字节读取文件内容,结果用ArrayBuffer对象表示 |
readAsBinaryString(file) | 按字节读取文件内容,结果为文件的二进制串 |
readAsDataURL(file) | 读取文件内容,结果用data:url的字符串形式表示 |
readAsText(file,encoding) | 按字符读取文件内容,结果用encoding编码的字符串形式表示 |
abort() | 终止文件读取操作 |
其中,readAsDataURL 方法可以将文件内容编码成 Base64 格式,这点很重要,这意味着我们可以用 Base64 编码统一 Canvas 图片压缩接口。
另外,FileReader提供了如下事件机制:
方法名称 | 描述 |
---|---|
onabort | 当读取操作被中止时调用 |
onerror | 当读取操作发生错误时调用 |
onload | 当读取操作成功完成时调用,一般使用用该方法进行回调 |
onloadend | 当读取操作完成时调用,无论成功或失败 |
onloadstart | 当读取操作开始时调用 |
onprogress | 在读取数据过程中周期性调用,进度回调 |
我们可以利用onload事件来处理文件内容。onprogress处理进度属性。 | |
回调函数原型如下: |
function(e){}
e 参数格式如下,我们可以通过 e.target.result 获得文件内容,如图所示,这是一连串 Base64 格式字符串。
3.2.1.3 获取文件内容
由以上可以获得读取文件内容的一般函数:
//file:这是一个文件对象 //onload :加载成功回调 //onerror 加载失败回调 //onprogress :加载进度回调function filetobase64withfilereader(file,onload,onerror,onprogress){//创建对象 var reader = new FileReader(); //发起请求 reader.readAsDataURL(file);//发起异步请求 //配置回调函数 reader.onload=function(ev){ if(!!onload){ onload({"code":200,"data":ev.target.result,"msg":""}) } } if(typeof onerror=="function"){ reader.onerror = function(ev){ onerror({"code":400,"data":ev,"msg":"加载文件出错"}) }; }else{ reader.onerror = console.log; } if(typeof onprogress=="function"){ reader.onprogress = onprogress; }else{ reader.onprogress = console.log; } }
3.2.2 利用 Canvas 对图进行压缩
3.2.2.1 Canvas 实现图片压缩的原理
Canvas.toDataURL(type, encoderOptions);
利用该方法可以返回 dataUrl 数据,这是一连串经过 Base64 编码后的图片内容,这些内容在大部分浏览器中都能直接显示。函数参数说明如下:
type 可选图片格式,默认为 image/png,jpg 为 image/jpeg。
encoderOptions 可选在指定图片格式为 image/jpeg 或 image/webp 的情况下,可以从 0 到 1 的区间内选择图片的质量。如果超出取值范围,将会使用默认值 0.92。其他参数会被忽略。
返回值:类似
data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNby// blAAAADElEQVQImWNgoBMAAABpAAFEI8ARAAAAAElFTkSuQmCC
,其中image/png
表示 png 图片类型,base64
为固定参数,标识这是 Base64 编码。上面所示字符串是一个白色图标。
3.2.2.2 实现 Canvas 压缩图片函数
综上所述,可以直接上代码了:
//压缩成功回调function oncompress(res){ console.log(res) document.getElementByID("testimg").src=res.data; }//报错回调function onerror(res) { console.log("onerror",res) }//这个函数的核心思路//首席按调用filereader对象获得文件内容base64格式//然后判断如果不需要压缩就返回base64//否则调研Canvas绘制图片,最后将Canvas上的内容导出为dataUrl格式,即base64格式//所有数据都通过回调函数传递function filetobase64withfilereader(file,onload,onerror,onprogress) { var reader = new FileReader(); //发起请求 reader.readAsDataURL(file);//发起异步请求 //配置回调函数 if (typeof onload == "function") { reader.onload = onload; } else { reader.onload = console.log; } if (typeof onerror == "function") { reader.onerror = onerror; } else { reader.onerror = console.log; } if (typeof onprogress == "function") { reader.onprogress = onprogress; } else { reader.onprogress = console.log; } }//图片压缩后的最大宽度var CompressMaxWIDth = 400;//图片压缩后的最大高度var CompressMaxHeight = 400;//图片压缩后获取图片质量var CompressQuality=0.92;//如下函数核心逻辑如下//通过先用filereader对象加载file,获得文件内容//在filereader加载完成后的回调函数onload里面//可以将内容填充到一个Image对象中,//在Image加载完成时onload回调函数中,//调用Canvas,的draw方法,将图片内容加载到Canvas上,//同时,可以通过设置Canvas画布大小,实现图片缩放,//最后利用Canvas的toDataUrl方法,获得画布上的图片内容function filetobase64withCanvas(file,onsuccess,onerror){//这个方法只支持image方法console.log(file.type.indexOf("image/"));if(!file || file.type.indexOf("image/")==-1){ onerror({"code":400,"msg":"不支持改格式"}) return ; }var reader = new FileReader();//加载图片文件到base64 编码,image可以直接加载啦reader.readAsDataURL(file);//发起异步请求var image = new Image(); reader.onload = function(ev){ image.src = ev.target.result; };// 缩放图片需要的Canvasreader.onerror = function(ev){ onerror({"code":400,"data":ev,"msg":"读取文件出错"}); } image.onerror = function(ev){ onerror({"code":400,"data":ev,"msg":"展示图片出错"}); }var Canvas=document.createElement("Canvas");var context=Canvas.getContext("2d");//定义image 事件//base64地址图片加载完毕后image.onload=function () { console.log("image",this);// 图片原始尺寸var originWIDth=this.wIDth;var originHeight=this.height;//期待的目标尺寸var targetWIDth=originWIDth, targetHeight=originHeight;//如果原始尺寸小于压缩的尺寸,说明是放大,不在我们这里处理的范围内。//如果原始尺寸大于压缩的尺寸,说明我们需要压缩。var needcompress =originWIDth>CompressMaxWIDth || originHeight>CompressMaxHeight;if(!needcompress){ onsuccess({ "code":200, "data":this.src, "msg":"加载成功" }) } else{var orate =originWIDth/originHeight;//假设目标宽度高度都是最大值//则目标尺寸的比列如下var drate = CompressMaxWIDth/CompressMaxHeight;var k = orate/drate;//要想得到等比缩放的压缩效果,//如果k=orate/drate=1说明是等比缩放了不要处理//如果k=orate/drate>1说明当前设置的目标宽高比相比理想比例偏小,//要么增加宽度,要么降低高度,显然不能增加宽度,因此只能降低高度了//如果orate/drate<1说明当前设置的目标宽高比比相比理想比例偏大,//要么降低宽度,要么增加高度,显然不能增加高度,因此只能降低宽度了if (k>1){ targetWIDth = CompressMaxWIDth; targetHeight= Math.round(CompressMaxHeight/k); } else { targetHeight=CompressMaxHeight; targetWIDth=Math.round(CompressMaxWIDth* k); }//Canvas对图片进行缩放Canvas.wIDth=targetWIDth; Canvas.height=targetHeight;// 清除画布context.clearRect(0, 0, targetWIDth, targetHeight);// 图片压缩 context.drawImage(image, 0, 0, targetWIDth, targetHeight);// Canvas压缩并上传var dataURL=Canvas.toDataURL(file.type,CompressQuality);//成功回调onsuccess({"code":200,"data":dataURL,"msg":""}) } } }
测试 Html 代码如下
<input type="file" name="file" onchange="filetobase64withCanvas(this.files[0],oncompress,onerror)"><img class="lazyload" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsQAAA7EAZUrDhsAAAANSURBVBhXYzh8+PB/AAffA0nNPuCLAAAAAElFTkSuQmCC" data-original="" ID="testimg"/> }
效果展示如下为压缩后的图片,该图模糊不清。大小 15kb 。
如下为未压缩的图片,显然该图清晰可见。大小 128kb 。
3.2.2.3 并非所有图片都适合用 Canvas 方式进行压缩
如下几个细节需要澄清:
size 小于 100kb 的图片不适合用 Canvas 进行压缩,经过该方法处理后存储得到的图片将大于 100kb。
Canvas 压缩会导致图片失真。如果我们的图片是票据等,不适合用改方法进行压缩。
对 Canvas 方法进行处理图片时,我们应该先行判断,该图片是否适合压缩。
使用 Canvas 进行压缩,伪代码如下:
function compress(filedom,onsuccess,onerror){ //定义变量 var file = filedom.files[0]; if(file.size<1024*100){ //使用filereader将文件转成base64,然后传入onsuccess filetobase64withfilereader(file, onsuccess, onerror); }else{ //使用Canvas 将文件内容转成base64字符串,然后传入onsuccess filetobase64withCanvas(file,onsuccess,onerror); } }
3.2.2.4 上传 Base64 格式数据注意事项
通过以上我们得到了 Base64 格式内容,接下来要做的就是将该内容上传到后端,但是因为 Canvas 或者 FileReader 得到的 Base64 格式字符串中存在特殊字符,因此上传前需要做相应转换,否则将会导致后端保存的图片报错。解决这些问题的方法很多,这里推荐一种方法,就是采用 encodeURIComponent(data) 函数对 Base64 字符串内容进行预处理。后端接收到后通过类似 urIDecode 的方法进行解密,最后保存为图片文件。 简单代码如下,本代码采用 x-www-form-urlencoded
格式发送,后端 ContentType 请使用相应的方式。
function uploadbase64(url,base64data,onsuccess,onerror){var xhr= new XMLHttpRequest();var data ={}//base64编码预处理data.base64data=base64data;//如果定义了上传成功回调if(typeof onsuccess!="function"){ onsuccess = console.log; }//如果定义了上传失败回调if(typeof onerror!="function"){ onerror = console.log; }//第二个参数是后端服务地址,将做重点说明xhr.open("POST", url);//ajax发送数据var postdata=[];for(var i in data){ postdata.push(i+"="+encodeURIComponent(data[i])); } postdata.join("&");//注意下面该函数的位置,在xhr.open之后才起作用。xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded"); xhr.send(postdata.join("&"));//时间成功回调xhr.onreadystatechange = function(){ //OnReadyStateChange事件 if(xhr.readyState == 4){ //4为完成 if(xhr.status == 200){ //200为成功 onsuccess(JSON.parse(xhr.responseText)) }else{ onerror(xhr) } } }; }
3.3 在后端代码层实现压缩
正如以上所述,前端压缩虽然能实现压缩,但是导致像素失真,很多关键信息都丢失了。当时我们采用前端压缩,小票信息失真,导致图像识别准确率大大降低。有鉴于此,我们将目光转向了后端压缩技术。
3.3.1 Java 实现后端压缩
Java 后端压缩方法很多, Graphics 类的 drawImage 方法可以实现压缩,也有采用开源包的。这里采用开源包 net.coobird.thumbnailator
。核心代码如下:
Thumbnails. of(sourcefilepath). scale(rate). outputQuality(quality). toFile(destfilepath);
相关参数说明如下:
sourcefilepath 源文件图像地址。
destfilepath 缩略图地址。
rate 图片压缩比列。
quality 输出图片质量。
使用该包需要引入依赖项,以 Maven 为例:
<dependency> <groupID>net.coobird</groupID> <artifactID>thumbnailator</artifactID> <version>0.4.8</version></dependency>
Java测试代码如下
//app.javapackage com.imwinlion.thumb;import java.io.IOException;import net.coobird.thumbnailator.Thumbnails;public class ThumbApplication { public static voID main(String[] args) throws IOException{ System.out.println(args.length); if(args.length!=2){ System.out.println("jave -jar app.jar src.jpg dst.jsp"); }else{ Thumbnails.of(args[0]).scale(0.5f).outputQuality(0.6).toFile(args[1]); } }
命令行参数
其中,rate=0.5、quality=0.6,裁剪后的效果如下:
对比前端上传,显然效果清晰。
3.3.2 Golang 实现后端压缩
Golang 后端压缩需要引入第三方包如下
import ( "image" "os" "image/gif" "errors" "image/jpeg" "image/png" "github.com/nfnt/resize" )
其中 github.com/nfnt/resize
是一个第三方包,里面封装了大部分常用的图片操作工具类。
Golang 实现后端压缩核心代码如下
//srcpath:源文件路径, // dstpath:缩略图路径 //dstMaxW:缩略图最大宽度 //dstMaxH:缩略图最大高度 func thumb(srcpath,dstpath string,dstMaxW,dstMaxH int)(err error) { //打开源图 file, err := os.Open(srcpath) defer file.Close() if err!=nil{ return } //获得图片对象,以及图片格式等 origin, fmtimg, err := image.Decode(file) if err!=nil{ return } bounds := origin.Bounds() //原始图片宽度 srcw := bounds.Max.X //原始图片高度 srch := bounds.Max.Y //得到原始宽高比和给定参数的宽高比 k := (srcw / srch) / (dstMaxW / dstMaxH) targetW := dstMaxW targetH := dstMaxH //如果k>1 说明 dstMaxW/dstMaxH 偏小 //那么 dstmaxh应该缩小(dstMaxw不能增大了) //如果k<1 说明 dstMaxW/dstMaxH 偏大 //那么 dstMaxW 应该缩小(dstMaxH不能增大了) if (k > 1) { targetH = dstMaxH / k } else { targetW = dstMaxW / k } //然后采用图片压缩 out, _ := os.Create(dstpath) defer out.Close() rect := image.Rect(0,0, targetW, targetH) switch fmtimg { case "jpeg": //jpg 格式 img := origin.(*image.YCbCr) subImg := img.SubImage(rect).(*image.YCbCr) return jpeg.Encode(out, subImg, &jpeg.Options{Quality:100*targetW/srcw,}) case "png": //png 格式 Canvas := resize.Thumbnail(uint(targetW), uint(targetH), origin, resize.Lanczos3) switch Canvas.(type) { case *image.NRGBA: img := Canvas.(*image.NRGBA) subImg := img.SubImage(rect).(*image.NRGBA) return png.Encode(out, subImg) case *image.RGBA: img := Canvas.(*image.RGBA) subImg := img.SubImage(rect).(*image.RGBA) return png.Encode(out, subImg) } case "gif": //gif 格式 img := origin.(*image.Paletted) subImg := img.SubImage(rect).(*image.Paletted) return gif.Encode(out, subImg, &gif.Options{}) //用户可以添加bmp格式支持,需要安装golang.org/x/image/bmp /* case "bmp": img := origin.(*image.RGBA) subImg := img.SubImage(rect).(*image.RGBA) return bmp.Encode(out, subImg) */ default: return errors.New("ERROR FORMAT") } return nil }
检验代码如下
func main() { thumb("src.png","dst.png",930,500) }
压缩得到的效果图,和 Java 效果一致。 需要注意的是,如果需要添加 BMP 支持,需要安装 BMP 操作类。 golang.org/x/image/bmp,因为某些原因,该包不能正常安装,解决方法是在 gopath/src 目录下 新建文件夹 golang.org\x 如下所示 然后执行 clone 操作
>mkdir -p golang.org\x>cd golang.org\x>git clone https://github.com/golang/image.git
事实上很多扩展包都可以通过此方法安装,如 protobuf
>git clone https://github.com/golang/net.git>git clone https://github.com/golang/text.git>git clone https://github.com/golang/protobuf.git
3.3.3 使用 PHP 进行压缩
PHP 图片压缩使用的核心函数
imagecreatefromxxx($filepath)
表示创建一块画布,并从$filepath
路径处,载入一副 xxx
类型的图像。常用的文件处理函数如下
imagecreatefromjpeg(filepath) imagecreatefrompng(filepath) imagecreatefromwbmp(filepath) imagecreatefromgif(filepath)
PHP 图片压缩的另俩个核心函数 imagecopyresized 和 imagecopyresampled ,这俩个函数都是用来缩放的,但是都有缺陷,imagecopyresampled 得到的图片偏大,imagecopyresized 得到的图片质量较差。
bool imagecopyresampled ( resource $dst_image , resource $src_image , int $dst_x , int $dst_y , int $src_x , int $src_y , int $dst_w , int $dst_h ,int $src_w , int $src_h ) bool imagecopyresized ( resource $dst_image , resource $src_image , int $dst_x , int $dst_y , int $src_x , int $src_y , int $dst_w , int $dst_h ,int $src_w , int $src_h )
参数说明如下:
$dst_image:新建的图片。$src_image:需要载入的图片。$dst_x:设定需要载入的图片在新图中的x坐标。$dst_y:设定需要载入的图片在新图中的y坐标。$src_x:设定载入图片要载入的区域x坐标。$src_y:设定载入图片要载入的区域y坐标。$dst_w:设定载入的原图的宽度(在此设置缩放)。$dst_h:设定载入的原图的高度(在此设置缩放)。$src_w:原图要载入的宽度。$src_h:原图要载入的高度
PHP 压缩文件一般流程如下
$filename="src.jpg";//创建一幅图片对象$src_image=imagecreatefromjpeg($filename);//获得图片的原始宽度和高度list($src_w,$src_h)=getimagesize($filename);//设置缩放比例$scale=0.5;//获得压缩后的图片宽度和高度,这个也可以借鉴golang例子自动计算获得。$dst_w=ceil($src_w*$scale); $dst_h=ceil($src_h*$scale);//创建一块画布$dst_image=imagecreatetruecolor($dst_w, $dst_h);//把源图片画到新创建的画布上imagecopyresampled($dst_image, $src_image, 0, 0, 0, 0, $dst_w, $dst_h, $src_w, $src_h);//设置输出格式header("content-type:image/jpeg");//输出图片imagejpeg($dst_image);//释放内存空间imagedestroy($src_image); imagedestroy($dst_image);
我们封装函数如下:
//$srcpath:源文件路径,//$dstpath:缩略图路径//$dstMaxW:缩略图最大宽度//$dstMaxH:缩略图最大高度//$method:默认的图片压缩方式.imagecopyresampledfunction thumb($srcpath,$dstpath,$dstMaxW,$dstMaxH,$method="sampled"){ /* getimagesize返回说明 索引 0 给出的是图像宽度的像素值 索引 1 给出的是图像高度的像素值 索引 2 给出的是图像的类型,返回的是数字,其中1 = GIF,2 = JPG,3 = PNG,4 = SWF,5 = PSD,6 = BMP,7 = TIFF(intel byte order),8 = TIFF(motorola byte order),9 = JPC,10 = JP2,11 = JPX,12 = JB2,13 = SWC,14 = IFF,15 = WBMP,16 = XBM 索引 3 给出的是一个宽度和高度的字符串,可以直接用于 HTML 的 <image> 标签 */ list($srcW, $srcH, $type, $attr) = getimagesize($srcpath); $imageinfo = array( 'wIDth'=>$srcW, 'height'=>$srcH, 'type'=>image_type_to_extension($type,false), 'attr'=>$attr ); $imagetype = $imageinfo['type']; //获得处理函数 $fun = "imagecreatefrom".$imagetype; //获得原始图片 $origin = $fun($srcpath); //得到原始宽高比和给定参数的宽高比 $k = ($srcW / $srcH) / ($dstMaxW / $dstMaxH); $targetW = $dstMaxW; $targetH = $dstMaxH; //如果k>1 说明 dstMaxW/dstMaxH 偏小 //那么 dstmaxh应该缩小(dstMaxw不能增大了) //如果k<1 说明 dstMaxW/dstMaxH 偏大 //那么 dstMaxW 应该缩小(dstMaxH不能增大了) if ($k > 1) { $targetH = $dstMaxH /$k; } else { $targetW = $dstMaxW /$k; } $thump = imagecreatetruecolor($targetW,$targetH); //将原图复制带图片载体上面,并且按照一定比例压缩,极大的保持了清晰度 $copyfun = "imagecopyresampled"; if($method=="resized"){ $copyfun = "imagecopyresized"; } $copyfun($thump,$origin,0,0,0,0,$targetW,$targetH,$srcW,$srcH); $funcs = "image".$imagetype; $funcs($thump,$dstpath); imagedestroy($origin); imagedestroy($thump); } //测试代码如下thumb("./src.png","resized.png",930,500,"resized"); thumb("./src.png","resampled.png",930,500,"resampled");
如下是我我们用 imagecopyresized 得到的效果图,大小 38kb,质量非常模糊。 如下使我们用 imagecopyresampled 得到的效果图,大小为 178kb,原图大小 128kb。
显然 imagecopyresampled 质量优于 imagecopyresized。 需要注意的是,PHP 图像处理需要开启 GD 库支持。具体操作,如下:
打开 PHP.ini 文件中可以加载 GD 库,可以在 PHP.ini 文件中到如下扩展,
;extension=PHP_gd2.dll
将选项前的分号删除,保存,再重启 Apache 服务器即可。
上面所述都是利用应用层代码实现压缩,实际上我们可以在服务器层面进行压缩,比如 Nginx 服务器,可以扩展图片压缩模块。
3.4 自建图片服务器进行压缩
Nginx 服务器可以扩展图片处理模块,它和需要缩略图机制的应用场景非常契合。该服务器一般与缓存配合使用。
3.4.1 安装模块
编译前请确认您的系统已经安装了libcurl-dev libgd2-dev libpcre-dev 依赖库
#Debian / Ubuntu 系统举例# 如果你没有安装GCC相关环境才需要执行$ sudo apt-get install build-essential m4 autoconf automake make $ sudo apt-get install libgd2-noxpm-dev libcurl4-openssl-dev libpcre3-dev#CentOS /RedHat / Fedora举例# 请确保已经安装了gcc automake autoconf m4 #$ sudo yum install gd-devel pcre-devel libcurl-devel 支持Nginx和Tengine,两者选其一# 下载Nginx$ wget http://nginx.org/download/nginx-1.4.0.tar.gz#解压$ tar -zxvf nginx-1.4.0.tar.gz $ cd nginx-1.4.0#下载 图片压缩模块$ wget https://github.com/oupula/ngx_image_thumb/archive/master.zip#解压$ unzip master.zip#配置$ ./configure --add-module=./nginx-image-master#编译$ make#安装$ sudo make install
3.4.2 设置配置文件
打开 Nginx 配置文件nginx.conf
vim /etc/nginx/nginx.conf
不同的系统该文件路径不一样,请按照自己的系统为准。
location / { root html; #添加以下配置 image on; image_output on; }
或者指定目录开启
location /mnt { root html; image on; image_output on; }
3.4.3 参数说明
image on/off:是否开启缩略图功能,默认关闭。 imagebackend on/off:是否开启镜像服务,当开启该功能时,请求目录不存在的图片(判断原图),将自动从镜像服务器地址下载原图。 imagebackendserver:镜像服务器地址。 imageoutput on/off:是否不生成图片而直接处理后输出,默认 off。 imagejpegquality 75:生成 JPEG 图片的质量默认值 75。 imagewater on/off:是否开启水印功能。 imagewatertype 0/1:水印类型 0,图片水印 1,文字水印。 imagewatermin 300 300:图片宽度 300 高度 300 的情况才添加水印。 imagewaterpos 0-9:水印位置默认值9,0为随机位置,1为顶端居左,2为顶端居中,3为顶端居右,4为中部居左,5为中部居中,6为中部居右,7为底端居左,8为底端居中,9为底端居右。 imagewaterfile: 水印文件(jpg/png/gif),绝对路径或者相对路径的水印图片。 imagewatertransparent: 水印透明度,默认 20,越小越透明,0最透明 。 imagewatertext:水印文字 "Power By Vampire"。 imagewaterfontsize:水印大小 默认 5 。 imagewaterfont: 文字水印字体文件路径。 imagewatercolor: 水印文字颜色,默认 #000000 。
3.4.4 调用说明
这里假设你的 Nginx 访问地址为 http://localhost/
,并在 Nginx 网站根目录存在一个 test.jpg 的图片。通过访问http://localhost/test.jpg!c300x200.jpg
,将会 生成或输出一个 300x200 的缩略图。其中 300 是生成缩略图的宽度,200 是生成缩略图的 高度。一共可以生成四种不同类型的缩略图。支持 jpeg/png/gif (Gif生成后变成静态图片)。
C 参数按请求宽高比例从图片高度 10% 处开始截取图片,然后缩放/放大到指定尺寸( 图片缩略图大小等于请求的宽高 )。
M 参数按请求宽高比例居中截图图片,然后缩放/放大到指定尺寸( 图片缩略图大小等于请求的宽高 )。
T 参数按请求宽高比例按比例缩放/放大到指定尺寸( 图片缩略图大小可能小于请求的宽高 )。
W 参数按请求宽高比例缩放/放大到指定尺寸,空白处填充白色背景颜色( 图片缩略图大小等于请求的宽高 )。
3.4.5 调用举例
正如前面所说,调用图片将采用如下所示格式:
http://oopul.vicp.net/12.jpg!c300x300.jpghttp://oopul.vicp.net/12.jpg!t300x300.jpghttp://oopul.vicp.net/12.jpg!m300x300.jpg
3.5 使用云服务进行压缩
提供图片服务的云平台有很多,这里以阿里云 OSS 为例。
3.5.1 关于存储
OSS 提供海量、安全、低成本、高可靠的云存储服务,提供 99.999999999% 的数据可靠性。使用 RESTful API 可以在互联网任何位置存储和访问,容量和处理能力弹性扩展,多种存储类型供选择全面优化存储成本。
3.5.2 关于缩略图
阿里云可以配置图片处理样式,在 OSS 后台 > 相应 Bucket > 图片处理 下新建样式如 thumb256 : 是要使用图片时,只需要按照如下格式即可使用。
域名/sample.jpg?x-oss-process=style/stylename 或者 域名/example.jpg@!panda_style
举个栗子:
http://image-demo.oss-cn-hangzhou.aliyuncs.com/example.jpg?x-oss-process=style/panda_style或者 http://image-demo.oss-cn-hangzhou.aliyuncs.com/example.jpg@!panda_style
3.6 关于图片旋转问题
在使用上传过程中,发现部分三星手机以及一部分苹果手机出现图片上传后旋转了 90 度的情况,解决思路如下:
Step1: 获得图片旋转角度 a,将 a 传递到后端,代码如下:
//引进Exif.js这个js能得到图片的一些旋转信息<script class="lazyload" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsQAAA7EAZUrDhsAAAANSURBVBhXYzh8+PB/AAffA0nNPuCLAAAAAElFTkSuQmCC" data-original="https://cdn.jsdelivr.net/npm/exif-js"></script> <script> //定义回调函数,获得旋转角度。 //orient是1-8之间的数字,1是正常的。 //关于orient,可以看下图function getorient(base64data,callback){ var image = new Image(); image.src = base64data; image.onload = function(){ var orient = getPhotoOrientation(image); callback(orient); } } </scipt>
Step2:根据 a 参数,判断要旋转矫正的角度,然后进行旋转。
这里的难点在于寻找 orient 与旋转的对应关系,以及翻转后重新寻找中心点。对于 1、3、6、8 旋转可以做到。 对于 2、4、5、7 ,需要进行 X 轴或者 Y 轴翻转。
function getangle(orient){ switch (orient){ case 8: return 2*Math.PI*270/360; case 3: return 2*Math.PI*180/360; case 6: return 2*Math.PI*90/360;case 1: return 0; default: return 0; } }
以 PHP 为例,src_im 为原图片,angle 为旋转的角度。
resource imagerotate(resource src_im , float angle, int bgd_color [,int ignore_transpatrent])
Golang 图片旋转需要使用 Google 提供的包
import ( "code.google.com/p/graphics-go/graphics" ) #核心代码 func main() { //加载图片 src, err := LoadImage("src.png") if err != nil { log.Fatal(err) } //获得一个新的图片用来存放旋转后的图片 dst := image.NewRGBA(image.Rect(0, 0, 350, 400)) //下面这个就是旋转多少度 err = graphics.Rotate(dst, src, &graphics.RotateOptions{3.5}) if err != nil { log.Fatal(err) } // 需要保存的文件 saveImage("dst.png", dst) }
运行效果如下,该例子来自互联网。
Step3:保存完成。
另外一种思路,在前端完成。需要用到 Canvas,用于前端 Canvas 数据采集质量较差,这里我们将不做介绍。
3.7 建议
就笔者经验,建议图片处理采用第三方云服务的形式,如七牛、阿里Oss、腾讯的 Oss 都是不错的选择。主要原因如下:
可以规避搭建图片服务器的一系列问题。
上传性能,下载响应速度都有保障。
存储空间够大,可以无限伸缩扩容。
提供的服务十分丰富,价钱合适。
后端接收图片
4.1 接收上传的文件
以 Golang 为例,后端上传文件后我们将文件一般按照如下流程进行处理:
func upload(w http.ResponseWriter, r *http.Request)string { //1.获取文件内容 要这样获取,这个file就是前端dom的名称 srcfile, head, err := r.FormFile("file") //head.Filename=测试文件.jpg tmp := strings.Split(head.Filename,"."); //2. 获得文件后缀 .jpg sufix := "."+tmp[len(tmp)-1] //3. 按照时间戳获得新文件名称 dstfilename dstfilename := fmt.Sprintf("%d",time.Now().Unix()) //4. 创建新文件 dstfile, err := os.Create(dstfilename + sufix) //5. 最后将上传的带的图片copy,到目的文件处,进行处理 _, err = io.Copy(dstfile, srcfile) //6. 返回文件路径 return filename+sufix }
具体起来,需要注意的是如下几个方面 :
解析上传文件操作函数 FormFile(filekey) 不能硬编码,在以上代码中已经被硬 编码成“file”,显然这是不利于扩展的,解决办法如下:
//javascript端添加一个字段,filekey;var file = this.files[0];var formdata=new FormData();var filekey = "file"; formdata.append("filekey",filekey); formdata.append(filekey,file); .....
后端接收时:
r.ParseForm() //先解析filekey,获得filekey="file" filekey := r.PostForm.Get("filekey") //然后再根据key解析这个文件 srcfile, head, err := r.FormFile(filekey)
做好异常处理。文件上传过程比较耗费时间,并且受网络影响较大,因此会存在各种莫名其妙的错误,这里需要处理好。
封装友好的返回结果。文件上传结果提示要友好,这里以返回 Json 为例说明:
{"code":0,//code是错误码,0表示成功,其他错误码可以自定义"data":"url",//当上传成功后该字段内容为图片的url地址"msg":"",//上传过程发生错误时,该字段用作错误提示。 }
考虑到以上几个方面,我们给出如下例子。
//封装返回到前端的结果 func RespJson(w http.ResponseWriter,data interface{}){ header :=w.Header() header.Set("Content-Type","application/json;charset=utf-8") w.WriteHeader(http.StatusOK) ret,err :=json.Marshal(data) if err!=nil{ fmt.Println(err.Error()) } w.Write(ret) } //定义返回的数据结构体 type H struct { Code int `json:"code"` Data interface{} `json:"data,omitempty"` Msg string `json:"msg,omitempty"` } //具体上传列子 func upload(w http.ResponseWriter, r *http.Request){ //获取文件内容 要这样获取,这个file就是前端dom的名称 //r.ParseForm() //获得filekey=file filekey := r.PostFormValue("filekey") //解析这个字段对应的文件 srcfile, head, err := r.FormFile(filekey) //错误一般是file名称不对导致的 if err != nil { fmt.Println(err) RespJson(w,H{ Code:-1,Msg:err.Error(), }) return } defer func() { srcfile.Close() }() //创建文件,head.Filename=测试文件.jpg tmp := strings.Split(head.Filename,"."); //获得文件后缀.jpg sufix := "."+tmp[len(tmp)-1] filename := fmt.Sprintf("%d",time.Now().Unix()) //创建文件夹,用来存储文件 os.MkdirAll("./mnt/",os.ModePerm) //然后打开一个文件 dstfile, err := os.Create("./mnt/"+filename + sufix) defer dstfile.Close() if err != nil { RespJson(w,H{ Code:-1,Msg:"文件保存失败", }) return } //最后将上传的带的图片,进行处理 _, err = io.Copy(dstfile, srcfile) defer func() { //上传上来的临时文件一律删除 os.Remove(head.Filename) }() if err != nil { RespJson(w,H{ Code:-1,Msg:"文件移动失败", }) return } RespJson(w,H{ Data: "/mnt/"+filename+sufix, Code:0, }) }
Curl 测试结果如下
>curl http://localhost:8080/upload -F "file=@./src.png" -F "filekey=file" -v * Trying ::1... * TCP_NODELAY set* Connected to localhost (::1) port 8080 (#0)> POST /upload HTTP/1.1 > Host: localhost:8080 > User-Agent: curl/7.55.1 > Accept: */* > Content-Length: 70703 > Expect: 100-continue > Content-Type: multipart/form-data; boundary=------------------------11e46dfc83644466 > < HTTP/1.1 100 Continue < HTTP/1.1 200 OK < Content-Type: application/json;charset=utf-8 < Date: Sat, 23 Feb 2019 13:47:37 GMT < Content-Length: 39 < {"code":0,"data":"/mnt/1550929657.png"}
注意其中的{"code":0,"data":"/mnt/1550929657.png"}
是返回的Json
4.2 接收 Base64 字符串
4.2.1 前端编码注意事项
具体获得 Base64 编码我们已经做了详细的阐述,这里将不再描述。需要注意的是,Base64 格式编码包含一些特殊字符,如; / ? : @ & = + $ , #
。 这些需要用 encodeURIComponent
进行处理,否则,后端将只能获得获得如上所述任意特殊字符前的内容,举例如下:
//如前端通过字段base64data发送数据 data:image/bmp;base64,Qk12HhUAAA..//后端接收到的数据如下,从:号处被截断了data
此时后端返回 JSON
{"code":-1,"msg":"illegal base64 data at input byte 4"}
这是因为后端仅仅收到一个字符串 data
,因此不能识别为Base64 文件,所以报错。
4.2.2 后端编码注意事项
后端编码主要干俩个事情,一个是获得图片的格式类型,一个是将内容编码成文件。
如何获得图片格式类型呢?这里提供俩种方法。
前端将格式类型传输到后端,后端接收。
//前端var fromdata = new FormData(); fromdata.append("filetype",".jpg")//后端接收filetype = r.PostFormValue("filetype")
后端根据类型自动解析
//解析获得前端发过来的 filekey := r.PostFormValue("filekey")//获得前端发过来的base64data:data:image/bmp;base64,Qk12HhUAAA..base64datawithhead:=r.PostFormValue(filekey) //将字符串截成俩部分 base64data := strings.Split(base64datawithhead,";base64,") if len(base64data)!=2{ return } //第二部分编码成文件内容 filebuf,err := base64.StdEncoding.DecodeString(base64data[1]) //错误一般是file名称不对导致的 if err != nil { fmt.Println(err) RespJson(w,H{ Code:-1,Msg:err.Error(), }) return } //默认一下文件类型为png, filetype := ".png" //如果包含data:image/png 就是png文件,其他依次类推 if strings.Contains(base64data[0],"data:image/png"){ filetype = ".png" }else if strings.Contains(base64data[0],"data:image/jpeg"){ filetype = ".jpg" }else if strings.Contains(base64data[0],"data:image/gif"){ filetype = ".gif" }else if strings.Contains(base64data[0],"data:image/bmp"){ filetype = ".bmp" }else{ RespJson(w,H{ Code:-1,Msg:"不支持的文件格式", }) return }
真正编码入图片的内容是 Base64 后面的内容。这意味着前端传入后端的数据需要在
;base64,
处分割掉。Java8 编码已经包含 Base64 编解码包,无需引入第三方包。Java7 需要引入第三方包。
此方法可能存在 Post 传递参数字段超出大小限制的情况,需要对服务器进行配置。
Tomcat 配置如下。
#tomcat/conf/server.xml<Connector port="8080" protocol="HTTP/1.1" connectionTimeout="20000" redirectPort="8443" maxPostSize="0"/>maxPostSize="0" 表示取消大小限制
Nginx 配置如下:
#/etc/nginx/nginx.confserver{#location /{ #配置最大1G client_max_body_size 1000m; } }
Apache+PHP 类型服务器:
#httpd.conf中添加如下10*1024*1024=10485760LimitRequestBody 10485760#PHP.ini中添加#max_execution_time = 30 ,每个脚本运行的最长时间,单位秒,修改为:max_execution_time = 150#max_input_time = 60,每个脚本可以消耗的时间,单位也是秒,修改为:max_input_time = 300#memory_limit = 128M,脚本运行最大消耗的内存,根据你需求修改为:memory_limit = 256M#post_max_size = 8M,表单提交最大数据为 8M,此项不是限制上传单个文件的大小,而是针对整个表单的提交数据进行限制的。限制范围包括表单提交的所有内容.例如:发表贴子时,贴子标题,内容,附件等…这里修改为:post_max_size = 20M#upload_max_filesize = 2M ,上载文件的最大许可大小 ,修改为:upload_max_filesize = 10M
Golang 类 大小无限制,多么 Happy 的一件事啊!我爱Golang!
4.3 文件名称的学问
4.3.1 传统文件命名方法存在的问题
在日常开发中,很多人对图片文件命名无任何设计, 所以的到的图片大概是这样的:
https://www.imwinlion.com/wp-content/uploads/2016/04/新建图像-300×300.png
像这种图片结构,在应用根目录下新建一个 uploads
文件夹,然后下面分日期 2016/04/
。这种方式,在小微型的应用中,还能勉强可用,一旦进入中大型应用场景,图片数量越来越多,并且,可能有其他的特殊需要,比如需要存储的是图片的 ID 时,我们该怎么做呢?这是很多初学者从来没想过的问题。
4.3.2 设计文件名称和存储路径
笔者研读阿里的 Fastdfs,并经过大量的应用实践,积累了一套实用性强,操作方便的图片命名策略,格式如下:
[hostID][depth][dirstr][filename][suffix]
这种名称策略怎么理解呢?
hostID:图片服务器资源 ID 这个涉及到我们自身的服务器资源规划问题。自定义的,比如我们应用占用了俩台服务器资源,一台是应用服务器,我们编号为 0,一台是资源服务器,我们将这个服务器资源编号为1,这个数据就是 hostID。
depth:相对于图片服务根目录来说,图片存储的文件夹目录深度,一般为俩层。比如我们存储的根目录为
/mnt/h5app/
,那么这下面 的 目录a/b/c
,depth 就是 3。dirstr:目录字符串,如上,目录字符串就是
abc
。filename:md5 策略或者uuID 策略生成 的文件名,如
md5(microtime() . mt_rand(1000,9999))
。suffix:文件的后缀,如 .jpg、.png。
举个列子。假设我们的应用服务器服务器编号是 0,根目录是 /alIData/www/www.imwinlion.com/
,对应的域名是 www.imwinlion.com
。又假如我们的图片服务器有 3 台,一台编号是 1,图片存储目录是/mnt/www.imwinlion.com/
,对应的域名是 res1.imwinlion.com
;另一台编号是 2,图片存储目录是 /mnt/www.imwinlion.cn/
,对应的域名是 res2.imwinlion.com
。
现在,我们根据我们设计的文件存储策略,假设名称是13abc01f9c76ce5c45aeec2e6f816c95b854b.jpg
,那么根据问件夹的第 1 位数字,我们很容易知道这个文件 hostID 是 1,文件存在编号为 1 的服务器上,也就是 /mnt/www.imwinlion.com/
下。 根据第二个参数 depth 为 3,我们知道这个文件有 3 个父级子目录,那么接下来的三个字母 abc 就是 dirstr , 接下来的 01f9c76ce5c45aeec2e6f816c95b854b
是 Md5 生成的随机字符串。最后 .jpg 是文件格式,那么这个文件的存储路径应该是这样
/mnt/www.imwinlion.com/a/b/c/13abc01f9c76ce5c45aeec2e6f816c95b854b.jpg
对应访问地址是这样
http://res1.imwinlion.com/a/b/c/13abc01f9c76ce5c45aeec2e6f816c95b854b.jpg
文件 ID 就是文件名称 13abc01f9c76ce5c45aeec2e6f816c95b854b.jpg 。
我们数据库存这个 ID ,前端只要根据策略和这个ID就能够获得 图片路径。这种策略的核心在于先根据业务逻辑规划出图片存放路径。是先知道了文件路径,然后再存储。但是我们常用的框架比如 ThinkPHP
,把文件的命名策略封闭起来了,我们只是先保存了文件,再获得一个返回的文件名,这是不可取的。
4.3.3 文件 ID 生成器
文件 ID 生成器,以 Golang 为例
//hostID:主机ID //depth:深度ID //dirstr:子目录字符串 //suffix:文件类型.png等 //return :文件路径(不包含hosID对于路径)和文件ID func fileID(hostID,depth int,dirstr,suffix string)(path,ID string){ var subdir []string = make([]string,0) for i:=0;i<depth;i++{ subdir =append(subdir,fmt.Sprintf("%c",dirstr[rand.Intn(1024)%len(dirstr)])) } uuIDs,_ := uuID.NewV4() randomstr := base64.StdEncoding.EncodeToString([]byte(uuIDs.String())) ID =fmt.Sprintf("%d%d%s%s%s",hostID,depth,strings.Join(subdir,""),randomstr,suffix) path = fmt.Sprintf("%s/%s",strings.Join(subdir,"/"),ID) return }
提升图片服务的性能
图片存储服务在上传过程中占用大量的 IO 资源,在图片处理过程中占用大量的 CPU 资源,反应在用户端,就是很卡,很慢,我们要想办法提升图片应用服务性能。
5.1 支持异步
5.1.1 Java 类应用
一般部署在 Tomcat 容器上,Tomcat6 以后 protocol 支持 BIO 和 NIO。Nio 方式比 Bio 具有更好的并发性。同时,我们还可以扩大线程池数目 maxThreads。主要配置如下。
#tomcat 配置文件server.xml<Connector port="8080" protocol="org.apache.coyote.http11.Http11NioProtocol"maxThreads="150"connectionTimeout="20000" redirectPort="8443" />
5.1.2 Golang 类应用
Golang 自身具备良好的并发性,Golang 可以采用携程机制,具体框架如下:
//定义最基础的处理单元type Task struct { Writer http.ResponseWriter Request *http.Request }//定义任务管理器type TaskMgr struct { TaskQueue chan Task }//初始化一个变量var taskMgr = TaskMgr{ TaskQueue:make(chan Task,1024), }//开启任务func StartTask(mgr TaskMgr){ go func() { for{ select { case task :=<-mgr.TaskQueue: go upload(task.Writer,task.Request) break; } } }() }func main() { //thumb("src.png","dst.png",930,500) //http.HandleFunc("/upload",upload) //将所有的/uploadbase64映射都发往chan队列中 http.HandleFunc("/uploadbase64", func(writer http.ResponseWriter, request *http.Request) { //需要对request对象进行特殊处理,要不然数据复制失败 bodyBytes, _ := ioutil.ReadAll(request.Body) request.Body.Close() // must close request.Body = ioutil.NopCloser(bytes.NewBuffer(bodyBytes)) //把数据传递到消息队列中 taskMgr.TaskQueue<-Task{ Writer:writer, Request:request, } }) //http.HandleFunc("/uploadbase64",uploadbase64) http.Handle("/html/",http.StripPrefix( "/html/", http.FileServer(http.Dir("html")))) http.Handle("/mnt/",http.StripPrefix( "/mnt/", http.FileServer(http.Dir("mnt")))) //fmt.Println(fileID(1,2,"123456789abcedf",".png")) //开启消息chan服务 StartTask(taskMgr) http.ListenAndServe(":8080",nil) }
5.2 资源服务和应用服务分离
资源服务和应用服务不一样,资源服务在图片应用中主要表现在图片的读取和显示。而应用服务主要是对数据的读写,以及一些数据计算,业务类型是不一样的。因此针对这种情况,我们建议将资源与应用服务相互分离。
5.3 建立服务器级别缓存
一般来说,我们对资源类的服务做缓存支持,可以极大地提升响应速度,以一张 300kb 图片为例,不做缓存,响应速度 300ms,增加缓存配置后,响应速度降低到 80ms,这是非常有效的。 不同的服务器配置和网络环境上述参数是不一样的。
以 Nginx 为例, 参数举例如下:
proxy_cache cachename;proxy_cache_valID 304 2h;proxy_cache_valID 403 444 24h;proxy_cache_valID 404 2h;proxy_cache_valID 500 502 2h;proxy_cache_use_stale invalID_header http_403 http_404 http_500 http_502;proxy_cache_lock on;proxy_cache_lock_timeout 5s;proxy_no_cache $proxynocache_atomxml $proxynocache_sitemapxml;
•proxy_cache:对应 http 段的 keyzone,是你定义的 proxy_cache 所使用的共享空间的名称。 •proxy_cachevalID:对指定的 HTTP 状态进行缓存,并指定缓存时间。可以自定义写入多个配置项。 •proxy_cachestale:这个可以大大减少回源次数,因此可以将 inactive 适当延长。 •proxy_cachelock:同样是减少回源次数,和上面的差别在于缓存是否存在。 •proxy_no_cache: 0 表示使用缓存,其他任何值都表示不使用缓存。
另一方面,我们还可以使用 expires
指令对资源过期时间进行限制,以达到减少从服务器读取内容的次数的目的。
server { listen 80 default_server; server_name www.imwinlion.com; # 通过此语句来映射静态资源 root /data/www/html/; #任何图片都缓存七天 location ~ .*\.(?:jpg|jpeg|gif|png|ico|cur|gz|svg|svgz|mp4|ogg|ogv|webm)$ { expires 7d; } #css 类的资源7天过期 location ~ .*\.(?:js|css)$ { expires 7d; } #html类的资源不缓存 location ~ .*\.(?:htm|html)$ { add_header Cache-Control "private, no-store, no-cache, must-revalIDate, proxy-revalIDate"; } }
5.4 单机 Or 分布式文件系统
在系统开始我们犯了一个错误,搭建了分布式文件系统 Fastdfs,事实证明完全没必要。主要有如下几点:
小批量应用,完全可以由单机存储应付,因为硬盘存储价格便宜。
对于海量图片应用,建议采用 OSS。OSS 本身能提供海量资源存储功能,另外支持常用的图片服务,如裁剪、缩略图等,价格也适宜。
5.5 没有使用 CDN
CDN 加速似乎成为了海量资源应用服务的标配,但是我们这个应用没有使用 CDN 加速功能,因为甲方的客户都在南方。我们的服务器也在南方。重要的事情只说一遍:
用不用 CDN 应该视目标受众群的网络情况而定。
5.6 使用了子域名
为什么要使用子域名?因为子域名和应用域名分离,图片资源访问时,可以少携带一些参数,提高响应速度。
最初,我们文件系统部署在自建服务器上,随着文件越来越多,服务越来越慢。后来做了优化,使用了子域名,但是很尴尬,效果并不明显。
最后我们搬迁到 OSS 上,从此以后响应速度快了,存储空间不需担心了,图片裁剪性能也不用担心了,我们的程序兄弟过上了幸福的生活。
5.7 善于使用缩略图
前端列表显示时候,如果后端能提供缩略图功能,建议加上缩略图,这样做可以减小图片大小,减少图片下载时间,也就降低手机端图片的渲染时间。OSS 本身提供缩略图功能,大爱!
5.8 其他
其他的一些细节
一页每次加载不超过 40 条记录,记录太多则加载时间长,页面卡顿。
列表应用页面应支持上拉加载和下拉刷新,用户肯定不会多么讨厌你的应用。
上传过程中一定要添加进度条提示,这样用户就不会焦躁不安啦。
关键性操作按钮,比如上传按钮,应该加锁,防止用户重复点击。为什么?因为手机环境网络不稳定的所以上传时好时坏,用户要是觉得没有反应会再次点击,从此进入一个死循环。
获得源代码及更多支持
笔者已经将前端压缩代码重构成一个不到1kb的 JS,请加 betaidea 回复golang 获得。
共同学习,写下你的评论
评论加载中...
作者其他优质文章