即時通訊

pigzww發表於2020-10-16

即時通訊

1.即時通訊簡介

即時通訊(Instant Messaging)是一種基於網際網路的即時交流訊息的業務。

型別:

  • 線上push
    • 適用:web頁面 和 App
    • 自己構建IM伺服器
      • 使用WebSocket
      • 採用成熟的框架方案Socket.IO
      • 對於App還可自己封裝socket
    • 使用第三方IM服務商提供的服務
  • 離線push
    • 適用:App
    • 對於iOS,使用APNs
    • 對於andorid,使用FCM(國外)或第三方IM服務商提供的服務

提供第三方IM服務的服務商有:

  • 網易雲信
  • 融雲
  • 環信
  • LeanCloud

需求場景

服務端需要主動推送訊息給客戶端,例如

  • 使用者下了訂單,需要在運營管理後臺向運營人員推送新訂單通知
  • 使用者A關注了使用者B,系統需要向使用者B推送提示訊息
  • 即時聊天

傳統的推送實現

# 使用http協議實現
# 策略:輪詢
輪詢是在特定的的時間間隔(如每1秒),由客戶端對伺服器發出HTTP請求,瞭解伺服器有沒有新的資訊,然後由伺服器告知有無新資料或返回最新的資料給客戶端。

# Comet (基於長連線)

長輪詢 長輪詢是在開啟一條連線以後保持,等待伺服器推送來資料再關閉的方式。
iframe流 iframe流方式是在頁面中插入一個隱藏的iframe,利用其src屬性在伺服器和客戶端之間建立一條長連結,伺服器向iframe傳輸資料(通常是HTML,內有負責插入資訊的javascript),來實時更新頁面。

缺點:
依然需要反覆發出請求,而且長連線也會消耗伺服器資源。

03-WebSocket協議

WebSocket API中,瀏覽器和伺服器只需要完成一次握手兩者之間就直接可以建立永續性的連線,並進行雙向資料傳輸。

優點:

較少的控制開銷

更強的實時性---由於協議是全雙工的,所以伺服器可以隨時主動給客戶端下發資料

沒有同源限制,客戶端可以與任意伺服器通訊。

可以傳送文字,也可以傳送二進位制資料

更好的壓縮效果

04-建立socketio伺服器

# 1.安裝
pip install python-socketio



# 方式1  使用多程式多執行緒模式的單獨的WSGI伺服器對接(如uWSGI、gunicorn)
sio = socketio.Server()
# 打包成WSGI應用,可以使用WSGI伺服器託管執行
app = socketio.WSGIApp(sio)  # Flask  Django


# 方式2 作為Flask、Django 應用中的一部分
  sio = socketio.Server()
  app = socketio.WSGIApp(sio, app)

# 缺點: 
上述兩種都需要遵循WSGI協議,而WSGI協 議是開啟多程式多執行緒的方式執行,而我們對接的使用者特別多的情況,需要開啟成千上百個程式執行緒,肯定是效能低下的。


# 解決方案:
	在協程中執行

05-協程—(協同工作的程式)

概念:

	協程,又稱微執行緒,纖程,英文名Coroutine。
	不是程式也不是執行緒 理解成--不帶的函式、子程式 切換
  協程看上去也是程式呼叫,但執行過程中,在程式內部可中斷,然後轉而執行別的子程式,在適當的時候再返回來接著執行。

    
# yield  生成器

# 生成器定義
def func1():
    print("可不可以不上班?")
    # 暫停程式碼 儲存現場
    yield
    print("我養你呀!")
    yield 


# 生成器定義
def func2():
    print("不上班你養我啊?")
    # 暫停程式碼 儲存現場
    yield
    print("程式碼都敲不好,怎麼養活我?")


if __name__ == '__main__':

       genrate1 = func1()
       genrate2 = func2()
       next(genrate1)
       next(genrate2)
       next(genrate1)
       next(genrate2)



# 最大的優勢就是協程極高的執行效率。
執行緒切換 作業系統
子程式切換 程式
1>因為子程式切換不是執行緒切換,而是由程式自身控制,因此,沒有執行緒切換的開銷,和多執行緒比,執行緒數量越多,協程的效能優勢就越明顯。

2>第二大優勢就是不需要多執行緒的鎖機制,因為只有一個執行緒,也不存在同時寫變數衝突,在協程中控制共享資源不加鎖,只需要判斷狀態就好了,所以執行效率比多執行緒高很多。

# 問題:雖然協程是單執行緒執行,那怎麼利用多核CPU呢?

最簡單的方法是 多程式+協程,既充分利用多核,又充分發揮協程的高效率,可獲得極高的效能。

5.1.協程常用第三方庫

  • gevent

  • eventlet

gevent
gevent是一個第三方庫,基於greenlet實現協程,可以實現併發:

