魔坊APP项目-16-种植园、websocket协议、服务端基于socket提供服务(基于房间管理分发信息)、种植园页面展示
种植园
我们需要完成的种植园,是一个互动频繁,并且要求有一定即时性的模块,所以如果继续基于http协议开发,那么需要通过ajax发送大量http请求,同时因为http本身属于单向通讯,所以服务端无法主动发送信息提供给客户端。所以对于客户端使用来说,非常不友好,所以我们需要基于socket通讯来完成这个模块的开发。当然,如果我们服务端基于socket实现tcp/ip通讯的同时,那么客户端必须也要使用websocket来实现tcp/ip通讯才能正常运作。
一、websocket协议
文档:https://tools.ietf.org/html/rfc6455
一直以来,HTTP是无状态、单向通信的网络协议,即客户端请求一次,服务器回复一次,默认情况下,只允许浏览器向服务器发出请求后,服务器才能返回相应的数据。如果想让服务器消息及时下发到客户端,需要采用类似于轮询的机制,大部分情况就是客户端通过定时器使用ajax频繁地向服务器发出请求。这样的做法效率很低,而且HTTP数据包头本身的字节量较大,浪费了大量带宽和服务器资源。
为了提高效率,HTML5推出了WebSocket技术。
WebScoket是一种让客户端和服务器之间能进行全双工通信(full-duplex)的技术。它是HTML最新标准HTML5的一个协议规范,本质上是个基于TCP的协议,它通过HTTP/HTTPS协议发送一条特殊的请求进行握手后创建了一个TCP连接,此后浏览器/客户端和服务器之间便可随时随地以通过此连接来进行双向实时通信,且交换的数据包头信息量很小。
同时为了方便使用,HTML5提供了非常简单的操作就可以让前端开发者直接实现socket通讯,开发者只需要在支持WebSocket的浏览器中,创建Socket之后,通过onopen、onmessage、onclose、onerror四个事件的实现即可处理Socket的响应。
注意:websocket是HTML5技术的一部分,但是websocket并非只能在浏览器或者HTML文档中才能使用,事实上在python或者C++等语言中只要能实现websocket协议报文,均可使用。
客户端报文:
GET /mofang/websocket HTTP/1.1
Host: 127.0.0.1
Origin: http://127.0.0.1:5000
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: sN9cRrP/n9NdMgdcy2VJFQ== # Sec-WebSocket-Key 是随机生成的
Sec-WebSocket-Version: 13
服务端报文:
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: HSmrc0sMlYUkAGmm5OPpG2HaGWk= # 结合客户端提供的Sec-WebSocket-Key基于固定算法计算出来的
Sec-WebSocket-Protocol: chat
WebSocket与Socket的关系
他们两的关系就像Java和JavaScript,并非完全没有关系,只能说有点渊源。
Socket严格来说,其实并不是一个协议,而是为了方便开发者使用TCP或UDP协议而对TCP/IP协议进行封装出来的一组接口,是位于应用层和传输控制层之间的接口。通过Socket接口,我们可以更简单,更方便的使用TCP/IP协议。
WebSocket是实现了浏览器与服务器的全双工通信协议,一个模拟Socket的应用层协议。
二、服务端基于socket提供服务
在python中实现socket服务端的方式有非常多,一种最常用的有python-socketio
,而我们现在使用的flask框架也有一个基于python-socket
模块进行了封装的flask-socketio
模块.
官方文档:https://flask-socketio.readthedocs.io/en/latest/
注意:
因为目前还有会存在一小部分的设备或者应用是不支持websocket的.所以为了保证功能的可用性,我们使用socektio,但是由此带来了2个问题,必须要注意的:
- python服务端使用基于socketio进行通信服务,则另一端必须也是基于socetio来进行对接通信,否则无法进行通信
- socketio还有一个版本对应的问题, 版本不对应则无法通信.回报版本错误.
如果使用了javascript io 1.x或者2.x版本,则python-socketio或者flask-socketio的版本必须是4.x
如果使用了javascriptio 3.x版本,则python-socketio或者flask-socketio的版本必须是5.x.
我们当前使用的flask-socketio版本是5.x,所以javasctipt的socketio版本就必须是3.x.
终端下执行命令,安装:
pip install flask-socketio
pip install gevent-websocket
模块初始化,application/__init__.py
,代码:
import os,sysfrom flask import Flask
from flask_script import Manager
from flask_sqlalchemy import SQLAlchemy
from flask_redis import FlaskRedis
from flask_session import Session
from flask_migrate import Migrate, MigrateCommand
from flask_jsonrpc import JSONRPC
from flask_marshmallow import Marshmallow
from flask_jwt_extended import JWTManager
from flask_admin import Admin
from flask_babelex import Babel
from faker import Faker
from flask_pymongo import PyMongo
from flask_qrcode import QRcode
from flask_socketio import SocketIOfrom application.utils import init_blueprint
from application.utils.config import load_config
from application.utils.session import init_session
from application.utils.logger import Log
from application.utils.commands import load_command# 创建终端脚本管理对象
manager = Manager()# 创建数据库链接对象
db = SQLAlchemy()# redis链接对象
redis = FlaskRedis()# Session存储对象
session_store = Session()# 数据迁移实例对象
migrate = Migrate()# 日志对象
log = Log()# jsonrpc模块实例对象
jsonrpc = JSONRPC()# 数据转换器的对象创建
ma = Marshmallow()# jwt认证模块实例化
jwt = JWTManager()# flask_admin模块实例化
admin = Admin()# flask_babelex模块实例化
babel = Babel()# mongoDB
mongo = PyMongo()# qrcode
QRCode = QRcode()# socketio
socketio = SocketIO()def init_app(config_path):"""全局初始化"""# 创建app应用对象app = Flask(__name__)# 项目根目录app.BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))# 加载导包路径sys.path.insert(0, os.path.join(app.BASE_DIR,"application/utils/language"))# 加载配置Config = load_config(config_path)app.config.from_object(Config)# 数据库初始化db.init_app(app)app.db = dbredis.init_app(app)mongo.init_app(app)# 数据转换器的初始化ma.init_app(app)# session存储初始化init_session(app)session_store.init_app(app)# 数据迁移初始化migrate.init_app(app, db)# 添加数据迁移的命令到终端脚本工具中manager.add_command('db', MigrateCommand)# 日志初始化app.log = log.init_app(app)# 蓝图注册init_blueprint(app)# jsonrpc初始化jsonrpc.service_url = "/api" # api接口的url地址前缀jsonrpc.init_app(app)# jwt初始化jwt.init_app(app)# admin初始化admin.init_app(app)# 国际化本地化模块的初始化babel.init_app(app)# 初始化终端脚本工具manager.app = app# 数据种子生成器[faker]app.faker = Faker(app.config.get('LANGUAGE'))# qrcode初始化配置QRCode.init_app(app)# socketiosocketio.init_app(app, cors_allowed_origins=app.config['CORS_ALLOWED_ORIGINS'], async_mode=app.config['ASYNC_MODE'], debug=app.config['DEBUG'])# 改写runserver命令if sys.argv[1] == 'runserver':manager.add_command('run', socketio.run(app, host=app.config['HOST'], port=app.config['PORT']))# 注册自定义命令load_command(manager)return manager
配置文件,application/settings/dev.py
,代码:
# socketioCORS_ALLOWED_ORIGINS = '*'ASYNC_MODE = NoneHOST = '0.0.0.0'PORT = 5000
application/utils/__init__.py
,在加载蓝图的过程中,自动加载socket服务端的api,代码:
def init_blueprint(app):"""自动注册蓝图"""blueprint_path_list = app.config.get("INSTALLED_APPS")# 加载admin站点总配置文件try:import_module(app.config.get('ADMIN_PATH'))except:passfor blueprint_path in blueprint_path_list:blueprint_name = blueprint_path.split(".")[-1]# 自动创建蓝图对象blueprint = Blueprint(blueprint_name,blueprint_path)# 蓝图自动注册和绑定视图和子路由url_module = import_module(blueprint_path+".urls") # 加载蓝图下的子路由文件for url in url_module.urlpatterns: # 遍历子路由中的所有路由关系blueprint.add_url_rule(**url) # 注册到蓝图下# 读取总路由文件url_path = app.config.get("URL_PATH")urlpatterns = import_module(url_path).urlpatterns # 加载蓝图下的子路由文件url_prefix = "" # 蓝图路由前缀for urlpattern in urlpatterns:if urlpattern["blueprint_path"] == blueprint_name+".urls":url_prefix = urlpattern["url_prefix"]break# 注册模型import_module(blueprint_path+".models")# 加载蓝图内部的admin站点配置try:import_module(blueprint_path + '.admin')except:pass# 加载蓝图内部的socket接口try:import_module(blueprint_path + '.socket')except:pass# 注册蓝图对象到app应用对象中, url_prefix 蓝图的路由前缀app.register_blueprint(blueprint, url_prefix=url_prefix)
因为我们是基于python-socketio
模块提供的服务端,所以客户端必须基于socketIO.js
才能与其进行通信,所以客户端引入socketio.js。
socket.io.js的官方文档: https://socket.io/docs/v3
socket.io.js的github: https://github.com/socketio/socket.io/releases
我们可以新建一个orchard.html作为将来种植园模块的主页面,并在这个页面中使用socketio和服务端的flask-socketIO进行通信。
html/orchard
代码:
<!DOCTYPE html>
<html>
<head><meta charset="UTF-8"><title></title><script type="text/javascript" src="../static/js/socket.io.js"></script>
</head>
<body><script>// 命名空间namespace = '/mofang';var socket = io.connect('ws://192.168.20.180:5000' + namespace, {transports:['websocket']});socket.on('connect', function(){console.log('客户端连接socket服务端');});</script>
</body>
</html>
修改html/login.html
<!DOCTYPE html>
<html lang="en">
<head><title>首页</title><meta charset="UTF-8"><meta name="viewport" content="width=device-width,minimum-scale=1.0,maximum-scale=1.0,user-scalable=no"><meta name="format-detection" content="telephone=no,email=no,date=no,address=no"><link rel="stylesheet" href="../static/css/main.css"><script src="../static/js/vue.js"></script><script src="../static/js/axios.js"></script><script src="../static/js/main.js"></script><script src="../static/js/uuid.js"></script><script src="../static/js/settings.js"></script>
</head>
<body><div class="app" id="app"><img class="music" :class="music_play?'music2':''" @click="music_play=!music_play" src="../static/images/player.png"><div class="bg"><img src="../static/images/bg0.jpg"></div><ul><li><img class="module1" @click='go_orchard' src="../static/images/image1.png"></li><li><img class="module2" @click="go_home" src="../static/images/image2.png"></li><li><img class="module3" src="../static/images/image3.png"></li><li><img class="module4" src="../static/images/image4.png"></li></ul></div><script>apiready = function(){init();new Vue({el:"#app",data(){return {music_play:true, // 默认播放背景音乐prev:{name:"",url:"",params:{}}, // 上一页状态current:{name:"index",url:"index.html","params":{}}, // 下一页状态}},watch:{music_play(){if(this.music_play){this.game.play_music("../static/mp3/bg1.mp3");}else{this.game.stop_music();}}},created(){this.app_listener();this.check_user_login();},methods:{app_listener(){// 使用appintenr监听并使用appParam接收URLScheme的参数// 收集操作保存起来,并跳转到注册页面.// 注册frame中, 用户注册成功以后,记录邀请信息.api.addEventListener({name: 'appintent' // 当前事件监听必须是唯一的,整个APP中只能编写一次,否则冲突导致监听无效}, (ret, err)=>{var appParam = ret.appParam;this.game.print(typeof appParam); // {"uid":"15"}// 保存URLScheme参数到本地this.game.fsave(appParam);// 跳转到注册页面this.game.goWin('user', 'register.html', this.current);});},check_user_login(){let token = this.game.get('access_token') || this.game.fget('access_token');this.game.checkout(this, token, (new_access_token)=>{if(new_access_token.errno == 1005){this.game.save({'access_token': ''});this.game.fremove('access_token');}});},go_home(){if(this.game.get('access_token') || this.game.fget('access_token')){this.game.goWin('user','user.html', this.current);}else {this.game.goWin('user','login.html', this.current);}},go_orchard(){if(this.game.get('access_token') || this.game.fget('access_token')){this.game.goWin('orchard','orchard.html', this.current);}else {this.game.goWin('user','login.html', this.current);}}}})}</script>
</body>
</html>
服务端创建并注册蓝图目录orchard,终端命令如下:
cd application/apps/
python ../../manage.py blue -n=orchard
application/urls.py
,代码:
from application.utils import include
urlpatterns = [include("", "home.urls"),include("/users", "users.urls"),include("/marsh", "marsh.urls"),include("/orchard", "orchard.urls"),
]
applicaion/settings/dev.py
,代码:
# 注册蓝图INSTALLED_APPS = ["application.apps.home","application.apps.users","application.apps.marsh","application.apps.orchard",]
1.创建socket连接
在蓝图下面创建socket.py文件,并提供连接接口, orchard/socket.py
:
from application import socketio
from flask import request@socketio.on('connect', namespace='/mofang')
def user_connect():# request.sid socketIO基于客户端生成的唯一会话IDprint('用户%s连接过来了!' % request.sid)@socketio.on('disconnect', namespace='/mofang')
def user_disconnect():print('用户%s退出了种植园' % request.sid)
2.客户端vue结合socketio
html/orchard.html
客户端代码:
<!DOCTYPE html>
<html>
<head><title>用户中心</title><meta name="viewport" content="width=device-width,minimum-scale=1.0,maximum-scale=1.0,user-scalable=no"><meta charset="utf-8"><link rel="stylesheet" href="../static/css/main.css"><script src="../static/js/vue.js"></script><script src="../static/js/axios.js"></script><script src="../static/js/main.js"></script><script src="../static/js/uuid.js"></script><script src="../static/js/settings.js"></script><script src="../static/js/socket.io.js"></script>
</head>
<body><div class="app orchard" id="app"><img class="music" :class="music_play?'music2':''" @click="music_play=!music_play" src="../static/images/player.png"><div class="orchard-bg"><img src="../static/images/bg2.png"><img class="board_bg2" src="../static/images/board_bg2.png"></div><img class="back" @click="go_index" src="../static/images/user_back.png" alt=""></div><script>apiready = function(){init();new Vue({el:"#app",data(){return {music_play:true,namespace: '/mofang_orchard',token:"",socket: null,timeout: 0,prev:{name:"",url:"",params:{}},current:{name:"orchard",url:"orchard.html",params:{}},}},created(){this.checkout();},methods:{checkout(){var token = this.game.get("access_token") || this.game.fget("access_token");this.game.checkout(this,token,(new_access_token)=>{this.connect();});},connect(){// socket连接this.socket = io.connect(this.settings.socket_server + this.namespace, {transports: ['websocket']});this.socket.on('connect', ()=>{this.game.print("开始连接服务端");});},go_index(){this.game.outWin("orchard");},}});}</script>
</body>
</html>
js/settings.js
代码:
function init(){if (Game) {var game = new Game("../mp3/bg1.mp3");Vue.prototype.game = game;}server_url = 'http://192.168.20.180:5000';if(axios){// 初始化axiosaxios.defaults.baseURL = server_url+"/api" // 服务端api接口网关地址axios.defaults.timeout = 2500; // 请求超时时间axios.defaults.withCredentials = false; // 跨域请求资源的情况下,忽略cookie的发送Vue.prototype.axios = axios;Vue.prototype.uuid = UUID.generate;}// 接口相关的配置项Vue.prototype.settings = {captcha_app_id: "2041284967", // 腾讯防水墙验证码应用IDavatar_url: server_url+"/users/avatar",code_url: server_url,socket_server: 'ws://192.168.20.180:5000',socket_namespace: '/mofang',}
}
css样式,main.css
,代码:
.app .orchard-bg{margin: 0 auto;width: 100%;max-width: 100rem;position: absolute;;z-index: -1;top: -6rem;
}
.app .orchard-bg .board_bg2{position: absolute;top: 1rem;
}
.orchard .back{position: absolute;width: 3.83rem;height: 3.89rem;z-index: 1;top: 2rem;left: 2rem;
}
.orchard .music{right: 2rem;
}
.orchard .header{position: absolute;top: 0rem;left: 0;right: 0;margin: auto;width: 32rem;height: 19.28rem;
}.orchard .info{position: absolute;z-index: 1;top: 0rem;left: 4.4rem;width: 8rem;height: 9.17rem;
}
.orchard .info .avata{width: 8rem;height: 8rem;position: relative;
}
.orchard .info .avatar_bf{position: absolute;z-index: 1;margin: auto;width: 6rem;height: 6rem;top: 0;bottom: 0;left: 0;right: 0;
}
.orchard .info .user_avatar{position: absolute;z-index: 1;width: 6rem;height: 6rem;margin: auto;top: 0;bottom: 0;left: 0;right: 0;border-radius: 1rem;
}
.orchard .info .avatar_border{position: absolute;z-index: 1;margin: auto;top: 0;bottom: 0;left: 0;right: 0;width: 7.2rem;height: 7.2rem;
}
.orchard .info .user_name{position: absolute;left: 8rem;top: 1rem;width: 11rem;height: 3rem;line-height: 3rem;font-size: 1.5rem;text-shadow: 1px 1px 1px #aaa;border-radius: 3rem;background: #ff9900;text-align: center;
}.orchard .wallet{position: absolute;top: 3.4rem;right: 4rem;width: 16rem;height: 10rem;
}
.orchard .wallet .balance{margin-top: 1.4rem;float: left;margin-right: 1rem;
}
.orchard .wallet .title{color: #fff;font-size: 1.2rem;width: 6.4rem;text-align: center;
}
.orchard .wallet .title img{width: 1.4rem;margin-right: 0.2rem;vertical-align: sub;height: 1.4rem;
}
.orchard .wallet .num{background: url("../images/btn3.png") no-repeat 0 0;background-size: 100%;width: 6.4rem;font-size: 0.8rem;color: #fff;height: 2rem;line-height: 1.8rem;text-indent: 1rem;
}
.orchard .header .menu-list{position: absolute;top: 9rem;left: 2rem;
}
.orchard .header .menu-list .menu{color: #fff;font-size: 1rem;float: left;width: 4rem;height: 4rem;text-align: center;margin-right: 2rem;
}
.orchard .header .menu-list .menu img{width: 3.33rem;height: 3.61rem;display: block;margin: auto;margin-bottom: 0.4rem;
}
.orchard .footer{position: absolute;width: 100%;height: 6rem;bottom: -2rem;background: url("../images/board_bg3.png") no-repeat -1rem 0;background-size: 110%;
}
.orchard .footer .menu-list{width: 100%;height: 4rem;display: flex;position: absolute;top: -1rem;
}
.orchard .footer .menu-list .menu,
.orchard .footer .menu-list .menu-center{float: left;width: 4.44rem;height: 5.2rem;font-size: 1.5rem;color: #fff;line-height: 4.44rem;text-align: center;background: url("../images/btn5.png") no-repeat 0 0;background-size: 100%;flex: 1;margin-left: 4px;margin-right: 4px;
}
.orchard .footer .menu-list .menu-center{background: url("../images/btn6.png") no-repeat 0 0;background-size: 100%;flex: 2;
}
3.基于事件接受信息
①基于未定义事件进行通信
html/orchard.html
客户端代码:
<!DOCTYPE html>
<html>
<head><title>用户中心</title><meta name="viewport" content="width=device-width,minimum-scale=1.0,maximum-scale=1.0,user-scalable=no"><meta charset="utf-8"><link rel="stylesheet" href="../static/css/main.css"><script src="../static/js/vue.js"></script><script src="../static/js/axios.js"></script><script src="../static/js/main.js"></script><script src="../static/js/uuid.js"></script><script src="../static/js/settings.js"></script><script src="../static/js/socket.io.js"></script>
</head>
<body><div class="app orchard" id="app"><img class="music" :class="music_play?'music2':''" @click="music_play=!music_play" src="../static/images/player.png"><div class="orchard-bg"><img src="../static/images/bg2.png"><img class="board_bg2" src="../static/images/board_bg2.png"></div><img class="back" @click="go_index" src="../static/images/user_back.png" alt=""></div><script>apiready = function(){init();new Vue({el:"#app",data(){return {music_play:true, token:"",socket: null,timeout: 0,prev:{name:"",url:"",params:{}},current:{name:"orchard",url:"orchard.html",params:{}},}},created(){this.checkout();},methods:{checkout(){var token = this.game.get("access_token") || this.game.fget("access_token");this.game.checkout(this,token,(new_access_token)=>{this.connect();});},connect(){// socket连接this.socket = io.connect(this.settings.socket_server + this.settings.socket_namespace, {transports: ['websocket']});this.socket.on('connect', ()=>{this.game.print("开始连接服务端");this.login();});},login(){var id = this.game.fget('id');// 通过send方法可以直接发送数据,不需要自定义事件,数据格式是json格式this.socket.send({'uid': id});},go_index(){this.game.outWin("orchard");},}});}</script>
</body>
</html>
application/apps/orchard/socket.py
服务端代码:
from application import socketio
from flask import request# 建立socket通信
@socketio.on('connect', namespace='/mofang')
def user_connect():# request.sid socketIO基于客户端生成的唯一会话IDprint('用户%s连接过来了!' % request.sid)# 断开socket通信
@socketio.on('disconnect', namespace='/mofang')
def user_disconnect():print('用户%s退出了种植园' % request.sid)# 未定义事件通信,客户端没有指定事件名称
@socketio.on('message', namespace='/mofang')
def user_message(data):print('接收到来自%s发送的数据:' % request.sid)print(data)print(data['uid'])"""用户eVUPnaT_AqIAPlyJAAAB连接过来了!接收到来自eVUPnaT_AqIAPlyJAAAB发送的数据:{'uid': '51'}51"""
②基于自定义事件进行通信
html/orchard.html
客户端代码:
<!DOCTYPE html>
<html>
<head><title>用户中心</title><meta name="viewport" content="width=device-width,minimum-scale=1.0,maximum-scale=1.0,user-scalable=no"><meta charset="utf-8"><link rel="stylesheet" href="../static/css/main.css"><script src="../static/js/vue.js"></script><script src="../static/js/axios.js"></script><script src="../static/js/main.js"></script><script src="../static/js/uuid.js"></script><script src="../static/js/settings.js"></script><script src="../static/js/socket.io.js"></script>
</head>
<body><div class="app orchard" id="app"><img class="music" :class="music_play?'music2':''" @click="music_play=!music_play" src="../static/images/player.png"><div class="orchard-bg"><img src="../static/images/bg2.png"><img class="board_bg2" src="../static/images/board_bg2.png"></div><img class="back" @click="go_index" src="../static/images/user_back.png" alt=""></div><script>apiready = function(){init();new Vue({el:"#app",data(){return {music_play:true,token:"",socket: null,timeout: 0,prev:{name:"",url:"",params:{}},current:{name:"orchard",url:"orchard.html",params:{}},}},created(){this.checkout();},methods:{checkout(){var token = this.game.get("access_token") || this.game.fget("access_token");this.game.checkout(this,token,(new_access_token)=>{this.connect();});},connect(){// socket连接this.socket = io.connect(this.settings.socket_server + this.settings.socket_namespace, {transports: ['websocket']});this.socket.on('connect', ()=>{this.game.print("开始连接服务端");this.login();});},login(){var id = this.game.fget('id');// 通过send方法可以直接发送数据,不需要自定义事件,数据格式是json格式// this.socket.send({'uid': id});this.socket.emit('login', {'uid': id});},go_index(){this.game.outWin("orchard");},}});}</script>
</body>
</html>
application/apps/orchard/socket.py
服务端代码:
from application import socketio
from flask import request# 建立socket通信
@socketio.on('connect', namespace='/mofang')
def user_connect():# request.sid socketIO基于客户端生成的唯一会话IDprint('用户%s连接过来了!' % request.sid)# 断开socket通信
@socketio.on('disconnect', namespace='/mofang')
def user_disconnect():print('用户%s退出了种植园' % request.sid)# 未定义事件通信,客户端没有指定事件名称
@socketio.on('message', namespace='/mofang')
def user_message(data):print('接收到来自%s发送的数据:' % request.sid)print(data)print(data['uid'])"""用户eVUPnaT_AqIAPlyJAAAB连接过来了!接收到来自eVUPnaT_AqIAPlyJAAAB发送的数据:{'uid': '51'}51"""# 自定义事件通信
@socketio.on('login', namespace='/mofang')
def user_login(data):print('接收到来自客户端%s发送的数据:' % request.sid)print(data)print(data['uid'])"""用户tdpnmCy-44Bg1CJpAAAD连接过来了!接收到来自客户端tdpnmCy-44Bg1CJpAAAD发送的数据:{'uid': '51'}51"""
4.服务端响应信息
application/apps/orchard/socket.py
from application import socketio
from flask import request
from application.apps.users.models import User# 建立socket通信
@socketio.on('connect', namespace='/mofang')
def user_connect():# request.sid socketIO基于客户端生成的唯一会话IDprint('用户%s连接过来了!' % request.sid)# 主动响应数据给客户端length = User.query.count()socketio.emit('server_response', {'count': length}, namespace='/mofang')# 断开socket通信
@socketio.on('disconnect', namespace='/mofang')
def user_disconnect():print('用户%s退出了种植园' % request.sid)# 未定义事件通信,客户端没有指定事件名称
@socketio.on('message', namespace='/mofang')
def user_message(data):print('接收到来自%s发送的数据:' % request.sid)print(data)print(data['uid'])"""用户eVUPnaT_AqIAPlyJAAAB连接过来了!接收到来自eVUPnaT_AqIAPlyJAAAB发送的数据:{'uid': '51'}51"""# 自定义事件通信
@socketio.on('login', namespace='/mofang')
def user_login(data):print('接收来自客户端%s发送的数据:' % request.sid)print(data)print(data['uid'])"""用户tdpnmCy-44Bg1CJpAAAD连接过来了!接收到来自客户端tdpnmCy-44Bg1CJpAAAD发送的数据:{'uid': '51'}51"""
html/orchard.html
客户端接收响应信息,代码:
<!DOCTYPE html>
<html>
<head><title>用户中心</title><meta name="viewport" content="width=device-width,minimum-scale=1.0,maximum-scale=1.0,user-scalable=no"><meta charset="utf-8"><link rel="stylesheet" href="../static/css/main.css"><script src="../static/js/vue.js"></script><script src="../static/js/axios.js"></script><script src="../static/js/main.js"></script><script src="../static/js/uuid.js"></script><script src="../static/js/settings.js"></script><script src="../static/js/socket.io.js"></script>
</head>
<body><div class="app orchard" id="app"><img class="music" :class="music_play?'music2':''" @click="music_play=!music_play" src="../static/images/player.png"><div class="orchard-bg"><img src="../static/images/bg2.png"><img class="board_bg2" src="../static/images/board_bg2.png"></div><img class="back" @click="go_index" src="../static/images/user_back.png" alt=""></div><script>apiready = function(){init();new Vue({el:"#app",data(){return {music_play:true,token:"",socket: null,timeout: 0,prev:{name:"",url:"",params:{}},current:{name:"orchard",url:"orchard.html",params:{}},}},created(){this.checkout();},methods:{checkout(){var token = this.game.get("access_token") || this.game.fget("access_token");this.game.checkout(this,token,(new_access_token)=>{this.connect();});},connect(){// socket连接this.socket = io.connect(this.settings.socket_server + this.settings.socket_namespace, {transports: ['websocket']});this.socket.on('connect', ()=>{this.game.print("开始连接服务端");this.login();this.get_count();});},get_count(){this.socket.on('server_response', (res)=>{this.game.print(res.count);alert(`欢迎来到种植园,当前有${res.count}人在忙碌着~`)});},login(){var id = this.game.fget('id');// 通过send方法可以直接发送数据,不需要自定义事件,数据格式是json格式// this.socket.send({'uid': id});this.socket.emit('login', {'uid': id});},go_index(){this.game.outWin("orchard");},}});}</script>
</body>
</html>
5.基于房间管理分发信息
application/apps/orchard/socket.py
服务端代码:
from application import socketio
from flask import request
from application.apps.users.models import User
from flask_socketio import join_room, leave_room# 建立socket通信
@socketio.on('connect', namespace='/mofang')
def user_connect():# request.sid socketIO基于客户端生成的唯一会话IDprint('用户%s连接过来了!' % request.sid)# 主动响应数据给客户端length = User.query.count()socketio.emit('server_response', {'count': length, 'sid': '%S' % request.sid}, namespace='/mofang')# 断开socket通信
@socketio.on('disconnect', namespace='/mofang')
def user_disconnect():print('用户%s退出了种植园' % request.sid)# 未定义事件通信,客户端没有指定事件名称
@socketio.on('message', namespace='/mofang')
def user_message(data):print('接收到来自%s发送的数据:' % request.sid)print(data)print(data['uid'])"""用户eVUPnaT_AqIAPlyJAAAB连接过来了!接收到来自eVUPnaT_AqIAPlyJAAAB发送的数据:{'uid': '51'}51"""# 自定义事件通信
@socketio.on('login', namespace='/mofang')
def user_login(data):print('接收来自客户端%s发送的数据:' % request.sid)print(data)print(data['uid'])"""用户tdpnmCy-44Bg1CJpAAAD连接过来了!接收到来自客户端tdpnmCy-44Bg1CJpAAAD发送的数据:{'uid': '51'}51"""# 一般基于用户id分配不同的房间room = data['uid']join_room(room)socketio.emit('login_response', {'data': '登录成功'}, namespace='/mofang', room=room)
html/orchard.html
客户端代码:
<!DOCTYPE html>
<html>
<head><title>用户中心</title><meta name="viewport" content="width=device-width,minimum-scale=1.0,maximum-scale=1.0,user-scalable=no"><meta charset="utf-8"><link rel="stylesheet" href="../static/css/main.css"><script src="../static/js/vue.js"></script><script src="../static/js/axios.js"></script><script src="../static/js/main.js"></script><script src="../static/js/uuid.js"></script><script src="../static/js/settings.js"></script><script src="../static/js/socket.io.js"></script>
</head>
<body><div class="app orchard" id="app"><img class="music" :class="music_play?'music2':''" @click="music_play=!music_play" src="../static/images/player.png"><div class="orchard-bg"><img src="../static/images/bg2.png"><img class="board_bg2" src="../static/images/board_bg2.png"></div><img class="back" @click="go_index" src="../static/images/user_back.png" alt=""></div><script>apiready = function(){init();new Vue({el:"#app",data(){return {music_play:true,token:"",socket: null,timeout: 0,prev:{name:"",url:"",params:{}},current:{name:"orchard",url:"orchard.html",params:{}},}},created(){this.checkout();},methods:{checkout(){var token = this.game.get("access_token") || this.game.fget("access_token");this.game.checkout(this,token,(new_access_token)=>{this.connect();});},connect(){// socket连接this.socket = io.connect(this.settings.socket_server + this.settings.socket_namespace, {transports: ['websocket']});this.socket.on('connect', ()=>{this.game.print("开始连接服务端");this.login();this.get_count();this.login_response();});},login_response(){this.socket.on('login_response', (res)=>{alert(res.data);});},get_count(){this.socket.on('server_response', (res)=>{this.game.print(res.count);alert(`欢迎${res.sid}来到种植园,当前有${res.count}人在忙碌着~`);});},login(){var id = this.game.fget('id');// 通过send方法可以直接发送数据,不需要自定义事件,数据格式是json格式// this.socket.send({'uid': id});this.socket.emit('login', {'uid': id});},go_index(){this.game.outWin("orchard");},}});}</script>
</body>
</html>
6.服务端定时推送数据
application/apps/orchard/socket.py
服务端代码:
from application import socketio
from flask import request
from application.apps.users.models import User
from flask_socketio import join_room, leave_room# 建立socket通信
@socketio.on('connect', namespace='/mofang')
def user_connect():# request.sid socketIO基于客户端生成的唯一会话IDprint('用户%s连接过来了!' % request.sid)# 主动响应数据给客户端length = User.query.count()socketio.emit('server_response', {'count': length, 'sid': '%s' % request.sid}, namespace='/mofang')# 断开socket通信
@socketio.on('disconnect', namespace='/mofang')
def user_disconnect():print('用户%s退出了种植园' % request.sid)# 未定义事件通信,客户端没有指定事件名称
@socketio.on('message', namespace='/mofang')
def user_message(data):print('接收到来自%s发送的数据:' % request.sid)print(data)print(data['uid'])"""用户eVUPnaT_AqIAPlyJAAAB连接过来了!接收到来自eVUPnaT_AqIAPlyJAAAB发送的数据:{'uid': '51'}51"""# 自定义事件通信
@socketio.on('login', namespace='/mofang')
def user_login(data):print('接收来自客户端%s发送的数据:' % request.sid)print(data)print(data['uid'])"""用户tdpnmCy-44Bg1CJpAAAD连接过来了!接收到来自客户端tdpnmCy-44Bg1CJpAAAD发送的数据:{'uid': '51'}51"""# 一般基于用户id分配不同的房间room = data['uid']join_room(room)socketio.emit('login_response', {'data': '登录成功'}, namespace='/mofang', room=room)# 定时推送数据
from threading import Lock
import random
thread = None
thread_lock = Lock()@socketio.on('chat', namespace='/mofang')
def chat(data):global threadwith thread_lock:if thread is None:thread = socketio.start_background_task(target=background_thread)def background_thread(uid):while True:socketio.sleep(1)t = random.randint(1, 100)socketio.emit('server_response', {'count': t}, namespace='/mofang')
html/orchard.html
客户端代码:
<!DOCTYPE html>
<html>
<head><title>用户中心</title><meta name="viewport" content="width=device-width,minimum-scale=1.0,maximum-scale=1.0,user-scalable=no"><meta charset="utf-8"><link rel="stylesheet" href="../static/css/main.css"><script src="../static/js/vue.js"></script><script src="../static/js/axios.js"></script><script src="../static/js/main.js"></script><script src="../static/js/uuid.js"></script><script src="../static/js/settings.js"></script><script src="../static/js/socket.io.js"></script>
</head>
<body><div class="app orchard" id="app"><img class="music" :class="music_play?'music2':''" @click="music_play=!music_play" src="../static/images/player.png"><div class="orchard-bg"><img src="../static/images/bg2.png"><img class="board_bg2" src="../static/images/board_bg2.png"></div><img class="back" @click="go_index" src="../static/images/user_back.png" alt=""><h1 style="position: absolute; top: 20rem;">{{num}}</h1></div><script>apiready = function(){init();new Vue({el:"#app",data(){return {music_play:true,token:"",num: '',socket: null,timeout: 0,prev:{name:"",url:"",params:{}},current:{name:"orchard",url:"orchard.html",params:{}},}},created(){this.checkout();},methods:{checkout(){var token = this.game.get("access_token") || this.game.fget("access_token");this.game.checkout(this,token,(new_access_token)=>{this.connect();});},connect(){// socket连接this.socket = io.connect(this.settings.socket_server + this.settings.socket_namespace, {transports: ['websocket']});this.socket.on('connect', ()=>{this.game.print("开始连接服务端");this.login();this.get_count();this.login_response();});},login_response(){this.socket.on('login_response', (res)=>{alert(res.data);});},get_count(){this.socket.on('server_response', (res)=>{this.num = res.count;// alert(`欢迎${res.sid}来到种植园,当前有${res.count}人在忙碌着~`);});},login(){var id = this.game.fget('id');// 通过send方法可以直接发送数据,不需要自定义事件,数据格式是json格式// this.socket.send({'uid': id});// this.socket.emit('login', {'uid': id});this.socket.emit('chat', {'uid': id})},go_index(){this.game.outWin("orchard");},}});}</script>
</body>
</html>
7.服务端推送广播信息
# 推送广播信息
from flask_socketio import emit
@socketio.on('my_broadcast', namespace='mofang')
def my_broadcast(data):emit('broadcast_response', data, broadcast=True)socketio.emit('some event', {'data': 42})# 只要不声明房间ID,则默认返回给整个命名空间下所有的用户都可以接收
三、种植园页面展示
主框架,html/orchard.html
,代码:
<!DOCTYPE html>
<html>
<head><title>用户中心</title><meta name="viewport" content="width=device-width,minimum-scale=1.0,maximum-scale=1.0,user-scalable=no"><meta charset="utf-8"><link rel="stylesheet" href="../static/css/main.css"><script src="../static/js/vue.js"></script><script src="../static/js/axios.js"></script><script src="../static/js/main.js"></script><script src="../static/js/uuid.js"></script><script src="../static/js/settings.js"></script><script src="../static/js/socket.io.js"></script>
</head>
<body><div class="app orchard" id="app"><img class="music" :class="music_play?'music2':''" @click="music_play=!music_play" src="../static/images/player.png"><div class="orchard-bg"><img src="../static/images/bg2.png"><img class="board_bg2" src="../static/images/board_bg2.png"></div><img class="back" @click="go_index" src="../static/images/user_back.png" alt=""><div class="header"><div class="info" @click='go_home'><div class="avatar"><img class="avatar_bf" src="../static/images/avatar_bf.png" alt=""><img class="user_avatar" src="../static/images/avatar.png" alt=""><img class="avatar_border" src="../static/images/avatar_border.png" alt=""></div><p class="user_name">好听的昵称</p></div><div class="wallet"><div class="balance"><p class="title"><img src="../static/images/money.png" alt="">钱包</p><p class="num">99,999.00</p></div><div class="balance"><p class="title"><img src="../static/images/integral.png" alt="">果子</p><p class="num">99,999.00</p></div></div><div class="menu-list"><div class="menu"><img src="../static/images/menu1.png" alt="">排行榜</div><div class="menu"><img src="../static/images/menu2.png" alt="">签到有礼</div><div class="menu"><img src="../static/images/menu3.png" alt="">道具商城</div><div class="menu"><img src="../static/images/menu4.png" alt="">邮件中心</div></div></div><div class="footer"><ul class="menu-list"><li class="menu">新手</li><li class="menu">背包</li><li class="menu-center">商店</li><li class="menu">消息</li><li class="menu">好友</li></ul></div></div><script>apiready = function(){init();new Vue({el:"#app",data(){return {music_play:true,namespace: '/mofang_orchard',token:"",socket: null,timeout: 0,prev:{name:"",url:"",params:{}},current:{name:"orchard",url:"orchard.html",params:{}},}},created(){this.game.goFrame('orchard', 'my_orchard.html', this.current, {x: 0,y: 180,w: 'auto',h: 'auto'}, null);this.checkout();},methods:{checkout(){var token = this.game.get("access_token") || this.game.fget("access_token");this.game.checkout(this,token,(new_access_token)=>{this.connect();});},connect(){// socket连接this.socket = io.connect(this.settings.socket_server + this.namespace, {transports: ['websocket']});this.socket.on('connect', ()=>{this.game.print("开始连接服务端");});},go_index(){this.game.outWin("orchard");},go_friends(){this.game.goFrame('friends', 'friends.html', this.current);this.game.goFrame('friend_list', 'friend_list.html', this.current, {x: 0,y: 190,w: 'auto',h: 'auto'}, null, true);},go_home(){this.game.goWin('user', 'user.html', this.current);}}});}</script>
</body>
</html>
我的果园,html/my_orchard.html
,代码:
<!DOCTYPE html>
<html>
<head><title>用户中心</title><meta name="viewport" content="width=device-width,minimum-scale=1.0,maximum-scale=1.0,user-scalable=no"><meta charset="utf-8"><link rel="stylesheet" href="../static/css/main.css"><script src="../static/js/vue.js"></script><script src="../static/js/axios.js"></script><script src="../static/js/main.js"></script><script src="../static/js/uuid.js"></script><script src="../static/js/settings.js"></script><script src="../static/js/socket.io.js"></script>
</head>
<body><div class="app orchard orchard-frame" id="app"><div class="background"><img class="grassland2" src="../static/images/grassland2.png" alt=""><img class="mushroom1" src="../static/images/mushroom1.png" alt=""><img class="stake1" src="../static/images/stake1.png" alt=""><img class="stake2" src="../static/images/stake2.png" alt=""></div><div class="pet-box"><div class="pet"><img class="pet-item" src="../static/images/pet3.png" alt=""></div><div class="pet turned_off"><img class="turned_image" src="../static/images/turned_off.png" alt=""><p>请购买宠物</p></div></div><div class="tree-list"><div class="tree-box"><div class="tree"><img src="../static/images/tree4.png" alt=""></div><div class="tree"><img src="../static/images/tree3.png" alt=""></div><div class="tree"><img src="../static/images/tree4.png" alt=""></div></div><div class="tree-box"><div class="tree"><img src="../static/images/tree3.png" alt=""></div><div class="tree"><img src="../static/images/tree2.png" alt=""></div><div class="tree"><img src="../static/images/tree2.png" alt=""></div></div><div class="tree-box"><div class="tree"><img src="../static/images/tree1.png" alt=""></div><div class="tree"><img src="../static/images/tree0.png" alt=""></div><div class="tree"><img src="../static/images/tree0.png" alt=""></div></div></div><div class="prop-list"><div class="prop"><img src="../static/images/prop1.png" alt=""><span>1</span><p>化肥</p></div><div class="prop"><img src="../static/images/prop2.png" alt=""><span>0</span><p>修剪</p></div><div class="prop"><img src="../static/images/prop3.png" alt=""><span>1</span><p>浇水</p></div><div class="prop"><img src="../static/images/prop4.png" alt=""><span>1</span><p>宠物粮</p></div></div><div class="pet-hp-list"><div class="pet-hp"><p>宠物1 饱食度</p><div class="hp"><div style="width: 85%;" class="process">85%</div></div></div><div class="pet-hp"><p>宠物2 饱食度</p><div class="hp"><div style="width: 0;" class="process">0%</div></div></div></div></div><script>apiready = function(){init();new Vue({el:"#app",data(){return {namespace: '/mofang',token:"",socket: null,timeout: 0,prev:{name:"",url:"",params:{}},current:{name:"orchard",url:"orchard.html",params:{}},}},created(){},methods:{}});}</script>
</body>
</html>
css样式,main.css
代码:
.orchard-frame .background{position: absolute;top: 0;left: 0;width: 100%;height: 100rem;
}
.orchard-frame .background .grassland1{width: 31.22rem;height: 13.53rem;position: absolute;top: 4rem;
}
.orchard-frame .background .grassland2{width: 31.22rem;height: 13.53rem;position: absolute;top: 5rem;
}
.orchard-frame .background .mushroom1{width: 4.56rem;height: 4.83rem;position: absolute;right: 1rem;top: 11rem;
}
.orchard-frame .background .stake1{width: 4.56rem;height: 4.83rem;position: absolute;top: 3rem;left: 0rem;
}
.orchard-frame .background .stake2{width: 6.31rem;height: 4.83rem;position: absolute;top: 3rem;left: 13rem;
}
.orchard-frame .pet-box{position: absolute;top: -2rem;left: 0;display: flex;
}
.orchard-frame .pet-box .pet{position: relative;width: 14.16rem;height: 15rem;flex: 1;margin-left: 1rem;margin-right: 1rem;background: url("../images/tree1.png") no-repeat 0 -0.5rem;background-size: 100%;
}
.orchard-frame .pet-box .turned_off .turned_image{width: 5.14rem;height: 6.83rem;position: absolute;top: 0;left: 0;right: 0;bottom: 0;margin: auto;
}
.orchard-frame .pet-box .turned_off p{position: absolute;top: 0;left: 0;right: 0;bottom: 0;margin: auto;border: 1px solid #fff;border-radius: 1rem;width: 8rem;height: 3rem;line-height: 3rem;font-size: 1.5rem;word-wrap: break-word;padding: 1rem;color: #000;text-align: center;background: rgba(255,255,255,.6);
}
.orchard-frame .pet-box .pet-item{width: 10rem;height: 10rem;position: absolute;top: 0;left: 0;right: 0;bottom: 0;margin: auto;
}
.orchard-frame .tree-list{position: absolute;top: 9rem;width: 100%;
}
.orchard-frame .tree-box{margin-left: 3rem;margin-right: 3rem;
}
.orchard-frame .tree-box .tree{width: 9rem;height: 4rem;margin-bottom: 2rem;float: left;
}
.orchard-frame .tree-box .tree img{width: 9rem;height: 8rem;max-height: 8rem;
}
.orchard-frame .prop-list{position: absolute;bottom: 6rem;width: 100%;
}
.orchard-frame .prop-list .prop{float: left;margin-left: 1rem;width: 3rem;position: relative;
}
.orchard-frame .prop-list .prop img{width: 2.5rem;height: 2.5rem;margin: auto;display: block;
}
.orchard-frame .prop-list .prop span{position: absolute;top: -4px;right: -4px;border-radius: 50%;width: 1rem;height; 1rem;font-size: .8rem;color: #fff;background-color: #cc0000;text-align: center;line-height: 1rem;padding: 2px;
}
.orchard-frame .prop-list .prop p{text-align: center;color: #fff;
}
.orchard-frame .pet-hp-list{position: absolute;right: 0;bottom: 8rem;width: 11rem;height: 4rem;
}
.orchard-frame .pet-hp-list .pet-hp{margin-bottom: 5px;
}
.orchard-frame .pet-hp-list .pet-hp p{}
.orchard-frame .pet-hp-list .pet-hp .hp{border: 1px solid #fff;border-radius: 5rem;width: 10rem;padding: 1px;
}
.orchard-frame .pet-hp-list .pet-hp .process{font-size: 0.5rem;background-color: red;color: #fff;border-radius: 5rem;border-top-right-radius: 0;border-bottom-right-radius: 0;text-align: center;
}
魔坊APP项目-16-种植园、websocket协议、服务端基于socket提供服务(基于房间管理分发信息)、种植园页面展示相关推荐
- 魔坊APP项目-22-种植园,种植栏的功能实现,客户端根据激活状态和未激活状态分别显示树桩、服务端提供种植植物的相关数据、解锁树桩、植物相关道具使用
种植园 一.种植栏的功能实现 1. 客户端需要的植物相关参数: 总树桩数量, 当前用户激活树桩数量, 当前种植的树桩数量, 树桩列表状态 2. 客户端根据激活状态和未激活状态分别显示树桩 3. 服务端 ...
- 魔坊APP项目-27-直播、客户端中调整窗口大小、能播放rtmp格式直播流的播放器模块
直播 客户端中直播的界面调整和当前窗口一致 live_list.html <!DOCTYPE html> <html lang="en"> <head ...
- 魔坊APP项目-15-邀请好友(业务逻辑流程图、服务端提供邀请好友的二维码生成接口、客户端通过第三方识别微信二维码,服务端提供接口允许访问、App配置私有协议,允许第三方应用通过私有协议,唤醒APP)
邀请好友 1.业务逻辑流程图 客户端提供点击"邀请好友"以后的页面frame,html/invite.html,代码: <!DOCTYPE html> <html ...
- 魔坊APP项目-17-种植园,商城页面、服务端提供商品api,解决App打包编译以后的跨域限制、客户端获取商品列表并进行展示,集成Alipayplus模块完成支付
种植园 一.商城页面 orchard.html,代码: <!DOCTYPE html> <html> <head><title>用户中心</tit ...
- 魔坊APP项目-19-种植园,我的背包、道具购买
种植园 一.我的背包 打开背包,orchard.html,代码: <!DOCTYPE html> <html> <head><title>用户中心< ...
- 魔坊APP项目-20-种植园,背包显示道具、用户购买道具的时候,判断背包存储是否达到上限、背包解锁
种植园 一.背包显示道具 在背包中显示道具,会涉及到用户的背包格子的显示以及解锁问题,所以我们需要在服务端准备一个参数信息, 用于保存种植园中用户的业务参数,例如: 格子的初始化数量, 每次解锁背包格 ...
- 魔坊APP项目-26-直播、docker安装OSSRS流媒体直播服务器、基于APICloud的acLive直播推流模块实现RTMP直播推流、直播流管理
一.docker安装OSSRS流媒体直播服务器 在外界开发中, 如果要实现直播功能.常用的方式有: 1. 通过第三方接口来实现.可以申请阿里云,腾讯云,网易云,七牛云的直播接口,根据文档,下载集成SD ...
- 魔坊APP项目-18-种植园,基于支付宝提供的沙箱测试环境开发支付接口、服务端, 处理支付结果的同步通知和异步通知、修复页面底部菜单无法被点击的BUG
种植园 一.基于支付宝提供的沙箱测试环境开发支付接口 沙箱环境: https://openhome.alipay.com/platform/appDaily.htm?tab=info 开发文档: ht ...
- 魔坊APP项目-21-种植园,宠物栏的功能实现、服务端提供显示宠物的api接口、客户端中展示宠物栏和宠物列表以及饱食度、宠物道具的使用
种植园 一.宠物栏的功能实现 1. 宠物的显示 2. 宠物的使用 3. 宠物的饱食度 4. 宠物的开锁 1.服务端提供显示宠物的api接口 socket.py,代码 ... import math f ...
最新文章
- windows性能计数器搜集方法
- AI人才抢夺“生猛”: 应届博士年薪涨到80万元
- mysql索引详解_MySQL索引详解
- 网络通信-1(InetAddress、UDP、TCP、DatagramPacket、DatagramSocket、UDP通信示例)
- php 魔术方法使用说明详细
- 子组件触发父组件的方法
- SVN分支管理那些事儿
- linux终端黑客帝国代码雨效果
- windows程序设计之定义窗口句柄
- 运维学习部分基础知识概括
- 无线鼠标服务器,remote mouse
- vue常用的几个框架
- 【DP】洛谷 P1510 精卫填海
- TexturePacker纹理打包打方法及技巧
- 过孔在覆铜后不出现十字孔
- iOS快速清除全部的消息推送
- 沭阳学爬虫10高效存储MongoDB
- linux的mysql占用cpu过高_linux 系统中Mysql 进程占用cpu过高的解决
- More Effective C++读书笔记
- 示波器测量汽车进气压力传感器信号及波形分析
热门文章
- 为什么计算机网络使用数字信号,计算机网络数字数据在数字信道传输时为什么要进行..._网络编辑_帮考网...
- 【51单片机】计时器/计数器中断
- C语言fclose函数了解
- 建站之星网站迁移攻略
- 图书馆管理系统利用c语言编写,图书馆管理系统 c语言编写.doc
- 新网工李白——>李白你好(来抽大奖啦~)
- (java)水果类(增删改查)
- 10个常用的数据分析商业模型之价值链分析模型(三)
- 2019年IT行业就业形势
- 工业控制计算机系统总线,工业控制计算机总线技术.ppt