가장 빨리 만나는 Go 언어 Unit 67. 실전 예제: 채팅 서버 작성하기

저작권 안내
  • 책 또는 웹사이트의 내용을 복제하여 다른 곳에 게시하는 것을 금지합니다.
  • 책 또는 웹사이트의 내용을 발췌, 요약하여 강의 자료, 발표 자료, 블로그 포스팅 등으로 만드는 것을 금지합니다.

실전 예제: 채팅 서버 작성하기

이재홍 http://www.pyrasis.com 2014.12.17 ~ 2015.02.07

이번에는 Go 언어의 채널을 활용하여 채팅 서버와 사이트를 만들어보겠습니다. 완벽한 형태의 채팅 서버와 사이트를 만들려면 코드량이 많아지므로 예제에서는 다음 기능만 간략하게 구현하겠습니다.

  • 채팅 메시지 보내기 및 받기
  • 사용자 입장 및 퇴장 메시지 출력

일단 웹에서 채팅 메시지를 주고받으려면 웹 브라우저에서 사용할 수 있는 실시간 통신 프로토콜이 필요합니다. 이번 예제에서는 socket.io를 사용하여 실시간 채팅을 구현하겠습니다.

Go 언어로 채팅 서버 작성하기

채팅 서버에서 구조체 및 채널의 주요 부분을 그림으로 표현하면 다음과 같습니다.

그림 67-1 채팅 서버의 구조체와 채널

다음 내용을 GOPATH/src/chat/chat.go 파일로 저장합니다.

GOPATH/src/chat/chat.go
package main

import (
	"container/list"
	"log"
	"net/http"
	"time"

	"github.com/googollee/go-socket.io" // socket.io 패키지 사용
)

...

현재 Go 언어용 socket.io 패키지가 여러 개 나와있습니다. 그중 github.com/googollee/go-socket.io를 사용하겠습니다. 다음과 같이 import에서 패키지 주소를 지정해줍니다.

채팅 이벤트와 구독 구조체를 정의합니다. 여기서 구독(Subscription)은 채팅방에서 오고가는 메시지를 받겠다는 뜻입니다.

GOPATH/src/chat/chat.go
...

// 채팅 이벤트 구조체 정의
type Event struct {
	EvtType   string  // 이벤트 타입
	User      string  // 사용자 이름
	Timestamp int     // 시간 값
	Text      string  // 메시지 텍스트
}

// 구독 구조체 정의
type Subscription struct {
	Archive []Event       // 지금까지 쌓인 이벤트를 저장할 슬라이스
	New     <-chan Event  // 새 이벤트가 생길 때마다 데이터를 받을 수 있도록
	                      // 이벤트 채널 생성
}

...

Event 구조체에는 이벤트 타입, 사용자 이름, 시간 값, 메시지 텍스트가 들어갑니다. 사용할 수 있는 이벤트 타입은 다음과 같습니다.

  • join: 사용자가 채팅방에 들어왔을 때 이벤트입니다.
  • leave: 사용자가 채팅방에서 나갔을 때 이벤트입니다.
  • message: 채팅 메시지 이벤트입니다.

Subscription 구조체에는 사용자가 처음 채팅방에 들어왔을 때 지금까지 쌓인 이벤트를 받을 수 있도록 []Event처럼 Event 구조체를 슬라이스로 가지고 있습니다. 그리고 새 이벤트가 생길 때마다 해당 사용자가 데이터를 받을 수 있도록 Event 채널 New를 생성합니다. Event 채널은 데이터를 받기만 할 것이므로 New <-chan Event와 같이 받기 전용 채널로 정의합니다.

이벤트 생성 함수를 작성합니다.

GOPATH/src/chat/chat.go
...

// 이벤트 생성 함수
func NewEvent(evtType, user, msg string) Event {
	return Event{evtType, user, int(time.Now().Unix()), msg}
}

...

Event 구조체 인스턴스를 생성하여 리턴하며 현재 시간은 time.Now().Unix() 함수를 사용합니다.

이제 구독, 구독 해지, 이벤트 발행(Publish) 채널을 생성합니다.

GOPATH/src/chat/chat.go
...

