五、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_argsngx.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的弹性网关实践(二)相关推荐

  1. 基于OpenResty的弹性网关实践(一)

    一.介绍 1. OpenResty简介 官方地址:http://openresty.org/cn/ github地址:https://github.com/openresty/ OpenResty最佳 ...

  2. 网易数帆基于 Envoy 的云原生网关实践

    本文根据 InfoQ 公开课<如何基于开源Envoy,构建高性能云原生微服务网关>整理,有删减. 简介:Envoy 是由 Lyft 开源的高性能网络代理软件.相比于 Nginx.HAPro ...

  3. 网易基于 Envoy 的云原生网关实践

    简介:Envoy 是由 Lyft 开源的高性能网络代理软件.相比于 Nginx.HAProxy 等经典代理软件,Envoy 具备丰富的可观察性和灵活的可扩展性,并且引入了基于 xDS API 的动态配 ...

  4. 基于华为云弹性云服务器ECS(搭载openEuler的鲲鹏通用计算增强型)完成鲲鹏代码迁移工具实践【华为云至简致远】

    [摘要] 基于华为云弹性云服务器ESC(鲲鹏服务器),部署鲲鹏代码迁移工具利用扫描迁移工具进行源码分析,根据扫描建议修改源码,让源码在鲲鹏平台可以正常编译运行 零.前情提要 先来说句题外话,最近华为鲲 ...

  5. 云原生架构下的 API 网关实践: Kong (二)

    Kong 是 Mashape 开源的一款云原生架构下的分布式 API 网关,其性能和可扩展性在同类组件中,表现都很优异.Kong 官方提供了很多直接可用的插件,此外,Kong 还可以通过插件扩展已有功 ...

  6. 日志平台(网关层) - 基于Openresty+ELKF+Kafka

    背景介绍 1.问题现状与尝试 没有做日志记录的线上系统,绝对是给系统运维人员留下的坑.尤其是前后端分离的项目,后端的接口日志可以解决对接.测试和运维时的很多问题.之前项目上发布的接口都是通过Oracl ...

  7. Amazon EKS基于GitLab的CICD实践二 基础架构和应用架构创建篇

    关于GitLab的CI/CD的实践具体分成如下的内容,其中(一)和(二)已经在上面一篇关于GitLab的CICD的实践一 GitLab的部署和配置篇中介绍完成了. 全系列目录: (一)部署的架构 (二 ...

  8. 基于Domoticz智能家居系统(十六)DIY一款基于MySensors的ESP8266+NRF24L01的MQTT(WIFI)和RF无线网关(二)正式DIY

    DIY一款基于MySensors的ESP8266+NRF24L01的MQTT(WIFI)和RF无线网关(二)正式DIY 正式DIY 一.本文参考的国外DIY项目 二.本文采用的模块和连接线路 1.ES ...

  9. 比心云平台基于阿里云容器服务 ACK 的弹性架构实践

    作者:韩韬|比心技术 前言 应用容器化改造后,不可避免地会面临这样一个问题:Kubernetes 集群的 Node 资源配置不足会导致 Pod 无法及时运行,购买过多的 Node 又会导致资源的闲置浪 ...

最新文章

  1. TPU 3.0,Android P...Google带来了哪些惊喜?
  2. Eclipse下svn的创建分支/合并/切换使用
  3. 树形选择排序的基本概念
  4. 二阶系统阶跃响应实验_自控原理二阶系统阶跃响应及性能分析实验报告
  5. python loop call soon_python3-asyncio 学习笔记 1 -- call_soon
  6. 牛客题霸 NC11 将升序数组转化为平衡二叉搜索树
  7. 阿里云Lindorm联合智臾科技发布,金融高频交易数据量化分析与处理方案
  8. 移动云亮相 2021 IDC 年度盛典 共话变革与赋能
  9. mysql数据库rp集群_MySQL集群入门(PXC)
  10. 安卓学习笔记35:广播接收者
  11. java final 内存语义_final 域的内存语义
  12. Excel文件下载From Linux
  13. 浙大 计算机 毕业论文格式,浙大硕士毕业论文格式
  14. eterm 350转443转接器
  15. 神通数据库常见问题解决方案
  16. 扬州大学matlab课程设计报告,自动控制原理课程设计报告
  17. 非线性发展方程定解问题
  18. 对冲基金小镇 鬼城_未来系统,代码寿命和网络鬼城
  19. python数据分析(六)—数据清洗2
  20. 二十岁决定男人的一生

热门文章

  1. 进程间通信(2) 内存映射FileMap
  2. UnboundLocalError: local variable ‘XXX‘ referenced before assignment解决办法
  3. Linux常用的基本命令head、tail、tar、grep、date、cal(二)
  4. [How TO]-How to install maven
  5. Android keymaster的介绍和总结
  6. docker搭建pwn环境
  7. http://blog.sina.com.cn/s/blog_458f3c010100n4st.html
  8. CreateFileMapping 内存映射读写文件
  9. 【模拟】P1563 玩具谜题
  10. IgniteMe debug 寒假逆向生涯(2/100)