【Skynet 入门实战练习】事件模块 | 批处理模块 | GM 指令 | 模糊搜索
文章目录
- 前言
- 事件模块
- 批处理模块
- GM 指令模块
- 模糊搜索
- 最后
前言
本节完善了项目,实现了事件、批处理、模糊搜索模块、GM 指令模块。
事件模块
什么是事件模块?事件模块是用来在各系统之间传递事件消息的。
为什么需要事件模块?主要目的是用来减少模块之间的耦合。
事件模块使用场景:
- 常见的有网游中的任务系统,监听角色的升级事件,分派相应的任务
- 监听登录登出事件,做相应的资源分配与销毁
- 监听玩家的属性变化,更新其他模块缓存玩家的数据
事件模块实现
实现三个基本接口:
- 加入监听列表:
add_listener(event_type, func)
- 从监听列表删除:
del_listener(id)
- 触发一个事件:
fire_event(event_type, ...)
lualib/event.lua
:
local _M = {}
local handler_inc_id = 1
local dispatchs = {} -- event type: { id: func }
local handlers = {} -- id: event typefunction _M.add_listener(event_type, func)local cbs = dispatchs[event_type]if not cbs then cbs = {} dispatchs[event_type] = cbsend handler_inc_id = handler_inc_id + 1local id = handler_inc_idcbs[id] = func handlers[id] = event_typereturn id
end function _M.del_listener(id)local event_type = handlers[id]if not event_type then return end handlers[id] = nil local cbs = dispatchs[event_type] if not cbs then return end cbs[id] = nil
end function _M.fire_event(event_type, ...)local cbs = dispatchs[event_type]if not cbs or not next(cbs) then return end local res = truefor id, func in pairs(cbs) do local ok, err = xpcall(func, debug.traceback, ...)if not ok then logger.error("[event]", "fire event error", "event type:", event_type, "handle id:", id, "err:", err)res = false end end return res
end return _M
handler_inc_id
:处理函数的对应自增 IDdispatchs
:记录事件类型对应的回调函数列表handlers
:记录处理函数属于哪个事件
add_listener
和 del_listener
两个函数就是维护上述两个表。fire_event
即对要触发的事件类型的所有回调函数进行执行,采用 xpcall
保证每个触发逻辑互不影响,某个处理报错,其余逻辑仍会正常执行。
测试事件模块
测试玩家升级接口触发的升级事件
添加一个事件常量表,lualib/event_type.lua
:
local _M = {}
_M.EVENT_TYPE_UPLEVEL = "UPLEVEL" -- 玩家升级事件
return _M
添加一个经验值常量表,data/lvexp.lua
:
return {[1] = {["exp"] = 0},[2] = {["exp"] = 300},[3] = {["exp"] = 700},[4] = {["exp"] = 1700},[5] = {["exp"] = 4450},[6] = {["exp"] = 10950},[7] = {["exp"] = 28000},[8] = {["exp"] = 58000},[9] = {["exp"] = 83000},[10] = {["exp"] = 104000},[11] = {["exp"] = 153500},[12] = {["exp"] = 222500},[13] = {["exp"] = 357500}
}
类似的数值表一般由策划给定,通过导表工具,将 excel 表导出为可供使用的 lua 表结构,如上述。
添加经验值增加接口实现,监听事件模块实现,并通过 GM 指令进行升级测试
module/cached/user.lua
:
local function get_next_lv(lv)local next_lv = lv + 1local cfg = data_lvexp[next_lv]if not cfg then return falseend return true, next_lv, cfg.exp
end function CMD.add_exp(uid, cache, exp)_M.add_exp(uid, cache, exp)return cache.lv, cache.exp
end function _M.add_exp(uid, cache, exp)cache.exp = cache.exp + exp local lvchanged = false while true do local lv = cache.lv local cur_exp = cache.explocal ok, next_lv, next_exp = get_next_lv(lv)if ok and cur_exp >= next_exp then cur_exp = cur_exp - next_expcache.exp = cur_expcache.lv = next_lvlvchanged = true elsebreak end end if lvchanged then event.fire_event(event_type.EVENT_TYPE_UPLEVEL, uid, cache.lv)end
end
在缓存模块下相应的用户模块,添加增加经验值的接口,CMD.add_exp
供其他服务调用。get_next_lv
获取升到下一级所需经验值,_M.add_exp
为实际经验值增加逻辑实现,判断如果升级则需要对监听升级事件的相关逻辑进行触发。
完整代码:module/cached/user.lua
到此我们还需要添加一个监听升级事件的模块,物品模块,用来验证升级后能否正确触发逻辑。
module/cached/item.lua
local function init_cb(uid, cache)if not cache.items then cache.items = {}end
endlocal function on_uplevel(uid, lv)logger.debug("item", "on_uplevel", "uid:", uid, "lv:", lv)
end function _M.init()mng.register_cmd("user", "item", CMD)mng.register_init_cb("user", "item", init_cb)event.add_listener(event_type.EVENT_TYPE_UPLEVEL, on_uplevel)
end
on_uplevel
即为监听到升级事件后需要触发的模块,简单打印用户和等级
通过 event.add_listener
主动将这个模块加到事件模块的升级事件中,对用该事件触发的回调函数即为 on_uplevel
。
不要忘了模块的初始化,service/cached.lua
:
local item = require "cached.item"skynet.start(function()item.init()
end)
通过 GM 指令(后续讨论),增加经验值后,可以看到能正常执行 on_uplevel
:
上述就是事件模块的简单使用,设计一个监听某事件的模块,事件类型写在常量表 event_type
中,然后在初始化 init
时,主动添加 add_listener
进事件模块中。
在其余服务中,如果产生了相应的事件,则主动调用 fire_event
,触发事件即可。
实现完了事件模块,可以发现事件模块的使用有什么好处?
如果没有事件模块的解耦,那么每个监听玩家等级变化的模块,都需要在等级模块插入一行代码,不利于维护,模块之间的直接调用代码非常丑且容易漏调用。
批处理模块
什么是批处理模块?批处理模块用于自动化批量执行任务的。
为什么需要批处理模块?分批次处理任务,避免某个任务长时间占用资源。
批处理模块的应用场景:
- 系统维护,批量踢出用户
- 系统广播,批量给在线玩家广播数据
- 排行榜结算,批量给玩家发放奖励
批处理模块实现
实现两个基本接口:
new_batch_task(tid, interval, step, list, cb, ...)
,创建一个批处理任务remove_batch_task(tid)
,删除一个批处理任务
lualib/batch.lua
:
local all_batch_tasks = {} -- taskid: taskinfo
local all_batch_tasks_cnt = 0 -- 待处理任务数-- 创建新任务
local function new_empty_batch_task(tid)local info = {}all_batch_tasks[tid] = info all_batch_tasks_cnt = all_batch_tasks_cnt + 1return info
end function _M.new_batch_task(tid, interval, step, list, cb, ...)local info = new_empty_batch_task(tid) info.timer = timer.timeout(interval, batch_task_heartbeat, tid) info.deal_idx = 0 -- 已处理数量info.list = list info.interval = intervalinfo.step = step info.func = cb info.args = pack(...)return true
end -- 删除任务
function _M.remove_batch_task(tid)if all_batch_tasks[tid] and all_batch_tasks[tid].timer then timer.cancel(all_batch_tasks[tid].timer)end all_batch_tasks[tid] = nil all_batch_tasks_cnt = all_batch_tasks_cnt - 1
end
-
all_batch_tasks_cnt
:待处理的批处理任务数量 -
all_batch_tasks
:存放所有待处理任务,一个任务 ID 对应一个批处理任务,批处理任务包含下面几个字段timer
:定时器 ID,定时器用于定时分批处理任务,并且方便随时中断批处理deal_idx
:表示已经处理到第几个逻辑list
:需要处理逻辑的数组,每次执行处理函数时,它每个值都作为第一个参数传入interval
:每次处理事件间隔,单位为秒step
:每次处理步长,即一次消化多少个func
:处理逻辑函数args
:处理逻辑函数的其他参数
创建和删除批处理任务如上述逻辑,维护 all_batch_tasks
表,初始化创建相应的批处理任务字段。
定时处理的函数逻辑,batch_task_heartbeat
任务心跳循环:
local function batch_task_heartbeat(tid)local info = all_batch_tasks[tid]local list_cnt = #info.list local start_idx = info.deal_idx + 1if info.deal_idx > list_cnt then _M.remove_batch_task(tid)return end local end_idx = start_idx + info.step - 1if end_idx > list_cnt then end_idx = list_cnt_M.remove_batch_task(tid)else -- 这批次还没处理完,开启定时器等下次再处理info.deal_idx = end_idxinfo.timer = timer.timeout(info.interval, batch_task_heartbeat, tid)end -- 处理本批次for i = start_idx, end_idx do local ok, err = xpcall(info.func, traceback, info.list[i], unpack(info.args, 1, info.args.n))end
end
通过 deal_idx
和 step
两个字段,计算出当前批次要处理的逻辑的起始 start_idx
和结尾 end_idx
。
如果本批次能处理完该任务,则删除,否则继续创建该任务的下一个定时器。
最后执行当前批次所有逻辑,通过 xpcall
保护环境进行调用。
完整代码:lualib/batch.lua
批处理实现广播消息,通过 GM 指令测试
ws_gate.lua
:
-- 发送消息接口
local function send_msg(fd, msg)if connection[fd] then websocket.write(fd, msg)end
end -- 广播消息接口
function CMD.broadcast(source, msg)local fds = utils_table.klist(connection)-- 调用批处理接口local ok, err = batch.new_batch_task({"broadcast", source, msg}, 1, 100, fds, send_msg, msg)
end
在网关服务中,实现广播接口 broadcast
,消息通过批处理模块调用 send_msg
处理函数,回发给每个客户端。
utils/table.lua
模块实现了 klist
接口,将 lua 表以 key
值存放为一个 array
类型的 table
结构:
function _M.klist(t)local klist = {}local idx = 1for k, _ in pairs(t) do klist[idx] = kidx = idx + 1end return klist
end
还可以发现,我们创建批处理任务,传入的 tid
是一个表结构。这样每次广播消息时,都是用的新任务 ID。
但有些逻辑需要防止重入,比如定时批量保存玩家数据,上一次保存逻辑没有处理完毕时,下一次批处理需要忽略,直接延迟到下下一次即可。这时候只需要传入一个字符串作为任务 ID 即可。
之前实现的缓存模块中,定时保存玩家数据可以修改为批处理任务执行,这里就不做演示了。
我们修改一下参数 step = 1
参数,这里开启三个客户端,并且一个客户端广播消息:
从上图可以看出,每秒执行一次消息广播,三个客户端先后收到消息。
GM 指令模块
前面两个模块测试时都使用了 GM 指令,当然通过之前的通信的方法也可以实现,module/cached/user
模块和 test/cmds/ws
模块下写指令接口即可。
那为什么还需要 GM 指令呢?
GM(Game Manager) 指令在维基百科上是这样解释的:
在游戏正式发布之前,游戏公司通常会组织专人对游戏内容进行全面测试,而为了方便测试,游戏程序员在开发时就将大量专供测试和操作游戏内容用的专用命令写入。这些开放给 GM 使用的命令就是 GM 指令。
指令一般会涵盖游戏的全部功能,这些指令包括对服务器操作类(服务器重启,刷新,关闭等)、操作角色类(修改角色属性,角色位置,角色状态等)、广播类(发送全服消息,发布游戏活动消息),亦有方便 GM 活动的 GM 隐身,无敌等指令。例如:魔兽世界新的资料片巫妖王之怒开放的 GM 指令就包括直接到达 80 级等
由于 GM 指令功能多样,一些私服为了吸引玩家,也有将 GM 指令开放给普通玩家的。
GM 指令在游戏发开中有两个用途:
- 游戏开发期间用指令制造测试环境
- 游戏上线期间用指令修复玩家数据
GM 指令模块实现
指定客户端输入格式:gm user setname cauchy
(指令名称、模块、函数、参数)。
上述表示
gm
指令,在用户user
模块下,执行setname
函数,参数是cauchy
。
客户端实现 gm 模块,test/cmds/gm.lua
:
local cjson = require "cjson"
local websocket = require "http.websocket"local _M = {}------------ CMD ------------------- 执行指令
function _M.run_command(ws_id, ...)local cmd = table.concat({...}, " ")local req = {pid = "c2s_gm_run_cmd",cmd = cmd,}websocket.write(ws_id, cjson.encode(req))
end return _M
指令上行协议统一为 c2s_gm_run_cmd
,在服务端做逻辑分解。
服务器端 gm 模块:
| gm
-- |- main.lua
-- |- user.lua
ws_agent/gm/main.lua
local _M = {}
local RPC = {}
local gm_cmds = {} -- 指令模块-- 执行对应模块下的 CMD 中对应的指令 cmd
function _M.do_cmd(CMD, uid, cmd, ...)local cb = CMD[cmd]local func = cb.func local args_format = cb.args local ok, n, args = parse_cmd_args(uid, args_format, ...)return func(table.unpack(args, 1, n))
end -- req.cmd: "user" "setname" "cauchy"
-- GM 指令: 模块、指令、参数
function RPC.c2s_gm_run_cmd(req, fd, uid)local iter = string.gmatch(trim(req.cmd), "[^ ,]+")local mod = iter() -- 获取第一个参数:cmdlocal args = {}for v in iter do table.insert(args, v)end local ok = falselocal msg -- 获取对应模块local m = gm_cmds[mod]if m then ok, msg = _M.do_cmd(m.CMD, uid, table.unpack(args))else msg = "invalid cmd!"end local res = {pid = "c2s_gm_run_cmd",ok = ok, msg = msg,}return res
end function _M.init() gm_cmds.user = require "ws_agent.gm.user"
end _M.RPC = RPCreturn _M
gm/main.lua
进行封装接口,接受上行的消息,c2s_gm_run_cmd
即对 user setname cauchy
这种格式的消息进行参数提取,然后执行对应 user
模块的 setname
函数。
gm/user.lua
的实现:
local _M = {}local function set_name(uid, name)local ret = mng.set_username(uid, name)return true, "set name success"
end _M.CMD = {setname = {func = set_name,args = { "uid", "string" },},
}return _M
到此 gm 指令执行流程就一目了然了。
客户端输入的消息指令存在 cmd
中,通过协议 c2s_gm_run_cmd
上行到 gm/main.lua
的 RPC.c2s_gm_run_cmd
中处理,分解指令后,通过其他模块注册的 CMD
函数表,找到对应模块 do_cmd
执行的相应方法,并且该方法需要的参数 args
以字符串列表的形式指定,然后自定义解析参数 parse_cmd_args
,传入函数并执行。
不要忘记将 gm 指令模块的 RPC 注册到 ws_agent/mng.lua
中,这样上行的消息才能正确找到并执行。
ws_agent/mng.lua
:
local gm = require "ws_agent.gm.main"function _M.register_rpc(rpc)for k, v in pairs(rpc) do RPC[k] = vend
endfunction _M.init(gate, watchdog)gm.init()_M.register_rpc(gm.RPC)
end
完整代码:ws_agent/gm/main.lua、ws_agent/gm/user.lua
模糊搜索
模糊搜索模块使用场景:
- 加好友时搜索玩家昵称
- 购买物品时搜索物品名称
- 加帮会时搜索帮会名称
模糊搜索模块实现
实现一个缓存,在玩家搜索时,如果缓存命中直接返回,否则调用数据库接口,实现模糊匹配,并记入缓存。
以搜索玩家昵称为例,这里先在数据库模块中,提供接口 find_by_name
,使用 mongodb
自带的模糊匹配,并且忽略大小写返回匹配结果。
ws_agent/db.lua
:
-- 根据 name 名字查找,忽略大小写
function _M.find_by_name(name, limit) -- 查询语法local query = {name = {['$regex'] = name,["$options"] = 'i',}}-- 映射集local proj = {["_id"] = 0,["uid"] = 1,["name"] = 1,}local ret = mongo_col:find(query, proj):limit(limit)local ret_list = {}while ret:hasNext() do local data = ret:next()table.insert(ret_list, {uid = data.uid,name = data.name,})end return ret_list
end
模糊搜索模块 ws_agent/search.lua
:
local skynet = require "skynet"
local lru = require "lru"
local db = require "ws_agent.db"local _M = {}
local lru_cache_data local limit = tonumber(skynet.getenv("search_limit")) or 10
local expire = tonumber(skynet.getenv("search_expire")) or 10
local cache_max_cnt = tonumber(skynet.getenv("search_max_cache")) or 100function _M.search(name)local now = skynet.time()local cache_ret = lru_cache_data:get(name)if cache_ret and cache_ret.expire > now and cache_ret. search_list then return cache_ret.search_listend local search_list = db.find_by_name(name, limit)lru_cache_data:set(name, {expire = now + expire,search_list = search_list,})return search_list
end function _M.init()lru_cache_data = lru.new(cache_max_cnt)
end return _M
lru_cache_data
用来缓存历史查询结果,并且在LRU
基础上加了超时机制。即使在缓存中找出了历史查询的结果,如果时间超出了设定时间,也从数据库里重新查询。从数据库里查询到结果后,把结果放入缓存中。
模块需要在 ws_agent/mng.lua
中初始化
function _M.init(gate, watchdog)search.init()
end
最后
本节完善了项目最后的几个小功能,提供了几个简单的 gm 指令接口进行测试,这里不在做演示。基本的项目框架构建完成了,只是相应的业务逻辑不够丰富,感兴趣的读者可以自己新增模块,新增接口去完善。
完整代码参考项目地址:https://gitee.com/Cauchy_AQ/skynet_practice/tree/skynet
路漫漫其修远兮,学习游戏服务器开发的路途才刚开始,skyent 作为入门级首选框架,到此也才算入门。只是能简单使用这个框架,要学习的东西还很多,继续努力!!!
之后还会继续学习 skynet 相关的项目,比如:@huahua132 大佬的项目 skynet_fly, @hanxi 大佬的项目 skynet-demo。
万国觉醒的源码还未尝研究,等有能力了在考虑研读一下这份源码,听说质量也是参差不齐,但总归是个大项目,能学不少知识。
未来也可能会尝试阅读一下 skynet 的源码,深入的理解底层机制。一起加油!
相关文章:

