iT邦幫忙

2023 iThome 鐵人賽

DAY 14
0

今天來寫最後server的部分,首先新增server/server.go,透過initRouter註冊一個gin的engine後,調用RegisterHandlers來註冊昨天在Router層寫好的路由,之後如果要對gin.Engine統一做一些操作也可以放在這邊,像是註冊middleware之類的~

func initRouter(rootCtx context.Context, app *app.Application) (ginRouter *gin.Engine) {

	// create *gin.Engine
	ginRouter = gin.New()

	// RegisterHandlers
	router.RegisterHandlers(ginRouter, app)

	return ginRouter
}

接著我們把原本寫在main.go,建立GinLambda的部分拉出來,放到NewGinLambda()這邊,這樣我們可以讓main()乾淨一點點

func NewGinLambda() *ginadapter.GinLambda {
	rootCtx, _ := context.WithCancel(context.Background()) //nolint
	ssmsvc := ssm.NewSSM()

	lineSecret, err := ssmsvc.FindParameter(rootCtx, ssmsvc.Client, "CHANNEL_SECRET")
	if err != nil {
		log.Println(err)
	}

	lineAccessToken, err := ssmsvc.FindParameter(rootCtx, ssmsvc.Client, "CHANNEL_ACCESS_TOKEN")
	if err != nil {
		log.Println(err)
	}
	lineClientLambda, err := linebot.New(lineSecret, lineAccessToken)
	if err != nil {
		log.Fatal(err)
	}
	log.Println("LineBot Create Success")

	db := dynamodb.NewTableBasics("google-oauth")

	app := app.NewApplication(rootCtx, db, lineClientLambda)
	ginRouter := initRouter(rootCtx, app)
	return ginadapter.New(ginRouter)
}

下一步,我們寫一個StartNgrokServer(),跟NewGinLambda()很像只是我們改去env拿那些secret去New Linebot的Client,還有把dynamodb換成連線到local的。使用runNgrokServer()啟動Ngrok Server,並實現graceful shutdown。

func StartNgrokServer() {
	// 初始化root上下文和取消函數
	rootCtx, rootCtxCancelFunc := context.WithCancel(context.Background())
	// 使用sync.WaitGroup等待所有goroutine的完成
	wg := sync.WaitGroup{}
	// 初始化LineBot客戶端
	lineClient, err := linebot.New(os.Getenv("CHANNEL_SECRET"), os.Getenv("CHANNEL_ACCESS_TOKEN"))
	if err != nil {
		log.Fatal(err.Error())
	}
	
	// 初始化DynamoDB連接,然後切換到本地DynamoDB
	db := dynamodb.NewTableBasics("google-oauth")
	db.DynamoDbClient = dynamodb.CreateLocalClient(8000)
	// 初始化Application
	app := app.NewApplication(rootCtx, db, lineClient)
	// 初始化Gin路由
	ginRouter := initRouter(rootCtx, app)

	// 啟動 ngrok
	wg.Add(1)
	runNgrokServer(rootCtx, &wg, ginRouter)

	// 監聽SIGTERM/SIGINT信號來進行優雅的關閉
	var gracefulStop = make(chan os.Signal, 1)
	signal.Notify(gracefulStop, syscall.SIGTERM, syscall.SIGINT)
	// 阻塞,當收到信號時執行下面的code觸發Ngrok的關閉
	<-gracefulStop
	rootCtxCancelFunc()

	// 使用goroutine等待所有服務的結束,等待最多10s
	var waitUntilDone = make(chan struct{})
	go func() {
		wg.Wait()
		close(waitUntilDone)
	}()
	select {
	case <-waitUntilDone:
		log.Println("success to close all services")
	case <-time.After(10 * time.Second):
		log.Println(context.DeadlineExceeded, "fail to close all services")
	}

}

這邊我們一樣從env拿到Static Domain和Authtoken,創建ngrok通道後,在goroutine中將server跑起來,並使用另一個goroutine來等待rootCtx的完成,然後透過tun.CloseWithContext來關閉Ngrok。

func runNgrokServer(rootCtx context.Context, wg *sync.WaitGroup, ginRouter *gin.Engine) {
	// 創建Ngrok通道
	tun, err := ngrok.Listen(rootCtx,
		config.HTTPEndpoint(config.WithDomain(os.Getenv("NGROK_DOMAIN"))),
		ngrok.WithAuthtokenFromEnv(),
	)

	if err != nil {
		log.Fatal(err)
	}
	log.Println("Application available at:", tun.URL())

	// 在goroutine中運行伺服器
	go func() {
		err = http.Serve(tun, ginRouter)
		if err != nil {
			log.Fatal(err)
		}
	}()

	// 等待rootCtx的完成
	go func() {
		<-rootCtx.Done()
		// Create a context with a timeout for closing the ngrok tunnel
		log.Println("Shutting down ngrok server...")
		ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
		defer cancel()
		// Use the created context to close the ngrok tunnel with a timeout
		if err := tun.CloseWithContext(ctx); err != nil {
			log.Printf("Error closing ngrok tunnel: %v\n", err)
		}
		log.Println("ngrok server gracefully stopped")
		wg.Done()
	}()

}

完整的程式碼如下

package server

