191번째 줄: |
191번째 줄: |
| </syntaxhighlight> | | </syntaxhighlight> |
| |} | | |} |
− | === 참가자 구현 === | + | === 참가자(컨슈머) 구현 === |
| {| class="wikitable" | | {| class="wikitable" |
| !과정 | | !과정 |
261번째 줄: |
261번째 줄: |
| | | |
| = 채널 레이어 구현 = | | = 채널 레이어 구현 = |
− |
| |
− | === 체널레이어 구현 ===
| |
| consumer 인스턴스는 자동으로 유일한 channel name을 구성하기 때문에 서로 소통하려면 layer가 필요하다. | | consumer 인스턴스는 자동으로 유일한 channel name을 구성하기 때문에 서로 소통하려면 layer가 필요하다. |
− | {| class="wikitable"
| |
− | !과정
| |
− | !설명
| |
− | !방법
| |
− | |-
| |
− | |패키지 설치
| |
− | |Channels가 Redis인터페이스를 인식하도록.
| |
− | |pip install channels_redis
| |
− | |-
| |
− | |settings.py 설정
| |
− | |ASGI 아래 추가하자.
| |
− | |<syntaxhighlight lang="python">
| |
− | ASGI_APPLICATION = 'routing.application' # routing.py 파일의 application을 불러온다.
| |
− | CHANNEL_LAYERS = {
| |
− | 'default': {
| |
− | 'BACKEND': 'channels_redis.core.RedisChannelLayer',
| |
− | 'CONFIG': {
| |
− | "hosts": [('127.0.0.1', 6379)],
| |
− | },
| |
− | },
| |
− | }
| |
− | </syntaxhighlight>
| |
− | |}
| |
| | | |
− | | + | == 레디스 == |
− | == 개요 == | |
| 레디스 서버를 이용한 실시간 채팅 구현. | | 레디스 서버를 이용한 실시간 채팅 구현. |
| | | |
− | 꽤 오래된 버전인 5를 사용하는데, 아마도 이는.. 5에서 처음으로 consumer groups이 구현된 버전인데, 이걸로 구현했기 때문이 아닐까 싶다. | + | 꽤 오래된 버전인 5를 사용하는데, 아마도 이는.. 5에서 처음으로 consumer groups이 구현된 버전인데, 이걸로 구현했기 때문이 아닐까 싶다.(최신버전에서도 될진.. 검증이 필요하다.) |
| ===사전설정=== | | ===사전설정=== |
| {| class="wikitable" | | {| class="wikitable" |
301번째 줄: |
275번째 줄: |
| | 서버 설치 | | | 서버 설치 |
| |설치 후 딱히 옵션을 변경할 내용은 없다. | | |설치 후 딱히 옵션을 변경할 내용은 없다. |
| + | 어차피 로컬에서 모든 작동이 수행되기 때문에. |
| + | |
| + | 네이티브로 설치하는 게 아니라 도커를 사용할 거라면 아래로 넘어가자. |
| | | |
− | 어차피 로컬에서 모든 작동이 수행되기 때문에.
| + | 단순 apt-get install redis로 설치해 5 이상의 버전이 되어도 작동한다. |
| |[http://id8436.iptime.org:8080/mediawiki/index.php/Radis Redis] 문서 참조. | | |[http://id8436.iptime.org:8080/mediawiki/index.php/Radis Redis] 문서 참조. |
| |- | | |- |
− | |서버 테스트 | + | |도커 |
− | |서버가 잘 설치되었는지 테스트. | + | |도커가 설치되어 있다면 아래 명령으로 바로 설치, 서비스가 시작된다. |
− | 서버 구동에 대한 메시지가 뜬다.
| + | |
− | |redis-server
| + | docker run -p 6379:6379 -d redis:5 |
| + | |아래 문서를 통해 도커 설치를 참고하자. |
| + | http://id8436.iptime.org:8080/mediawiki/index.php/Docker |
| |- | | |- |
| |연동 라이브러리 설치 | | |연동 라이브러리 설치 |
327번째 줄: |
306번째 줄: |
| } | | } |
| </syntaxhighlight>을 추가해준다. | | </syntaxhighlight>을 추가해준다. |
− | |-
| |
− | |기능 확인
| |
− | |리눅스 기반의 OS에서 개발과 서비스를 하고 있다면 기능의 확인이 수월하겠지만... 윈도우 기반의 OS에서 개발이 진행된다면 쉽지 않다.
| |
− | 우측과 같이 파이썬 쉘을 통해 서버의 실행을 점검해보자.
| |
− | |<syntaxhighlight lang="bash">
| |
− | python3 manage.py shell
| |
− | >>> import channels.layers
| |
− | >>> channel_layer = channels.layers.get_channel_layer()
| |
− | >>> from asgiref.sync import async_to_sync
| |
− | >>> async_to_sync(channel_layer.send)('test_channel', {'type': 'hello'})
| |
− | >>> async_to_sync(channel_layer.receive)('test_channel')
| |
− | {'type': 'hello'}
| |
− | </syntaxhighlight>
| |
| |} | | |} |
− | ==모델작성== | + | ==기존 컨슈머 변형== |
− | chat 앱의 models.py 작성<syntaxhighlight lang="python">
| + | 위에서 작성한 컨슈머는 중간 확인을 위한 동기식 함수인데, 비동기식으로 다시 쓰이면 성능이 좋아진다. 아래와 같이 변형하자.<syntaxhighlight lang="python"> |
− | from django.db import models | + | import json |
− | from django.conf import settings | + | |
| + | from channels.generic.websocket import AsyncWebsocketConsumer |
| + | |
| + | |
| + | class ChatConsumer(AsyncWebsocketConsumer): |
| + | async 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 |
| + | await self.channel_layer.group_add(self.room_group_name, self.channel_name) |
| + | |
| + | await self.accept() |
| + | |
| + | async def disconnect(self, close_code): |
| + | # Leave room group |
| + | await self.channel_layer.group_discard(self.room_group_name, self.channel_name) |
| + | |
| + | # Receive message from WebSocket |
| + | async def receive(self, text_data): |
| + | text_data_json = json.loads(text_data) |
| + | message = text_data_json["message"] |
| | | |
− | class Room(models.Model):
| + | # Send message to room group |
− | room_name = models.CharField(max_length=100, blank=True)
| + | await self.channel_layer.group_send( |
− | users = models.ManyToManyfield(
| + | self.room_group_name, {"type": "chat_message", "message": message} |
− | settings.AUTH_USER_MODEL, # 유저모델과 연결한다. | + | ) |
− | blank=True, | |
− | related_name = 'rooms') # 룸이라는 인덱스 지정.
| |
− |
| |
− | def __str__(self):
| |
− | return self.room_name
| |
− |
| |
− | class Message(models.Model):
| |
− | user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, default=1)
| |
− | room = models.ForeignKey(Room, related_name='messages', default=1, on_delete=models.CASCADE)
| |
− | content = models.TextField(blank=True)
| |
− | created_at = models.DateTimeField(auto_now_add=True)
| |
− |
| |
− | def __str__(self):
| |
− | return self.content | |
− | </syntaxhighlight>chat앱의 admin.py 작성<syntaxhighlight lang="python">
| |
− | from django.contrib import admin
| |
− | from .models import Room, Message
| |
| | | |
− | @admin.register(Room)
| + | # Receive message from room group |
− | class RoomAdmin(admin.ModelAdmin):
| + | async def chat_message(self, event): |
− | list_display = ['id', 'room_name'] | + | message = event["message"] |
− | list_display_links = ['room_name'] | |
− |
| |
− | @admin.register(Room)
| |
− | class MessageAdmin(admin.ModelAdmin):
| |
− | list_display = ['user', 'room', 'content', 'created_at']
| |
− | list_display_links = ['user', 'room', 'content', 'created_at']
| |
− | </syntaxhighlight>대화모델 작성(보통 친구모델 등에 작성된다.)(근데 그냥 chat 앱 안에 작성하면 안되나...?)<syntaxhighlight lang="python">
| |
− | ...
| |
− | from .chat.models import Room, Message
| |
| | | |
− | class chat_connection(models.Model):
| + | # Send message to WebSocket |
− | target = models.ForeignKey(settings.AUTH_USER_MODEL, blank=True, on_delete=models.CASCADE) # 상대방
| + | await self.send(text_data=json.dumps({"message": message})) |
− | room = models.ForeignKey(Room, blank=True, on_delete=models.SET_NULL, null=True) # null옵션이 있어야 하나..? 없어도 될듯, 강의에선 친구모델 위에 더하느라 붙인듯.
| |
− | user = models.ForeignKey(settings.AUTH_USER_MODEL, blank=True, on_delete=models.CASCADE) # 나
| |
− | created_at = models.DateField(auto_now_add=True)
| |
− |
| |
− | def __str__(self):
| |
− | return self.user.username + "채팅" # 사용자명을 반환한다.
| |
| </syntaxhighlight> | | </syntaxhighlight> |
− | ==뷰 작성==
| |
− | <syntaxhighlight lang="python">
| |
− | from django.shortcuts import render, get_object_or_404
| |
− | forom django.contrib.auth import get_user_model
| |
− | from chat.models import *
| |
− | import logging
| |
| | | |
− | def start_chat(request):
| + | = 서버에 반영 = |
− | from_user = # 본인의 유저모델 가져오기
| + | 튜토리얼엔 나오지 않지만(공식문서 한참 뒤에 나온다..), 서버에 반영하게 되면 ws/ 경로에 대하여 새로운 설정을 해주어야 한다.(필자는 이걸 몰라서 이것저것 건드리다가 sql 날려먹었다.. 젠장) |
− | to_user = 유저모델.objects.get(pk=target_request_id) # 상대의 유저모델 가져오기(이름이나 pk로 불러오게 하면 될듯)
| + | |
− |
| + | 일반적으로 WSGI 통신을 gunicorn으로 처리한다면, ASGI통신은 daphne으로 처리한다.(daphne에서도 wsgi통신이 가능하지만, 보안상 권장되진 않는다.) |
− | room_name = "{},{}".format(from_user.username, to_user.username)
| + | {| class="wikitable" |
− | room = Room.objects.create(room_name=room_name) # 룸을 만든다.
| + | !과정 |
− |
| + | !설명 |
− | def chat_list(request):
| + | !방법 |
− | user = request.user
| + | |- |
− | user_profile = user.user_profile
| + | | 설치 |
− | friends = user.friends.all() # 모든 친구 불러오기...(채팅자 목록이라고 보면 됨.)
| + | |daphne은 channels를 설치하면 자동으로 설치가 된다. |
− |
| + | | |
− | context = {'user_profile': user_profile,
| + | |- |
− | 'friends':friends,
| + | |확인 |
− | }
| + | |실행 명령은 우측과 같다.(asgi의 경로를 지정해주면 된다.) |
− |
| + | |
− | return render(request, 'chat/chat_list.html', context)
| + | 포트번호는 필요에 따라 지정하면 된다. |
− |
| + | |
− | def room(request, room_id):
| + | 해당 포트로 접속해보면 사이트와 채팅이 잘 작동한다. |
− | user = request.user
| + | |<code>daphne -b 0.0.0.0 -p 8001 config.asgi:application</code> |
− | user_profile = user.profile
| + | |- |
− | friends = user.friends.all() # 모든 친구 불러오기...(채팅자 목록이라고 보면 됨.)
| + | |서버에서 실행 |
− |
| + | |서비스를 위해선 백그라운드로 진행해주어야 한다. |
− | room = Room.objects.get(pk=room_id) # 룸 모델에서 해당 pk에 맞는 룸 불러오기
| + | 실행 여부는 <code>ps -ef | grep daphne</code>로 확인해보자. 실행이 안되면 nohup 떼서 에러메시지 확인. |
− | friends_uer = room.users.all().exclude(pk=user.id).first()
| + | |<code>nohup daphne -b 0.0.0.0 -p 8001 config.asgi:application &</code> |
− |
| + | |- |
− | context = {'current_user': user,
| + | |웹서버에 반영 |
− | 'user_profile': user_profile,
| + | |/static을 반영하듯, /ws로 시작되는 주소는 다핀으로 넘겨준다. |
− | 'friends':friends,
| + | 이후 관련 서비스들을 재시작하면 설정들이 반영된다. |
− | 'room':room,
| |
− | 'friends_user': friends_user,
| |
− | }
| |
− |
| |
− | return render(request, 'chat/room.html', context)
| |
| | | |
− |
| + | 채팅도 물론 성공적...! |
| + | |<syntaxhighlight lang="bash"> |
| + | location /ws { |
| + | proxy_pass http://localhost:8001; |
| + | proxy_http_version 1.1; |
| + | proxy_set_header Upgrade $http_upgrade; |
| + | proxy_set_header Connection "upgrade"; |
| + | } |
| </syntaxhighlight> | | </syntaxhighlight> |
− | ==url 작성==
| + | |} |
− | config의 url을 넘김 처리하고..
| + | 웹소켓에서도 https 인증을 적용할 수 있다.([https://victorydntmd.tistory.com/265 추후에 반영해보자.]) |
| | | |
− | chat앱 안의 urls.py를 작성한다.<syntaxhighlight lang="python">
| + | == 관련에러 == |
− | from .views import *
| |
| | | |
− | app_name= 'chat'
| + | === The app module <module '앱이름' (<_frozen_importlib_external._NamespaceLoader object at 0x7f2faed9a8f0>)> has multiple filesystem locations (['/./앱경로', '/앱경로']); you must configure this app with an AppConfig subclass with a 'path' class attribute. === |
| + | 생각지도 못한 문제점이었다... 앱은 하나의 모듈처리가 되어 디렉토리 안에 __init__.py가 담기는데, 이게 없는 경우 경로를 제대로 찾지 못해 발생하는 에러이다. 앱 디렉토리 안에 __init__.py를 제대로 넣어주면 해결됨. |
| | | |
− | urlpatterns = [
| + | = 마무리. 각 요소들이 어떤 역할을 하는지. = |
− | path('', chat_list, name='chat_list'),
| + | 순서대로 어떤 경로를 거쳐 작동하는지 살펴보자. |
− | paht('<str:room_id', room, name='room'),
| + | |
− | ]
| + | === asgi.py === |
− | </syntaxhighlight>
| + | {| class="wikitable" |
− | ==탬플릿 작성 == | + | !항목 |
− | chat 앱 안의 templates 폴더를 만든 다음 넣는다.<syntaxhighlight lang="html+django">
| + | !설명 |
− | <!--확장코드-->
| + | |- |
| + | | ProtocolTypeRouter |
| + | |연결의 종류를 파악하여 처리. http로 넘길지, 웹소켓 처리로 넘길지. |
| | | |
− | <!--들어가야 할 것들--> | + | * http 연결일 경우 보통 django_asgi_app<code>(get_asgi_application())</code> 가 실행된다. |
− | <script src="{% static 'js/messenger.js' %}"></script> | + | * websocket 연결(ws:// 혹은 wss://)일 경우 <code>AuthMiddlewareStack</code>으로 앱별로 <code>routing.py</code>에 적어놓은 코드로 라우팅 한다. |
| + | |- |
| + | | |
| + | | |
| + | * |
| + | |} |
| | | |
− | <div id="user_list">
| + | === routing.py === |
− | {% for friend in friends %}
| + | 들어온 경로와 컨슈머를 연결해준다. urls.py와 view.py의 관계. |
− | {% if friend.room %}
| |
− | <li name="{{ friend.room.id }}"><a href="/chat/{{ friend.room.id }}/"
| |
− | <div>{{ friend.user }}</div>
| |
− | </div>
| |
| | | |
− | <div id="time_line"> | + | 다음과 같은 형태로 컨슈머를 연결한다.<syntaxhighlight lang="python"> |
− | <div id="main_section">
| + | websocket_urlpatterns = [ |
− | <div id="feed">
| + | re_path(r'ws/notification/(?P<center_name>\w+)/$', consumers.NotificationConsumer.as_asgi()), |
− | <div id="text_field">
| + | ] |
− | <input type="text" id="txt">
| |
− | <button type="submit" id="btn">전송</button>
| |
− | </div>
| |
− | </div>
| |
− | </div>
| |
− | </div>
| |
| </syntaxhighlight> | | </syntaxhighlight> |
| + | |
| + | === [컨슈머는 컨슈머 문서로 옮김] === |
| [[분류:장고 웹소켓]] | | [[분류:장고 웹소켓]] |