var (
	subscribe   = make(chan (chan<- Subscription), 10) // 구독 채널
	unsubscribe = make(chan (<-chan Event), 10)        // 구독 해지 채널
	publish     = make(chan Event, 10)                 // 이벤트 발행 채널
)

...
  • subscribe: 새로운 사용자가 채팅방에 들어왔을 때 Subscription 채널을 전달하는 채널입니다. 여기서 실제 채팅 이벤트는 Subscription 구조체가 처리하며 이 subscribe 채널은 사용자의 Event 채널을 구독자 목록에 추가할 때 사용합니다. 이처럼 채널 안에 채널을 정의하면 채널 변수 자체를 주고 받을 수 있습니다. Subscription 채널은 보내기만 할 것이므로 chan<- Subscription과 같이 보내기 전용 채널로 정의합니다.
  • unsubscribe: 사용자가 채팅방에서 나갔을 때 이벤트 채널을 전달하는 채널이며 구독자 목록에서 Event 채널을 삭제할 때 사용합니다. 그리고 사용자가 나갔을 때 이벤트 채널에서 값을 모두 꺼낼 것입니다. 따라서 <-chan Event와 같이 받기 전용 채널로 정의합니다.
  • publish: 이벤트를 발행하는 채널입니다. publish 채널을 통해 전달된 이벤트는 구독자 목록의 모든 사용자에게 전달됩니다

subscribe, unsubscribe, publish는 버퍼가 10개인 채널로 생성했습니다. 버퍼가 있는 비동기 채널이나 버퍼가 없는 동기 채널이나 동작에는 큰 영향이 없습니다. 버퍼가 10개라 하더라도 10명이상의 사용자, 10개 이상의 이벤트를 처리할 수 있습니다.

새로운 사용자가 들어왔을 때 이벤트를 구독할 함수를 작성합니다.

GOPATH/src/chat/chat.go
...

// 새로운 사용자가 들어왔을 때 이벤트를 구독할 함수
func Subscribe() Subscription {
	c := make(chan Subscription) // 채널을 생성하여
	subscribe <- c               // 구독 채널에 보냄
	return <-c
}

...

Subscription 타입의 채널을 생성한 뒤 subscribe 채널에 보냅니다. 뒤에 나오는 Chatroom 함수에서 지금까지 쌓인 이벤트와 이벤트를 받을 수 있는 Event 채널을 생성한 뒤 Subscription 구조체를 c에 보낼 것입니다. 따라서 return <-c는 생성된 Subscription 구조체가 올 때까지 대기한 뒤 구조체를 꺼내서 리턴합니다.

사용자가 나갔을 때 구독을 취소할 함수를 작성합니다.

GOPATH/src/chat/chat.go
...

// 사용자가 나갔을 때 구독을 취소할 함수
func (s Subscription) Cancel() {
	unsubscribe <- s.New // 구독 해지 채널에 보냄

	for { // 무한 루프
		select {
		case _, ok := <-s.New: // 채널에서 값을 모두 꺼냄
			if !ok {           // 값을 모두 꺼냈으면 함수를 빠져나옴
				return
			}
		default:
			return
		}
	}
}

...

unsubscribe <- s.New처럼 이벤트 채널을 unsubscribe 채널로 보냅니다. 뒤에 나오는 Chatroom 함수에서 Event 채널을 구독자 목록에서 삭제할 것입니다. 그리고 사용자가 나갔기 때문에 이벤트들은 필요없어졌으므로 for 반복문을 실행하면서 s.New 채널에서 값을 모두 꺼냅니다. 채널에 값이 들어오지 않거나, 값을 모두 꺼냈으면 함수를 빠져나옵니다.

사용자가 들어올 때, 사용자가 나갔을 때, 채팅 메시지를 보냈을 때 이벤트를 발행할 함수를 작성합니다.

GOPATH/src/chat/chat.go
...

// 사용자가 들어왔을 때 이벤트 발행
func Join(user string) {
	publish <- NewEvent("join", user, "")
}

// 사용자가 채팅 메시지를 보냈을 때 이벤트 발행
func Say(user, message string) {
	publish <- NewEvent("message", user, message)
}

// 사용자가 나갔을 때 이벤트 발행
func Leave(user string) {
	publish <- NewEvent("leave", user, "")
}

