openresty實現圖片(文件)服務器

介紹

前序

該功能是利用openresty的lua腳本實現的圖片(文件)保存功能,文件上傳使用java代碼開發的

數據定義

上傳數據和文件信息不分前後,但系統只會保存最後一對信息

  • 數據格式:
{"fileDir":"文件保存的目錄","fileName":"文件名"}
  • 返回結果
{"status":"是否成功","result":"返回結果","msg":"異常原因"}
enum status:["success","failed"]
  • 保存文件夾
    所保存到那個文件夾下,在nginx的perfix變量中定義

代碼實現

Nginx配置

如下:

server {
    listen       80;
    server_name  localhost;
# 配置保存的文件夾
    set $prefix "/data";

    location /uploadimage {
# 配置是否每次lua更改都生效,適合調試時使用
#       lua_code_cache off;
# 配置lua腳本
        content_by_lua_file /openresty-web/luascript/luascript;
    }
# 用來配合理解傳入到nginx的報文結構
    location /uploadtest{
#       lua_code_cache off;
        content_by_lua_file /openresty-web/luascript/luauploadtest;
    }
    error_page   500 502 503 504  /50x.html;
    location = /50x.html {
        root   html;
    }
}

lua腳本

luascript:

package.path = '/openresty-web/lualib/resty/?.lua;'
local upload = require "upload"
local cjson = require("cjson")

Result={status="success",result="",msg=""}
Result.__index=Result
function Result.conSuccess(ret)
    ret["status"]="success"
    ret["result"]="upload success"
    return ret
end

function Result.conFailed(ret,err)
    ret["status"]="failed"
    ret["msg"]=err
    ret["result"]="upload failed"
    return ret
end

function Result:new()
    local ret={}
    setmetatable({},Result)
    return ret
end

-- lua-resty-upload
local chunk_size = 4096
local form = upload:new(chunk_size)
if not form then
    ngx.say(cjson.encode(Result.conFailed(Result:new(),"plase upload right info")))
    return 
end
local file
local filelen=0
form:set_timeout(0) -- 1 sec
local filename
local prefix=ngx.var.prefix

-- 匹配文件名,當前案例用於判斷是否是文件模塊
function get_filename(res)
    local filename = ngx.re.match(res,'(.+)filename="(.+)"(.*)')
    if filename then 
        return filename[2]
    end
end


-- 用來開啓輸入流,當文件夾不存在時自動創建
function openstream(fileinfo,opt)
    local file,err=io.open(prefix..fileinfo["fileDir"],"r")
    if not file then
        local start=string.find(err,"No such file or directory")
        if start then
            local exeret=os.execute("mkdir -p "..prefix..fileinfo["fileDir"])
            if exeret ~= 0 then
                return nil,"Make directory failed"
            end
        else
            return nil,err
        end
    end
    file,err=io.open(prefix..fileinfo["fileDir"]..fileinfo["fileName"],opt)
    return file,err
end

local osfilepath
local tmpfiletbl
local hasFile=false
local loopfile=false
local fileinfostr
local fileinfo
local result=Result:new()
-- 循環讀取文件和文件信息
while true do
    local typ, res, err = form:read()
    if not typ then
        break
    end
    if typ == "header" then
        if res[1] ~= "Content-Type" then
            filename = get_filename(res[2])
            if filename then
                loopfile=true
                hasFile=true
                -- 判斷是否有文件信息
                -- 如果沒有記錄內存
                if fileinfo then
                    file,err=openstream(fileinfo,"w")
                    if not file then
                        break
                    end
                else
                    tmpfiletbl={}
                end
            else
                loopfile = false
                fileinfostr = ""
            end
        end
    end
    if loopfile then
        if typ == "body" then
            if file then
                filelen= filelen + tonumber(string.len(res))    
                file:write(res)
            else
                table.insert(tmpfiletbl,res)
            end
        elseif typ == "part_end" then
            if file then
                file:close()
                file = nil
            end
        end
    else
        if typ == "body" then
            fileinfostr=fileinfostr .. res
        elseif typ == "part_end" then
            fileinfo = cjson.decode(fileinfostr)
        end
    end
    if typ == "eof" then
        break
    end
