๐Ÿ”ฅ ์ฑ„๋„ Channel & ์›น์†Œ์ผ“ Websocket (feat.django tutorial)

yeeun leeยท2020๋…„ 5์›” 11์ผ
12

2์ฐจ ํ”„๋กœ์ ํŠธ์—์„œ ์•”ํ˜ธํ™”ํ ๊ฑฐ๋ž˜์†Œ ํด๋ก ์„ ํ•˜๊ฒŒ ๋˜์–ด, ๊ณ„์†์ ์œผ๋กœ ๋“ค์–ด์˜ค๋Š” ๋ฐ์ดํ„ฐ๋ฅผ getํ•  ์ˆ˜ ์žˆ๋Š” api๋ฅผ ๊ตฌํ˜„ํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ์ฐพ๊ณ  ์žˆ๋‹ค. ๊ธฐ์ˆ  ์กฐ์‚ฌ ๊ณผ์ •์—์„œ channel, websocket์— ๋Œ€ํ•œ ๋‚ด์šฉ์„ ์ •๋ฆฌํ•˜๋ ค๊ณ  ํ•œ๋‹ค.

ํ•ด๋‹น ๊ฒŒ์‹œ๋ฌผ์€ ์žฅ๊ณ  ๊ณต์‹ ๋ฌธ์„œ, ์žฅ๊ณ  ์ฑ„๋„ ํŠœํ† ๋ฆฌ์–ผ์„ ๋ฒˆ์—ญํ–ˆ๋‹ค.

1. django channel

์ฑ„๋„์€ django๊ฐ€ ๋น„๋™๊ธฐ์ asynchronous์œผ๋กœ ์ฝ”๋“œ๋ฅผ ์งค ์ˆ˜ ์žˆ๋„๋ก ๋ฐ”๊ฟ”์ฃผ๊ณ , django์˜ ๋™๊ธฐ์ ์ธ ํ•ต์‹ฌ์„synchronous core ํ†ตํ•ด ํ”„๋กœ์ ํŠธ๊ฐ€ HTTP๋ฟ ์•„๋‹ˆ๋ผ, ์žฅ๊ธฐ๊ฐ„ ์—ฐ๊ฒฐ์„ ํ•„์š”๋กœ ํ•˜๋Š” ํ”„๋กœํ† ์ฝœ Websockets, MQTT, chatbots, amateur radio ๋“ฑ์„ ํ•ธ๋“ค๋งํ•  ์ˆ˜ ์žˆ๋„๋ก ํ•ด์ค€๋‹ค.

django์˜ ๋™๊ธฐ์ ์ด๊ณ  ์‰ฝ๊ฒŒ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋Š” ์†์„ฑ์„ ์œ ์ง€ํ•˜๋ฉด์„œ ์ฝ”๋“œ ์Šคํƒ€์ผ์„ ๊ณ ๋ฅผ ์ˆ˜ ์žˆ๋„๋ก ํ•ด์ฃผ์–ด ์œ ์šฉํ•˜๋‹ค. django view style์˜ ๋™๊ธฐ์  ํŠน์ง•์ด๋‚˜, ์™„์ „ํžˆ ๋น„๋™๊ธฐ์ ์ด๊ฑฐ๋‚˜, ๋‘˜ ๋‹ค ์„ž๊ฑฐ๋‚˜๋ฅผ ํ•  ์ˆ˜ ์žˆ๋‹ค.

๋ฌด์—‡๋ณด๋‹ค๋„ django์˜ auth, session system๊ณผ ํ†ตํ•ฉ๋˜์–ด HTTP-only ํ”„๋กœ์ ํŠธ๋ฅผ ๋‹ค๋ฅธ ํ”„๋กœํ† ์ฝœ๋กœ ํ™•์žฅ์‹œ์ผœ์ฃผ๋Š” ๊ฒƒ์ด ์‰ฝ๋‹ค.

channel ์„ค์น˜ ๋ฐฉ๋ฒ•
๋„์ปค ์„ค์น˜ ๋งํฌ

๋„์ปค๋ž€?
๋„์ปค(Docker)๋Š” ๋ฆฌ๋ˆ…์Šค์˜ ์‘์šฉ ํ”„๋กœ๊ทธ๋žจ๋“ค์„ ์†Œํ”„ํŠธ์›จ์–ด ์ปจํ…Œ์ด๋„ˆ ์•ˆ์— ๋ฐฐ์น˜์‹œํ‚ค๋Š” ์ผ์„ ์ž๋™ํ™”ํ•˜๋Š” ์˜คํ”ˆ ์†Œ์Šค ํ”„๋กœ์ ํŠธ์ด๋‹ค. ๋„์ปค ์›น ํŽ˜์ด์ง€์˜ ๊ธฐ๋Šฅ์„ ์ธ์šฉํ•˜๋ฉด ๋‹ค์Œ๊ณผ ๊ฐ™๋‹ค:

