websocket

小满三岁啦發表於2024-05-06

websocket,web版的socket

原web中:

  • http協議,無狀態&短連結
    • 客戶端主動連線伺服器
    • 客戶端向伺服器傳送訊息,伺服器收到返回資料
    • 客戶端收到資料
    • 斷開連線
  • https一些 + 對資料進行加密。

我們在開發過程中想要保留一些狀態資訊,基於cookie來做

現在支援:

  • http協議。一次請求一次響應。

  • websocket協議,建立持久的連線不斷開,基於這個連線可以進行收發資料。【服務端向客戶端主動推送訊息】

    • web聊天室
    • 實時圖表格,柱狀圖,餅圖(highcharts)

websocket原理

  • http協議
    • 連線
    • 資料傳輸
    • 埠連線
  • webscoket協議,是建立在http協議之上的
    • 連線,客戶端發起
    • 握手(驗證),客戶端傳送一個訊息,後端接收到訊息再做一些特殊處理並返回。服務端支援websocket。
    • 收發資料(加密)
    • 斷開連線。

django框架

django預設不支援websocket,需要安裝元件:

pip install channels
pip install daphne

配置

  • 註冊channels, daphne 必須在 INSTALLED_APPS 中的 django.contrib.staticfiles 之前,所以建議這兩個app註冊到其他之前,daphne又需要在channels之前。

    INSTALLED_APPS = [
        "daphne", # 註冊
        "channels",  # 註冊
        "django.contrib.admin",
        "django.contrib.auth",
        "django.contrib.contenttypes",
        "django.contrib.sessions",
        "django.contrib.messages",
        "django.contrib.staticfiles",
        
    ]
    
  • 在settings.py中新增asgi_application

    ASGI_APPLICATION = "專案名.asgi.application"  # django3會自動生成application在asgi.py模組中
    
  • 修改asgi.py檔案

    import os
    from django.core.asgi import get_asgi_application
    from channels.routing import ProtocolTypeRouter, URLRouter
    
    from . import routing
    
    
    os.environ.setdefault("DJANGO_SETTINGS_MODULE", "ws_demo.settings")
    
    # application = get_asgi_application()  預設的
    
    application = ProtocolTypeRouter({
        "http": get_asgi_application(),  # 自動走urls.py 找檢視 就是之前正常的流程
        "websocket": URLRouter(routing.websocket_urlpatterns), # 建立的routing就相當於之前的urls.py 建立的consumer.py 就相當於之前的views.py
    })
    
  • 在settings.py的同級目錄建立routing.py

    from django.urls import re_path, path
    
    from app01 import consumers
    
    websocket_urlpatterns = [
        # re_path(r"ws/(?P<group>\w+)/$", consumers.ChatConsumer.as_asgi()),
        path("ws/", consumers.ChatConsumer.as_asgi())
    ]
    
  • 在app01(就是你的app)目錄下建立consumers.py,編寫處理websocket的業務邏輯。

    from channels.generic.websocket import WebsocketConsumer
    from channels.exceptions import StopConsumer
    
    
    class ChatConsumer(WebsocketConsumer):
        # 有客戶端向後端傳送websocket連線的請求時,自動觸發。
        def websocket_connect(self, message):
            
            # 服務端允許客戶端創立連線
            self.accept()
        
        # 瀏覽器基於websocket向後端傳送資料,自動觸發接收訊息。
        def websocket_receive(self, message):
            print(message)
            
            self.send("不要回復訊息 不要")
            # self.close() 服務端也可以主動斷開連線, self.close()
            
        # 客戶端主動埠連線時,會自動觸發
        def websocket_disconnect(self, message):
            print("連線已斷開")
            raise StopConsumer()
    

    在django中需要了解的

    wsgi,之前預設使用的都是wsgi

    image-20240503234131360

    asgi,等於wsgi + 非同步 + websocket

    image-20240503233034812

http

urls.py
views.py

websocket

routings.py
consumers.py

