构建基于Nginx的文件服务器思路与实现 - Go语言中文社区

构建基于Nginx的文件服务器思路与实现


在Web项目中使用独立的服务器来保存文件和图片的好处很多,如:便于统一管理,分流web服务器的压力,可进行访问加速等.另外当web服务器需要做集群进行负载均衡时,图片和文件上传在各个服务器之间同步将是个麻烦.关于图片服务器的方案,网上搜集到过一些,都不太合意。于是自己想了个方案,用Nginx来做图片服务器,现在已经初步实现了。下面先说说我的思路,而后在介绍一下初步是如何实现的。
    想到用Nginx来做文件服务器,是看到Nginx有几个扩展模块,分别可实现文件的上传,图片的缩放,以及访问的代理。有了这些功能,文件可上传到服务器,访问文件时前端又可做多个代理进行分流,而且Nginx自身的高并发能力又没的说,另外还附带了一个图片缩放的功能,干嘛不用呢?
于是着手研究了一下这几个模块。发现只有一点不符合我们的要求,那就是文件上传模块的机制是支持在Nginx配置一个文件上传的url,此URL接收提交的文件并将文件临时放到Nginx所在主机的一个指定目录,而后转发请求给后台程序(也就是我们自己的web程序),由我们自己的程序实现移动文件和将文件路径等信息写入数据库的工作。这也就要求我们的后台处理程序要跟Nginx部署在同一台主机,要不然怎么能够移动文件呢?这显然不符合我们的初衷----将文件服务器独立于其他Web应用。如果我们能解决移动文件的问题,就可以清除障碍了。
    要解决移动文件,需要如下几步:
1.  利用文件上传模块原有机制,将上传的文件保存在临时目录。
2.  移动文件到我们期望的目录,并更改文件名防止重名。
3.  将移动后的目录以及文件名称等信息转发给后台web程序,由web程序自己将信息写入自己的数据库。
第一步Nginx上传模块已经实现,我们只要可以移动文件并转发请求就可以了。转发请求是nginx的强项,这个不用担心。移动文件Nginx自身却没有这个能力。有两种方法实现:1. 做一个web程序与nginx部署在一起,负责移动文件。2. 想办法让Nginx来完成移动文件。显然第一种方式比较容易实现,但第二种方式才是我想要的。
Nginx有一个扩展模块lua_nginx,此模块支持在Nginx上使用基于c的lua脚本。有脚本语言支持,可编程性就大大提高了,要完成我们移动文件的目的当然不在话下。
下面介绍下我设想的这几个模块综合应用后的文件及图片服务器的结构:

如上图,至少需要3太nginx服务器,分别负责图上标示的这些功能。当然,如果不需要将图片的访问做负载均衡,所有功能集中在一台服务器上也是可以的。
下面是实现上述功能的Nginx的安装以及配置(【路由与负载均衡】部分就不详细介绍了,这方面的资料很多。):
Nginx的安装网上很多介绍,这里不再详细说了。为了附加扩展模块,我们不能使用yum的安装方式。只能下载代码包以及扩展模块的代码包,然后使用./configure 然后make install的方式来安装。安装过程中可能会遇到一些问题,基本是缺少一些依赖什么的,根据错误提示,下载和安装缺少的软件包后就可以解决。另外,我曾遇到过upload模块与Nginx版本冲突的问题,以及make时报“md5.h 没有那个文件或目录”的错误,在我另外一篇文章里有介绍(CentOS下安装Nginx并添加nginx_upload_module).
下面先介绍下文件上传服务器的安装以及我的配置。依赖问题解决后,使用下面的脚本安装:

java 代码
  1. ./configure --prefix=/B2B/servers/nginx --add-module=/B2B/tars/masterzen-nginx-upload-progress-module-a788dea --add-module=/B2B/tars/nginx_upload_module-2.2.0 --add-module=/B2B/tars/lua-nginx-module-master --with-pcre=/B2B/tars/pcre-8.21 --with-openssl=/B2B/tars/openssl-1.0.0e 
