이번 프로젝트를 진행하면서 웹 알람 기능을 구현하게 되었다. 

알람 기능이라 하면 유저는 특정 이벤트에 대해 특정 행동을 하지 않더라도 이벤트를 확인할 수 있어야 한다. 

팀원들은 기존 real-time은 늘 구현했던 WebSocket 방식을 이용해서 구현하는것으로 이야기했다. 
내가 왜 SSE를 채택하고 팀원을 설득하면서 공부했던 내용을 기록하고자 한다.


최초 위에 제공된 기능을 구현하기 위해 고려된 방식으로는 아래 3가지이다.

1. 폴링방식 - 클라이언트 측에서 일정시간을 두고 주기적으로 호출하는 방식이다.

2. 웹소켓 - ws프로토콜 방식으로 서버와 클라이언트는 데이터를 주고받을 수 있다.

3. SSE 통신 - http프로토콜을 이용해 단방향으로 데이터를 송신,수신 할수 있다. 

 

Polling Web-Socket Server Sent Event 
일반적인 Rest 방식의 클라이언트->서버 소통  클라이언트 <-> 서버의 양방향 소통 클라이언트 <- 서버의 단방향 소통
HTTP Protocol WebSockt Protocol HTTP Protocol
Json, Plain,Form 등등 다양한 데이터 텍스트 & 바이너리 데이터 평문 형태의 메세지

 

폴링방식은 클라이언트에서 지속적으로 요청을 보내다 보니 아무래도 네트워크 통신비용의 낭비가 어느 정도 있다고 판단했다. 
(사실 웹소켓과 SSE 둘다 HTTP의 폴링방식의 한계점을 보완하기 위해 등장한 기술이다.)

 

웹소켓과 SSE 두 가지 방법 중에서 사실 고민이 많이 되었다. 

real-time을 구현하기 위해서는 기존에 이미 적용되고 많이 사용된 web-socket 방식이 있었고, 
기술적으로 우리가 구현하고자 하는 정확한 유즈케이스는 SSE 였다.

SSE에 대해 정확하게 알아보자. 


SSE의 등장 배경

SSE, webSocket 등장 이전에는 클라이언트에서 서버의 데이터 변경을 확인하기 위해 주로 polling 방식을 사용했다. 

polling 방식이 되었을 때 어떠한 문제점이 존재하는가?

  • 비효율성 : polling은 불필요한 요청을 발생시켜 네트워크 트래픽을 증가시키고 서버 부하를 야기한다.
  • 지연 시간 : polling 간격이 길면 사용자는 새로운 데이터를 받는 데 지연 시간을 경험하게 된다.
  • 실시간성 부족 : polling은 실시간 데이터 스트리밍을 지원하지 못한다.

위와 같은 문제를 해결하기 위해 등장하게 되었다. 

SSE, WebSocket 모두 HTML5의 표준기술로 채택되었는데, 실시간 데이터 스트리밍, 낮은 네트워크 트래픽, 비동기 업데이트  등의 이유로 채택되었다. (이외에도 HTML5에는 WebRTC 또한 표준기술로 추가되었다.)

SSE, WebSocket 은 위의 단점을 해소시켜준다.  
SSE는 단방향 통신, WebSocket은 양방향 통신을 타깃으로 사용된다. 물론 Protocol을 차이점이 있지만 가장 기본적인 목적은 데이터의 흐름이 어떻게 이동하는가에 있다.


SSE vs WebSocket 
둘 모두 HTML5 표준기술로 채택되었는데 어떠한 기술을 어떻게 활용해야 할지 고민이 될 수 있다. 아래 표를 통해 다시 한번 확인해 보자

