基于OpenResty的弹性网关实践(二)
五、11个指令介绍
OpenResty 有 11 个 *_by_lua指令,它们和 NGINX 阶段的关系如下图所示
其中, init_by_lua 只会在 Master 进程被创建时执行,init_worker_by_lua 只会在每个 Worker 进程被创建时执行。其他的 *_by_lua 指令则是由终端请求触发,会被反复执行。
所以在 init_by_lua 阶段,我们可以预先加载 Lua 模块和公共的只读数据,这样可以利用操作系统的 COW(copy on write)特性,来节省一些内存。
对于业务代码来说,其实大部分的操作都可以在 content_by_lua 里面完成,但我更推荐的做法,是根据不同的功能来进行拆分,比如下面这样:
set_by_lua*
: 流程分支处理判断变量初始化rewrite_by_lua*
: 转发、重定向、缓存等功能(例如特定请求代理到外网)access_by_lua*
: IP 准入、接口权限等情况集中处理(例如配合 iptable 完成简单防火墙)content_by_lua*
: 内容生成header_filter_by_lua*
: 响应头部过滤处理(例如添加头部信息)body_filter_by_lua*
: 响应体过滤处理(例如完成应答内容统一成大写)log_by_lua*
: 会话完成后本地异步完成日志记录(日志可以记录在本地,还可以同步到其他机器)
我们假设,你对外提供了很多明文 API,现在需要增加自定义的加密和解密逻辑。那么请问,你需要修改所有 API 的代码吗?
# 明文协议版本
location /mixed {content_by_lua '...'; # 处理请求
}
当然不用。事实上,利用阶段的特性,我们只需要简单地在 access 阶段解密,在 body filter 阶段加密就可以了,原来 content 阶段的代码是不用做任何修改的:
# 加密协议版本
location /mixed {access_by_lua '...'; # 请求体解密content_by_lua '...'; # 处理请求,不需要关心通信协议body_filter_by_lua '...'; # 应答体加密
}
真实代码示例:
下面是一个真实项目中权限校验的某个Lua脚本
nginx.conf
location ~ /paopao/(game/callback/recharge) {access_by_lua_file lua/util/commonVerifyNotUid.lua;proxy_pass http://paopao;proxy_redirect off;proxy_set_header Host $host;proxy_set_header X-Real-IP $remote_addr;proxy_set_header Remote-Addr $remote_addr;proxy_set_header X-Forwarded-For $http_x_forwarded_for;
}
upstream paopao{server xxx.xxx.xxx.xxx:port max_fails=1 fail_timeout=8s weight=5;
}
commonVerifyNotUid.lua
module("util.commonVerify", package.seeall)
local cjson = require"cjson"
local config = require"config"
local securityver = require"util.securityver"
local args = ngx.req.get_uri_args()
local headers = ngx.req.get_headers()
--安全验证 必须参数
local requestId = headers.requestId --流水号
local appid = headers.appId -- appid
local c = headers.c -- 平台
local vn = headers.vn -- 版本
local ua = headers.ua --ua
local sign = headers.sign -- 签名
local u = headers.u -- 渠道号
local subAppId = headers.subAppId -- 子AppId
ngx.req.read_body()
local postargs = ngx.req.get_post_args()
if not u or u == true or u == "" thenngx.log(ngx.ERR, "u header is nil")ngx.print(cjson.encode({ result = 1 , msg ='gateway parameter absent: u'}))ngx.exit(200)return
end
if not requestId or requestId == true or requestId == "" thenngx.log(ngx.ERR, "requestId header is nil")ngx.print(cjson.encode({ result = 1 , msg ='gateway parameter absent: requestId'}))ngx.exit(200)return
end
if not appid or appid == true or appid == "" then-- 同上
end
if not c or c == true or c == "" then-- 同上
end
if not vn or vn == true or vn == "" then-- 同上
end
if not ua or ua == true or ua == "" then-- 同上
end
if not sign or sign == true or sign == "" then-- 同上
end
-- 黑名单imei
local f = io.open("/data/paopao/paopao_gw/lua/black_list", "r")
local black_list = f:read("*all")
f:close()
--ngx.log(ngx.INFO, black_list)
black_list = loadstring("return " .. black_list)()
function getimei(s)for k,v in string.gmatch(s, "(%w+)=(%w+)") doif(k == 'imei')thenreturn vendend
end
function ifblock(imei)for k, v in ipairs(black_list) doif v == imei thenreturn trueendendreturn false
end
imei_str = getimei(ua)
if ifblock(imei_str) thenngx.print(cjson.encode({ result = 1, msg = 'black list'}))ngx.log(ngx.INFO, "黑名单")ngx.exit(200)return
end
if ngx.var.args thenngx.var.args = ngx.var.args .. '&pv=' .. config.PV[c] .. '&v=' .. vn .. '&appId=' .. appid .. '&u=' .. u
elsengx.var.args = 'pv=' .. config.PV[c] .. '&v=' .. vn .. '&appId=' .. appid .. '&u=' .. u
end
if subAppId ~= nil thenngx.var.args = ngx.var.args .. '&subAppId=' .. subAppId
end
六、API介绍
OpenResty 是基于 NGINX 的 Web 服务器,但它与 NGINX 却有本质的不同:NGINX 由静态的配置文件驱动,而 OpenResty 是由 Lua API 驱动的,所以能提供更多的灵活性和可编程性。
OpenResty 的 API 主要分为下面几个大类:
处理请求和响应;
SSL 相关;
shared dict;
cosocket;
处理四层流量;
process 和 worker;
获取 NGINX 变量和配置;
字符串、时间、编解码等通用功能。
penResty 的 API 不仅仅存在于 lua-nginx-module 项目中,也存在于 lua-resty-core 项目中,比如 ngx.ssl、ngx.base64、ngx.errlog、ngx.process、ngx.re.split、ngx.resp.add_header、ngx.balancer、ngx.semaphore、ngx.ocsp 这些 API 。
而对于不在 lua-nginx-module 项目中的 API,你需要单独 require 才能使用。举个例子,比如你想使用 split 这个字符串分割函数,就需要按照下面的方法来调用:
$ resty -e 'local ngx_re = require "ngx.re"local res, err = ngx_re.split("a,b,c,d", ",", nil, {pos = 5})print(res)'
下面我们介绍几个常用的API:
1.获取uri参数
获取一个 uri 有两个方法:ngx.req.get_uri_args
、ngx.req.get_post_args
,二者主要的区别是参数来源有区别。
参考下面例子:
server {listen 80;server_name localhost;
location /print_param {content_by_lua_block {local arg = ngx.req.get_uri_args()for k,v in pairs(arg) dongx.say("[GET ] key:", k, " v:", v)end
ngx.req.read_body() -- 解析 body 参数之前一定要先读取 bodylocal arg = ngx.req.get_post_args()for k,v in pairs(arg) dongx.say("[POST] key:", k, " v:", v)end}}
}
输出结果:
➜ ~ curl '127.0.0.1/print_param?a=1&b=2' -d 'c=3&d=4'
[GET ] key:b v:2
[GET ] key:a v:1
[POST] key:d v:4
[POST] key:c v:3
从以上输入结果可以看出:前者来自 uri 请求参数,而后者来自 post 请求内容。
我们拿真实的案例来分析一下:
nginx文件中需要拦截的路径加入下面代码
access_by_lua_file lua/util/commonVerify.lua;
以下为Lua文件中部分代码
lua会获取到get和post的请求参数,然后会对参数加密得到一个sign,同时对客户端传递过来的sign进行比对,如果失败,则返回错误提示。
commonVerify.lua
local securityver = require"util.securityver"
local args = ngx.req.get_uri_args()
local postargs = ngx.req.get_post_args()
--安全验证
local status1, res = pcall(securityver.Verify, args, postargs)
if not status1 thenngx.log(ngx.ERR, "error in function securityver.Verify. status is nil")ngx.print(cjson.encode({ result = 3 , msg ='gateway verify: inner exception' }))ngx.exit(200)return
end
seurityver.lua
module("util.securityver", package.seeall)
package.path = package.path .. ';/data/uxin/http_gw_v3/lua/protobuf/?.lua'
package.cpath = package.cpath .. ';/data/uxin/http_gw_v3/lua/protobuf/?.so'
require 'ssid_pb'
local config = require"config"
local string = require("string")
local commonutil = require"util.commonutil"
local map = { d = 0, e = 1, y = 2, b = 3, i = 4, p = 5, v = 6, k = 7, z = 8, o = 9 }
local function sha1(src)local resty_sha1 = require"resty.sha1"local sha1 = resty_sha1:new()if not sha1 thenreturn ""end
local ok = sha1:update(src)if not ok thenreturn ""end
local digest = sha1:final() -- binary digest
local str = require"resty.string"return str.to_hex(digest)
end
local function VerifySign(args,postargs)local appid = ngx.req.get_headers().appidlocal ua = ngx.req.get_headers().ualocal requestId = ngx.req.get_headers().requestIdlocal vn = ngx.req.get_headers().vnlocal paramsTable = {}local sign = ngx.req.get_headers().signlocal signSrc = ""
ngx.log(ngx.ERR, "----vn :" .. vn)if vn >= "1.0.0" then--table.merge(args, postargs)for k,v in pairs(postargs) dongx.log(ngx.ERR, "args k" .. k) ngx.log(ngx.ERR, "args type: " .. type(v)) if type(v) ~= 'table' thenngx.log(ngx.ERR, "args v" .. v) args[k] = vendendendngx.log(ngx.ERR, "----vn :" .. vn)if args thenfor k, v in pairs(args) doif k ~= "sign" and v ~= true and v ~= "" thenngx.log(ngx.ERR, "args k" .. k) table.insert(paramsTable, k)endendtable.sort(paramsTable, function(a, b)return string.lower(a) < string.lower(b)end)
for i = 1, #(paramsTable) dongx.log(ngx.ERR, "arg:" .. paramsTable[i] .."--- " .. args[paramsTable[i]])signSrc = signSrc .. args[paramsTable[i]]endend
--local SignKey = config.SIGNKEYngx.log(ngx.INFO, "----------")ngx.log(ngx.INFO, config.SIGN_KEY[appid])ngx.log(ngx.INFO, "----------")local SignKey = config.SIGN_KEY[appid]local signStr = signSrc .. requestId .. ua .. appid .. SignKeylocal sing1 = sha1(signStr)--ngx.log(ngx.ERR, "----bad sign, signStr:" .. signStr)--ngx.log(ngx.ERR, "----bad sign, source sign:" .. sign)--ngx.log(ngx.ERR, "----bad sign, native sign:" .. sing1)if sing1 ~= sign thenngx.log(ngx.ERR, "bad sign, signStr:" .. signStr)ngx.log(ngx.ERR, "bad sign, source sign:" .. sign)ngx.log(ngx.ERR, "bad sign, native sign:" .. sing1)return falseendreturn true
end
-- 防止用户重复请求的方法,客户端会传一个requestId,为随机生成的字符串
local function VerifyRequestId(args)--make sure account+sn is different in one hour, then sign is differentlocal uid = args.uidif not uid thenargs = ngx.req.get_uri_args()uid = args.uidendlocal snSet = ngx.shared.SnSetlocal sn = ngx.req.get_headers().requestId
if uid thensn = uid .. snend--ngx.log(ngx.ERR, "duplicate requestId" .. sn)local success, err, forcible = snSet:add(sn, 1, 5 * 60)if not success and err == "exists" thenngx.log(ngx.ERR, "duplicate requestId" .. sn)return false-- return trueendreturn true
end
local function VerifyParam(args)return true
end
function Verify(args,postargs)-- 注销--return trueif not VerifyParam(args) thenngx.log(ngx.ERR, "VerifyParam is error")return falseendif not VerifyRequestId(args) thenngx.log(ngx.ERR, "verify requestId is error")return falseend
return VerifySign(args,postargs)
end
config.lua
我们将常量单独存放到一个lua类中保管
module("config", package.seeall)
SIGNKEY = "xxxxxxxxxxxxxxxxxxxxxxx"
2.获取请求body
在 Nginx 的典型应用场景中,几乎都是只读取 HTTP 头即可,例如负载均衡、正反向代理等场景。但是对于 API Server 或者 Web Application ,对 body 可以说就比较敏感了。由于 OpenResty 基于 Nginx ,所以天然的对请求 body 的读取细节与其他成熟 Web 框架有些不同。
我们先来构造最简单的一个请求,POST 一个名字给服务端,服务端应答一个 “Hello xxx”。
http {server {listen 80;
location /test {content_by_lua_block {local data = ngx.req.get_body_data()ngx.say("hello ", data)}}}
}
测试结果:
➜ ~ curl 127.0.0.1/test -d jack
hello nil
大家可以看到 data 部分获取为空,如果你熟悉其他 web 开发框架,估计立刻就觉得 OpenResty 弱爆了。查阅一下官方 wiki 我们很快知道,原来我们还需要添加指令 lua_need_request_body 。究其原因,主要是 Nginx 诞生之初主要是为了解决负载均衡情况,而这种情况,是不需要读取 body 就可以决定负载策略的,所以这个点对于 API Server 和 Web Application 开发的同学有点怪。
参看下面例子:
http {server {listen 80;
# 默认读取 bodylua_need_request_body on;
location /test {content_by_lua_block {local data = ngx.req.get_body_data()ngx.say("hello ", data)}}}
}
再次测试,符合我们预期:
➜ ~ curl 127.0.0.1/test -d jack
hello jack
3.日志输出
OpenResty 的标准日志输出原句为 ngx.log(log_level, ...)
,几乎可以在任何 ngx_lua 阶段进行日志的输出。
我们用泡泡真实项目中的日志模拟一下sign签名校验错误的日志输入
nginx中的日志配置如下:
log_format main '$remote_addr - $remote_user [$time_local] "$request" ''$status $body_bytes_sent "$http_referer" ''"$http_user_agent" "-"' '"upstream_addr:' '$upstream_addr' '-response_time:' '$upstream_response_time"';
server{access_log logs/httpserver.access.log main;error_log logs/httpserver.error.log error;
}
httpserver.error.log日志如下:
2020/09/24 15:09:19 [error] 29978#0: *793273 [lua] securityver.lua:95: bad sign, signStr:50492122955321ssadaadf1200rko753*qpsd5vbalt#$%^19plmo!@&kn, client: 114.249.22.19, server: localhost, request: "GET /v1/paopao/game?gameId=1 HTTP/1.1", host: "xxx.xxx.xxx.xxx:12345"
2020/09/24 15:09:19 [error] 29978#0: *793273 [lua] securityver.lua:96: bad sign, source sign:asadfdfa3124321asd, client: 114.249.22.19, server: localhost, request: "GET /v1/paopao/game?gameId=1 HTTP/1.1", host: "xxx.xxx.xxx.xxx:12345"
2020/09/24 15:09:19 [error] 29978#0: *793273 [lua] securityver.lua:97: bad sign, native sign:7b5a29291b78e9d7432e0a2bca39b94a1c8857e1, client: 114.249.22.19, server: localhost, request: "GET /v1/paopao/game?gameId=1 HTTP/1.1", host: "xxx.xxx.xxx.xxx:12345"
2020/09/24 15:09:19 [error] 29978#0: *793273 [lua] commonVerify.lua:103: error in function securityver.Verify. res is nil, client: 114.249.22.19, server: localhost, request: "GET /v1/paopao/game?gUid=122955&gameId=50492 HTTP/1.1", host: "xxx.xxx.xxx.xxx:12345"
4.发起http请求
OpenResty 最主要的应用场景之一是 API Server,有别于传统 Nginx 的代理转发应用场景,API Server 中心内部有各种复杂的交易流程和判断逻辑。
① 引用 resty.http
库资源,它来自 github https://github.com/pintsized/lua-resty-http。
② 参考 resty-http
官方 wiki 说明,我们可以知道 request_uri 函数完成了连接池、HTTP 请求等一系列动作。
下面的代码是我们利用http库自定义的一个简单的发起http请求的方法:
module("util.httplib", package.seeall)
local os = require("os")
function geturl(purl)local http = require"resty.http"local hc = http:new()local startTime = os.time()local ok, code, headers, status, body = hc:request{url = purl,timeout = 4000,method = "GET"}if not ok thenngx.log(ngx.ERR, "WARN: " .. purl .. " code: " .. (code or "nil") .. " status: " .. (status or "nil"))endif (os.time() - startTime) >= 4 thenngx.log(ngx.ERR, "WARN: call remote server timeout. the url: " .. purl)endreturn ok, code, headers, status, body
end
function posturl(purl, pbody)local http = require"resty.http"local hc = http:new()local startTime = os.time()local ok, code, headers, status, body = hc:request{url = purl,timeout = 4000,method = "POST",body = pbody,-- add post content-type and cookieheaders = { ["Content-Type"] = "application/x-www-form-urlencoded" },}if not ok thenngx.log(ngx.ERR, "WARN: " .. purl .. " pbody: " .. pbody .. " code: " .. (code or "nil") .. " status: " .. (status or "nil"))endif (os.time() - startTime) >= 4 thenngx.log(ngx.ERR, "WARN: call remote server timeout. the url: " .. purl .. pbody)endreturn ok, code, headers, status, body
end
七、OpenResty缓存
ngx.shared.DICT
我们在nginx.conf中配置
lua_shared_dict SnSet 300m;
lua_shared_dict Threshold 300m;
我们可以用 shared dict 来共享数据,这些数据可以在多个 worker 之间共享。内部使用的 LRU 算法(最近最少使用)来判断缓存是否在内存占满时被清除。
在上面的多处代码中我们都使用了配置文件所定义好的全局变量。
它对外提供了 20 多个 Lua API,不过所有的这些 API 都是原子操作,你不用担心多个 worker 和高并发的情况下的竞争问题。
这些 API 都有官方详细的文档,使用的时候可以查阅:shared_dict
继续看 shared dict 的 API,这些 API 可以分为下面三个大类,也就是字典读写类、队列操作类和管理类这三种。
1.字典读写类
首先来看字典读写类。在最初的版本中,只有字典读写类的 API,它们也是共享字典最常用的功能。下面是一个最简单的示例:
$ resty --shdict='dogs 1m' -e 'local dict = ngx.shared.dogsdict:set("Tom", 56)print(dict:get("Tom"))'
除了 set 外,OpenResty 还提供了 safe_set、add、safe_add、replace 这四种写入的方法。这里safe 前缀的含义是,在内存占满的情况下,不根据 LRU 淘汰旧的数据,而是写入失败并返回 no memory 的错误信息。
除了 get 外,OpenResty 还提供了 get_stale 的读取数据的方法,相比 get 方法,它多了一个过期数据的返回值:
value, flags, stale = ngx.shared.DICT:get_stale(key)
还可以调用 delete 方法来删除指定的 key,它和 set(key, nil) 是等价的。
2.管理类
用户申请了 100M 的空间作为 shared dict,那么这 100M 是否够用呢?里面存放了多少 key?具体是哪些 key 呢?
首先是 get_keys(max_count?),它默认也只返回前 1024 个 key;如果你把 max_count 设置为 0,那就返回所有 key。
然后是 capacity 和 free_space,这两个 API 都属于 lua-resty-core 仓库,所以需要你 require 后才能使用:
require "resty.core.shdict"
local cats = ngx.shared.catslocal capacity_bytes = cats:capacity()local free_page_bytes = cats:free_space()
它们分别返回的,是共享内存的大小(也就是 lua_shared_dict 中配置的大小)和空闲页的字节数。因为 shared dict 是按照页来分配的,即使 free_space 返回为 0,在已经分配的页面中也可能存在空间,所以它的返回值并不能代表共享内存实际被占用的情况。
3.队列操作类
lpush/rpush,表示在队列两端增加元素;
lpop/rpop,表示在队列两端弹出元素;
llen,表示返回队列的元素数量。
下面是代码示例:
=== TEST 1: lpush & lpop
--- http_configlua_shared_dict dogs 1m;
--- configlocation = /test {content_by_lua_block {local dogs = ngx.shared.dogs
local len, err = dogs:lpush("foo", "bar")if len thenngx.say("push success")elsengx.say("push err: ", err)end
local val, err = dogs:llen("foo")ngx.say(val, " ", err)
local val, err = dogs:lpop("foo")ngx.say(val, " ", err)
local val, err = dogs:llen("foo")ngx.say(val, " ", err)
local val, err = dogs:lpop("foo")ngx.say(val, " ", err)}}
--- request
GET /test
--- response_body
push success
1 nil
bar nil
0 nil
nil nil
--- no_error_log
[error]
八、典型的应用场景
在 Lua 中混合处理不同 Nginx 模块输出(proxy, drizzle, postgres, Redis, memcached 等)。
在请求真正到达上游服务之前,Lua 中处理复杂的准入控制和安全检查。
比较随意的控制应答头(通过 Lua)。
从外部存储中获取后端信息,并用这些信息来实时选择哪一个后端来完成业务访问。
在内容 handler 中随意编写复杂的 web 应用,同步编写异步访问后端数据库和其他存储。
在 rewrite 阶段,通过 Lua 完成非常复杂的处理。
在 Nginx 子查询、location 调用中,通过 Lua 实现高级缓存机制。
对外暴露强劲的 Lua 语言,允许使用各种 Nginx 模块,自由拼合没有任何限制。该模块的脚本有充分的灵活性,同时提供的性能水平与本地 C 语言程序无论是在 CPU 时间方面以及内存占用差距非常小。所有这些都要求 LuaJIT 2.x 是启用的。其他脚本语言实现通常很难满足这一性能水平。
基于OpenResty的弹性网关实践(二)相关推荐
- 基于OpenResty的弹性网关实践(一)
一.介绍 1. OpenResty简介 官方地址:http://openresty.org/cn/ github地址:https://github.com/openresty/ OpenResty最佳 ...
- 网易数帆基于 Envoy 的云原生网关实践
本文根据 InfoQ 公开课<如何基于开源Envoy,构建高性能云原生微服务网关>整理,有删减. 简介:Envoy 是由 Lyft 开源的高性能网络代理软件.相比于 Nginx.HAPro ...
- 网易基于 Envoy 的云原生网关实践
简介:Envoy 是由 Lyft 开源的高性能网络代理软件.相比于 Nginx.HAProxy 等经典代理软件,Envoy 具备丰富的可观察性和灵活的可扩展性,并且引入了基于 xDS API 的动态配 ...
- 基于华为云弹性云服务器ECS(搭载openEuler的鲲鹏通用计算增强型)完成鲲鹏代码迁移工具实践【华为云至简致远】
[摘要] 基于华为云弹性云服务器ESC(鲲鹏服务器),部署鲲鹏代码迁移工具利用扫描迁移工具进行源码分析,根据扫描建议修改源码,让源码在鲲鹏平台可以正常编译运行 零.前情提要 先来说句题外话,最近华为鲲 ...
- 云原生架构下的 API 网关实践: Kong (二)
Kong 是 Mashape 开源的一款云原生架构下的分布式 API 网关,其性能和可扩展性在同类组件中,表现都很优异.Kong 官方提供了很多直接可用的插件,此外,Kong 还可以通过插件扩展已有功 ...
- 日志平台(网关层) - 基于Openresty+ELKF+Kafka
背景介绍 1.问题现状与尝试 没有做日志记录的线上系统,绝对是给系统运维人员留下的坑.尤其是前后端分离的项目,后端的接口日志可以解决对接.测试和运维时的很多问题.之前项目上发布的接口都是通过Oracl ...
- Amazon EKS基于GitLab的CICD实践二 基础架构和应用架构创建篇
关于GitLab的CI/CD的实践具体分成如下的内容,其中(一)和(二)已经在上面一篇关于GitLab的CICD的实践一 GitLab的部署和配置篇中介绍完成了. 全系列目录: (一)部署的架构 (二 ...
- 基于Domoticz智能家居系统(十六)DIY一款基于MySensors的ESP8266+NRF24L01的MQTT(WIFI)和RF无线网关(二)正式DIY
DIY一款基于MySensors的ESP8266+NRF24L01的MQTT(WIFI)和RF无线网关(二)正式DIY 正式DIY 一.本文参考的国外DIY项目 二.本文采用的模块和连接线路 1.ES ...
- 比心云平台基于阿里云容器服务 ACK 的弹性架构实践
作者:韩韬|比心技术 前言 应用容器化改造后,不可避免地会面临这样一个问题:Kubernetes 集群的 Node 资源配置不足会导致 Pod 无法及时运行,购买过多的 Node 又会导致资源的闲置浪 ...
最新文章
- TPU 3.0,Android P...Google带来了哪些惊喜?
- Eclipse下svn的创建分支/合并/切换使用
- 树形选择排序的基本概念
- 二阶系统阶跃响应实验_自控原理二阶系统阶跃响应及性能分析实验报告
- python loop call soon_python3-asyncio 学习笔记 1 -- call_soon
- 牛客题霸 NC11 将升序数组转化为平衡二叉搜索树
- 阿里云Lindorm联合智臾科技发布,金融高频交易数据量化分析与处理方案
- 移动云亮相 2021 IDC 年度盛典 共话变革与赋能
- mysql数据库rp集群_MySQL集群入门(PXC)
- 安卓学习笔记35:广播接收者
- java final 内存语义_final 域的内存语义
- Excel文件下载From Linux
- 浙大 计算机 毕业论文格式,浙大硕士毕业论文格式
- eterm 350转443转接器
- 神通数据库常见问题解决方案
- 扬州大学matlab课程设计报告,自动控制原理课程设计报告
- 非线性发展方程定解问题
- 对冲基金小镇 鬼城_未来系统,代码寿命和网络鬼城
- python数据分析(六)—数据清洗2
- 二十岁决定男人的一生
热门文章
- 进程间通信(2) 内存映射FileMap
- UnboundLocalError: local variable ‘XXX‘ referenced before assignment解决办法
- Linux常用的基本命令head、tail、tar、grep、date、cal(二)
- [How TO]-How to install maven
- Android keymaster的介绍和总结
- docker搭建pwn环境
- http://blog.sina.com.cn/s/blog_458f3c010100n4st.html
- CreateFileMapping 内存映射读写文件
- 【模拟】P1563 玩具谜题
- IgniteMe debug 寒假逆向生涯(2/100)