14 登录服务

​ 现在的网络游戏大部分是需要登录的,一般会有一个专门的登录服务来处理,登录服务要解决的问题:1、用户登录信息保密工作。2、实际登录点分配工作。

14.1 加密算法

14.1.1 DHexchange密钥交换算法

​ DHexchange密钥交换算法主要用来协商一个服务器与客户端的密钥。云风已经帮我们封装好了这个加密方法,可以直接这么使用:

package.cpath = "luaclib/?.so"
local crypt = require "client.crypt"
--如果在skynet中使用直接 local crypt = require "skynet.crypt"
​
--dhexchange转换8字节的key
crypt.dhexchange(key)
​
--通过key1与key2得到密钥
crypt.dhsecret(key1, key2)

示例代码testdhexchange.lua:

package.cpath = "luaclib/?.so"
local crypt = require "client.crypt"
​
local clientkey = "11111111" --8byte random
print("clientkey:" , clientkey)
local ckey = crypt.dhexchange(clientkey)
print("ckey:\t" , crypt.hexencode(ckey)
​
local serverkey = "22222222"
print("serverkey:" , serverkey)
​
local skey = crypt.dhexchange(serverkey)
print("skey:\t" , crypt.hexencode(skey))
​
local csecret = crypt.dhsecret(skey, clientkey)
print("use skey clientkey dhsecret:", crypt.hexencode(csecret)) --交换成功
​
local ssecret = crypt.dhsecret(ckey, serverkey)
print("use ckey serverkey dhsecret:", crypt.hexencode(ssecret)) --交换成功
​
local ssecret = crypt.dhsecret(ckey, skey)                  --交换失败
print("use ckey skey dhsecret:\t", crypt.hexencode(ssecret))

直接在终端运行结果:

$ ./3rd/lua/lua my_workspace/testdhexchange.lua
clientkey:  11111111
ckey:       D5 8A 46 9C FD ED 70 5E
serverkey:  22222222
skey:       B3 60 21 D9 C4 C5 1B 0C
use skey clientkey dhsecret:    95 69 12 B6 88 B3 3B 42 #交换成功
use ckey serverkey dhsecret:    95 69 12 B6 88 B3 3B 42 #交换成功
use ckey skey dhsecret:     C7 19 E5 5F 0A 34 DC E8     #交换不成功

​ 需要注意的是,这个库是独立的库,不需要在skynet的lua虚拟机里运行,普通虚拟机也运行使用。

14.1.2 随机数

package.cpath = "luaclib/?.so"
local crypt = require "client.crypt"
--如果在skynet中使用直接 local crypt = require "skynet.crypt"
​
--产生一个8字节的随机数,一般作为对称加密算法的随机密钥
crypt.randomkey()

14.1.3 hmac64哈希算法

​ hmac64算法主要用于密钥验证。

package.cpath = "luaclib/?.so"
local crypt = require "client.crypt"
--如果在skynet中使用直接 local crypt = require "skynet.crypt"
​
--HMAC64运算利用哈希算法,以一个密钥secret和一个消息challenge为输入,生成一个消息摘要hmac作为输出。
local hmac = crypt.hmac64(challenge, secret)

14.1.4 base64编解码

​ Base64就是一种基于64个可打印字符来表示二进制数据的方法。

package.cpath = "luaclib/?.so"
local crypt = require "client.crypt"
--如果在skynet中使用直接 local crypt = require "skynet.crypt"
​
--编码
crypt.base64encode(str)
​
--解码
crypt.base64decode(str)

14.1.5 DES加解密

package.cpath = "luaclib/?.so"
local crypt = require "client.crypt"
--如果在skynet中使用直接 local crypt = require "skynet.crypt"
​
--用key加密plaintext得到密文,key必须是8字节
crypt.desencode(key, plaintext)
--用key解密ciphertext得到明文,key必须是8字节
crypt.desdecode(key, ciphertext)

14.1.6 hashkey算法

​ 云风自实现的hash算法,只能哈希小于8字节的数据,返回8字节数据的hash

package.cpath = "luaclib/?.so"
local crypt = require "client.crypt"
--如果在skynet中使用直接 local crypt = require "skynet.crypt"
​
--云风自实现的hash算法,只能哈希小于8字节的数据,返回8字节数据的hash
crypt.hashkey(str)

14.2 loginserver原理

​ skynet 提供了一个通用的登陆服务器模版 snax.loginserver 。框架原理如下图:

​ login服务开启监听,客户端主动去连接login服务,他们之间的通信协议是行结尾协议(即:每个数据包都是一行ascii字符,如果要发送byte字节流,则通过base64编码)。这假如称login服务为L,客户端为C。

​ (1)L产生随机数challenge,并发送给C,主要用于最后验证密钥secret是否交换成功。

​ (2)C产生随机数clientkey,clientkey是保密的,只有C知道,并通过dhexchange算法换算clientkey,得到ckey。 把base64编码的ckey发送给L。

​ (3)L也产生随机数serverkey,serverkey是保密的,只有L知道,并通过dhexchange算法换算serverkey,得到skey。把base64编码的skey发送给C。

​ (4)C使用clientkey与skey,通过dhsecret算法得到最终安全密钥secret。

​ (5)L使用serverKey与ckey, 通过dhsecret算法得到最终安全密钥secret。C 和 L最终得到的secret是一样的,而传输过程只有ckey skey是通过网络公开的,即使ckey skey泄露了,也无法推算出secret。

​ (6)密钥交换完成后,需要验证一下双方的密钥是否是一致的。C使用密钥secret通过hmac64哈希算法加密第1步中接收到的challenge,得到CHmac,然后转码成base64 CHmac发送给L。

​ (7)L收到CHmac后,自己也使用密钥secret通过hmac64哈希算法加密第1步中发送出去的challenge,得到SHmac,对比SHmac与CHmac是否一致,如果一致,则密钥交换成功。不成功就断开连接。

​ (8)C组合base64 user@base64 server:base64 passwd字符串(server为客户端具体想要登录的登录点,远端服务器可能有多个实际登录点),使用secret通过DES加密,得到etoken,发送base64 etoken。

​ (9)使用secret通过DES解密etoken,得到user@server:passwd,校验user与passwd是否正确,通知实际登录点server,传递user与secret给server,server生成subid返回。发送状态码 200 base64 subid给C。

​ (10)C得到subid后就可以断开login服务的连接,然后去连接实际登录点server了。(实际登录点server,可以由L通知C,也可以C指定想要登录哪个点,将在下一个章提到)

14.3 loginserver 模板

local login = require "snax.loginserver"
local server = {    host = "127.0.0.1",
    port = 8001,
    multilogin = false, -- disallow multilogin
    name = "login_master",
     -- config, etc
}
​
login(server) --服务启动点
  • host 是监听地址,通常是 "0.0.0.0" 。

  • port 是监听端口。

  • name 是一个内部使用的名字,不要和 skynet 其它服务重名。在上面的例子,登陆服务器会注册为 .login_master 这个名字,相当于skynet.register(".login_master")

  • multilogin 是一个 boolean ,默认是 false 。关闭后,当一个用户正在走登陆流程时,禁止同一用户名进行登陆。如果你希望用户可以同时登陆,可以打开这个开关,但需要自己处理好潜在的并行的状态管理问题。

同时,你还需要注册一系列业务相关的必要方法。

--你需要实现这个方法,对一个客户端发送过来的 token 做验证。如果验证不能通过,可以通过 error 抛出异常。如果验证通过,需要返回用户希望进入的登陆点以及用户名。(登陆点可以是包含在 token 内由用户自行决定,也可以在这里实现一个负载均衡器来选择)
function server.auth_handler(token) end
​
--你需要实现这个方法,处理当用户已经验证通过后,该如何通知具体的登陆点(server )。框架会交给你用户名(uid)和已经安全交换到的通讯密钥。你需要把它们交给登陆点,并得到确认(等待登陆点准备好后)才可以返回。
function server.login_handler(server, uid, secret) end
​
--实现command_handler,用来处理lua消息,必须注册
function server.command_handler(command, ...) end

​ 登录服务返回给客户端的状态码:

200 [base64(subid)] --登录成功会返回一个subid,这个subid是这次登录的唯一标识
400 Bad Request --握手失败
401 Unauthorized --自定义的 auth_handler 不认可 token
403 Forbidden --自定义的 login_handler 执行失败
406 Not Acceptable --该用户已经在登陆中。(只发生在 multilogin 关闭时)

14.4 运用loginserver模板

示例代码:myloginserver.lua

local login = require "snax.loginserver"
local crypt = require "skynet.crypt"
local skynet = require "skynet"
​
​
local server = {    host = "127.0.0.1",
    port = 8001,
    multilogin = false, -- disallow multilogin
    name = "login_master",
}
​
function server.auth_handler(token)
    -- the token is base64(user)@base64(server):base64(password)
    --通过正则表达式,解析出各个参数
    local user, server, password = token:match("([^@]+)@([^:]+):(.+)")
    user = crypt.base64decode(user)
    server = crypt.base64decode(server)
    password = crypt.base64decode(password)
    skynet.error(string.format("%s@%s:%s", uid, server, password))
    --密码不对直接报错中断当前协程,千万不要返回nil值,一定要用assert中断或者error报错终止掉当前协程
    assert(password == "password", "Invalid password")
    return server, user
end
​
local subid = 0
function server.login_handler(server, uid, secret)
    skynet.error(string.format("%s@%s is login, secret is %s", uid, server, crypt.hexencode(secret)))
    subid = subid + 1 --分配一个唯一的subid
    return subid
end
​
local CMD = {}
​
​
function CMD.register_gate(server, address)
    skynet.error("cmd register_gate")
end
--实现command_handler,必须要实现,用来处理lua消息
function server.command_handler(command, ...)
    local f = assert(CMD[command])
    return f(...)
end
​
login(server) --服务启动需要参数
​

示例代码:myclient.lua

package.cpath = "luaclib/?.so"
​
local socket = require "client.socket"
local crypt = require "client.crypt"
​
if _VERSION ~= "Lua 5.3" then
    error "Use lua 5.3"
end
​
local fd = assert(socket.connect("127.0.0.1", 8001))
​
local function writeline(fd, text)
    socket.send(fd, text .. "\n")
end
​
local function unpack_line(text)
    local from = text:find("\n", 1, true)
    if from then
        return text:sub(1, from-1), text:sub(from+1)
    end
    return nil, text
end
​
local last = ""
​
local function unpack_f(f)
    local function try_recv(fd, last)
        local result
        result, last = f(last)
        if result then
            return result, last
        end
        local r = socket.recv(fd)
        if not r then
            return nil, last
        end
        if r == "" then
            error "Server closed"
        end
        return f(last .. r)
    end
​
    return function()
        while true do
            local result
            result, last = try_recv(fd, last)
            if result then
                return result
            end
            socket.usleep(100)
        end
    end
end
​
local readline = unpack_f(unpack_line)
​
local challenge = crypt.base64decode(readline()) --接收challenge
​
local clientkey = crypt.randomkey()
--把clientkey换算后比如称它为ckeys,发给服务器
writeline(fd, crypt.base64encode(crypt.dhexchange(clientkey)))
--服务器也把serverkey换算后比如称它为skeys,发给客户端,客户端用clientkey与skeys所出secret
local secret = crypt.dhsecret(crypt.base64decode(readline()), clientkey)
--secret一般是8字节数据流,需要转换成16字节的hex字符串来显示。
print("sceret is ", crypt.hexencode(secret))
--加密的时候还是需要直接传递secret字节流
local hmac = crypt.hmac64(challenge, secret)
writeline(fd, crypt.base64encode(hmac))
​
local token = {    server = "sample",
    user = "hello",
    pass = "password",
}
​
local function encode_token(token)
    return string.format("%s@%s:%s",
        crypt.base64encode(token.user),
        crypt.base64encode(token.server),
        crypt.base64encode(token.pass))
end
--使用DES加密token得到etoken, etoken是字节流
local etoken = crypt.desencode(secret, encode_token(token))
etoken = crypt.base64encode(etoken)
--发送etoken,mylogin.lua将会调用auth_handler回调函数, 以及login_handler回调函数。
writeline(fd, etoken)
​
local result = readline() --读取最终的返回结果。
print(result)
local code = tonumber(string.sub(result, 1, 3))
assert(code == 200)
socket.close(fd)  --可以关闭链接了
​
local subid = crypt.base64decode(string.sub(result, 5)) --解析出subid
​
print("login ok, subid=", subid)

先运行服务器端:

$ ./skynet examples/config
mylogin #终端输入
[:01000010] LAUNCH snlua mylogin  #会发现默认会启动多个服务
[:01000012] LAUNCH snlua mylogin
[:01000019] LAUNCH snlua mylogin
[:0100001a] LAUNCH snlua mylogin
[:0100001b] LAUNCH snlua mylogin
[:0100001c] LAUNCH snlua mylogin
[:0100001d] LAUNCH snlua mylogin
[:0100001e] LAUNCH snlua mylogin
[:0100001f] LAUNCH snlua mylogin

再运行客户端:

$ 3rd/lua/lua my_workspace/myclient.lua
sceret is   57943de9381fce1e
200 MQ==
login ok, subid=    1  #登录成功了

回头再来看看服务器输出:

$ ./skynet examples/config
mylogin #终端输入
[:01000010] LAUNCH snlua mylogin  #会发现默认会启动多个服务
[:01000012] LAUNCH snlua mylogin
[:01000019] LAUNCH snlua mylogin
[:0100001a] LAUNCH snlua mylogin
[:0100001b] LAUNCH snlua mylogin
[:0100001c] LAUNCH snlua mylogin
[:0100001d] LAUNCH snlua mylogin
[:0100001e] LAUNCH snlua mylogin
[:0100001f] LAUNCH snlua mylogin
[:01000010] login server listen at : 127.0.0.1 8001 #第一个启动的服务去监听
[:01000012] connect from 127.0.0.1:47964 (fd = 9)   #有链接来就去处理
[:01000012] hello@sample:password
[:01000010] hello@sample is login, secret is 57943de9381fce1e

14.5 账户核对失败的处理

​ 账户核对失败,无非是账户密码不匹配时,那么这个时候,我们来观察一下返回客户端的状态码。

​ 在14.4的myclient.lua的基础上修改passwd,再次登录,例如:

local token = {    server = "sample",
    user = "hello",
    pass = "wrongpasswd",
}

客户端运行:

$ 3rd/lua/lua my_workspace/myclient.lua
sceret is   66ec31781d628739
401 Unauthorized #密码错误,认证不成功返回错误401
3rd/lua/lua: my_workspace/myclient.lua:88: assertion failed!
stack traceback:
    [C]: in function 'assert'
    my_workspace/myclient.lua:88: in main chunk
    [C]: in ?

服务器端状况:

$ ./skynet examples/config
mylogin
[:01000010] LAUNCH snlua mylogin
[:01000012] LAUNCH snlua mylogin
[:01000019] LAUNCH snlua mylogin
[:0100001a] LAUNCH snlua mylogin
[:0100001b] LAUNCH snlua mylogin
[:0100001c] LAUNCH snlua mylogin
[:0100001d] LAUNCH snlua mylogin
[:0100001e] LAUNCH snlua mylogin
[:0100001f] LAUNCH snlua mylogin
[:01000010] login server listen at : 127.0.0.1 8001
[:01000012] connect from 127.0.0.1:48260 (fd = 9)
[:01000012] hello@sample:passwords
[:01000010] invalid client (fd = 9) error = ./lualib/snax/loginserver.lua:127:  ./my_workspace/mylogin.lua:20: Invalid password    #报错终止掉当前协程。

​ 需要注意,一旦有登录请求进来,在调用回调函数server.auth_handlery以及 server.login_handler都是开启了一个协程来处理,assert与error都能终止掉当前协程,并不是终止掉整个服务。

​ 虽然我们在启动mylogin服务的时候一下启动的了9个服务,但这9个服务中一个是监听使用,其他服务负责与客户端交换密钥以及处理账号验证,八个服务共同分担处理任务,可以通过不断启动客户端来观察。八个服务轮流处理一次登录请求。

​ 例如:运行9次客户端:

$ ./skynet examples/config
mylogin
[:01000010] LAUNCH snlua mylogin
[:01000012] LAUNCH snlua mylogin
[:01000019] LAUNCH snlua mylogin
[:0100001a] LAUNCH snlua mylogin
[:0100001b] LAUNCH snlua mylogin
[:0100001c] LAUNCH snlua mylogin
[:0100001d] LAUNCH snlua mylogin
[:0100001e] LAUNCH snlua mylogin
[:0100001f] LAUNCH snlua mylogin
[:01000010] login server listen at : 127.0.0.1 8001
[:01000012] connect from 127.0.0.1:48268 (fd = 9)
[:01000012] hello@sample:password
[:01000010] hello@sample is login, secret is b7d37b00ed49bf50
[:01000019] connect from 127.0.0.1:48270 (fd = 10)
[:01000019] hello@sample:password
[:01000010] hello@sample is login, secret is 373dc4f4876d7636
[:0100001a] connect from 127.0.0.1:48272 (fd = 11)
[:0100001a] hello@sample:password
[:01000010] hello@sample is login, secret is 9a667284488692b8
[:0100001b] connect from 127.0.0.1:48274 (fd = 12)
[:0100001b] hello@sample:password
[:01000010] hello@sample is login, secret is 19178e716fb886ff
[:0100001c] connect from 127.0.0.1:48276 (fd = 13)
[:0100001c] hello@sample:password
[:01000010] hello@sample is login, secret is 039badb8016f59e6
[:0100001d] connect from 127.0.0.1:48278 (fd = 14)
[:0100001d] hello@sample:password
[:01000010] hello@sample is login, secret is 636e4ed64a797d36
[:0100001e] connect from 127.0.0.1:48280 (fd = 15)
[:0100001e] hello@sample:password
[:01000010] hello@sample is login, secret is e3ad3e8f070fe6c6
[:0100001f] connect from 127.0.0.1:48282 (fd = 16)
[:0100001f] hello@sample:password
[:01000010] hello@sample is login, secret is ee5d95258b470809
[:01000012] connect from 127.0.0.1:48284 (fd = 17)
[:01000012] hello@sample:password
[:01000010] hello@sample is login, secret is bf88c2f81ab14031

上面可以看到服务轮流着去处理请求。

14.6 login_handler错误处理

修改14.4中的mylogin.lua的skynet.login_handler函数:

function server.login_handler(server, uid, secret)
    skynet.error(string.format("%s@%s is login, secret is %s", uid, server, crypt.hexencode(secret)))
    error("login_handler") --加入这一行,终止掉当前协程
    subid = subid + 1 --分配一个唯一的subid
    return subid
end

然后重启mylogin.lua,在启动myclient.lua:

$ 3rd/lua/lua my_workspace/myclient.lua
sceret is   312fab4e6cd9908d
403 Forbidden   #自定义的 login_handler 执行失败
3rd/lua/lua: my_workspace/myclient.lua:88: assertion failed!
stack traceback:
    [C]: in function 'assert'
    my_workspace/myclient.lua:88: in main chunk
    [C]: in ?

服务器端显示:

$ ./skynet examples/config
mylogin
[:01000010] LAUNCH snlua mylogin
[:01000012] LAUNCH snlua mylogin
[:01000019] LAUNCH snlua mylogin
[:0100001a] LAUNCH snlua mylogin
[:0100001b] LAUNCH snlua mylogin
[:0100001c] LAUNCH snlua mylogin
[:0100001d] LAUNCH snlua mylogin
[:0100001e] LAUNCH snlua mylogin
[:0100001f] LAUNCH snlua mylogin
[:01000010] login server listen at : 127.0.0.1 8001
[:01000012] connect from 127.0.0.1:48288 (fd = 9)
[:01000012] hello@sample:password
[:01000010] hello@sample is login, secret is 312fab4e6cd9908d
[:01000010] invalid client (fd = 9) error = ./lualib/snax/loginserver.lua:148: ./my_workspace/mylogin.lua:26: login_handler

​ 与skynet.auth_handler的处理一样,一旦错误,也不需要我们返回任何值,只要终止掉当前协程,skynet.loginserver框架就会自动发送406 Not Acceptable给客户端。

14.7 登录重入报错

​ 在mylogin.lua中,multilogin设置为false表示不允许同时重复登录,这里的同时重复登录不是说一个client登录完成之后,另一个client端使用相同的账号密码就不能登录。而是说在登录过程当中,还没完成登录,这个时候突然又有一个client尝试登录。那么会报给这个客户端406 Not Acceptable .

​ 由于正常情况下,同时登录比较难模拟,所以我们在login_handler(不能在auth_handler)里面添加一个sleep延时5秒钟。例如在14.4的基础上:

function server.login_handler(server, uid, secret)
    skynet.error(string.format("%s@%s is login, secret is %s", uid, server, crypt.hexencode(secret)))
    subid = subid + 1
    skynet.sleep(500) --添加延时
    return subid
end

先运行服务,再运行两个客户端,查看第二客户端的运行结果:

$ 3rd/lua/lua my_workspace/myclient.lua
sceret is   1b739592afbcc437
406 Not Acceptable #该用户已经在登陆中
3rd/lua/lua: my_workspace/myclient.lua:88: assertion failed!
stack traceback:
    [C]: in function 'assert'
    my_workspace/myclient.lua:88: in main chunk
    [C]: in ?

其实不允许重复登录主要是login_handler不允许重入,因为如果重入了login_handler会造成subid分配出现并入。

下面就来模拟一下,允许重入的情况,我们把multilogin改为true:

先运行服务,再运行两个客户端,查看两个客户端的运行结果:

$ 3rd/lua/lua my_workspace/myclient.lua
sceret is   33769a939dc21114
200 Mg==
login ok, subid=    2   #分配到的subid为2
$
​
$ 3rd/lua/lua my_workspace/myclient.lua
sceret is   01588be8f9aeee99
200 Mg==
login ok, subid=    2   #分配到的subid也为2
$ 

所以为了减少这种麻烦事的出现,大家尽量让multilogin为false。

14.6 密钥交换失败

​ 密钥交换失败一般不会发生在前几个步骤,因为前几个步骤不会去验证双方的数据是否正确,只要在交换完密钥使用密钥加密challenge的时候才会验证一下,如果这个时候验证不成功将会返回给客户端一个:400 Bad Request

​ 下面我们就来模拟一下,修改14.4中的myclient.lua

local hmac = crypt.hmac64(challenge, secret) --加密的时候还是需要直接传递secret字节流
--改为
local hmac = crypt.hmac64("11111111", secret) --加密的时候还是需要直接传递secret字节流

运行服务,再运行myclient:

$ 3rd/lua/lua my_workspace/myclient.lua
sceret is   da1f3e758d04fc56
400 Bad Request #握手失败
3rd/lua/lua: my_workspace/myclient.lua:88: assertion failed!
stack traceback:
    [C]: in function 'assert'
    my_workspace/myclient.lua:88: in main chunk
    [C]: in ?
$ 

skynet框架应用 (十四) 登录服务相关推荐

  1. PyTorch框架学习十四——学习率调整策略

    PyTorch框架学习十四--学习率调整策略 一._LRScheduler类 二.六种常见的学习率调整策略 1.StepLR 2.MultiStepLR 3.ExponentialLR 4.Cosin ...

  2. 《深入理解 Spring Cloud 与微服务构建》第十四章 服务链路追踪 Spring Cloud Sleuth

    <深入理解 Spring Cloud 与微服务构建>第十四章 服务链路追踪 Spring Cloud Sleuth 文章目录 <深入理解 Spring Cloud 与微服务构建> ...

  3. skynet框架应用 (十二) snax框架

    12 snax框架 ​ snax 是一个方便 skynet 服务实现的简单框架.(简单是相对于 skynet 的 api 而言) ​ 使用 snax 服务先要在 Config 中配置 snax 用于路 ...

  4. Spring Cloud第十四篇: 服务注册(consul)

    这篇文章主要介绍 spring cloud consul 组件,它是一个提供服务发现和配置的工具.consul具有分布式.高可用.高扩展性. 一.consul 简介 consul 具有以下性质: 服务 ...

  5. 史上最简单的 SpringCloud 教程 | 第十四篇: 服务注册(consul)

    转:https://blog.csdn.net/forezp/article/details/70245644 这篇文章主要介绍 spring cloud consul 组件,它是一个提供服务发现和配 ...

  6. java版电子商务spring cloud分布式微服务b2b2c社交电商 (十四)服务注册(consul)

    Springcloud b2b2c电子商务社交平台源码请加企鹅求求:一零三八七七四六二六.这篇文章主要介绍 spring cloud consul 组件,它是一个提供服务发现和配置的工具.consul ...

  7. SSM框架之酒店管理系统十四(C端完善前台用户登录、计算日期之间的天数、房间预订)

    SSM框架之酒店管理系统十四(C端完善前台用户登录.计算日期之间的天数.房间预订) 当用户点击预定的时候,判断是否有登录的session 1.修改用户登录时保存的sessuin中的key 如果不修改的 ...

  8. 淘宝服务端高并发分布式架构的十四次演进之路

    1.概述 本文以淘宝作为例子,介绍从一百个并发到千万级并发情况下服务端的架构的演进过程,同时列举出每个演进阶段会遇到的相关技术,让大家对架构的演进有一个整体的认知,文章最后汇总了一些架构设计的原则. ...

  9. Spring Security技术栈学习笔记(十四)使用Spring Social集成QQ登录验证方式

    上一篇文章<Spring Security技术栈开发企业级认证与授权(十三)Spring Social集成第三方登录验证开发流程介绍>主要是介绍了OAuth2协议的基本内容以及Spring ...

  10. 【Microsoft Azure 的1024种玩法】五十四. 十分钟快速上手创建部署Azure speech服务

    [简介] Azure语音服务是Microsoft提供稳定可靠的云通信服务,其在单个 Azure 订阅中统合了语音转文本.文本转语音以及语音翻译功能,我们可以通过各种方式(语音 CLI.语音 SDK.S ...

最新文章

  1. AssertionError: backend 'postgresql' unavailable 与 AssertionError: backend 'mysql' unavailable
  2. oracle客户端下载 win8.1,WINDOWS8.1安装ORACLE客户端及配置
  3. 频谱分析:基于python画出时域频域波形
  4. git pull提示当前branch没有跟踪信息
  5. 奇怪的bug,不懂Atom在添加markdown-themeable-pdf,在配置好phantomjs的情况下报错
  6. Spring Data JPA 从入门到精通~@EntityListeners注解示例
  7. 21.和和instance of
  8. 重叠面积_重叠面积——动点产生的重叠面积问题
  9. 大数据总结微信自媒体运营
  10. 1636: Pascal山脉
  11. SQL Server Management Studio中SQL代码段
  12. Excel打印时,如何带上当前时间~
  13. 句柄详解,什么是句柄?句柄有什么用?
  14. IOS下,利用捏合手势实现图像缩放和显示
  15. Blender3.0资产浏览器
  16. php微信上传图文素材,php使用curl 上传微信公共平台素材文件
  17. android 支付宝未安装,调用支付宝接口Android客户端没有支付宝APP的情况下解决无法调用支付宝页面的问题...
  18. java.lang.NoClassDefFoundError: org/jdom2/JDOMException
  19. 【第1164期】从前端技术到体验科技
  20. 集体照的拍摄与后期合成处理

热门文章

  1. Oracle 转 PG- ERROR: recursive query “t“ column 2 has type character varying(150) in non-recursive t
  2. java happen-before_Java happen-before规则
  3. tnl 的 masterServer, client server 架构学习笔记
  4. WCDMA信令流程(非常详细)
  5. SSL证书常见错误和解决办法
  6. 关于运行项目时 vue-pdf 插件依赖报错的问题及解决办法
  7. 【游戏数据库】大型网络游戏数据库设计方面讨论?(微软平台) 游戏数据库
  8. 05.odoo12开源框架学习
  9. Android自定义View之CircleView
  10. 显示器分辨率一直跳_常见屏幕比例与显示器分辨率详解