end

if not hasFile then
    err="plase upload file"
elseif not fileinfo or not fileinfo["fileDir"] or not fileinfo["fileName"] then
    err="plase offer file info"
end

if err then
    ngx.log(ngx.ERR,err)
    Result.conFailed(result,err)
    ngx.say(cjson.encode(result))
    return 
end

-- 因爲有文件信息在文件之後傳送的
-- 所以需要將輸入到內存中的文件信息打印到磁盤
if tmpfiletbl and table.getn(tmpfiletbl) > 0 then
    file,err=openstream(fileinfo,"w")
    if not file then
        ngx.log(ngx.ERR,err)
        Result.conFailed(result,err)
        ngx.say(cjson.encode(result))
        return 
    else
        for index,value in ipairs(tmpfiletbl)
        do
            filelen= filelen + tonumber(string.len(value)) 
            file:write(value)
        end
        file:close()
        file=nil
    end
end


Result.conSuccess(result)
ngx.say(cjson.encode(result))

luauploadtest:

local upload = require "resty.upload"
local cjson = require "cjson"

local chunk_size = 5 -- should be set to 4096 or 8192
                     -- for real-world settings

local form, err = upload:new(chunk_size)
if not form then
    ngx.log(ngx.ERR, "failed to new upload: ", err)
    ngx.exit(500)
end

form:set_timeout(1000) -- 1 sec

while true do
    local typ, res, err = form:read()
    if not typ then
        ngx.say("failed to read: ", err)
        return
    end

    ngx.say("read: ", cjson.encode({typ, res}))

    if typ == "eof" then
        break
    end
end

local typ, res, err = form:read()
ngx.say("read: ", cjson.encode({typ, res}))

luauploadtest代碼是官方提供代碼

Java

ImageServer:

package cn.com.cgbchina.image;

import cn.com.cgbchina.image.exception.ImageDeleteException;
import cn.com.cgbchina.image.exception.ImageUploadException;
import org.springframework.web.multipart.MultipartFile;

/**
 * Created by 11140721050130 on 16-3-22.
 */
public interface ImageServer {
    /**
     * 刪除文件
     *
     * @param fileName 文件名
     * @return 是否刪除成功
     */
    boolean delete(String fileName) throws ImageDeleteException;

    /**
     *
     * @param originalName 原始文件名
     * @param file 文件
     * @return 文件上傳後的相對路徑
     */
    String upload(String originalName, MultipartFile file) throws ImageUploadException;
}

LuaResult:

package cn.com.cgbchina.image.nginx;

import lombok.Getter;
import lombok.Setter;

/**
 * Comment: 用來保存返回結果,
 * 原本想放入到LuaImageServiceImpl的內部類中,
 * 但是Jackson不支持,沒法反序列化
 * Created by ldaokun2006 on 2017/10/24.
 */
@Setter
@Getter
public class LuaResult{
    private LuaResultStatus status;
    private String result;
    private String msg;
    private String httpUrl;
    public LuaResult(){}

    public void setStatus(String result){
        status=LuaResultStatus.valueOf(result.toUpperCase());
    }
    public enum LuaResultStatus{
        SUCCESS,FAILED;
    }
}

ImageServerImpl:

package cn.com.cgbchina.image.nginx;

import cn.com.cgbchina.common.utils.DateHelper;
import cn.com.cgbchina.image.ImageServer;
import cn.com.cgbchina.image.exception.ImageDeleteException;
import cn.com.cgbchina.image.exception.ImageUploadException;
import com.github.kevinsawicki.http.HttpRequest;
import com.google.common.base.Splitter;
import com.spirit.util.JsonMapper;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;

import java.io.*;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * Comment: 實現文件上傳功能
 * Created by ldaokun2006 on 2017/10/16.
 */
@Service
@Slf4j
public class LuaImageServiceImpl implements ImageServer{
    // 存放nginx服務器url的,某些架構會有多個放置圖片的地方
    private List<String> httpUrls;
    private ExecutorService fixedThreadPool ;
    private Integer timeout;
    private int threadSize=50;

