基于Lua的游戏服务端框架简介

基于lua的游戏服务端框架简介

1. 引言

笔者目前在参与一款FPS端游的研发,说是端游,其实研发团队比很多手游团队还小.
        我们的服务端团队只有2个人,然而,小伙伴们发现:

-            后台开发极为快速,进度远远超前.

-            稳定,从不宕机.

-            Bug定位修复神速,服务器甚至无需重启.

当然,目前只是研发期,可能问题暴露较少,而战斗逻辑也由UE4引擎承担了.
         但是除此以外,是不是还有啥秘诀呢?
        这就是本文要介绍的: 基于lua的游戏服务端框架

2. 概述

本文所述内容,并不涉及服务器集群的进程划分与拓扑结构.
        为理解方便,我们假定服务器集群划分为如下的这些进程(跟鹅厂其他游戏项目大同小异):

-            router: 数据转发,多进程按负载分担,支持点对点,广播,主从,哈希等几种常见的数据转发逻辑.

-            gamesvr: 提供客户端接入逻辑,以及常规的游戏逻辑(如道具,商城,等等...), 多实例按负载分担.

-            dbagent: 提供数据库访问服务,多进程按哈希分布.

-            matchsvr: 提供战斗匹配服务,多进程主从备份.

-            其他服务进程,不再列举.

本文所述框架以C++为基础层,以lua为业务层,旨在解决以下3个问题.

· 如何方便的在进程之间通信?

假想一个情景:我们要从gamesvr向matchsvr发一个请求,将一个玩家队伍加入匹配队列.
        请求中包含的信息: gamesvr的id, 匹配的模式(几对几),是否接受机器人,各玩家的id,各玩家的段位(elo).
        我们的程序员要干些什么事情呢?

在协议描述的XML中定义这个消息结构,呃,可能还要嵌套结构.

转换XML,生成对应的.h,.c,.tdr之类的.

在gamesvr中编写发送消息代码.

在matchsvr编写消息处理逻辑代码.

呃,对了,可能还要在派发消息的地方注册一下消息

而上面这些,跟业务逻辑有关的,其实只有3,4,其他都是累赘.
        我们能不能只关注业务逻辑,不要这些累赘呢?
        当然可以,这就是基于lua的远程调用: 无需额外的协议定义,直接编写业务代码

如下所示,gamesvr发起调用:

local mode = 3; -- 3v3

local team = {...}; --队伍成员列表

local robot = false;

--从gamesvr调用matchsvr的消息函数: OnMatchRequest

remote.CallMatchSvr("OnMatchRequest", app.iId, mode, robot, team);

下面为matchsvr的响应代码:

--远程调用OnMatchRequest的实现

--约定所有远程调用都必须定义在全局表s2s中

--s2s含义为: server to server

function s2s.OnMatchRequest(svr, mode, robot, team)

-- 加入匹配池...

end

· 如何使得我们的开发过程更加顺畅,运维响应更加及时.

开发过程

继续上面的2.1节的场景,在传统的C++实现中,想想,程序员写完两边的消息代码,要继续干什么?

1、关掉服务器,嗯,如果共用服务器,还得吼一下: 我要关服了.

2、make ...

3、启动服务器.

4、呃,客户端联调的兄弟,你重新登录一下,对了,记得要开几个客户端重新组队哦.

真繁琐啊,能不能简单点?
        是的,在新框架下,写完业务逻辑,你需要做的只是Ctrl+S,代码立即生效,自动!
        这就是新框架下的代码自动热更新,它让上面的1,2,3,4都成多余.

运维事件

假定现在运营环境发现一个严重Bug,而我们知道只要简单的改一行代码就好了.
        又或者,谁都不知道咋回事,还不能上GDB暂停,只能先在某函数处加行日志看看.
        我们要经历怎样的过程? 嗯,谁经历谁知道啊.
        有了代码热更新技术,我们对在线Bug的修复不再头疼,甚至秒修.

·  如何彻底摆脱空指针,野指针,内存越界等顽疾,提供更加稳定的服务?

