Golang实践录:使用gin框架实现转发功能:管理后端服务

Posted 李迟

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Golang实践录:使用gin框架实现转发功能:管理后端服务相关的知识,希望对你有一定的参考价值。

近段时间需要实现一个转发 post 请求到指定后端服务的小工具,由于一直想学习 gin 框架,所以就使用这个框架进行尝试,预计会产生几篇文章。本文研究如何管理后端服务。

思路

在启动 gin 服务前,先启动所有的后端服务进程,并且分配好端口,为简单起见,本文根据请求时间带的月份来转发,后端服务端口从 9000 开始。因此,需启动 13 个服务,端口从 9000 到 9012,如请求时间为9月份,则转发到 9009 端口的服务,对于非法月份,则统一转发到 9000 端口。

实现

  • 添加/的响应,主要是为了后续方便使用 nginx
  • 分配好端口,启动后端服务。
  • 根据请求时间选择一个后端 URL。

代码

主要接口代码

func RunWebServer(args []string) {
    runWebOnlyPost()
}

func runWebOnlyPost() {
    
    // 先执行其它业务,再到http
    restartAll()

	router := gin.New()
	router.Use(gin.Logger())
	router.Use(gin.Recovery())

	testRouter(router)

	klog.Println("Server started at ", conf.Port)
	router.Run(":" + conf.Port)
}

func testRouter(r *gin.Engine) {
	fmt.Println("test post...")
    
    r.POST("/foobar/test", foobar_test)
    r.POST("/foobar/test_back", foobar_test_back)
    // 注:此处直接响应端口的访问,因为实际中使用nginx转发的
	r.POST("/", fee_test_back)
}

在上一版本基础上添加restartAll函数。下面给出实现。

实现代码

restartAll函数实现:

/*
停止后端服务(如有),
启动后端服务,同时分配端口
*/
func restartAll() {
	// 通过端口限定,仅作测试
	thePort, _ := strconv.Atoi(conf.Port)
	if thePort >= 9000 {
		return
	}
	klog.Println("restartAll...")

	appname := "./httpforward_back"

	klog.Printf("try to kill backend server %s\\n", appname[2:])
	// exec.Command("sh", "-c", fmt.Sprintf("pkill -SIGINT %s", appname[2:])).Output()
	exec.Command("sh", "-c", fmt.Sprintf("killall %s", appname[2:])).Output()

	// os.Exit(0)
	// 假定有12个端口,即12个后端服务,但额外有一个防止出错的端口
	startport := 9000
	conf.BackPorts = []int{}
	for i := 0; i < 13; i++ {
		port := startport + i
		klog.Println("run in port: ", port)
		conf.BackPorts = append(conf.BackPorts, port)
		// note:使用系自带的接口,只启动,不等待,必须用'sh -c'格式,且不能合并
		cmd := exec.Command("sh", "-c", fmt.Sprintf("%s -p %d -i \\"run in port %d\\" &", appname, port, port))
		err := cmd.Start()
		if err != nil {
			klog.Printf("!! NOTE !! server on port %d start failed: %v\\n", port, err.Error())
		}
	}
	fmt.Printf("run %d backend server ok\\n", len(conf.BackPorts))
}

转发实现函数:

