今天來寫最後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
,讓他調用到我們指定的路由。
go run main.go
後,打開linebot,輸入我們有存的lineID,就會回傳dynamodb裡有加上前綴的樣子囉~那我們明天見~