嗯,这个就属于lua语言本身的特性了.
        连指针都没有,所谓空指针,野指针,内存越界,就无从谈起.
        即便是新手程序员,也没机会犯这种错误.
        即便是除零之类的错误,也不过是当前这一条消息出错,下一条消息照常处理.
        另外,lua本身的实现,也属于公认的高质量代码,值得信赖.

3、 历史

lua在游戏领域的应用,大概是从<魔兽世界>火起来的.
        本文要介绍的技术基础,沿袭自<剑网三>(一些业界同行也有类似的实现).
        笔者在2004年至2011年期间,负责<剑网三>的服务端团队.
        <剑网三>服务端架构中,虽然一开始就引入了lua支持,但是早期只是作为业务粘合层而存在.
        在研发阶段的中后期,引入了两个技术点来加速开发速度:

-            远程调用: 从此摆脱C++层面的协议定义,数据组织编码,编译更新等繁琐的过程.

-            数据存储: C++层面无需关心数据存储结构,无需再写大量的DB操作代码(MySQL);

其实这两个点都是基于lua序列化&反序列化的.
到09年上线时,<剑网三>服务端的lua逻辑大概占比在30%左右,并不算高.
这是因为:

-            <剑网三>的服务端属于计算密集型(3D场景逻辑,战斗技能,AI,等等).

-            作为笔者入行的第一个项目,出于对性能的谨慎,限制了lua在服务端的应用广度.

2011年初,我离开服务了6年多的<剑网三>团队,出去创业.
        这时,我们已经不再担心性能问题,而更需要的是快速实现,快速响应,于是lua开始大行其道.
        特别是12年我们开始做手游时,C++层面差不多只剩下了网络层,客户端也是底层基于cocos2d-x,逻辑都在lua.
        顺便提一下,也差不多是那个时候,从网易出来创业的云风也推出了基于lua的skynet开源框架.

2015年的春天,怀着对创业的绝望,我来到了腾讯.
        嗯,惊奇的发现,腾讯的游戏服务端实现中,lua应用得非常少.
        于是,便有了此文,介绍一下我们正在使用的基于lua的技术框架.

4.、技术基础

· lua的C++绑定

实现原理

1、 为每个导出的class创建了一个table(lazy模式), 其中存放了class的成员函数指针以及成员变量偏移.

2、 在上面的class专属table中,我们将表中的__index, __newindex指向我们的定制的C++函数.

3、对象首次被push到lua时,会创建一个table与之绑定,称为影子对象,该table中记录了对象指针,并以第1步的table作为其元表.

4、当在脚本中通过影子对象访问C++对象的成员时,通过元表的__index, __newindex方法定向到C++对象成员.

5、C++对象上也记录了影子对象的引用,在对象析构的时候将清除影子对象中存放的指针.

C++对象导出示例

h 中的class 声明代码:

// class 声明中需要插入一行 DECLARE_LUA_CLASS

class CPlayer
{

char m_szName[32];

int m_iLevel;

int luaSay(lua_State* L);

DECLARE_LUA_CLASS(CPlayer); // 声明导出CPlayer类

};

.cpp 中的实现代码:

// 在 CPP 中增加如下的导出声明

IMPL_LUA_CLASS_BEGIN(CPlayer)

EXPORT_LUA_STRING(m_szName)

EXPORT_LUA_INT(m_iLevel)

EXPORT_LUA_FUNCTION(luaSay)

IMPL_LUA_CLASS_END()

int CPlayer::luaSay(lua_State* L)

{

// ...

return0;

}

注意,这不是实际存在的代码;对于业务逻辑都在 lua 中实现的项目而言,真正需要导出的 C++ 代码极少.

主要特性

-            在lua中读写对象的C++成员变量(也可声明为只读).

-            在lua中调用对象的C++成员函数.

-            在lua中对影子对象添加新的"成员变量","成员函数".

-            在lua中覆盖对象中导出的C++函数.

-            在C++中调用影子对象上的lua函数.

实际使用示例

C++部分代码: 在玩家登陆时调用login.lua中定义的lua函数.

void OnPlayerLogin(lua_State* L, int iConnIdx, CPlayer* player)

