| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | 5 | 6 | 7 |
| 8 | 9 | 10 | 11 | 12 | 13 | 14 |
| 15 | 16 | 17 | 18 | 19 | 20 | 21 |
| 22 | 23 | 24 | 25 | 26 | 27 | 28 |
| 29 | 30 | 31 |
Tags
- Vue.js
- cli
- Refactoring
- todo-list
- component
- Matrix
- channel
- websocket
- goroutine
- PROPS
- method
- Server
- CDN
- URL
- SFC
- goroutines
- TODO
- Dictionary
- map
- 행렬
- golang
- reactivity
- graceful shutdown
- container
- localStorage
- Vue
- App.vue
- go
- emit
- toggle
Archives
- Today
- Total
ksundev 님의 블로그
[WebSocket] 채팅방 데모버전 만들어보기 (backend) 본문
카테고리로 분류되는 채팅방과 각 채팅방에 접속한 유저들과 채팅으로 소통할 수 있는 앱을 만들어보았습니다.
프론트엔드 코드는 이 게시글에 없고, 백엔드 코드만 작성되었습니다.
이후 프론트엔드 코드까지 업로드하여 링크 달아놓겠습니다!
전체 코드
package main
import (
"fmt"
"net/http"
"strings"
"time"
"github.com/gorilla/websocket"
)
type Message struct {
Username string `json:"username"`
Content string `json:"content"`
Room string `json:"room"`
Timestamp time.Time `json:"timestamp"`
}
type Client struct {
Username string
Room string
Conn *websocket.Conn
Send chan Message
}
type Hub struct {
Clients map[*Client]bool
Broadcast chan Message
Register chan *Client
Unregister chan *Client
}
var upgrader = websocket.Upgrader{
CheckOrigin: func(r *http.Request) bool {
return true
},
}
func NewHub() *Hub {
return &Hub{
Clients: make(map[*Client]bool),
Broadcast: make(chan Message),
Register: make(chan *Client),
Unregister: make(chan *Client),
}
}
func (h *Hub) Run() {
for {
select {
case client := <-h.Register:
h.Clients[client] = true
fmt.Printf("✅ [%s] %s님이 입장했습니다 (총 %d명)\n",
client.Room, client.Username, len(h.Clients))
case client := <-h.Unregister:
if _, ok := h.Clients[client]; ok {
delete(h.Clients, client)
close(client.Send)
fmt.Printf("❌ [%s] %s님이 퇴장했습니다 (총 %d명)\n",
client.Room, client.Username, len(h.Clients))
}
case message := <-h.Broadcast:
fmt.Printf("💬 [%s] %s: %s\n",
message.Room, message.Username, message.Content)
for client := range h.Clients {
// 같은 방에 있는 클라이언트에게만 전송
if client.Room == message.Room {
select {
case client.Send <- message:
default:
delete(h.Clients, client)
close(client.Send)
}
}
}
}
}
}
func (c *Client) ReadPump(hub *Hub) {
defer func() {
hub.Unregister <- c
c.Conn.Close()
}()
for {
var msg Message
err := c.Conn.ReadJSON(&msg)
if err != nil {
break
}
msg.Username = c.Username
msg.Room = c.Room
msg.Timestamp = time.Now()
hub.Broadcast <- msg
}
}
func (c *Client) WritePump() {
defer c.Conn.Close()
for {
select {
case message, ok := <-c.Send:
if !ok {
c.Conn.WriteMessage(websocket.CloseMessage, []byte{})
return
}
c.Conn.WriteJSON(message)
}
}
}
func handleWebSocket(hub *Hub, w http.ResponseWriter, r *http.Request) {
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
return
}
username := r.URL.Query().Get("username")
if username == "" {
username = fmt.Sprintf("User_%d", time.Now().Unix()%1000)
}
room := r.URL.Query().Get("room")
if room == "" {
room = "general"
}
client := &Client{
Username: username,
Room: room,
Conn: conn,
Send: make(chan Message, 256),
}
fmt.Printf("🔌 연결 시도: %s -> %s방\n", username, room)
hub.Register <- client
go client.WritePump()
go client.ReadPump(hub)
}
func main() {
hub := NewHub()
go hub.Run()
http.HandleFunc("/ws", func(w http.ResponseWriter, r *http.Request) {
handleWebSocket(hub, w, r)
})
fmt.Println("🚀 멀티룸 WebSocket 채팅 서버 시작!")
fmt.Println("📡 연결 URL: ws://localhost:8080/ws?username=이름&room=방이름")
fmt.Println("📋 예시: ws://localhost:8080/ws?username=홍길동&room=개발")
fmt.Println(strings.Repeat("=", 30))
http.ListenAndServe(":8080", nil)
}
main.go 코드 상세 설명
1️⃣ 패키지 및 임포트 (1-8줄)
package main
import (
"fmt" // 출력 함수 (Printf, Println 등)
"net/http" // HTTP 서버 기능
"strings" // 문자열 처리
"time" // 시간 관련 기능
"github.com/gorilla/websocket" // WebSocket 라이브러리
)
2️⃣ Message 구조체 (10-15줄)
type Message struct {
Username string `json:"username"` // 메시지 작성자
Content string `json:"content"` // 메시지 내용
Room string `json:"room"` // 채팅방 이름
Timestamp time.Time `json:"timestamp"` // 메시지 작성 시간
}
- JSON 태그로 클라이언트와 데이터 교환 시 필드명 지정
3️⃣ Client 구조체 (17-22줄)
type Client struct {
Username string // 클라이언트 사용자명
Room string // 현재 접속한 방
Conn *websocket.Conn // WebSocket 연결 객체
Send chan Message // 메시지 수신용 채널 (버퍼 256)
}
4️⃣ Hub 구조체 (24-29줄)
type Hub struct {
Clients map[*Client]bool // 연결된 모든 클라이언트 목록
Broadcast chan Message // 모든 클라이언트에게 전송할 메시지
Register chan *Client // 새 클라이언트 등록 요청
Unregister chan *Client // 클라이언트 등록 해제 요청
}
- 중앙 제어소: 모든 클라이언트와 메시지를 관리
5️⃣ WebSocket 업그레이더 (31-35줄)
var upgrader = websocket.Upgrader{
CheckOrigin: func(r *http.Request) bool {
return true // 모든 Origin 허용 (개발용)
},
}
- HTTP 연결을 WebSocket으로 업그레이드
CheckOrigin: true는 보안상 위험하지만 개발 편의를 위해 사용
6️⃣ NewHub 함수 (37-43줄)
func NewHub() *Hub {
return &Hub{
Clients: make(map[*Client]bool), // 빈 맵 생성
Broadcast: make(chan Message), // 메시지 채널 생성
Register: make(chan *Client), // 등록 채널 생성
Unregister: make(chan *Client), // 해제 채널 생성
}
}
7️⃣ Hub.Run 메서드 (45-75줄)
func (h *Hub) Run() {
for { // 무한 루프로 이벤트 대기
select {
case client := <-h.Register: // 새 클라이언트 등록
h.Clients[client] = true
fmt.Printf("✅ [%s] %s님이 입장했습니다 (총 %d명)\n",
client.Room, client.Username, len(h.Clients))
case client := <-h.Unregister: // 클라이언트 등록 해제
if _, ok := h.Clients[client]; ok {
delete(h.Clients, client) // 맵에서 제거
close(client.Send) // 채널 닫기
fmt.Printf("❌ [%s] %s님이 퇴장했습니다 (총 %d명)\n",
client.Room, client.Username, len(h.Clients))
}
case message := <-h.Broadcast: // 메시지 브로드캐스트
fmt.Printf("💬 [%s] %s: %s\n",
message.Room, message.Username, message.Content)
for client := range h.Clients {
if client.Room == message.Room { // 같은 방 사용자에게만
select {
case client.Send <- message: // 메시지 전송
default: // 전송 실패 시
delete(h.Clients, client) // 클라이언트 제거
close(client.Send)
}
}
}
}
}
}
8️⃣ Client.ReadPump 메서드 (77-95줄)
func (c *Client) ReadPump(hub *Hub) {
defer func() { // 함수 종료 시 실행
hub.Unregister <- c // Hub에서 제거
c.Conn.Close() // 연결 종료
}()
for {
var msg Message
err := c.Conn.ReadJSON(&msg) // 클라이언트로부터 JSON 메시지 읽기
if err != nil {
break // 오류 시 루프 종료
}
msg.Username = c.Username // 사용자명 설정
msg.Room = c.Room // 방 이름 설정
msg.Timestamp = time.Now() // 현재 시간 설정
hub.Broadcast <- msg // Hub로 메시지 전송
}
}
9️⃣ Client.WritePump 메서드 (97-109줄)
func (c *Client) WritePump() {
defer c.Conn.Close()
for {
select {
case message, ok := <-c.Send: // Hub로부터 메시지 수신
if !ok { // 채널이 닫혔으면
c.Conn.WriteMessage(websocket.CloseMessage, []byte{})
return
}
c.Conn.WriteJSON(message) // 클라이언트에게 JSON 메시지 전송
}
}
}
🔟 handleWebSocket 함수 (111-139줄)
func handleWebSocket(hub *Hub, w http.ResponseWriter, r *http.Request) {
conn, err := upgrader.Upgrade(w, r, nil) // HTTP → WebSocket 업그레이드
if err != nil {
return
}
username := r.URL.Query().Get("username") // URL에서 사용자명 추출
if username == "" {
username = fmt.Sprintf("User_%d", time.Now().Unix()%1000) // 기본값
}
room := r.URL.Query().Get("room") // URL에서 방 이름 추출
if room == "" {
room = "general" // 기본 방
}
client := &Client{ // 새 클라이언트 생성
Username: username,
Room: room,
Conn: conn,
Send: make(chan Message, 256), // 256개 메시지 버퍼
}
fmt.Printf("연결 시도: %s -> %s방\n", username, room)
hub.Register <- client // Hub에 클라이언트 등록
go client.WritePump() // 고루틴으로 메시지 전송 시작
go client.ReadPump(hub) // 고루틴으로 메시지 수신 시작
}
1️⃣1️⃣ main 함수 (141-162줄)
func main() {
hub := NewHub() // Hub 생성
go hub.Run() // 고루틴으로 Hub 실행
http.HandleFunc("/ws", func(w http.ResponseWriter, r *http.Request) {
handleWebSocket(hub, w, r) // /ws 경로로 WebSocket 연결 처리
})
fmt.Println("멀티룸 WebSocket 채팅 서버 시작!")
fmt.Println("연결 URL: ws://localhost:8080/ws?username=이름&room=방이름")
fmt.Println("예시: ws://localhost:8080/ws?username=홍길동&room=개발")
fmt.Println(strings.Repeat("=", 30))
http.ListenAndServe(":8080", nil) // 8080 포트에서 서버 시작
}
전체 동작 흐름 요약:
- 서버 시작 → Hub 생성 및 실행
- 클라이언트 연결 → HTTP → WebSocket 업그레이드
- 클라이언트 등록 → Hub에 추가
- 메시지 송수신 → ReadPump(송신) + WritePump(수신)
- 방별 메시지 전송 → 같은 방 사용자에게만 브로드캐스트
- 연결 종료 → 클라이언트 제거 및 정리
'[개발] Go > 중급 프로젝트' 카테고리의 다른 글
| [URL Checker][New] Go Routine을 장착한 빠른 URL Checker🔥 (1) | 2025.07.14 |
|---|---|
| Channels 2 - chan 타입 바꾸고 반복문 활용하기 (1) | 2025.07.14 |
| Channels (0) | 2025.06.27 |
| Goroutines (0) | 2025.06.27 |
| [URL Checker] hitURL 그리고 느린 URL Checker (순차적) (0) | 2025.06.26 |