【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 type

function _M.add_listener(event_type, func)
    local cbs = dispatchs[event_type]
    if not cbs then 
        cbs = {} 
        dispatchs[event_type] = cbs
    end 

    handler_inc_id = handler_inc_id + 1
    local id = handler_inc_id
    cbs[id] = func 
    handlers[id] = event_type

    return 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 = true
    for 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:处理函数的对应自增 ID
  • dispatchs:记录事件类型对应的回调函数列表
  • handlers:记录处理函数属于哪个事件

add_listenerdel_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 + 1
    local cfg = data_lvexp[next_lv]
    if not cfg then 
        return false
    end 

    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.exp
        local ok, next_lv, next_exp = get_next_lv(lv)

        if ok and cur_exp >= next_exp then 
            cur_exp = cur_exp - next_exp
            cache.exp = cur_exp
            cache.lv = next_lv
            lvchanged = true 
        else
            break  
        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 
end

local 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 + 1
    return 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 = interval
    info.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 + 1
    if info.deal_idx > list_cnt then 
        _M.remove_batch_task(tid)
        return 
    end 

    local end_idx = start_idx + info.step - 1
    if end_idx > list_cnt then 
        end_idx = list_cnt
        _M.remove_batch_task(tid)
    else 
        -- 这批次还没处理完,开启定时器等下次再处理
        info.deal_idx = end_idx
        info.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_idxstep 两个字段,计算出当前批次要处理的逻辑的起始 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 = 1
    for k, _ in pairs(t) do 
        klist[idx] = k
        idx = idx + 1
    end 
    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() -- 获取第一个参数:cmd
    local args = {}
    for v in iter do 
        table.insert(args, v)
    end 

    local ok = false
    local 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 = RPC

return _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.luaRPC.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] = v
    end 
end
 
function _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 100

function _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_list
    end 

    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 的源码,深入的理解底层机制。一起加油!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/263323.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

由浅入深走进Python异步编程【多进程】(含代码实例讲解 || multiprocessing、异步进程池、进程通信)

写在前面 从底层到第三方库,全面讲解python的异步编程。这节讲述的是python的多线程实现,纯干货,无概念,代码实例讲解。 本系列有6章左右,点击头像或者专栏查看更多内容,陆续更新,欢迎关注。 …

群多多社群人脉H5-2.1.4多开插件+小程序独立前端+搭建教程

功能介绍: 1、群多多社群大全,是一个集发布、展示社群信息、人脉推广的裂变工具/平台。 2、通过人脉广场,将商家信息通过名片进行展示,让资源对接、人脉推广更加便捷高效。 3、行业群、兴趣群、知识付费群、交友群、商家活动推…

DMA实验3-外设到内存搬运