    public LuaImageServiceImpl(String httpUrls){
        this(httpUrls,30000);
    }

    /**
     *
     * @param httpUrls 存放nginx服務器url
     * @param timeout http超時時間
     */
    public LuaImageServiceImpl(String httpUrls,int timeout){
        this.httpUrls=Splitter.on(";").splitToList(httpUrls);
        // 沒啥看得,就是想讓線程池的名字易懂些
        this.fixedThreadPool= new ThreadPoolExecutor(threadSize, threadSize,
                0L, TimeUnit.MILLISECONDS,
                new LinkedBlockingQueue<Runnable>(),new ThreadFactory(){
                    private final AtomicInteger poolNumber = new AtomicInteger(1);
                    private final ThreadGroup group;
                    private final AtomicInteger threadNumber = new AtomicInteger(1);
                    private final String namePrefix;

                    {
                        SecurityManager s = System.getSecurityManager();
                        group = (s != null) ? s.getThreadGroup() :
                                Thread.currentThread().getThreadGroup();
                        namePrefix = "LuaUploadPool-" +
                                poolNumber.getAndIncrement() +
                                "-thread-";
                    }

                    public Thread newThread(Runnable r) {
                        Thread t = new Thread(group, r,
                                namePrefix + threadNumber.getAndIncrement(),
                                0);
                        if (t.isDaemon())
                            t.setDaemon(false);
                        if (t.getPriority() != Thread.NORM_PRIORITY)
                            t.setPriority(Thread.NORM_PRIORITY);
                        return t;
                    }
                });
        this.timeout=timeout;
    }

    /**
     * Comment: 沒必要開發刪除功能
     * @param fileName 文件名
     * @return
     * @throws ImageDeleteException
     */
    @Override
    public boolean delete(String fileName) throws ImageDeleteException {
        return true;
    }

    /**
     * Commont: 用來給SpringMVC用
     * @param originalName 原始文件名
     * @param file 文件
     * @return
     * @throws ImageUploadException
     */
    @Override
    public String upload(String originalName, MultipartFile file) throws ImageUploadException {
        try {
            return this.upload(originalName,file.getInputStream());
        } catch (IOException e) {
            log.error("upload fail : " + e.getMessage(), e);
            throw new ImageUploadException("upload fail : "+e.getMessage(),e);
        }
    }

    /**
     * Commont: 上傳圖片核心代碼
     * @param originalName 原始文件名
     * @param inputStream 要上傳文件的文件流
     * @return
     * @throws ImageUploadException
     */
    private String upload(String originalName,InputStream inputStream) throws ImageUploadException {
        ByteArrayOutputStream byteOutStream = null;
        try {
            //準備數據
            byte[] tmpData=new byte[1024];
            byte[] inputData;
            byteOutStream = new ByteArrayOutputStream();
            int len=0;
            while((len=inputStream.read(tmpData,0,tmpData.length))!=-1){
                byteOutStream.write(tmpData,0,len);
            }
            inputData=byteOutStream.toByteArray();
            LuaSend sendInfo = new LuaSend(generateFileDir(),generateFileName(originalName));
            List<Future<LuaResult>> resultList=new ArrayList<>(httpUrls.size());

            //發送圖片
            for(String httpUrl:httpUrls) {
                SendImg sendImg = new SendImg(httpUrl,sendInfo, inputData,this.timeout);
                resultList.add(fixedThreadPool.submit(sendImg));
            }
            for(Future<LuaResult> future:resultList) {
                // 線程池異常在這裏拋出
                LuaResult resultLuaResult = future.get();
                if (LuaResult.LuaResultStatus.SUCCESS != resultLuaResult.getStatus()) {
                    throw new ImageUploadException("lua result url:"+resultLuaResult.getHttpUrl()+" msg : " + resultLuaResult.getMsg());
                }
            }

            return sendInfo.toString();
        }catch (Exception e){
            log.error("upload fail : "+e.getMessage(),e);
            throw new ImageUploadException("upload fail : "+e.getMessage(),e);
        }finally {
            try {
                if(byteOutStream!=null) {
                    byteOutStream.close();
                }
                if(inputStream!=null) {
                    inputStream.close();
                }
            } catch (IOException e) {
                throw new ImageUploadException("upload fail : "+e.getMessage(),e);
            }
        }
    }
    String separator=File.separator;
    String dateFormat=separator+"yyyy"+separator+"MM"+separator+"dd"+ separator;