func foobar_test(ctx *gin.Context) {

	// 2种方式都可,但 ctx.Request.FormFile 可以得到文件句柄,可直接拷贝
	//file, err := ctx.FormFile("file")
	file, header, err := ctx.Request.FormFile("file")
	if err != nil {
		ctx.JSON(http.StatusBadRequest, gin.H{
			"error": err,
		})
		return
	}

	// 拿到文件和长度,后面使用到
	var jsonfilename string = header.Filename
	mysize := header.Size
	fmt.Printf("filename: %s size: %d\\n", jsonfilename, mysize)

	if err != nil {
		ctx.JSON(http.StatusBadRequest, gin.H{
			"error": err,
		})
		return
	}

	// 处理json文件
	jsonbuf := make([]byte, mysize)
	_, err = file.Read(jsonbuf)
	// 注:读取了文件,要回到文件头,否则就没有内容了,因此这里用seek
	file.Seek(0, 0)

	var data map[string]interface{}
	err = json.Unmarshal(jsonbuf, &data)
	//fmt.Println("unmarshal: ", err, data)

	var exTime string
	exTime1 := data["exTime"]
	// 如果出口时间没有,出错
	if exTime1 == nil {
		fmt.Println("exTime not found!")

		ctx.JSON(
			http.StatusOK,
			gin.H{
				"code": -1,
				"msg":  "failed",
				"data": gin.H{
					"result": "exTime not found",
				},
			},
		)
		return
	}

	exTime = exTime1.(string)
	// exTime不合法
	if len(exTime) == 0 {
		ctx.JSON(
			http.StatusOK,
			gin.H{
				"code": -1,
				"msg":  "failed",
				"data": gin.H{
					"result": "exTime is empty",
				},
			},
		)

		return
	}

	fmt.Printf("exTime: %s\\n", exTime)

	// 此处选择一个URL

	url := getOneServerUrl(exTime)
	// 返回空,可能后端服务未启动或内部错误
	if len(url) == 0 {
		ctx.JSON(
			http.StatusOK,
			gin.H{
				"code": -1,
				"msg":  "failed",
				"data": gin.H{
					"result": "ant get backend server url",
				},
			},
		)

		return
	}

	resp, err := post_data_gin(url, jsonfilename, file)
	// 返回空,可能后端服务未启动或内部错误
	if len(resp) == 0 {
		ctx.JSON(
			http.StatusOK,
			gin.H{
				"code": -1,
				"msg":  "failed",
				"data": gin.H{
					"result": fmt.Sprintf("backend server error: %s", err.Error()), //"backend server error: " + err.Error(),
				},
			},
		)

		return
	}

	// 解析返回字符切片,得到map,当成json,赋值给gin
	var data1 map[string]interface{}
	err = json.Unmarshal(resp, &data1)
	//fmt.Println("muti unmarshal: ", err, data1)

	ctx.JSON(http.StatusOK, data1)

	return
}

getOneServerUrl函数实现如下:

// 根据时间选择一个后端服务器URL
func getOneServerUrl(exTime string) (url string) {
	url = ""
	if len(exTime) == 0 {
		return
	}

	// 时间有2个格式,这里都判断一下
	mytime, _ := time.Parse("2006-01-02T15:04:05", exTime)
	themonth := int(mytime.Month())
	// 如果不合法,年月日均为1
	if mytime.Year() == 1 && themonth == 1 && mytime.Day() == 1 {
		mytime, _ = time.Parse("2006-01-02 15:04:05", exTime)
		// 还是不合法
		if mytime.Year() == 1 && themonth == 1 && mytime.Day() == 1 {
			themonth = 0
		} else {
			themonth = int(mytime.Month())
		}
	}
	if themonth >= len(conf.BackPorts) {
		themonth = 0
	}

	url = fmt.Sprintf("http://127.0.0.1:%d", conf.BackPorts[themonth])
	fmt.Println("got url: ", url)

	return
}

/*
 模拟后台的仅获取file字段的json,不作其它处理
 curl http://127.0.0.1:84/foobar/test_back -X POST -F  "file=@sample.json"
*/
func foobar_test_back(ctx *gin.Context) {
	// 2种方式都可,但 ctx.Request.FormFile 可以得到文件句柄,可直接拷贝
	//file, err := ctx.FormFile("file")
	file, header, err := ctx.Request.FormFile("file")
	if err != nil {
		ctx.JSON(
			http.StatusBadRequest,
			gin.H{
				"code": -1,
				"msg":  "failed",
				"data": gin.H{
					"result": "failed in back end server, port:" + conf.Port,
				},
			},
		)

		return
	}

	// 拿到文件和长度,后面使用到
	var myfile string = header.Filename
	mysize := header.Size
	fmt.Printf("filename: %s size: %d\\n", myfile, mysize)

	if mysize <= 0 {
		ctx.JSON(
			http.StatusBadRequest,
			gin.H{
				"code": -1,
				"msg":  "failed",
				"data": gin.H{
					"result": "failed in back end server, json size 0, port: " + conf.Port,
				},
			},
		)

		return
	}

	// 此处可保存文件

	/
	// 处理json文件
	jsonbuf := make([]byte, mysize)
	_, err = file.Read(jsonbuf)
	// 注:读取了文件,要回到文件头,否则就没有内容了,因此这里用seek
	file.Seek(0, 0)
	//fmt.Printf("read %d %v\\n%v\\n", n, err, string(jsonbuf));

	var data map[string]interface{}
	err = json.Unmarshal(jsonbuf, &data)
	//fmt.Println("unmarshal: ", err, data)

	var exTime string
	exTime1 := data["exTime"]
	// 如果出口时间没有,出错
	if exTime1 == nil {
		fmt.Println("exTime not found!")

		ctx.JSON(
			http.StatusOK,
			gin.H{
				"queryState":   0, // 0表示失败
				"massage":      "exTime not found",
				"provinceFees": gin.H{},
			},
		)

		return
	}

	exTime = exTime1.(string)
	fmt.Println("extime: ", exTime)
	/

	//保存成功返回正确的Json数据
	ctx.JSON(
		http.StatusOK,
		gin.H{
			"code": 0,
			"msg":  "ok",
			"data": gin.H{
				"result": "ok in back end server, port: " + conf.Port + " info: " + conf.BackInfo,
			},
		},
	)

	return
}