实验要求 使用 DMA 的方式将串口接收缓存寄存器的值搬运到内存中,同时闪烁 LED1 。 CubeMX 配置 DMA 配置: 串口中断配置 代码实现 如何判断串口接收是否完成?如何知道串口收到数据的长度? 使用串口空闲中断(IDL…

十一、W5100S/W5500+RP2040之MicroPython开发<MQTT阿里云示例>

文章目录 1. 前言2. 平台操作流程3. WIZnet以太网芯片4. 示例讲解以及使用4.1 程序流程图4.2 测试准备4.3 连接方式4.4 相关代码4.5 烧录验证 5. 注意事项6. 相关链接 1. 前言 在这个智能硬件和物联网时代,MicroPython和树莓派PICO正以其独特的优势引领着嵌入式开发…

谷歌 | Duet AI 让洞察、聚类模型和可视化变得简单

迷失在数据的海洋 我们都经历过这样的情况:淹没在数据的海洋中,努力驾驭复杂的管道,感觉数据令人头晕。管理大量充满不同工具和 Google 搜索的选项卡以及花费大量时间筛选数据和代码以创建满足您需求的模型所带来的挫败感,真的会…

探索UX设计师的日常任务,赶紧看看

UX 设计师专注于产品开发的各个方面,包括设计、可用性、功能、甚至品牌和营销。他们的工作涉及用户与产品交互的整个端到端旅程,包括为产品和业务识别新的机会。 鉴于他们广泛的范围,UX 设计师根据公司和项目的要求,执行多种不同…

探索 MajicStudio:一款多功能视频编辑软件

一、产品简介 MajicStudio是一款基于人工智能的图片编辑与设计工具,拥有简洁的界面与丰富功能。采用深度学习和计算机视觉技术可以自动识别图片要素。 二、应用场景 MajicStudio的AI图像功能适用于多场景,包括艺术设计、电商、游戏和文创等场景。 三…

【Proteus仿真】【Arduino单片机】蓝牙遥控小车

文章目录 一、功能简介二、软件设计三、实验现象联系作者 一、功能简介 本项目使用Proteus8仿真Arduino单片机控制器,使LCD1602液晶,L298电机,直流电机,HC05/06蓝牙模块等。 主要功能: 系统运行后,LCD1602…

数据结构和算法笔记2:二分法

二分法网上有两种写法&#xff0c;一种左闭右闭&#xff0c;一种左闭右开&#xff0c;个人习惯左闭右闭的写法&#xff0c; 有序数组查找数 这是标准二分法&#xff0c;对应力扣的704. 二分查找&#xff1a; 求值为target的索引 int search(vector<int>& nums, i…

Thread类的基本用法

⭐ 作者&#xff1a;小胡_不糊涂 &#x1f331; 作者主页&#xff1a;小胡_不糊涂的个人主页 &#x1f4c0; 收录专栏&#xff1a;JavaEE &#x1f496; 持续更文&#xff0c;关注博主少走弯路&#xff0c;谢谢大家支持 &#x1f496; Thread 1. 线程创建1.1 继承Thread类1.2 实…

arcEngine修改字段标注

修改字段标注 在arcEngine中&#xff0c;有时候需要修改图层要素的标注值&#xff0c;而且每个字段值对应了要修改的内容&#xff0c;如字段值”1“替换成”A“&#xff0c;字段值”2“替换成”B“等&#xff0c;这就需要在替换的图层中&#xff0c;遍历每个要素&#xff0c;查…

LeetCode 21 合并两个有序链表

题目描述 合并两个有序链表 将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。 示例 1&#xff1a; 输入&#xff1a;l1 [1,2,4], l2 [1,3,4] 输出&#xff1a;[1,1,2,3,4,4]示例 2&#xff1a; 输入&#xff1a;l1 [],…

「用户与社区的深度对话」2023年度IvorySQL满意度调研

致IvorySQL社区成员&#xff0c; &#x1f3c3;‍♂️2023年即将进入尾声&#xff0c;感谢每一位社区朋友对IvorySQL的支持。我们诚挚地邀请您参与我们的社区满意度调研。您的反馈对我们至关重要&#xff0c;将有助于改进我们的服务&#xff0c;为您提供更好的社区体验&#xf…

[数据结构进阶 C++] 二叉搜索树(BinarySearchTree)的模拟实现

文章目录 1、二叉搜索树1.1 二叉搜索数的概念1.2 二叉搜索树的操作1.2.1 二叉搜索树的查找1.2.2 二叉搜索树的插入1.2.3 二叉搜索树的删除 2、二叉搜索树的应用2.1 K模型2.2 KV模型 3、二叉搜索树的性能分析4、K模型与KV模型完整代码4.1 二叉搜索树的模拟实现&#xff08;K模型…

设计模式(三)-结构型模式(6)-享元模式

一、为何需要享元模式&#xff08;Flyweight&#xff09;? 假如在网页中渲染这样的一个画面&#xff1a;大小不一的星星铺满了整个画布&#xff0c;并且都在不断的进行移动闪烁着。一批星星消失了&#xff0c;另一批又从另一边缘处出现。 要实现这样的渲染效果&#xff0c;在…

C语言之初识C语言

文章目录 前言一、什么是C语言二、第一个C语言程序三、数据类型四、变量&#xff0c;常量1、变量1.1 变量的命名1.2 变量的分类1.3 变量的使用1.4 变量的作用域和生命周期2、变量 五、字符串1. 概念2. 求解字符串的长度【strlen】3. 转义字符【含笔试题】 六、注释七、选择语句…

ESP8266 TCP/串口透传

简介 先在PC上做测试, 使用串口软件对ESP8266 模块进行设置, 使用网络助手软件与串口软件进行自由收发设置 ATRST ## 复位 ATCWMODE_DEF1 ## 设置为Station模式 ATCWJAP_DEF“路由器wifi名称”,“路由器wifi密码” ## 设置ESP连接的路由器名称密码 ATCIPSTART“TCP”,“192.1…

Alpha突触核蛋白神经退行性疾病

Alpha突触核蛋白科研背景 ● Alpha突触核蛋白约 15kDa, 140个氨基酸 ● StressMarq在E. coli中过表达人源基因然后将蛋白从细胞质基质中纯化出来 ● 未折叠的alpha突触核蛋白单体在12% SDS-PAGE上为~15 kDa的条带 StressMarq/欣博盛生物的Alpha突触核蛋白有以下两类&#xf…

[uni-app] mescroll与 page 本身的滚动冲突处理, 动态禁用下拉刷新

参考贴: uniapp动态禁用mescroll-body组件的下拉刷新,或者动态禁用mescroll-body组件的上拉加载 记录问题场景 如图: 搜索和 第二个标签栏, 都是随页面滚动的, 当页面滚动一定距离, 会触发标签栏的吸顶 即如上图, 问题描述 当列表页面数据部满屏时, 且页面已经由于滚动而吸顶…

许可式邮件营销与垃圾邮件的区别:合规与效果的关键区分

接触过邮件营销的人一定不陌生“垃圾邮件”和“许可式邮件营销”这两个名词。在各大电商节到来之际&#xff0c;小编帮助大家弄清楚什么是垃圾邮件&#xff1f;什么是许可式邮件营销&#xff1f;为什么会变成垃圾邮件&#xff1f;怎么做许可式邮件营销&#xff1f;让大家在促销…