里面分别附加了nginx_upload_module(用来接收上传文件并临时保存),nginx-upload-progress-module(可获得上传进度)lua-nginx-module(使nginx支持lua脚本,用来移动文件转发请求给后台)。
下面先上配置文件nginx.conf
js 代码
  1. #user  nobody;  
  2. worker_processes  1;  
  3.  
  4. #error_log  logs/error.log;  
  5. #error_log  logs/error.log  notice;  
  6. #error_log  logs/error.log  info;  
  7.  
  8. #pid        logs/nginx.pid;  
  9.   
  10.   
  11. events {  
  12.     worker_connections  1024;  
  13. }  
  14.   
  15.   
  16. http {  
  17.     include       mime.types;  
  18.     default_type  'text/html';  
  19.     sendfile        on;  
  20.     keepalive_timeout  65;  
  21.        
  22.     server {  
  23.         listen       80;  
  24.         server_name  localhost;  
  25.           lua_code_cache off;  
  26.           set $callback_url "/";  
  27.         location / {  
  28.             root   html;  
  29.             index  index.html index.htm;  
  30.                       }  
  31.   
  32.         location /upload{  
  33.                 client_max_body_size 35m;                # 上传文件大小限制  
  34.                 upload_cleanup 500-505;    # 发生这些错误删除文件 400 404 499 500-505  
  35.                 upload_store_access user:rw group:rw all:rw;            # 访问权限  
  36.                 # upload_limit_rate 128k;                 # 上传速度限制  
  37.                 upload_pass_args on;                        # 允许上传参数传递到后台  
  38.   
  39.                 if ($uri ~* "^/upload/(.*)") {  
  40.                     set $sub_path $1;  
  41.                 }  
  42.                 if ($uri !~* "^/upload/(.*)") {  
  43.                     set $sub_path "default";  
  44.                 }  
  45.   
  46.                 if (-d $cookie_username) {  
  47.                     set $user_name $cookie_username;  
  48.                 }  
  49.                 if (!-d $cookie_username){  
  50.                     set $user_name "nobody";  
  51.                 }  
  52.   
  53.   
  54.   
  55.                 upload_store /B2B/uploadfiles/temp;                # /B2B/uploadfiles/用户/日期/文件类型/文件名        # 本地存储位置  
  56.   
  57.                 upload_set_form_field "callback" $arg_callback;  
  58.                 upload_set_form_field "use_date" $arg_use_date;  
  59.                 upload_set_form_field "sub_path" $sub_path;   
  60.                 upload_set_form_field "user_name" $user_name;   
  61.                 upload_set_form_field "file_name" $upload_file_name;   
  62.                 upload_set_form_field "file_content_type" $upload_content_type;  
  63.                 upload_aggregate_form_field "file_md5" $upload_file_md5;  
  64.             upload_aggregate_form_field "file_size" $upload_file_size;  
  65.                 upload_set_form_field "temp_path" $upload_tmp_path;  
  66.                 upload_pass_form_field ".*";  
  67.   
  68.                 upload_pass /prossfile; # 转给文件处理(移动文件,转发请求)  
  69.                      }  
  70.             # 处理文件:使用lua脚本处理文件,将文件移动并重命名到特定的文件夹。而后将文件信息转发给后台处理程序。  
  71.         location /prossfile{  
  72.                 lua_need_request_body on;  
  73.                 content_by_lua_file /B2B/servers/nginx/luas/onupload.lua;  
  74.                      }  
  75.  
  76.           # 文件上传后台程序处理路径  
  77.          include /B2B/servers/nginx/conf/upload_callback.conf;  
  78.  
  79.            # 文件访问路径  
  80.          location /files/{  
  81.                 default_type  'application/octet-stream';  
  82.                 alias /B2B/uploadfiles/;  
  83.            }  
  84.         }  
其中location /upload接收文件的上传放到的临时目录,并整理参数,而后转发给location /prossfile。location /prossfile将使用lua脚本来处理文件的移动并再次转发请求给后台的网站。
另外,include /B2B/servers/nginx/conf/upload_callback.conf;这一句引入了另一个配置文件,这里面配置的一些location是后台的web应用用来接收文件上传的url地址。如:
js 代码
  1. location /B2B {  
  2.               proxy_pass http://192.168.3.32:8080/cookie.test/index.jsp;  
  3.          }  
  4.        location /example {  
  5.               proxy_pass http://www.oecp.cn;  
  6.          } 
实现文件移动和转发请求的部分在lua里面,也是重点部分。在实现时遇到一些麻烦,可能是lua这个模块与上传模块的冲突(具体原因不详),文件上传模块将请求转发到lua后,文件信息和form里的内容居然成了无法识别的。正常情况下,应该可以转化为一种table对象,以key-value来存取,但现在确是一个只有一行的table,key和value都是很长的字符串,为此,只能暂时用分离字符串的方式将form的内容拆分出来,而后才能转发给后台处理的web应用。
下面是脚本luas/onupload.lua的具体内容:
cpp 代码
  1. function onupload()  
  2.     ngx.req.read_body();  
  3.     local post_args = ngx.req.get_post_args();                    -- 读取参数  
  4.     local tab_params = getFormParams_FixBug(post_args);        -- 处理参数错误  
  5.   
  6.     pressFile(tab_params);        -- 处理文件  
  7.     -- ngx.log(ngx.ERR,"#############@" ,tab_params["callback"],"@###########");  
  8.     if (tab_params["callback"] and tab_params["callback"] ~= "") then  
  9.         ngx.exec(tab_params["callback"],tab_params);  -- 转发请求  
  10.     else  
  11.         ngx.say("Callback not specified!!");  
  12.     end  
  13. end  
  14.   
  15.   
  16. --[[  
  17.     处理文件  
  18.     主要进行 创建目录 & 移动文件 等操作。  
  19. ]]  
  20. function pressFile(params)  
  21.     local dirroot = "/B2B/uploadfiles/";  
  22.     local todir = params["user_name"].."/";  
  23.     if(params["sub_path"]) then  
  24.         todir = params["sub_path"].."/"..todir;  
  25.     end  
  26.     if(trim(params["use_date"]) == "Y") then  
  27.          todir = todir..os.date('%Y-%m-%d').."/"  
  28.     end  
  29.     todir = trim(todir);  
  30.     local tofile = todir..params["file_md5"]..getFileSuffix(params["file_name"]);  
  31.     tofile = trim(tofile);  
  32.   
  33.     local sh_mkdir = "mkdir -p " ..dirroot..todir;  
  34.     local sh_mv = "mv "..trim(params["temp_path"]).." "..dirroot..tofile;  
  35.   
  36.     params["file_path"] = tofile;  
  37.     if(os.execute(sh_mkdir) ~= 0) then  
  38.         ngx.exec("/50x.html");  
  39.     end  
  40.     if(os.execute(sh_mv) ~= 0) then  
  41.         ngx.exec("/50x.html");  
  42.     end  
  43. end  
  44.   
  45. function getFileSuffix(fname)  
  46.     local idx,idx_end = string.find(fname,"%.");  
  47.     return string.sub(fname,idx_end);  
  48. end  
  49.   
  50. function trim(str)  
  51.     if(str ~= nil) then  
  52.         return string.gsub(str, "%s+""");  
  53.     else  
  54.         return nil;  
  55.     end  
  56. end  
  57.   
  58. function urlencode(str)  
  59.     if (str) then  
  60.         str = string.gsub (str, "n""rn")  
  61.         str = string.gsub (str, "([^%w ])",  
  62.         function (c) return string.format ("%%%02X", string.byte(c)) end)  
  63.         str = string.gsub (str, " ""+")  
  64.     end  
  65.     return str  
  66. end  
  67. function urldecode(str)  
  68.   str = string.gsub (str, "+"" ")  
  69.   str = string.gsub (str, "%%(%x%x)",  
  70.       function(h) return string.char(tonumber(h,16)) end)  
  71.   str = string.gsub (str, "rn""n")  
  72.   return str  
  73. end  
  74.   
  75. --[[  
  76.  * 修复form提交后参数转发丢失问题。  
  77.  * 文件上传成功后,转发到另一个URL作后继处理。此时表单数据和文件信息丢失。原因不明,猜测可能是上传模块与lua模块冲突导致。  
  78.  * 转发过来的from内容lua收到后现为如下形式的table对象:  
  79. -----------------------------5837197829760   
  80. Content-Disposition: form-data; name"test_name"   
  81.    
  82. 交易.jpg   
  83. -----------------------------5837197829760   
  84. Content-Disposition: form-data; name="test_content_type"   
  85.    
  86. image/jpeg   
  87. -----------------------------5837197829760  
  88.  * 因此自行处理来分离出表单内容。  
  89.  * 使用分离字符串的方式。注意!!!字段名称中不能使用半角双引号。  
  90. ]]  
  91. function getFormParams_FixBug(post_args)  
  92.     local str_params;  
  93.     if (post_args) then  
  94.         for key,val in pairs(post_args) do  
  95.             str_params = key ..val;  
  96.         end  
  97.     else  
  98.         return nil;  
  99.     end  
  100.   
  101.     local tab_params = {};  
  102.     local str_start = " name";  
  103.     local str_start_len = string.len(str_start);  
  104.     local str_end = "%-%-%-%-%-%-%-%-%-%-%-%-%-%-%-%-%-%-%-%-%-%-%-%-%-%-%-%-%-";  
  105.     local str_sign = """;  
  106.     local idx,idx_end = string.find(str_params,str_start);  
  107.     local i = 0;  
  108.   
  109.     -- 如果字符串内仍有开始标记,则说明还有内容需要分离。继续分离到没内容为止。  
  110.     while idx do  
  111.         str_params = string.sub(str_params,idx_end); -- 截取开始标记后所有字符待用  
  112.         i = string.find(str_params,str_sign); -- 查找字段名开始处的引号索引  
  113.         str_params = string.sub(str_params,i+1); -- 去掉开始处的引号  
  114.         i = string.find(str_params,str_sign); -- 查找字段名结束位置的索引  
  115.         f_name = string.sub(str_params,0,i-1); -- 截取到字段名称  
  116.   
  117.         str_params = string.sub(str_params,i+1); -- 去掉名称字段以及结束时的引号  
  118.         i,i2 = string.find(str_params,str_end); -- 查找字段值结尾标识的索引  
  119.         f_value = string.sub(str_params,1,i-1); -- 截取到字段值  
  120.         tab_params[f_name] = f_value;  
  121.         idx = string.find(str_params,str_start,0); -- 查找判断下一个字段是否存在的  
  122.     end  
  123.     tab_params["callback"] = urldecode(trim(tab_params["callback"]));  
  124.       
  125.     return tab_params;  
  126. end  
  127.   
  128. onupload(); 
脚本与配置文件相互配合,根据用户上传时的提交的路径和当前用户,是否使用日期区分等参数,来创建子文件夹,将文件移动至存放目录。并使用文件的md5值来作为文件名,以防止重名。最后根据url参数里的callback参数来转发文件信息到后台的web程序。
至此文件上传功能就完成了,然后我们再来看一下文件缩放以及代理的安装和配置:
文件缩放模块的安装比较简单,不需要太多的依赖,安装过程也不会遇到太多问题。也不需要多说,安装前,在./configure时候添加 –with-http_image_filter_module就行了。
下面上一下配置
js 代码
  1. #user  nobody;  
  2. worker_processes  1;  
  3.  
  4. #error_log  logs/error.log;  
  5. #error_log  logs/error.log  notice;  
  6. #error_log  logs/error.log  info;  
  7.  
  8. #pid        logs/nginx.pid;  
  9.   
  10.   
  11. events {  
  12.     worker_connections  1024;  
  13. }  
  14.   
  15.   
  16. http {  
  17.     include       mime.types;  
  18.     default_type  application/octet-stream;  
  19.  
  20.     #log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '  
  21.     #                  '$status $body_bytes_sent "$http_referer" '  
  22.     #                  '"$http_user_agent" "$http_x_forwarded_for"';  
  23.  
  24.     #access_log  logs/access.log  main;  
  25.   
  26.     sendfile        on;  
  27.     #tcp_nopush     on;  
  28.  
  29.     #keepalive_timeout  0;  
  30.     keepalive_timeout  65;  
  31.  
  32.     #gzip  on;  
  33.   
  34.     server {  
  35.         listen       80;  
  36.         server_name  localhost;  
  37.  
  38.         #charset koi8-r;  
  39.  
  40.         #access_log  logs/host.access.log  main;  
  41.   
  42.         location / {  
  43.             root   html;  
  44.             index  index.html index.htm;  
  45.                       }  
  46.   
  47.       location /img {  
  48.         #  图片被代理过来以后实际存放的根目录  
  49.         alias /tmp/nginx/resize;  
  50.         set $width 9999;  
  51.         set $height 9999;  
  52.         set $dimens "";  
  53.  
  54.          # 请求中带有尺寸的,分离出尺寸,并拼出文件夹名称  
  55.         if ($uri ~* "^/img_(d+)x(d+)/(.*)" ) {  
  56.             set $width $1;  
  57.             set $height $2;  
  58.             set $image_path $3;  
  59.             set $demins "_$1x$2";  
  60.         }  
  61.         if ($uri ~* "^/img/(.*)" ) {  
  62.             set $image_path $1;  
  63.         }  
  64.  
  65.         # 本地没有找到图片时,调用获取图片并压缩图片的连接  
  66.         set $image_uri img_filter/$image_path?width=$width&height=$height;  
  67.           if (!-f $request_filename) {  
  68.             proxy_pass http://127.0.0.1:80/$image_uri;  
  69.             break;  
  70.         }  
  71.         proxy_store /tmp/nginx/resize$demins/$image_path;  
  72.         proxy_store_access user:rw group:rw all:rw;  
  73.         proxy_temp_path /tmp/images;  
  74.         proxy_set_header Host $host;  
  75.   
  76.     }  
  77.     # 此处为图片实际地址,可以为远程地址  
  78.    location /img_filter/ {  
  79. 版权声明:本文来源CSDN,感谢博主原创文章,遵循 CC 4.0 by-sa 版权协议,转载请附上原文出处链接和本声明。
    原文链接:https://blog.csdn.net/joeyon/article/details/46624079
    站方申明:本站部分内容来自社区用户分享,若涉及侵权,请联系站方删除。
    • 发表于 2020-04-19 16:59:51
    • 阅读 ( 1134 )
    • 分类:Go Web框架

0 条评论

请先 登录 后评论

官方社群

GO教程

猜你喜欢