...

NewEvent 함수로 join, leave, message 이벤트를 생성하여 publish 채널에 보냅니다. join, leave 이벤트는 단순히 사용자가 들어왔거나 나갔다는 것을 알려주기만 합니다.

이제 구독, 구독 해지, 발행된 이벤트를 처리할 Chatroom 함수입니다.

GOPATH/src/chat/chat.go
...

// 구독, 구독 해지, 발행 된 이벤트를 처리할 함수
func Chatroom() {
	archive := list.New()      // 쌓인 이벤트를 저장할 연결 리스트
	subscribers := list.New()  // 구독자 목록을 저장할 연결 리스트

	for {
		select {
		case c := <-subscribe: // 새로운 사용자가 들어왔을 때
			var events []Event

			for e := archive.Front(); e != nil; e = e.Next() { // 쌓인 이벤트가 있다면
				// events 슬라이스에 이벤트를 저장
				events = append(events, e.Value.(Event))
			}

			subscriber := make(chan Event, 10) // 이벤트 채널 생성
			subscribers.PushBack(subscriber)   // 이벤트 채널을 구독자 목록에 추가

			c <- Subscription{events, subscriber} // 구독 구조체 인스턴스를
                                                  // 생성하여 채널 c에 보냄

		case event := <-publish: // 새 이벤트가 발행되었을 때
			// 모든 사용자에게 이벤트 전달
			for e := subscribers.Front(); e != nil; e = e.Next() {
				// 구독자 목록에서 이벤트 채널을 꺼냄
				subscriber := e.Value.(chan Event)

				// 방금 받은 이벤트를 이벤트 채널에 보냄
				subscriber <- event
			}

			// 저장된 이벤트 개수가 20개가 넘으면
			if archive.Len() >= 20 {
				archive.Remove(archive.Front()) // 이벤트 삭제
			}
			archive.PushBack(event) // 현재 이벤트를 저장

		case c := <-unsubscribe: // 사용자가 나갔을 때
			for e := subscribers.Front(); e != nil; e = e.Next() {
				// 구독자 목록에서 이벤트 채널을 꺼냄
				subscriber := e.Value.(chan Event)

				// 구독자 목록에 들어있는 이벤트와 채널 c가 같으면
				if subscriber == c {
					subscribers.Remove(e) // 구독자 목록에서 삭제
					break
				}
			}
		}
	}
}

...

먼저 연결 리스트로 지금까지 쌓인 이벤트를 저장할 archive 변수와 구독자 목록(이벤트)을 저장할 subscribers 변수를 생성합니다.

archive := list.New()      // 쌓인 이벤트를 저장할 연결 리스트
subscribers := list.New()  // 구독자 목록을 저장할 연결 리스트

for 반복문에서 select case로 각 채널을 처리합니다. 먼저 새로운 사용자가 들어와서 subscribe 채널에 값이 들어왔을 때입니다.

case c := <-subscribe: // 새로운 사용자가 들어왔을 때
	var events []Event

	for e := archive.Front(); e != nil; e = e.Next() { // 쌓인 이벤트가 있다면
		// events 슬라이스에 이벤트를 저장
		events = append(events, e.Value.(Event))
	}

	subscriber := make(chan Event, 10) // 이벤트 채널 생성
	subscribers.PushBack(subscriber)   // 이벤트 채널을 구독자 목록에 추가

	c <- Subscription{events, subscriber} // 구독 구조체 인스턴스를 생성하여 채널 c에 보냄

subscribe 채널에 값이 들어왔다면 꺼내서 c에 저장합니다. 그리고 archive에 쌓인 이벤트가 있다면 events 슬라이스에 이벤트를 저장합니다. 그리고 발행된 이벤트를 받을 Event 채널을 생성하여 구독자 목록 subscribers에 추가합니다.

지금까지 쌓인 이벤트와 Event 채널이 준비되었다면 Subscription 구조체 인스턴스를 생성하여 채널 c에 보냅니다.

다음은 새 이벤트가 발행되었을 때 이벤트를 구독자 목록의 모든 Event 채널에 보냅니다.