{

CSafeStack guard(L);

// 除了获取文件中的函数,还有其他的相关的API

// 用来获取影子对象上的函数,以及全局 table 中的函数等等

Lua_GetFileFunction(L, "login.lua", "OnPlayerLogin");

lua_pushinteger(L, iConnIdx);

Lua_PushObject(L, player);

Lua_XCall(L, 2, 0);

}

lua部分代码: 响应上面C++代码触发的玩家登陆事件.

function OnPlayerLogin(connIdx, player)

--访问成员变量: 读

log_debug("player login, name="..player.szName);

--访问成员变量: 写

player.iLevel = 1;

--调用成员函数

player.Say("皇上吉祥");

--在player对象上加入新的函数/变量

player.OnExit = function()

-- do something !

end

end

另一个实现

关于lua的C++绑定,其实还有另一个基于C++14的实现(还在完善中,欢迎提意见:).
        主要特性在于函数参数操作不再需要写一堆的lua_to***, lua_push***之类的代码,可直接导出一般C++函数.
        遗憾的是,我司的编译器版本并不支持C++14标准,即便是tlinux2.0也不支持(GCC 4.8.2,其实C++11也只是部分支持).
        也许某天Docker的普及可以让项目自己指定编译器版本.
        C++14版本的实现

· lua文件沙盒

代码示例

关键函数: import

在main.lua中import另外两个文件: a.lua, b.lua

--main.lua

a = import("a.lua");

b = import("b.lua");

print("a.txt="..a.txt); --输出: A

a.txt = "X"; --修改 a 中的变量,不影响 b.

print("b.txt="..b.txt); --输出: B

a.lua,注意它也import了b.lua,但是b.lua在main.lua中已经加载了,两处会引用同一份实例.

txt = "A";

b = import("b.lua");

print("b.txt="..b.txt); --输出: B

b.lua,注意跟上面的a.lua定义了同名变量,但实际互不影响:

--变量txt并不是真正的全局变量

--而是存在于本文件的环境表中.

txt = "B";

实现原理

-            内部维护了一个文件加载表,记录了文件名及加载时间之类的.

-            加载lua文件时,会先为其创建一个独立的环境表,然后再执行文件,这样,文件中的"全局"符号实际上就定义在了环境表中.

-            import一个文件时,先检查文件是否已经加载,如已经加载,则不再加载,直接返回其环境表.

-            重新加载时,跟之前使用同一个环境表.

为啥不用自带的require?

-            require一个文件时,文件中声明的变量默认是全局的(除非加个local),项目大了容易发生名字冲突覆盖,

-            require当然也支持在文件加载时返回一个导出表,但是得去自己去写这个导出表.

-            我们需要对已经加载的文件做变更检测并热更新.

·  序列化

这个是整个框架中一个很基础的模块,但并不复杂,简单来说有几点:

-            序列化的数据是二进制的,反序列化时无需额外的文本解析过程.

-            序列化数据是自描述的,解析数据无需原来生成数据的代码.

-            采用了变长整数来减少小整数的空间占用(类似utf-8的编码方式).

-            采用了共享字符串以减少重复字符串的空间占用.

-            数据长度达到一定阈值时,会加一层lz4压缩.

 

 · 远程调用与持久化

此两项技术均基于上一节的序列化而实现:

-            远程调用: 函数调用参数序列化,通过通信层转到目标进程再展开,调用.

-            数据持久化: 将lua数据结构序列化,存入数据库.

远程调用原理示意图:

在"概述"一章中,已经简单介绍了远程调用的实际用法. 示例中,remote是一个C++全局导出对象,CallMatchSvr是其导出的一个成员函数.
        实际上,对应于更多种类的服务进程以及转发方式,还有很多对应的接口.
        这些接口的C++实现都差不多,其实是由同一个模板通过不同的参数特化而来. 这里举几个典型的例子,方便理解:

-            remote.CallMatchSvr(FuncName, ...): 调用MatchSvr的函数,按主从逻辑转发.

-            remote.CallGameSvrAll(FuncName, ...): 调用所有GameSvr的某函数,也就是广播.

