社区微信群开通啦,扫一扫抢先加入社区官方微信群
社区微信群
构建基于Nginx的文件服务器思路与实现 - Go语言中文社区
Toggle navigation
文章
(current)
Go面试题
热
Go导航
Go教程
官方文档
中文文档
标准库文档
Golang入门指南
Go话题
登录
注册
构建基于Nginx的文件服务器思路与实现
gin
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"
,
"rn"
)
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,
"rn"
,
"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/ {
版权声明:本文来源CSDN,感谢博主原创文章,遵循 CC 4.0 by-sa 版权协议,转载请附上原文出处链接和本声明。
原文链接:https://blog.csdn.net/joeyon/article/details/46624079
站方申明:本站部分内容来自社区用户分享,若涉及侵权,请联系站方删除。
发表于 2020-04-19 16:59:51
阅读 ( 1134 )
分类:
Go Web框架
0 推荐
收藏
你可能感兴趣的文章
基于nginx的PHP本地测试环境构建软件pinyshop发布
1273 浏览
margin:0pxauto居中问题解决方案
1093 浏览
CSS中使用margin属性定义网页边距
893 浏览
IE6下margin双倍边距Bug处理办法
1094 浏览
Nginx TSL/SSL优化握手性能
1138 浏览
Nginx TSL/SSL优化握手性能
1138 浏览
nginx的request_time 包含ssl 握手时间吗?
1487 浏览
nginx的request_time 包含ssl 握手时间吗?
1487 浏览
Google App Engine免费配额降低公告
1118 浏览
精选的优质文章
也许 Go 开发可以更简单!
10585 浏览
如何使用 Golang 日志监控你的应用程序?
12057 浏览
从Go语言实现模板设计模式浅谈Go的抽象能力
14107 浏览
阿里云基于 Go 的微服务架构分享
23971 浏览
java是否会被取代?Go会否给Java带来冲击?
28494 浏览
千万级规模高性能、高并发的网络架构经验分享
30050 浏览
阿里部分面试题汇总,对想进阿里的同学非常实用
62347 浏览
实用好文:知乎实时数仓架构实践及演进
31368 浏览
支撑马蜂窝「双11」营销大战背后的技术架构
228310 浏览
想进大厂?50个多线程面试题,你会多少?(一)
23105 浏览
0 条评论
请先
登录
后评论
官方社群
关注公众号
—— 加入社区微信群 ——
→
「Go语言教程」
领取
GO教程
1. Go语言简介
1.1 Go语言简介
1.2 Go语言的特性
1.3 Go语言为并发而生
1.4 哪些项目使用Go语言开发?
1.5 哪些大公司正在使用Go语言
1.6 Go语言的性能如何?
1.7 Go语言标准库强大
1.8 Go语言上手简单
1.9 Go语言代码风格清晰、简单
1.10 Go语言工程结构详述
1.11 第一个Go语言程序
1.12 Go语言历史版本
2. Go语言基本语法
2.1 Go语言变量的声明
2.2 Go语言变量的初始化
2.3 Go语言多个变量同时赋值
2.4 Go语言匿名变量
2.5 Go语言变量的作用域
2.6 Go语言整型(整数类型)
2.7 Go语言浮点类型(小数类型)
2.8 Go语言复数
2.9 Go语言bool类型(布尔类型)
2.10 Go语言字符串
2.11 Go语言字符类型(byte和rune)
2.12 Go语言数据类型转换
2.13 Go语言指针
2.14 Go语言变量的生命周期
2.15 Go语言常量
2.16 Go语言类型别名
2.17 Go语言关键字与标识符
2.18 Go语言运算符的优先级
3. Go语言数据结构
3.1 Go语言数组
3.2 Go语言多维数组
3.3 Go语言切片
3.4 使用append()为切片添加元素
3.5 Go语言切片复制
3.6 Go语言从切片中删除元素
3.7 Go语言range关键字
3.8 Go语言多维切片
3.9 Go语言map(映射)
3.10 Go语言遍历map
3.11 map元素的删除和清空
3.12 Go语言sync.Map
3.13 Go语言list(列表)
3.14 Go语言nil:空值/零值
4. Go语言流程控制
4.1 Go语言分支结构
4.2 Go语言循环结构
4.4 Go语言键值循环
4.5 Go语言switch语句
4.6 Go语言goto语句
4.7 Go语言break(跳出循环)
4.8 Go语言continue
5. Go语言函数
5.1 Go语言函数声明
5.2 Go语言函数变量
5.3 Go语言匿名函数
5.4 Go语言函数类型实现接口
5.5 Go语言闭包(Closure)
5.6 Go语言可变参数
5.7 Go语言defer(延迟执行语句)
5.8 Go语言递归函数
5.9 Go语言处理运行时错误
5.10 Go语言宕机(panic)
5.11 Go语言宕机恢复(recover)
5.12 Go语言计算函数执行时间
5.13 Go语言Test功能测试函数
6. Go语言结构体
6.1 Go语言结构体定义
6.2 Go语言实例化结构体
6.3 初始化结构体的成员变量
6.4 Go语言构造函数
6.5 类型内嵌和结构体内嵌
6.6 初始化内嵌结构体
6.7 内嵌结构体成员名字冲突
6.8 Go语言垃圾回收和SetFinalizer
6.9 Go语言链表操作
6.10 Go语言数据I/O对象及操作
7. Go语言接口
7.1 Go语言接口声明(定义)
7.2 Go语言实现接口的条件
7.3 Go语言类型与接口的关系
7.4 Go语言类型断言
7.5 Go语言排序
7.6 Go语言接口的嵌套组合
7.9 Go语言接口和类型之间的转换
7.10 Go语言空接口类型
7.11 Go语言类型分支
7.12 Go语言error接口
8. Go语言包
8.1 包的基本概念
8.2 Go语言封装简介及实现细节
8.3 Go语言GOPATH
8.4 Go语言常用内置包
8.5 Go语言自定义包
8.6 Go语言package
8.7 Go语言导出包中的标识符
8.8 Go语言import导入包
8.9 Go语言sync包与锁
8.10 Go语言big包
8.11 Go语言正则表达式:regexp包
8.12 Go语言time包:时间和日期
8.13 Go语言os包用法简述
8.14 Go语言flag包:命令行参数解析
8.15 Go语言go mod包依赖管理工具
8.16 Go语言runtime包:运行时
9. Go语言并发
9.1 Go语言并发简述
9.2 Go语言轻量级线程
9.3 Go语言并发通信
9.4 Go语言竞争状态
9.5 Go语言调整并发的运行性能
9.6 并发和并行的区别
9.7 goroutine和coroutine的区别
9.8 Go语言通道(chan)
9.9 示例:并发打印
9.10 Go语言单向通道
9.11 Go语言无缓冲的通道
9.12 Go语言带缓冲的通道
9.13 Go语言channel超时机制
9.14 Go语言多核并行化
9.15 互斥锁和读写互斥锁
9.16 Go语言等待组
9.17 死锁、活锁和饥饿概述
9.18 Go语言CSP:通信顺序进程简述
9.19 示例:聊天服务器
10. Go语言反射
10.1 Go语言反射(reflection)
10.2 Go语言反射规则浅析
10.3 通过反射获取类型信息
10.4 通过反射获取指针指向的元素类型
10.5 通过反射获取结构体的成员类型
10.6 Go语言结构体标签
10.7 通过反射获取值信息
10.8 通过反射访问结构体成员的值
10.9 判断反射值的空和有效性
10.10 通过反射修改变量的值
10.11 通过类型信息创建实例
10.12 通过反射调用函数
10.13 Go语言inject库:依赖注入
11. Go语言文件处理
11.1 Go语言自定义数据文件
11.2 Go语言JSON文件的读写操作
11.3 Go语言XML文件的读写操作
11.4 Go语言使用Gob传输数据
11.5 Go语言纯文本文件的读写操作
11.6 Go语言二进制文件的读写操作
11.7 Go语言自定义二进制文件的读写操作
11.8 Go语言zip归档文件的读写操作
11.9 Go语言tar归档文件的读写操作
11.10 Go语言使用buffer读取文件
11.11 Go语言文件的写入、追加、读取、复制操作
11.12 Go语言文件锁操作
12. Go语言编译和工具链
12.1 go build命令
12.2 go clean命令
12.3 go run命令
12.4 go fmt命令
12.5 go install命令
12.6 go get命令
12.7 go generate命令
12.8 go test命令
12.9 go pprof命令
13. Go语言进阶
13.1 Go语言的深拷贝和浅拷贝
13.2 Go语言引用传递和值传递
13.3 Go语言的Socket编程
14. 常见面试题
14.1 Golang Map底层实现
14.2 go语言触发异常的场景有哪些
14.3 Printf()、Sprintf()、Fprintf()函数的区别用法是什么
14.4 详细说说new和make的区别
14.5 详细说说切片和数组的区别
14.6 Golang的内存模型,为什么小对象多了会造成gc压力
14.7 Data Race问题怎么解决?能不能不加锁解决这个问题
14.8 在 range 迭代 slice 时,你怎么修改值的
14.9 select可以用于什么
14.10 go语言编程的好处是什么
14.11 你是否主动关闭过http连接,为啥要这样做
14.12 recover的执行时机
14.13 说出一个避免Goroutine泄露的措施
14.14 如何跳出for select 循环
14.15 如何初始化带嵌套结构的结构体
14.16 Printf()、Sprintf()、Fprintf()函数的区别用法是什么
14.17 go语言中的引用类型包含哪些
14.18 说说go语言的select机制
推荐文章
强大!Nginx配置在线一键生成“神器”
强大!Nginx配置在线一键生成“神器”
如何在nginx中缓存静态文件
如何在nginx中缓存静态文件
nginx配置http 改为 https(Windows10版)
nginx配置http 改为 https(Windows10版)
揭开ADO.NET TracinginSyncServices使用之谜
C#线程:BeginInvoke和EndInvoke方法
mysql plugin 调用_MySQL插件调用
【博文推荐 】 python Howto之logging模块
猜你喜欢
随便看看
beego的路由机制
beego如何获取前一个页面URL
Beego Models之二
Beego如何在orm模型中给字段添加注释
8-Beego优选显示地址
beego config模块源码分析笔记二
基于Golang的单页博客-iNote
用 Go 写一个轻量级的 ldap 测试工具
9.Beego优选删除地址
beego创建项目流程
×
发送私信
发给:
内容:
×
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!