GithubHelp home page GithubHelp logo

tyjiosen / skynet-demo Goto Github PK

View Code? Open in Web Editor NEW

This project forked from xingshuo/skynet-demo

0.0 1.0 0.0 214 KB

a simple skynet demo of login and world chat

License: MIT License

Lua 64.01% Makefile 0.95% C 34.69% Shell 0.35%

skynet-demo's Introduction

Skynet-demo

编译连接

sh init.sh

环境搭建

服务端:
    sh rungs.sh
客户端:
    sh runcs.sh -u pid(默认1) #多终端启动,终端输入quit or Ctrl-C退出登录,输入其他字符串为世界频道消息,支持重登

服务端工作流程分析

server为工程目录,init.sh中建立了其软连接到skynet目录下,server/config为启动配置文件,文件中所有配置将做为进程内的环境变量被记录,可通过skynet.getenv获取,进程启动目录为skynet/, config文件中luaservice,lua_path,lua_cpath,preload等都是以其做参照的相对路径, start(或main)对应的是skynet进程启动的用户定义的初始化服务, 这里是server/service/init.lua,该服务由默认的bootstrap服务启动. 下面分析init.lua

#service/init.lua
local Skynet = require "skynet"
local BcApi = require "broadcast.api"
local AgentApi = require "agent.api"

local function __init__()
    print("===========game_init begin=========",GetDate())
    BcApi.init()

    AgentApi.init()

    Skynet.newservice("database")

    local gate = Skynet.newservice("gamegate")
    local login_port = tonumber(Skynet.getenv("login_port"))
    Skynet.send(gate, "lua", "open", {port = login_port})

    Skynet.newservice("gamelogin")

    Skynet.newservice("chat")

    print("===========game_init end=========")
    Skynet.exit()
end

Skynet.start(__init__)

Skynet.start先将skynet.dispatch_message注册为服务收到消息的回调函数,再通过定时器回射的方式执行__init__函数.

#broadcast/api.lua
local Skynet = require "skynet"

local broadcast_list = {}

--世界频道广播服
table.insert(broadcast_list, "WORLD_CHAT_BC")

--通用广播服
table.insert(broadcast_list, "PUB_BC")

local M = {}

function M.init()
    for _,service in ipairs(broadcast_list) do
        Skynet.newservice("broadcast", service)
    end
end
function M.register_fd(uuid, fd)
    for _,service in ipairs(broadcast_list) do
        Skynet.send(service, "lua", "register_fd", uuid, fd)
    end
end

function M.unregister_fd(uuid)
    for _,service in ipairs(broadcast_list) do
        Skynet.send(service, "lua", "unregister_fd", uuid)
    end
end

BcApi.init 启动了一个世界频道广播服务和一个通用广播服,当玩家登录成功后,会把其pid与socket在逻辑层对应fd的映射关系通过register_fd注册到所有广播服务, 下线的时候通过unregister_fd从所有广播服务删除其映射关系.这样做的目的是无论在哪个服务想要给客户端发消息,只需将玩家的pid和消息内容发往指定的广播服,该广播服务会自动将消息发送给玩家,lualib/net.lua 里封装了4个接口用来处理给玩家发包的功能. demo支持了json和sproto两种CS通信协议的解决方案,可以通过common/pubdefines.lua文件配置,该过程也被封装在lualib/net.lua的pack和unpack函数中,common/protocol文件是协议的内容说明. 下面看下broadcast服务启动文件

#service/broadcast.lua
local Skynet = require "skynet"
local Debug = require "lualib.debug"
local Handle = require "broadcast.command"
local service_name = ...

local function __init__()
    Skynet.dispatch("lua", function(_, _, cmd, ...)
        local f = assert(Handle[cmd], cmd)
        Skynet.retpack(f(...))
    end)
    Skynet.register(service_name)
    Debug.fprint("====service %s start====",service_name)
end

Skynet.start(__init__)

Skynet.dispatch("lua", ...)这一行注册了"lua"类型消息的回调函数,实际上是在这里被调用, local f = assert(Handle[cmd], cmd) 设置回调函数根据broadcast/command.lua返回的table做为接收cmd的处理方案,例如register_fd函数中发送"lua"类型的"register_fd"命令给所有广播服,广播服在收到该消息后后执行到这里 Skynet.retpack(f(...))这一行把执行函数的返回值打包,再根据发送服务请求消息中的session值是否为0(发送服务调用skynet.send还是skynet.call),来决定是否把打包的返回值回应给发送服务 Skynet.register(service_name) 这一行把服务启动时接收到的参数service_name 这里应该就是"WORLD_CHAT_BC"或者"PUB_BC"作为服务的字符串标识注册到C层,所以skynet.send和skynet.call的目标服务地址参数, 既可以填目标服务调用skynet.self()返回的整形handle,也可以填Skynet.register注册的字符串.