-            remote.CallDBAgentHash(Acc, FuncName, ...): 以Acc为Key,按哈希的方式转发,调用DBAgent函数.

-            remote.CallTarget(target, FuncName, ...): 调用指定tbus id(target)进程的函数.

· 我为什么不喜欢协程?

先看个例子,猜猜里面有几个Bug:

-- ibcenter,代表另一个进程,专门负责道具交易管理

-- ibcenter.BuyItem,内部用协程实现,实际上是异步的

function BuyItem(player, itemIdx, price)

if player.fighting then

--战斗中不能买东西

return;

end

if player.money <= price then

--钱不够

return;

end

local id, item = ibcenter.BuyItem(itemIdx); --协程异步

if id then

player.items[id] = item;

player.money = player.money - price;

end

end

通过这个示例,我们应该能感受到协程的实际问题:

-              隐藏了函数调用的异步性,容易让不知内情的人写出意外的代码.

-              带有状态数据的协程,往往是藏污纳垢之处.

· 热更新

原理已经在文件沙盒一节中阐明,这里说说注意事项.

不要把持import的文件内部符号,否则在文件重新加载后可能不被更新.

如下所示,这里的代码把持了config.lua的的内部变量config.

--注意这里的cfg,在文件config.lua被热更新后将仍然是旧的.

cfg = import("config.lua").config;

print("config.txt="..cfg.txt);

文件内的"全局"变量定义,要考虑文件热更新,否则更新时可能会丢失运行时数据

这样写在热更新后会丢失数据:

-- 加入的玩家列表

-- 这样写在热更新后会丢失数据

playerTable = {};

function c2s.OnPlayerJoin(connIdx)

local ss = ssmgr.GetSession(connIdx);

playerTable[ss.acc] = os.time();

end

这样写在热更新后数据保持:

-- 这样写在热更新后数据还在

ifnot playerTable then

playerTable = {};

end

function c2s.OnPlayerJoin(connIdx)

local ss = ssmgr.GetSession(connIdx);

playerTable[ss.acc] = os.time();

end

5、项目实际中的其他问题

· 与TDR组件的适配

以client到gamesvr的上行消息为例:
        TDR 消息定义.

< span=""> name="MoveItemReq" version="1" desc="移动道具">

< span=""> name="Item" type="uint64" desc=""/>

< span=""> name="Bag" type="uint8" desc="移动到哪个包"/>

< span=""> name="Pos" type="uint16" desc="包中的位置"/>

C++通信层在收到上行的请求包后,调用中间的C++适配层函数,将消息传入lua.
        注意这里的C++适配层代码仅为示意,跟实际差别很大.
        在项目,我们通过一个python脚本自动生成这个适配层代码,无需手工编写.
        我们之所以还有这个适配层代码,是因为我们的客户端不支持lua脚本.
        对大多数项目,是无需做这个适配层的.

void OnMoveItemReq(lua_State* L, int iConnIdx, TFMsgBody& msgBdy)

{

if (!Lua_GetTableFunction(L, "c2s", "OnMoveItemReq"))

return;

lua_pushinteger(L, iConnIdx);

tagMoveItemReq& o = msgBdy.stMoveItemReq;

lua_pushinteger(L, o.ullItem);

lua_pushinteger(L, o.bBag);

lua_pushinteger(L, o.wPos);

Lua_XCall(L, 4, 0);

}

lua 业务层代码.

function c2s.OnMoveItemReq(connIdx, itemId, bag, pos)

local player = playerTable[connIdx];

-- do some thing ...

tdr.SendSyncItemData(connIdx, item);

end

·  策划表格的读取

我们通过一个 python 脚本,将 excel 文件直接转换为 lua 代码文件.

-            转换结果是文本文件,一目了然.

-            无需额外写任何加载的代码.

-            与lua逻辑代码一样方便的热更新.

index

说明

适用职业

槽位

是否消耗品

10101

机枪

1,3,5

1

0

10102

能量枪

1

2

0

20101

榴弹

2

3

1

excel 经过一个 python 脚本转换后变成这样:

weapons =

