转: 构建基于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
转: 构建基于Nginx的文件服务器思路与实现的更多相关文章
- 基于nginx tomcat redis分布式web应用的session共享配置
一.前言 nginx 作为目前最流行的开源反向代理HTTP Server,用于实现资源缓存.web server负载均衡等功能,由于其轻量级.高性能.高可靠等特点在互联网项目中有着非常普遍的应用,相关 ...
- 【转载】【JAVA秒会技术之图片上传】基于Nginx及FastDFS,完成图片的上传及展示
基于Nginx及FastDFS,完成商品图片的上传及展示 一.传统图片存储及展示方式 存在问题: 1)大并发量上传访问图片时,需要对web应用做负载均衡,但是会存在图片共享问题 2)web应用服务器的 ...
- windows环境下基于nginx搭建rtmp服务器
基于nginx搭建rtmp服务器需要引入rtmp模块,引入之后需重新编译nginx linux环境几个命令行就能实现编译,笔者未尝试,网上有很多教程. windows环境还需要安装一系列的编译环境,例 ...
- .net core 跨平台开发 微服务架构 基于Nginx反向代理 服务集群负载均衡
1.概述 反向代理(Reverse Proxy)方式是指以代理服务器来接受internet上的连接请求,然后将请求转发给内部网络上的服务器,并将从服务器上得到的结果返回给internet上请求连接的客 ...
- 使用gitlab构建基于docker的持续集成(三)
使用gitlab构建基于docker的持续集成(三) gitlab docker aspnetcore 持续集成 构建发布思路: aspnetcore 下的dockerfile编写 发布docker- ...
- 基于Nginx dyups模块的站点动态上下线并实现简单服务治理
简介 今天主要讨论一下,对于分布式服务,站点如何平滑的上下线问题. 分布式服务 在分布式服务下,我们会用nginx做负载均衡, 业务站点访问某服务站点的时候, 统一走nginx, 然后nginx根据一 ...
- css013 构建基于浮动的布局
css013 构建基于浮动的布局 基于浮动的布局时利用float属性是网页上的元素并排,并创建列 float有三个值:left .right .none 1.假设要把一张图片浮动到网页的左侧 .flo ...
- 构建基于WCF Restful Service的服务
前言 传统的Asmx服务,由于遵循SOAP协议,所以返回内容以xml方式组织.并且客户端需要添加服务端引用才能使用(虽然看到网络上已经提供了这方面的Dynamic Proxy,但是没有这种方式简便), ...
- 基于nginx的tomcat负载均衡和集群
要集群tomcat主要是解决SESSION共享的问题,因此我利用memcached来保存session,多台TOMCAT服务器即可共享SESSION了. 你可以自己写tomcat的扩展来保存SESSI ...
随机推荐
- RAID(冗余硬盘阵列)
一. RAID定义 RAID( Redundant Array of Independent Disk 独立冗余磁盘阵列 )技术是加州大学伯克利分校1987年提出,最初是为了组合小的廉价磁盘来代替大的 ...
- 【APUE】Chapter15 Interprocess Communication
15.1 Introduction 这部分太多概念我不了解.只看懂了最后一段,进程间通信(IPC)内容被组织成了三个部分: (1)classical IPC : pipes, FIFOs, messa ...
- 【廖雪峰老师python教程】——模块
使用模块 任何模块代码的第一个字符串都被视为模块的文档注释: 使用__author__变量把作者写进去,这样当你公开源代码后别人就可以瞻仰你的大名: 当我们在命令行运行模块文件时,Python解释器把 ...
- MySQL训练营01
一.数据库基础知识: 1. 数据库(database):保存有组织的数据的容器(通常是一个或者一组文件) 2. 数据库管理系统(DBMS):数据库软件,外界通过DBMS来创建和操纵数据库,具体是什么, ...
- highcharts图表插件初探
转载请注明出处:http://www.cnblogs.com/liubei/p/highchartsOption.html HighCharts简介 Highcharts 是一个用纯JavaScrip ...
- sysctl -P 报错解决办法 error: "net.bridge.bridge-nf-call-ip6tables" is an unknown key
error: "net.bridge.bridge-nf-call-ip6tables" is an unknown keyerror: "net.bridge.brid ...
- MySQL 初识01
最近开始学习MySQL 所以将这两天所学习到的知识简单小结一下 1.status 显示数据库信息 2.数据类型: a.字符串: char(m):固定长度的字符,最多255个字符: varchar(m) ...
- select2赋值需要注意
$('#mySelect2').val(data.id).trigger('change'); 需要在赋值后,调用下change事件,不然的话展示值的span不会显示select最新的选中值.
- js定时器实现图片轮播
效果展示如下: setInterval(moverleft,3000);定时器设置为3秒,而且实现图片下方的小圆点序号跟图片对应,点击小圆点也能切换图片. 代码如下: <!DOCTYPE htm ...
- PAT 1086 就不告诉你
https://pintia.cn/problem-sets/994805260223102976/problems/1038429065476579328 做作业的时候,邻座的小盆友问你:“五乘以七 ...