前面我们完成了一个网页端的上传oss程序:https://blog.csdn.net/daily886/article/details/103366145
现在我们把前后端分离
前端使用walk开发,window界面 ,客户端操作流程:https://blog.csdn.net/daily886/article/details/103432917
后台服务器使用go接收图片并上传到oss
服务器端目录结构如下:
项目根目录是 goossserver
goossserver的目录结构
├── conf # 配置文件统一存放目录
│ ├── config.yaml # 配置文件
├── config # 专门用来处理配置和配置文件的Go package
│ └── config.go
├── handler # 控制输出到浏览器
│ ├── handler.go
├── logs # 日志记录
├── model # 操作模型
│ ├── aliyunoss.go # 阿里云oss操作模型
├── router # 路由相关处理
│ ├── middleware # API服务器用的是Gin Web框架,Gin中间件存放位置
│ │ ├── header.go # Gin中间件
│ └── router.go # 路由
├── service # 实际业务处理函数存放位置
│ └── hotupdate.go # 热重启服务
│ └── service.go # 业务处理函数
├── uploads # 上传文件本地存储目录
├── main.go # Go程序唯一入口
下面,我们根据目录结构,从上往下建立文件夹和文件
建立文件夹和文件 goossserver/conf/config.yaml ,config.yaml 内容如下:
common:
#https://help.aliyun.com/document_detail/87712.html?spm=a2c4g.11186623.6.882.36c55837XaJBzg 阿里云oss接口
aliyunoss:
accessid: XXX # 阿里云的accessid
accesskey: XXX # 阿里云的accesskey
endpoint: XXX # 阿里云的endpoint
bucket: XXX # 阿里云的 bucket名称
uploaddir: test # 所有上传文件都存在这个目录下面
domain: http://XXX.com # 文件访问域名
server: #服务器配置
runmode: debug # 开发模式, debug, release, test
addr: :6669 # HTTP绑定端口
name: ossserver # API Server的名字
url: :6669 # pingServer函数请求的API服务器的ip:port
max_ping_count: 10 # pingServer函数尝试的次数
建立文件夹和文件 goossserver/config/config.go ,config.yaml 内容如下:
package config
import (
"github.com/spf13/viper"
"time"
"os"
"log"
)
// LogInfo 初始化日志配置
func LogInfo() {
file := "./logs/" + time.Now().Format("2006-01-02") + ".log"
logFile, _ := os.OpenFile(file,os.O_RDWR| os.O_CREATE| os.O_APPEND, 0755)
log.SetFlags(log.Ldate | log.Ltime | log.Lshortfile)
log.SetOutput(logFile)
}
// Init 读取初始化配置文件
func Init() error {
//初始化配置
if err := Config();err != nil{
return err
}
//初始化日志
LogInfo()
return nil
}
// Config viper解析配置文件
func Config() error{
viper.AddConfigPath("conf")
viper.SetConfigName("config")
if err := viper.ReadInConfig();err != nil{
return err
}
return nil
}
建立文件夹和文件 goossserver/handler/handler.go ,handler.go 内容如下:
package handler
import (
"net/http"
"github.com/gin-gonic/gin"
)
type Response struct {
Code int `json:"code"`
Message string `json:"message"`
Data interface{} `json:"data"`
}
//返回json 格式
/*
param c *gin.Context gin上下文 必传
param code int 代号 必传
param message string 提示语 必传
param data ...interface{} 返回数据 选填
*/
func SendResponse(c *gin.Context,code int,message string,data ...interface{}){
if data == nil{
data = make([]interface{},0)
}
//总是返回http状态ok
c.JSON(http.StatusOK,Response{
Code: code,
Message:message,
Data: data,
})
}
//返回成功
func SendSuccess(c *gin.Context,message string,data ...interface{}){
SendResponse(c,1,message,data)
}
//返回提示
func SendTips(c *gin.Context,message string,data ...interface{}){
SendResponse(c,0,message,data)
}
//返回失败
func SendFailure(c *gin.Context,message string,data ...interface{}){
SendResponse(c,-1,message,data)
}
建立文件夹 goossserver/logs 权限为777
建立文件夹和文件 goossserver/model/aliyunoss.go ,aliyunoss.go 内容如下:
package model
import (
"fmt"
"log"
"strings"
"github.com/spf13/viper"
"github.com/aliyun/aliyun-oss-go-sdk/oss"
)
type OssStruct struct {
Url string `json:"url"`
}
//sdk 版本
func Aliossversion()(version string){
return oss.Version
}
//初始化oss服务
func Initserver()(client *oss.Client,err error){
// Endpoint以杭州为例,其它Region请按实际情况填写。
endpoint := viper.GetString("common.aliyunoss.endpoint")
// 阿里云主账号AccessKey拥有所有API的访问权限,风险很高。强烈建议您创建并使用RAM账号进行API访问或日常运维,请登录 https://ram.console.aliyun.com 创建RAM账号。
accessKeyId := viper.GetString("common.aliyunoss.accessid")
accessKeySecret := viper.GetString("common.aliyunoss.accesskey")
// 创建OSSClient实例。
client, err = oss.New(endpoint, accessKeyId, accessKeySecret)
if err != nil{
return
}
return
}
//获取文件列表
func GetFilelist()(list []string,err error){
list = make([]string,100)
client,err := Initserver()
// 获取存储空间。
bucketName := viper.GetString("common.aliyunoss.bucket")
bucket, err := client.Bucket(bucketName)
if err != nil {
return list,err
}
// 列举文件。
marker := ""
for {
lsRes, err := bucket.ListObjects(oss.Marker(marker))
if err != nil {
return list,err
}
// 打印列举文件,默认情况下一次返回100条记录。
for _, object := range lsRes.Objects {
log.Printf("object.Key:%v\n",object.Key)
list = append(list,object.Key)
}
if lsRes.IsTruncated {
marker = lsRes.NextMarker
} else {
break
}
}
return list,err
}
//上传文件
func UploadFile(localfile string,uploadfile string)(resultfile string,err error){
resultfile = ""
// 创建OSSClient实例。
client,err := Initserver()
bucketName := viper.GetString("common.aliyunoss.bucket")
// <yourObjectName>上传文件到OSS时需要指定包含文件后缀在内的完整路径,例如abc/efg/123.jpg。
uploaddir := viper.GetString("common.aliyunoss.uploaddir")
uploadfile = strings.Trim(uploadfile,"/")
objectName := fmt.Sprintf("%s/%s",uploaddir,uploadfile) //完整的oss路径
// <yourLocalFileName>由本地文件路径加文件名包括后缀组成,例如/users/local/myfile.txt。
localFileName := localfile
// 获取存储空间。
bucket, err := client.Bucket(bucketName)
if err != nil {
return
}
// 上传文件。
err = bucket.PutObjectFromFile(objectName, localFileName)
if err != nil {
return
}
resultfile = objectName
return
}
建立文件夹和文件 goossserver/router/middleware/header.go ,header.go 内容如下:
package middleware
import (
"net/http"
"time"
"github.com/gin-gonic/gin"
)
//无缓存头部中间件 ,
//要来防止客户端获取已经缓存的响应信息
func NoCache(c *gin.Context){
c.Header("Cache-Control", "no-cache, no-store, max-age=0, must-revalidate, value")
c.Header("Expires", "Thu, 01 Jan 1970 00:00:00 GMT")
c.Header("Last-Modified", time.Now().UTC().Format(http.TimeFormat))
c.Next()
}
//选项中间件
//要来给预请求 终止并退出中间件 ,链接并结束请求
func Options(c *gin.Context){
if c.Request.Method != "OPTIONS"{
c.Next()
}else{
c.Header("Access-Control-Allow-Origin","*")
c.Header("Access-Control-Allow-Methods", "GET,POST,PUT,PATCH,DELETE,OPTIONS")
c.Header("Access-Control-Allow-Headers", "authorization, origin, content-type, accept")
c.Header("Allow", "HEAD,GET,POST,PUT,PATCH,DELETE,OPTIONS")
c.Header("Content-Type", "application/json")
c.AbortWithStatus(200)
}
}
//安全中间件
//要来保障数据安全的头部
func Secure(c *gin.Context){
c.Header("Access-Control-Allow-Origin", "*")
c.Header("X-Frame-Options", "DENY")
c.Header("X-Content-Type-Options", "nosniff")
c.Header("X-XSS-Protection", "1; mode=block")
if c.Request.TLS != nil {
c.Header("Strict-Transport-Security", "max-age=31536000")
}
//也可以考虑添加一个安全代理的头部
//c.Header("Content-Security-Policy", "script-src 'self' https://cdnjs.cloudflare.com")
}
建立文件夹和文件 goossserver/router/router.go ,router.go 内容如下:
package router
import (
"net/http"
"github.com/gin-gonic/gin"
"goossserver/service"
"goossserver/router/middleware"
)
//初始化路由
func InitRouter(g *gin.Engine){
middlewares := []gin.HandlerFunc{}
//中间件
g.Use(gin.Recovery())
g.Use(middleware.NoCache)
g.Use(middleware.Options)
g.Use(middleware.Secure)
g.Use(middlewares...)
//404处理
g.NoRoute(func(c *gin.Context){
c.String(http.StatusNotFound,"该路径不存在")
})
//健康检查中间件
g.POST("/ossupload",service.OssUpload)//上传oss
}
建立文件夹和文件 goossserver/service/hotupdate.go ,hotupdate.go 内容如下:
package service
import (
"context"
"errors"
"flag"
"log"
"net"
"net/http"
"os"
"os/exec"
"os/signal"
"syscall"
"time"
)
/************************** 热重启 ***************************/
var (
listener net.Listener = nil
graceful = flag.Bool("graceful", false, "listen on fd open 3 (internal use only)")
)
//监听服务器
func Listenserver(server *http.Server){
var err error
//解析参数
flag.Parse()
//设置监听的对象(新建或已存在的socket描述符)
if *graceful {
//子进程监听父进程传递的 socket描述符
log.Println("listening on the existing file descriptor 3")
//子进程的 0 1 2 是预留给 标准输入 标准输出 错误输出
//因此传递的socket 描述符应该放在子进程的 3
f := os.NewFile(3,"")
listener,err = net.FileListener(f)
log.Printf( "graceful-reborn %v %v %#v \n", f.Fd(), f.Name(), listener)
}else{
//启动守护进程
daemonProcce(1,1);
//父进程监听新建的 socket 描述符
log.Println("listening on a new file descriptor")
listener,err = net.Listen("tcp",server.Addr)
log.Printf("Actual pid is %d\n", syscall.Getpid())
}
if err != nil{
log.Fatalf("listener error: %v\n",err)
}
go func(){
err = server.Serve(listener) //http
//err = server.ServeTLS(listener,pemPath,keyPath) //https
log.Printf("server.Serve err: %v\n",err)
tcp,_ := listener.(*net.TCPListener)
fd,_ := tcp.File()
log.Printf( "first-boot %v %v %#v \n ", fd.Fd(),fd.Name(), listener)
}()
//监听信号
handleSignal(server)
log.Println("signal end")
}
//处理信号
func handleSignal(server *http.Server){
//把信号 赋值给 通道
ch := make(chan os.Signal, 1)
//监听信号
signal.Notify(ch, syscall.SIGINT,syscall.SIGTERM,syscall.SIGUSR2)
//阻塞主进程, 不停的监听系统信号
for{
//通道 赋值给 sig
sig := <-ch
log.Printf("signal receive: %v\n", sig)
ctx,_ := context.WithTimeout(context.Background(),20*time.Second)
switch sig{
case syscall.SIGINT,syscall.SIGTERM: //终止进程执行
log.Println("shutdown")
signal.Stop(ch) //停止通道
server.Shutdown(ctx) //关闭服务器窗口
log.Println("graceful shutdown")
return
case syscall.SIGUSR2: //进程热重启
log.Println("reload")
err := reload() //执行热重启
if err != nil{
log.Fatalf("listener error: %v\n",err)
}
//server.Shutdown(ctx)
log.Println("graceful reload")
return
}
}
}
//热重启
func reload() error{
tl, ok := listener.(*net.TCPListener)
if !ok {
return errors.New("listener is not tcp listener")
}
//获取socket描述符
currentFD, err := tl.File()
if err != nil {
return err
}
//设置传递给子进程的参数(包含 socket描述符)
args := []string{"-graceful"}
//args = append(args, "-continue")
cmd := exec.Command(os.Args[0],args...)
cmd.Stdout = os.Stdout //标准输出
cmd.Stderr = os.Stderr //错误输出
cmd.ExtraFiles = []*os.File{currentFD} //文件描述符
err = cmd.Start()
log.Printf("forked new pid %v: \n",cmd.Process.Pid)
if err != nil{
return err
}
return nil
}
/*
我们在父进程执行 cmd.ExtraFiles = []*os.File{f} 来传递 socket 描述符给子进程,子进程通过执行 f := os.NewFile(3, "") 来获取该描述符。值得注意的是,子进程的 0 、1 和 2 分别预留给标准输入、标准输出和错误输出,所以父进程传递的 socket 描述符在子进程的顺序是从 3 开始。
*/
//nochdir 是 程序初始路径 1是当前路径,0是系统根目录
//noclose 是 错误信息输出 1是输出当前, 0是不显示错误信息
func daemonProcce(nochdir, noclose int) (int,error){
// already a daemon
log.Printf("syscall.Getppid() %+v\n",syscall.Getppid())
//如果是守护进程 syscall.Getppid() = 1
if syscall.Getppid() == 1 {
/* Change the file mode mask */
syscall.Umask(0)
if nochdir == 0 {
os.Chdir("/")
}
return 0, nil
}
files := make([]*os.File, 3, 6)
if noclose == 0 {
nullDev, err := os.OpenFile("/dev/null", 0, 0)
if err != nil {
return 1, err
}
files[0], files[1], files[2] = nullDev, nullDev, nullDev
} else {
files[0], files[1], files[2] = os.Stdin, os.Stdout, os.Stderr
}
dir, _ := os.Getwd()
sysattrs := syscall.SysProcAttr{Setsid: true}
attrs := os.ProcAttr{Dir: dir, Env: os.Environ(), Files: files, Sys: &sysattrs}
proc, err := os.StartProcess(os.Args[0], os.Args, &attrs)
if err != nil {
return -1, err
}
proc.Release()
os.Exit(0)
return 0, nil
}
建立文件夹和文件 goossserver/service/service.go ,service.go 内容如下:
package service
import (
"log"
"fmt"
"time"
"github.com/gin-gonic/gin"
"github.com/spf13/viper"
. "goossserver/handler"
"goossserver/model"
)
func OssUpload(c *gin.Context){
//读取cookie
cookie, _ := c.Cookie("go-cookie")
log.Printf("go-cookie: ",cookie)
//防止多次重复提交表单
//解决方案是在表单中添加一个带有唯一值的隐藏字段。
// 在验证表单时,先检查带有该唯一值的表单是否已经递交过了。
// 如果是,拒绝再次递交;如果不是,则处理表单进行逻辑处理。
res1 := verifyToken(c)
if !res1 {
return
}
//上传文件
header, err := c.FormFile("files")
if err != nil {
//ignore
SendFailure(c,"上传失败")
return
}
localfile := "./uploads/"+header.Filename //本地文件路径
// gin 简单做了封装,拷贝了文件流
if err := c.SaveUploadedFile(header, localfile); err != nil {
log.Printf("SaveUploadedFile err: ",err)
// ignore
SendFailure(c,"本地上传失败")
return
}
log.Printf("本地上传成功")
//上传到阿里云oss
dateyear := time.Now().Format("2006") //获取当前年
datemonth := time.Now().Format("01")//获取当前月
dateday := time.Now().Format("02")//获取当前日
yunfiletmp := fmt.Sprintf("uploads/%v/%v/%v/%v",dateyear,datemonth,dateday,header.Filename)
yunfile,err := model.UploadFile(localfile,yunfiletmp)
if err != nil{
log.Printf("UploadFile err: ",err)
// ignore
SendFailure(c,"阿里云上传失败")
return
}
log.Printf("阿里云路径: ",yunfile)
domain := viper.GetString("common.aliyunoss.domain")
domain = fmt.Sprintf("%s/%s",domain,yunfile)
oss := model.OssStruct{
Url: domain,
}
log.Printf("阿里云上传成功: %s",oss)
SendSuccess(c,"阿里云路径",oss)
}
//防止多次重复提交表单
func verifyToken(c *gin.Context) bool{
token := c.PostForm("token")
log.Printf("token: %s",token)
if token != ""{
// 验证 token 的合法性
if len(token) <10{
SendFailure(c,"token验证失败")
return false
}
}else{
//不存在token 报错
SendFailure(c,"token验证失败")
return false
}
log.Printf("token验证通过")
return true
}
建立文件夹 goossserver/uploads 权限为777
建立文件夹和文件 goossserver/main.go ,main.go 内容如下:
package main
import (
"log"
"net/http"
"github.com/gin-gonic/gin"
"github.com/spf13/viper"
"goossserver/config"
"goossserver/router"
"goossserver/service"
)
func main() {
if err := config.Init();err != nil{
panic(err)
}
//设置gin模式
gin.SetMode(viper.GetString("common.server.runmode"))
//创建一个gin引擎
g := gin.New()
router.InitRouter(g)
log.Printf("开始监听服务器地址: %s\n", viper.GetString("common.server.url"))
//使用热重启
// kill -USR2 pid 重启
// kill -INT pid 关闭
add := viper.GetString("common.server.addr")
srv := &http.Server{
Addr: add,
Handler: g,
}
log.Printf( "srv.Addr %v \n", srv.Addr)
service.Listenserver(srv)
}
初始化模块
[root@izj6c4jirdug8kh3uo6rdez goossserver]# go mod init goossserver
go: creating new go.mod: module goossserver
打包并运行服务
# 打包到本地
[root@izj6c4jirdug8kh3uo6rdez goossserver]# go build
# 运行
[root@izj6c4jirdug8kh3uo6rdez goossserver]# ./goossserver
[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
- using env: export GIN_MODE=release
- using code: gin.SetMode(gin.ReleaseMode)
[GIN-debug] POST /ossupload --> goossserver/service.OssUpload (5 handlers)
[root@izj6c4jirdug8kh3uo6rdez goossserver]# [GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
- using env: export GIN_MODE=release
- using code: gin.SetMode(gin.ReleaseMode)
[GIN-debug] POST /ossupload --> goossserver/service.OssUpload (5 handlers)
服务器端结束
参考walk:https://github.com/lxn/walk