{

[10101] = {index=10101, desc="机枪", profession={1,3,5}, slot=1, consumable=nil},

[10102] = {index=10102, desc="能量枪", profession={1}, slot=2, consumable=nil},

[20101] = {index=20101, desc="榴弹", profession={2}, slot=3, consumable=true},

};

· 远程调用与持久化中的版本兼容处理

序列化数据用在远程调用和数据持久化中,就不能不提及版本兼容问题.
        实际上,由于我们的序列化数据是自描述的,所以非常易于实现版本兼容.
        比如我们旧版角色数据如下:

player

├─ lastLoginIP: 172.16.11.152

├─ name: 张三

├─ lastLoginTime: 1460514943

└─ level: 10

现在我们要在新版中增加一个任务系统,新版的角色数据像这样.
        也就是多了一个tasks的table用来记录任务进度.

player

├─ tasks

│  ├─ 12

│  │  ├─ id: 12

│  │  └─ count: 123

│  └─ 11

│     ├─ id: 11

│     └─ count: 1

├─ name: 张三

├─ level: 10

├─ lastLoginTime: 1460515197

└─ lastLoginIP: 172.16.11.152

那么,我们如何实现版本兼容呢?
        其实很简单,只需要在登录加载时做一个判断即可:

function OnLoadFromDB(player)

--没有player.tasks数据项,说明是旧版的

ifnot player.tasks then

player.tasks = {};

end

end

· 调试辅助

lua 在常被人诟病的一点是调试器不好用.
        不过以我实际体验来看,这并不是什么问题.
        顺便说一句,代码难于调试通常是实现者的问题,跟语言没啥关系:)
        但我们还是有些辅助手段.

详尽的错误日志,大部分错误通过看日志能知道基本脉络.

20151028 16:14:41: [Lua_XCall] [string "match_script/match.lua"]:288:

attempt to perform arithmetic on a table value (field 'sideA') stack traceback:

[string "match_script/match.lua"]:288: in global 'MatchAcrossBucket'

[string "match_script/match.lua"]:187: in global 'MatchForBucket'

[string "match_script/match.lua"]:181: in global 'MatchForPool'

[string "match_script/match.lua"]:545: in field 'MatchAll'

[string "match_script/main.lua"]:17: in function <[string "match_script/main.lua"]:15>

图形化的数据显示,让人直观的理解复杂数据结构,只需要简单的一句 tree.Show(data) :

20151028 16:18:05: Match sideA:

20151028 16:18:05: ├─ 1

20151028 16:18:05: │  ├─ ids

20151028 16:18:05: │  │  ├─ 1: 1000

20151028 16:18:05: │  │  ├─ 2: 1001

20151028 16:18:05: │  │  └─ 3: 1002

20151028 16:18:05: │  ├─ tag: 3.a

20151028 16:18:05: │  ├─ elos

20151028 16:18:05: │  │  ├─ 1: 800

20151028 16:18:05: │  │  ├─ 2: 800

20151028 16:18:05: │  │  └─ 3: 800

20151028 16:18:05: │  └─ svr: 1

20151028 16:18:05: └─ 2

20151028 16:18:05:    ├─ ids

20151028 16:18:05:    │  ├─ 1: 3000

20151028 16:18:05:    │  └─ 2: 3001

20151028 16:18:05:    ├─ tag: 2.c

20151028 16:18:05:    ├─ elos

20151028 16:18:05:    │  ├─ 1: 800

20151028 16:18:05:    │  └─ 2: 800

20151028 16:18:05:    └─ svr: 1

· 工程建议

-            尽量不要在 lua 中去模拟其他语言特性,如 class, 多态继承之类的.

-            适时重构,保持代码目录结构,文件划分的简单清晰.

-            一个好的编辑器不只是让编码顺畅,还能帮助我们避开很多手误.

-            协程当然可以适当的用,但一个到处都是yield的项目,最后很可能会是代码维护的噩梦.

6、回顾,问题与展望

· 回顾

通过基于lua的新框架,我们获得了哪些优势:

-            远程调用: 方便快捷的跨进程通信,无需额外做协议定义&转换.

-            序列化存储: 无需额外做数据格式定义,数据自描述,不存在数据与结构体定义不一致的问题.