import (
	"context"
	"log"
	"net/http"
	"os"
	"os/signal"
	"sync"
	"syscall"
	"time"

	ginadapter "github.com/awslabs/aws-lambda-go-api-proxy/gin"
	"github.com/gin-gonic/gin"
	_ "github.com/joho/godotenv/autoload"
	"github.com/line/line-bot-sdk-go/v7/linebot"
	"github.com/onepiece010938/Line2GoogleDriveBot/internal/adapter/dynamodb"
	"github.com/onepiece010938/Line2GoogleDriveBot/internal/adapter/ssm"
	"github.com/onepiece010938/Line2GoogleDriveBot/internal/app"
	"github.com/onepiece010938/Line2GoogleDriveBot/internal/router"
	"golang.ngrok.com/ngrok"
	"golang.ngrok.com/ngrok/config"
)

func NewGinLambda() *ginadapter.GinLambda {
	rootCtx, _ := context.WithCancel(context.Background()) //nolint
	ssmsvc := ssm.NewSSM()

	lineSecret, err := ssmsvc.FindParameter(rootCtx, ssmsvc.Client, "CHANNEL_SECRET")
	if err != nil {
		log.Println(err)
	}

	lineAccessToken, err := ssmsvc.FindParameter(rootCtx, ssmsvc.Client, "CHANNEL_ACCESS_TOKEN")
	if err != nil {
		log.Println(err)
	}
	lineClientLambda, err := linebot.New(lineSecret, lineAccessToken)
	if err != nil {
		log.Fatal(err)
	}
	log.Println("LineBot Create Success")

	db := dynamodb.NewTableBasics("google-oauth")

	app := app.NewApplication(rootCtx, db, lineClientLambda)
	ginRouter := initRouter(rootCtx, app)
	return ginadapter.New(ginRouter)
}

func initRouter(rootCtx context.Context, app *app.Application) (ginRouter *gin.Engine) {

	ginRouter = gin.New()
	router.RegisterHandlers(ginRouter, app)

	return ginRouter
}

func StartNgrokServer() {
	rootCtx, rootCtxCancelFunc := context.WithCancel(context.Background())
	wg := sync.WaitGroup{}

	lineClient, err := linebot.New(os.Getenv("CHANNEL_SECRET"), os.Getenv("CHANNEL_ACCESS_TOKEN"))
	if err != nil {
		log.Fatal(err.Error())
	}

	db := dynamodb.NewTableBasics("google-oauth")
	db.DynamoDbClient = dynamodb.CreateLocalClient(8000)

	app := app.NewApplication(rootCtx, db, lineClient)

	ginRouter := initRouter(rootCtx, app)
	
	wg.Add(1)
	runNgrokServer(rootCtx, &wg, ginRouter)

	var gracefulStop = make(chan os.Signal, 1)
	signal.Notify(gracefulStop, syscall.SIGTERM, syscall.SIGINT)
	<-gracefulStop
	rootCtxCancelFunc()

	var waitUntilDone = make(chan struct{})
	go func() {
		wg.Wait()
		close(waitUntilDone)
	}()
	select {
	case <-waitUntilDone:
		log.Println("success to close all services")
	case <-time.After(10 * time.Second):
		log.Println(context.DeadlineExceeded, "fail to close all services")
	}

}
func runNgrokServer(rootCtx context.Context, wg *sync.WaitGroup, ginRouter *gin.Engine) {

	tun, err := ngrok.Listen(rootCtx,
		config.HTTPEndpoint(config.WithDomain(os.Getenv("NGROK_DOMAIN"))),
		ngrok.WithAuthtokenFromEnv(),
	)

	if err != nil {
		log.Fatal(err)
	}
	log.Println("Application available at:", tun.URL())

	go func() {
		err = http.Serve(tun, ginRouter)
		if err != nil {
			log.Fatal(err)
		}
	}()

	go func() {
		<-rootCtx.Done()
		log.Println("Shutting down ngrok server...")
		ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
		defer cancel()
		if err := tun.CloseWithContext(ctx); err != nil {
			log.Printf("Error closing ngrok tunnel: %v\n", err)
		}
		log.Println("ngrok server gracefully stopped")
		wg.Done()
	}()

}

最後回到main.go,根據Gin模式的不同來決定要跑NewGinLambda()還是StartNgrokServer()

package main

import (
	"context"
	"log"

	"github.com/aws/aws-lambda-go/events"
	"github.com/aws/aws-lambda-go/lambda"
	"github.com/gin-gonic/gin"

	ginadapter "github.com/awslabs/aws-lambda-go-api-proxy/gin"
	"github.com/onepiece010938/Line2GoogleDriveBot/server"
)

var ginLambda *ginadapter.GinLambda

func Handler(ctx context.Context, request events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) {
	return ginLambda.ProxyWithContext(ctx, request)
}

func main() {
	// env GIN_MODE="release"
	if gin.Mode() == gin.ReleaseMode {
		log.Println("Run on Lambda")
		ginLambda = server.NewGinLambda()
		lambda.Start(Handler)
	} else if gin.Mode() == gin.DebugMode {
		log.Println("Debug mode run on local")
		server.StartNgrokServer()
	}
}

最後的最後,我們到Line Developer的系統上,把我們的Webhook URL換成Ngrok的靜態網域+/api/v1/callback,讓他調用到我們指定的路由。

https://ithelp.ithome.com.tw/upload/images/20230929/20115990PAFjYhPzXB.png

go run main.go後,打開linebot,輸入我們有存的lineID,就會回傳dynamodb裡有加上前綴的樣子囉~那我們明天見~

https://ithelp.ithome.com.tw/upload/images/20230929/20115990SJHLul56Fw.png


上一篇
Day13 Sever"啟動" - Gin + ( Lambda or Ngrok ) 03
下一篇
Day15 GoogleDrive API 01
系列文
Golang LineBot X GoogleDrive:LINE有各種限制!? 那就丟上Drive吧!30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言