go实践十四 http服务器+redis+热重启

本文使用 gin 做 http服务器 ,redis读写数据, graceful 热重启

redis安装参考:https://blog.csdn.net/daily886/article/details/78569658

需要用到的第三方包有:

github.com/gin-gonic/gin
github.com/go-redis/redis
github.com/spf13/viper

目录结构如下: 
项目根目录是 httpredis


httpredis的目录结构
├── conf                               # 配置文件统一存放目录
│   ├── config.yaml              # 配置文件
├── config                            # 专门用来处理配置和配置文件的Go package
│   └── config.go                 
├── handler                          # 类似MVC架构中的C,用来读取输入,并将处理流程转发给实际的处理函数,最后返回结果
│   ├── handler.go
├── logs                               # 日志文件
├── model                            # 模型
│   ├── redis.go                    # redis模型
├── pkg                                # 引用的包
│   ├── errno                        # 错误码存放位置
│   │   ├── code.go
│   │   └── errno.go
├── router                             # 路由相关处理
│   ├── middleware               # API服务器用的是Gin Web框架,Gin中间件存放位置
│   │   ├── header.go
│   └── router.go                   # 路由
├── service                           # 实际业务处理函数存放位置
│   └── hotupdate.go            # 热重启服务
│   └── service.go
├── main.go                          # Go程序唯一入口

下面,我们根据目录结构,从上往下建立文件夹和文件

建立文件夹和文件 httpredis/conf/config.yaml ,config.yaml 内容如下:

common:
  server: #服务器配置
    runmode: debug               # 开发模式, debug, release, test
    addr: :6663                  # HTTP绑定端口
    name: apiserver              # API Server的名字
    url: http://10.10.87.243:6663   # pingServer函数请求的API服务器的ip:port
    max_ping_count: 10           # pingServer函数尝试的次数
  redis: #redis配置
    host: 10.10.87.242
    port: 6001
    db: 0
    auth:        #密码,没有就不需要

建立文件夹和文件 httpredis/config/config.go ,config.go 内容如下:

package config

import (
	"time"
	"os"
	"log"

	"github.com/spf13/viper"
)

// 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
}

建立文件夹和文件 httpredis/handler/handler.go ,handler.go 内容如下:

package handler

import (
	"bytes"
	"net/http"
	"io/ioutil"

	"github.com/gin-gonic/gin"

	"httpredis/pkg/errno"
)

type Response struct {
	Code    int         `json:"code"`
	Message string      `json:"message"`
	Data    interface{} `json:"data"`
}

//返回json 格式
func SendResponse(c *gin.Context,err error,data interface{}){
	code,message := errno.DecodeErr(err)

	//总是返回http状态ok
	c.JSON(http.StatusOK,Response{
		Code: code,
		Message:message,
		Data: data,
	})

}

//返回html 格式
func SendResponseHtml(c *gin.Context,err error,data string){
	c.Header("Content-Type", "text/html; charset=utf-8")
	//总是返回http状态ok
	c.String(http.StatusOK,data)
}

//http请求
func HttpRequest(api string,json string,method string) (string, error) {
	jsonStr := []byte(json)
	req, err := http.NewRequest(method, api, bytes.NewBuffer(jsonStr))
	req.Header.Set("Content-Type", "application/json") //使用json格式传参

	client := &http.Client{}
	resp, err := client.Do(req)
	if err != nil {
		return "", errno.ApiServerError
	}
	defer resp.Body.Close()

	body, _ := ioutil.ReadAll(resp.Body)

	if !(resp.StatusCode == 200) {
		return "",  errno.ApiServerError
	}
	return string(body), nil
}

建立文件夹 httpredis/logs 存放日志文件, logs文件夹的权限 0777

建立文件夹和文件 httpredis/model/redis.go ,redis.go 内容如下:

package model

import (
	"log"

	"github.com/go-redis/redis"
	"github.com/spf13/viper"
)