case event := <-publish: // 새 이벤트가 발행되었을 때
	// 모든 사용자에게 이벤트 전달
	for e := subscribers.Front(); e != nil; e = e.Next() {
		// 구독자 목록에서 이벤트 채널을 꺼냄
		subscriber := e.Value.(chan Event)

		// 방금 받은 이벤트를 이벤트 채널에 보냄
		subscriber <- event
	}

	// 저장된 이벤트 개수가 20개가 넘으면
	if archive.Len() >= 20 {
		archive.Remove(archive.Front()) // 이벤트 삭제
	}
	archive.PushBack(event) // 현재 이벤트를 저장

publish 채널에 값이 들어왔다면 꺼내서 event에 저장합니다. 그리고 구독자 목록 subscribers에서 Event 채널을 모두 꺼낸 뒤 채널에 방금 받은 eventEvent 채널에 보냅니다. 이렇게하면 이벤트가 발생할 때마다 모든 사용자에게 이벤트를 전달해줄 수 있습니다.

발생한 이벤트를 무한정 저장할 수는 없으므로 archive 개수가 20개를 넘으면 처음 저장된 이벤트는 삭제합니다. 그리고 현재 이벤트를 archive에 저장합니다.

다음은 사용자가 나가서 구독이 취소되었을 때입니다.

case c := <-unsubscribe: // 사용자가 나갔을 때
	for e := subscribers.Front(); e != nil; e = e.Next() {
		subscriber := e.Value.(chan Event) // 구독자 목록에서 이벤트 채널을 꺼냄

		if subscriber == c {      // 구독자 목록에 들어있는 이벤트와 채널 c가 같으면
			subscribers.Remove(e) // 구독자 목록에서 삭제
			break
		}
	}
}

unsubscribe 채널에 값이 들어왔다면 꺼내서 c에 저장합니다. 그리고 현재 채널 c를 구독자 목록 subscribers에 들어있는 Event 채널과 비교해서 같으면 구독자 목록에서 삭제합니다.

이제 main 함수에서 socket.io와 앞에서 만든 함수를 사용하여 채팅 서버를 구현합니다.

GOPATH/src/chat/chat.go
...

func main() {
	server, err := socketio.NewServer(nil) // socker.io 초기화
	if err != nil {
		log.Fatal(err)
	}

	go Chatroom() // 채팅방을 처리할 함수를 고루틴으로 실행

	// 웹 브라우저에서 socket.io로 접속했을 때 실행할 콜백 설정
	server.On("connection", func(so socketio.Socket) {
		 // 웹 브라우저가 접속되면
		s := Subscribe() // 구독 처리
		Join(so.Id())    // 사용자가 채팅방에 들어왔다는 이벤트 발행

		for _, event := range s.Archive { // 지금까지 쌓인 이벤트를
			so.Emit("event", event)       // 웹 브라우저로 접속한 사용자에게 보냄
		}

		newMessages := make(chan string)

		// 웹 브라우저에서 보내오는 채팅 메시지를 받을 수 있도록 콜백 설정
		so.On("message", func(msg string) {
			newMessages <- msg
		})

		// 웹 브라우저의 접속이 끊어졌을 때 콜백 설정
		so.On("disconnection", func() {
			Leave(so.Id())
			s.Cancel()
		})

		go func() {
			for {
				select {
				case event := <-s.New:      // 채널에 이벤트가 들어오면
					so.Emit("event", event) // 이벤트 데이터를 웹 브라우저에 보냄
				case msg := <-newMessages:  // 웹 브라우저에서 채팅 메시지를 보내오면
					Say(so.Id(), msg)       // 채팅 메시지 이벤트 발행
				}
			}
		}()
	})

	http.Handle("/socket.io/", server)               // /socket.io/ 경로는 socket.io
                                                     // 인스턴스가 처리하도록 설정

	http.Handle("/", http.FileServer(http.Dir("."))) // 현재 디렉터리를 파일 서버로 설정

	http.ListenAndServe(":80", nil)                  // 80번 포트에서 웹 서버 실행
}

먼저 socket.io를 초기화합니다.

server, err := socketio.NewServer(nil) // socker.io 초기화
if err != nil {
	log.Fatal(err)
}