【Skynet 入门实战练习】事件模块 | 批处理模块 | GM 指令 | 模糊搜索
文章目录 前言事件模块批处理模块GM 指令模块模糊搜索最后 前言 本节完善了项目,实现了事件、批处理、模糊搜索模块、GM 指令模块。 事件模块 什么是事件模块?事件模块是用来在各系统之间传递事件消息的。 为什么需要事件模块?主要目的是…...

Web组态可视化编辑器-by组态
演示地址: http://www.by-lot.com http://www.byzt.net web组态可视化编辑器:引领未来可视化编辑的新潮流 随着网络的普及和快速发展,web组态可视化编辑器应运而生,为人们在网络世界中创建和编辑内容提供了更加便捷的操作方式。这…...

PDF.js介绍以及使用
一、PDF.js是什么 PDF.js是一个JavaScript库,可以在现代Web浏览器中渲染和显示PDF文件。它的主要作用是将PDF文件转换为HTML5格式,以便在浏览器上进行展示和交互。 PDF.js的主要功能包括: 在浏览器中显示PDF:PDF.js使用HTML5的…...

经常使用的排序算法
一、直接插入排序 #include <stdio.h>void insert_sort(int arr[], int n){int i, j, tmp;for (i 1; i < n; i){tmp arr[i];j i - 1;while (j > 0 && arr[j] > tmp){ // 将要插入的元素与数组中的元素比较(从后向前比)arr[j …...