//初始化redis
func InitRedis() (*redis.Client,error) {
	var addr = viper.GetString("common.redis.host")+":"+viper.GetString("common.redis.port")
	var auth = viper.GetString("common.redis.auth")
	var db = viper.GetInt("common.redis.db")
	cli := redis.NewClient(&redis.Options{
		Addr:     addr,
		Password: auth,
		DB:       db,
	})

	pong, err := cli.Ping().Result()
	if(err != nil){
		return cli,err
	}
	log.Println("redis init ",pong)
	// Output: PONG <nil>
	return cli,nil
}

建立文件夹和文件 httpredis/pkg/errno/code.go ,code.go 内容如下:

package errno

var (
	// Common errors
	OK                  = &Errno{Code: 0, Message: "OK"}
	VALUEERROR        = &Errno{Code: -1, Message: "输入错误"}

	InternalServerError = &Errno{Code: 10001, Message: "服务器错误"}
	ApiServerError = &Errno{Code: 20001, Message: "接口服务器错误"}
	ModelError = &Errno{Code: 30001, Message: "redis模型错误"}
)

建立文件夹和文件 httpredis/pkg/errno/errno.go ,errno.go 内容如下:

package errno

import "fmt"

type Errno struct {
	Code int
	Message string
}

//返回错误信息
func (err Errno) Error() string{
	return err.Message
}

//设置 Err 结构体
type Err struct {
	Code int
	Message string
	Err error
}

//声明构造体
func New(errno *Errno,err error) *Err{
	return &Err{Code:errno.Code,Message:errno.Message,Err:err}
}

//添加错误信息
func (err *Err) Add(message string) error{
	err.Message += " " + message
	return err
}

//添加指定格式的错误信息
func (err * Err) Addf(format string,args...interface{}) error{
	err.Message += " " + fmt.Sprintf(format,args...)
	return err
}

//拼接错误信息字符串
func (err *Err) Error() string{
	return fmt.Sprintf("Err - code: %d, message: %s, error: %s",err.Code,err.Message,err.Err)
}

// 解析 错误信息, 返回字符串
func DecodeErr(err error) (int,string){
	if err == nil{
		return OK.Code,OK.Message
	}
	switch typed := err.(type) {
	case *Err:
		return typed.Code,typed.Message
	case *Errno:
		return typed.Code,typed.Message
	default:
	}
	return InternalServerError.Code,err.Error()
}

建立文件夹和文件 httpredis/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")
}

建立文件夹和文件 httpredis/router/router.go ,router.go 内容如下:

package router

import (
	"net/http"

	"github.com/gin-gonic/gin"

	"httpredis/service"
	"httpredis/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.GET("/",service.Index)//主页
	g.GET("/setRedis",service.SetRedis) //设置redis
	g.GET("/getRedis",service.GetRedis) //获取redis
}