當一個greenlet遇到IO操作時,比如訪問網路,就自動切換到其他的greenlet,等到IO操作完成,再在適當的時候切換回來繼續執行。由於IO操作非常耗時,經常使程式處於等待狀態,有了gevent為我們自動切換協程,就保證總有greenlet在執行,而不是等待IO。遇到I/O自動切換,其實是基於I/O模型的IO multiplexing

5.2.思考:協程切換的場景?

  • 程式發生阻塞的時候切換

    • 讀磁碟
    • 讀寫檔案
    • 網路io操作
    • 收發http請求

06-使用協程的方式執行socketio伺服器

import socketio
import eventlet

# 將系統中所有io標準函式都替換eventlet同名的函式,遇到阻塞eventlet自動協程自動切換
#  read --> 非阻塞
# f.read()  --> eventlet.read()
eventlet.monkey_patch()


# 1.構建socketio伺服器--使用協程開啟伺服器
sio = socketio.Server(async_mode="eventlet")

# 2.獲取app物件,給協程呼叫的app
app = socketio.Middleware(sio)

# 需求:動態獲取埠號
# sys.argv :獲取終端命令列,後面的引數
# python server.py 8001
# sys.argv == [server.py,  8001]
if len(sys.argv) < 2:
    print("useage: python server.py [port] ")
    # 程式異常退出
    exit(1)

# 動態獲取埠號
port = int(sys.argv[1])


# sockct伺服器監聽的ip和埠
# '' 代表 0.0.0.0
SOCKET_ADDRESS = ('', port)
socket = eventlet.listen(SOCKET_ADDRESS)

# 繫結伺服器ip地址和埠號,sio伺服器使用協程執行
eventlet.wsgi.server(socket,app)

11-IM聊天服務端實現—傳送訊息

# im目錄中建立chat.py

from server import sio

"""
# IM聊天伺服器
# 需求:客戶端連線伺服器,給他傳送一個連線成功的訊息
# 需求:接受客戶端傳送的訊息資料,並且回覆對應訊息資料
# 前後端約定:訊息事件-message
# 資料格式:
    {
       "msg": data,
       "timestamp": 代表傳送訊息的時間戳
    }
"""
import time


# 需求:客戶端連線伺服器,給他傳送一個連線成功的訊息
@sio.on("connect")
def contect(sid, enviroment):
    """
    客戶端連線伺服器成功,自動呼叫
    :param sid: 連線的客戶端id
    :param enviroment: 第一次握手字典資料
    :return:
    """
    print("sid:{}".format(sid))
    print("enviroment:{}".format(enviroment))

    # 連線成功,向客戶端傳送一個訊息
    data = {
        "msg": "恭喜你連線成功",
        "timestamp": round(time.time())
    }
    sio.emit(event="message", data=data, room=sid)


# 需求:接受客戶端傳送的訊息資料,並且回覆對應訊息資料
@sio.on("message")
def get_client_message(sid, data):
    """
    接受客戶端傳送的訊息資料
    :param sid: 連線的客戶端id
    :param data: 客戶端傳送的訊息資料
    :return: 回覆對應訊息資料
    """
    reply_data = {
        "msg": "i have receive your message: {}".format(data),
        "timestamp": round(time.time())
    }
    # sio.emit(event="message", data=reply_data, room=sid)
    sio.send(data=reply_data, room=sid)

12-調整程式碼結構

# main.py   IM伺服器啟動檔案 
python main.py 8000

# server.py  
1.建立sio物件
2.將sio物件包裝成WSGI物件

# chat.py   IM伺服器聊天訊息檔案
1.@sio.on("connect") 連線IM伺服器
2.@sio.on("message") 接受回覆訊息

13-訊息通知—將訊息新增到rabbitmq訊息佇列

# 需求:訊息需要放到訊息佇列,新增RabbitMQ訊息佇列功能



# 使用Redis
  mgr = socketio.RedisManager('redis://')
  sio = socketio.Server(client_manager=mgr)


# 使用RabbitMQ -- 採用
# 安裝  pip install kombu

mgr = socketio.KombuManager('amqp://')
sio = socketio.Server(client_manager=mgr)


# ---------------修改server檔案補充的邏輯 ---------------
 
RABBITMQ = 'amqp://python:rabbitmqpwd@localhost:5672/toutiao'
# 建立socketio物件
# client_manager : 補充rabbitmq訊息佇列功能
sio = socketio.Server(async_model="eventlet", client_manager=client_manager)
app = socketio.Middleware(sio)
 


# ---------------Flask中補充的邏輯 ---------------
# 1.將sio_mgr物件儲存到app中
# 後續可以通過sio_mgr物件釋出要進行即時訊息推送的任務,由socketio伺服器從rabbitmq訊息佇列中取出任務,推送訊息
	app.sio_mgr = socketio.KombuManager(app.config['RABBITMQ'], write_only=True)
    
# 2.通過Kombu物件,呼叫emit方法,將關注成功的訊息,新增到rabbitmq訊息佇列中,攜帶了關注的目標使用者target

	current_app.sio_mgr.emit('following notify', data=_data, room=str(target))

相關文章