-            高效率开发,无需在语言本身的特性上挣扎,把更多的精力投到业务逻辑本身上来.

-            修改代码存盘即生效,省去繁琐的编译,重启,再登录等过程,开发调试过程更顺畅.

-            彻底摆脱空指针,野指针,内存越界,妈妈再也不用担心服务器半夜宕机了.

-            运营过程中的快速热修复,更及时的运营响应.

-            降低对程序员的要求,语言简单,即学即会.

-            减少团队人员需求,降低项目成本.

· 可能的问题

动态一时爽,重构火葬场.

这话固然有些夸张,但不可否认,动态语言对如何编写高性能&易维护的代码提出了新的挑战.
所谓重构火葬场即是说如果在编码时不注重代码的易维护性,写得"太聪明",别说重构,几天后甚至自己都看不懂.
lua保障了程序的最差的情况不会宕机之类的,但是一个写不好C++的程序员,通常也写不好lua.
反之亦然,一个始终关注代码性能与可维护性的程序员,能写好C++, 更能写好lua.

性能,性能,性能

尽管lua已经是脚本语言中性能最好的,但是还是要强调一下性能.

-            尽量使用局部变量,某些情况下会比全局变量或table成员变量性能好很多.

-            注意table的填充,不同的写法性能有较大差异.

-            注意table实际上分为数组和哈希两种,性能也有差异.

-            拼接字符串是有消耗的.

-            尽量避免零碎的,临时的,大量的table,以及string.

-            对某种写法的性能有疑惑的话,除了实测,还可以查看字节码(类似汇编).

-            lua对数字很多情况下都用double实现,可以全局性的定义成int64_t提升性能.

-            读一读参考文献中的文章,写出高性能的lua代码并不难.

· 展望

没有什么技术在所有领域都是最好的,更不可能一直是最好的.
        在过去端游年代,大型MMORPG大行其道,服务端计算密集,C++是不二之选.
        而在手游时代,即使同样是MMORPG,已经很少是计算密集型了,而行业竞争却愈演愈烈.
        在现阶段,我们更需要的是一种能快速实现,快速响应的技术,lua算是一个不错的选择.
        然而,随着游戏技术与web技术的逐渐融合,谁知道哪天node.js会不会成为新的选择呢?
        作为行业技术人员,我们需要做的,便是永远保持开发的心态.

7、参考文献

-            如何编写高质量的lua代码,也有中文版

-            lua非官方FAQ,值得一看

-            一个印度人搞的lua方言,值得一看

基于Lua的游戏服务端框架简介的更多相关文章

  1. Go游戏服务端框架从零搭建(一)— 架构设计

    五邑隐侠,本名关健昌,10年游戏生涯,现隐居海边. 本教程以Go语言分区游戏服务端框架搭建为例. Go语言是Google开发的一种静态强类型.编译型.并发型.具有垃圾回收功能的编程语言.语法上近似C语 ...

  2. 分享一个C++与Python开发的中小型通用游戏服务端框架(跨平台,开源,适合MMORPG游戏)

    在开发一款游戏项目时,在立项时我们往往会考虑或者纠结很多,比如: 1,对于开发来说:服务端和客户端应该选择什么语言?用什么协议通信才更效率?协议后期如何维护?Socket是用长连接还是短连接?TCP还 ...

  3. 转:云风skynet服务端框架研究

    转:  http://forthxu.com/blog/skynet.html skynet是云风编写的服务端底层管理框架,底层由C编写,配套lua作为脚本使用,可换python等其他脚本语言.sky ...

  4. Node.js服务端框架谁才是你的真爱

    1. Express 背景: Express, 疯一般快速(而简洁)的服务端JavaScript Web开发框架,基于Node.js和V8 JavaScript引擎. Express 是一个基于 No ...

  5. 《Python》网络编程之客户端/服务端框架、套接字(socket)初使用

    一.软件开发的机构 我们了解的涉及到两个程序之间通讯的应用大致可以分为两种: 第一种是应用类:QQ.微信.网盘等这一类是属于需要安装的桌面应用 第二种是web类:比如百度.知乎.博客园等使用浏览器访问 ...

  6. 手游服务端框架之使用Guava构建缓存系统

    缓存的作用与应用场景 缓存,在项目中的应用非常之广泛.诸如这样的场景,某些对象计算或者获取的代码比较昂贵,并且在程序里你不止一次要用到这些对象,那么,你就应该使用缓存. 缓存跟java的Coucurr ...

  7. 超级好用的前端开发测试Chrome插件-基于REST的Web服务客户端

    基于REST的Web服务客户端是一款功能强大的谷歌浏览器插件,使用基于REST的Web服务客户端(模拟REST客户端)可以让用户使用谷歌浏览器模拟REST请求来测试REST风格. 基于REST的Web ...

  8. 基于 Cocos2d-x-lua 的游戏开发框架 Dorothy 简介

    基于 Cocos2d-x-lua 的游戏开发框架 Dorothy 简介 概述 Dorothy 是一个在 Cocos2d-x-lua 基础上发展起来的分支, 它去掉 Cocos2d-x-lua 那些过多 ...

  9. Pomelo:网易开源基于 Node.js 的游戏服务端框架

    Pomelo:网易开源基于 Node.js 的游戏服务端框架 https://github.com/NetEase/pomelo/wiki/Home-in-Chinese