"๋„์ปค ์ปจํ…Œ์ด๋„ˆ๋Š” ์†Œํ”„ํŠธ์›จ์–ด๋ฅผ ์†Œํ”„ํŠธ์›จ์–ด์˜ ์‹คํ–‰์— ํ•„์š”ํ•œ ๋ชจ๋“  ๊ฒƒ์„ ํฌํ•จํ•˜๋Š” ์™„์ „ํ•œ ํŒŒ์ผ ์‹œ์Šคํ…œ ์•ˆ์— ๊ฐ์‹ธ๋Š” ๊ฒƒ์ด๋‹ค. ์—ฌ๊ธฐ์—๋Š” ์ฝ”๋“œ, ๋Ÿฐํƒ€์ž„, ์‹œ์Šคํ…œ ๋„๊ตฌ, ์‹œ์Šคํ…œ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ๋“ฑ ์„œ๋ฒ„์— ์„ค์น˜๋˜๋Š” ๋ฌด์—‡์ด๋“  ์•„์šฐ๋ฅธ๋‹ค. ์ด๋Š” ์‹คํ–‰ ์ค‘์ธ ํ™˜๊ฒฝ์— ๊ด€๊ณ„ ์—†์ด ์–ธ์ œ๋‚˜ ๋™์ผํ•˜๊ฒŒ ์‹คํ–‰๋  ๊ฒƒ์„ ๋ณด์ฆํ•œ๋‹ค."

1.1 turtles all the way down

"Turtles all the way down"์€ ๋ฌดํ•œ ํšŒ๊ท€์˜ ๋ฌธ์ œ์— ๋Œ€ํ•œ ํ‘œํ˜„์ด๋‹ค. ๋ฌดํ•œ ํšŒ๊ท€๋Š” P1์˜ ์ง„์‹ค์— ๋Œ€ํ•œ ์ „์ œ๋กœ P2๊ฐ€ ํ•„์š”ํ•˜๊ณ , P2์— ๋Œ€ํ•œ ์ „์ œ๋กœ P3์ด ํ•„์š”ํ•œ ๋ฌธ์ œ๊ฐ€ ๋ฌดํ•œ๋Œ€๋กœ ๋ฐœ์ƒํ•˜๋Š” ๊ฒƒ์ด๋‹ค.

์žฅ๊ณ  ๊ณต์‹ ๋ฌธ์„œ์—์„œ ์ฑ„๋„์€ 'turtles all the way down' ๋ฐฉ์‹์œผ๋กœ ์šด์˜๋œ๋‹ค๊ณ  ๋‚˜์™€์žˆ๋‹ค. ๋ฌดํ•œ ํšŒ๊ท€ ๋ฌธ์ œ์™€ ๋Œ€์ฒด ๋ญ” ์ƒ๊ด€์ธ์ง€ stackoverflow์— ์ฐพ์•„๋ณด๋‹ˆ, pureํ•œ ๊ฐ์ฒด์ง€ํ–ฅ ์–ธ์–ด ํ˜น์€ self-hosting ํ”„๋กœ๊ทธ๋ž˜๋ฐ ์–ธ์–ด๋ฅผ ๊ฐ€๋ฆฌํ‚ค๋Š” ๋ง์ด๋ผ๊ณ  ํ•˜๋Š”๋ฐ, ์ถ”๊ฐ€ ์„ค๋ช…์ด ์—†์–ด์„œ ์ •ํ™•ํ•˜๊ฒŒ ์ดํ•ดํ•˜๊ธด ์–ด๋ ต๋‹ค ๐Ÿค”

์ฑ„๋„์€ basic consumer, url ๋ผ์šฐํŒ…, protocol detection, ๋“ฑ full application์„ ๋งŒ๋“ค๊ธฐ ์œ„ํ•œ ๋„๊ตฌ๋ฅผ ํ•œ ๋ฐ ๋ชจ์•„์ค€๋‹ค.

์‚ฌ์‹ค ์ „ํ†ต์ ์ธ django view๋„ channels๊ฐ€ ์žˆ๊ณ  ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๊ธด ํ•˜๋‹ค. channels.httl.AsgiHandler๋ผ๋Š” ASGI application ์— ๋ชจ์—ฌ์žˆ๋‹ค๊ณ  ํ•œ๋‹ค. ํ•˜์ง€๋งŒ ์ฑ„๋„์„ ํ†ตํ•ด ์ปค์Šคํ…€ํ™”๋œ HTTP long-polling handling ์ฆ‰ ์›น์†Œ์ผ“ receiver๋ฅผ ์จ์„œ ๋‚ด ์กด์žฌํ•˜๋Š” ์ฝ”๋“œ์™€ ํ•จ๊ป˜ ๋‘˜ ์ˆ˜ ์žˆ๋‹ค.

- ASGI?

Asynchronous Server Gateway Interface. WSGI(Web Server Gateway Interface)๋Š” ์›น์„œ๋ฒ„์™€ ์›น ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์˜ ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ์œ„ํ•œ ํŒŒ์ด์ฌ ํ”„๋ ˆ์ž„์›Œํฌ์ธ๋ฐ, ์š”๊ฑฐ์˜ ๋น„๋™๊ธฐ์  ๋ฒ„์ „์ด๋‹ค. ์ฑ„๋„์ด ๋งŒ๋“ค์–ด์ง€๋Š” ๋น„๋™๊ธฐ์  ์„œ๋ฒ„ spec์˜ ์ด๋ฆ„์ด๊ณ , WSGI์™€ ๋™์ผํ•˜๊ฒŒ channels์™€ daphne์— ๊ฐ‡ํ˜€์žˆ๊ธฐ๋ณด๋‹ค ๋‹ค๋ฅธ ์„œ๋ฒ„๋“ค๊ณผ ํ”„๋ ˆ์ž„์›Œํฌ ์ค‘์—์„œ ๋‚ด๊ฐ€ ์„ ํƒํ•  ์ˆ˜ ์žˆ๋„๋ก ๋””์ž์ธ๋˜์–ด์žˆ๋‹ค.