    /**
     * Comment:根據時間做路徑,防止某一個文件夾東西太多
     * @return 返回要保存的路徑
     */
    private String generateFileDir(){
        return DateHelper.date2string(new Date(),dateFormat);
    }

    /**
     * Comment: 用UUID防止文件名重複
     * @param originalName 源文件名字
     * @return 要保存的文件名
     */
    private String generateFileName(String originalName){
        return UUID.randomUUID().toString();
    }

    /**
     * Comment: 用來發送圖片的
     */
    @AllArgsConstructor
    class SendImg implements  Callable<LuaResult>{

        private String httpUrl;
        private LuaSend sendInfo;
        private byte[] inputStream;
        private Integer timeout;


        @Override
        public LuaResult call() throws Exception {
            try {
                String resultStr = HttpRequest
                        .post(httpUrl, false)
                        .part("fileInfo", JsonMapper.JSON_NON_EMPTY_MAPPER.toJson(sendInfo))
                        // 這個地方有個坑,part上傳圖片必須要用這個方式,
                        // 不能用沒有Content-Type和fileName的
                        .part("file", sendInfo.getFileName(), "multipart/form-data; boundary=00content0boundary00", new ByteArrayInputStream(inputStream))
                        .connectTimeout(timeout).body();
                log.info("result:"+resultStr);
                LuaResult result = JsonMapper.JSON_NON_DEFAULT_MAPPER.fromJson(resultStr, LuaResult.class);
                result.setHttpUrl(httpUrl);
                return result;
            }catch(Exception e){
                throw new ImageUploadException("upload failed url:"+httpUrl+" info:"+sendInfo.toString(),e);
            }
        }
    }

    /**
     * Comment:文件數據
     */
    @Setter
    @Getter
    @AllArgsConstructor
    class LuaSend {
        // 文件目錄
        private String fileDir;
        // 文件名
        private String fileName;
        @Override
        public String toString(){
            return fileDir+fileName;
        }
    }


    /**
     * Comment:測試用
     * @param args
     * @throws ImageUploadException
     * @throws FileNotFoundException
     */
    public static void main(String[] args) throws ImageUploadException, FileNotFoundException {
        LuaImageServiceImpl service=new LuaImageServiceImpl("http://192.168.99.102/uploadimage");
        try {
            System.out.println(service.upload("qqqqq", new FileInputStream("D:\\shsh.txt")));
        }finally {
            service.fixedThreadPool.shutdown();
        }
    }
}

總結

可能出現的問題

  1. 上傳兩個圖片或圖片信息時系統只保留最後一個信息
  2. 圖片和圖片信息可以隨意放置,但是這兩個必須成對發送,建議先發送圖片信息後發送圖片,這樣圖片不用在lua處保存到內存中
  3. 上傳大圖片時會出現文件太大的提示,需要在nginx配置文件中添加client_max_body_size 100M;
  4. Http Header的Content-Type必須使用multipart/form-data;
    boundary=00content0boundary00
    ,boundary必須存在不然不好用
  5. 傳送圖片HttpRequest.part上傳圖片必須寫明Content-type和fileName,不然不好用但是Content-type不用非的用例子上的方式
  6. 圖片信息必須拷貝成byte型,因爲多線程使用時需要各自發送

開發中遇到的問題

  1. 傳送圖片HttpRequest.part上傳圖片必須寫明Content-type,不然不好用
  2. Jackson和fastjson對於需要反序列化的類,必須有無參構造函數,並且不能是內部類
  3. lua的string.find如果沒有找到,返回結果爲nil
  4. CSDN的編輯器,無需功能不好用

涉及到知識

  1. HttpRequest.part用來上傳Content-type:multipart/form-data;
  2. lua的使用:http://www.runoob.com/lua/lua-tutorial.html
  3. openresty的api:http://openresty.org/cn/components.html
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章