{"id":21285839,"url":"https://github.com/heatingma/chat-website-tutorial","last_synced_at":"2026-02-23T07:07:53.688Z","repository":{"id":214986393,"uuid":"734645102","full_name":"heatingma/Chat-Website-Tutorial","owner":"heatingma","description":"Chat Website Tutorial","archived":false,"fork":false,"pushed_at":"2024-01-01T16:25:27.000Z","size":42422,"stargazers_count":2,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-10-25T19:31:15.986Z","etag":null,"topics":["chatroom","chattingroom","tutorial"],"latest_commit_sha":null,"homepage":"https://gnetchat.cn","language":"HTML","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/heatingma.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null}},"created_at":"2023-12-22T08:17:53.000Z","updated_at":"2025-04-17T02:15:01.000Z","dependencies_parsed_at":"2024-01-01T18:42:11.995Z","dependency_job_id":null,"html_url":"https://github.com/heatingma/Chat-Website-Tutorial","commit_stats":null,"previous_names":["heatingma/chat-website-tutorial"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/heatingma/Chat-Website-Tutorial","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/heatingma%2FChat-Website-Tutorial","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/heatingma%2FChat-Website-Tutorial/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/heatingma%2FChat-Website-Tutorial/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/heatingma%2FChat-Website-Tutorial/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/heatingma","download_url":"https://codeload.github.com/heatingma/Chat-Website-Tutorial/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/heatingma%2FChat-Website-Tutorial/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":29739024,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-02-23T04:51:08.365Z","status":"ssl_error","status_checked_at":"2026-02-23T04:49:15.865Z","response_time":90,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.5:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["chatroom","chattingroom","tutorial"],"created_at":"2024-11-21T11:25:55.549Z","updated_at":"2026-02-23T07:07:53.659Z","avatar_url":"https://github.com/heatingma.png","language":"HTML","readme":"# \u003ccenter\u003e聊天程序实验报告\u003c/center\u003e\n\n#### ps: 本实验报告以教程的形式呈现，所有实现细节都会进行详细说明\n\n## 一、实验环境\n\n#### Platform: Windows 11\n#### Language: Python\n#### Framework: Django\n\n\n## 二、基础学习\n\n#### 2.1 django\n\n* django学习网址：https://developer.mozilla.org/zh-CN/docs/Learn/Server-side/Django\n\n#### 2.2 Redis\n\n* redis学习网址：https://www.zhihu.com/tardis/bd/art/487583440?source_id=1001\n\n#### 2.3 websocket\n\n* websocket学习网址：https://blog.csdn.net/weixin_45172107/article/details/126590769\n\n## 三 安装实验环境\n\n### 3.1 打开管理员界面依次输入\n\n```bash\nconda create --name chat_env python=3.8\nconda activate chat_env\npip config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple\npip install django==4.2.5\npip install channels==3.0.5\npip install channels-redis==4.1.0\npip install pillow==10.0.1\npip install pypinyin\n```\n\n### 3.2 管理员界面构建django项目\n\n``` bash\ndjango-admin startproject Chat_Website_Tutorial\n```\n\n### 3.3 创建聊天和用户两个模块\n\n``` bash\ncd Chat_Website_Tutorial\npython manage.py startapp users\npython manage.py startapp chat\n```\n\n### 3.4 在Chat_Website_Tutorial下的settings.py中注册模块\n![](https://notes.sjtu.edu.cn/uploads/upload_f3aefa1933397134fd38a9a7e585fbbe.png)\n\n\n### 3.5 下载Redis(windows)\n\n* 下载链接：https://github.com/tporadowski/redis/releases\n* 选择最新版本（Redis for Windows 5.0.14.1）\n* 解压到Chat_Website_Tutorial文件夹，重命名为redis\n\n### 3.6 创建三个文件夹\n\n* 在Chat_Website_Tutorial文件夹下创建templates、media、static文件夹\n* 在settings.py中将以下代码进行修改\n\n```python\nALLOWED_HOSTS = []\n\nTIME_ZONE = \"UTC\"\n\nSTATIC_URL = \"static/\"\n```\n修改为：\n\n```python\nALLOWED_HOSTS = ['*']\n\nTIME_ZONE = \"Asia/Shanghai\"\n\nimport os\nSTATIC_URL = \"static/\"\nSTATICFILES_DIRS = [\n    os.path.join(BASE_DIR, 'static'),\n    os.path.join(BASE_DIR, 'static/css'),\n    os.path.join(BASE_DIR, 'static/js'),\n]\n```\n\n* 在TEMPLATES的DIRS中添加'./templates'\n```python\nTEMPLATES = [\n    {\n        \"BACKEND\": \"django.template.backends.django.DjangoTemplates\",\n        'DIRS': ['./templates'],\n        \"APP_DIRS\": True,\n        \"OPTIONS\": {\n            \"context_processors\": [\n                \"django.template.context_processors.debug\",\n                \"django.template.context_processors.request\",\n                \"django.contrib.auth.context_processors.auth\",\n                \"django.contrib.messages.context_processors.messages\",\n            ],\n        },\n    },\n]\n```\n\n* 添加以下代码\n```python\nMEDIA_ROOT = BASE_DIR / 'media'\nMEDIA_URL = '/media/'\nASGI_APPLICATION = 'Chat_Website_Tutorial.asgi.application'\n```\n\n### 3.7 目录结构\n\n![](https://notes.sjtu.edu.cn/uploads/upload_77b3b99b1319b037e804847894205f5f.png)\n\n\n\n## 四、用户登录界面\n\n### 4.1 前端页面制作\n\n* 在templates中新建users目录，在里面创建log.html并设计登录界面\n* 在static文件夹下创建css、js、images三个文件夹，分别放css文件、javascript文件和静态图片\n\n### 4.2 修改settings.py文件\n\n* 在Chat_Website_Tutorial/settings.py中添加\n\n### 4.3 创建User类\n* 在users/models.py中添加用户类，以下是一个样例\n\n```python\nfrom django.db import models\nfrom django.contrib.auth.models import AbstractUser\n\nclass User(AbstractUser):\n    email = models.EmailField('email address', primary_key=True, unique=True)\n    USERNAME_FIELD = 'email'\n    REQUIRED_FIELDS = [\"username\"]\n```\n\n### 4.4 注册User类\n* users/admin.py中注册User类\n\n```python\nfrom django.contrib import admin\nfrom django.contrib.auth.admin import UserAdmin\nfrom .models import User\n\nadmin.site.register(User, UserAdmin)\n```\n\n### 4.5 创建User相关的表单\n* 由于用户注册和登录涉及到表单的填写与提交，因此需要在Users中创建forms.py文件，并在其中创建相关表单\n```python\nfrom django.contrib.auth.forms import UserCreationForm\nfrom django import forms\nfrom .models import User\n\nclass RegisterForm(UserCreationForm):\n    email = forms.CharField()\n    email_code = forms.CharField()\n    class Meta:\n        model = User\n        fields = (\"username\", \"email\", \"email_code\", \"password1\", \"password2\")\n        \nclass LoginForm(forms.Form):\n    login_email = forms.CharField()\n    login_password = forms.CharField()\n```\n\n### 4.6 添加请求处理函数\n* django在处理Http请求时是通过自定义的视图函数进行处理的，需要在views.py中添加处理登录或者注册请求的函数\n```python\nfrom django.shortcuts import render, redirect\nfrom django.http import HttpRequest\nfrom .forms import LoginForm, RegisterForm\nfrom django.contrib.auth import login, authenticate\nfrom django.contrib import messages\n\n\ndef log(request: HttpRequest):\n    # action when request method is GET\n    if request.method == 'GET':\n        register_form = RegisterForm()\n        login_form = LoginForm()\n        # context to render\n        context = {\n            \"register_form\": register_form,\n            \"login_form\": login_form,\n        }\n    # action when request method is POST\n    elif request.method == 'POST':\n        # transform the request post to Forms \n        register_form = RegisterForm(request.POST)\n        login_form = LoginForm(request.POST)\n        # check whether login success\n        if login_form.is_valid():\n            email = login_form.cleaned_data[\"login_email\"]\n            password = login_form.cleaned_data[\"login_password\"]\n            # We check if the data is correct\n            user = authenticate(email=email, password=password)\n            if user:  # If the returned object is not None\n                login(request, user)  # we connect the user\n                return redirect('chat:my')\n            else:  # otherwise an error will be displayed\n                context = {\n                    \"register_form\": register_form,\n                    \"login_form\": login_form,\n                    \"login_error\": \"Error email or Error password!\"\n                }\n        # check whether regist success\n        elif register_form.is_valid():\n            user = register_form.save()\n            login(request, user)\n            messages.success(request, \"Congratulations, you are now a registered user!\")\n            return redirect('chat:my')\n        # collect errors\n        else:\n            # return errors for user\n            username_errors = register_form.errors.get('username')\n            email_errors = register_form.errors.get('email')\n            password_errors = register_form.errors.get('password2')\n            context = {\n                \"register_form\": register_form,\n                \"login_form\": login_form,\n                \"username_errors\": username_errors,\n                \"email_errors\": email_errors,\n                \"password_errors\":  password_errors,\n            }            \n\n    return render(\n        request=request, \n        template_name='users/log.html', \n        context = context\n    )\n```\n* log函数的输入是一个Http请求，函数首先判断请求的方式是GRT还是POST，然后分别进行处理，最后调用我们在4.1中制作完成的users/log.html模板，并将模板中需要的参数以字典的形式传入\n\n\n### 4.7 添加访问路径(url)\n* 在users中建立url.py文件\n```python\nfrom django.urls import path\nfrom users import views\n\nurlpatterns = [\n    path('', views.log, name='log'),\n]\n```\n* 修改Chat_Website_Tutorial/urls.py如下\n```python\nfrom django.contrib import admin\nfrom django.urls import path\nfrom django.conf.urls import include\nfrom django.conf import settings\nfrom django.conf.urls.static import static\n\nurlpatterns = [\n    path(\"admin/\", admin.site.urls),\n    path('', include(('users.urls', 'users'), namespace='users'), name='users'),\n]\n\nurlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)\n\n```\n* 添加url之后，当在浏览器访问根目录（对应''）时，就会到log界面，调用log函数处理http请求\n\n\n### 4.8 更新数据库\n* 在根目录下创建update.py文件，内容如下\n```python\nimport os\n\nos.system(\"python manage.py makemigrations\")\nos.system(\"python manage.py migrate\")\nos.system(\"python manage.py runserver\")\n```\n\n### 4.9页面展示\n* 在浏览器中输入 http://127.0.0.1:8000/， 可以看到如下界面\n![](https://notes.sjtu.edu.cn/uploads/upload_a4e5ca5521b7850e09d161843a8f0863.png)\n\n\n### 4.10创建超级用户\n* 在根目录下创建superuser.py文件\n```python\nimport os\nos.system(\"python manage.py createsuperuser\")\n```\n\n### 4.11注册两个测试账号\n\n* 根据注册要求注册test001和test002两个账号\n\n![](https://notes.sjtu.edu.cn/uploads/upload_06902894e35d6cc975a676cb569e7208.png)\n* 正常注册的时候应该看到如下界面\n![](https://notes.sjtu.edu.cn/uploads/upload_d9d35ba53e3b2926f7629d087b4979ee.png)\n* 这是因为我们在log函数中写了以下代码\n```python\n# check whether regist success\nelif register_form.is_valid():\n    user = register_form.save()\n    login(request, user)\n    messages.success(request, \"Congratulations, you are now a registered user!\")\n    return redirect('chat:my')\n```\n* 当注册成功时会重定向到chat模块的my页面，但是由于我们暂时还没有写这一部分，因此会报错\n* 当我们用4.10中创建的超级用户登录管理员界面 http://127.0.0.1:8000/admin， 可以看到两个账户已经被创建成功了\n\n![](https://notes.sjtu.edu.cn/uploads/upload_bcba73537cb0282958ceeb54fc35f822.png)\n\n\n## 五、用户主页设计\n\n### 5.1 用户主页模板设计\n\n* 将一个页面分成4个part部分\n    * side_menus：侧边栏\n    * leftsidebar：左侧部分\n    * body：主体部分\n    * rightsidebar：右侧部分\n* 利用django特有的block机制，首先设计一个base.html，所有的页面都继承base.html，然后每一个html页面根据需求按照上述四个部分（不一定全部需要）分别设计\n* 例如my页面设计如下：\n\n```htmlmixed=\n{% extends 'chat/base.html' %}\n\n\u003c!-- side_menu --\u003e\n{% block side_menu %}\n    {% include \"chat/side_menus/side_menu_my.html\" %}\n{% endblock %}\n\u003c!-- side_menu --\u003e\n\n\u003c!-- leftsidebar --\u003e\n{% block leftsidebar %}\n    {% include \"chat/leftsidebar/leftsidebar_my.html\" %}\n{% endblock %}\n\u003c!-- leftsidebar --\u003e\n\n\u003c!-- body --\u003e\n{% block chat_conversation %}\n    {% include \"chat/body/body_my.html\" %}\n{% endblock %}\n\u003c!-- body --\u003e\n```\n\n* 编写好相关的html模板，如下所示\n\n![](https://notes.sjtu.edu.cn/uploads/upload_4757528bdfb5d58f91d28839a51e61d6.png)\n\n### 5.2 用户主页相关类的实现\n\n* 与第四章介绍的一样，我们需要创建一些类、表单、路径、视图函数等，具体代码由于过长就不在这里一一列出，详细请直接参考代码文件包，以下是需要创建的相关内容。\n* Profile类：用户的个人画像类\n* EditProfileForm表单：修改Profile\n* PasswordChangeForm表单：修改密码\n* my函数，用户主页视图函数\n* settings函数，用户设置页面视图函数\n* chatroom函数，用户聊天界面视图函数（在第六章实现，这里需要放一个空函数）\n* innerroom函数，用户聊天界面内部视图函数（在第六章实现，这里需要放一个空函数）\n```python\n@login_required\ndef chatroom(request: HttpRequest, dark=False):\n    pass\n    \n@login_required\ndef innerroom(request: HttpRequest, room_name, post_name, dark=False):\n   pass\n```\n\n### 5.3 signal\n\n* 在设计User类的时候，并没有考虑到Profile类的初始化。实际上，我们希望的是在User类被创建的时候，就创建一个对应的Profile类，这需要通过信号函数实现\n* 在chat文件夹下创建signal.py文件\n```python\nfrom django.db.models.signals import post_save\nfrom django.dispatch import receiver\nfrom users.models import User\nfrom .models import Profile\n\n@receiver(post_save, sender=User)\ndef create_profile(sender, instance, created, **kwargs):\n    if created:\n        Profile.objects.create(user=instance)\n```\n* 修改chat/apps.py如下\n```python\nfrom django.apps import AppConfig\n\nclass ChatConfig(AppConfig):\n    default_auto_field = \"django.db.models.BigAutoField\"\n    name = \"chat\"\n\n    def ready(self):\n        from . import signals\n```\n\n### 5.4 用管理员删除用户并重新注册用户\n\n* 在执行了上述几个步骤后运行update，会发现以下情况\n\n![](https://notes.sjtu.edu.cn/uploads/upload_3058c9f8deb3139489da279521e7e92e.png)\n\n* 这是因为我们在注册用户之前还没有考虑过5.3，这时候需要我们登录管理员账号删除原本创建的两个用户，然后重新注册即可\n\n![](https://notes.sjtu.edu.cn/uploads/upload_2d555547348166d665d0d8dcba75e0c6.png)\n\n### 5.5 用户主页展示\n\n![](https://notes.sjtu.edu.cn/uploads/upload_a1acde8daf9e8996d17c581021f86084.png)\n\n### 5.6 更改用户个人信息\n\n* 在settings界面上传个人图片并修改个人介绍，地点等\n\n\n![](https://notes.sjtu.edu.cn/uploads/upload_3cde262dc6e4be0f0d328305eae3c151.png)\n\n\n\n## 六、用户聊天设计\n\n### 6.1 实时聊天原理：WebSocket\n* 什么是WebSocket\n    * WebSocket 是一种网络传输协议，可在单个 TCP 连接上进行全双工通信，位于 OSI 模型的应用层\n\n* 为什么使用WebSocket\n    * WebSocket可以在浏览器里使用且使用很简单\n    * 客户端和服务器只需要完成一次握手，两者之间就可以创建持久性的连接，并进行双向数据传输。\n\n* WebSocket建立过程\n\n    * 客户端发送一个 HTTP GET 请求到服务器，请求的路径是 WebSocket 的路径（类似 ws://http://example.com/socket）。请求中包含一些特殊的头字段，如 Upgrade: websocket 和 Connection: Upgrade，以表明客户端希望升级连接为 WebSocket。\n    * 服务器收到这个请求后，会返回一个 HTTP 101 状态码（协议切换协议）。同样在响应头中包含 Upgrade: websocket 和 Connection: Upgrade，以及一些其他的 WebSocket 特定的头字段，例如 Sec-WebSocket-Accept，用于验证握手的合法性。\n    * 客户端和服务器之间的连接从普通的 HTTP 连接升级为 WebSocket 连接。之后，客户端和服务器之间的通信就变成了 WebSocket 帧的传输，而不再是普通的 HTTP 请求和响应。\n\n### 6.2 用户聊天相关类的实现\n\n* 与5.2一样，这里列出用户聊天相关类、表单等的简介\n* models\n    * Room类：聊天室\n    * Tag类：标签\n    * Post类：帖子\n    * RoomMessage：聊天内容\n* forms\n    * RoomForm：创建聊天室的表单\n    * PostForm：创建帖子的表单\n    * AttachmentForm：附件相关表单\n    * ChangeRoomForm：修改聊天室信息表单\n    * EditPostForm：编辑帖子信息的表单\n    * ConfirmDeletePostForm：删除帖子的表单\n    * ConfirmDeleteChatroomForm：删除聊天室的表单\n* views\n    * chatroom：聊天室\n    * innerroom：帖子\n* 帖子和聊天室的关系\n    * 每一个聊天室作为聊天的基本空间单位\n    * 每一个聊天室在创建时会自动创建一个以chatting_开头的帖子，用于聊天\n    * 在聊天室内部可以任意创建帖子，用于讨论相关话题\n\n### 6.3 关键技术——roomers\n\n* 在chat文件夹中创建roomers.py文件\n```python\nimport json\nfrom asgiref.sync import async_to_sync\nfrom channels.generic.websocket import WebsocketConsumer\nfrom .models import Room, RoomMessage, Post\n\n\nclass Roommers(WebsocketConsumer):\n    \"\"\"\n    The member of the Room\n    \"\"\"\n    def __init__(self, *args, **kwargs):\n        super().__init__(args, kwargs)\n        self.room_name = None\n        self.room_group_name = None\n        self.room = None\n        self.user = None\n        self.user_inbox = None\n```\n\n* Roomers继承了Django库中一个重要的通信消费者类WebsocketConsumer\n* Django Channels是一个用于构建实时Web应用程序的扩展，它使用了WebSocket和其他协议来实现实时通信。\n* WebsocketConsumer用于处理WebSocket连接和消息的消费者，以下是它的一些重要方法\n    * connect(self)：当一个WebSocket连接建立时，Channels将调用connect方法，在这个方法中执行与连接相关的初始化工作\n    * disconnect(self, close_code)：当WebSocket连接关闭时，Channels将调用disconnect方法，执行一些清理工作。\n    * receive(self, text_data=None, bytes_data=None)：当WebSocket接收到消息时，Channels将调用receive方法，处理接收到的消息。\n    * send(self, text_data=None, bytes_data=None, close=False)：通过WebSocket发送消息给客户端。\n    * group_send(self, group, message)：将消息发送给一个指定的组，用于实现广播消息或群聊功能。\n\n* 以下是我对WebsocketConsumer类的connect的重构\n    * 首先通过scope中包含的信息获得当前聊天室名称和帖子名称\n    * room_group_name是一个特殊的变量，用于唯一标志特定的群组\n    * 通过聊天室名称确定用户所在的room，并向当前用户发送他所在room的所有用户列表\n    * 判断用户合法性，若合法，则向所在room的群组广播当前用户进入的信息\n    * 对room实体的online变量添加当前用户\n```python\ndef connect(self):\n    # read info from self.scope\n    self.room_name = self.scope['url_route']['kwargs']['room_name']\n    self.post_name = self.scope['url_route']['kwargs']['post_name']\n    self.room_group_name = f'chat_chatroom_{self.room_name}_{self.post_name}'\n    self.room = Room.objects.get(name=self.room_name)\n    self.user = self.scope['user']\n    self.user_inbox = f'inbox_{self.user.username}'\n    self.accept()\n    async_to_sync(self.channel_layer.group_add)(\n        self.room_group_name,\n        self.channel_name,\n    )\n\n    # send all online users by self.send func\n    self.send(json.dumps({\n        'type': 'user_list',\n        'users': [user.username for user in self.room.online.all()],\n    }))\n\n    # check if the user is valid\n    if self.user.is_authenticated:\n        # create a user inbox for private messages\n        async_to_sync(self.channel_layer.group_add)(\n            self.user_inbox,\n            self.channel_name,\n        )\n        # send the join event to the room\n        async_to_sync(self.channel_layer.group_send)(\n            self.room_group_name,\n            {\n                'type': 'user_join',\n                'user': self.user.username,\n            }\n        )\n        self.room.online.add(self.user)\n```\n* disconnect原理与connect一致，代码不在这里展示\n* 以下是我对WebsocketConsumer类的receive的重构\n    * 这一部分与后面将会介绍的websocket.js相关\n    * 当用户收到了消息，首先用json解析出其中的消息字典\n    * 然后判断消息字典中是否存在\"uid\"的键值，若存在，说明这是一个删除聊天记录的命令\n    * 若不存在，说明这是一个添加聊天的命令，需要向所在群组的所有用户广播这个聊天信息\n```python\ndef receive(self, text_data=None, bytes_data=None):\n\n    text_data_json = json.loads(text_data)\n    if \"uid\" in text_data_json:\n        uid = text_data_json['uid']\n        rm = RoomMessage.objects.get(uid = uid)\n        rm.delete()\n    else:\n        message = text_data_json['message']\n        post_name = text_data_json['post_name']\n\n        # check if the user is valid\n        if not self.user.is_authenticated:\n            return\n\n        async_to_sync(self.channel_layer.group_send)(\n            self.room_group_name,\n            {\n                'type': 'chat_message',\n                'message': message,\n                'user': self.user.username,\n            }\n        )\n\n        RoomMessage.objects.create(\n            user=self.user, \n            room=self.room, \n            belong_post = Post.objects.get(title=post_name, belong_room=self.room),\n            content=message\n        )\n```\n\n### 6.4 asgi\n\n* 在创建完毕roomers文件之后，需要再创建一个routing.py文件\n```python\nfrom django.urls import re_path\nfrom .roomers import Roommers, FRRoommers\n\nwebsocket_urlpatterns = [\n    re_path(r'ws/chat/chatroom/(?P\u003croom_name\u003e\\w+)/(?P\u003cpost_name\u003e\\w+)$', \n            Roommers.as_asgi()),\n]\n```\n\n\n* 然后修改Chat_Website_Tutorial/asgi.py如下\n```python\nimport os\nimport django\n\nos.environ.setdefault('DJANGO_SETTINGS_MODULE', 'website.settings')\ndjango.setup()\n\nfrom django.core.asgi import get_asgi_application\nfrom channels.routing import ProtocolTypeRouter,URLRouter\nfrom channels.auth import AuthMiddlewareStack\nimport chat.routing\n\napplication = ProtocolTypeRouter({\n  'http': get_asgi_application(),\n  'websocket': AuthMiddlewareStack(\n        URLRouter(\n            chat.routing.websocket_urlpatterns\n        )\n    ),\n})\n\n```\n\n* 这么做的主要原因是为了将http请求和websocket请求分发到不同的处理程序\n\n\n### 6.5 关键技术——websocket.js\n\n* 在本报告中很少涉及css和js的介绍，这是因为作者认为读者应当自己掌握，但是websocket.js中涉及到了通信相关的内容，因此需要单独讲解一下其中涉及到的重要函数\n\n* 以下是添加消息函数，这里采用的是当用户发送消息的时候，采用在js中添加html文本来实现实时聊天内容的增加\n```javascript=\n// add message\nfunction add_message(user, message){\n    img_url = user_img_urls[user];\n    var flag = 0;\n    if (img_url == undefined \u0026\u0026 flag == 0) {\n        location.reload();\n        flag = 1;\n    }\n    if (user != cur_user){\n        chatLog.innerHTML += \n    `\u003cli\u003e\u003cdiv class=\"conversation-list\" style=\"max-width: 40%;\u003e\n        \u003c!-- HIS OR HER AVATAR --\u003e\n        \u003cdiv class=\"chat-avatar\"\u003e\u003cimg src=${img_url} alt=\"\"\u003e\u003c/div\u003e\n        \u003c!-- HIS OR HER AVATAR --\u003e\n        \u003c!-- CONTENT MAIN --\u003e\n        \u003cdiv class=\"user-chat-content\" style=\"max-width: 100%;\"\u003e\n            \u003cdiv class=\"ctext-wrap\"\u003e\n                \u003c!-- CONTENT \u0026 TIME --\u003e\n                \u003cdiv class=\"ctext-wrap-content\" style=\"max-width: 100%;\"\u003e\n                    \u003cp class=\"mb-0\" style=\"word-break:break-all;\"\u003e\n                        ${message}\n                    \u003c/p\u003e\n                \u003c/div\u003e\n                \u003c!-- CONTENT \u0026 TIME --\u003e\n            \u003c/div\u003e\n            \u003c!-- HIS OR HER NAME --\u003e\n            \u003cdiv class=\"conversation-name\"\u003e${user}\u003c/div\u003e\n        \u003c/div\u003e\n        \u003c!-- CONTENT --\u003e\n    \u003c/div\u003e\u003c/li\u003e`}\n    else{        \n        chatLog.innerHTML +=\n    `\u003cli class=\"right\"\u003e\u003cdiv class=\"conversation-list\" style=\"max-width: 40%;\"\u003e\n        \u003c!-- HIS OR HER AVATAR --\u003e\n        \u003cdiv class=\"chat-avatar\"\u003e\u003cimg src=${img_url} alt=\"\"\u003e\u003c/div\u003e\n        \u003c!-- HIS OR HER AVATAR --\u003e\n        \u003c!-- CONTENT MAIN --\u003e\n        \u003cdiv class=\"user-chat-content\" style=\"max-width: 100%;\"\u003e\n            \u003cdiv class=\"ctext-wrap\"\u003e\n                \u003c!-- CONTENT \u0026 TIME --\u003e\n                \u003cdiv class=\"ctext-wrap-content\" style=\"max-width: 100%;\"\u003e\n                \u003cp class=\"mb-0\" style=\"word-break:break-all; text-align: left;\"\u003e\n                    ${message}\n                \u003c/p\u003e\n                \u003c/div\u003e\n                \u003c!-- CONTENT \u0026 TIME --\u003e\n            \u003c/div\u003e\n            \u003c!-- HIS OR HER NAME --\u003e\n            \u003cdiv class=\"conversation-name\"\u003e${user}\u003c/div\u003e\n        \u003c/div\u003e\n        \u003c!-- CONTENT --\u003e\n    \u003c/div\u003e\u003c/li\u003e`\n    }\n}\n```\n\n* 以下是有用户进入或者离开时的处理\n```python\n# onlineUsersSelectorAdd\nfunction onlineUsersSelectorAdd(user){\n    if (document.querySelector(\"option[value='\" + user + \"']\")) return;\n    let newOption = document.createElement(\"option\");\n    newOption.value = user;\n    newOption.innerHTML = user;\n    onlineUsersSelector.appendChild(newOption);\n}\n\n# removes an option from 'onlineUsersSelector'\nfunction onlineUsersSelectorRemove(user) {\n    let oldOption = document.querySelector(\"option[value='\" + user + \"']\");\n    if (oldOption !== null) oldOption.remove();\n}\n```\n\n* 以下是连接函数\n* 其中\"chat_message\"、\"user_list\"这些type都是与chat/roomers.py中发送的消息字典对应的\n* new WebSocket( ) 中的路径是与chat/routing.py中的路径是对应的\n```javascript=\n// connect\nfunction connect() {\n    chatSocket = new WebSocket(\"ws://\" + \n                               window.location.host + \n                               \"/ws/chat/chatroom/\" + \n                               room_name +\"/\" + \n                               cur_post\n                              );\n    // connect the WebSocket\n    chatSocket.onopen = function(e) {\n        console.log(\"Successfully connected to the WebSocket.\");\n    }\n    // deal with connection error\n    chatSocket.onclose = function(e) {\n        console.log(\"WebSocket connection closed unexpectedly. \n                    Trying to reconnect in 2s...\");\n        setTimeout(function() {\n            console.log(\"Reconnecting...\");\n            connect();\n        }, 2000);\n    };\n    // deal with message error \n    chatSocket.onerror = function(err) {\n        console.log(\"WebSocket encountered an error: \" + err.message);\n        console.log(\"Closing the socket.\");\n        chatSocket.close();\n    }\n    // send message\n    chatSocket.onmessage = function(e) {\n        const data = JSON.parse(e.data);\n        switch (data.type) {\n            case \"chat_message\":\n                add_message(data.user, data.message);\n                break;\n            case \"user_list\":\n                for (let i = 0; i \u003c data.users.length; i++) \n                    onlineUsersSelectorAdd(data.users[i]);\n                break;\n            case \"user_join\":\n                onlineUsersSelectorAdd(data.user);\n                break;\n            case \"user_leave\":\n                onlineUsersSelectorRemove(data.user);\n                break;\n            default:\n                console.error(\"Unknown message type!\");\n                break;\n        }\n        chatLog_container.scrollTop = chatLog.scrollHeight;\n    }\n    \n}\n```\n\n### 6.6 signal\n* 由于要求创建聊天室时自动创建一个帖子，并且我们希望聊天室在创建时会有一个创建成功的聊天记录，因此需要在chat/signal.py文件添加：\n```python\nfrom .models import Profile, RoomMessage, Room, Post, Tag\nfrom django.shortcuts import get_object_or_404\n\n@receiver(post_save, sender=Room)\ndef create_rm(sender, instance, created, **kwargs):\n    if created:\n        # create the default chatting_post\n        author = User.objects.get(username=instance.owner_name)\n        profile = get_object_or_404(Profile, user=author)\n        post = Post.objects.create(\n            title =  \"chatting_\" + instance.name,\n            author = author, \n            author_profile = profile,\n            about_post = \"The special and default post for chatting\",\n            belong_room=instance, \n        )\n        try:\n            post.tags.add(get_object_or_404(Tag, name=\"default\"))\n            post.tags.add(get_object_or_404(Tag, name=\"chatting\"))\n        except:\n            Tag.objects.create(name=\"default\")\n            Tag.objects.create(name=\"chatting\")\n            post.tags.add(get_object_or_404(Tag, name=\"default\"))\n            post.tags.add(get_object_or_404(Tag, name=\"chatting\"))\n        \n        # create the defult success message for the chatting_post\n        content = \"Congratulations to {} for creating a new chatroom \\\n            named {}\".format(instance.owner_name, instance.name)   \n        RoomMessage.objects.create(\n            user=author, \n            belong_post = post,\n            room=instance, \n            content=content\n        )\n```\n\n\n### 6.7 redis\n\n* Redis（Remote Dictionary Server）是一个开源的内存数据存储系统，它提供了高性能、可扩展和灵活的键值存储。\n* 在Chat_Website_Tutorial/settings.py添加以下代码，把channels的后端设置成redis\n```python\nCHANNEL_LAYERS = {\n    'default': {\n        'BACKEND': 'channels_redis.core.RedisChannelLayer',\n        'CONFIG': {\n            \"hosts\": [('localhost', 6379)],\n        },\n    },\n}\n```\n\n### 6.8 页面展示\n\n![](https://notes.sjtu.edu.cn/uploads/upload_c2f531acb137c093540b6722d844ef80.png)\n\n\n## 七、实验总结与附加材料说明\n\n### 7.1 程序文件列表\n\n![](https://notes.sjtu.edu.cn/uploads/upload_a3da0026b1401fbc700aa0d71ca318b7.png)\n\n### 7.2 体会与建议\n\n* 聊天软件的制作有三大难点，一个是前端页面的制作，一个是django的系统学习，一个是websocket的熟练运用，这些都需要大量的前置学习\n* 如何设计美观的界面是制作软件前必须要思考的问题，需要花较长时间去寻找和制作合适的html模板\n* websocket、django的WebsocketConsumer类以及websocket.js以及asgi这些对象或者文件的关系需要熟练掌握\n* 课程建议：建议可以将大作业改为多人合作\n\n### 7.3 软件后续\n\n* 目前软件实现了多人聊天、互传照片与文件等，后续还可以增加一些好友等其他功能\n* 软件可以尝试搭建在服务器上，添加域名、升级成https和wss等\n\n### 7.4附加材料说明\n\n* 【启动redis.mp4】windows平台命令行启动redis server\n* 【注册登录用户界面.mp4】如何注册账户以及修改用户个人信息\n* 【用户聊天.mp4】两个用户的基本实时聊天（可支持多个用户）\n* 【文件传输.mp4】用户之间文件和照片的传输，需要刷新显示\n* 【消息撤回.mp4】撤回发送后的消息，需要刷新显示\n* 【模式切换与退出.mp4】黑夜与白天模式的切换以及退出登录\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fheatingma%2Fchat-website-tutorial","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fheatingma%2Fchat-website-tutorial","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fheatingma%2Fchat-website-tutorial/lists"}