Daphne
django channel์„ ๋ณด๊ฐ•ํ•˜๊ธฐ ์œ„ํ•ด ๊ฐœ๋ฐœ๋œ HTTP, HTTP2 ๊ทธ๋ฆฌ๊ณ  ์›น์†Œ์ผ“ ํ”„๋กœํ† ์ฝœ ์„œ๋ฒ„(for ASGI, ASGI-HTT)์ด๋‹ค. ์ž๋™ํ™”๋œ ํ”„๋กœํ† ์ฝœ ์ค‘์žฌ๋ฅผ ์ง€์›ํ•œ๋‹ค. HTTP endpoint์™€ ์›น์†Œ์ผ“ endpoint๋ฅผ ๊ฒฐ์ •ํ•  ๋•Œ URL์„ prefixingํ•˜์ง€ ์•Š์•„๋„ ๋œ๋‹ค.

2. django websocket

๋ณดํ†ต django๋Š” ํด๋ผ์ด์–ธํŠธ์™€ ์„œ๋ฒ„ ๊ฐ„ ์†Œํ†ตํ•˜๊ธฐ ์œ„ํ•ด ๋‹ค์Œ๊ณผ ๊ฐ™์ด HTTP๋ฅผ ์‚ฌ์šฉํ•œ๋‹ค.

  1. ํด๋ผ์ด์–ธํŠธ๊ฐ€ ์„œ๋ฒ„์— HTTP request๋ฅผ ๋ณด๋‚ธ๋‹ค.
  2. django๋Š” request๋ฅผ ์ชผ๊ฐœ๊ณ , url์„ view์— ๋งค์นญํ•œ๋‹ค.
  3. view๊ฐ€ ์š”์ฒญ์„ ์ˆ˜ํ–‰ํ•ด ์„œ๋ฒ„๊ฐ€ ํด๋ผ์ด์–ธํŠธ์— reponse๋ฅผ ๋ณด๋‚ธ๋‹ค.

HTTP์—์„œ๋Š” ํŠธ๋ฆญ์„ ์‚ฌ์šฉํ•ด ์‹ค์‹œ๊ฐ„์ฒ˜๋Ÿผ ๋ณด์ด๋„๋ก ํ†ต์‹ ํ•  ์ˆ˜ ์žˆ๊ธด ํ•˜๋‹ค. ํด๋ผ์ด์–ธํŠธ์—์„œ ์„œ๋ฒ„๋กœ ๊ณ„์† ์š”์ฒญ์„ ๋ณด๋‚ด ์ด๋ฒคํŠธ๋ฅผ ๋ฐ›๊ฑฐ๋‚˜(polling), ํด๋ผ์ด์–ธํŠธ์—์„œ ๊ณ„์† ์š”์ฒญ์„ ๋ณด๋‚ด๊ณ  ํ•„์š”ํ•œ ์ด๋ฒคํŠธ๊ฐ€ ์žˆ์„ ๋•Œ์—๋งŒ response๋ฅผ ๋ณด๋‚ธ๋‹ค๋“ ์ง€(streaming)ํ•˜๋Š” ๋ฐฉ์‹์ด๋‹ค.

HTTP์™€ ๋‹ค๋ฅด๊ฒŒ, ์›น์†Œ์ผ“ ํ”„๋กœํ† ์ฝœ์€ ์–‘๋ฐฉํ–ฅ ์ปค๋ฎค๋‹ˆ์ผ€์ด์…˜(bi-directional communication)์„ ํ—ˆ์šฉํ•œ๋‹ค. ์„œ๋ฒ„์™€ ํด๋ผ์ด์–ธํŠธ ์‚ฌ์ด์— ์†Œ์ผ“์—ฐ๊ฒฐ(ํด๋ผ์ด์–ธํŠธ โ€” ์„œ๋ฒ„์—ฐ๊ฒฐ์ด ๊ณ„์† ์œ ์ง€๋˜์–ด ์žˆ๋Š”์ƒํƒœ)์„ ์ง€์›ํ•˜๋Š” ๊ฒƒ์ด๋‹ค.push ๋ฐฉ์‹์ด๋ผ๊ณ  ํ•˜๋Š”๋ฐ, ์„œ๋ฒ„๋Š” ์œ ์ €์˜ ์š”์ฒญ ์—†์ด ํด๋ผ์ด์–ธํŠธ์— ๋ฐ์ดํ„ฐ๋ฅผ ์ค„ ์ˆ˜ ์žˆ๋‹ค๋Š” ๋œป์ด๋‹ค. HTTP๋กœ๋Š” request์„ ๋ณด๋‚ธ ํด๋ผ์ด์–ธํŠธ๋งŒ response๋ฅผ ๋ฐ›์„ ์ˆ˜ ์žˆ๋Š” ๋ฐ˜๋ฉด, ์›น์†Œ์ผ“์œผ๋กœ๋Š” ๋‹ค์ค‘ ํด๋ผ์ด์–ธํŠธ์™€ ๋™์‹œ์— ์†Œํ†ตํ•  ์ˆ˜ ์žˆ๋‹ค. html5 ๋ฒ„์ „์ด ๋‚˜์˜ฌ ๋•Œ ์›น์†Œ์ผ“๋„ ํ•จ๊ป˜ ๋“ฑ์žฅํ–ˆ๋‹ค๊ณ  ํ•œ๋‹ค.