测试

本文使用 sample.json 文件测试,内容如下:

{
	"enID": "ID250",
	"exID": "ID251",
    "exTime": "2020-09-17T20:00:27",
	"type": 1,
	"money": 250.44,
	"distance": 274050
}

为简单起见,将可执行文件拷贝一份,命名为httpforward_back

先运行 84 端口服务,会自动启动所有的后端进程。打印如下:

# ./httpforward.exe -p 84
[2021-09-09 14:56:40.691 restart.go:26] restartAll...
[2021-09-09 14:56:40.691 restart.go:38] try to kill backend server httpforward_back
[2021-09-09 14:56:40.700 restart.go:55] run in port:  9000
[2021-09-09 14:56:40.707 restart.go:55] run in port:  9001
[2021-09-09 14:56:40.712 restart.go:55] run in port:  9002
[2021-09-09 14:56:40.729 restart.go:55] run in port:  9003
[2021-09-09 14:56:40.732 restart.go:55] run in port:  9004
[2021-09-09 14:56:40.761 restart.go:55] run in port:  9005
[2021-09-09 14:56:40.768 restart.go:55] run in port:  9006
[2021-09-09 14:56:40.783 restart.go:55] run in port:  9007
[2021-09-09 14:56:40.803 restart.go:55] run in port:  9008
[2021-09-09 14:56:40.807 restart.go:55] run in port:  9009
[2021-09-09 14:56:40.809 restart.go:55] run in port:  9010
[2021-09-09 14:56:40.811 restart.go:55] run in port:  9011
[2021-09-09 14:56:40.849 restart.go:55] run in port:  9012
run 13 backend server ok
[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)

test post...
[GIN-debug] POST   /foobar/test                 --> goweb/cmd/gin.fee_test (3 handlers)
[GIN-debug] POST   /foobar/test_back            --> goweb/cmd/gin.fee_test_back (3 handlers)
[GIN-debug] POST   /                         --> goweb/cmd/gin.fee_test_back (3 handlers)
[2021-09-09 14:56:40.885 busy.go:77] Server started at  84
[GIN-debug] Listening and serving HTTP on :84

启动一终端,执行测试命令:

curl http://127.0.0.1:84/foobar/ -X POST -F  "file=@sample.json"

可以修改sample.json文件的exTime观察转发的端口和返回值

84 服务打印:

filename: sample.json size: 133
exTime: 2020-09-17T20:00:27
got url:  http://127.0.0.1:9009
[GIN] 2021/09/09 - 14:56:44 | 200 |    3.664936ms |    192.168.28.5 | POST     "/foobar/test"

filename: sample.json size: 133
exTime: 2020-12-17T20:00:27
got url:  http://127.0.0.1:9012
[GIN] 2021/09/09 - 15:04:30 | 200 |    2.465313ms |    192.168.28.5 | POST     "/foobar/test"

测试命令返回:

  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100   434  100    98  100   336  16333  56000 --:--:-- --:--:-- --:--:-- 72333{"code":0,"data":{"result":"ok in back end server, port: 9009 info: run in port 9009"},"msg":"ok"}

  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100   434  100    98  100   336   5764  19764 --:--:-- --:--:-- --:--:-- 27125{"code":0,"data":{"result":"ok in back end server, port: 9012 info: run in port 9012"},"msg":"ok"}

也可直接向后端服务请求:

$ curl http://127.0.0.1:85/foobar/test_back -X POST -F  "file=@sample.json"
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100   361  100    63  100   298  63000   291k --:--:-- --:--:-- --:--:--  352k{"code":0,"data":{"result":"ok in back end server"},"msg":"ok"}

2021.9.18 夜

以上是关于Golang实践录:使用gin框架实现转发功能:管理后端服务的主要内容,如果未能解决你的问题,请参考以下文章

Golang实践录:使用gin框架实现转发功能:利用nginx转发

Golang实践录:使用gin框架实现转发功能:上传文件并转

Golang实践录:使用gin框架实现转发功能:上传文件并转

Golang实践录:使用gin框架实现转发功能:管理后端服务

Golang实践录:使用gin框架实现转发功能:管理后端服务

Golang实践录:使用gin框架实现转发功能:一些负载均衡算法的实现