특징 SSE(Server-Sent-Event) WebSocket
통신방향 단방향  ( 서버 -> 클라이언트) 양방향 (서버 <-> 클라이언트)
프로토콜 HTTP WebSocket
(초기 연결은 HTTP 이후 websockt 전용 프로토콜 사용)
연결유지 지속적인 HTTP 연결 지속적인 WebSocket 연결
데이터 형식 텍스트( EventStream 형식) 텍스트 및 바이너리 데이터
재연결 자동 재연결 지원 클라이언트 측에서 재연결 로직 필요
브라우저 호환성 대부분의 현대 브라우저 지원, IE 지원 안됨 대부분의 현대 브라우저 지원
방화벽 / 프록시 통과 HTTP와 동일하여 대부분 문제 없음 
(HTTP 프로토콜을 사용하기 때문)
일부 방화벽/프록시에서 차단 가능
사용 사례 실시간 뉴스 피드, 주식 가격 업데이트, 알림 등 실시간 채팅, 게임, 주식거래 플랫폼 등
전송 효율성 서버에서 클라이언트로 효율적 전송 양방향 통신이 필요할떄 매우 효율적
복잡성 구현이 비교적 단순 구현이 복잡하며 클라이언트/서버 모두 처리필요


내가 구현하고자 하는 케이스는 소켓보다는 SSE에 보다 적합하다는 판단이 섰다.


 

그렇다면 간단하게 한번 구현해 보자.

func main() {
	http.HandleFunc("/sse", func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Access-Control-Allow-Origin", "*")
		w.Header().Set("Content-Type", "text/event-stream")
		w.Header().Set("Cache-Control", "no-cache")
		w.Header().Set("Connection", "keep-alive")

		for i := 0; i < 10; i++ {
			fmt.Fprintf(w, "data: %s\n\n", fmt.Sprintf("Event %d", i))
			time.Sleep(2 * time.Second)
			w.(http.Flusher).Flush()
		}
	})
	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		w.Write([]byte("Normal Request and Response \n"))
	})

	if err := http.ListenAndServe(":8080", nil); err != nil {
		log.Fatal("ListenAndServe: fail", err)
	}
}


구현은 생각보다 쉽다. 

Http의 요청을 For Loop를 통해 커넥션을 계속 들고 있어 주면 된다. 

이중 특이한 게 눈에 보이는데 바로 Content-Type이다. text/event-stream? 정말 생소하다.

Content-Type: text/event-stream은 서버에서 클라이언트에게 이벤트 형식으로 데이터를 스트리밍 하는 데 사용되는 MIME 유형이다.
데이터는 특정 형식을 따라야 한다. 

형식규칙
각 이벤트는 줄 바꿈("\n\n")으로 구분된다.
각 데이터는 data: 로 시작하며, 여러 줄의 데이터를 포함할 수 있다.
선택적으로 'id:', 'event:', 'retry:' 필드를 포함할 수 있다.

 

Curl 요청을 통해 비교를 해보자.

일반 HTTP REQ-RES SSE HTTP REQ - RES


SSE통신의 경우 응답헤더에 Transfer_Encdoing -> chunked라는 게 다른 부분이 있다. 
주로 서버가 응답을 생성하면서 데이터를 클라이언트로 전송해야 할 때, 전체 응답의 크기를 미리 알 수 없을 경우 사용된다.

 

즉 SSE의 경우 지속적으로 응답이 내려가기 때문에 응답값의 크기를 지정할 수 없기 때문에 위와 같이 데이터가 내려간다.

 


 

 

 

서버 적용 간의 문제점 

 

1. 우리 Web Server의 경우 timeout 미들웨어를 공용으로 사용하고 있다. 따라서 구현상의 w.(http.Flush). Flush()는 실행이 불가능했다. 

우선적으로 서버에서는 구현이 어떻게 되는지 확인해 보면 왜 함수 호출이 불가능한지 이해할 수 있다. 

SSE 핸들러를 구현하게 되면 무한루프를 통해 Request를 끊지 않고 이어간다. 

