openresty实现图片(文件)服务器

文章目录

[隐藏]

  • 介绍
    • 前序
    • 数据定义
  • 代码实现
    • Nginx配置
    • lua脚本
    • Java
  • 总结
    • 可能出现的问题
    • 开发中遇到的问题
    • 涉及到知识
介绍

前序

该功能是利用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

原文出处:csdn -> https://blog.csdn.net/ldaokun2006/article/details/78333622

本站所发布的一切资源仅限用于学习和研究目的;不得将上述内容用于商业或者非法用途,否则,一切后果请用户自负。本站信息来自网络,版权争议与本站无关。您必须在下载后的24个小时之内,从您的电脑中彻底删除上述内容。如果您喜欢该程序,请支持正版软件,购买注册,得到更好的正版服务。如果侵犯你的利益,请发送邮箱到 [email protected],我们会很快的为您处理。