socketio.NewServer 함수는 매개변수에 polling, websocket 등의 통신 방식을 설정할 수 있는데 여기서는 nil을 설정하여 모든 통신 방식을 사용합니다.

채팅방을 처리할 Chatroom 함수를 고루틴으로 실행합니다.

go Chatroom() // 채팅방을 처리할 함수를 고루틴으로 실행

이제 웹 브라우저에서 socket.io로 접속했을 때 실행할 콜백(Callback)을 설정합니다.

server.On("connection", func(so socketio.Socket) {
... (생략)
}

socket.io 인스턴스에서 On 함수를 사용하여 각 상황마다 콜백 함수를 실행시킬 수 있습니다. 여기서는 첫 번째 매개변수에 connection을 지정하여 웹 브라우저에서 socket.io 서버에 접속했을 때 콜백 함수를 실행시킵니다.

웹 브라우저가 접속되면 먼저 구독 처리 및 사용자가 채팅방에 들어왔다는 이벤트를 발행합니다.

// 웹 브라우저가 접속되면
s := Subscribe() // 구독 처리
Join(so.Id())    // 사용자가 채팅방에 들어왔다는 이벤트 발행

Subscribe 함수로 Subscription 구조체를 생성합니다. 앞에서 설명했듯이 Subscription 구조체는 Chatroom 함수에서 생성되어 채널로 전달됩니다. 그리고 Join 함수를 사용하여 사용자가 방에 들어왔다는 이벤트를 발행합니다. 여기서 사용자 이름은 socket.io의 세션 ID를 사용하겠습니다(예제에서는 간단하게 socket.io의 세션 ID를 사용했지만 제대로 구현하려면 사용자 가입 및 로그인 처리가 필요합니다).

지금까지 쌓인 이벤트(이전 대화 내용 및 접속 기록)를 웹 브라우저로 접속한 사용자에게 보냅니다.

for _, event := range s.Archive { // 지금까지 쌓인 이벤트를
	so.Emit("event", event)       // 웹 브라우저로 접속한 사용자에게 보냄
}

Subscription 인스턴스의 Archive에 저장된 모든 이벤트를 웹 브라우저(클라이언트)에 보냅니다. socketio.Socket 인스턴스의 Emit 함수로 클라이언트에 메시지를 보낼 수 있으며 첫 번째 매개변수에는 socket.io 메시지 이름을 설정하고 두 번째 매개변수에는 데이터를 넣습니다. 여기서는 메시지 이름을 event로 설정하며 HTML의 JavaScript에서 socket.io 메시지를 받을 때 사용됩니다.

웹 브라우저에서 보내오는 채팅 메시지를 받을 수 있도록 콜백을 설정합니다.

newMessages := make(chan string)

// 웹 브라우저에서 보내오는 채팅 메시지를 받을 수 있도록 콜백 설정
so.On("message", func(msg string) {
	newMessages <- msg
})

On 함수에서 첫 번째 매개변수에 받을 socket.io 메시지 이름을 설정합니다. 여기서는 message로 설정합니다. 그리고 문자열 형식으로 newMessages 채널을 생성한 뒤 콜백이 실행되었을 때 채팅 메시지 문자열을 newMessages 채널에 보냅니다.

이번에는 웹 브라우저의 접속이 끊어졌을 때 콜백을 설정합니다.

// 웹 브라우저의 접속이 끊어졌을 때 콜백 설정
so.On("disconnection", func() {
	Leave(so.Id())
	s.Cancel()
})

On 함수에 disconnection를 지정하면 접속이 끊어졌을 때 콜백을 실행시킬 수 있습니다. 여기서는 콜백이 실행되었을 때 Leave 함수로 사용자가 나갔다는 이벤트를 발행하고, Cancel 함수로 이벤트 구독을 취소합니다.

이제 새 이벤트가 발생하면 Subscription 구조체의 New 채널에 값이 들어옵니다(Chatroom 함수의 case event := <-publish: 부분에서 값을 보냅니다). 그리고 앞에서 생성한 newMessages 채널도 처리해줍니다.

go func() {
	for {
		select {
		case event := <-s.New:      // 채널에 이벤트가 들어오면
			so.Emit("event", event) // 이벤트 데이터를 웹 브라우저에 보냄
		case msg := <-newMessages:  // 웹 브라우저에서 채팅 메시지를 보내오면
			Say(so.Id(), msg)       // 채팅 메시지 이벤트 발행
		}
	}
}()

고루틴을 실행한 뒤 for 반복문에서 select case로 각 채널에 값이 들어왔을 때 처리를 해줍니다. s.New 채널에 이벤트가 들어오면 Emit 함수로 이벤트 데이터를 웹 브라우저에 보냅니다. 그리고 newMessages 채널에 값이 들어오면 Say 함수를 실행하여 이벤트를 발행합니다. 여기서도 사용자 이름은 socket.io의 세션 ID를 사용하겠습니다.

마지막으로 socket.io 핸들러와 파일 서버 핸들러를 설정한 뒤 웹 서버를 실행합니다.

http.Handle("/socket.io/", server)  // /socket.io/ 경로는
                                    // socket.io 인스턴스가 처리하도록 설정

http.Handle("/", http.FileServer(http.Dir("."))) // 현재 디렉터리를 파일 서버로 설정

http.ListenAndServe(":80", nil)                  // 80번 포트에서 웹 서버 실행

http.Handle 함수에서 /socket.io/ 경로는 socket.io 인스턴스가 처리하도록 두 번째 매개변수에 server를 넣습니다. 그리고 http.Handle 함수에서 / 경로는 index.html 파일을 보여줄 수 있도록 파일 서버로 설정합니다. 파일 서버는 **http.Dir(".")**처럼 현재 디렉터리를 설정합니다.

http.ListenAndServe 함수에 포트 번호를 지정하여 웹 서버를 실행합니다. 방금 핸들러를 설정했으므로 두 번째 매개변수는 nil로 설정합니다.

다음은 전체 소스 코드입니다.

GOPATH/src/chat/chat.go
package main

import (
	"container/list"
	"log"
	"net/http"
	"time"

	"github.com/googollee/go-socket.io" // socket.io 패키지 사용
)

var (
	subscribe   = make(chan (chan<- Subscription), 10) // 구독 채널
	unsubscribe = make(chan (<-chan Event), 10)        // 구독 해지 채널
	publish     = make(chan Event, 10)                 // 이벤트 발행 채널
)

// 채팅 이벤트 구조체 정의
type Event struct {
	EvtType   string  // 이벤트 타입
	User      string  // 사용자 이름
	Timestamp int     // 시간 값
	Text      string  // 메시지 텍스트
}

// 구독 구조체 정의
type Subscription struct {
	Archive []Event       // 지금까지 쌓인 이벤트를 저장할 슬라이스
	New     <-chan Event  // 새 이벤트가 생길 때마다 데이터를 받을 수 있도록
                          // 이벤트 채널 생성
}

// 이벤트 생성 함수
func NewEvent(evtType, user, msg string) Event {
	return Event{evtType, user, int(time.Now().Unix()), msg}
}

// 새로운 사용자가 들어왔을 때 이벤트를 구독할 함수
func Subscribe() Subscription {
	c := make(chan Subscription) // 채널을 생성하여
	subscribe <- c               // 구독 채널에 보냄
	return <-c
}

// 사용자가 나갔을 때 구독을 취소할 함수
func (s Subscription) Cancel() {
	unsubscribe <- s.New // 구독 해지 채널에 보냄

	for { // 무한 루프
		select {
		case _, ok := <-s.New: // 채널에서 값을 모두 꺼냄
			if !ok {           // 값을 모두 꺼냈으면 함수를 빠져나옴
				return
			}
		default:
			return
		}
	}
}

// 사용자가 들어왔을 때 이벤트 발행
func Join(user string) {
	publish <- NewEvent("join", user, "")
}

// 사용자가 채팅 메시지를 보냈을 때 이벤트 발행
func Say(user, message string) {
	publish <- NewEvent("message", user, message)
}

// 사용자가 나갔을 때 이벤트 발행
func Leave(user string) {
	publish <- NewEvent("leave", user, "")
}

// 구독, 구독 해지, 발행 된 이벤트를 처리할 함수
func Chatroom() {
	archive := list.New()      // 쌓인 이벤트를 저장할 연결 리스트
	subscribers := list.New()  // 구독자 목록을 저장할 연결 리스트

	for {
		select {
		case c := <-subscribe: // 새로운 사용자가 들어왔을 때
			var events []Event

			// 쌓인 이벤트가 있다면
			for e := archive.Front(); e != nil; e = e.Next() {
				// events 슬라이스에 이벤트를 저장
				events = append(events, e.Value.(Event))
			}

			subscriber := make(chan Event, 10) // 이벤트 채널 생성
			subscribers.PushBack(subscriber)   // 이벤트 채널을 구독자 목록에
                                               // 추가

			c <- Subscription{events, subscriber} // 구독 구조체 인스턴스를
                                                  // 생성하여 채널 c에 보냄

		case event := <-publish: // 새 이벤트가 발행되었을 때
			// 모든 사용자에게 이벤트 전달
			for e := subscribers.Front(); e != nil; e = e.Next() {
				// 구독자 목록에서 이벤트 채널을 꺼냄
				subscriber := e.Value.(chan Event)

				// 방금 받은 이벤트를 이벤트 채널에 보냄
				subscriber <- event
			}

			// 저장된 이벤트 개수가 20개가 넘으면
			if archive.Len() >= 20 {
				archive.Remove(archive.Front()) // 이벤트 삭제
			}
			archive.PushBack(event) // 현재 이벤트를 저장

		case c := <-unsubscribe: // 사용자가 나갔을 때
			for e := subscribers.Front(); e != nil; e = e.Next() {
				subscriber := e.Value.(chan Event) // 구독자 목록에서 이벤트 채널을 꺼냄

				if subscriber == c { // 구독자 목록에 들어있는 이벤트와 채널 c가 같으면
					subscribers.Remove(e) // 구독자 목록에서 삭제
					break
				}
			}
		}
	}
}

func main() {
	server, err := socketio.NewServer(nil) // socker.io 초기화
	if err != nil {
		log.Fatal(err)
	}

	go Chatroom() // 채팅방을 처리할 함수를 고루틴으로 실행

	// 웹 브라우저에서 socket.io로 접속했을 때 실행할 콜백 설정
	server.On("connection", func(so socketio.Socket) {
		// 웹 브라우저가 접속되면
		s := Subscribe() // 구독 처리
		Join(so.Id())    // 사용자가 채팅방에 들어왔다는 이벤트 발행

		for _, event := range s.Archive { // 지금까지 쌓인 이벤트를
			so.Emit("event", event)       // 웹 브라우저로 접속한 사용자에게 보냄
		}

		newMessages := make(chan string)

		// 웹 브라우저에서 보내오는 채팅 메시지를 받을 수 있도록 콜백 설정
		so.On("message", func(msg string) {
			newMessages <- msg
		})

		// 웹 브라우저의 접속이 끊어졌을 때 콜백 설정
		so.On("disconnection", func() {
			Leave(so.Id())
			s.Cancel()
		})

		go func() {
			for {
				select {
				case event := <-s.New:      // 채널에 이벤트가 들어오면
					so.Emit("event", event) // 이벤트 데이터를 웹 브라우저에 보냄

				case msg := <-newMessages:  // 웹 브라우저에서 채팅 메시지를 보내오면
					Say(so.Id(), msg)       // 채팅 메시지 이벤트 발행
				}
			}
		}()
	})

	http.Handle("/socket.io/", server)  // /socket.io/ 경로는
                                        // socket.io 인스턴스가 처리하도록 설정

	http.Handle("/", http.FileServer(http.Dir("."))) // 현재 디렉터리를 파일 서버로 설정

	http.ListenAndServe(":80", nil)                  // 80번 포트에서 웹 서버 실행
}

저작권 안내

이 웹사이트에 게시된 모든 글의 무단 복제 및 도용을 금지합니다.
  • 블로그, 게시판 등에 퍼가는 것을 금지합니다.
  • 비공개 포스트에 퍼가는 것을 금지합니다.
  • 글 내용, 그림을 발췌 및 요약하는 것을 금지합니다.
  • 링크 및 SNS 공유는 허용합니다.

Published

2015-06-01