Search
Duplicate
📒

[Network Study] 04-x. SSE, WebSocket, Web Push

수업
Network Study
주제
HTTP
5 more properties
참고

1. 목차

NOTE

Server-sent events(SSE)

NOTE
SSE는 HTML5의 기능 중 하나이며, 기술적으로는 HTTP/1.1의 청크 형식을 이용한 통신 기능을 바탕으로 합니다. 웹소켓과 유사하지만, 단방향 통신(서버 → 클라이언트)만을 지원한다는 차이가 있습니다.
SSE는 HTTP 프로토콜을 사용하여 서버 → 클라이언트 방향으로 실시간 데이터를 푸시할 수 있는 기술입니다. 청크 전송 방식을 사용하여 클라이언트가 1번 연결을 열면, 서버가 지속적으로 데이터를 전송합니다.
1.
클라이언트는 서버에 HTTP Request을 보낸다.
2.
서버는 클라이언트에 Content-Typetext/event-stream으로 설정한뒤 Response를 보낸다.
3.
서버는 연결이 열려있는 동안 클라이언트에 이벤트를 전송합니다.
4.
클라이언트는 서버로부터 전송된 이벤트를 수신하고 처리합니다.

서버 구현

서버에서는 HTTP 헤더에 Content-Typetext/event-stream으로 설정하고, 이 포맷에 맞춰 데이터를 전송합니다.
package main import ( "fmt" "log" "net/http" "time" ) func sseHandler(w http.ResponseWriter, r *http.Request) { // CORS 헤더 설정 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") // 클라이언트 연결이 끊길 때를 감지 notify := w.(http.CloseNotifier).CloseNotify() go func() { <-notify log.Println("Client disconnected") }() // 이벤트 ID 설정 id := time.Now().Format("15:04:05") // 주기적으로 데이터를 클라이언트에 전송 for { select { case <-time.After(1 * time.Second): fmt.Fprintf(w, "id: %s\n", id) fmt.Fprintf(w, "data: %s\n\n", time.Now().Format(time.RFC3339)) // 플러시하여 데이터 전송 if f, ok := w.(http.Flusher); ok { f.Flush() } } } } func main() { http.HandleFunc("/events", sseHandler) log.Println("SSE server running on port 3000") log.Fatal(http.ListenAndServe(":3000", nil)) }
JavaScript
복사
Cache-Control: no-cache: SSE 연결동안, 브라우저나 중간 프록시가 이벤트 스트림을 캐시하지 않도록 합니다.
SSE는 실시간 데이터 전송을 위해 사용하므로, 캐싱이 필요없습니다.
no-cache 헤더는 캐시없이 매번 최신데이터를 가져옵니다.
Connection: Keep-alive: HTTP 연결을 지속적으로 연결하여, 여러번의 요청을 하나의 연결로 처리하게 해준다.
SSE는 지속적인 연결을 필요로하며, 연결이 닫히면 자동으로 재연결을 시도합니다.
Go언어의 경우 기본적으로 Keep-alive를 지원하지만, 명시적으로 설정하기 위해 작성했습니다.

클라이언트 구현

웹 브라우저에서는 EventSource 인터페이스를 사용하여 이 이벤트 스트림에 연결하고 데이터를 받습니다.
<!DOCTYPE html> <html> <head> <title>SSE Example</title> </head> <body> <h1>Server-Sent Events</h1> <ul id="eventList"></ul> <script> const evtSource = new EventSource("http://localhost:3000/events"); // 일반 메시지의 이벤트 핸들러 evtSource.onmessage = (e) => { const newElement = document.createElement("li"); newElement.innerHTML = "message: " + e.data; document.getElementById("eventList").appendChild(newElement); }; // 'ping' 이벤트 리스너 추가 evtSource.addEventListener("ping", (e) => { const newElement = document.createElement("li"); const obj = JSON.parse(e.data); newElement.innerHTML = "ping at " + obj.time; document.getElementById("eventList").appendChild(newElement); }, false); </script> </body> </html>
HTML
복사
1.
EventSource 객체 생성:
EventSource는 지정된 URL(ssedemo.php)에서 서버로부터 이벤트를 받기 위한 객체를 생성합니다.
이 객체는 서버에서 발송한 이벤트를 자동으로 수신합니다.
2.
일반 메시지 처리:
onmessage 핸들러는 특별한 이벤트 타입이 지정되지 않은 모든 메시지를 처리합니다.
수신된 데이터(e.data)를 사용하여 새로운 <li> 요소를 생성하고, 그 요소를 문서의 eventList라는 ID를 가진 요소에 추가합니다.
3.
특정 이벤트 처리 (ping):
addEventListener를 사용하여 ping 이벤트를 특별히 처리합니다.
ping 이벤트에 대한 데이터는 JSON 포맷으로 가정하며, 이를 파싱하여 시간 정보를 추출합니다.
추출된 시간 정보를 포함하는 새로운 <li> 요소를 생성하여 eventList에 추가합니다.

웹소켓

NOTE
웹소켓은 HTML5의 일부로, 클라이언트-서버간의 양방향 통신을 가능하게 해주는 프로토콜이며, HTTP를 통해 초기 핸드셰이크 이후 전용 프로토콜로 업그레이드 됩니다.
지속적인 연결로 양방향 통신이 가능!
웹소켓은 서버/클라이언트 사이에 오버헤드가 적은 양방향 통신을 실현합니다. 통신이 확립되면 서버/클라이언트 사이에서 1:1 통신을 수행합니다.
프레임 단위로 송수신하지만, 상대방이 정해져 있으므로 전송할 곳에 관한 정보는 가지지 않습니다. HTTP의 기본 요소중에서 바디만 보내는 것과 같습니다.

웹소켓 구현

go get github.com/gorilla/websocket
Bash
복사
package main import ( "fmt" "net/http" "github.com/gorilla/websocket" ) // 업그레이더 설정: HTTP 연결을 웹소켓으로 업그레이드하는 데 사용 var upgrader = websocket.Upgrader{ // 모든 도메인에서의 요청을 허용 CheckOrigin: func(r *http.Request) bool { return true }, } // 연결된 클라이언트들을 저장하기 위한 맵 var clients = make(map[*websocket.Conn]bool) // 메시지를 전달하기 위한 채널 var broadcast = make(chan Message) // 메시지 구조체 type Message struct { Username string `json:"username"` Message string `json:"message"` } func main() { // 파일 서버 설정: 현재 디렉토리의 파일을 제공 fs := http.FileServer(http.Dir("./")) http.Handle("/", fs) // 웹소켓 핸들러 설정 http.HandleFunc("/ws", handleConnections) // 메시지 처리 고루틴 시작 go handleMessages() // 서버 시작 fmt.Println("HTTP server started on :8080") err := http.ListenAndServe(":8080", nil) if err != nil { fmt.Println("ListenAndServe: ", err) } } // 웹소켓 연결을 처리하는 함수 func handleConnections(w http.ResponseWriter, r *http.Request) { // HTTP 요청을 웹소켓 연결로 업그레이드 ws, err := upgrader.Upgrade(w, r, nil) if err != nil { fmt.Println(err) return } defer ws.Close() // 새로운 클라이언트를 맵에 추가 clients[ws] = true // 무한 루프: 클라이언트로부터 메시지를 읽고 브로드캐스트 채널로 전달 for { var msg Message err := ws.ReadJSON(&msg) if err != nil { fmt.Println("error: ", err) // 에러 발생 시 클라이언트를 맵에서 제거 delete(clients, ws) break } broadcast <- msg } } // 메시지를 처리하고 모든 클라이언트에 전달하는 함수 func handleMessages() { for { // 브로드캐스트 채널에서 메시지 수신 msg := <-broadcast // 모든 클라이언트에 메시지 전송 for client := range clients { err := client.WriteJSON(msg) if err != nil { fmt.Println("error: ", err) client.Close() delete(clients, client) } } } }
Go
복사
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>WebSocket Chat</title> <style> body { font-family: Arial, sans-serif; } #chat { max-width: 600px; margin: 0 auto; } #messages { border: 1px solid #ccc; padding: 10px; height: 300px; overflow-y: scroll; } #input { display: flex; } #username { width: 100px; } #message { flex: 1; } </style> </head> <body> <div id="chat"> <h2>WebSocket Chat</h2> <div id="messages"></div> <div id="input"> <input type="text" id="username" placeholder="Username"> <input type="text" id="message" placeholder="Message"> <button onclick="sendMessage()">Send</button> </div> </div> <script> // 웹소켓 서버와의 연결 설정 const ws = new WebSocket('ws://localhost:8080/ws'); // 서버로부터 메시지를 수신했을 때 호출되는 함수 ws.onmessage = function(event) { const message = JSON.parse(event.data); const messages = document.getElementById('messages'); // 새로운 메시지를 채팅창에 추가 messages.innerHTML += `<p><strong>${message.username}</strong>: ${message.message}</p>`; // 스크롤을 아래로 이동 messages.scrollTop = messages.scrollHeight; }; // 메시지 전송 함수 function sendMessage() { const username = document.getElementById('username').value; const message = document.getElementById('message').value; if (username && message) { // 메시지를 JSON 형식으로 웹소켓을 통해 서버로 전송 ws.send(JSON.stringify({ username: username, message: message })); // 메시지 입력란 초기화 document.getElementById('message').value = ''; } } </script> </body> </html>
HTML
복사

HTTP 웹 푸시

NOTE
웹 푸시는 푸시 알람을 브라우저를 통해 사용자에게 전달하는 기술이며, 실시간 알림을 전송하는데 사용됩니다. 이는 사용자가 웹 애플리케이션을 활성화하지 않아도 브라우저를 통해 알람을 받습니다.
HTTP 웹 푸시는 서비스 워커(Service Worker)와 푸시 API를 활용하여 구현되며, 사용자에게 실시간 알림을 제공하여 사용자 경험을 향상시키는 데 중요한 역할을 합니다.
크롬이 이용하는 푸시 서비스는 GCM, FCM을 기반으로 합니다. 따라서 각각 애플리케이션을 등록해야하고 애플리케이션 별로 전용 키 및 URL이 제공됩니다.

주요 구성요소

서비스 워커
// addEventListener() 이벤트 핸들러를 등록해두면, 필요할 때만 실행되어 처리 self.addEventListener('push', function(event) { const data = event.data.json(); const options = { body: data.body, icon: '/images/icon.png', badge: '/images/badge.png' }; event.waitUntil( self.registration.showNotification(data.title, options) ); });
JavaScript
복사
백그라운드에서 실행되는 스크립트로, 푸시 알람을 수신/처리 할 수 있습니다.
웹 애플리케이션이 열려 있지 않더라도 푸시 알림을 수신할 수 있게 해줍니다.
각 서비스에서 서비스의 ID와 전송용 키를 가져옵니다. 수신할 땐 ID가 전송할 땐 ID와 키가 필요합니다.
푸시 API
서버가 브라우저 푸시 메시지를 전송할 수 있게 해주는 API입니다.
푸시 메시지는 서비스 워커로 전달되어 브라우저에서 알림을 표시합니다.
알림 API
푸시 메시지를 사용자에게 시각적으로 알리기 위해 사용됩니다.
브라우저에서 네이티브 알람을 표시할 수 있습니다.

목차

NOTE

1. 목차

NOTE

목차

NOTE