当前位置: 首页 > news >正文

【Skynet 入门实战练习】实现网关服务 | 用户代理 | RPC 协议 | 客户端

文章目录

  • 前言
    • 网关服务
    • RPC 协议
    • 看门狗服务
    • 代理服务
    • 客户端
    • 逻辑梳理

前言

上两章学习了如何搭建一个项目,简单实现了几个基础模块。本章节会实现基本的客户端与服务端的通信,包括网关(gate)、看门狗(watchdog)、代理(agent)三个重要的服务,以及客户端的实现等。

网关服务

参考:websocket-gate 实现网关服务

一般客户端连接服务器选用长链接模式,skynet 支持 TCPwebsocket,我们采用 websocket 的连接方式。

网关负责客户端的网络连接,通过 websocket 和客户端交换数据。我们可以通过普通服务创建方式来创建一个 gate 服务,但这个服务启动后,并不是马上开始工作,需要发一个 lua 消息 open,告诉 gate 监听的端口、最大连接数、延时等信息。

网关服务 service/ws_gate.lua 需要的基本接口:

local CMD = {} -- gate 服务接口
local handler = {} -- websocket 操作接口function handler.connect(fd)
end function handler.handshake(fd, header, url) 
end function handler.message(fd, msg)
end function handler.ping(fd)
endfunction handler.pong(fd)
endfunction handler.close(fd, code, reason)
endfunction handler.error(fd)
endfunction handler.warning(fd, size)
endfunction CMD.open(source, conf) 
end skynet.register_protocol {name = "client",id = skynet.PTYPE_CLIENT,
}skynet.start(function()skynet.dispatch("lua", function(session, source, cmd, ...)local f = CMD[cmd]if not f then skynet.ret(skynet.pack({ok=false}))return end if session == 0 then f(source, ...)else skynet.ret(skynet.pack(f(source, ...)))end end)skynet.register(".ws_gate")
end)

CMD 是一个服务的 lua 消息回调函数表,gate 服务会注册 (dispatch) 相关的 lua 消息,其他服务与 gate 通信,那么就会去到 CMD 中查找相关的处理函数,并根据调用方式 skynet.callskynet.send 做相关的数据返回。例如 if session == 0 then 即判断是 skynet.send 调用就只需要执行即可无需返回。而是 skynet.call 调用方式,通过 skynet.ret(skynet.pack()) 进行消息打包返回给调用方。

来看具体的 CMD.open 实现:

-- call by ws_watchdog(start)
function CMD.open(source, conf) WATCHDOG = conf.watchdog or source MAXCLIENT = conf.maxclient or 1024nodelay = conf.nodelaylocal protocol = conf.protocol or "ws"local port = assert(conf.port)local address = conf.address or "0.0.0.0"local fd = socket.listen(address, port)logger.info(SERVICE_NAME, string.format("Listen websocket port: %s protocol: %s", port, protocol))socket.start(fd, function(fd, addr) logger.info(SERVICE_NAME, string.format("accept client socket_fd: %s addr: %s", fd, addr))websocket.accept(fd, handler, protocol, addr)end)
end 

该方法是由 ws_watchdog 服务调用,方法中我们会获取到 watchdog 的地址,最大客户端连接数,TCP 是否延迟,通信协议 protocol,以及网关需要监听的地址 address 端口 port

-- main.lua
-- 通知 ws_watchdog 启动服务
skynet.call(ws_watchdog, "lua", "start", {port = watchdog_port, maxclient = max_online_client,nodelay = true, protocol = ws_protocol,
})-- ws_watchdog.lua
function CMD.start(conf)-- 开启 gate 服务skynet.call(GATE, "lua", "open", conf)
end

对于服务器,通常我们需要监听一个端口,并转发某个接入连接的处理权。那么可以用如下 API :

socket.listen(address, port) 监听一个端口,返回一个 id ,供 start 使用。
socket.start(id , accept) accept 是一个函数。每当一个监听的 id 对应的 socket 上有连接接入的时候,都会调用 accept 函数。这个函数会得到接入连接的 id 以及 ip 地址。你可以做后续操作。

socket 的 id 对于整个 skynet 节点都是公开的。也就是说,你可以把 id 这个数字通过消息发送给其它服务,其他服务也可以去操作它。任何一个服务只有在调用 socket.start(id) 之后,才可以收到这个 socket 上的数据。