随机推荐

  1. 题解 P4344 【[SHOI2015]脑洞治疗仪】

    前言 这道题目呢,看上去很难,实际上我们可以用线段树解决这道题目. 正文 我们维护 sum.len.tag.lmax.rmax.ans. sum 就是这段区间非脑洞的个数 len 就是这段区间的长度 ...

  2. jviisualvm监控远程主机java程序实战与问题排查

    1.远程主机运行jstatd 首先新建文件 jstatd.all.policy ,内容如下 grant codebase "file:${java.home}/../lib/tools.ja ...

  3. 【分布式锁】07-Zookeeper实现分布式锁:Semaphore、读写锁实现原理

    前言 前面已经讲解了Zookeeper可重入锁的实现原理,自己对分布式锁也有了更深的认知. 我在公众号中发了一个疑问,相比于Redis来说,Zookeeper的实现方式要更好一些,即便Redis作者实 ...

  4. w3cshool -- 排列组合去重算法挑战

    function permAlone(str) { if(str.length == 1) return str; var a = str.substr(0, 1), one = [a], count ...

  5. java 环境变量配置与第一个程序运行

    从开始下载jdk,到运行出java第一个程序 ,花了5天时间 ,不过我相信万事开头难 ,以后会越来越好的 ,加油! jdk的下载: 在oracle官网上即可下载,jdk安装包,下载完以后运行安装 ,路 ...

  6. 一文彻底读懂MySQL事务的四大隔离级别

    前言 之前分析一个死锁问题,发现自己对数据库隔离级别理解还不够清楚,所以趁着这几天假期,整理一下MySQL事务的四大隔离级别相关知识,希望对大家有帮助~ 事务 什么是事务? 事务,由一个有限的数据库操 ...

  7. SpringBoot整合Springfox-Swagger2

    前言 不管Spring Boot整合还是SpringMVC整合Swagger都基本类似,重点就在于配置Swagger,它的精髓所在就在于配置. @ 目录 1.Swagger简介 2.整合前可能遇到的问 ...

  8. C++头文件应该干的事情

    C++头文件应该干的事情 最近在写自己项目的时候,头文件老是编译错误,后来发现还是对头文件掌握不牢. 头文件应该干什么? 所谓的头文件,其实它的内容跟 .cpp 文件中的内容是一样的,都是 C++ 的 ...

  9. Java内存可见性volatile

    概述 JMM规范指出,每一个线程都有自己的工作内存(working memory),当变量的值发生变化时,先更新自己的工作内存,然后再拷贝到主存(main memory),这样其他线程就能读取到更新后 ...

  10. Jmeter压力测试笔记(5)问题原因

    压测链路是jmeter=>slb=>nginx => php=>rds 报 Too Many Connections 的原因是前端同时保持了 16000 个连接,达到实例规格的 ...