ํด๋ผ์ด์–ธํŠธ์™€ ์„œ๋ฒ„ ๊ฐ„ ๊ธฐ๋ณธ ์—ฐ๊ฒฐ์„ ํ•ธ๋“ค๋ง ํ•  ์ฒซ ๋ฒˆ์งธ consumer ์˜ˆ์‹œ๋ฅผ ๋งŒ๋“ค์–ด๋ณด์ž.

2.1 consumer

consumer๋Š” ์›น์†Œ์ผ“ ์—ฐ๊ฒฐ์„ ๋ฐ›๋Š”๋‹ค. ํŠœํ† ๋ฆฌ์–ผ์— consumer๊ฐ€ django view์˜ counterpart๋ผ๊ณ  ๋‚˜์™€์žˆ๋Š”๋ฐ, channel : consumer = django : view๋กœ ๋ณด๋ฉด ๋œ๋‹ค.

http request๋ฅผ ๋ฐ›์„ ๋•Œ, django๋Š” view function์„ ๋ณด๊ธฐlookup ์œ„ํ•ด root URLconf๋ฅผ ์ฐพ๊ณ  ์š”์ฒญ์„ ์ฒ˜๋ฆฌํ•˜๊ธฐ ์œ„ํ•ด view function์„ ํ˜ธ์ถœํ•œ๋‹ค. ๋น„์Šทํ•˜๊ฒŒ, ์ฑ„๋„์€ web socket์˜ ์—ฐ๊ฒฐ์„ ๋ฐ›๊ณ , consumer๋ฅผ ๋ณด๊ธฐ ์œ„ํ•ด root routing ์„ค์ •์„ ์ฐพ๋Š”๋‹ค. ๊ทธ๋ฆฌ๊ณ  ์—ฐ๊ฒฐ์œผ๋กœ๋ถ€ํ„ฐ ์˜จ ์ด๋ฒคํŠธ๋“ค์„ ํ•ธ๋“ค๋งํ•˜๊ธฐ ์œ„ํ•ด consumer์˜ ์—ฌ๋Ÿฌ ํ•จ์ˆ˜๋ฅผ ํ˜ธ์ถœํ•œ๋‹ค.

์›น ์†Œ์ผ“ ์—ฐ๊ฒฐ์„ ๋ฐ›์•„์„œ ์›น์†Œ์ผ“์œผ๋กœ๋ถ€ํ„ฐ ์–ด๋–ค ๋ฉ”์‹œ์ง€๋“  ๋ฐ›๊ณ , ๊ฐ™์€ ์›น์†Œ์ผ“์œผ๋กœ echo back ํ•˜๋Š” basic consumer code๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™๋‹ค.

import json
from channels.generic.websocket import WebsocketConsumer

#์›น์†Œ์ผ“ class instance๋ฅผ ๋งŒ๋“ค์–ด์š”
class ChatConsumer(WebsocketConsumer):
    #์›น์†Œ์ผ“์— ์—ฐ๊ฒฐ, ํ˜น์€ ์—ฐ๊ฒฐ ํ•ด์ œํ•ด์š”
    def connect(self):
        self.accept()

    def disconnect(self, close_code):
        pass

    def receive(self, text_data):
    	#josn์œผ๋กœ ์ฑ„ํŒ… ๋ฉ”์‹œ์ง€๋ฅผ ๋ฐ›์•„์š” 
        text_data_json = json.loads(text_data)
        message = text_data_json['message']

	#json ๊ฐ์ฒด๋ฅผ ์ธ์ฝ”๋”ฉ ํ•ด์„œ ๋ณด๋‚ด์š”
        self.send(text_data=json.dumps({
            'message': message
        }))

2.2 routes

Django url ์„ค์ •๊ณผ ์œ ์‚ฌํ•˜๊ฒŒ ์ž‘๋™ํ•˜๋Š” ๊ฒƒ์ด routes๋‹ค. urlpatterns ๋Œ€์‹  channel_routing, url() ๋Œ€์‹  route()์„ ์ •์˜ํ•˜๊ฒŒ ๋œ๋‹ค. ์ด ๋•Œ consumer functions๋ฅผ ์›น์†Œ์ผ“์— ์—ฐ๊ฒฐํ•œ ์ ์„ ์ฃผ๋ชฉํ•˜์ž!