#agent/api.lua
local Skynet = require "skynet"
local Player = require "agent.player"

local M = {}

function M.get_user_agent( pid )
    local iAgentCnt = Skynet.getenv("AGENT_CNT")
    local n = pid % iAgentCnt
    if n == 0 then
        n = iAgentCnt
    end
    n = math.floor(n)
    return "AGENT" .. n
end

function M.new_player(pid, mArgs)
    return Player:new(pid, mArgs)
end

function M.init()
    local iAgentCnt = Skynet.getenv("AGENT_CNT")
    for i=1,iAgentCnt do
        Skynet.newservice("agent", i)
    end
end

return M

AgentApi.init 启动了AGENT_CNT(config文件配置参数)个agent服务,作为玩家对象的生存服务,根据get_user_agent可以看出pid与agent的映射关系. 大部分情况下,登录的所有玩家会被相对均匀的分配到启动了的AGENT_CNT个agent服务中.

Skynet.newservice("database") 启动了一个简陋的数据库服务,它只是简单的把玩家的信息存储在内存中,实际应用可通过该服务与mysql,mongodb等数据库建立连接

local gate = Skynet.newservice("gamegate") 启动了网关服务gamegate,它的实现参考了这里

local login_port = tonumber(Skynet.getenv("login_port"))
Skynet.send(gate, "lua", "open", {port = login_port})
这两行实现了网关服务对本地config文件中配置的login_port端口的监听,下面具体分析下gamegate的实现

#service/gamegate.lua
local Skynet = require "skynet"
local Netpack = require "skynet.netpack"
local Socketdriver = require "skynet.socketdriver"
local Utils = require "lualib.utils"
local Debug = require "lualib.debug"
local Connection = require "login/connection"
local socket    -- listen socket
local queue     -- message queue
local CMD = setmetatable({}, { __gc = function() Netpack.clear(queue) end })

function CMD.open(source, conf)
    local address = conf.address or "0.0.0.0"
    local port = assert(conf.port)
    Skynet.error(string.format("====Listen on %s:%d start====", address, port))
    socket = Socketdriver.listen(address, port)
    Socketdriver.start(socket)
    Skynet.error(string.format("====Listen on %s:%d %d end====", address, port,socket))
end

function CMD.close()
    assert(socket)
    Socketdriver.close(socket)
end

 #Omitted some code here...

local MSG = {}

local function dispatch_msg(fd, msg, sz)
    local conn = Connection.get_conn(fd)
    if not conn then
        return
    end
    if conn.m_Agent then
        Skynet.send(conn.m_Agent, "lua", "unpack", conn.m_Pid, msg, sz)
    else
        Skynet.send("GAMELOGIN", "lua", "unpack", fd, msg, sz)
    end
end

MSG.data = dispatch_msg

local function dispatch_queue()
    local fd, msg, sz = Netpack.pop(queue)
    if fd then
        Skynet.fork(dispatch_queue)
        dispatch_msg(fd, msg, sz)

        for fd, msg, sz in Netpack.pop, queue do
            dispatch_msg(fd, msg, sz)
        end
    end
end

MSG.more = dispatch_queue

function MSG.open(fd, msg)
    Socketdriver.start(fd)
    Socketdriver.nodelay(fd)
    Connection.new_conn(fd)
end

function MSG.close(fd)
    Connection.del_conn(fd)
end

function MSG.error(fd, msg)
    Connection.del_conn(fd)
end

Skynet.register_protocol {
    name = "socket",
    id = Skynet.PTYPE_SOCKET,   -- PTYPE_SOCKET = 6
    unpack = function ( msg, sz )
        return Netpack.filter( queue, msg, sz)
    end,
    dispatch = function (_, _, q, type, ...)
        queue = q
        if type then
            MSG[type](...)
        end
    end
}

Skynet.start(function()
    AddTimer(5*60*100, function () Connection.check_conns() end, "CheckConnections")
    Skynet.dispatch("lua", function (_, address, cmd, ...)
        local f = CMD[cmd]
        if f then
            Skynet.ret(Skynet.pack(f(address, ...)))
        end
    end)
    Skynet.register("GAMEGATE")
    Debug.print("====service GAMEGATE start====")
end)