Go에서는 하나의 Request 요청은 하나의 고 루틴을 통해  작동한다. 하나의 고 루틴이 생성되어 Request는 추상화되어 미들웨어를 타면서 핸들러까지 도착하게 되는데 이중 timeout 미들웨어는 Flush() 의 기능없이 Requesst 객체로 추상화 되어 다음 미들웨어로 넘어가면서 Flush()가 수행되지 않는 오류가 있었다.

 

2. 현재 쿠버네티스 환경의 한 개의 파즈만 유저서버스에 할당되기 때문에 SSE유저의 연결을 메모리에서 관리하도록 작성했다. 
알람의 기능이 가야 하기 때문에 모든 유저에 대해 알람유무를 조회할 필요가 없기 때문에 유저의 커넥션을 서버 인메모리에서 관리되도록 작성했다. 
이는 파즈가 늘어날 시 문제가 될 것으로 판단되고, 파즈가 늘어나야 된다고 판단될 시 Redis의 Pub, Sub을 이용해 간단하게 구현할 것으로 판단된다.

Hub 구현 방식

더보기
type HubClient struct {
	db       *gorm.DB
	log      *zerolog.Logger
	mu       sync.Mutex
	users    map[string]*sseClient
	alarms   map[string]bool
	register chan *sseClient
}

func (client *HubClient) Register(userId string) {
	sseClient := &sseClient{
		userId: userId,
		data:   make(chan bool),
	}
	client.mu.Lock()
	client.users[userId] = sseClient
	client.mu.Unlock()

	client.register <- sseClient
}

func (client *HubClient) Unregister(userId string) {
	client.mu.Lock()
	user := client.users[userId]
	delete(client.users, user.userId)
	close(user.data)
	client.mu.Unlock()
}

func (client *HubClient) GetData(userId string) <-chan bool {
	return client.users[userId].data
}

func (client *HubClient) getAlarm(id string) bool {
	_, ok := client.alarms[id]
	if ok == true {
		return true
	}
	return false
}

func (client *HubClient) SetAlarm() error {
	var (
		data []tbAlarmMember
	)
	if err := client.db.WithContext(context.Background()).Table(alarmMemberTableName).
		Distinct("member_id").Where("is_read = ?", false).Find(&data).Error; err != nil {
		client.log.Err(err).Msgf("fail to find tb alarm members")
		return err
	}

	client.mu.Lock()
	clear(client.alarms)
	for _, alarm := range data {
		client.alarms[alarm.MemberId] = true
	}
	client.mu.Unlock()

	return nil
}

func (client *HubClient) Run() {
	defer client.log.Debug().Msgf("hub client exit")
	var (
		failCnt   = 0
		getAlarm  = time.NewTicker(3 * time.Second)
		sendAlarm = time.NewTicker(1 * time.Second)
	)

	for {
		select {
		case _ = <-getAlarm.C:
			if err := client.SetAlarm(); err != nil {
				failCnt++
				client.log.Err(err).Msgf("fail to set alarm fail count %d", failCnt)
			}
		case _ = <-sendAlarm.C:
			for _, user := range client.users {
				_, ok := client.alarms[user.userId]
				user.data <- ok
			}
		case user := <-client.register:
			user.data <- client.getAlarm(user.userId)
		}
	}
}

func NewHubClient(db *gorm.DB, log *zerolog.Logger) *HubClient {
	return &HubClient{
		db:       db,
		log:      log,
		mu:       sync.Mutex{},
		users:    make(map[string]*sseClient),
		alarms:   make(map[string]bool),
		register: make(chan *sseClient),
	}
}


위와 같이 구현했다. 받았던 리뷰로는 register 할 때 chan의 사이즈를 두어 비동기적으로 등록하는 게 좋지 않냐는 의견이 나왔으며, 
그렇게 구현되었을 시 유저의 커넥션이 종료되었을 때 chan 내부에서도 확인하여 제거해 주는 추가적인 로직이 필요할 것으로 보여, 추후 고도화 작업을 진행할 때 위에 제시된 리뷰를 적용하기로 했다.


'Go > ehco' 카테고리의 다른 글