์ฑ„๋„์ด ๊ฐœ๋ฐœ ์„œ๋ฒ„์— ์—ฐ๊ฒฐ๋˜๋ฉด, ProtocolTypeRouter๊ฐ€ ์—ฐ๊ฒฐ์ด ์–ด๋–ค ํƒ€์ž…์ธ์ง€ ์กฐ์‚ฌํ•œ๋‹ค. ๋งŒ์•ฝ socket ์—ฐ๊ฒฐ(ws:// or wss://)์ด๋ฉด, ์—ฐ๊ฒฐ์„ AuthMiddlewareStack๋กœ ๋ณด๋‚ธ๋‹ค. AuthMiddlewareStack์€ ์—ฐ๊ฒฐ ๋ฒ”์œ„๋ฅผ ์ตœ๊ทผ์— ๊ฒ€์ฆ๋œ ์œ ์ €๋กœ ํ™•์žฅํ•˜๊ณ , ๋‹ค์‹œ ์—ฐ๊ฒฐ์„ URLRouter๋กœ ๋ณด๋‚ธ๋‹ค. URLRouter๋Š” urlpatterns๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ํŠน์ • consumer์—๊ฒŒ ์—ฐ๊ฒฐ๋œ http path๋ฅผ ์กฐ์‚ฌ ํ•œ๋‹ค.

# chatapp/routing.py
from django.urls import re_path

from . import consumers

websocket_urlpatterns = [
    re_path(r'ws/chat/(?P<room_name>\w+)/$', consumers.ChatConsumer),
]

# rootapp/routing.py
from channels.auth import AuthMiddlewareStack
from channels.routing import ProtocolTypeRouter, URLRouter
import chat.routing

application = ProtocolTypeRouter({
    # (http->django views is added by default)
    'websocket': AuthMiddlewareStack(
        URLRouter(
            chat.routing.websocket_urlpatterns
        )
    ),
})

์ผ๋ฐ˜์ ์ธ HTTP ์—ฐ๊ฒฐ๊ณผ ์›น์†Œ์ผ“ ์—ฐ๊ฒฐ์„ ๊ตฌ๋ถ„ํ•˜๊ธฐ ์œ„ํ•ด, path prefix์— /ws/์™€ ๊ฐ™์€ ๊ฒƒ์„ ๋„ฃ์–ด์ฃผ๋Š”๊ฒŒ ์ข‹๋‹ค. ์™œ๋ƒํ•˜๋ฉด production ํ™˜๊ฒฝ์—์„œ ์ฑ„๋„์„ ๋Š์„ ๋•Œ ํŠน์ • ์„ค์ •์„ ๋” ์‰ฝ๊ฒŒ ๋งŒ๋“ค์–ด์ฃผ๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค.

2.3 channel layer

์ฑ„๋„ ๋ ˆ์ด์–ด๋Š” ์ปค๋ฎค๋‹ˆ์ผ€์ด์…˜ ์‹œ์Šคํ…œ์˜ ์ผ์ข…์ด๋‹ค. ์‚ฌ์šฉ์ž๋“ค์ด ์„œ๋กœ ์–˜๊ธฐํ•˜๊ธฐ ์œ„ํ•ด ์—ฌ๋Ÿฌ consumer instances๋ฅผ ๋งŒ๋“ค๊ฒŒ ํ•ด์ค€๋‹ค. ๋ชจ๋“  consumer instance๋Š” ์ž๋™์œผ๋กœ ์ƒ์„ฑ๋œ unique channel name์ด ์žˆ๊ณ , ์ฑ„๋„ ๋ ˆ์ด์–ด๋ฅผ ํ†ตํ•ด ํ†ต์‹ ํ•  ์ˆ˜ ์žˆ๋‹ค.

  • ์ฑ„๋„: ๋ฉ”์‹œ์ง€๊ฐ€ ๋ณด๋‚ด์ง€๋Š” ๋ฉ”์ผ๋ฐ•์Šค๋‹ค. ๋ชจ๋“  ์ฑ„๋„์€ ์ด๋ฆ„์ด ์žˆ๊ณ , ๊ทธ ์ฑ„๋„ ์ด๋ฆ„์ด ์žˆ๋Š” ๋ˆ„๊ตฌ๋“  ์ฑ„๋„์— ๋ฉ”์‹œ์ง€๋ฅผ ๋ณด๋‚ผ ์ˆ˜ ์žˆ๋‹ค.
  • ๊ทธ๋ฃน: ์ฑ„๋„๊ณผ ๊ด€๋ จ๋œ ๊ฒƒ๋“ค์˜ ๋ฌถ์Œ์ด๊ณ  ๋งˆ์ฐฌ๊ฐ€์ง€๋กœ ์ด๋ฆ„์ด ์žˆ๋‹ค. ๊ทธ๋ฃน์˜ ์ด๋ฆ„์„ ์•Œ๊ณ  ์žˆ๋Š” ๋ˆ„๊ตฌ๋“  ๊ทธ๋ฃน์— ์žˆ๋Š” ๋ชจ๋“  ์ฑ„๋„์— ๋ฉ”์‹œ์ง€๋ฅผ ๋ณด๋‚ด๊ฑฐ๋‚˜ ๊ทธ๋ฃน์— ์ฑ„๋„์„ ์ถ”๊ฐ€, ์‚ญ์ œํ•  ์ˆ˜ ์žˆ๋‹ค. ์ฑ„๋„์— ํŠน์ • ์ฑ„๋„์ด ์žˆ๋Š”์ง€ ์ผ์ผํžˆ ์„ธ๋Š” ๊ฒƒ์€ ๋ถˆ๊ฐ€ํ•˜๋‹ค.

๊ฐ™์€ ๋ฐฉ์— ์žˆ๋Š” ์‚ฌ๋žŒ๋“ค์ด ์„œ๋กœ ์†Œํ†ตํ•˜๊ฒŒ ๋งŒ๋“ค๊ธฐ ์œ„ํ•ด์„œ๋Š”, consumers.py์—์„œ ๋งŒ๋“  ChatConsumer class์˜ instance๋ฅผ ์—ฌ๋Ÿฌ ๊ฐœ ๋งŒ๋“ค์–ด์•ผ ํ•œ๋‹ค. ๊ทธ๋ž˜์„œ ๋ชจ๋“  ChatConsumer๊ฐ€ ์ฑ„ํŒ…๋ฐฉ์˜ ์ด๋ฆ„์„ ๊ธฐ๋ฐ˜์œผ๋กœ ํ•œ ๊ทธ๋ฃน์— ์ฑ„๋„์„ ์ถ”๊ฐ€ํ•˜๋„๋ก ๋งŒ๋“ค๊ณ , ๊ฐ™์€ ๋ฐฉ์— ์žˆ๋Š” ๋‹ค๋ฅธ ChatConsumer๊ฐ€ ๋ฉ”์‹œ์ง€๋ฅผ ์ „์†กํ•˜๋„๋ก ํ—ˆ๊ฐ€ํ•ด์•ผ ํ•œ๋‹ค.

ํŠœํ† ๋ฆฌ์–ผ์—์„œ๋Š” backing store๋กœ์„œ Redis๋ฅผ ์ฑ„๋„ ๋ ˆ์ด์–ด๋กœ ์‚ฌ์šฉํ–ˆ๋‹ค. port 6379์—์„œ Redis server๋ฅผ runํ•˜๊ณ , chennel_redis๋ฅผ ์„ค์น˜ํ•ด์•ผ ํ•œ๋‹ค.

$ docker run -p 6379:6379 -d redis:5
$ python3 -m pip install channels_redis

root app settings.py ์— ์ฑ„๋„ ๋ ˆ์ด์–ด๋ฅผ ์„ค์ •ํ•œ๋‹ค.

ASGI_APPLICATION = 'mysite.routing.application'
CHANNEL_LAYERS = {
    'default': {
        'BACKEND': 'channels_redis.core.RedisChannelLayer',
        'CONFIG': {
            "hosts": [('127.0.0.1', 6379)],
        },
    },
}

์ฑ„๋„ ๋ ˆ์ด์–ด๊ฐ€ ์ƒ๊ฒผ์œผ๋ฉด, consumers.py์—์„œ ์ฝ”๋“œ๋ฅผ ๋‹ค์‹œ ์ž‘์„ฑํ•œ๋‹ค. ์•„๋ž˜ ์ฝ”๋“œ์™€ ์„ค๋ช…์„ ๋ณด์ž.

import json
from asgiref.sync import async_to_sync
from channels.generic.websocket import WebsocketConsumer

class ChatConsumer(WebsocketConsumer):
    def connect(self):
        self.room_name = self.scope['url_route']['kwargs']['room_name']
        self.room_group_name = 'chat_%s' % self.room_name

        # Join room group
        async_to_sync(self.channel_layer.group_add)(
            self.room_group_name,
            self.channel_name
        )

        self.accept()

    def disconnect(self, close_code):
        # Leave room group
        async_to_sync(self.channel_layer.group_discard)(
            self.room_group_name,
            self.channel_name
        )

    # Receive message from WebSocket
    def receive(self, text_data):
        text_data_json = json.loads(text_data)
        message = text_data_json['message']

        # Send message to room group
        async_to_sync(self.channel_layer.group_send)(
            self.room_group_name,
            {
                'type': 'chat_message',
                'message': message
            }
        )

    # Receive message from room group
    def chat_message(self, event):
        message = event['message']

        # Send message to WebSocket
        self.send(text_data=json.dumps({
            'message': message
        }))

self.scope['url_route']['kwargs']['room_name']

  • consumer์—๊ฒŒ websocket ์—ฐ๊ฒฐ์„ ์—ด์–ด์ฃผ๋Š” chat/routing.py URLrout๋กœ๋ถ€ํ„ฐ 'room_name'์„ ์–ป๋Š”๋‹ค.
  • ๋ชจ๋“  consumer๋Š” ์ž์‹ ์˜ ์—ฐ๊ฒฐ, URLrout๋กœ๋ถ€ํ„ฐ ๋ฐ›์€ positinal or keyword argument, ์ตœ๊ทผ ์ธ์ฆ๋œ ์œ ์ € ๋“ฑ์— ๋Œ€ํ•œ ์ •๋ณด๋ฅผ ํฌํ•จํ•˜๋Š” scope๋ฅผ ๊ฐ€์ง€๊ณ  ์žˆ๋‹ค.

self.roomgroup_name = 'chat%s' % self.room_name

  • ์œ ์ €์— ํŠน์ •ํ™”๋œ ์ฑ„ํŒ…๋ฐฉ์˜ ์ด๋ฆ„์œผ๋กœ๋ถ€ํ„ฐ ์ฑ„๋„์˜ ๊ทธ๋ฃน ์ด๋ฆ„์„ ์ƒ์„ฑํ•œ๋‹ค.

async_to_sync(self.channel_layer.group_add)(...)

  • ๊ทธ๋ฃน์— ์ฐธ์—ฌํ•˜๋Š” ๊ฒƒ์ด๋‹ค.
  • async_to_sync wrapper๊ฐ€ ํ•„์š”๋กœ ๋˜๋Š” ์ด์œ ๋Š”, ChatConsumer๊ฐ€ ๋™๊ธฐ์ ์ธ ์›น์†Œ์ผ“ consummer์ด์ง€๋งŒ ๋น„๋™๊ธฐ์ ์ธ ์ฑ„๋„ ๋ ˆ์ด์–ด ๋ฉ”์†Œ๋“œ๋ฅผ ์‚ฌ์šฉํ•˜๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค. (๋ชจ๋“  ์ฑ„๋„ ๋ ˆ์ด์–ด method๋Š” ๋น„๋™๊ธฐ ๋ฐฉ์‹์ด๋‹ค)

self.accept()

  • ์›น์†Œ์ผ“ ์—ฐ๊ฒฐ์„ ๋ฐ›๋Š”๋‹ค.
  • connect() method์—์„œ accept()๋ฅผ ํ˜ธ์ถœํ•˜์ง€ ์•Š์œผ๋ฉด ์—ฐ๊ฒฐ์€ ๊ฑฐ์ ˆ๋œ๋‹ค. ๋งŒ์•ฝ ์š”์ฒญํ•œ ์œ ์ €๊ฐ€ ์š”์ฒญํ•œ ํ–‰๋™์— ๋Œ€ํ•ด ์ธ์ฆ๋˜์ง€ ์•Š์•˜๋”ฐ๋ฉด, ์—ฐ๊ฒฐ์„ ๊ฑฐ์ ˆํ•˜๊ณ  ์‹ถ์„ ์ˆ˜ ์žˆ๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค.
  • ํŠœํ† ๋ฆฌ์–ผ์—์„œ accept()๋ฅผ ์“ฐ๊ณ  ์‹ถ๋‹ค๋ฉด connect()์˜ ๋งˆ์ง€๋ง‰ action์œผ๋กœ ๋„ฃ์„ ๊ฒƒ์„ ๊ถŒ์žฅํ•œ๋‹ค.

async_to_sync(self.channel_layer.group_discard)(...)

  • ๊ทธ๋ฃน์„ ๋– ๋‚˜๋Š” ๊ฒƒ์ด๋‹ค.

async_to_sync(self.channel_layer.group_send)

  • ๊ทธ๋ฃน์— ์ด๋ฒคํŠธ๋ฅผ ๋ณด๋‚ธ๋‹ค.
  • ์ด๋ฒคํŠธ๋ฅผ ๋ฐ›๋Š” consumers์—์„œ ๋ฐœ์ƒํ•ด์•ผ ํ•  method์˜ ์ด๋ฆ„์— ๋Œ€์‘ํ•˜๋Š” ํŠน๋ณ„ํ•œ 'type' key๋ฅผ ๊ฐ–๊ณ  ์žˆ๋‹ค.

์—ฌ๊ธฐ๊นŒ์ง€๊ฐ€ ํŠœํ† ๋ฆฌ์–ผ์˜ 1,2์— ๋Œ€ํ•œ ๋‚ด์šฉ์ด๋‹ค.

ํŠœํ† ๋ฆฌ์–ผ 3์—์„œ consumer ์ฝ”๋“œ๋ฅผ ์™„์ „ ๋น„๋™๊ธฐ์ ์œผ๋กœ ๋งŒ๋“œ๋Š” ๊ฒƒ์ด ์žˆ๋Š”๋ฐ, ํŠน์ • ํ•จ์ˆ˜์™€ inheritํ•˜๋Š” ํด๋ž˜์Šค๋ฅผ async๋กœ ๋ฐ”๊พธ๋Š” ๊ฒƒ์ด๊ธฐ ๋•Œ๋ฌธ์— ๋งํฌ๋งŒ ์ฒจ๋ถ€ํ•œ๋‹ค.

ํŠœํ† ๋ฆฌ์–ผ 4์—์„œ๋Š” ์…€๋ ˆ๋‹ˆ์›€์„ ํ†ตํ•ด ์•„๋ž˜ ๋‚ด์šฉ์„ ํ™•์ธํ•˜๋Š” test code๋ฅผ ๋งŒ๋“ ๋‹ค.

  • ๊ฐ™์€ ๋ฐฉ์— ์žˆ๋Š” ๋ชจ๋“  ์‚ฌ๋žŒ๋“ค์ด ํฌ์ŠคํŠธ๋œ ๋ฉ”์‹œ์ง€๋ฅผ ๋ณผ ์ˆ˜ ์žˆ๋Š”์ง€
  • ๋ฉ”์‹œ์ง€๊ฐ€ ์˜ฌ๋ผ์˜ฌ ๋•Œ ๋‹ค๋ฅธ ๋ฐฉ์— ์žˆ๋Š” ์‚ฌ๋žŒ์€ ๋ณผ ์ˆ˜ ์—†๋Š”์ง€

chating app์„ ๋งŒ๋“œ๋Š” tutorial๋กœ ์›น์†Œ์ผ“์ด ๋ฌด์—‡์ธ์ง€ ์ดํ•ด๋ฅผ ํ–ˆ๋Š”๋ฐ, ์ด์ œ ์›น์†Œ์ผ“์„ ๊ฐ€์ง€๊ณ  ์–ด๋–ป๊ฒŒ ๋ฐ์ดํ„ฐ๋ฅผ ์˜๋Š”์ง€๊ฐ€ ๋ฌธ์ œ๋‹ค.

์ผ๋‹จ ๊ฐœ๋…์€ ์•Œ์•˜์œผ๋‹ˆ ์–ด๋–ป๊ฒŒ ํ™œ์šฉํ•ด์•ผ ํ•˜๋Š”์ง€์— ๋Œ€ํ•ด ๊ณต๋ถ€ํ•ด์•ผ๊ฒ ๋‹ค ๐Ÿค“

3. ์ถ”๊ฐ€ ๊ฐœ๋…

Handshake

Websocket Handshake๋Š” ์žฌ๋ฏธ์žˆ๋Š” ์ ์ด ์žˆ๋‹ค. ๋ฐ”๋กœ Upgrade ๋ผ๋Š” ํ—ค๋”์ธ๋ฐ. 'websocket' ์œผ๋กœ ์ง€์ •๋˜์–ด ์žˆ๋‹ค.
๋ถ„๋ช…ํžˆ ์ง€๊ธˆ์€ GET ํƒ€์ž…์œผ๋กœ ๋ณด๋‚ด๊ณ  ์žˆ์ง€๋งŒ, ์„œ๋ฒ„์—์„œ ์Šค์œ„์นญ ์‹œ์ผœ์ค€๋‹ค.

  1. Client -> Server
    GET /chat HTTP/1.1
    Host: server.example.com
    Upgrade: websocket
    Connection: Upgrade
    Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
    Origin: http://example.com
    Sec-WebSocket-Protocol: chat, superchat
    Sec-WebSocket-Version: 13
  1. Server -> Client

    HTTP/1.1 101 Switching Protocols
    Upgrade: websocket
    Connection: Upgrade
    Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=

'101 Switching Protocols' ๊ฐ€ response๋กœ ์˜ค๋ฉด Websocket ์—ฐ๊ฒฐ์ด ๋œ ๊ฒƒ์ด๋‹ค. ๊ทธ ์ดํ›„๋กœ๋Š” Socket Event๋ฅผ ๋“ฑ๋กํ•˜์—ฌ Emit (๋ฉ”์‹œ์ง€ ์ „์†ก) , socket.on (๋ฉ”์‹œ์ง€ ๋ฆฌ์Šค๋‹) ๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ๋œ๋‹ค. ํด๋ผ์ด์–ธํŠธ - ์„œ๋ฒ„ ์–‘๋ฐฉํ–ฅ ํ†ต์‹ ์ด๋ฏ€๋กœ ์„œ๋ฒ„์—์„œ ์›ํ•˜๋Š” ์‹œ์ ์— ํด๋ผ์ด์–ธํŠธ๋กœ Emit (๋ฉ”์‹œ์ง€ ์ „์†ก) ์„ ๋‚ ๋ฆฌ๋ฉด ๋ฐ”๋กœ ํด๋ผ์ด์–ธํŠธ๊ฐ€ ์‘๋‹ตํ•œ๋‹ค.

์ถœ์ฒ˜: https://jjshun.tistory.com/46 [์†ŒํŒŒ์˜ ๊ณต๋ถ€๋ฐฉ]

profile
์ด์‚ฌ๊ฐ„ ๋ธ”๋กœ๊ทธ: yenilee.github.io

2๊ฐœ์˜ ๋Œ“๊ธ€

comment-user-thumbnail
2020๋…„ 5์›” 13์ผ

์ง์ง์ง

๋‹ต๊ธ€ ๋‹ฌ๊ธฐ
comment-user-thumbnail
2021๋…„ 6์›” 11์ผ

์•ˆ๋…•ํ•˜์„ธ์š”! ์ข‹์€ ๊ธ€ ๊ฐ์‚ฌํ•ฉ๋‹ˆ๋‹ค.
ํ˜น์‹œ ์กฐ๊ธˆ๋” ๊นŠ์ด ๊ณต๋ถ€ํ•˜๊ณ  ์‹ถ์€๋ฐ ์ถ”์ฒœ ํ•  ๋งŒํ•œ ์ฑ…, ์˜์ƒ, ํ˜น์€ ๊ธฐํƒ€๋“ฑ๋“ฑ ์ฑ„๋„์Šค๋ฅผ ์ด์šฉํ•œ ์›น ์†Œ์ผ“ ์„œ๋น„์Šค์— ๊ด€ํ•œ ์ •๋ณด ํŒ ์—†์„๊นŒ์š”?

๋‹ต๊ธ€ ๋‹ฌ๊ธฐ