近段时间需要实现一个转发 post 请求到指定后端服务的小工具,由于一直想学习 gin 框架,所以就使用这个框架进行尝试,预计会产生几篇文章。本文研究如何管理后端服务。
思路
在启动 gin 服务前,先启动所有的后端服务进程,并且分配好端口,为简单起见,本文根据请求时间带的月份来转发,后端服务端口从 9000 开始。因此,需启动 13 个服务,端口从 9000 到 9012,如请求时间为9月份,则转发到 9009 端口的服务,对于非法月份,则统一转发到 9000 端口。
实现
添加/
的响应,主要是为了后续方便使用 nginx。
分配好端口,启动后端服务。
根据请求时间选择一个后端 URL。
代码
主要接口代码
funcRunWebServer(args[]string){runWebOnlyPost()}funcrunWebOnlyPost(){//先执行其它业务,再到httprestartAll()router:=gin.New()router.Use(gin.Logger())router.Use(gin.Recovery())testRouter(router)klog.Println("Serverstartedat",conf.Port)router.Run(":"+conf.Port)}functestRouter(r*gin.Engine){fmt.Println("testpost...")r.POST("/foobar/test",foobar_test)r.POST("/foobar/test_back",foobar_test_back)//注:此处直接响应端口的访问,因为实际中使用nginx转发的r.POST("/",fee_test_back)}
在上一版本基础上添加restartAll
函数。下面给出实现。
实现代码
restartAll
函数实现:
/*停止后端服务(如有),启动后端服务,同时分配端口*/funcrestartAll(){//通过端口限定,仅作测试thePort,_:=strconv.Atoi(conf.Port)ifthePort>=9000{return}klog.Println("restartAll...")appname:="./httpforward_back"klog.Printf("trytokillbackendserver%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:=9000conf.BackPorts=[]int{}fori:=0;i<13;i++{port:=startport+iklog.Println("runinport:",port)conf.BackPorts=append(conf.BackPorts,port)//note:使用系自带的接口,只启动,不等待,必须用'sh-c'格式,且不能合并cmd:=exec.Command("sh","-c",fmt.Sprintf("%s-p%d-i\"runinport%d\"&",appname,port,port))err:=cmd.Start()iferr!=nil{klog.Printf("!!NOTE!!serveronport%dstartfailed:%v\n",port,err.Error())}}fmt.Printf("run%dbackendserverok\n",len(conf.BackPorts))}
转发实现函数:
funcfoobar_test(ctx*gin.Context){//2种方式都可,但ctx.Request.FormFile可以得到文件句柄,可直接拷贝//file,err:=ctx.FormFile("file")file,header,err:=ctx.Request.FormFile("file")iferr!=nil{ctx.JSON(http.StatusBadRequest,gin.H{"error":err,})return}//拿到文件和长度,后面使用到varjsonfilenamestring=header.Filenamemysize:=header.Sizefmt.Printf("filename:%ssize:%d\n",jsonfilename,mysize)iferr!=nil{ctx.JSON(http.StatusBadRequest,gin.H{"error":err,})return}//处理json文件jsonbuf:=make([]byte,mysize)_,err=file.Read(jsonbuf)//注:读取了文件,要回到文件头,否则就没有内容了,因此这里用seekfile.Seek(0,0)vardatamap[string]interface{}err=json.Unmarshal(jsonbuf,&data)//fmt.Println("unmarshal:",err,data)varexTimestringexTime1:=data["exTime"]//如果出口时间没有,出错ifexTime1==nil{fmt.Println("exTimenotfound!")ctx.JSON(http.StatusOK,gin.H{"code":-1,"msg":"failed","data":gin.H{"result":"exTimenotfound",},},)return}exTime=exTime1.(string)//exTime不合法iflen(exTime)==0{ctx.JSON(http.StatusOK,gin.H{"code":-1,"msg":"failed","data":gin.H{"result":"exTimeisempty",},},)return}fmt.Printf("exTime:%s\n",exTime)//此处选择一个URLurl:=getOneServerUrl(exTime)//返回空,可能后端服务未启动或内部错误iflen(url)==0{ctx.JSON(http.StatusOK,gin.H{"code":-1,"msg":"failed","data":gin.H{"result":"antgetbackendserverurl",},},)return}resp,err:=post_data_gin(url,jsonfilename,file)//返回空,可能后端服务未启动或内部错误iflen(resp)==0{ctx.JSON(http.StatusOK,gin.H{"code":-1,"msg":"failed","data":gin.H{"result":fmt.Sprintf("backendservererror:%s",err.Error()),//"backendservererror:"+err.Error(),},},)return}//解析返回字符切片,得到map,当成json,赋值给ginvardata1map[string]interface{}err=json.Unmarshal(resp,&data1)//fmt.Println("mutiunmarshal:",err,data1)ctx.JSON(http.StatusOK,data1)return}
getOneServerUrl
函数实现如下:
//根据时间选择一个后端服务器URLfuncgetOneServerUrl(exTimestring)(urlstring){url=""iflen(exTime)==0{return}//时间有2个格式,这里都判断一下mytime,_:=time.Parse("2006-01-02T15:04:05",exTime)themonth:=int(mytime.Month())//如果不合法,年月日均为1ifmytime.Year()==1&&themonth==1&&mytime.Day()==1{mytime,_=time.Parse("2006-01-0215:04:05",exTime)//还是不合法ifmytime.Year()==1&&themonth==1&&mytime.Day()==1{themonth=0}else{themonth=int(mytime.Month())}}ifthemonth>=len(conf.BackPorts){themonth=0}url=fmt.Sprintf("http://127.0.0.1:%d",conf.BackPorts[themonth])fmt.Println("goturl:",url)return}/*模拟后台的仅获取file字段的json,不作其它处理curlhttp://127.0.0.1:84/foobar/test_back-XPOST-F"file=@sample.json"*/funcfoobar_test_back(ctx*gin.Context){//2种方式都可,但ctx.Request.FormFile可以得到文件句柄,可直接拷贝//file,err:=ctx.FormFile("file")file,header,err:=ctx.Request.FormFile("file")iferr!=nil{ctx.JSON(http.StatusBadRequest,gin.H{"code":-1,"msg":"failed","data":gin.H{"result":"failedinbackendserver,port:"+conf.Port,},},)return}//拿到文件和长度,后面使用到varmyfilestring=header.Filenamemysize:=header.Sizefmt.Printf("filename:%ssize:%d\n",myfile,mysize)ifmysize<=0{ctx.JSON(http.StatusBadRequest,gin.H{"code":-1,"msg":"failed","data":gin.H{"result":"failedinbackendserver,jsonsize0,port:"+conf.Port,},},)return}//此处可保存文件///////////////////////////////////////////处理json文件jsonbuf:=make([]byte,mysize)_,err=file.Read(jsonbuf)//注:读取了文件,要回到文件头,否则就没有内容了,因此这里用seekfile.Seek(0,0)//fmt.Printf("read%d%v\n%v\n",n,err,string(jsonbuf));vardatamap[string]interface{}err=json.Unmarshal(jsonbuf,&data)//fmt.Println("unmarshal:",err,data)varexTimestringexTime1:=data["exTime"]//如果出口时间没有,出错ifexTime1==nil{fmt.Println("exTimenotfound!")ctx.JSON(http.StatusOK,gin.H{"queryState":0,//0表示失败"massage":"exTimenotfound","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":"okinbackendserver,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-p84[2021-11-1309:56:40.691restart.go:26]restartAll...[2021-11-1309:56:40.691restart.go:38]trytokillbackendserverhttpforward_back[2021-11-1309:56:40.700restart.go:55]runinport:9000[2021-11-1309:56:40.707restart.go:55]runinport:9001[2021-11-1309:56:40.712restart.go:55]runinport:9002[2021-11-1309:56:40.729restart.go:55]runinport:9003[2021-11-1309:56:40.732restart.go:55]runinport:9004[2021-11-1309:56:40.761restart.go:55]runinport:9005[2021-11-1309:56:40.768restart.go:55]runinport:9006[2021-11-1309:56:40.783restart.go:55]runinport:9007[2021-11-1309:56:40.803restart.go:55]runinport:9008[2021-11-1309:56:40.807restart.go:55]runinport:9009[2021-11-1309:56:40.809restart.go:55]runinport:9010[2021-11-1309:56:40.811restart.go:55]runinport:9011[2021-11-1309:56:40.849restart.go:55]runinport:9012run13backendserverok[GIN-debug][WARNING]Runningin"debug"mode.Switchto"release"modeinproduction.-usingenv:exportGIN_MODE=release-usingcode:gin.SetMode(gin.ReleaseMode)testpost...[GIN-debug]POST/foobar/test-->goweb/cmd/gin.fee_test(3handlers)[GIN-debug]POST/foobar/test_back-->goweb/cmd/gin.fee_test_back(3handlers)[GIN-debug]POST/-->goweb/cmd/gin.fee_test_back(3handlers)[2021-11-1309:56:40.885busy.go:77]Serverstartedat84[GIN-debug]ListeningandservingHTTPon:84
启动一终端,执行测试命令:
curlhttp://127.0.0.1:84/foobar/-XPOST-F"file=@sample.json"
可以修改sample.json
文件的exTime
观察转发的端口和返回值
84 服务打印:
filename:sample.jsonsize:133exTime:2020-09-17T20:00:27goturl:http://127.0.0.1:9009[GIN]2021/11/13-10:00:21|200|3.664936ms|192.168.28.5|POST"/foobar/test"filename:sample.jsonsize:133exTime:2020-12-17T20:00:27goturl:http://127.0.0.1:9012[GIN]2021/11/13-10:02:56|200|2.465313ms|192.168.28.5|POST"/foobar/test"
测试命令返回:
%Total%Received%XferdAverageSpeedTimeTimeTimeCurrentDloadUploadTotalSpentLeftSpeed100434100981003361633356000--:--:----:--:----:--:--72333{"code":0,"data":{"result":"okinbackendserver,port:9009info:runinport9009"},"msg":"ok"}%Total%Received%XferdAverageSpeedTimeTimeTimeCurrentDloadUploadTotalSpentLeftSpeed10043410098100336576419764--:--:----:--:----:--:--27125{"code":0,"data":{"result":"okinbackendserver,port:9012info:runinport9012"},"msg":"ok"}
也可直接向后端服务请求:
$curlhttp://127.0.0.1:85/foobar/test_back-XPOST-F"file=@sample.json"%Total%Received%XferdAverageSpeedTimeTimeTimeCurrentDloadUploadTotalSpentLeftSpeed1003611006310029863000291k--:--:----:--:----:--:--352k{"code":0,"data":{"result":"okinbackendserver"},"msg":"ok"}