msyql 24day 数据库主从 主从复制 读写分离 master slave 有数据如何增加
目录 环境介绍读写分离纵向扩展横向扩展 数据库主从准备环境主库环境(master)从库配置(slave)状态分析重新配置问题分析 报错解决从库验证 有数据的情况下 去做主从清理环境环境准备数据库中的锁的机制主库配置从库配置最后给主库解锁常见错误 环境介绍 将一个数据库的数据 复…...

使用 Taro 开发鸿蒙原生应用 —— 探秘适配鸿蒙 ArkTS 的工作原理
背景 在上一篇文章中,我们已经了解到华为即将发布的鸿蒙操作系统纯血版本——鸿蒙 Next,以及各个互联网厂商开展鸿蒙应用开发的消息。其中,Taro作为一个重要的前端开发框架,也积极适配鸿蒙的新一代语言框架 —— ArkTS。 本文将…...

Linux下 自定义多线程并发快速压缩解压缩脚本
文章目录 自定义多线程压缩解压缩脚本使用 Linux下 自定义多线程并发快速压缩解压缩脚本 Linux下常用的tar工具无法支持并行 压缩和解压,对于大量小文件的解压缩,可借助pigz工具实现多线程并行工作,实现更为高效的压缩和解压缩。 自定义多线…...