handler 是一个 websocket 协议的接口表,需要有 connect / handshake / message / ping / pong / close / error这些接口方法实现。使用 websocket.accept 监听端口时,需要传入这个表,以供对上行的 socket 消息进行分别处理。该方法的源码地址:websocket.accept,每来一个连接执行一次 accept,内部使用 socket.start 获取客户端上行数据,并且执行 xpcall(resolve_accept, ...) 调用。在函数 resolve_accept 中,会先执行 connecthandshake 这两个注册在 handler 表中的方法,然后循环去读取上行的数据,对应执行其他的方法。

在这里插入图片描述

如上图,一个客户端连入,网关会监听到,并执行 handler.connecthandler.handshake 这两个方法。

来看一下 handler.connect

function handler.connect(fd)logger.debug(SERVICE_NAME, "ws connect from: ", tostring(fd))if client_number >= MAXCLIENT then socketdriver.close(fd)return end if nodelay then socketdriver.nodelay(fd)end client_number = client_number + 1local addr = websocket.addrinfo(fd)local c = {fd = fd,ip = addr, }connection[fd] = cskynet.send(WATCHDOG, "lua", "socket", "open", fd, addr)
end 

网关会控制当前连入的客户端数量,超过就不再授入连接,并且会对每个连接都设置 nodelay 属性,确保数据以小包的形式实时发送,在服务端无需额外对上行的数据进行 TCP 的拆包操作。详细的 nodelay 知识补充参考:浅谈tcp_nodelay的作用

网关服务还应该维护着客户端的连接,connection 表通过 fd 映射每一个客户端连接,同时在客户端下线,也应该清理对应的连接。

连接处理函数的最后一行还执行了一次向 ws_watchdog 服务发送的 socket消息,通知看门狗当前这个新连接的连入做相应处理。因为网关服务只是做一个通信层面的,负责客户端上行数据的转发,不做太多的逻辑处理。而客户端上行数据的逻辑处理主要就交给代理服务 ws_agent。本项目没有单独开一个登陆注册的服务,对这方面没有太复杂的需求,所以简单的登录注册逻辑就交给看门狗服务了。


客户端通信的逻辑:

client -> websocket.connect -> gate(handler.connect) -> watchdog(SOCKET.data) -> agent(login)