리버스 프록시(echo,reverse proxy)  (0) 2023.08.08
Ehco Https 설정하기  (0) 2023.07.12

엔진엑스를 사용하지 않고 에코에서 는 리버스 프록시 미들웨어를 제공해 준다. 해당 기능을 이용해서 작성해 보자.

그전에 프록시 에 대해 보다 명확하게 하고 넘어가자.

 

프록시 란?

프록시란? 대리라는 의미로 주로 네트워크 상에서 통신이 불가능한 두 점 사이를 통신 가능하게 만들어 주는 중계기 역할을 의미한다.

그 외에도 프록시는 보안, 로드밸런싱, 캐싱 다양한 기능을 제공한다.

먼저 그중 자주 언급되는 2가지에 대해 알아보자.

1. 로드벨런싱 :  여러 서버 사이의 트래픽을 분산시켜 서비스의 안정성과 성능을 향상한다.

예를 들어 8080 8081 이렇게 두 개의 포트로 동일한 서버가 존재하고 있다면 프로시 서버에서는 하나의 요청을 통해 어디 포트로 요청할지
특정 알고리즘을 활용해 각 서버의 트래픽 부하를 분산시켜 주는 것을 생각하면 편하다.

2. 캐싱 : 자주 요청되는 데이터 또는 특정 데이터들을 프록시 서버에 보관해 리소스를 효율적으로 사용하며 성능을 향상한다.

외부서버 와 데이터 통신을 줄여 네트워크의 병목현상을 방지하는 효과도 얻을 수 있게 된다.

 

(위 2가지의 기능을 본다면 어떤 특정 소프트웨어가 떠올라야 한다.)

 

이러한 프록시에는 2가지 종류가 있다. 여기서 보안의 목적이 나온다.

 

1. 포워드 프록시

포워드 프록시는 그림에서 보는것처럼 요청자,클라이언트 들은 인터넷에 직접 요청을 하는것이 아닌 프록시 서버가 요청을 받아 해당 인터넷 요청 결과를 전달해주는 것이다.

딱 봐도 저 박스 안에 갇혀있는 유저들을 관리하기 쉽다. 정해진 사이트의 요청만 가능하도록 제한하기 용이하며, 요청이 한 군데로 모여 결국 프록시 서버와 인터넷 이 통신을 하기때문에 비용절감에 탁월하다. 이에 기업환경에서 많이 사용된다.

 

2.  리버스 프록시

포워드 프록시와 다른점은 인터넷 과 프록시 서버의 위치가 변경되었다. 

이렇게 되면 클라이언트, 유저 들은 프록시에 연결되었다는 사실을 인지할 수 없으며 마치 요청을 최종 요청을 내부망에 보는 것과 같이 느끼게 된다. 이렇게 하는 이유는 보안이 주된 이유이다. 

내부망이 바로 인터넷과 통신하여도 문제는 없다 다면 내부망 혹은 내부 서비스를 제공하는 서버가 해킹당하거나, 털리는 경우 심각한 보안문제를 초래할 수 있다. 
그렇기에 리버스프록시를 설정하여 위와 같은 구조를 가져가게 된다.

 

두 개 중 한 개를 택해서 구현할 수도 있고 두 개를 복합적으로 선택해서 구현할 수도 있으니 이분법 적인 사고에 갇히지 말자.

 

개념을 알게 되었으니 이제 고에서 제공하는 리버스 프록시 미들웨어 구현을 바로 가보자.

func setupProxyGroup(e *echo.Echo, path string, url *url.URL, transport http.RoundTripper) {
	group := e.Group(path)
	group.Use(middleware.ProxyWithConfig(middleware.ProxyConfig{
		Balancer:  middleware.NewRoundRobinBalancer([]*middleware.ProxyTarget{{URL: url}}),
		Transport: transport,
	}))
}