ubuntu20.04下安装pcl_ubuntu安装pcl
pcl点云数据库,用来进行3D信息的获取与处理,和opencv相比较,opencv是用来处理二维信息,他是学术界与工业界针对点云最全的库,且网络上相关的资料很多。以下是pcl的安装步骤以及遇到的问题。 提前说明,本人…...

阿里云常用配置:日志采集、OSS、RAM 权限策略
文章目录 引言I 日志采集1.1 具体查询语法1.2 查询示例1.3 设置token时间(登录过期时间)II OSS2.1 设置防盗链2.2 验证Referer防盗链是否生效III 通义灵码 (智能编码)IV RAM 权限策略4.1 短信策略4.2 内容风险检测引言 SLS I 日志采集...

回顾丨2023 SpeechHome 第三届语音技术研讨会
下面是整体会议的内容回顾: 18日线上直播回顾 18日上午9:30,AISHELL & SpeechHome CEO卜辉宣布研讨会开始,并简要介绍本次研讨会的筹备情况以及报告内容。随后,CCF语音对话与听觉专委会副主任、清华大学教授郑方,…...

【flink】状态清理策略(TTL)
flink的keyed state是有有效期(TTL)的,使用和说明在官网描述的篇幅也比较多,对于三种清理策略没有进行横向对比得很清晰。 全量快照清理(FULL_STATE_SCAN_SNAPSHOT)增量清理(INCREMENTAL_CLEANUP)rocksdb压缩清理(ROCKSDB_COMPACTION_FILTER) 注意&…...