客户端通过 websocket.connect 连接 gategate 通知(openwatchdog 为该连接设置定时器 timer,在时限之内,客户端需要发送登录请求,消息被 gatehandler.message 收到,发现该连接没有绑定代理 agent,消息就发往 watchdog,执行登录流程,验证成功则调用 agentlogin 方法,通知网关绑定上代理。否则消息发往 agent,执行相应逻辑处理。

下面来看 handler.message 方法:

function handler.message(fd, msg)logger.debug(SERVICE_NAME, "ws message from: ", tostring(fd), ", msg: ", msg)-- recv a package, forward itlocal c = connection[fd]local agent = c and c.agent if agent then -- msg is stringskynet.redirect(agent, c.client, "client", fd, msg)elseskynet.send(WATCHDOG, "lua", "socket", "data", fd, msg)end 
end 

客户端通过 websocket 发送上来的数据 msg 是一个 lua 字符串,在 message 中进行消息分发。如果连接绑定了代理就以 client 消息类型转发 redirect 到代理处理,否则发送 socket 类型消息给看门狗。

skynet.redirect(addr, source, type, ...):伪装成 source 地址,向 addr 发送一个消息。只有注册了 client 消息,才能使用 skynet.redirect 来发送 client 消息。

我们在 gate 服务中注册了 client 消息,专门用来将接收到的网络数据转发给 agent。不需要解包,也不需要打包。

skynet.register_protocol {name = "client",id = skynet.PTYPE_CLIENT,
}

网关服务完整代码:ws_gate.lua


RPC 协议

客户端和服务器交互的协议采用 JSON 格式,第三方工具:lua-cjson。

客户端发给服务端协议名前缀 c2s,服务端返回给客户端协议名前缀 s2c。协议的 ID 使用字段 pid 表示对应要处理的协议函数名。

例如协议 login 和 协议 heartbeat

客户端:

{pid = "c2s_heartbeat",
}{pid = "c2s_login",token = "token",acc = "account",sign = "checksum",
}

服务端:

{pid = "s2c_login",uid = "user id",msg = "Login success"
}

本项目通信设计的 RPC 协议,需要 pid 和其他参数字段。


接下来需要设计服务端和用户端的通信模块,服务端逻辑处理模块都放在 module 文件夹下,ws_agent/mng.lua 对应代理的服务逻辑处理模块,ws_watchdog/mng.lua 对应看门狗服务对应的逻辑处理模块。

这里我们先关注这两个模块的 RPC 逻辑:

ws_watchdog/mng.lua

local _M = {} -- 模块接口
local RPC = {} -- RPC 协议接口local function check_sign(token, acc, sign)local checkstr = token .. acc local checksum = md5.sumhexa(checkstr)if checksum == sign then return true end return false 
end -- 登录协议处理函数
function RPC.c2s_login(req, fd)-- token 验证if not check_sign(req.token, req.acc, req.sign) then _M.close_fd(fd)return end -- 登录成功,分配代理,移除超时队列local res = skynet.call(AGENT, "lua", "login", req.acc, fd)noauth_fds[fd] = nil return res 
end-- 协议根据 pid 执行对应函数
function _M.handle_proto(req, fd)local f = RPC[req.pid]local res = f(req, fd)return res 
endreturn _M  

上面谈及,看门狗服务我们会用来处理用户的登录逻辑。模块中 handle_proto 函数作为 RPC 协议处理的入口,根据 req.pid 对应到服务端应该执行的客户端上行协议处理函数。简单阅读上述代码,登录判定就是用 md5token .. acc 进行编码与 sign 对比,成功后会通知 ws_agent 服务,并传入用户账号 acc 和客户端 fd,然后在网关绑定客户端和代理,客户端通信就移交给了代理服务。

ws_agent/mng.lua

local _M = {} 
local RPC = {} -- c2s_heartbeat
function RPC.c2s_heartbeat(req, fd, uid)local user = online_users[uid]if not user thenreturn end user.heartbeat = skynet.time()
end function _M.handle_proto(req, fd, uid)local f = RPC[req.pid]local res = f(req, fd, uid)return res
end return _M 

同理,在代理逻辑处理模块中,用于处理除了登录协议之外的其他协议,比如这里的 heartbeat。客户端登录成功,客户端设置了每 5 秒一个心跳包,服务端收到后,就会执行 user.heartbeat = skynet.time() ,维护在线用户表中用户的 heartbeat 字段。并且服务端会定时一分钟一次检测,超时踢出。

-- ws_agent/mng.luafunction _M.check_user_online(uid)local user = online_users[uid]if user then -- 心跳超时踢出if not user.heartbeat or skynet.time() - user.heartbeat >= user_alive_keep_time then _M.close_fd(user.fd)end end 
end function _M.login(acc, fd)local uid = 1 -- 数据库加载数据local user = {fd = fd, acc = acc,}online_users[uid] = user fd2uid[fd] = uid -- 通知 gate 消息由 agent 接管,绑定客户端和代理skynet.call(GATE, "lua", "forward", fd)-- 定时检查心跳local timerid = timer.timeout_repeat(60, _M.check_user_online, uid)user.timerid = timeridlocal res = {pid = "s2c_login",uid = uid, msg = "Login success",}return res
end 

至此,服务端是如何与客户端通信,如何实现用户的登录逻辑,我们就已经有了一个概念。其他服务及客户端的具体实现,且继续往下看。


看门狗服务

看门狗服务主要负责 gate 的创建,agent 的创建与退出。本项目只有一个 agent,所有客户端都会绑定这个代理。如果是一个客户端对应一个代理服务,那么看门狗就可以做一个代理池,进行代理分配,代理回收等。

在主服务 main.lua 中,启动看门狗服务:

-- 开启 ws_watchdog 服务
local ws_watchdog = skynet.newservice("ws_watchdog")-- 通知 ws_watchdog 启动服务
skynet.call(ws_watchdog, "lua", "start", {port = watchdog_port, maxclient = max_online_client,nodelay = true, protocol = ws_protocol,
})

ws_watchdog.lua

local skynet = require "skynet"
local mng = require "ws_watchdog.mng"
local cjson = require "cjson"local CMD = {} -- 服务操作接口
local SOCKET = {} -- socket 相关操作接口
local GATE -- gate 服务地址
local AGENT -- agent 服务地址function SOCKET.data(fd, msg)local req = cjson.decode(msg)if not req.pid then return end -- 判断客户端认证是否通过if not mng.check_auth(fd) then -- 没认证,且不是登录协议,踢下线if not mng.is_no_auth(req.pid) then mng.close_fd(fd)return end end -- 登录协议 or 其他协议处理local res = mng.handle_proto(req, fd)if res then skynet.call(GATE, "lua", "response", fd, cjson.encode(res))end 
endfunction CMD.start(conf)-- 开启 gate 服务skynet.call(GATE, "lua", "open", conf)
endfunction CMD.kick(fd)-- 踢客户端下线mng.close_fd(fd)
endskynet.start(function()skynet.dispatch("lua", function(session, source, cmd, subcmd, ...)if cmd == "socket" thenlocal f = SOCKET[subcmd]f(...)-- socket api don't need returnelselocal f = assert(CMD[cmd])skynet.ret(skynet.pack(f(subcmd, ...)))endend)GATE = skynet.newservice("ws_gate")AGENT = skynet.newservice("ws_agent")mng.init(GATE, AGENT)skynet.call(AGENT, "lua", "init", GATE, skynet.self())
end)

看门狗服务启动skynet.newservice("ws_watchdog"),会依次启动网关和代理服务。这里主要来看一下处理连接登录逻辑的具体步骤,先判断客户端认证是否通过(check_auth),没通过就需要限制当前看门狗服务能处理的逻辑,只能处理登录逻辑(is_no_auth),不是登录协议就不受理。

ws_watchdog/mng.lua

local skynet = require "skynet"
local timer = require "timer"
local md5 = require "md5"local _M = {} -- 模块接口
local GATE -- gate 服务地址
local AGENT -- agent 服务地址local noauth_fds = {} -- 未通过认证的服务端
local TIMEOUT_AUTH = tonumber(skynet.getenv("ws_watchdog_timeout_auth")) or 10-- 标记哪些协议不用登录就能访问
local no_auth_proto_list = {c2s_login = true, 
}function _M.is_no_auth(pid)return no_auth_proto_list[pid]
end -- 超时检测,踢掉没通过认证的客户端
local function timeout_auth(fd)local time = noauth_fds[fd]if not time then return end if skynet.time() - time < TIMEOUT_AUTH then return end _M.close_fd(fd)
endfunction _M.init(gate, agent)GATE = gate AGENT = agent
end function _M.open_fd(fd)noauth_fds[fd] = skynet.time()timer.timeout(TIMEOUT_AUTH + 1, timeout_auth, fd)
end function _M.close_fd(fd)skynet.send(GATE, "lua", "kick", fd)skynet.send(AGENT, "lua", "disconnect", fd)noauth_fds[fd] = nil 
endfunction _M.check_auth(fd)if noauth_fds[fd] then return falseend return true 
end return _M 

ws_watchdog/mng 模块中,维护了 noauth_fds 表,还未通过认证的客户端。设定了连接登录的超时时间 TIMEOUT_AUTH ,可在配置文件中修改。

完整代码:ws_watchdog.lua、ws_watchdog/mng.lua


代理服务

代理服务主要负责接受 gate 转发的请求,处理业务,然后直接把应答响应给 gate 发到客户端。

ws_agent.lua

skynet.register_protocol {name = "client",id = skynet.PTYPE_CLIENT,unpack = skynet.tostring, dispatch = function(fd, address, msg)skynet.ignoreret()  -- session is fd, don't call skynet.ret-- 解析消息,pid:协议idlocal req = cjson.decode(msg)if not req.pid then return end -- 登录成功会绑定 fd: uidlocal uid = mng.get_uid(fd)if not uid then mng.close_fd(fd) -- close_fd 应该实现给watchdog发消息关闭清空资源吧,end     local res = mng.handle_proto(req, fd, uid) if res then skynet.call(GATE, "lua", "response", fd, cjson.encode(res))end end
}skynet.start(function()skynet.dispatch("lua", function(_, _, command, ...)-- skynet.trace()local f = CMD[command]skynet.ret(skynet.pack(f(...)))end)skynet.register(".ws_agent")
end)

代理服务注册 skynet.PTYPE_CLIENT 类型消息,还记得网关也注册了吗?网关收到客户端网络消息,包装成 client 消息,直接发给代理,代理通过 unpack = skynet.tostring 解包消息,通过 dispatch 分发客户端上行的网络消息。

完整代码:ws_agent.lua、ws_agent/mng.lua


客户端

客户端我们也实现为一个 skynet 中的 lua 服务,需要指定配置文件启动 ./skynet/skynet etc/config.client

etc/config.client

include "config"thread = 2server_host = "127.0.0.1" bootstrap = "snlua bootstrap" 
start = "test/client"
logtag = "client"

主服务指定为 test/client,日志文件标识为 logtag = "client"

下面来看 test/client.lua

local ws_id -- websocket 连接 ID
local cmds = {} -- 命令模块,ws: ws.lua、gm: gm.lua-- 搜索加载命令模块
local function fetch_cmds()local t = utils_file.scandir("test/cmds")for _, v in pairs(t) do local cmd = utils_string.split(v, ".")[1] -- ws、gmlocal cmd_mod = "test.cmds." .. cmd cmds[cmd] = require(cmd_mod)end 
end skynet.start(function()dns.server() -- 初始化 dnsfetch_cmds()skynet.fork(websocket_main_loop)skynet.fork(console_main_loop)
end)

启动客户端,初始化设置 dns 服务器。cmds 作为命令模块,相关的 RPC 通信协议命令写在 ws.lua 模块中,GM 指令模块写在 gm.lua 模块中。fetch_cmds 会利用 filestring 工具搜索加载命令模块,存储在 cmds 中。

参考 官方 wiki skynet.dns:

在 skynet 的底层,当使用域名而不是 ip 时,由于调用了系统 api getaddrinfo ,有可能阻塞住整个 socket 线程(不仅仅是阻塞当前服务,而是阻塞整个 skynet 节点的网络消息处理)。虽然大多数情况下,我们并不需要向外主动建立连接。但如果你使用了类似 httpc 这样的模块以域名形式向外请求时,一定要关注这个问题。

skynet 暂时不打算在底层实现非阻塞的域名查询。但提供了一个上层模块来辅助你解决 dns 查询时造成的线程阻塞问题。

local dns = require "skynet.dns" 加载这个模块

dns.server(ip, port) : port 的默认值为 53 。如果不填写 ip 的话,将从 /etc/resolv.conf 中找到合适的 ip 。
dns.resolve(name, ipv6) : 查询 name 对应的 ip ,如果 ipv6 为 true 则查询 ipv6 地址,默认为 false 。如果查询失败将抛出异常,成功则返回 ip ,以及一张包含有所有 ip 的 table 。
dns.flush() : 默认情况下,模块会根据 TTL 值 cache 查询结果。在查询超时的情况下,也可能返回之前的结果。dns.flush() 可以用来清空 cache 。注意:cache 保存在调用者的服务中,并非针对整个 skynet 进程。所以,推荐写一个独立的 dns 查询服务统一处理 dns 查询。

上述代码还启用了两个协程,分别执行 websocket_main_loopconsole_main_loop

-- 网络循环
local function handle_resp(ws_id, res)for _, cmd_mod in pairs(cmds) do if cmd_mod.handle_res then cmd_mod.handle_res(ws_id, res)end end 
end local function websocket_main_loop()-- 连接服务器local ws_protocol = skynet.getenv("ws_watchdog_protocol")local ws_port = skynet.getenv("ws_watchdog_port")local server_host = skynet.getenv("server_host")local url = string.format("%s://%s:%s/client", ws_protocol, server_host, ws_port)ws_id = websocket.connect(url)while true do local res, close_reason = websocket.read(ws_id)local ok, err = xpcall(handle_resp, debug.traceback, ws_id, cjson.decode(res))websocket.ping(ws_id)end 
end 

网络循环中,连接服务器成功后会对服务器下行的数据进行读取处理,调度到模块中的 handle_res 分别找到不同的模块处理相应的网络协议。

-- 执行注册的命令
local function run_command(cmd, ...)local cmd_mod = cmds[cmd]if cmd_mod then cmd_mod.run_command(ws_id, ...) end 
end -- 命令交互
-- ws login acc
local function console_main_loop()local stdin = socket.stdin()while true do local cmdline = socket.readline(stdin, "\n")if cmdline ~= "" then local split = split_cmdline(cmdline)local cmd = split[1]local ok, err = xpcall(run_command, debug.traceback, cmd, select(2, table.unpack(split)))end end 
end 

命令的读取循环中,通过输入 ws login user_count 等指令,找到 ws 模块下的 login 回调方法,对应 c2s_login RPC 协议发送数据 websocket.write 给服务端。

完整代码:test/client.lua、test/cmds/ws.lua


逻辑梳理

通过上面几节描述,我们已经实现了自定义 RPC 协议,完成客户端与服务端的通信。

最后,我们再来梳理一下完整的服务端启动逻辑,客户端连入、退出等逻辑。

服务端启动:主服务 main.lua 启动看门狗服务,看门狗启动网关和代理服务,并且主服务发了一个 start 消息给看门狗,看门狗随即发了一个 open 消息给网关,网关则打开了监听端口,等待连接的接入。同时代理也注册好了 client 消息,等待客户端的数据交互。

客户端连接登录:客户端 websocket.connect(url) 成功连入服务器,网关 websocket.accept(fd, handler, protocol, addr) 感知到是一个连接消息,handler.connect(fd) 随即执行,设置好该连接相关属性后通知看门狗对连接进行处理,即设置一个连接超时定时器。然后,客户端输入登录命令,数据 websocket.write(ws_id, cjson.encode(req)) 上行到服务器,handler.message(fd, msg) 感知到来了一条客户端消息,对该连接判断是否绑定代理,未绑定代理则通知看门狗处理一下这条 socket 消息,仅限登录协议 c2s_login 消息能处理。随后,看门狗验证通过,通知代理授理登录消息。代理服务通知网关将这条连接绑定上代理。之后客户端上行的数据,网关通过 skynet.redirect(agent, c.client, "client", fd, msg) 直接转发给代理处理。

客户端退出:

  1. 客户端主动退出 CTRL + C:触发网关的 handler.error(fd),调用 close_fd 清空网关维护的该连接资源,并且通知看门狗这条 socketerror 消息,看门狗调用 close_fd 清空看门狗维护的该连接资源,并由看门狗通知代理 disconnect 该连接和网关 kick 该连接。代理清空维护的该客户端的资源,网关则执行 websocket.close(fd) 实际的关闭了连接,但由于客户端主动断开,未经 websocket 协议正常关闭流程,不触发 handler.close
  2. 客户端被动退出:连接超时,看门狗 timeout_auth 进行超时检测,调用 close_fd 同上述。但是在网关 websocket.close(fd) 执行后,服务端发送一个 websocket 关闭帧通知客户端并等待回应,所以会触发 handler.close(fd, code, reason),然后执行网关 close_fd ,并给看门狗发了一个 socketclose 消息,看门狗再一次 close_fd。编码保证了多次释放资源不会造成问题。
  3. 客户端被动退出:心跳超时,代理 check_user_online 进行心跳检测,调用 close_fd,会执行 disconnect 逻辑释放代理维护的资源,然后通知网关 kick 连接,同上述。

相关文章:

【Skynet 入门实战练习】实现网关服务 | 用户代理 | RPC 协议 | 客户端

文章目录 前言网关服务RPC 协议看门狗服务代理服务客户端逻辑梳理 前言 上两章学习了如何搭建一个项目&#xff0c;简单实现了几个基础模块。本章节会实现基本的客户端与服务端的通信&#xff0c;包括网关&#xff08;gate&#xff09;、看门狗&#xff08;watchdog&#xff0…...

eclipse - jee 建立项目后没有 web.xml

eclipse -- jee 建立项目后没有 web.xml 处理它的方法是&#xff0c;点 File - New - Dynamic Web Project , 此时起一个项目名如M4 然后next 然后next 出现如此所示:...

Miniconda虚拟环境安装(chatglm2大模型安装步骤二)

1.服务器配置 服务器系统&#xff1a;Centos7.9 x64 显卡&#xff1a;RTX3090 &#xff08;24G&#xff09; 2.安装环境 2.1 检查conda是否安装 输入命令&#xff1a;conda -V 如果显示conda 4.7.12&#xff0c;说明已经有不需要安装 2.2 安装Miniconda 在家目录建一个mi…...

C++学习之路(四)C++ 实现简单的待办事项列表命令行应用 - 示例代码拆分讲解

本期示例介绍: 本期示例《待办事项列表应用》展示了一个简单的任务管理系统&#xff0c;用户可以通过命令行界面执行添加任务、删除任务和显示任务列表等操作。 功能描述&#xff1a; 添加任务功能&#xff1a; 用户可以输入任务描述&#xff0c;将新的任务添加到任务列表中。…...

函数指针数组指针数组传参的本质字符指针

&#x1f680; 作者&#xff1a;阿辉不一般 &#x1f680; 你说呢&#xff1a;不服输的你&#xff0c;他们拿什么赢 &#x1f680; 专栏&#xff1a;爱上C语言 &#x1f680;作图工具&#xff1a;draw.io(免费开源的作图网站) 如果觉得文章对你有帮助的话&#xff0c;还请点赞…...

Linux swapon命令教程:如何在Linux中启用和禁用交换空间(附实例教程和注意事项)

Linux swapon命令介绍 Linux的swapon命令用于启用指定设备和文件的交换。当物理内存&#xff08;RAM&#xff09;达到其最大容量时&#xff0c;Linux使用交换空间。如果系统需要更多的内存&#xff0c;而RAM不足&#xff0c;内存中的非活动页面将被移动到交换空间。交换空间是…...

云计算领域的第三代浪潮!

根据IDC不久前公布的数据&#xff0c;2023年上半年中国公有云服务整体市场规模(IaaS/PaaS/SaaS)为190.1亿美元&#xff0c;阿里云IaaS、PaaS市场份额分别为29.9%和27.9%&#xff0c;都远超第二名&#xff0c;是无可置疑的行业领头羊。 随着人工智能&#xff08;AI&#xff09;…...

面试题目总结(一)

1. 谈谈数据库的乐观锁和悲观锁 乐观锁和悲观锁是数据库并发控制中常用的两种策略&#xff0c;用于处理多个事务同时访问和修改同一个数据时的并发冲突问题。 数据库的乐观锁是指在读取数据时&#xff0c;不对数据进行加锁&#xff0c;而是在更新数据时检查数据版本是否发生变…...

建造者设计模式

3. 建造者设计模式 3.1 原理 Builder 模式&#xff0c;中文翻译为建造者模式或者构建者模式&#xff0c;也有人叫它生成器模式。 建造者模式是用来创建一种类型的复杂对象&#xff0c;通过设置不同的可选参数&#xff0c;“定制化”地创建不同的对象。 创建者模式主要包含以…...

YOLO目标检测——垃圾检测数据集下载分享【含对应voc、coco和yolo三种格式标签】

实际项目应用&#xff1a;智能化垃圾分类系统、垃圾回收和处理领域的优化管理等方面数据集说明&#xff1a;垃圾分类检测数据集&#xff0c;真实场景的高质量图片数据&#xff0c;数据场景丰富&#xff0c;含报纸、蛋壳、矿泉水瓶、电池、拉链顶罐、塑料餐盒、纸质药盒、香蕉皮…...

Vue CLI的介绍【vue利器之一】

文章目录 前言Vue CLI 介绍CLICLI 服务CLI 插件后言 前言 hello world欢迎来到前端的新世界 &#x1f61c;当前文章系列专栏&#xff1a;vue.js &#x1f431;‍&#x1f453;博主在前端领域还有很多知识和技术需要掌握&#xff0c;正在不断努力填补技术短板。(如果出现错误&am…...

【学习笔记】插值之拉格朗日插值(Lagrange)

0 插值介绍 插值法是广泛应用于理论研究和工程实际的重要数值方法。用提供的部分离散的函数值来进行理论分析和设计都是极不方便的&#xff0c;因此希望能够用一个既能反映原函数特征&#xff0c;又便于计算的简单函数去近似原函数。 1 低次拉格朗日插值 定理&#xff1a;设…...

无人机电力巡检系统运行流程全解读

随着电力行业体系不断完善&#xff0c;保障电网运营的安全成为至关重要的任务。传统的人工巡检方式在面对电力设备广泛分布和复杂工况时显得效率低下&#xff0c;为了解决这一难题&#xff0c;无人机电力巡检系统应运而生&#xff0c;以智能化的运行流程&#xff0c;为电网安全…...

有关全局变量和sizeof的题

#define _CRT_SECURE_NO_WARNINGS 1 #include<stdio.h> int i; int main() {i--;if (i > sizeof(i)){printf(">");}else{printf("<");}return 0; } 这道题结果是 > 首先对于一个全局变量&#xff0c;当没有对其初始化时&#xff0c;它…...

vue简述

vue为渐进式框架&#xff1a;vmmv 1.易用 有html、css、javascript基础&#xff0c;即可学习vue框架 2.高效、开发前端页面 非常高效 1.vue的体积小、压缩完只需要20k的大小 2.超快的虚拟dom操作js中非常多的dom操作&#xff0c;vue设计虚拟dom非常快 3.设计时vue底层深度优化 …...

YOLOv8 训练自己的分割数据集

之前写过一篇 使用YOLOv8训练自己的【目标检测】数据集-【收集数据集】-【标注数据集】-【划分数据集】-【配置训练环境】-【训练模型】-【评估模型】-【导出模型】&#xff0c;里面带大家整个流程走过一遍了&#xff0c; 这篇文章我们来介绍如何使用 YOLOv8 训练分割数据集&a…...

Python实现DDos攻击实例详解

文章目录 SYN 泛洪攻击Scapy3k 基本用法代码实现DDos 实现思路argparse 模块socket 模块代码实现Client 端程序测试后记关于Python技术储备一、Python所有方向的学习路线二、Python基础学习视频三、精品Python学习书籍四、Python工具包项目源码合集①Python工具包②Python实战案…...

微信小程序实现【点击 滑动 评分 评星(5星)】功能

wxml文件&#xff1a; <view class"wxpl_xing"><view class"manyidu">{{scoreContent}}</view><view><block wx:for{{scoreArray}} wx:for-item"item"><view classstarLen bindtapchangeScore data-sy"{{…...

堡垒机的用途

堡垒机的用途 堡垒机&#xff0c;即在一个特定的网络环境下&#xff0c;为了保障网络和数据不受来自外部和内部用户的入侵和破坏&#xff0c;而运用各种技术手段监控和记录运维人员对网络内的服务器、网络设备、安全设备、数据库等设备的操作行为&#xff0c;以便集中报警、及时…...

超全超实用行业解决方案合集,覆盖十大行业数据应用需求

现代企业面对复杂的业务需求&#xff0c;对数据分析的需求日益增加。 从实时销售到市场趋势&#xff0c;从客户行为到产品优化&#xff0c;每个环节都依赖于数据支持。然而&#xff0c;传统的数据分析平台常分散在不同系统和团队中&#xff0c;形成数据孤岛&#xff0c;降低了…...

RocketMQ延迟消息机制

两种延迟消息 RocketMQ中提供了两种延迟消息机制 指定固定的延迟级别 通过在Message中设定一个MessageDelayLevel参数&#xff0c;对应18个预设的延迟级别指定时间点的延迟级别 通过在Message中设定一个DeliverTimeMS指定一个Long类型表示的具体时间点。到了时间点后&#xf…...

从零实现富文本编辑器#5-编辑器选区模型的状态结构表达

先前我们总结了浏览器选区模型的交互策略&#xff0c;并且实现了基本的选区操作&#xff0c;还调研了自绘选区的实现。那么相对的&#xff0c;我们还需要设计编辑器的选区表达&#xff0c;也可以称为模型选区。编辑器中应用变更时的操作范围&#xff0c;就是以模型选区为基准来…...

mongodb源码分析session执行handleRequest命令find过程

mongo/transport/service_state_machine.cpp已经分析startSession创建ASIOSession过程&#xff0c;并且验证connection是否超过限制ASIOSession和connection是循环接受客户端命令&#xff0c;把数据流转换成Message&#xff0c;状态转变流程是&#xff1a;State::Created 》 St…...

理解 MCP 工作流:使用 Ollama 和 LangChain 构建本地 MCP 客户端

&#x1f31f; 什么是 MCP&#xff1f; 模型控制协议 (MCP) 是一种创新的协议&#xff0c;旨在无缝连接 AI 模型与应用程序。 MCP 是一个开源协议&#xff0c;它标准化了我们的 LLM 应用程序连接所需工具和数据源并与之协作的方式。 可以把它想象成你的 AI 模型 和想要使用它…...

Java-41 深入浅出 Spring - 声明式事务的支持 事务配置 XML模式 XML+注解模式

点一下关注吧&#xff01;&#xff01;&#xff01;非常感谢&#xff01;&#xff01;持续更新&#xff01;&#xff01;&#xff01; &#x1f680; AI篇持续更新中&#xff01;&#xff08;长期更新&#xff09; 目前2025年06月05日更新到&#xff1a; AI炼丹日志-28 - Aud…...

Matlab | matlab常用命令总结

常用命令 一、 基础操作与环境二、 矩阵与数组操作(核心)三、 绘图与可视化四、 编程与控制流五、 符号计算 (Symbolic Math Toolbox)六、 文件与数据 I/O七、 常用函数类别重要提示这是一份 MATLAB 常用命令和功能的总结,涵盖了基础操作、矩阵运算、绘图、编程和文件处理等…...

浪潮交换机配置track检测实现高速公路收费网络主备切换NQA

浪潮交换机track配置 项目背景高速网络拓扑网络情况分析通信线路收费网络路由 收费汇聚交换机相应配置收费汇聚track配置 项目背景 在实施省内一条高速公路时遇到的需求&#xff0c;本次涉及的主要是收费汇聚交换机的配置&#xff0c;浪潮网络设备在高速项目很少&#xff0c;通…...

前端中slice和splic的区别

1. slice slice 用于从数组中提取一部分元素&#xff0c;返回一个新的数组。 特点&#xff1a; 不修改原数组&#xff1a;slice 不会改变原数组&#xff0c;而是返回一个新的数组。提取数组的部分&#xff1a;slice 会根据指定的开始索引和结束索引提取数组的一部分。不包含…...

WPF八大法则:告别模态窗口卡顿

⚙️ 核心问题&#xff1a;阻塞式模态窗口的缺陷 原始代码中ShowDialog()会阻塞UI线程&#xff0c;导致后续逻辑无法执行&#xff1a; var result modalWindow.ShowDialog(); // 线程阻塞 ProcessResult(result); // 必须等待窗口关闭根本问题&#xff1a…...

如何配置一个sql server使得其它用户可以通过excel odbc获取数据

要让其他用户通过 Excel 使用 ODBC 连接到 SQL Server 获取数据&#xff0c;你需要完成以下配置步骤&#xff1a; ✅ 一、在 SQL Server 端配置&#xff08;服务器设置&#xff09; 1. 启用 TCP/IP 协议 打开 “SQL Server 配置管理器”。导航到&#xff1a;SQL Server 网络配…...