聊天室(單個人)

  • 訪問地址看到聊天室的頁面,HTTP請求。

  • 讓客戶端主動向服務端發起websocket連線,服務端接受到連線後透過(握手)。

    • 客戶端:

      const socket = new WebSocket("ws://127.0.0.1:8888/ws/");
      
      // 必須以ws:開頭 ws結尾
      
    • 服務端

      from channels.generic.websocket import WebsocketConsumer
      from channels.exceptions import StopConsumer
      
      
      class ChatConsumer(WebsocketConsumer):
          # 有客戶端向後端傳送websocket連線的請求時,自動觸發。
          def websocket_connect(self, message):
              print('收到請求了')
              # 服務端允許客戶端創立連線
              self.accept()
              
      
  • 收發訊息

    • 客戶端
    socket.onopen = (event)=>{
        socket.send("hello server!")
    };
    
    • 服務端

class ChatConsumer(WebsocketConsumer):
# 上面的程式碼和之前一樣
def websocket_receive(self, message):
# message是一個字典 {'type': 'websocket.receive', 'text': 'hello server!'}
print("收到客戶端的訊息", message.get('text')) # hello server!
# 服務端給客戶端發訊息
self.send("不要回復訊息 不要")
# self.close() 服務端也可以主動斷開連線, self.close()
```

  • 收發訊息(服務端主動發給客戶端)

    from channels.generic.websocket import WebsocketConsumer
    from channels.exceptions import StopConsumer
    
    
    class ChatConsumer(WebsocketConsumer):
        # 有客戶端向後端傳送websocket連線的請求時,自動觸發。
        def websocket_connect(self, message):
            # 服務端允許客戶端創立連線
            self.accept()
            # 連線建立後 服務端主動給客戶端傳送訊息
            self.send('客官您來啦~') 
            # self 代表連線物件
    
  • 斷開連線(客戶端主動斷開)

    • 客戶端

      socket.close()
      
    • 服務端

      from channels.generic.websocket import WebsocketConsumer
      from channels.exceptions import StopConsumer
      
      
      class ChatConsumer(WebsocketConsumer):
      	# 其他程式碼一樣
          # 客戶端主動埠連線時,會自動觸發
          def websocket_disconnect(self, message):
              print("連線已斷開")
              # 這個異常表示同意斷開
              raise StopConsumer()
      
    • 斷開連線(客戶端傳送關鍵字,服務端收到後先斷開服務端,然後觸發客戶端的回撥函式onclose)

      • 客戶端

        socket.send('關閉')
        
      • 服務端

        from channels.generic.websocket import WebsocketConsumer
        from channels.exceptions import StopConsumer
        
        
        class ChatConsumer(WebsocketConsumer):
        	# 其他程式碼一樣
            def websocket_receive(self, message):
                text = message['text']
                if text == '關閉':
                    self.close() # 執行這個會觸發客戶端的 socket.onclose()回撥函式,也會執行服務端自己的斷開websocket_disconnect,總結就是隻要客戶端和服務端誰執行了close,整個連線就都斷開了。
        
      • 客戶端

        // 服務端主動斷開連線的時候,該方法會被觸發
        socket.onclose = (event)=>{
            console.log("連結已關閉");
        }
        
      • 補充

        # 如果想要服務端主動斷開連線的時候,不觸發websocket_disconnect,那麼在服務端斷開之後,raise StopConsumer()即可
         def websocket_receive(self, message):
                text = message['text']
                if text == '關閉':
                    self.close() # 執行這個會觸發客戶端的 socket.onclose()回撥函式
                    raise StopConsumer()
                self.send(text + "謝謝!")
                
            # 客戶端主動埠連線時,會自動觸發,也就是這裡只觸發客戶端斷開的邏輯,而不是傳送關鍵字讓服務端斷開的邏輯。
            def websocket_disconnect(self, message):
                print("連線已斷開")
                raise StopConsumer()
        

程式碼整合

# 後端
from channels.generic.websocket import WebsocketConsumer
from channels.exceptions import StopConsumer


class ChatConsumer(WebsocketConsumer):
    def websocket_connect(self, message):
        self.accept()
        
    def websocket_receive(self, message):
        text = message['text']
        if text == '關閉':
            self.close() 
            raise StopConsumer()
        self.send(text + "謝謝!")

    def websocket_disconnect(self, message):
        print("連線已斷開")
        raise StopConsumer()
// 前端
const socket = new WebSocket("ws://127.0.0.1:8888/ws/");

// 建立好連線自動觸發(服務端self.accept())後自動觸發。
socket.onopen = (event)=>{
    this.isShow = true
    // 往服務端傳送訊息
    socket.send(this.msg)
};

// 當websocket收到服務端的訊息時,會自動觸發這個函式
// 資料放在event裡面,透過event.data能獲取到資料的內容
socket.onmessage = (event)=>{
    this.messageList.push({
        id: this.messageList.length + 1,
        data: event.data
    })
};

// 如果有錯誤
socket.onerror = (error)=>{
    console.log("websocket error:", error);
};

// 服務端主動斷開連時,這個方法會觸發
socket.onclose = (event)=>{
    swal('服務端已斷開連結')
    console.log("websocket connection closed:", event);
}
},

小結:

基於django實現websocket請求,但只能對某個人進行處理。主要就是後端的self

聊天室(群聊一)

修改後端即可

from channels.generic.websocket import WebsocketConsumer
from channels.exceptions import StopConsumer

# 1. 建立一個列表,儲存全部連線物件
# 因為每一個self都是一個連線物件,所以把他接入到一個列表中
CONN_LIST= []

class ChatConsumer(WebsocketConsumer):
    def websocket_connect(self, message):
        self.accept()
        
        CONN_LIST.append(self) # 把連線物件加入到列表

    def websocket_receive(self, message):
        text = message['text']
        
        for conn in CONN_LIST: # 迴圈遍歷列表,拿到每一個連線物件,給每一個連線物件發訊息
            conn.send(text + "訊息已收到,這是自動回覆。")
        
    def websocket_disconnect(self, message):
        CONN_LIST.remove(self) # 當連線斷開後,把這個連線物件送物件列表中移除
        raise StopConsumer()

前端程式碼

<template>
  <div class="home">
    <div class="content">
      <p v-show="isShow">[連結建立成功]</p>
      <hr v-show="isShow">
      <ul v-for="(item, index) in messageList" :key="item.id">
        <li>收到來自服務端的訊息:{{ index }}-{{ item.data }}</li>
      </ul>
    </div>
    
    <div class="inner">
      <label for="message">請輸入:</label>
      <el-input id="message" v-model="msg"></el-input>

      <p>
      <el-row>
        <el-button type="success" @click="sendWS">傳送</el-button>
        <el-button type="danger" @click="closeConn">關閉連線</el-button>
        <el-button type="warning" @click="messageList=[]">清空訊息</el-button>
      </el-row>
    </p>
    </div>
    
  </div>
</template>

<script>


export default {
  data() {
    return {
      msg: '',
      isShow: false,
      messageList: []
    }
  },
  methods: {
    sendWS(){
      const socket = new WebSocket("ws://127.0.0.1:8888/");

      // 建立好連線自動觸發(服務端self.accept())後自動觸發。
      socket.onopen = (event)=>{
        this.isShow = true
        // 往服務端傳送訊息
        socket.send(this.msg)
      };

      // 當websocket收到服務端的訊息時,會自動觸發這個函式
      // 資料放在event裡面,透過event.data能獲取到資料的內容
      socket.onmessage = (event)=>{
        this.messageList.push({
          id: this.messageList.length + 1,
          data: event.data
        })

      };

      // 如果有錯誤
      socket.onerror = (error)=>{
        console.log("websocket error:", error);
      };

      // 服務端主動斷開連時,這個方法會觸發
      socket.onclose = (event)=>{
        swal('服務端已斷開連結')
        console.log("websocket connection closed:", event);
      }
    },

    // 關閉連線函式
    closeConn(){
      swal("關閉連線函式已觸發")
      socket.close()
    }
  },
  created(){
    let token = sessionStorage.getItem("token")
    if (!token){
      swal('請先登入').then(()=>{
        this.$router.push("/login")
      })
    }
  }
}
</script>

<style scoped>
  .home .content {
    border: 1px solid #dddddd;
    width: 800px;
    height: 500px;
    margin: 50px auto;
    overflow: auto;
  }
  .home .inner {
    width: 800px;
    margin: 50px auto;
  }
</style>

聊天室(群聊二)

基於channels中提供的channel layers來實現

  • settings中配置

    CHANNEL_LAYERS = {
        "default": {
            "BACKEND": "channels.layers.InMemoryChannelLayer"
        }
    }
    

    如果想要換成redis的

    pip3 install channels-redis
    
    # 寫法1
    CHANNEL_LAYERS = {
        "default": {
            "BACKEND": "channels_redis.core.RedisChannelLayer",
            "CONFIG": {
                "hosts": [('IP', 6379)]
            },
        },
    }
    
    # 寫法2
    CHANNEL_LAYERS = {
        'default': {
        'BACKEND': 'channels_redis.core.RedisChannelLayer',
        'CONFIG': {"hosts": ["redis://IP:6379/1"],},
        },
    } 
    
    # 寫法3
    CHANNEL_LAYERS = {
        "default": {
            "BACKEND": "channels_redis.core.RedisChannelLayer",
            "CONFIG": {
                "hosts": ["redis://:password@IP:6379/0"],
                "symmetric_encryption_keys": [SECRET_KEY],
            },
        },
    }
    
  • consumers中新增特殊程式碼

    from channels.generic.websocket import WebsocketConsumer
    from channels.exceptions import StopConsumer
    

from asgiref.sync import async_to_sync

class ChatConsumer(WebsocketConsumer):
    # 有客戶端向後端傳送websocket連線的請求時,自動觸發。
    def websocket_connect(self, message):
        # 服務端允許客戶端創立連線
        self.accept()

        # 將這個客戶端的連線物件加入到某個地方(內容 or redis)
        # xxx 表示一個群名,隨便寫 slef.channel_name 表示一個隨機的別名
        # 需要將非同步轉成同步,用同步的方式做
        self.group = self.scope['url_route']['kwargs']['room_id']
        async_to_sync(self.channel_layer.group_add)(self.group, self.channel_name)

    # 瀏覽器基於websocket向後端傳送資料,自動觸發接收訊息。
    def websocket_receive(self, message):
        text = message['text']
        if text == '關閉':
            self.close() # 執行這個會觸發客戶端的 socket.onclose()回撥函式
            raise StopConsumer()
            return
        
        # 通知組內的所有客戶端,執行xx_oo 方法,在此方法中可以自己取定義任意的功能,只是在欄位的值中需要寫成 xx.oo
        async_to_sync(self.channel_layer.group_send)(self.group, {"type":"xx.oo", "message": message})
        
    def xx_oo(self, event):
        text = event["message"]['text']
        # 這裡的send表示給組裡的每一個人send不是單獨發哪一個,就是群聊裡面發
        self.send(text + "噠咩")
        
    # 客戶端主動埠連線時,會自動觸發
    def websocket_disconnect(self, message):
        print("連線已斷開")
        # 連線斷開,將這個連線從組裡移除掉
        async_to_sync(self.channel_layer.group_discard)(self.group, self.channel_name)
        raise StopConsumer()
```
  • routing的路徑為

    from django.urls import  path
    
    from app01 import consumers
    
    websocket_urlpatterns = [
        path("chat/<str:room_id>/", consumers.ChatConsumer.as_asgi())
    
  • 前端訪問的地址為

    const socket = new WebSocket("ws://127.0.0.1:8888/chat/123/");
    

簡單總結

  • websocket是什麼?協議。
  • django中實現websocket,channels元件。
    • 單獨連線和收發資料
    • 手動建立連線表 & channel layers

相關文章