转: 构建基于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代码
./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代码
#user nobody;
worker_processes 1;
#error_log logs/error.log;
#error_log logs/error.log notice;
#error_log logs/error.log info;
#pid logs/nginx.pid;
events {
worker_connections 1024;
}
http {
include mime.types;
default_type 'text/html';
sendfile on;
keepalive_timeout 65;
server {
listen 80;
server_name localhost;
lua_code_cache off;
set $callback_url "/";
location / {
root html;
index index.html index.htm;
}
location /upload{
client_max_body_size 35m; # 上传文件大小限制
upload_cleanup 500-505; # 发生这些错误删除文件 400 404 499 500-505
upload_store_access user:rw group:rw all:rw; # 访问权限
# upload_limit_rate 128k; # 上传速度限制
upload_pass_args on; # 允许上传参数传递到后台
if ($uri ~* "^/upload/(.*)") {
set $sub_path $1;
}
if ($uri !~* "^/upload/(.*)") {
set $sub_path "default";
}
if (-d $cookie_username) {
set $user_name $cookie_username;
}
if (!-d $cookie_username){
set $user_name "nobody";
}
upload_store /B2B/uploadfiles/temp; # /B2B/uploadfiles/用户/日期/文件类型/文件名 # 本地存储位置
upload_set_form_field "callback" $arg_callback;
upload_set_form_field "use_date" $arg_use_date;
upload_set_form_field "sub_path" $sub_path;
upload_set_form_field "user_name" $user_name;
upload_set_form_field "file_name" $upload_file_name;
upload_set_form_field "file_content_type" $upload_content_type;
upload_aggregate_form_field "file_md5" $upload_file_md5;
upload_aggregate_form_field "file_size" $upload_file_size;
upload_set_form_field "temp_path" $upload_tmp_path;
upload_pass_form_field ".*";
upload_pass /prossfile; # 转给文件处理(移动文件,转发请求)
}
# 处理文件:使用lua脚本处理文件,将文件移动并重命名到特定的文件夹。而后将文件信息转发给后台处理程序。
location /prossfile{
lua_need_request_body on;
content_by_lua_file /B2B/servers/nginx/luas/onupload.lua;
}
# 文件上传后台程序处理路径
include /B2B/servers/nginx/conf/upload_callback.conf;
# 文件访问路径
location /files/{
default_type 'application/octet-stream';
alias /B2B/uploadfiles/;
}
}
}
其中location /upload接收文件的上传放到的临时目录,并整理参数,而后转发给location /prossfile。location /prossfile将使用lua脚本来处理文件的移动并再次转发请求给后台的网站。
另外,include /B2B/servers/nginx/conf/upload_callback.conf;这一句引入了另一个配置文件,这里面配置的一些location是后台的web应用用来接收文件上传的url地址。如:
js代码
location /B2B {
proxy_pass http://192.168.3.32:8080/cookie.test/index.jsp;
}
location /example {
proxy_pass http://www.oecp.cn;
}
实现文件移动和转发请求的部分在lua里面,也是重点部分。在实现时遇到一些麻烦,可能是lua这个模块与上传模块的冲突(具体原因不详),文件上传模块将请求转发到lua后,文件信息和form里的内容居然成了无法识别的。正常情况下,应该可以转化为一种table对象,以key-value来存取,但现在确是一个只有一行的table,key和value都是很长的字符串,为此,只能暂时用分离字符串的方式将form的内容拆分出来,而后才能转发给后台处理的web应用。
下面是脚本luas/onupload.lua的具体内容:
cpp代码
function onupload()
ngx.req.read_body();
local post_args = ngx.req.get_post_args(); -- 读取参数
local tab_params = getFormParams_FixBug(post_args); -- 处理参数错误
pressFile(tab_params); -- 处理文件
-- ngx.log(ngx.ERR,"#############@" ,tab_params["callback"],"@###########");
if (tab_params["callback"] and tab_params["callback"] ~= "") then
ngx.exec(tab_params["callback"],tab_params); -- 转发请求
else
ngx.say("Callback not specified!!");
end
end
--[[
处理文件
主要进行 创建目录 & 移动文件 等操作。
]]
function pressFile(params)
local dirroot = "/B2B/uploadfiles/";
local todir = params["user_name"].."/";
if(params["sub_path"]) then
todir = params["sub_path"].."/"..todir;
end
if(trim(params["use_date"]) == "Y") then
todir = todir..os.date('%Y-%m-%d').."/"
end
todir = trim(todir);
local tofile = todir..params["file_md5"]..getFileSuffix(params["file_name"]);
tofile = trim(tofile);
local sh_mkdir = "mkdir -p " ..dirroot..todir;
local sh_mv = "mv "..trim(params["temp_path"]).." "..dirroot..tofile;
params["file_path"] = tofile;
if(os.execute(sh_mkdir) ~= 0) then
ngx.exec("/50x.html");
end
if(os.execute(sh_mv) ~= 0) then
ngx.exec("/50x.html");
end
end
function getFileSuffix(fname)
local idx,idx_end = string.find(fname,"%.");
return string.sub(fname,idx_end);
end
function trim(str)
if(str ~= nil) then
return string.gsub(str, "%s+", "");
else
return nil;
end
end
function urlencode(str)
if (str) then
str = string.gsub (str, "\n", "\r\n")
str = string.gsub (str, "([^%w ])",
function (c) return string.format ("%%%02X", string.byte(c)) end)
str = string.gsub (str, " ", "+")
end
return str
end
function urldecode(str)
str = string.gsub (str, "+", " ")
str = string.gsub (str, "%%(%x%x)",
function(h) return string.char(tonumber(h,16)) end)
str = string.gsub (str, "\r\n", "\n")
return str
end
--[[
* 修复form提交后参数转发丢失问题。
* 文件上传成功后,转发到另一个URL作后继处理。此时表单数据和文件信息丢失。原因不明,猜测可能是上传模块与lua模块冲突导致。
* 转发过来的from内容lua收到后现为如下形式的table对象:
-----------------------------5837197829760
Content-Disposition: form-data; name"test_name"
交易.jpg
-----------------------------5837197829760
Content-Disposition: form-data; name="test_content_type"
image/jpeg
-----------------------------5837197829760
* 因此自行处理来分离出表单内容。
* 使用分离字符串的方式。注意!!!字段名称中不能使用半角双引号。
]]
function getFormParams_FixBug(post_args)
local str_params;
if (post_args) then
for key,val in pairs(post_args) do
str_params = key ..val;
end
else
return nil;
end
local tab_params = {};
local str_start = " name";
local str_start_len = string.len(str_start);
local str_end = "%-%-%-%-%-%-%-%-%-%-%-%-%-%-%-%-%-%-%-%-%-%-%-%-%-%-%-%-%-";
local str_sign = "\"";
local idx,idx_end = string.find(str_params,str_start);
local i = 0;
-- 如果字符串内仍有开始标记,则说明还有内容需要分离。继续分离到没内容为止。
while idx do
str_params = string.sub(str_params,idx_end); -- 截取开始标记后所有字符待用
i = string.find(str_params,str_sign); -- 查找字段名开始处的引号索引
str_params = string.sub(str_params,i+1); -- 去掉开始处的引号
i = string.find(str_params,str_sign); -- 查找字段名结束位置的索引
f_name = string.sub(str_params,0,i-1); -- 截取到字段名称
str_params = string.sub(str_params,i+1); -- 去掉名称字段以及结束时的引号
i,i2 = string.find(str_params,str_end); -- 查找字段值结尾标识的索引
f_value = string.sub(str_params,1,i-1); -- 截取到字段值
tab_params[f_name] = f_value;
idx = string.find(str_params,str_start,0); -- 查找判断下一个字段是否存在的
end
tab_params["callback"] = urldecode(trim(tab_params["callback"]));
return tab_params;
end
onupload();
脚本与配置文件相互配合,根据用户上传时的提交的路径和当前用户,是否使用日期区分等参数,来创建子文件夹,将文件移动至存放目录。并使用文件的md5值来作为文件名,以防止重名。最后根据url参数里的callback参数来转发文件信息到后台的web程序。
至此文件上传功能就完成了,然后我们再来看一下文件缩放以及代理的安装和配置:
文件缩放模块的安装比较简单,不需要太多的依赖,安装过程也不会遇到太多问题。也不需要多说,安装前,在./configure时候添加 –with-http_image_filter_module就行了。
下面上一下配置
js代码
#user nobody;
worker_processes 1;
#error_log logs/error.log;
#error_log logs/error.log notice;
#error_log logs/error.log info;
#pid logs/nginx.pid;
events {
worker_connections 1024;
}
http {
include mime.types;
default_type application/octet-stream;
#log_format main '$remote_addr - $remote_user [$time_local] "$request" '
# '$status $body_bytes_sent "$http_referer" '
# '"$http_user_agent" "$http_x_forwarded_for"';
#access_log logs/access.log main;
sendfile on;
#tcp_nopush on;
#keepalive_timeout 0;
keepalive_timeout 65;
#gzip on;
server {
listen 80;
server_name localhost;
#charset koi8-r;
#access_log logs/host.access.log main;
location / {
root html;
index index.html index.htm;
}
location /img {
# 图片被代理过来以后实际存放的根目录
alias /tmp/nginx/resize;
set $width 9999;
set $height 9999;
set $dimens "";
# 请求中带有尺寸的,分离出尺寸,并拼出文件夹名称
if ($uri ~* "^/img_(\d+)x(\d+)/(.*)" ) {
set $width $1;
set $height $2;
set $image_path $3;
set $demins "_$1x$2";
}
if ($uri ~* "^/img/(.*)" ) {
set $image_path $1;
}
# 本地没有找到图片时,调用获取图片并压缩图片的连接
set $image_uri img_filter/$image_path?width=$width&height=$height;
if (!-f $request_filename) {
proxy_pass http://127.0.0.1:80/$image_uri;
break;
}
proxy_store /tmp/nginx/resize$demins/$image_path;
proxy_store_access user:rw group:rw all:rw;
proxy_temp_path /tmp/images;
proxy_set_header Host $host;
}
# 此处为图片实际地址,可以为远程地址
location /img_filter/ {
image_filter_buffer 20M;
proxy_pass http://192.168.3.239/files/;
image_filter resize $arg_width $arg_height;
image_filter_jpeg_quality 75;
allow 127.0.0.0/8;
deny all;
}
}
}
上面配置中,各个部分比较容易读懂,而且写了注释,不再过多解释。
服务配置好以后我们能够用它做什么,怎么用呢?
当我们需要上传文件的时候,页面将文件post到上传服务器如:http://192.168.3.239/upload/test?use_date=Y&callback=/example
服务器将会这样存放文件 test/username/日期/md5.扩展名
,其中test是来自于url中/upload/后面的部分一直到问号之前,url路径中除了/upload/是固定的,后面都是可以自己定义的,用来指定文件夹结构;username是来自于cookie里的username属性,如果没有的话,默认为nobody;日期是可选的,根据url后的参数use_date是否等于Y来决定是否区分日期子文件夹;callback参数是用来指定上传完成后,用来后继处理的web程序的url,但是这个url不能直接写连接,只能使用配置在conf/upload_callback.conf里面的location,这么做有连个原因,直接写路径提交给Nginx后,将会被URLEncode,nginx跳转时不能使用,另外也可以防止非法的使用者自定义跳转到其他地址。上传成功后转发给后台的参数如下:
参数名 含义
callback 上传完成后跳转到的路径
use_date 是否需要日期做子文件夹
sub_path 指定的子文件夹
user_name 上传的用户名
file_name 原始文件名
file_path 文件在服务器存放的相对路径(包括文件名)
file_content_type 文件类型
file_md5 文件的md5
file_size 文件的大小
temp_path 文件上传到Nginx后的临时目录名
图片上传完成后,访问”http://192.168.3.240/img/ test/username/日期/md5”.扩展名就可以打开图片了,如果需要缩放图片可以访问http://192.168.3.240/img_长x宽/test//username/日期/md5来获得缩放后的图片。图片的访问和缩放都经过图片缩放主机的代理做缓存处理,只有第一次访问的时候才会请求图片上传服务器,以此来获得镜像加速,使集群成为可能。
此帖子为转载 原地址:http://www.oecp.cn/hi/slx/blog/1168734