4. 行为模式 - 中介者模式
亦称: 调解人、控制器、Intermediary、Controller、Mediator 意图 中介者模式是一种行为设计模式, 能让你减少对象之间混乱无序的依赖关系。 该模式会限制对象之间的直接交互, 迫使它们通过一个中介者对象进行合作。 问题 假如你有一个创建…...

2015年第四届数学建模国际赛小美赛A题飞机上的细长座椅解题全过程文档及程序
2015年第四届数学建模国际赛小美赛 A题 飞机上的细长座椅 原题再现: 航空公司座位是指在旅途中乘客可以乘坐的座位。一些航空公司现在推出了新的经济舱“超薄”座位。这些座椅除了重量较轻外,理论上还允许航空公司在不显著影响乘客舒适度的情况下增加运…...

机器学习笔记(二)使用paddlepaddle,再探波士顿房价预测
目标 用paddlepaddle来重写之前那个手写的梯度下降方案,简化内容 流程 实际上就做了几个事: 数据准备:将一个批次的数据先转换成nparray格式,再转换成Tensor格式前向计算:将一个批次的样本数据灌入网络中ÿ…...

【Linux】权限篇(二)
权限目录 1. 前言2. 权限2.1 修改权限2.2 有无权限的对比2.3 另外一个修改权限的方法2.3.1 更改用户角色2.3.2 修改文件权限属性 3. 第一个属性列4. 目录权限5. 默认权限 1. 前言 在之前的一篇博客中分享了关于权限的一些知识,这次紧接上次的进行,有需要…...