func main() {
	flag.Parse()

	cfg, err := config.Init(*configFile)
	if err != nil {
		fmt.Printf("config init failure file(%s) : %s\n", *configFile, err)
		os.Exit(-1)
	}
	fmt.Println(cfg)
	utils.ProcessIgnoreSignal()

	e := echo.New()
	e.Use(middleware.CORSWithConfig(middleware.CORSConfig{
		AllowOrigins: []string{"*"},
		AllowHeaders: []string{"*"},
		AllowMethods: []string{"*"},
	}))

	e.Use(middleware.Recover())
	e.Use(middleware.LoggerWithConfig(middleware.LoggerConfig{
		Format: "[Proxy] ${status} ${method} ${host}${path} ${latency_human} ${time_rfc3339}" + "\n",
	}))

	transport := &http.Transport{
		TLSClientConfig: &tls.Config{
			InsecureSkipVerify: true,
		},
	}
	/**
	cms server
	*/
	cmsServerURL, err := url.Parse(cfg.Server.CmsServer)
	if err != nil {
		logrus.WithError(err).Errorf("cms server parse err %v", cfg.Server.CmsServer)
	}
    // 주소 생략

	/**
	api server
	*/
	apiServerURL, err := url.Parse(cfg.Server.ApiServer)
	if err != nil {
		logrus.WithError(err).Errorf("api server parse error %v", cfg.Server.ApiServer)
		os.Exit(-1)
	}
    // 주소 생략

	/**
	chat server
	*/
	chatServerURL, err := url.Parse(cfg.Server.ChatServer)
	if err != nil {
		logrus.WithError(err).Errorf("chat server parse err %v", cfg.Server.ChatServer)
	}
	setupProxyGroup(e, "/chat/live/:id", chatServerURL, transport)

	/**
	manager server
	*/
	managerServerURL, err := url.Parse(cfg.Server.ManagerServer)
	if err != nil {
		logrus.WithError(err).Errorf("chat server parse err %v", cfg.Server.ChatServer)
	}
	setupProxyGroup(e, "/chat", managerServerURL, transport)

	server := http.Server{
		Addr:    cfg.Server.Port,
		Handler: e,
		TLSConfig: &tls.Config{
			NextProtos: []string{acme.ALPNProto},
		},
	}
	log.Error(server.ListenAndServeTLS(cfg.Server.SSLCrt, cfg.Server.SSLKey))
}

리버스 프록시가 되어야 하기 때문에 CORS에 해당하는 모든 부분은 열어주었다. 왜? 내부서비스 가장 앞단에 위치해야 하기 때문이다.

중간에 보면 Recover와 Logger의 미들웨어를 추가적으로 사용했다.

우선 Recover는 http 프로토콜 통신간 어느 한 체인에서 패닉이 발생하더라도 리커버로 해당 패닉을 수습해 계속 서버를 유지하기 위해 작성했다.
실제 서비스에서 이러한 옵션은 버그를 양산할 수 있는 부분이 될 수 있으니 사용을 지양해야 한다. 
리버스프록시 서버의 목적에 맞게 옵션을 추가해서 사용하자.

 

프록시그룹 셋업 함수를 살펴보면 

func setupProxyGroup(e *echo.Echo, path string, url *url.URL, transport http.RoundTripper) {
	group := e.Group(path)
	group.Use(middleware.ProxyWithConfig(middleware.ProxyConfig{
		Balancer:  middleware.NewRoundRobinBalancer([]*middleware.ProxyTarget{{URL: url}}),
		Transport: transport,
	}))
}

이렇게 각 해당 서비스 별로 패스를 구분 지어 그룹 단위로 셋업을 진행했다. 
이중 ProxyCofig 구조체 생성간에 Balancer는 필수 값이다. 
위에서 언급한 것처럼 프록시에 는 로드밸런싱 기능이 있는데 에코에서는 이를 필수 값으로 지정해 타깃으로 정한다. 
작성된 코드는 오직 하나의 url을 이용하기 때문에 로드밸런싱의 기능은 전혀 활용하지 못하고 있다고 보면 된다.

 

