即时通讯
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
gevent是一个第三方库,基于greenlet实现协程,可以实现并发:
当一个greenlet遇到IO操作时,比如访问网络,就自动切换到其他的greenlet,等到IO操作完成,再在适当的时候切换回来继续执行。由于IO操作非常耗时,经常使程序处于等待状态,有了gevent为我们自动切换协程,就保证总有greenlet在运行,而不是等待IO。遇到I/O自动切换,其实是基于I/O模型的IO multiplexing
5.2.思考:协程切换的场景?
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))