reduce累加器的应用
有如下json数据,需要统计Status的值为0和1的数量 const data {"code": "001","results": [{"Status": "0",},{"Status": "0",},{"Status": "1",}] }方法一:用reduce方…...

助力硬件测试工程师之EMC项目测试。
1:更新该系列的目的 接下来的一个月内,将更新硬件测试工程师的其中测试项目--EMC项目,后续将会出安规等项目,助力测试工程师的学习。 2:如何高效率的展现项目的基础以及一些细节知识点 通过思维导图以及标准的规定进行…...

Github 2023-12-23 开源项目日报 Top10
根据Github Trendings的统计,今日(2023-12-23统计)共有10个项目上榜。根据开发语言中项目的数量,汇总情况如下: 开发语言项目数量Python项目6C项目2C项目1Jupyter Notebook项目1HTML项目1Go项目1非开发语言项目1 免费API集体清单 创建周期…...

Quartz.net 正则表达式触发器
1、创建项目 项目类型控制台应用程序,.Net Framework框架版本 4.7.2 2、引入框架 NuGet\Install-Package Quartz -Version 3.8.0 3、创建Job 自定义Job实现接口IJob,在Execute方法实现定时逻辑, using Quartz; using System; using Sys…...

【已解决】修改了网站的class样式name值,会影响SEO,搜索引擎抓取网站及排名吗?
问题: 修改了网站的class样式name值,会影响搜索引擎抓取网站及排名吗? 解答: 如果你仅仅修改了网站class样式的名称,而没有改变网站的结构和内容,那么搜索引擎通常不会因此而影响它对网站的抓取和排名。但…...