Transport라는 생소한 부분이 보일 텐데 이는 Custom Tls 즉 사설 인증서를 사용하는 경우 필수 적이라고 명시되어 있다.

해당 포트 및 서버는 사설 인증을 받아 https를 테스트 용도의 목적으로 개설했기 때문에 옵션을 설정해주어야 한다.

 

모든 프록시는 동일한 http.TransPort를 받는데

	transport := &http.Transport{
		TLSClientConfig: &tls.Config{
			InsecureSkipVerify: true,
		},
	}

저기서 InsecureSkipVerify는 기본이 false 옵션이다. true 옵션을 추가하게 된다면  crypto/tls의 패키지에서는 아무 인증서나 패스해 주는 것을 의미한다. 따라서 이는 테스트 환경에서만 적용할 것을 권장한다고 주석 처리 되어 있으니 해당 옵션은 테스트 서버에서만 사용하자.

 

엔진엑스를 사용 못하고, 경량화된 프록시의 역할을 하는 무언가가 필요하다면  이렇게 에코에서 제공하는 리버스 프록시 서버를 작성해 보는 것도 좋은 방법 중에 하나가 될 수 있을 것 같다.

 

이외에도 에코 에는 구조체, 인터페이스 별로 주석이 정말 잘 달려있다. 무조건 함수 혹은 인터페이스 타고 들어가서 구현체 확인해 보자. 
구글도 알려주지 않는 것을 주석에서 알려주고 있다.

'Go > ehco' 카테고리의 다른 글

SSE 적용하기  (0) 2024.05.19
Ehco Https 설정하기  (0) 2023.07.12

1. 기본 설정은 다음과 같으며 http://localhost:8080/test 요청과 응답 결과이다.

(* 루트 권한이 없는 상태에서 설정하는 방법을 베이스로 합니다.)

 

2. 에코에서 제공하는 autoSSL 사용하기 (실패)

ssl 인증과 같은 과정을 거치지 않고 에코에서 제공하는 단순한 방법을 활용해서 작성하고자 했다.

func main() {
	e := echo.New()
	e.GET("/test", func(c echo.Context) error {
		return c.JSON(200, struct {
			Name    string `json:"name"`
			Message string `json:"message"`
		}{"testing", "From Server !"})
	})

	go func() {
		log.Error(e.StartAutoTLS(":8081"))
	}()

	log.Error(e.Start(":8080"))
}

기존 8080 은 http 요청으로 띄우고 https는 8081로 띄우고 싶었다. 기존 http 요청으로 사용하던 api들이 있을 수 있다는 생각에 위와 같이 작성 

 

포스트맨 응답
Error: write EPROTO 4818108888:error:10000438:SSL routines:OPENSSL_internal:TLSV1_ALERT_INTERNAL_ERROR:../../../../src/third_party/boringssl/src/ssl/tls_record.cc:594:SSL alert number 80

 

1. 인증서와 키 파일이 올바른지 확인하기: 인증서와 키 파일이 올바르게 생성되었고, 파일 경로와 이름이 올바르게 지정되었는지 확인하세요. 또한, 인증서가 CA에 의해 서명되었다면, 모든 중간 인증서가 올바르게 체인에 포함되어 있는지 확인해야 합니다.
2. SSL/TLS 설정이 올바른지 확인하기: 서버와 클라이언트가 모두 지원하는 SSL/TLS 버전을 사용하고 있는지 확인해야 합니다. 예를 들어, 클라이언트가 TLSv1.2만 지원하는데 서버가 TLSv1.3만 지원한다면, 이러한 종류의 오류가 발생할 수 있습니다.
3. 클라이언트의 이슈가 아닌지 확인하기: 오류가 서버의 문제가 아니라 클라이언트의 문제일 수도 있습니다. 다른 클라이언트에서 같은 요청을 시도해 보고, 같은 문제가 발생하는지 확인하세요.
4. 로깅을 통해 추가적인 정보 수집: Go의 http 패키지는 기본적으로 SSL/TLS 에러에 대한 많은 정보를 제공하지 않습니다. 따라서 net/http 패키지의 httptrace 패키지를 사용하여 추가적인 디버깅 정보를 수집하거나, OpenSSL의 s_client 도구를 사용하여 SSL/TLS 핸드셰이크를 수동으로 시도해 볼 수 있습니다.

