pre
- 很久没有找过工作了,之前以为需要关注的重点是经验、架构、设计、性能问题的处理;写了2年java做了微服务方式的架构设计以为是优势,没想外面大部分公司已经开始找大世界、偏重度游戏开发经验的人了,休闲游戏的经验显得没有点简单;再之前MMO的经验是个很传统的方式,做的内容也显得有点浅;一种已经和现在的游戏脱节的感觉….而且现在开始面算法题,知道原理的还好,一些突然出现的题目有点慌张没头绪,用笨拙的方法,甚至还很可能是错误的实现….是落后了不少,心态也慢慢着急了
- 记录下这些问题吧,也是成长
问题
TCP 关闭后需要等 2MSL 的时间
:
- 这个等待也就是 TIME_WAIT 状态,发起关闭的一方在发完最后一个ACK后进入 TIME_WAIT 状态,因为这时候session已经关闭了,发起方会占用着这个端口等待 2*MSL 是因为不确定 ACK 是否被对端接收到,一个MSL也就是一个报文在网络中最大的保留时间,超时会被丢弃;这里认为对端没有收到,那么对端会重发 FIN 的消息,一个FIN + 自己的ACK 两个消息在网络中最大保留时间就是 2MSL;避免延迟的数据段被相同的4元组连接收到
自旋锁、互斥锁、原子
:
自旋锁
是一个以消耗CPU不断轮训为代价更快获得锁的方式互斥锁
是sleep-wait condition 的方式,不耗CPU原子
是最小的执行单位
login服务这种没有token验证的请求,怎么防御攻击
:
- login更及时的反馈需要更快的缓存、更小粒度的锁;
- 熔断策略:对于某个IP单位时间内请求超过一定次数后,屏蔽后续请求,至少不让他影响内部服务;
- 也可以选择抵御ddos攻击的lb、cf服务作为网络入口;
- 带token的可以用相同的熔断策略,下发token的可以用令牌桶的方式限制token的下发数量。这里还有一个比较常见的场景,比如登录这种,一般会到第三方服务器做账号认证,这个过程是个异步的过程,会有一定的延迟。
场景同步的优化
:
减少双端同步量&处理量
,避免无谓的消息处理和带宽占用。场景同步的游戏中,可以考虑在玩家处于某些状态的时候不要同步那么多事件到客户端;例如在九宫格地图(玩家一屏的可见视野是9个格子)中我正在打怪,而我攻击的最大范围只有一个格子,这时候服务端同步的主要内容应该集中在这一个格子里对象的事件,对于周边的其他格子信息,可以换成更小的数据包(减少客户端解析数据&处理数据的时间消耗),或者由客户端以lazy帧的频率主动向服务端请求变化信息(当然这样相比服务端主动推送多了1次RTT)关于让客户端主动请求的题外话:
服务器端一般情况下对于每个客户端相同行为处理的消耗时间是相同的,也就是服务端会以相同的频率向客户端广播消息(排除一些时间服务端由于性能问题处理的差异),而客户端是玩家的设备,也就是不同设备支持的最高帧率、不同网络环境的延迟都是不一样的;客户端的处理事件需要一个个处理,可能A设备1档机处理一个事件1s(比较用的单位时间),B设备3档机需要3s,服务端以相同的频率给到2个客户端的消息,他们实际处理的效率是不一样的,无端的浪费了带宽;所以是不是或者至少在某些状态下,由客户端主动发起请求,服务端再回执而不是主动push到客户端表现会更好。在gateway做广播目标对象的筛选:
这里认为传统服务器单个场景是在一个gameservice的,但是单个场景的玩家是在多个gateway的,一次事件gs只需要把事件本身(也就是消息同步的message-body)广播到gateway上,由gateway做需要广播玩家的筛选,因为gateway是多组并且可以水平扩展,这样子做可以减轻gs遍历玩家的成本,也减轻gs到gateway的带宽压力(即使常规认为内网通讯是1Gbps的带宽,跑了网络传输,大包和小包影响还是很大的);这样子做就需要gateway上有一部分场景的信息,比如九宫格放到gateway上,用二维(三维)数组方式空间换时间,拿到需要广播的对象,把消息广播出去
C11一些语法
,auto和decltype, move foward区别, lamada表达式实现原理。
- auto 根据=右边的初始值 value 推导出变量的类型;变量必须初始化,也就是在定义变量的同时必须给它赋值
- decltype 根据 exp 表达式推导出变量的类型,跟=右边的 value 没有关系。
- std::move执行一个无条件的对rvalue的转化。 对于它自己本身来说, 它不会move任何东西;若对一个对象做move操作,就不要声明为 const. 因为对const对象的move请求会执行到copy操作上
- std::forward在参数被绑定为rvalue的情况下才会将它转化为rvalue
- std::move和std::forward在runtime时啥都不做
- 下面的代码,如果函数 fn 参数去掉 && 符号,将是不一样的结果,这里的 && 符号是取地址
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
struct tt {
std::string name_ = "defaulted";
tt() { std::cout << "1:" << this << std::endl; };
tt(const char* name) : name_(name) { std::cout << "2:" << this << std::endl; }
tt(tt&& rhs) { std::swap(name_, rhs.name_); name_ += " moved"; std::cout << "3:" << this << std::endl; }
~tt() {std::cout<< "destroy:" << this << std::endl;}
std::string print() { name_ += ":1"; return (name_); }
};
void fn(std::string&& s) { std::cout << "string(" << s << ")\n"; }
void fn(tt& s) { std::cout << "----lfn(" << s.print() << ")\n"; }
void fn(tt&& s) { std::cout << "----fn(" << s.print() << ")\n"; }
void fn(tt* &&s) { std::cout << "--------fn_ptr(" << (s != nullptr ? s->print() : "empty") << ")\n"; }
void fn(std::shared_ptr<tt> &&s) { std::cout << "------------fn_sptr(" << (s != nullptr ? s->print() : "empty") << "|" << s.use_count() << ")\n"; }
template<typename T>
void fwd_test(T&& t) {
fn(std::forward<T>(t));
fn(std::forward<T>(t));
}
template<typename T>
void move_test(T&& t) {
fn(std::move(t));
fn(std::move(t));
}
int main() {
tt tt_("lvalue");
fwd_test(tt_);
fwd_test(tt("source"));
move_test(tt("source"));
fwd_test(new tt("ptr"));
move_test(new tt("ptr"));
fwd_test(std::make_shared<tt>("sptr"));
move_test(std::make_shared<tt>("sptr"));
}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
262:0x7ffe8fe310e0
----lfn(lvalue:1)
----lfn(lvalue:1:1)
2:0x7ffe8fe31100
----fn(source:1)
----fn(source:1:1)
destroy:0x7ffe8fe31100
2:0x7ffe8fe31120
----fn(source:1)
----fn(source:1:1)
destroy:0x7ffe8fe31120
2:0x2279ec0
--------fn_ptr(ptr:1)
--------fn_ptr(ptr:1:1)
2:0x2279ef0
--------fn_ptr(ptr:1)
--------fn_ptr(ptr:1:1)
2:0x2279f30
------------fn_sptr(sptr:1|1)
------------fn_sptr(sptr:1:1|1)
destroy:0x2279f30
2:0x2279f30
------------fn_sptr(sptr:1|1)
------------fn_sptr(sptr:1:1|1)
destroy:0x2279f30
destroy:0x7ffe8fe310e0
rvo
: result value optimization
- https://en.cppreference.com/w/cpp/language/copy_elision 之前对 move 的使用有误区,上面一些测试作为参考,关键词 RVO 优化,编译级别的优化,受编译器影响
volatile
:
- 编译器对访问该变量的代码就不再进行优化,从而可以提供对特殊地址的稳定访问,当要求使用 volatile 声明的变量的值的时候,系统总是重新从它所在的内存读取数据
malloc 申请内存
:
- 分配的是虚拟内存,如果分配后的虚拟内存没有被访问的话,是不会将虚拟内存不会映射到物理内存,这样就不会占用物理内存了。小于 128 KB,则通过 brk() free 后不释放,大于 128 KB,则通过 mmap() free后释放
constexpr
- 是尽量在编译器处理的常量,string不可以,因为编译期无法调用string的构造函数
开发流程上避免崩溃
:
- 开发前会规定进程的职责,减小崩溃的影响面,使用vld、libasan检查内存问题,使用压测方式尽量覆盖所有功能点,使用segvcatch在运行期捕获崩溃并生成codedump(进程要不要继续需要根据项目结构来定,比如说踩坏内存那种崩溃,有可能踩坏到了别的内存,这时候再继续跑可能会有更严重的问题);上线的进程一定是高可用部署的,避免崩溃造成服务不可用这种情况。崩溃了需要有监测程序快速把进程拉起来,如果解决了问题了就更新或者热更掉,没有的话可以给功能模块加开关,先关掉这部分功能。
virtual关键字是必须的么
?什么情况必须有有这个关键字:
- 是必须的,如果没有这个关键字,子类对象转换为父类之后服务调用到子类自己重写的函数;父类的析构必须有这个关键字,不然析构的时候调用不到父类的析构。
virtual template区别
:
- virtual是动多态,template是静多态,virtual是相同的入参不同的函数实体,template是不同的入参类型,相同的处理函数实体
redis 雪崩的原因
:
- redis缓存中大量的key同时失效,此时又刚好有大量的请求打进来
….
做了一段时间的架构,这套架构确实解决了一些问题,甚至在某些方面的处理是讨巧且有效的,但是耐不住在面试中被一次次打击;做好自己的当前应该做的事情吧,希望有机会做一款满意的游戏,希望不是日常的996