微信小程序开发系列-02注册小程序
上一篇文章,创建了一个最小的小程序,但是,还有3个疑问没有弄清楚,还是基于demo1工程,这篇文章继续探索。 当前的目录结构是否是完备的呢?(虽然小程序可以运行起来)app.js文件内容还…...

安装 PyCharm 2021.1 保姆级教程
作者:billy 版权声明:著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处 前言 目前能下载到的最新版本是 PyCharm 2021.1。 请注意对应 Python 的版本: Python 2: 2.7Python 3: >3.6, <3.11…...

浏览器 cookie 的原理(详)
目录 1,cookie 的出现2,cookie 的组成浏览器自动发送 cookie 的条件 3,设置 cookie3.1,服务端设置3.1,客户端设置3.3,删除 cookie 4,使用流程总结 整理和测试花了很大时间,如果对你有…...

StringBuilder和StringBuffer区别是什么?
想象一下,你在写信,但是你需要不断地添加新的内容或者修改一些词句。在编程中,当你需要这样操作字符串时,就可以用StringBuffer或StringBuilder。 StringBuffer StringBuffer就像是一个多人协作写作的工具。如果你和你的朋友们一…...

【数据分析】数据指标的分类及应用场景
数据分析之数据指标的分类 数据分析离不开对关键指标的分析与跟踪,这些指标通常与具体的业务直接相关。好的指标能够促进业务的健康发展,因为指标与业务目标是一致的,此时指标就能反映业务变化,指标发生变化,行动也发…...

首涂第二十八套_新版海螺M3多功能苹果CMSv10自适应全屏高端模板
首涂第二十八套_新版海螺M3多功能苹果cmsv10自适应全屏高端模板 多功能苹果cmsv10自适应全屏高端模板开源授权版 这是一款带“主题管理系统”的模板。这是一款好模板。 花大价钱收购了海螺这两个模板的版权。官方正品,非盗版。关闭域名授权 后台自定义菜单 请把…...

MatGPT - 访问 OpenAI™ ChatGPT API 的 MATLAB® 应用程序
系列文章目录 前言 MatGPT 是一款 MATLAB 应用程序,可让您轻松访问 OpenAI 的 ChatGPT API。使用该应用程序,您可以加载特定用例的提示列表,并轻松参与对话。如果您是 ChatGPT 和提示工程方面的新手,MatGPT 不失为一个学习的好方…...

Tomcat转SpringBoot、tomcat升级到springboot、springmvc改造springboot
Tomcat转SpringBoot、tomcat升级到springboot、springmvc改造springboot 起因:我接手tomcat-springmvc-hibernate项目,使用tomcat时问题不大。自从信创开始,部分市场使用国产中间件,例如第一次听说的宝兰德、东方通,还…...

浅述无人机技术在地质灾害应急救援场景中的应用
12月18日23时,甘肃临夏州积石山县发生6.2级地震,震源深度10千米,灾区电力、通信受到影响。地震发生后,无人机技术也火速应用在灾区的应急抢险中。目前,根据受灾地区实际情况,翼龙-2H应急救灾型无人机已出动…...

js-cookie的使用以及存储token安全的注意要点
js-cookie的使用以及存储token安全的注意要点 npm 安装 npm i js-cookie -S // https://www.npmjs.com/package/js-cookie引入使用 import Cookies from js-cookie获取 Cookies.get(token); // 读取token Cookies.get() // 读取所有可见的 Cookie > { token: value }设置…...