首先看下CMD.open函数:socket = Socketdriver.listen(address, port) 将完成创建TCP socket -> bind -> listen的流程,并将包装过的逻辑层fd返回.Socketdriver.start(socket) 将对应的系统fd注册到epoll或kqueue中.
服务在初始化的时候调用Skynet.register_protocol 注册了"socket"类型消息的unpack和dispatch方法,网络线程读取到某系统fd的网络流后会将其以"socket"类型的消息发给fd对应的注册服务. 服务收到"socket"消息后,通过unpack方法调用Netpack.filter进行网络流的解析 当有客户端connect login_port,网络线程完成accept后,会将新建socket包装过的逻辑层fd返回,这里会将消息传递给MSG.open函数做处理,这里同样会把新创建的fd注册到epoll或kqueue中,同时调用Connection.new_conn(fd)创建一个连接对象. accept的fd接收到网络流会以消息传递给MSG.data(收到的流长度正好为2字节包头指定长度)和MSG.more(收到的流长度大于2字节包头指定长度),最后都会将完整包交给dispatch_msg函数处理. 同理,accept的fd断开连接时,会被MSG.close处理,产生错误时,会被MSG.error处理.

#login/command.lua
local Skynet = require "skynet"
local Utils = require "lualib.utils"
local Net = require "lualib.net"
local AgentApi = require "agent.api"

local M = {}

function M.unpack(fd, msg, sz)
    local proto,param = Net.unpack(msg, sz)
    if proto == "c2gs_login" then
        local pid = param.pid
        local name
        local is_new_role = false
        local mdb = Skynet.call("DB", "lua", "query_usr_data", pid)
        if mdb then
            name = mdb.name
        else
            name = Utils.random_name()
            is_new_role = true
            Skynet.send("DB", "lua", "set_usr_data", pid, {name = name, create_time = GetSecond()})
        end
        local agent = AgentApi.get_user_agent(pid)
        local mArgs = {pid = pid, agent = agent, name = name, fd = fd, is_new_role = is_new_role}
        Skynet.send("GAMEGATE", "lua", "loginsuc", fd, mArgs)
    end
end

return M

Skynet.newservice("gamelogin") 启动了登录中心服务,它的主要作用是sdk验证(这里没有这部分),去数据库加载玩家数据(实际应用中可能是去数据中心拉取账号下角色信息),
并根据玩家pid分配一个agent服,然后发送"loginsuc"的命令给网关服务,下面看下loginsuc的处理

#service/gamegate.lua
function CMD.loginsuc(source, fd, mArgs)
    local conn = Connection.get_conn(fd)
    if not conn then
        Debug.print("login error:no conn",fd,Utils.table_str(mArgs))
        return
    end
    if conn.m_Agent then
        Debug.print("login error:re login",fd,Utils.table_str(mArgs))
        return
    end
    local oldcon = Connection.get_pid_conn(mArgs.pid)
    if oldcon then --已经登录的直接踢下线
        Connection.del_conn(oldcon.m_fd)
    end
    conn:loginsuc(mArgs.agent, mArgs.pid)
    Skynet.send(conn.m_Agent, "lua", "start", mArgs.pid, mArgs)
    Debug.print("login suc",fd,Utils.table_str(mArgs))
end

这里会获取CS connect时,Connection.new_conn建立的连接对象,并把pid和Agent的信息通过conn:loginsuc(mArgs.agent, mArgs.pid)记录到连接对象中,到此,玩家的登录验证就完成了.
Skynet.send(conn.m_Agent, "lua", "start", mArgs.pid, mArgs) 将正式登录的消息发往对应agent服,根据玩家数据mArgs创建玩家对象,并向聊天服务注册自己的信息
再看下dispatch_msg接口的实现

local function dispatch_msg(fd, msg, sz)
    local conn = Connection.get_conn(fd)
    if not conn then
        return
    end
    if conn.m_Agent then
        Skynet.send(conn.m_Agent, "lua", "unpack", conn.m_Pid, msg, sz)
    else
        Skynet.send("GAMELOGIN", "lua", "unpack", fd, msg, sz)
    end
end

易知网关收到包后,如果该连接已完成登录验证,则直接将消息发往其对应的agent服务,否则发送消息到登录中心去走上述验证流程
Skynet.newservice("chat") 启动了聊天服务,服务启动时会创建世界频道,管理聊天相关逻辑.
最后调用Skynet.exit()注销自己.至此init服务的全部内容便分析完了.
下面是完整流程图:
flowchart

skynet-demo's People

Contributors

xingshuo avatar

Watchers

James Cloos avatar

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.