建立文件夹和文件 httpredis/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{
		//父进程监听新建的 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)
		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...)
	log.Println("os.Args[0]",os.Args[0])
	log.Printf("%+v\n",os.Args)
	log.Printf("args %+v \n",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 开始。
*/

建立文件夹和文件 httpredis/service/service.go ,service.go 内容如下:

package service

import (
	"log"

	"github.com/gin-gonic/gin"
	"github.com/go-redis/redis"

	"httpredis/pkg/errno"
	. "httpredis/handler"
)
var cli *redis.Client

//设置redis客户端构造体
func SetRedisCli(client *redis.Client){
	cli = client
}

//首页
func Index(c *gin.Context){
	html := `
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>hello world</title>
</head>
<body>
    hello world
</body>
</html>
`
	SendResponseHtml(c,nil,html)
}

func SetRedis(c *gin.Context){
	key := c.Query("key")
	value := c.Query("value")
	if key == "" || value == ""{
		SendResponse(c,errno.VALUEERROR,nil)
		return
	}
	_,err := cli.Set(key,value,0).Result();
	if(err != nil){
		SendResponse(c,errno.ApiServerError,err)
	}
	results := "成功"
	SendResponse(c,nil,results)
}

func GetRedis(c *gin.Context){
	key := c.Query("key")
	if key == ""{
		SendResponse(c,errno.VALUEERROR,nil)
		return
	}
	log.Printf("key  %+v\n",key)
	results,err := cli.Get(key).Result();
	if(err != nil){
		SendResponse(c,errno.ApiServerError,err)
	}
	log.Printf("results  %+v\n",results)
	SendResponse(c,nil,results)
}

建立文件夹和文件 httpredis/main.go ,main.go 内容如下:

package main

import (
	"log"
	"net/http"

	"github.com/gin-gonic/gin"
	"github.com/spf13/viper"

	"httpredis/router"
	"httpredis/config"
	"httpredis/model"
	"httpredis/service"
)

func main() {
	//初始化配置
	if err := config.Init();err != nil{
		panic(err)
	}

	//初始化redis
	redisCli,err := model.InitRedis();
	if err != nil{
		panic(err)
	}
	service.SetRedisCli(redisCli)

	//设置gin模式
	gin.SetMode(viper.GetString("common.server.runmode"))

	//创建一个gin引擎
	g := gin.New()

	router.InitRouter(g)
	log.Printf("开始监听服务器地址: %s\n", viper.GetString("common.server.url"))
	//不使用热重启
	//if err := g.Run(viper.GetString("common.server.addr"));err != nil {
	//	log.Fatal("监听错误:", err)
	//}

	//使用热重启
	// 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)
}

切换到httpredis目录中, 初始化包

[root@localhost httpredis]# go mod init httpredis
go: creating new go.mod: module httpredis

打包项目

[root@localhost httpredis]# go build

启动项目服务器 

[root@localhost httpredis]# ./httpredis
[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] GET    /                         --> httpredis/service.Index (5 handlers)
[GIN-debug] GET    /setRedis                 --> httpredis/service.SetRedis (5 handlers)
[GIN-debug] GET    /getRedis                 --> httpredis/service.GetRedis (5 handlers)

浏览器运行结果:

查看服务器 pid:

[root@localhost /]# netstat -tunlp | grep 6663
tcp6       0      0 :::6663                 :::*                    LISTEN      6216/./httpredis  

热重启服务器:

[root@localhost /]# kill -USR2 6216

重启结果:

2019/07/25 10:45:52 hotupdate.go:73: signal receive: user defined signal 2
2019/07/25 10:45:52 hotupdate.go:83: reload
2019/07/25 10:45:52 hotupdate.go:110: os.Args[0] ./httpredis
2019/07/25 10:45:52 hotupdate.go:111: [./httpredis]
2019/07/25 10:45:52 hotupdate.go:112: args [-graceful] 
2019/07/25 10:45:52 hotupdate.go:118: forked new pid 6468: 
2019/07/25 10:45:52 hotupdate.go:89: graceful reload
2019/07/25 10:45:52 hotupdate.go:60: signal end
2019/07/25 10:45:52 redis.go:26: redis init  PONG
2019/07/25 10:45:52 main.go:34: 开始监听服务器地址: http://10.10.87.243:6663
2019/07/25 10:45:52 main.go:48: srv.Addr  :6663  
2019/07/25 10:45:52 hotupdate.go:36: listening on the existing file descriptor 3
2019/07/25 10:45:52 hotupdate.go:41: graceful-reborn  3   &net.TCPListener{fd:(*net.netFD)(0xc0000e3700)} 

关闭服务器:

[root@localhost /]# kill -INT 6468

关闭结果:

2019/07/25 10:47:15 hotupdate.go:73: signal receive: interrupt
2019/07/25 10:47:15 hotupdate.go:77: shutdown
2019/07/25 10:47:15 hotupdate.go:80: graceful shutdown
2019/07/25 10:47:15 hotupdate.go:60: signal end

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值