검색 결과 위와 같은 방식의 검증이 필요한 것으로 보인다. AutoSSL 함수  호출을 열어보면 letsentcrypt.org로부터 인증서를 자동으로 받아와 세팅을 해준다. 

우리의 리눅스 서버에 해당 주소의 권한이 없어서 인증서 발급이 명확하게 되지 않아 실패한 것으로 보인다.

root 권한이 없기 때문에 이 방법은 패스하도록 한다.

 

3. cusotm SSL 설정

ssl을 돈 주고 사기 싫으니깐 개인적으로 생성한다.

openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem -days 365 -nodes

X.509 인증서를 발급을 openssl에 요청하는 커맨드이다. key.pem 키 파일로 , cert.pem 은 인증서 파일로 365일의 유효기간을 가지고 있으며, 개인키는 암호화되지 않음을 의미한다.

 

명령어를 입력하면 위와 같은 커맨드 내용이 보인다. Common name에 실제 도메인을 적어주면 된다.

위와 같은 명령어를 실행하면 key.pem, cert.pem 파일을 각각 준다

scp -P "key 파일경로" "cert 파일경로" 유저@도메인:/옮기고자 하는 디렉토리

코드 전문

func main() {
	e := echo.New()
	e.GET("/test", func(c echo.Context) error {
		return c.JSON(200, struct {
			Name    string `json:"name"`
			Message string `json:"message"`
		}{"testing", "From Server !"})
	})

	go func() {
		s := http.Server{
			Addr:    ":9087",
			Handler: e,
			TLSConfig: &tls.Config{
				NextProtos: []string{acme.ALPNProto},
			},
		}
		log.Error(s.ListenAndServeTLS("cert.pem 파일경로", "key.pem 파일경로"))
	}()

	log.Error(e.Start(":9086"))
}

Server를 직접 구조체를 만드는 데 핸들러에 echo 프레임 워크를 주입해서 만들어 주면 된다.

 

빌드해주고 서버로 날려주자

env GOOS=linux GOARCH=amd64 go build -o guiwoo_test

 

위에 보이는 것처럼 두 개의 요청 모두 성공한다.

 

 

4. 미들웨어로 열어놓고 리다이렉트 하기

func main() {
	e := echo.New()
	e.GET("/test", func(c echo.Context) error {
		return c.JSON(200, struct {
			Name    string `json:"name"`
			Message string `json:"message"`
		}{"testing", "From Server !"})
	})

	e.Pre(func(next echo.HandlerFunc) echo.HandlerFunc {
		return func(c echo.Context) error {
			if c.Request().TLS != nil {
				redirectURL := "http://" + "localhost:9086" + c.Request().URL.String()
				return c.Redirect(http.StatusMovedPermanently, redirectURL)
			}
			return next(c)
		}
	})

	go func() {
		s := http.Server{
			Addr:    ":9087",
			Handler: e,
			TLSConfig: &tls.Config{
				NextProtos: []string{acme.ALPNProto},
			},
		}
		log.Error(s.ListenAndServeTLS("/Users/guiwoopark/Documents/cert.pem", "/Users/guiwoopark/Documents/key.pem"))
	}()

	log.Error(e.Start(":9086"))
}

9087 https로 요청온 request를 낚아서 HTTP 포트로 리다이렉트 하는 방법이다.

nginx 도 이런 비슷한 방식을 사용하니 nginx 방법도 고려해 보자.

 

'Go > ehco' 카테고리의 다른 글

SSE 적용하기  (0) 2024.05.19
리버스 프록시(echo,reverse proxy)  (0) 2023.08.08

+ Recent posts