- A+
上一篇文章中我在xlua引入了lua-protobuf的库,本来是想在客户端中也把服务端写了的,但是启动后设置reuseaddr出现了权限问题,这也不知道咋解决了,所以决定用一个也是用lua写业务逻辑的服务端skynet来做服务端了。
客户端
一、启动的时候引入lua-protobuf
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
luaState.AddBuildin("pb", XLua.LuaDLL.Lua.LoadLuaProfobuf);//luaState 为虚拟机 //新建的 BuildInInit.cs文件 namespace XLua.LuaDLL { using System.Runtime.InteropServices; public partial class Lua { [DllImport(LUADLL, CallingConvention = CallingConvention.Cdecl)] public static extern int luaopen_pb(System.IntPtr L); [MonoPInvokeCallback(typeof(LuaDLL.lua_CSFunction))] public static int LoadLuaProfobuf(System.IntPtr L) { return luaopen_pb(L); } } } |
二、lua中加载和解析proto
引入lua-protobuff的protoc.lua文件用于加载.proto文件和解析,这个库只是单单的对协议的内容进行解析,前提是你需要知道是哪一个包的message,我称它为cmd。所以需要对数据做一个规定,首先对于协议,我们收到字节流的时候需要知道哪个是头到哪里是尾,所以可以约定前两个字节来表示后面包体的长度,后面四个字节映射一个cmd对应的hash值,最后才跟上proto的解析数据。下面看看这个工具类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 |
local protoc = require "Frame/Net/protoc" local crc32 = require "Frame.Net.crc32" local pb = require "pb" local M = {} local name2code = {} local code2name = {} local function analysis_file(content) local lines = string.split(content, "\n") local pk_name = "" for k, line in ipairs(lines) do local s, c = string.gsub(line, "^%s*package%s*([^%s]+)%s*[;%s].*$", "%1") if c > 0 then pk_name = s end local s, c = string.gsub(line, "^%s*message%s*([^%s]+)%s*[{%s].*$", "%1") if c > 0 then local name = pk_name.."."..s local code = crc32.hash(name) if not name2code[pk_name] then name2code[pk_name] = {} end name2code[pk_name][name] = code code2name[code] = name end end record_proto_info(pk_name, content) end function M.add_proto_file(proto_name) local content if PlatformUtil.IsEditor() then content = M.get_proto_content_from_editor(proto_name .. ".proto") else content = M.get_proto_content_from_mobile(proto_name) end if content then protoc:load(content, proto_name) analysis_file(content) end end local PROTO_FILE_PATH = "/../Src/Proto" function M.get_proto_content_from_editor(proto_name) if not M.flag_reg_path then M.flag_reg_path = true local dataPath = PathManager:GetInstance():GetDataPath() PROTO_FILE_PATH = dataPath .. PROTO_FILE_PATH protoc:addpath(PROTO_FILE_PATH) end local file_path = PROTO_FILE_PATH .. "/" .. proto_name local file = io.open(file_path, "r") if file == nil then zerror( string.format("get_proto_content_from_editor io.open path:%s error", file_path) ) return end local content = file:read("*a") file:close() return content end function M.get_proto_content_from_mobile(proto_name) local address = string.format("Proto/%s.proto.bytes" ,proto_name) local content = AddressableManager:GetInstance():GetProtoContent(address) if content ~= "" then return content else zerror("get_proto_content_from_mobile content load error ", address) end end function M.pack(cmd, msg) local proto = string.split(cmd, ".") if not name2code[proto[1]] then return nil, string.format("not register proto = %s", proto[1] or "nil") end local code = name2code[proto[1]][cmd] if not code then return nil, string.format("protopack_pb fail, cmd:%s", cmd or "nil") end --pbstr local pbstr = pb.encode(cmd, msg) local pblen = string.len(pbstr) local len = 4+pblen local f = string.format("> i2 I4 c%d", pblen) local str = string.pack(f, len, code, pbstr) return str end function M.unpack(str) local pblen = string.len(str)-4 local f = string.format("> I4 c%d", pblen) local code, pbstr = string.unpack(f, str) local cmd = code2name[code] if not cmd then return nil, string.format("code2name[%d] not exist", code) end -- zprint("unpack cmd " .. tostring(cmd) ) local msg = pb.decode(cmd, pbstr) return cmd, msg end return M |
首先是加载proto文件,加载之后会分享和计算出两个列表,用来做cmd和hash的互相映射,方便打包了解包使用。打包的时候,只要传入cmd就可以获取到hashcode用来告知服务端是哪个包,需要前后端使用同一个hash算法。pack打包成"> i2 I4 c%d"的形式,解包的时候先获取前两个字节,解出长度,再接收这个长度的字节流交给unpack处理。
安全进阶:加密和解密、数据校验。通过字码表做映射加密解密,弄一个256长度的加密表和解密表,加密的时候遍历包体的所有字节,字节a转10进制num,该字节就替换为加密表对应索引为num的字节b。同理解密表索引为b的10进制的字节就是a了。数据校验可以将所有的字节10进制相加再加上1,然后取反,字节流头部加多4字节存这个check。拿到数据时就可以将check和所有数据包的字节10进制相加,最后为0就通过验证了。
三、基础链接接口
长连接接口,一般有链接、断开链接、发送消息、接收消息、设置链接成功失败断开等回调、还有辅助获取状态的接口等。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 |
local M = Class("Frame.Net.BaseConnection") function M:ctor() self.connect_timeout = 10 self.read_timeout = 20 self.name = 'default' end function M:SetConnectName( name ) self.name = name end function M:GetConnectName() return self.name end --@brief 连接服务器 function M:Connect(ip, port, connect_timeout) self.ip = ip or self.ip self.port = port or self.port self.connect_timeout = connect_timeout or self.connect_timeout end function M:SetConnectTimeout(value) if value then self.connect_timeout = value end end function M:SetConnectCallback(cb_connect_suc, cb_connect_fail, cb_connect_close) self.cb_connect_suc = cb_connect_suc self.cb_connect_fail = cb_connect_fail self.cb_connect_close = cb_connect_close end function M:SetReceiveCallback( cb ) self.cb_receive_callback = cb end --virual function M:SetPacket(proto_alias, data) end --virual function M:RecivePacket() end --@brief 断开连接 function M:Disconnect() end --virual function M:GetSocket() end --virual function M:IsConnected() end --virual function M:IsConnecting() end --virual function M:IsClosed() end --virual function M:ConnIsValid() end return M |
四、TCP链接
实现基类的方法。发送的时候通过上面写的proto工具,打包协议数据。接收的时候根据规则,获取两字节的包长度,再根据长度获取后面的包体数据,等达到需要的数据时,再解包调用回调去分发消息。随后再接收两字节的包体长度,就这样循环。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 |
local base = require("Frame.Net.BaseConnection") local M = Class("Frame.Net.TcpConnection", base) local pb_proto = require("Frame.Net.pb_proto") local socket = require ("socket") local MAX_RECV_TIMEOUT_COUNT = 3 function M:ctor() self.connect = nil self.net_state = FrameConst.TcpState.NONE self:ResetData() end function M:ResetData() self.recvbuff = "" self.head_data = "" self.need_recv_len = 0 self.pack_len = 0 self.pack_data = "" self.recv_timeout_count = 0 end function M:Connect(ip, port, timeout) if self:IsConnecting() or self:IsConnected() then return end base.Connect(self, ip, port, timeout) local addrinfo, err = socket.dns.getaddrinfo(ip) if not addrinfo then zerror("TcpConnection Connect addrinfo is nil") self:__OnConnectFail(err) return end local connect, ok, err for _, alt in ipairs(addrinfo) do if alt.family == "inet" then connect, err = socket.tcp() else connect, err = socket.tcp6() end if not connect then zerror("TcpConnection Connect addrinfo not connect") self:__OnConnectFail(err) return end connect:settimeout(0) connect:setoption("tcp-nodelay", true) ok, err = connect:connect(alt.addr, self.port) if ok or err == "timeout" then ok = true ip = alt.addr break end end if not ok then self:__OnConnectFail(err) return end self.connect = connect end function M:Disconnect() self.net_state = FrameConst.TcpState.NONE if self.connect then zprint(string.format("TcpConnection Disconnect socket closed ip:%s port:%s", self.ip, self.port), '__traceback') self.connect:shutdown() self.connect:close() self:ResetData() if self.cb_connect_close then self.cb_connect_close(self.name) end self.connect = nil else self:ResetData() end end function M:SendPacket(proto_alias, data) if not self:IsConnected() then zprint("TcpConnection SendPacket Fail For Socket Is Not Conencted", proto_alias) return end local prot_str, err_msg = pb_proto.pack(proto_alias, data) if not prot_str then zerror(err_msg) return end local byte, err, last_send = self.connect:send(prot_str, 0) if not byte then zerror("SendPacket Error And Disconnect Connection proto_alias " .. proto_alias) self:Disconnect() return false else if FrameConst.Debug then zprint(string.format("SendPacket %s Success", proto_alias) , " ", data, "__tb", " ", Json.encode(data)) end end return true end function M:RecivePacket() self:__HandlePacket() end function M:__HandlePacket() while true do if not self:ConnIsValid() then self:Disconnect() return end local continue = false local need_len = self.need_recv_len > 0 and self.need_recv_len or 2 local str, err = self.connect:receive(need_len) if not str then if not str then if err == "timeout" or err == "nodata" then self.recv_timeout_count = self.recv_timeout_count + 1 if self.recv_timeout_count > MAX_RECV_TIMEOUT_COUNT then self.recv_timeout_count = 0 self:Disconnect() end else --closed zprint("__HandlePacket error ip:%s port:%s err:%s", self.ip, self.port, err) self:Disconnect() end return end end self.recv_timeout_count = 0 self.recvbuff = self.recvbuff .. str local bufflen = #self.recvbuff if self.pack_len <= 0 then --接受头部的长度数据 if #self.head_data + bufflen >= 2 then --头部标志的长度数据够了 local need_head_len = 2 - #self.head_data local head_str = string.sub(self.recvbuff, 1, need_head_len) self.head_data = self.head_data .. head_str --解析头部数据得到包的长度 self.pack_len = string.unpack(">I2", self.head_data) self.need_recv_len = self.pack_len - (bufflen - need_head_len) if bufflen > need_head_len then --剩下的数据 self.recvbuff = string.sub(self.recvbuff, need_head_len) else self.recvbuff = "" end else --继续接收头部数据 self.head_data = self.head_data .. self.recvbuff self.recvbuff = "" self.need_recv_len = 2 - #self.head_data continue = true end elseif #self.pack_data < self.pack_len then --接收包体数据 if bufflen <= self.need_recv_len then self.pack_data = self.pack_data .. self.recvbuff self.need_recv_len = self.need_recv_len - bufflen self.recvbuff = "" else self.pack_data = self.pack_data .. string.sub(self.recvbuff, 1, self.need_recv_len) self.recvbuff = string.sub(self.recvbuff, self.need_recv_len) self.need_recv_len = 0 end end if not continue and #self.pack_data == self.pack_len then --收到完整的包 self.pack_len = 0 self.head_data = "" self.need_recv_len = 0 local pb_str = self.pack_data self.pack_data = "" --xpcall下,防止某个包解析或处理时出现问题导致后续包不能立即处理 xpcall(function() local cmd, msg = pb_proto.unpack(pb_str) if FrameConst.Debug then zprint(string.format("RecivePacket %s Success", cmd) , " ", msg, "__tb") end self.cb_receive_callback(cmd, msg) end, function(errmsg) zerror(errmsg) end) end end end function M:GetSocket() return self.connect end function M:IsConnected() return self.net_state == FrameConst.TcpState.CONNECTED end function M:IsConnecting() return self.net_state == FrameConst.TcpState.CONNECTING end function M:IsClosed() return self.net_state == FrameConst.TcpState.NONE end function M:ConnIsValid() if not self.connect or self.connect:getfd() < 0 then return false end return true end function M:__OnConnectFail(err) zprint(string.format("__OnConnectFail ip:%s port:%s", self.ip, self.port)) self.net_state = FrameConst.TcpState.NONE if self.cb_connect_fail then self.cb_connect_fail(self.name, err) end self.connect = nil end function M:__OnConnectSuc() zprint(string.format("__OnConnectSuc ip:%s port:%s", self.ip, self.port)) self.timeout_count = 0 self.net_state = FrameConst.TcpState.CONNECTED if self.cb_connect_suc then self.cb_connect_suc(self.name) end end return M |
这里会出现一个问题,在创建tcp链接的时候,设置了timeout为0,但是在接收数据的时候,解析出了前面2字节的包体长度时,reveive传需要的是整个剩余包体的长度,如果很长,缓存区数据不够会直接返回timeout,后面会卡住一直接收不到数据,也不知道为什么。看了下luasocket的源码,发现应该是当缓存区长度不够时会去等到数据达到需要的长度知道超时,我们超时是0,所以数据不够直接返回timeout了,那只要修改判断一下是timeout的话看看有没有获取到数据,修改错误返回获取到的数据。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
/*-------------------------------------------------------------------------*\ * Reads a fixed number of bytes (buffered) \*-------------------------------------------------------------------------*/ static int recvraw(p_buffer buf, size_t wanted, luaL_Buffer *b) { int err = IO_DONE; size_t total = 0; while (err == IO_DONE) { size_t count; const char *data; err = buffer_get(buf, &data, &count); count = MIN(count, wanted - total); luaL_addlstring(b, data, count); buffer_skip(buf, count); total += count; if (total >= wanted) break; } if (err == IO_TIMEOUT && total > 0) { err = IO_DONE; } return err; } |
五、链接管理NetManager
前面都只是实现了一些可以提供调用的方法,其中接收是需要一直去调用的,所以集中在NetManager去管理链接和调用。
1.加载proto,注册和分析。
1 2 3 4 5 6 |
local M = { "Test/Test", "Maniac/Maniac", } return M |
1 2 3 4 5 |
function M:InitProto() for i,v in ipairs(ProtoList) do pb_proto.add_proto_file(v) end end |
2.创建和管理链接
通过几个table存储链接,可以判断正在链接的是否链接成功。已经链接成功的,映射socket和Tcp的实例,方便对方法的调用。
1 2 3 4 5 6 7 |
function M:Init() self.connectings = {} self.connecteds = {} self.read_sockes = {} self.write_sockets = {} self.socket_connect_map = {} end |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
function M:CreateConnect(name, ip, port) if self.connectings[name] or self.connecteds[name] then zprint(string.format("CreateConnect %s is connecting or connected", name)) return end local connect = TcpConnection.New() if name then connect:SetConnectName(name) end connect:SetConnectTimeout(3) connect:SetConnectCallback( Handler(self, self.OnConnectSuccess), Handler(self, self.OnConnectFail), Handler(self, self.OnConnectClose) ) connect:SetReceiveCallback(Handler(self, self.OnRecivePacket)) connect:Connect(ip, port) local curr_time = os.time() connect.start_connect_time = curr_time connect.last_recv_time = curr_time local s = connect:GetSocket() table.insert(self.write_sockets, s) self.connectings[name] = connect self.socket_connect_map[s] = connect end |
在回调中管理这几个table。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 |
function M:OnConnectFail( name, err ) if err then zprint("OnConnectFail " .. err) end local connect = self.connecteds[name] if not connect then connect = self.connectings[name] end if not connect then return end local s = connect:GetSocket() for i = 1, #self.write_sockets do if self.write_sockets[i] == s then table.remove(self.write_sockets, i) break end end for i = 1, #self.read_sockes do if self.read_sockes[i] == s then table.remove(self.read_sockes, i) break end end self.socket_connect_map[s] = nil self.connecteds[name] = nil CallEvent(FrameEvent.NET_ON_CONNECT_FAIL, name, err) end function M:OnConnectClose( name ) zprint("NetManager OnConnectClose " .. name) local s = self.connecteds[name]:GetSocket() for i = 1, #self.write_sockets do if self.write_sockets[i] == s then table.remove(self.write_sockets, i) break end end for i = 1, #self.read_sockes do if self.read_sockes[i] == s then table.remove(self.read_sockes, i) break end end self.socket_connect_map[s] = nil self.connecteds[name]:Dispose() self.connecteds[name] = nil CallEvent(FrameEvent.NET_ON_CONNECT_CLOSE, name) end |
3.判断是否连接成功,是否需要接收数据
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 |
function M:Init() self.update = RegistUpdate(self.Update, self) end function M:Update() self:__TcpUpdate() end function M:__TcpUpdate() local curr_time = os.time() for k,v in pairs(self.connectings) do --链接超时 if curr_time - v.start_connect_time >= v.connect_timeout then v:__OnConnectFail("connect timeout") self.connectings[k] =nil end end if #self.read_sockes == 0 and #self.write_sockets == 0 then return end local rl, wl, err = socket.select(self.read_sockes, self.write_sockets, 0) if #wl > 0 then for i,s in ipairs(wl) do local connect = self.socket_connect_map[s] if connect then local addr, port = s:getpeername() if addr and port then local name = connect:GetConnectName() self.connecteds[name] = connect connect:__OnConnectSuc() self.connectings[name] =nil else connect:__OnConnectFail("not connect") end end end end if #rl > 0 then for i,s in ipairs(rl) do if self.socket_connect_map[s] then self.socket_connect_map[s].last_recv_time = curr_time self.socket_connect_map[s]:RecivePacket() end end end end |
4.事件分发,需要分发的数据类继承这个基类,在构造的时候就会调用注册方法注册进去,需要监听哪个cmd就在CMD_FUNCTION_MAP中用cmd做key,value为方法的字符串或方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
local M = Class("Frame.Net.BaseSocketHandler") M.CMD_FUNCTION_MAP = {} function M:ctor() self:RegisterSocketHandler() end function M:dtor() self:UnregisterSocketHandler() end function M:RegisterSocketHandler() if not self.handler_id then self.handler_id = NetManager:GetInstance():AddSocketHandler(Handler(self, self.OnReceivePacket)) end end function M:UnregisterSocketHandler() if self.handler_id then NetManager:GetInstance():RemoveSocketHandler(self.handler_id) end end function M:OnReceivePacket(cmd, data) local obj = self while obj and obj.CMD_FUNCTION_MAP do if obj.CMD_FUNCTION_MAP[cmd] then local func = obj.CMD_FUNCTION_MAP[cmd] local ty = type(func) if ty == "function" then func(self, data) elseif ty == "string" then self[func](self, data) end break else obj = obj.super end end end return M |
NetManager维护分发列表:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
function M:Init() self.socket_handler_list = SocketHandlerList.New() end function M:OnRecivePacket( cmd, data) self.socket_handler_list:DispatchPacker(cmd, data) end function M:AddSocketHandler( handler) return self.socket_handler_list:AddHandler( handler ) end function M:RemoveSocketHandler( id ) self.socket_handler_list:RemoveHandler( id ) end |
SocketHandlerList管理一个列表:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
local M = Class("Frame.Net.SocketHandlerList") function M:ctor() self.handler_id = 0 self.handlers = {} end function M:AddHandler( handler ) local item = {} self.handler_id = self.handler_id + 1 item.id = self.handler_id item.handler = handler table.insert(self.handlers, item) return self.handler_id end function M:RemoveHandler( id ) for i,v in ipairs(self.handlers) do if v.id == id then table.remove(self.handlers, i) break end end end function M:DispatchPacker(cmd, data) for i,v in ipairs(self.handlers) do if v.handler then v.handler(cmd, data) end end end function M:dtor() self.handlers = nil end return M |
例如:DataBase也是继承SocketReciveData
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
local M = Class("Maniac.Datas.SocketReciveData", DataBase) function M:ctor() end function M:S_Use_Skill( data ) local playCtrl = ControllerManager:GetInstance():Get(G_SCENE_CONFIG.PlayerController,data.uid) if playCtrl then playCtrl:PlaySkill(data.skillID) end end M.CMD_FUNCTION_MAP = { ["Maniac.S_Use_Skill"] = "S_Use_Skill", } return M |
5.发送消息
1 2 3 4 5 6 7 8 |
function M:SendPacket(name, proto_alias, data) name = name or 'default' if not self.connecteds[name] then zprint(string.format("SendPacket %s is not connected", name)) return end self.connecteds[name]:SendPacket(proto_alias, data) end |
六、使用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
function ManiacScene:Awake() G_SCENE_CONFIG = SceneConfig self.socket_reciv_data = SocketReciveData:GetInstance() --监听接收 NetManager:GetInstance():CreateConnect("Maniac", "xxx.xxx.xx.xx", xxxx) self.connect_success_handler = RegistEvent(FrameEvent.NET_ON_CONNECT_SUCCESS, Handler(self, self.OnConnectSuccess)) end function ManiacScene:OnConnectSuccess( name ) if name == "Maniac" then local net = {} net.uid = math.random(10000, 20000) NetManager:GetInstance():SendPacket("Maniac", "Maniac.C_Login_Server", net) end end |
七、暂定,心跳包和断线重连
服务端
一、服务端框架编译
- 下载skynet1.2.0版本和jemalloc5.1.0版本,将jemalloc放到3rd目录下,整体放到linux服务器上。
- cd到skynet目录下,执行make linux编译。
- 新建game目录存放项目代码。
二、库的引入
- 下载lua-protobuf,放到game/lualib-src下,cd到该目录,执行
1 |
gcc -O2 -shared -fPIC pb.c -o pb.so |
把pb.so放到game/luaclib
2. 下载luafilesystem,放到game/lualib-src下,cd到该目录,执行
1 |
make lib |
把lfs.so放到game/luaclib下
三、启动配置
- 在game目录下新建config,新建文件config.path和config,添加game下面相关的搜索路径。设置启动服务为startup_node.lua
1 2 3 4 5 6 7 |
root = "/home/red-packer-server/" luaservice = root.."game/service/?.lua;"..root.."service/?.lua;"..root.."test/?.lua;"..root.."examples/?.lua;"..root.."test/?/init.lua" lualoader = root .. "lualib/loader.lua" lua_path = root.."game/lualib/?.lua;"..root.."lualib/?.lua;"..root.."lualib/?/init.lua" lua_cpath = root .. "game/luaclib/?.so;"..root .. "luaclib/?.so" cpath = root.."cservice/?.so;"..root.."game/cservice/?.so;" --snax = root.."examples/?.lua;"..root.."test/?.lua" |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
include "./config.path" -- preload = "./examples/preload.lua" -- run preload.lua before every lua service run thread = 8 logger = nil logpath = "." harbor = 1 address = "127.0.0.1:2526" master = "127.0.0.1:2013" start = "startup_node" -- main script bootstrap = "snlua bootstrap" -- The service for bootstrap standalone = "0.0.0.0:2013" -- snax_interface_g = "snax_g" cpath = root.."cservice/?.so" -- daemon = "./skynet.pid" |
四、创建启动服务和引入lua库
在game目录下新建lualib和service,在lualib中复制一些脚本进去
修改pb_proto.lua,proto协议文件放在game/proto下面
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 |
local skynet = require "skynet" local protoc = require "protoc" local crc32 = require "crc32" local lfstool = require "lfstool" local pb = nil local M = {} local name2code = {} local code2name = {} local check = 0 --分析proto文件,计算映射表 local function analysis_file(path) local file = io.open(path, "r") local package = "" local name = "" local flagc = 0 for line in file:lines() do local s, c = string.gsub(line, "^%s*package%s*([^%s]+)%s*[;%s].*$", "%1") if c > 0 then package = s flagc = 1 end local s, c = string.gsub(line, "^%s*message%s*([^%s]+)%s*[{%s].*$", "%1") if c > 0 then if flagc == 1 then name = package.."."..s else name = s end local code = crc32.hash(name) name2code[name] = code code2name[code] = name end end file:close() end local function init() --导入proto文件,并analysis_file local path = "/home/red-packer-server/game/proto" local r_path = "/home/red-packer-server/game/proto/" protoc:addpath(path) lfstool.attrdir(path, function(file) local is_proto = string.match(file, "/(.+%.proto)") if is_proto then local name = string.gsub(file, r_path, "") protoc:loadfile(name) analysis_file(file) end end) end function M.pack(cmd, msg) if not pb then pb=require("pb") init() end --code local code = name2code[cmd] if not code then log.error(string.format("protopack_pb fail, cmd:%s", cmd or "nil")) return end --pbstr local pbstr = pb.encode(cmd, msg) local pblen = string.len(pbstr) --len local len = 4+4+pblen local f = string.format("> i2 i4 c%d", pblen) local str = string.pack(f, len, code, pbstr) print(string.format("send: %s %s", cmd, len)) return str end function M.unpack(str) if not pb then init() pb = require "pb" end local pblen = string.len(str)-4 local f = string.format("> i4 c%d", pblen) local code, pbstr = string.unpack(f, str) local cmd = code2name[code] if not cmd then return nil, nil, string.format("code2name[%d] not exist", code) end local msg = pb.decode(cmd, pbstr) print(string.format("receive: %s %s", cmd, pblen)) return cmd, msg end return M |
在game/service目录下新建startup_node.lua,启动游戏的服务game/maniac.lua
1 2 3 4 5 6 7 8 9 10 11 |
local skynet = require "skynet" require "string" --全局 local function start_maniac_server() local p = skynet.newservice("maniac", "maniac") end skynet.start(function() --注册服务 start_maniac_server() end) |
五、maniac游戏服务,这里就不详说了,监听数据解析和打包和解包。没有引入数据库和保存数据,同步等等,单纯的转发数据,也没有做分发模块等等,我单纯的想跑通。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 |
local skynet = require "skynet" local socket = require "skynet.socket" local pb_proto = require "pb_proto" local clients = {} local createPoints = { {x = 290, y = 0, z = 240}, {x = 363, y = 0, z = 300}, {x = 115, y = 0, z = 308}, {x = 231, y = 0, z = 192}, {x = 380, y = 0, z = 145}, {x = 104, y = 0, z = 277}, } local monsterInfos = {} local function connect(fd, addr) socket.start(fd) clients[fd] = {} local need_read = 2 local headdata = '' local pack_len = 0 local packdata = '' local recvbuf = '' local continue = false while true do continue = false need_read = need_read == 0 and 2 or need_read local readdata = socket.read(fd, need_read) if not readdata then print("close socket " .. fd) socket.close(fd) return end recvbuf = recvbuf .. readdata local bufflen = #recvbuf if pack_len <= 0 then if #headdata + bufflen >= 2 then local need_head_len = 2 - #headdata local headstr = string.sub(recvbuf, 1, need_head_len) headdata = headdata .. headstr pack_len = string.unpack(">I2", headdata) need_read = pack_len headdata = '' packdata = '' if bufflen > need_head_len then recvbuf = string.sub(recvbuf, need_head_len) else recvbuf = '' end else headdata = headdata .. recvbuf recvbuf = '' need_read = 2 - #headdata continue = true end elseif #packdata < pack_len then if bufflen <= need_read then need_read = need_read - bufflen packdata = packdata .. recvbuf recvbuf = '' else packdata = packdata .. string.sub(recvbuf, 1, need_read) recvbuf = string.sub(recvbuf, need_read) need_read = 0 end end if not continue and #packdata == pack_len then pack_len = 0 headdata = '' need_read = 0 local pack_str = packdata packdata = '' local cmd, data, err = pb_proto.unpack(pack_str) if not cmd then print(err) end OnManiacReveivePacket(fd, cmd, data) end end end function OnManiacReveivePacket(fd, cmd, data) if cmd == 'Maniac.C_Login_Server' then local uid = data.uid local dataNet = {} dataNet.uid = uid dataNet.playerinfo = {} dataNet.playerinfo.uid = uid dataNet.playerinfo.pos = {x = 102, y = 0, z = 165} dataNet.playerinfo.type = 1 dataNet.playerinfo.hp = 10000 dataNet.playerinfo.maxHp = 10000 dataNet.playerinfo.attack = 88 local str = pb_proto.pack('Maniac.S_Login_Server', dataNet) socket.write(fd, str) --初始化怪物 if #monsterInfos == 0 then for i,v in ipairs(createPoints) do local point = createPoints[i] local create_num = math.random(5, 10) for j = 1, create_num do local monster = {} monster.type = 1000 monster.uid = math.random(1000000,9999999) local oX = math.random(-20,20) local oZ = math.random(-20,20) monster.pos = {x = point.x + oX, y = point.y, z = point.z + oZ} monster.moveSpeed = 3 monster.runSpeed = 8 monster.lockuid = 0 monster.hp = 1000 monster.maxHp = 1000 monster.attack = math.random(100, 250) monster.patrolPoss = {} for k = 1, 3 do local offsetX = math.random(-20,20) local offsetZ = math.random(-20,20) monster.patrolPoss[k] = {x = monster.pos.x + offsetX, y = monster.pos.y, z = monster.pos.z + offsetZ} end table.insert(monsterInfos, monster) end end end str = pb_proto.pack('Maniac.S_Sync_MonsterInfo', {monsters = monsterInfos}) print('send to:' .. fd) socket.write(fd, str) elseif cmd == 'Maniac.C_Move_Dir' then local str = pb_proto.pack('Maniac.S_Move_Dir', data) for k,v in pairs(clients) do socket.write(k, str) end elseif cmd == 'Maniac.C_Use_Skill' then local str = pb_proto.pack('Maniac.S_Use_Skill', data) for k,v in pairs(clients) do socket.write(k, str) end end end skynet.start(function() print("start maniac listen 0.0.0.0:xxxx") local listenfd = socket.listen("0.0.0.0", xxxx) socket.start(listenfd, connect) end) |
六、启动和关闭
在skynet目录下,执行
1 |
./skynet ./game/config/config |
远程SSH链接服务器的时候,执行脚本启动斌不是守护进程启动的,但是直接关掉远程链接,服务是不会关掉的,然后不能用ctrl+c去结束了,可以执行:
1 |
ps aux | grep skynet | awk '/config/{print $2}' | xargs kill |