Code Monkey home page Code Monkey logo

blog's People

Contributors

zuhd avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar

blog's Issues

比特币源码研读---开篇

btc.png

0x00 为啥要读

如果说是在18年以前阅读比特币代码,是价值投资的基本操作,那今年再去做这个事,绝逼是信仰了。比特币作为虚拟货币的开山鼻祖,运行至今近10年,在没有中心化结构的运营下,几乎没有出现过重大事故,相信BAT的产品也不敢吹这牛逼吧,所以作为技术人员读一读比特币代码是修炼内功的绝佳选择。由于比特币代码相比于我们之前所研发的C(B)/S结构不同,理解起来总会有一些吃力,如果能有一群水平差不多的朋友在一起互开脑洞,共同勉励,那更是一件美妙的事情。机缘巧合认识到国内第一批研读比特币代码的大神@菜菜子,经过几次沟通,他组织发起我们共同去做这个事情,在此特别感谢!

0x01 阅读思路

按照我以前的习惯,如果想去学习一门技术,那首先要去学会使用这门技术。话说学习使用比特币成本还是蛮大的,按照今天的行情BTC一枚也要4W多人民币。不过没有关系,我们还有很多其他的山寨币,操作起来也都是大同小异。利用3个月的时间,我从交易所到钱包,从公链到侧链,从炒币到各种DAPP几乎都玩了一遍。这个时候再去学习区块链的技术,知道自己该如何做信息过滤了,身边的同仁也会越来越多,遇到问题的时候,解决起来也就得心应手了。
在开始阅读代码之前,《比特币白皮书》和《精通比特币》这两份材料是必须要读的,对,必须。即使读不懂也没关系,至少从宏观上对比特币的技术有个大致的了解,脑子里有区块链模糊的样子,这是很重要的,如果一开始就从某个技术细节入手,比如密码学,P2P,很容易就走进一个死胡同,最后无疾而终。
精通比特币.jpg

0x02 环境搭建

工欲善其事,必先利其器。首先我们先选个版本,我用的是Bitcoin Core Daemon version v0.16.99.0-b1dc39d。在开始研读代码之前,我们先把环境搭好。由于常年在Windows下面做开发,我尝试过搭建bitcoin的windows环境,网上也能搜索到教程,但我不推荐大家去这么做,因为这是一个巨坑,详细就不多赘述了。我选择的系统环境是Ubuntu 14.04 Server LTS,我不太喜欢用桌面版。具体搭建的教程在互联网上能搜到@菜菜子的教程,讲的很详细,一步一步做下去就是了。IDE我尝试过几个,sublime,vscode,甚至Idea,我觉得都不够好用,最主要的是对“代码引用”这个功能支持的不好,最后只好祭出了江湖杀器Visual Stuio,但VS导入文件夹的功能不是很友好,只好自己先去Create Filter,然后再导入文件。最后我想强调一点的是关于代码的调试,可能很多朋友喜欢用Log调试,我个人还是喜欢debug,我用的是gdb,不过要注意的是,在make文件之前要修改所有目录下的makefile,把编译的优化禁止掉,也就是把g++的编译选项-O2改成-O0,这样就能跟踪代码的完整的执行流程了。
gdb.jpg

0x03 目录结构&数据结构

整个的项目的目录结构可以参考下图(图片来源于互联网)
目录结构.png

读了一下前辈的研读代码,基本都是从函数入口进行介绍的,是基于函数跳转分析的,也就是我们平时调试的callstack。我研读的方式稍有不同,我是从整个比特币的数据结构入手的。我们都知道比特币代码是基于区块链技术的,脑子里都有链表这样的一个概念,一环一环利用指针依次连接起来。如果我们知道每个节点里的数据结构,甚至是内存布局,然后再去分析每个数据结构在代码中扮演的角色,各个数据结构之间的组织方式,是Has-A,还是Is-A,按照这个思路去阅读代码,就轻松愉悦加开心了。
首先来分析chain.h和chain.cpp这两个文件,里面包含了这样的类。

CBlockIndex
CDiskBlockIndex
CChain

类的关系成员变量及关系图如下:
BlockIndex.jpg

再来分析block.h和block.cpp这两个文件,里面包含如下几个类。

CBlockHeader
CBlock
CBlockLocator

类的关系成员变量及关系图如下:
Block.jpg

从上面的分析可以看出CBlockIndex是Block的内存索引,Block的详细数据是懒加载(lazy-load),只有在使用的时候才会用硬盘数据读取。Block在硬盘序列化的数据除了类里的成员变量之外,还有一些额外的数据如下图所示,看到这些是不是很眼熟,在刚开始接触windows PE文件的时候,思路和这个也是类似的。
BlockDB.jpg

有网友总结出来的更简单粗暴的图,如下(图片来源于互联网),这个图,我很喜欢,哈哈哈。
详细.png

0x04 总结

本开篇小节主要讲述了研读BTC代码的动机和方法,立Flag去做一件事情可能会很简单,但能坚持下来是一件很不容易的事情,由于本人能力有限,文中有描述不当的地方,还请大家多多包涵。下一小节,主要讲交易的数据结构,See u then!

网络游戏服务器开发杂记---战场技能

1,CS的同步机制

在MMO游戏中,我们一般采用的是C/S的状态同步,可能会有人问道,为什么不用帧同步?帧同步当然也可以,这就要取决游戏的类型和玩法。如果是注重打击感或是对操作的实时性要求很高的游戏,如FPS或是MOBA类,这类游戏我们一般用帧同步,同步所有玩家的Input,服务器只负责广播,然后在客户端计算,这类游戏我们一般玩的是快感。另一类游戏,比如WOW,国内的梦幻西游,他们的共性就是角色拥有异常复杂的属性和技能系统,节奏也不像上类的游戏那么快,简单的说这类游戏玩的就是数据,所以要严格遵守服务器的规则。本文中战场技能的同步,主要讲状态同步,我会单独抽出一篇文章来讲FPS游戏的同步。

2,模块化开发

技能模块庞大而且复杂,不仅考验代码的架构设计,对性能的要求也是非常的高。因为在游戏的大地图战场中,战斗模块被高频率调用,一旦有热点,必然会影响玩家体验。按照功能的独立,可以给技能模块做如下解耦,拆分后模块和代码,及配置文件相映射。

2.1 技能的基础信息

包含常用的技能消耗,条件,以及其他信息。注意这个模块中有客户端和服务器共用的代码,要做独立的封装。客户端渲染的部分做出标识。需要重点考虑的是带有位移的技能设计,比如冲锋,闪烁等,有可能会有多个Hit点,在设计之初,就要考虑到有可能是list的数据,避免后期更新代码成本较高。在设计技能的时候,没有太多经验的LD有可能会遗漏一些玩法或是字段,这些程序都要有**准备,优秀的程序员也要尽量去帮LD完善一些工作,尤其是一些重要的信息,比如:
1,技能是否锁定目标
2,技能是否原地释放
3,释放技能中是否可以移动以及转向
mega技能.jpg

2.2 技能施法

在MMO游戏中技能的释放一般分为瞬发以及吟唱(搓球),瞬发的技能比较简单,带有吟唱的技能可能要多考虑一点,这里我重点强调一下延迟补偿的设计。
1,施法距离
这个时候施法者到服务器有1个RTT的延迟,有可能在这个期间,目标已经离开施法范围,这样的游戏体验就不是很好。解决这个问题,客户端和服务器都可以做延迟补偿,做法为客户端减少施法距离,或是服务器加大施法距离,delta distance = RTT*velocity,我们采用的是客户端补偿。
技能读条.jpg

2,吟唱时间
施法者A在吟唱的时候,有可能是可以被技能中断的,那么这个中断的时间区间也要做延迟补偿的。假设吟唱时间1s,B看到A开始吟唱的时候,已经延迟RTT(A)+RTT(B),当B用技能去打断的时候,服务器收到中断消息又多了一个RTT(B),所以补偿后的吟唱区间为[1s, 1s+RTT(A)+2*RTT(B)]

2.3 技能拾取目标

技能拾取要注意AOE技能的几种常见的区域类型,如矩形,扇形,圆形。在区域内pick目标的时候,要注意目标的类型,是有益类还是损益类,要根据目标的阵营类型来判断。一般是随机拾取的,并且有拾取上限。
AOE.jpg

2.4 伤害计算

这里的伤害就是指在目标头上漂的数字,是经过系列运算之后的结果。每个技能中配置了HitEvent事件,每个事件维护了上限为10的Hit点,每个点包含相应的Damage和SkillEffect,当然也是允许其中的某个为空的,比如某个Hit点只有伤害,没有Effect,这也是很常见的。再说回Damage的计算,只要照着套策划的公式就OK了,需要注意的是某些伤害,以及伤害率计算的先后顺序,比如命中率,暴击率等。另个需要注意的就是在公式的计算中,要主动扩大临时变量的值域范围,比如伤害最后的结果是uint类型,在计算过程中尽量用uint64,避免有除法运算时被取整成0的情况。
暴击.jpg

2.5 技能Effect

正如上文所说,技能Effect和damage是成对出现的,它的主要结果就是触发生成了一个operation,什么是operation呢?简单的讲就是该次技能作用后,对施法者或是目标的属性,状态等产生的更新,这里的更新内容就非常的复杂和繁琐了,不过常见的类别我们在热门的游戏中(WoW/Dota/LoL)都能碰到,我们会在下面的operation具体的章节中详细陈述。通常我们会根据策划的需求,假设出具有最多参数的operation,把这些带参数的operation做为callback传入在SkillEffect中。

2.6 Effect触发后续的operation

现在我们来详细说说operation,根据策划的经验以及MMO的日常套路,一般我们可以把operation分成这几类。
1,修改entity的状态
比如目标中毒之后会有减速效果;施法者释放技能后有狂暴效果(攻击速度加成);或是群体魔免等等。
2,修改entity的属性
比如被牧师奶了以后加了200点HP;被法师吸蓝后失去了100点MP;
3, 触发了一个buff/debuff
终于说到了buff,buff的设计本来就是一个复杂的系统,我们要搞清楚它,我们可以先搞清楚它和技能的异同点,首先肯定的是,它们是有相同的部分,也就是说部分的buff是可以设计成技能的,如瞬间加血,或是修改entity状态类的buff,这类的buff比较简单,基本上我们把技能的模板照搬过来就可以了。所以在设计这两个系统的时候,我们一般遵循buff是技能的超集这样的原则。现在来看看那些不同于技能的buff,看看他们有哪些特性。
1,buff的触发条件
2,buff的生命周期
3,buff被移除的条件
4,是否是光环类buff
5,是否可以叠加
6,下线是否保留
7,死亡是否保留

2.7 Buff基础信息

Buff的基础信息中包含该buff的大类和小类,考虑到不同buff之间的兼容或是叠加的设计,我们通常会用大小类来做进一步的判断。同时我们还要考虑到buff叠加的最大层数,如dota中蝙蝠的叠油,同时如果可叠加的buff还要考虑到新的buff是否刷新原有buff的生存周期。
蝙蝠骑士.jpg

2.8 Buff伤害

Buff的伤害我们可以重用上述的技能伤害系统。

2.9 Buff的Effect

同理BuffEffect也可以参照上述的技能Effect,但需要注意的是Buff触发的operation有可能是一个新的Buff,同时注意Buff的移除条件或是生命周期,避免陷入死循环。

2.10 召唤物,包含有弹道的飞行物

召唤物也算是技能中的一个比较复杂而且重要的模块。在游戏中有物理弹道的entity统称召唤物,当然大Boss召唤出来的小怪也可以叫召唤物,它只要创建新的creature,然后setup相应的AI就可以,这个很简单,就不做多述。我们主要来讨论有物理弹道的projectile,比如法师的火球。从本质上来讲召唤物也是一个entity,它也是有属性的,比较特殊的是它有自己的飞行速度,加速度以及生命周期,当然也有其碰撞的伤害甚至Effect。这里有2点需要特殊注意一下:
1,spawn的方式,比如出生点,角度,初速度等。
2,碰撞时的胶囊体逻辑。
导弹.jpg

3,挑战

从参与设计到编码开发,在技能模块中我还是遇到了很多的挑战。玩的MMO太少这一直是我开发该模块的最大绊脚石,这就导致很多问题想的不够深,不够透。在同事的帮助下,我仔细揣摩每个功能,主动找策划撕逼,慢慢的找到了很多感觉。现在回头看看,仍然觉得技能战场是一个充满挑战的工作。在编码之前,一定要和策划反复讨论,妥协,方案定了,approve之后再思考编码的工作。编码之前尽量从设计上多下功夫,性能暂放一遍,要让框架最大发挥它的易用性和扩展性,能做到向积木一样去拼装和重构。把各个类的之间关系梳理清楚,mock up自己也要手动去做一遍,设计上严格要求,对于自己和团队都是福祉。庞大的模块绝不是靠一人之力能完成的,要让新人能读懂你的代码以及快速上手才是合格的架构。性能在功能实现之后再去优化,用profiler去做热点分析,然后针对性的优化。以上,侵删!

FPS游戏同步机制

在开始正文之前,有个出发点非常重要,同步策略的宗旨:为了玩家的更好游戏体验--Everything for players’ better game experience。一定要牢记,后面还会提到。

一,概念

  1. authority 同步中心,通常我们说的服务器,有同步广播,纠错等功能

  2. master 玩家控制的entity

  3. replica 非玩家控制的其他entity在本地的拷贝

我们所说的同步就是指authority利用不同的策略,让replica保持和master状态同步。

二,(非)确定性模拟

根据不同的游戏类型,可以分为两大模拟策略:确定性模拟和非确定性模拟。

确定性模拟:对于不同的游戏玩家,不管在任何物理设备或外因下(机器硬件/地域/带宽),相同的输入,必然得到的输出,满足一致性。

非确定性模拟:要求较上面宽松很多,允许结果有差异性。

利用确定性模拟的游戏有我们非常熟悉的两款RTS游戏,starcraft & warcraft。总体来讲,这种游戏比较古老,受限于当时开发所处年代的硬件和带宽,这种游戏一般在局域网内体验会很好,battlenet就很糟心了。

确定性模拟的优势如下:

1,每次host广播的网络包体小,仅仅包含玩家的输入
2,游戏世界会严格同步,不会出现分歧

缺点好像更多:

1,对于不同的硬件(浮点数处理)开发和调试难度都很大,尤其是遇到bug很难重现,让程序员异常痛苦
2,一旦host上发现某一帧结果不一致,必须立即结束游戏,别无选择
3,玩家要对地图上所有的entity进行模拟计算,知道为什么澄海3C卡了吧
4,因为3,所以游戏对地图大小以及玩家数量都有限制,一般为开房规则
5,作弊变得容易,开局就口吐芬芳:挂B! ALT + Q再见
6,断线重连难以实现

三,确定性模拟同步策略

1,隐藏延迟的小把戏—延迟输入

我们在游戏里通常会遇到这样的画面,比如法师释放技能的时候搓球,战士砍人的时候把刀高高举起,有没有思考过这样做的目的是什么呢?仅仅是为了增强游戏的逼真性吗?这只是其一,其二便是我们利用前摇这段时间偷偷的把消息发给了服务器,让服务器再广播出去,打了一个提前量,用来对冲RTT的延迟。这是在游戏开发中常用的技巧,非常重要。

2,lockstep

player每一帧都会把消息发给host,host同时会广播给所有人,并发ack给player,让他继续发送下一帧,在此期间所有人都只能卡住等待最后一个人的确认消息。这样的游戏体验在war3或是dota1中我们经常能遇到,一旦有一个玩家有高延迟,所有人都会卡住,聊天频道立马就会各种问号问候全家。那在非LAN的模式下是如何保证游戏流畅运行的呢?除了要求Ping在100以下之外,还有就是上面的输入延迟起到了很大的作用。如果所有的玩家能在输入延迟之内处理掉网络消息,那么游戏就可以顺利的继续,否则有玩家有网络抖动的话,就会每隔几帧卡顿一次,一直到该玩家网络恢复正常。

d1.png

3,lockless

用lockstep固然能精确同步所有玩家的状态,但在网络抖动情况下,游戏体验太差,于是拥有7辆法拉利的游戏大神卡马克创造了lockless同步策略,这种乐观锁的**在后来的多线程或是异步事件处理中也能常常遇到。通常的做法是在host上缓存每个player的frame buffer,并允许客户端在未收到host的input1的ack之前,预判input2的行为结果,利用这段时间来对冲掉RTT,当然如果预判错误,host会无情的rollback到buffer中的历史状态。这样如果在游戏期间,player1出现了网络抖动,只要freeze住player1,其他的玩家正常游戏,再智能一点的做法就是设计一段AI代码为player1托管,让其他玩家感受不到player1的卡,尽管多数情况下这种托管都很傻。当player1网络恢复之后,就会快速播放保存在host端buffer中的Input,追赶上其他的玩家,但多数情况下player1会被干掉。为了保证游戏体验,一般会设置断线次数上限,当player1超出上限,就会被无情踢出游戏。
john carmark.jpg

四,非确定性模拟

非确定性模拟会尊重网络延迟的存在性,并通过策略的设计去妥协和弥补,以保证replica的状态不断的向master状态聚合,最终达到较好的游戏体验。它会针对玩家的延迟程度,低延迟,正常延迟,高延迟做出不同的策略。

  1. 同上,延迟输入是必要的。

  2. replica的状态永远迟于master,且replica会周期性收到的master的snapshot,周期会根据延迟自动调整,原理类似于TCP的滑动窗口。

  3. replica收到的snapshot是不连续的,需要用内插值(interpolation)让他们看起来更平滑。

  4. authority会对shooter做延迟补偿,也就是说为了对冲掉shooter的RTT,authority会让shooter以target的某个历史snapshot做为真正的目标,去进行逻辑判断。这就是为什么我们经常躲在墙角也能被击中的原因,这样做是为了照顾第一视角的游戏体验。

d2.png

  1. 延迟补偿并不能被过度的使用,这样势必会伤害到target。一般的处理办法就是在游戏中设置latency threshold,只有不高于该值才会做补偿,否则不做补偿,一句话:卡à死!对于那些更高的ping,还是早点删掉游戏为妙。

  2. 内插值是使用authority的广播数据,而且是历史数据,replica具有一定的滞后性。有一类游戏,可以使用外插值(dead reckoning)来保证replica和master之间的同步。如赛车类游戏,他们的状态相对简单,这样replica根据上一状态不断推导新的状态,并且向authority上报,一般情况下它是不关心authority的ack,除非偏离超过authority的threshold,这个时候强制同步一次即可,大大节约了带宽和时间。

racing_games.jpg

  1. 如何解决replica和master之间的偏离?

非确定性模拟的精髓就在于如何妥协偏离,它遵循的原则说的通俗一点就是:该软的时候软,该硬的时候就要硬。在网络抖动不是很大的时候,它会用滑动窗口和延迟补偿来让玩家尽量无感体验,一旦网络抖动较大,偏离超出了threshold,服务器可以立即使用拉回,穿墙,TP等不优雅的手段,强行保持一致。

五,首尾呼应

一切优化的目的:JUST FOR FUN!

GO语言绕坑指北

1,defer的使用

1.1 先判断执行没有err后,再使用defer释放资源
resp, err := http.Get(url)
// 先判断操作是否成功
if err != nil {
return err
}
// 如果操作成功,再进行Close操作
defer resp.Body.Close()
1.2 当发生panic时,所在goroutine的所有defer会被执行,但是当调用os.Exit()方法退出程序时,defer并不会被执行。

2, type assert

func foo2(key string) {
fmt.Printf("foo2=%s\n", key)
}

type Handler func(string)

func TestHandler(in interface{}) {
if v, ok := in.(Handler); ok {
v("hi")
}
}
// in main
TestHandler(Handler(foo1))

3, range channel

    ch := make(chan string)
wg := sync.WaitGroup{}

go func () {
	for m := range ch {
		fmt.Println(m)
		wg.Done()
	}
}()

ch <- "a"
wg.Add(1)
time.Sleep(10*time.Second)
ch <- "b"
wg.Add(1)

wg.Wait()

当对channel进行range遍历的时候,如果没有信号,会一直阻塞,一直等到channel被关闭或是goroutine会关闭,当main退出的时候,所有的goroutine都会被关闭,资源也被回收。当使用waitgroup的时候,如果最后的wait条件不被解开,有可能是因为死锁造成的。

4, grpc 错误处理

                    resp, err := stream.Recv()
		if err == io.EOF {
			log.Println("Server closed")
			break
		}

		errStatus, _ := status.FromError(err)
		if codes.Unavailable == errStatus.Code() {
			break
		}

		if err != nil {
			log.Println("Recv error:%v", err)
			continue
		}

5, , 可见性即对包外可见,当其他包调用当前包的变量时候是否允许可见(可访问)。

• 变量开头字符大写,表示可见
• 变量开头字母非大写,则表示私有,不可见

6, 接口

type IFile interface {
Read(buf []byte) (n int, err error)
Write(buf []byte) (n int, err error)
Seek(off int64, whence int) (pos int64, err error)
Close() error
}

Go语言可以根据下面的函数:
func (a Integer) Less(b Integer) bool

自动生成一个新的Less()方法:
func (a *Integer) Less(b Integer) bool {
return (*a).Less(b)
}

反之不行。

接口赋值在Go语言中分为如下两种情况:
1,将对象实例赋值给接口;
2,将一个接口赋值给另一个接口。
先讨论将某种类型的对象实例赋值给接口,这要求该对象实例实现了接口要求的所有方法。
只要两个接口拥
有相同的方法列表(次序不同不要紧),那么它们就是等同的,可以相互赋值。
var file1 Writer = ...
if file5, ok := file1.(two.IStream); ok {
...
}
这个if语句检查file1接口指向的对象实例是否实现了two.IStream接口,如果实现了,则执
行特定的代码。
// ReadWriter接口将基本的Read和Write方法组合起来
type ReadWriter interface {
Reader
Writer
}
这个接口组合了Reader和Writer两个接口,它完全等同于如下写法:
type ReadWriter interface {
Read(p []byte) (n int, err error)
Write(p []byte) (n int, err error)
}
由于Go语言中任何对象实例都满足空接口interface{},所以interface{}看起来像是可
以指向任何对象的Any类型,如下:
var v1 interface{} = 1 // 将int类型赋值给interface{}
var v2 interface{} = "abc" // 将string类型赋值给interface{}
var v3 interface{} = &v2 // 将*interface{}类型赋值给interface{}
var v4 interface{} = struct{ X int }{1}
var v5 interface{} = &struct{ X int }{1}

7,浮点数

因为浮点数不是一种精确的表达方式,所以像整型那样直接用==来判断两个浮点数是否相等
是不可行的,这可能会导致不稳定的结果。
下面是一种推荐的替代方案:
import "math"
// p为用户自定义的比较精度,比如0.00001
func IsEqual(f1, f2, p float64) bool {
return math.Fdim(f1, f2) < p
}

8,返回值

func (file *File) Read(b []byte) (n int, err Error)
同样,从上面的方法原型可以看到,我们还可以给返回值命名,就像函数的输入参数一样。
返回值被命名之后,它们的值在函数开始的时候被自动初始化为空。在函数中执行不带任何参数
的return语句时,会返回对应的返回值变量的值。

9,WaitGroup对象不是一个引用类型,在通过函数传值的时候需要使用地址。

10,select

在select中,如果有多个 case 都可以运行,Select 会随机公平地选出一个执行。其他不会执行。
否则:

  1. 如果有 default 子句,则执行该语句。
  2. 如果没有 default 子句,select 将阻塞,直到某个通信可以运行;Go 不会重新对 channel 或值进行求值。

11, package

go 里面一个目录为一个package, 一个package级别的func, type, 变量, 常量, 这个package下的所有文件里的代码都可以随意访问, 也不需要首字母大写。
import “a/b/c”
d.XXX()
意思是使用a/b/c这个目录下面的d包中的XXX函数。

12, sqlboiler

先在数据库里把table建好,然后去common下面运行 go generate就生成model了。Sqlboiler报错重新声明的原因是应该把所有的models放在一起自动生成一遍,要先根据mysql 中的表先生成models,然后再去添加models.go中的逻辑,即使自己新加的model,也要先生成。
// NOTE: Don't use --wipe option here for sqlboiler, we have manually added task_extension.go in ../models folder
//go:generate sqlboiler mysql --output ../models

13,slice,map

使用copy拷贝slice或是map的时候,必须要先make出来内存空间才可以进行深拷贝。
通过查看src/runtime/hashmap.go源代码发现,的确和我们猜测的一样,make函数返回的是一个hmap类型的指针*hmap。也就是说map===*hmap。 现在看func modify(p map)这样的函数,其实就等于func modify(p *hmap),和我们前面第一节什么是值传递里举的func modify(ip *int)的例子一样,可以参考分析。
所以在这里,Go语言通过make函数,字面量的包装,为我们省去了指针的操作,让我们可以更容易的使用map。这里的map可以理解为引用类型,但是记住引用类型不是传引用。

所以修改类型的内容的办法有很多种,类型本身作为指针可以,类型里有指针类型的字段也可以。
单纯的从slice这个结构体看,我们可以通过modify修改存储元素的内容,但是永远修改不了len和cap,因为他们只是一个拷贝,如果要修改,那就要传递*slice作为参数才可以。

14,WSL

在升级wsl上的framework的时候,如果升级mysql,要停掉windows上的mysql 服务。在用go generate生成数据库文件的时候,mysql的表结构要更新,sqlboiler要更新,最好把readme上的操作重新执行一遍。

15, for 陷阱

package main

import (
"fmt"
"unsafe"
)

func main() {
tasks := []func(){
func() { fmt.Printf("1. ") },
func() { fmt.Printf("2. ") },
}

for idx, task := range tasks {
task()
fmt.Printf("遍历 = %v, ", unsafe.Pointer(&task))
fmt.Printf("下标 = %v, ", unsafe.Pointer(&tasks[idx]))
task := task
fmt.Printf("局部变量 = %vn", unsafe.Pointer(&task))
}
}
这段代码的打印结果是:

  1. 遍历 = 0x40c140, 下标 = 0x40c138, 局部变量 = 0x40c150
  2. 遍历 = 0x40c140, 下标 = 0x40c13c, 局部变量 = 0x40c158
    不同机器上执行打印结果有所不同,但共同点是:
  3. 遍历时,数据的内存地址不变
  4. 通过下标取数时,内存地址不同
  5. for-loop 内创建的局部变量,即便名字相同,内存地址也不会复用

16, 构造函数

Person a(0); // 声明
a = foo(); // 赋值

Person a = foo(); // 声明
不一样,下面的只会调用一次拷贝构造函数,和const Person& a = foo(); 效果一样。

我的书单

正在读

技术类

非技术类

已完成

技术类

非技术类

想读

技术类

非技术类

Docker中搭建本地的ELK

sudo docker run -d --name elasticsearch --net elastic -p 9200:9200 -p 9300:9300 -e "discovery.type=single-node" elasticsearch:7.13.3

sudo docker run -d --name logstash --net elastic -p 127.0.0.1:5044:5044 -v /home/hefehoo/elk/logstash.conf:/usr/share/logstash/pipeline/logstash.conf logstash:7.13.3

sudo docker run -d --name kibana --net elastic -p 127.0.0.1:5601:5601 -v /home/hefehoo/elk/kibana.yml:/usr/share/kibana/config/kibana.yml kibana:7.13.3

//sudo docker run -d --name filebeat --net elastic -v /home/hefehoo/elk/filebeat.yml:/usr/share/filebeat/filebeat.yml docker.elastic.co/beats/filebeat:7.13.3

echo '{"message": "hello world"}' | nc localhost 5044

也可以用docker compose,更方便

docker-compose.yml

version: '3.7'
services:
  elasticsearch:
    image: docker.elastic.co/elasticsearch/elasticsearch:7.14.0
    container_name: elasticsearch
    environment:
      - discovery.type=single-node
    ports:
      - "9200:9200"
      - "9300:9300"
  logstash:
    image: docker.elastic.co/logstash/logstash:7.14.0
    container_name: logstash
    volumes:
      - ./logstash-config/:/usr/share/logstash/pipeline/
    ports:
      - "5000:5000"
  kibana:
    image: docker.elastic.co/kibana/kibana:7.14.0
    container_name: kibana
    ports:
      - "5601:5601"

logstash-config/logstash.conf

input {
  tcp {
    port => 5000
    codec => json_lines
  }
}

output {
  elasticsearch {
    hosts => ["elasticsearch:9200"]
    index => "logs"
  }
}

docker-compose up -d

网络游戏服务器开发杂记---登陆和角色列表

0x00 回顾

上一篇文章中我们讲到了游戏中的区服划分,其中也提到了一些账号登陆的流程。那这个小节我们好好梳理一下登陆的流程,以及登陆中遇到的一些异常情况,到最后的成功获取角色列表。

还记得我们上个小节中讲到的,账号密码验证成功后,登陆网关服务器(LoginGateServer)会获取到这个ack的信息,并且向中心服务器(CenterServer)去查询当前账号状态,并由CenterServer分配下一阶段的路由。这里大概会遇到以下的几种异常:

1,账号异常

这里的异常一般是针对账号安全所做的一些保护措施,可以做分级处理。举个例子,当发现与上次登陆的城市不同的时候,或是与上次登陆的设备ID不同的时候,就要给玩家发出安全确认的请求,常见的如各种奇葩的验证码,或是手机pin码,游戏采取实名制之后,后者最为常见。

另一种异常是运营中的常见手段,如封号,或是禁言等,当然也可以针对角色来做,那样更灵活,不过以国内的网游环境来讲,一般就是直接封账号,没啥好商量的。一个角色出现问题,同一个游戏世界内的其他角色都会受到牵连。

2,游戏人数拥挤,没有坑位

CenterServer会定时的去采集每一个与游戏服务器相连的网关服务器(GameGateServer)的负载情况。如果有空闲的坑位,CenterServer会把新的链接路由过去。如果当前游戏很火爆,如开服的第一天,很可能所有服务器都没有坑位了,那么CenterServer就会把当前的链接放到一个队列里,并且每隔一段时间会把当前队列的状态广播给所有的排队玩家,大家最关心的就是前面还有多少人。

如果当前账号一切正常,那么中心服务器会给当前链接分配一个新的游戏网关GameGateServer,包括这个服务器的ID,IP,Port,用户就会尝试去链接该GameGateServer,连接成功后,原来与LoginGateServer的链接就会关闭掉,空出资源。
到此为止,账号处理的相关流程,基本完毕,开始进行游戏内的数据加载。当然第一步就是加载当前游戏世界内的所有角色信息。
流程图

0x01 角色列表

我们回忆一下,在通常的角色列表界面,我们能看到什么?

1,角色名称
2,角色等级
3,角色性别
4,角色职业
5,角色装备
6,角色时装

这些是最基础的,通常也是必须显示的,其他还有一些策划会按照自己的习惯来要求的,各不相同,比如有:

1,角色工会
2,角色VIP等级
3,角色宠物
4,角色坐骑
5,...

那有哪些字段我们不需要呢?

1,角色属性
2,角色货币信息
3,角色好友信息
4,角色邮箱信息
5,...

类似于这种只有在游戏场景中才使用到的字段,在当前界面并不需要展示,所以也不用添加在通讯协议中。
wow
针对上面所述,我们应该也能设计出前后台的通讯协议了。

还有一些我们必需的字段,是我们看不到的,比如角色上次下线时所在的地图ID,以及在该地图的坐标(X,Y,Z),角色再次上线,我们要把它摆放在原来的位置。当然这是下一步的事情,到目前为止,我们已经登陆成功,并且把角色列表成功显示,本小节的任务也就完成了。

#0x03 总结
游戏的登陆逻辑不复杂,但需要注意的点会很琐碎,异常分级要做配置,避免硬编码。最后,留一个小问题,就是在游戏中什么时候需要重新验证登陆,什么时候只是切换服务器连接。这个问题考虑清楚了,对登陆流程也就没什么问题了。下一个小节,我们来探讨一下角色场景加载。

清华大学OS

一,分段
1,进程中的逻辑代码段,如数据段DS,代码段CS。
会通过段映射算法,映射到物理内存的段,然后再通过offset得到真正物理地址。

二,分页
1,页frame的尺寸是固定的,段不是。
2,通过page table,查找page number对应的frame number。
3,page size = frame size,但是total page size > total frame size,会使用虚拟内存。
4,TLB=translation look-aside buffer,存储在CPU中。

三,虚拟内存
1,覆盖技术,只限于同一个程序内部,函数的覆盖。由程序控制。
2,交换技术,用于不同进程的swap out和swap in。由操作系统控制。
3,虚存技术不同于交换技术,它不是针对整个进程进行交换,而是针对进程中的某一部分。
4,虚存技术要求程序本身满足局部性,即空间局部性,时间局部性,尽量少的产生缺页中断,减少交换次数。
比如int data[1024][1024]
// 产生1024次缺页中断
for i = 0; i < 1024; i++ {
for j = 0; j < 1024; j++ {
n = data[i][j]
}}

// 产生1024*1024次缺页中断
for j = 0; j < 1024; j++ {
for i = 0; i < 1024; i++ {
n = data[i][j]
}}
5,
image
6,缺页中断处理
image
7,代码段,动态加载段,如DLL,资源文件等会被放在硬盘上。
8,
image

9,用户线程操作系统看不到,os只能看到这个线程所属的进程,所以如果这个用户
线程阻塞,则整个进程阻塞。goroutine却是在用户态实现了多线程,且当前go阻塞的时候,当前的OS线程不阻塞。

网络游戏服务器开发杂记---开篇

离开盛大游戏已经有几个月了,毕业后这10年,一直奋斗在网络游戏的一线开发,负责过的几个产品成绩好坏参半,有的被腾讯代理的,也有打包出售的,也有的走一些小渠道赚了一笔快钱。这些年,我一直从事的游戏服务器开发,不管是底层的网络库编写,还是顶层的业务逻辑,都参与过,略有一些心得。打算花一些时间来整理下,就当是对自己的游戏生涯做个总结吧。

说起服务器开发这个方向,内容杂而多,学习路线较长而且要求有严谨的治学态度,但阶段性的成就感也很明显,比如3年前你解决了
c1K的问题,今天开始朝C10K的方向努力,中间无论是对语言的理解,操作系统的掌握,还是对网络协议的分析都会有明显的提升。
比起前端技术的日新月异,服务器显得保守甚至陈旧,但其深度的钻研不是说能一蹴而就的,要求能耐得住寂寞。

废话不多说,开篇第一章节,如何选择适合自己的服务器架构。首先明确自己做的是什么规模的游戏,比如按照运行的终端,我们可以分为端游,页游,手游,当然也有多端互通的,这里如果是同一家公司运营的同一款游戏,我们通常认为复杂度的关系是: 端游>页游>手游。比如梦幻西游的端游框架,通常会比手游更复杂,因为它的模块会更多更全,手游上会做适当的裁剪与阉割。从另一个维度来划分,也就是游戏的类型是什么,是MMORPG,还是ARPG,还是MOBA等,这里可能最复杂的就是MMORPG了,因为它包含的模块非常多,而且他还会有大地图,对服务器的性能考验会更加严格。由于本人涉及到的主要是MMORPG和ARPG,以及房间对战类,我会针对这两几类型的游戏做重点的讲解。

首先来看一下这些类型游戏之间的差别和侧重点。

  • MMORPG:多人同时在线,有能承载多人的大地图,C/S状态同步,服务器计算要求高,客户端能接受一定程度的延迟。
  • ARPG:主要玩法是游戏的打击感和节奏,伤害计算由客户端完成,无需状态同步,但需要随机的防作弊校验。
  • 房间对战类:低频互动,可以C/S状态同步,如卡牌类;高频互动,可以UDP的帧同步,如MOBA类,FPS类游戏。

以上从轮廓上描述了这几种类型的游戏,由于业务场景的不同会导致整体的游戏的架构略有不同,但是他们之间还是有很多的共同点的。相信做过游戏的朋友经常听说这样的几个名词,比如:网关服务器,游戏服务器,地图服务器,中心服务器,帐号服务器等等,这样类似的名词。是的,几乎所有的游戏服务器框架都或多或少出现这几个通用的模块,具体每个模块承载着什么样的功能,以及该如何设计,在接下来的几个章节,我会一一陈述。

这里我用processon画一下大家最熟悉的登录和游戏流程部分,当然内部的细节远不止这些:
服务器


最后想说的,游戏行业在资本的驱动下,有点偏离初衷了,最近有点审美疲劳了,打算暂时告别一下,当初做这个决定的时候也是挣扎了很久,人生就要不断的试错,不是吗?离开不代表分别,故有了做这个知识梳理的冲动,由于本人水平有限,难免有错误或是不严谨的地方,还请大家多多指教,下个小节见!

2019年小结

2018年断更了一年,想必是有很多不如意的地方,人总是记喜不记忧。但我翻了一下相册,感觉还是有很多值得记录的片段。今年我及早的在纸上打了个草稿,找时间再摘抄下来。理科生的思维还是喜欢列成List,尽管一目了然,但缺少了感情流露。话不多说,我分成工作,学习,生活三个部分来描述吧,如下。

工作

今年到了U工作后,在整体的工作感受和以前的国内企业还是有很大不同的。满意的地方就是对工作内容和技术探索的自由,不满意的地方就是工作的饱和度有欠缺。总体来说我觉得3A游戏公司的沉淀真的不是国内快餐游戏能同日而语的,我想这里的原因还是中西方的价值观念的差异吧,不管是从下而上,还是从上而下,都能看出一个公司对游戏品质的重视。

  • 参加了一次workshop,对项目的当前进度有了更清洗的认识,也知道了what on who
  • 开始了co-dev的漫长征程
  • 通过T的开发需求,对部分的游戏逻辑有了更深刻的理解,尤其对使用Perforce的工作流程更加熟练
  • 坚持阅读每一封工作邮件,对项目的pipeline也有了一定的认识,感受了大型的多人异地团队如何进行teamwork
  • 英语的阅读和口语能力得到一定提升,但速读提取的信息量还不够
  • 参加了公司的培训和GameJam,但是收获不是很大

学习

今年的学习比以前更有目的性,这也是一把双刃剑,知道自己的心之所向,不易迷失,但欲念也会与之俱增,只有心中无剑,才会大道至简。比较庆幸的是通过自律,使学习时间得到了保证,但效率却不尽人意,还是会有主动中断,通过反思最大的原因是手机上的微信消息,接下来会清理微信,尤其是一些无关紧要的群,给生活做减法。

  • 坚持周末带娃泡图书馆,不知不觉Leetcode 刷了135道题目,amazing
  • ML和Kaggle取得一定的成绩,但距离实战还有一大截路
  • Python的编程技能得到较大程度的提升
  • Docker和Hadoop的实战处于初级水平
  • Tushare交易接口以及CTP交易接口的实战,处于初级水平
  • 宽客阅读完毕,数学之美30%
  • 交易系统课程完毕,数学分析课程20%,ML课程10%
  • 世界格局课程80%,阅读宏观经济,培养自己对信息的敏感及萃取能力
  • 对RTMP以及WebRTC有初步认识
  • 老友记纯英文字母版30%

生活

值得庆幸的是今年我在家庭以及生活上投入了更多的时间和精力,也算是能达到了工作和生活的平衡了。不管任何时间,发生任何事情,家人永远是第一位的,我以后也会铭记这句格言,努力让自己的家人过的更幸福,也让自己睡的更踏实。但自己也还是常会在一些不重要的事情上庸人自扰,活得不够洒脱,仔细分析下来我还是太在意别人对自己的看法。我会有针对性去改变自己。

  • 最值得一提也许就是帮助周老师把培训班顺利开办起来,终于让周老师找到了自己喜欢做的事情,看她每天的开心充实,我也很开心
  • 上海植物园一日游,认识了奇花异草,感受了大自然的馈赠
  • 荣幸获得了和美家庭称号,受邀参加了元和的六一庆典
  • 顶着酷暑游玩了苏州动物园,被动物欣赏
  • 回徐州协助父母张罗某人的亲事,顺便去钓鱼了,还下水游了个泳,凉!
  • 健身(只坚持3个月),画画(只去了3次,都不配坚持这2个字),所以认清楚自己很重要
  • 和大磊哥哥一家游玩了南京博物馆,川菜馆真好吃
  • 夏天的每个周末都坚持陪Amy打篮球,当作事业来做啦
  • 陪老娘上海度假一周,广场的喷泉相当酷炫
  • 陪家人苏州留园一日游,照片拍的很美
  • 终于拔掉了智齿,补了牙齿,贵,但终于可以放开吃日料了
  • 陪全家人去南京,游玩了南京中山陵,很适合徒步的一个地方
  • 陪家人去了一次CBA Live,Amy说吴冠希好帅,但我觉得吧,算了,我没什么觉得
  • 终于去阳澄湖吃了一次大闸蟹,但体验一般
  • 杭州宋城
    最后重点写一句,睡的好,感觉真的好棒!

MicroK8S

1,https://microk8s.io/docs/registry-images
先按照官网上的教程去做。
su - $USER
2,microk8s kubectl proxy --address='0.0.0.0' -p=8001 --accept-hosts='^*$'
如果需要在外网访问,还需要SSH转发
ssh -L localhost:8001:localhost:8001 -NT [email protected]
http://localhost:8001/api/v1/namespaces/kube-system/services/https:kubernetes-dashboard:/proxy/#/login
3, microk8s kubectl get pods -n kube-system
4, microk8s.kubectl -n kube-system get secret
找到对应的dashboard,然后
microk8s.kubectl -n kube-system describe secret kubernetes-dashboard-token-4rtxd
把红色的部分换成相应的部分。
eyJhbGciOiJSUzI1NiIsImtpZCI6IklhSzlENllHX1A3b1NvcHdFZ3RBSEZ1ZFJNZ1M1dkxXMUhfZDBzczZTdTgifQ.eyJpc3MiOiJrdWJlcm5ldGVzL3NlcnZpY2VhY2NvdW50Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9uYW1lc3BhY2UiOiJrdWJlLXN5c3RlbSIsImt1YmVybmV0ZXMuaW8vc2VydmljZWFjY291bnQvc2VjcmV0Lm5hbWUiOiJrdWJlcm5ldGVzLWRhc2hib2FyZC10b2tlbi1ubmR4biIsImt1YmVybmV0ZXMuaW8vc2VydmljZWFjY291bnQvc2VydmljZS1hY2NvdW50Lm5hbWUiOiJrdWJlcm5ldGVzLWRhc2hib2FyZCIsImt1YmVybmV0ZXMuaW8vc2VydmljZWFjY291bnQvc2VydmljZS1hY2NvdW50LnVpZCI6IjE0YTZiMDcyLWU2YWMtNDNiZS1hMGZkLWU0NjhkYjY0YmYxMCIsInN1YiI6InN5c3RlbTpzZXJ2aWNlYWNjb3VudDprdWJlLXN5c3RlbTprdWJlcm5ldGVzLWRhc2hib2FyZCJ9.o5rGGdbTrIXU0Ku7jN3VznS7CELJAj495obahZOmV0HHI1eRfw9KYGj-YCvlyyFPVDd6iwunbVSi6ALmA2AloBz7QxMB_S2Kqze_7gDMoJ3Vd_o8OCywwMliogHBAhYHzVw_5ZRnJncyXZJTU2CYP3iP_jLasU1Aj7B87-XsC6JnjFwUSMT-R-HxAILZXHla5FfjPSIM4Wk1RRwxLOBaNrC0V3ij9JUId19R2UYuJxOHZHm7ZaGnxciskRo3RcPTtKGFtviJAbxP-LE3pwvaJn5VLysWLxIrAnEaZyyCj8AkJN-T1vkSdK_q2i2xCOcbMstFUNnszAT9WBc1xByM9A
5,按照4的操作后,生成了token,然后把token复制粘贴在网页上。
6,container中的pod的端口是pod之间相互通讯使用的,service中的port是用来监听request的,targetport是用来朝应用程序监听端口转发的。
7,发布deployment的模板:
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment
labels:
app: nginx
spec:
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: localhost:32000/mynginx:registry
ports:
- containerPort: 80

8,发布service的模板:
apiVersion: v1
kind: Service
metadata:
name: camunda-platform-service
spec:
selector:
app: camunda-platform
ports:

  • protocol: TCP
    port: 8080
    targetPort: 8080

9,在vmware中装tools,首先设置让cdrom加载安装目录下面的linux.so文件,这个时候还要通过命令行去运行。
Cd /mnt
Sudo mkdir cdrom
Sudo mount /dev/cdrom /mnt/cdrom
cd /tmp
tar zxf /mnt/cdrom/vmware-linux-tools.tar.gz
sudo umount /mnt/cdrom
cd vmware-tools-distrib
sudo ./vmware-install.pl
然后重启即可。
10, docker build . -t localhost:32000/mynginx:registry
要求当前目录下面必须要有docker file文件Dockerfile, 可以把可执行文件放在当前目录中,Dockerfile中必须要有Entrypoint。
11, 注意service和pod,以及nodes的概念。Nodes通常是指物理机器的节点,pods通常指的是某个service下面的多个服务,service通常是个桥梁的作用,它有对外暴漏的接口,有映射内部服务的接口。
12, 常用k8s对象
12.1. deployment
主要用于部署pod,支持滚动升级。
12.2. service
服务定义,主要用于暴露pods容器中的服务。
Service是K8S服务的核心,屏蔽了服务细节,统一对外暴露服务接口,真正做到了“微服务”。举个例子,我们的一个服务A,部署了3个备份,也就是3个Pod;对于用户来说,只需要关注一个Service的入口就可以,而不需要操心究竟应该请求哪一个Pod。优势非常明显:一方面外部用户不需要感知因为Pod上服务的意外崩溃、K8S重新拉起Pod而造成的IP变更,外部用户也不需要感知因升级、变更服务带来的Pod替换而造成的IP变化,另一方面,Service还可以做流量负载均衡。
12.3. ingress
http路由规则定义,主要用于将service暴露到外网中
12.4. ConfigMap
主要用于容器配置管理。

4,statefulset主要用于有状态的服务,比如mysql,当启动主从的mysql的时候,启动的pod的序号Ordinal会递增,所以
可以用0做master,用1做slave。先创建一个pvc,K8s会根据pvc生成pv,在pv中就可以双向绑定,把pv在k8s中的地址绑定到mysql的存储目录。然后在deployment中的时候就定义,如果序号为0就执行master.cnf,否则执行slave.cnf。
image

image
12.5 Pod(容器组)总是在 Node(节点) 上运行。Node(节点)是 kubernetes 集群中的计算机,可以是虚拟机或物理机。每个 Node(节点)都由 master 管理。一个 Node(节点)可以有多个Pod(容器组),kubernetes master 会根据每个 Node(节点)上可用资源的情况,自动调度 Pod(容器组)到最佳的 Node(节点)上。
每个 Kubernetes Node(节点)至少运行:
• Kubelet,负责 master 节点和 worker 节点之间通信的进程;管理 Pod(容器组)和 Pod(容器组)内运行的 Container(容器)。
• 容器运行环境(如Docker)负责下载镜像、创建和运行容器等。

要在集群中使用主节点的registry,需要编辑配置文件:/var/snap/microk8s/current/args/containerd-template.toml,将主节点的IP填入:
image
• 上述修改docker配置的操作,需要对集群的所有节点均进行修改
• 同时在主节点的/etc/docker/daemon.json中加入:
• {
• “insecure-registries”:[“localhost:32000”, “10.0.3.11:32000”]
• }
• 如果docker是snap安装的,则修改配置文件:/var/snap/docker/current/config/daemon.json
• 重启docker: sudo snap restart docker 或者 sudo systemctl restart docker
• 重启microk8s: sudo microk8s stop && microk8s start
• 之后推送镜像:sudo docker push 10.0.3.11:32000/awesome-project
• 部署镜像使用:image: '10.0.3.11:32000/awesome-project:latest',这样集群中所有节点均可拉取到镜像
• 如果集群中从节点还是获取不到主节点的镜像,重启对应节点的机器即可。或者整个集群机器重启。

13, GRPC
protobuf的命令使用
protoc --go_out=plugins=grpc:protos protos/greeting.proto
plugins=grpc是使用grpc的插件
后面的protos是生成的文件目的路径
Protos/greeting.proto是源文件

14,要保证高可用性,HA,就需要有多节点,同时要求强一致性。
R+W>N
• R
执行读取操作时所需联系的节点数R
• W
确认写入操作时所需征询的节点数W
• N
复制因子N

CThread的子类销毁时安全隐患

假设有以下的类的继承关系
CThread<--CThreadBase<--CMyThread
他们的构造函数和析构函数执行顺序如下:
construct CThread subobject (ThreadBase and MyThread do not exist yet)
construct ThreadBase subobject (MyThread does not exist yet)
construct MyThread (the object is fully constructed now)
destruct MyThread (MyThread does not exist anymore)
destruct ThreadBase subobject (ThreadBase does not exist anymore)
destruct CThread (the whole object has been destroyed)

struct CThread
{
void* vtbl_ptr;

CThread()
{
this->vtbl_ptr = CThread::vtbl;
RunUserCode();
}

virtual ~CThread()
{
this->vtbl_ptr = CThread::vtbl;
// try to block here when destruct, but NOT work
Join();
}

virtual void Run() = 0;

void StartThread()
{
// ask the OS to create a thread and within that thread do
ThreadEntryPoint();
}

void Join()
{
// wait until the OS spawned thread leaves the 'Run' function invoked in the 'StartThread' function
}

void ThreadEntryPoint()
{
this->Run();
}
};

CThread::vtbl[] =
{
&CThread::~CThread,
&pure_virtual // entry for "virtual void Run() = 0"
};

struct ThreadBase : CThread
{
ThreadBase()
{
this->CThread();
this->vtbl_ptr = ThreadBase::vtbl;
RunUserCode();
}

virtual ~ThreadBase()
{
this->vtbl_ptr = ThreadBase::vtbl;
RunUserCode();
this->~CThread();
}

virtual void Run()
{
this->ThreadRun();
}

virtual void ThreadRun() = 0;
};

ThreadBase::vtbl[] =
{
&ThreadBase::~ThreadBase,
&ThreadBase::Run,
&pure_virtual // entry for "virtual void ThreadRun() = 0"
};

struct MyThread : ThreadBase
{
MyThread()
{
this->ThreadBase();
this->vtbl_ptr = MyThread::vtbl;
RunUserCode();
}

virtual ~MyThread()
{
this->vtbl_ptr = MyThread::vtbl;
RunUserCode();
this->~ThreadBase();
}

virtual void ThreadRun()
{
}
};

MyThread::vtbl[] =
{
&MyThread::~MyThread,
&ThreadBase::Run,
&MyThread::ThreadRun
};

Now lets walk through it to see what happens:

MyThread* t = new MyThread();
t->StartThread();
delete t;

在多线程的执行环境中,执行情况如下:

应该改成:
MyThread* t = new MyThread();
t->StartThread();
t->Join();
delete t;

总结:
在上述示例中,由于析构函数为虚函数,所以在调用子类的析构函数时候,必然要查询虚表,需要注意的是在
析构函数中编译器对虚表的重新赋值操作,这样如果子线程调用虚函数,查询虚表有可能会访问错误的
函数地址,甚至是对虚表的访问越界。所以在多线程中,如果都是同时读取虚函数不会有什么问题,但如果是有写操作
比如在析构函数中对虚表的操作,这个时候就要注意了,需要对读操作或写操作进行加锁,用来保证一次完整的原子操作。

go中的拦截器技巧

package main

import "fmt"

type TestHandler func (*TestContext) error
type TestMiddleWare func (TestHandler) TestHandler

type TestContext struct {
	handler TestHandler
}

func (c *TestContext) OnResult() error {
	fmt.Println("on result")
	return nil
}

func (c *TestContext) WithMiddleWare(middleWare ...TestMiddleWare) *TestContext {
	lastHandler := TestHandler( func (c *TestContext) error {
		return c.OnResult()
	})

	for i := len(middleWare) - 1; i >= 0; i-- {
		lastHandler = middleWare[i](lastHandler)
	}

        // middleware FIFO,c.handler=midddleware[0], middleware[0]的next是middleware[1]
	c.handler = lastHandler
	return c
}

func (c *TestContext) Run () error {
	if c.handler != nil {
		return c.handler(c)
	}
	return c.OnResult()
}

func main() {
	c := &TestContext{}
	mid1 := TestMiddleWare( func (next TestHandler) TestHandler {
		return func (c *TestContext) error {
			fmt.Println("middle ware 1")
			return next(c)
		}
	})

	mid2 := TestMiddleWare( func (next TestHandler) TestHandler {
		return func (c *TestContext) error {
			fmt.Println("middle ware 2")
			return next(c)
		}
	})

	mid3 := TestMiddleWare( func (next TestHandler) TestHandler {
		return func (c *TestContext) error {
			fmt.Println("middle ware 3")
			return next(c)
		}
	})
        // should be optimized
	c.WithMiddleWare(mid1, mid2).WithMiddleWare(mid3)
	err := c.Run()
	if err != nil {
		fmt.Println("error")
	}
}

网络游戏服务器开发杂记---区服管理

0x00 分区的概念

经常玩游戏的朋友都知道,下载完游戏,注册完账号,就要进入选择区服了,比如以前双线机房还不普及的年代,会看到电信1区,电信2区。现在的游戏都是双网环境了,常见的分区如手Q1区,微信1区等。
wow

0x01 分区服务器的设计

先来解决第一个问题,这些区服的列表信息如何获取。一般的解决方案就是使用http请求来返回json数据,我们称这个服务器为索引服务器,这个索引服务器可以做成多点负载均衡,返回的json数据可以通过配置或是数据库文件来完成,它是能满足热跟新和多点容灾切换的,简单的说它就是一个微服务。设计如下图。
WebSever

0x02 区服服务器的连接信息

每一个区服背后会关联1个或是多个网关服务器,这些网关服务器会和账号中心AccountServer保持连接,同时也会和服务器监控中心CenterServer保持连接。AccountServer做的功能非常的简单专一,就是对客户端发过来的username和md5(password)做匹配校验,并把校验后的结果返回给GateServer,再由GateServer返回给玩家。当玩家的用户校验成功的情况下,GateServer还会通知CenterServer,当前的玩家的账号验证通过,并且让CenterServer按照一定的算法生成一个动态的Token保存下来,并且把该Token发给客户端保存下来,做为每次切换服务器的凭证,在玩家下线之后该Token失效,下次上线的时候重新生成。同时CenterServer还兼任着管理账号状态的功能,比如该账号是否有异常等。简单的讲,就是客户端每次在网关发起切换场景的时候,如果发生了切换进程行为,都必须要携带Token进行二次验证。这样做的目的是什么呢,我们假设这样一个场景,当玩家停留在角色选择界面,没有进入到一个具体的场景,同时通讯协议被破解了,而且没有Token校验,那外挂就很容易绕过登陆直接登陆场景了。
服务器

大区分好了,那里面的服又是怎么划分的呢?我们现在通常看到的大区里面通常都是一组服务器,也就是说该区只划分了一个游戏世界。这里所说的游戏世界,就是一个账号的角色所产生的所有数据。这样说起来可能还是有点绕口,简单概括就是当前的流行的微信1区,其实这个1区就只有1个游戏世界,因为他们并没有做多服的分配,如果按照大型游戏的做法,1区内还是可以划分为1服,2服的,当然1服和2服加起来就是1个区下面有2个游戏世界了。

0x03 内部服务器构成

那1个游戏世界里又有哪些服务器呢,这个要看游戏的类型和复杂程度了,通常的做法是划分为网关服务器GateServer,游戏服务器GameServer,地图管理器MapServer,日志服务器LogServer,数据库代理DBProxy,关系服务器RelationServer,充值服务器BillingServer,当然也有做的更加细分的,如战斗服务器BattleServer,角色信息服务器InfoServer等等。这些可以根据架构师的经验和对项目的运营周期评估做出合理的剪裁,没有最好的架构,只有最适合自己的。

总结一下,今天讲的区服管理是进入游戏前的第一步,它的逻辑简单容易理解,接下来的计划就是模拟正常玩家的游戏生命周期来逐个环节的分析,遇到重点的模块会单独的花点时间分析,比如任务,战场,副本等。下一个章节我们讲解账号和角色管理。

构造函数和operator =的tricky时刻

const CObj& a = obj.foo(); // 注意 这是一个常量的初始化,初始化的时候,只会调用构造或是拷贝构造函数,这里的a就是临时变量的引用。
a = obj.foo(); // 这是变量的赋值,会调用operator =
eax 返回值
ecx this指针
C++有返回值的函数,比如对象,不管是否有值来接受,如果返回的是对象值,都会调用一次拷贝构造函数,把这个对象构造在
eax中,eax一般会lea eax, ebp[xxx],也就是说这个临时对象已经分配在内存里,这也就是为什么 const CObj& a = obj.foo();
是一个初始化,因为它就是用ebp中的值来初始化这个obj。

const CObj a = obj.foo();
004573C7 lea eax,[a] // 直接用a的空间来接受返回值
004573CA push eax
004573CB lea ecx,[obj]
004573CE call CObj::foo (0451168h)
004573D3 mov byte ptr [ebp-4],2

const CObj& a = obj.foo();
003073C7 lea eax,[ebp-44h] // 申请一块临时变量用来接受返回值
003073CA push eax
003073CB lea ecx,[obj]
003073CE call CObj::foo (0301168h)
003073D3 mov byte ptr [ebp-4],2
003073D7 lea ecx,[ebp-44h]
003073DA mov dword ptr [a],ecx // a为这块临时变量的引用

比特币源码研读---交易

0x00 废话

距离上次开篇已有半个多月了,平时晚上回家又懒,周末回家还要带娃,研读代码工作进展很慢,趁今天出差的路上又对代码梳理了一下,趁热赶出这篇文章。热情还在,只是对自己要求在降级,从产出高质量的文章,降级到但求没有错误(最怕误人子弟啊)。调试环境已从linux上gdb降级到windows上的 visual gdb;为了能一睹作者最初的设计**,版本号也从0.14降级到0.8,隔离见证以及闪电网络的研读部分暂时不在范围之内。废话不多说,今天我们研读一下比特币的核心部分---交易。

0x01 UTXO

在比特币交易中有一个非常重要的概念UTXO(Unspent Transaction Output),也就是说比特币用UTXO取代了传统的账号系统。这句话如何理解呢,我们做个对比就知道了。假设A,B2位矿工分别挖到区块,获得coinbase奖励25btc,然后均转给C,C又转40个BTC给D。那么传统的账号系统,如下图:
传统账号

UTXO的流通方式如下:
UTXO

做过数据运维的朋友可能会说,UTXO不就是数据库里的日志表嘛?此话不假。我们知道银行或是其他的类似系统,不仅要对数据结果做落地入库,还要对交易记录做严格的管理,一旦发现数据异常,就要通过交易记录逐笔分析。所以严格意义上的讲,数据结果表是冗余的,因为结果是可以通过过程演算出来的,而且记录更容易查到那些作弊的人。当然记录推算结果也是要付出代价的,比如要消耗大量的计算,同时对记录的完整性有非常高的要求,不能有任何一条记录出错,否则全盘出错。比特币经过去中心化的分布式存储,以及共识机制的改良,把UTXO的**发挥到了极致。

我们再来看一下master bitcoin的书中对UTXO的描述:

比特币交易的基本单位是未经使用的一个交易输出,简称UTXO。UTXO是不能再分割、被所有者锁住或记录于区块链中的并被整个网络识别成货币单位的一定量的比特币货币。比特币网络监测着以百万为单位的所有可用的(未花费的)UTXO。当一个用户接收比特币时,金额被当作UTXO记录到区块链里。这样,一个用户的比特币会被当作UTXO分散到数百个交易和数百个区块中。实际上,并不存在储存比特币地址或账户余额的地点,只有被所有者锁住的、分散的UTXO。“一个用户的比特币余额”,这个概念是一个通过比特币钱包应用创建的派生之物。比特币钱包通过扫描区块链并聚合所有属于该用户的UTXO来计算该用户的余额。

0x02 Transaction

下面我们来分析UTXO中的TX,TX就是transaction的缩写。CTransaction有两个重要的成员变量std::vector vin和std::vector vout,交易输入和交易输出。看下面的类关系图更会一目了然。
类关系图

(上图有个笔误,CTxOut中的scriptPubKey应该是锁定脚本)
对应的数据库序列化如下:

普通交易输入(CTxIn)

字节 字段 描述
32 交易哈希值 指向被花费的UTXO所在的交易的哈希指针
4 输出索引 被花费的UTXO的索引号,第一个是0
1-9 解锁脚本大小 用字节表示的后面的解锁脚本长度
不定 解锁脚本 满足UTXO解锁脚本条件的脚本
4 序列号 目前未被使用的交易替换功能,设为0xFFFFFFFF

普通交易输出(CTxOut)

字节 字段 描述
8 总量 用聪表示的比特币值
1-9 锁定脚本大小 用字节表示的后面的锁定脚本长度
不定 锁定脚本 一个定义了支付输出所需条件的脚本

交易(CTransaction)

字节 字段 描述
4 版本 明确这笔交易参照的规则
1-9 输入计数器 包含的交易输入数量
不定 输入 一个或多个交易输入
1-9 输出计数器 包含的交易输出数量
不定 输出 一个或多个交易输出
4 锁定时间 一个区块号或UNIX时间戳

创世coinbase

字节 字段 描述
4 版本 这笔交易参照的规则
1-9 输入计数器 包含的交易输入数量
32 交易哈希 不引用任何一个交易,值全部为0
4 交易输出索引 固定为0xFFFFFFFF
1-9 Coinbase数据长度 coinbase数据长度
不定 Coinbase数据 在V2版本的区块中,除了需要以区块高度开始外,其它数据可以任意填写,用于extra nonce和挖矿标签
4 顺序号 值全部为1,0xFFFFFFFF
1-9 输出计数器 包含的交易输出数量
8 总量 用聪表示的比特币值
1-9 锁定脚本大小 用字节表示的后面的锁定脚本长度
不定 锁定脚本 一个定义了支付输出所需条件的脚本
4 锁定时间 一个区块号或UNIX时间戳

CTxIn类代码

/** An input of a transaction.  It contains the location of the previous
 * transaction's output that it claims and a signature that matches the
 * output's public key.
 */
class CTxIn
{
public:
    COutPoint prevout;          //上一笔交易输出位置(通过hash定位到交易,通过索引定位到vout)
    CScript scriptSig;          //解锁脚本
    unsigned int nSequence;     //序列号

    CTxIn()
    {
        nSequence = std::numeric_limits<unsigned int>::max();
    }

    explicit CTxIn(COutPoint prevoutIn, CScript scriptSigIn=CScript(), unsigned int nSequenceIn=std::numeric_limits<unsigned int>::max())
    {
        prevout = prevoutIn;
        scriptSig = scriptSigIn;
        nSequence = nSequenceIn;
    }

    CTxIn(uint256 hashPrevTx, unsigned int nOut, CScript scriptSigIn=CScript(), unsigned int nSequenceIn=std::numeric_limits<unsigned int>::max())
    {
        prevout = COutPoint(hashPrevTx, nOut);
        scriptSig = scriptSigIn;
        nSequence = nSequenceIn;
    }

    IMPLEMENT_SERIALIZE
    (
        READWRITE(prevout);
        READWRITE(scriptSig);
        READWRITE(nSequence);
    )

    bool IsFinal() const
    {
        return (nSequence == std::numeric_limits<unsigned int>::max());
    }

    friend bool operator==(const CTxIn& a, const CTxIn& b)
    {
        return (a.prevout   == b.prevout &&
                a.scriptSig == b.scriptSig &&
                a.nSequence == b.nSequence);
    }

    friend bool operator!=(const CTxIn& a, const CTxIn& b)
    {
        return !(a == b);
    }

    std::string ToString() const
    {
        std::string str;
        str += "CTxIn(";
        str += prevout.ToString();
        if (prevout.IsNull())
            str += strprintf(", coinbase %s", HexStr(scriptSig).c_str());
        else
            str += strprintf(", scriptSig=%s", scriptSig.ToString().substr(0,24).c_str());
        if (nSequence != std::numeric_limits<unsigned int>::max())
            str += strprintf(", nSequence=%u", nSequence);
        str += ")";
        return str;
    }

    void print() const
    {
        printf("%s\n", ToString().c_str());
    }
};

CTxOut类代码

/** An output of a transaction.  It contains the public key that the next input
 * must be able to sign with to claim it.
 */
class CTxOut
{
public:
    int64 nValue;           //输出金额
    CScript scriptPubKey;   //锁定脚本

    CTxOut()
    {
        SetNull();
    }

    CTxOut(int64 nValueIn, CScript scriptPubKeyIn)
    {
        nValue = nValueIn;
        scriptPubKey = scriptPubKeyIn;
    }

    IMPLEMENT_SERIALIZE
    (
        READWRITE(nValue);
        READWRITE(scriptPubKey);
    )

    void SetNull()
    {
        nValue = -1;
        scriptPubKey.clear();
    }

    bool IsNull() const
    {
        return (nValue == -1);
    }

    uint256 GetHash() const
    {
        return SerializeHash(*this);
    }

    friend bool operator==(const CTxOut& a, const CTxOut& b)
    {
        return (a.nValue       == b.nValue &&
                a.scriptPubKey == b.scriptPubKey);
    }

    friend bool operator!=(const CTxOut& a, const CTxOut& b)
    {
        return !(a == b);
    }

    bool IsDust() const;

    std::string ToString() const
    {
        if (scriptPubKey.size() < 6)
            return "CTxOut(error)";
        return strprintf("CTxOut(nValue=%"PRI64d".%08"PRI64d", scriptPubKey=%s)", nValue / COIN, nValue % COIN, scriptPubKey.ToString().substr(0,30).c_str());
    }

    void print() const
    {
        printf("%s\n", ToString().c_str());
    }
};

CTransaction类代码

/** The basic transaction that is broadcasted on the network and contained in
 * blocks. A transaction can contain multiple inputs and outputs.
 */
 /**
 * 交易可以在公网中通过p2p进行广播,也可以被打包在区块中。
 * 每一笔交易可以有多个输入和输出。
 */
class CTransaction
{
public:
    static int64 nMinTxFee;             //最小交易手续费
    static int64 nMinRelayTxFee;        //最小传播交易手续费
    static const int CURRENT_VERSION=1; //当前版本号
    int nVersion;                       //版本号
    std::vector<CTxIn> vin;             //交易输入列表
    std::vector<CTxOut> vout;           //交易输出列表 
    unsigned int nLockTime;             //锁定时间

    CTransaction()
    {
        SetNull();
    }

    IMPLEMENT_SERIALIZE
    (
        READWRITE(this->nVersion);
        nVersion = this->nVersion;
        READWRITE(vin);
        READWRITE(vout);
        READWRITE(nLockTime);
    )

    void SetNull()
    {
        nVersion = CTransaction::CURRENT_VERSION;
        vin.clear();
        vout.clear();
        nLockTime = 0;
    }

    bool IsNull() const
    {
        return (vin.empty() && vout.empty());
    }

    uint256 GetHash() const
    {
        return SerializeHash(*this);
    }

    bool IsFinal(int nBlockHeight=0, int64 nBlockTime=0) const
    {
        // Time based nLockTime implemented in 0.1.6
        if (nLockTime == 0)
            return true;
        if (nBlockHeight == 0)
            nBlockHeight = nBestHeight;
        if (nBlockTime == 0)
            nBlockTime = GetAdjustedTime();
        if ((int64)nLockTime < ((int64)nLockTime < LOCKTIME_THRESHOLD ? (int64)nBlockHeight : nBlockTime))
            return true;
        BOOST_FOREACH(const CTxIn& txin, vin)
            if (!txin.IsFinal())
                return false;
        return true;
    }

    bool IsNewerThan(const CTransaction& old) const
    {
        if (vin.size() != old.vin.size())
            return false;
        for (unsigned int i = 0; i < vin.size(); i++)
            if (vin[i].prevout != old.vin[i].prevout)
                return false;

        bool fNewer = false;
        unsigned int nLowest = std::numeric_limits<unsigned int>::max();
        for (unsigned int i = 0; i < vin.size(); i++)
        {
            if (vin[i].nSequence != old.vin[i].nSequence)
            {
                if (vin[i].nSequence <= nLowest)
                {
                    fNewer = false;
                    nLowest = vin[i].nSequence;
                }
                if (old.vin[i].nSequence < nLowest)
                {
                    fNewer = true;
                    nLowest = old.vin[i].nSequence;
                }
            }
        }
        return fNewer;
    }

    bool IsCoinBase() const
    {
        return (vin.size() == 1 && vin[0].prevout.IsNull());
    }

    /** Check for standard transaction types
        @return True if all outputs (scriptPubKeys) use only standard transaction forms
    */
    /*
    * 判断该交易是否合法,主要通过检查交易的极端的size,
    * txin的scriptSig,以及txout的scriptPubKey
    */
    bool IsStandard() const;

    /** Check for standard transaction types
        @param[in] mapInputs	Map of previous transactions that have outputs we're spending
        @return True if all inputs (scriptSigs) use only standard transaction forms
    */
     /*
     *检查交易输入的scriptSigs的合法性
     */
    bool AreInputsStandard(CCoinsViewCache& mapInputs) const;

    /** Count ECDSA signature operations the old-fashioned (pre-0.6) way
        @return number of sigops this transaction's outputs will produce when spent
    */
    unsigned int GetLegacySigOpCount() const;

    /** Count ECDSA signature operations in pay-to-script-hash inputs.

        @param[in] mapInputs	Map of previous transactions that have outputs we're spending
        @return maximum number of sigops required to validate this transaction's inputs
     */
    unsigned int GetP2SHSigOpCount(CCoinsViewCache& mapInputs) const;

    /** Amount of bitcoins spent by this transaction.
        @return sum of all outputs (note: does not include fees)
     */
    int64 GetValueOut() const
    {
        int64 nValueOut = 0;
        BOOST_FOREACH(const CTxOut& txout, vout)
        {
            nValueOut += txout.nValue;
            if (!MoneyRange(txout.nValue) || !MoneyRange(nValueOut))
                throw std::runtime_error("CTransaction::GetValueOut() : value out of range");
        }
        return nValueOut;
    }

    /** Amount of bitcoins coming in to this transaction
        Note that lightweight clients may not know anything besides the hash of previous transactions,
        so may not be able to calculate this.

        @param[in] mapInputs	Map of previous transactions that have outputs we're spending
        @return	Sum of value of all inputs (scriptSigs)
     */
    int64 GetValueIn(CCoinsViewCache& mapInputs) const;

    static bool AllowFree(double dPriority)
    {
        // Large (in bytes) low-priority (new, small-coin) transactions
        // need a fee.
        return dPriority > COIN * 144 / 250;
    }

    int64 GetMinFee(unsigned int nBlockSize=1, bool fAllowFree=true, enum GetMinFee_mode mode=GMF_BLOCK) const;

    friend bool operator==(const CTransaction& a, const CTransaction& b)
    {
        return (a.nVersion  == b.nVersion &&
                a.vin       == b.vin &&
                a.vout      == b.vout &&
                a.nLockTime == b.nLockTime);
    }

    friend bool operator!=(const CTransaction& a, const CTransaction& b)
    {
        return !(a == b);
    }


    std::string ToString() const
    {
        std::string str;
        str += strprintf("CTransaction(hash=%s, ver=%d, vin.size=%"PRIszu", vout.size=%"PRIszu", nLockTime=%u)\n",
            GetHash().ToString().c_str(),
            nVersion,
            vin.size(),
            vout.size(),
            nLockTime);
        for (unsigned int i = 0; i < vin.size(); i++)
            str += "    " + vin[i].ToString() + "\n";
        for (unsigned int i = 0; i < vout.size(); i++)
            str += "    " + vout[i].ToString() + "\n";
        return str;
    }

    void print() const
    {
        printf("%s", ToString().c_str());
    }


    // Check whether all prevouts of this transaction are present in the UTXO set represented by view
    // 判断该交易的所有的prevouts是否出现在视图的UTXO集合中
    bool HaveInputs(CCoinsViewCache &view) const;

    // Check whether all inputs of this transaction are valid (no double spends, scripts & sigs, amounts)
    // This does not modify the UTXO set. If pvChecks is not NULL, script checks are pushed onto it
    // instead of being performed inline.
    // 判断该交易的所有输入是否是合法(是否双花,scripts & sigs, 金额)
    bool CheckInputs(CValidationState &state, CCoinsViewCache &view, bool fScriptChecks = true,
                     unsigned int flags = SCRIPT_VERIFY_P2SH | SCRIPT_VERIFY_STRICTENC,
                     std::vector<CScriptCheck> *pvChecks = NULL) const;

    // Apply the effects of this transaction on the UTXO set represented by view
    // 更新该交易到视图的UTXO集合
    void UpdateCoins(CValidationState &state, CCoinsViewCache &view, CTxUndo &txundo, int nHeight, const uint256 &txhash) const;

    // Context-independent validity checks
    // 上下文无关的日常校验
    bool CheckTransaction(CValidationState &state) const;

    // Try to accept this transaction into the memory pool
    // 接受该交易,添加到交易池
    bool AcceptToMemoryPool(CValidationState &state, bool fCheckInputs=true, bool fLimitFree = true, bool* pfMissingInputs=NULL);

protected:
    static const CTxOut &GetOutputFor(const CTxIn& input, CCoinsViewCache& mapInputs);
};

关于锁定时间的解释如下:

交易的锁定时间
锁定时间定义了能被加到区块链里的最早的交易时间。在大多数交易里,它被设置成0,用来表示立即执行。如
果锁定时间不是0并且小于5亿,就被视为区块高度,意指在这个指定的区块高度之前,该交易没有被包含在区块
链里。如果锁定时间大于5亿,则它被当作是一个Unix纪元时间戳(从1970年1月1日以来的秒数),并且在这个
指定时点之前,该交易没有被包含在区块链里。锁定时间的使用相当于将一张纸质支票的生效时间予以后延。

0x03 交易流程

在熟悉交易的数据结构后,我们再来看一下交易流程:
交易流程

具体的步骤如下:
一、构造交易
钱包类CWallet的CreateTransaction函数创建交易。
主要分3步:
1、填充交易的输出交易(vout)
创建交易时,指定输出交易的信息,主要是输出的脚本(地址构造成CScript)、输出的币数量(nValue)。

 // vouts to the payees
 BOOST_FOREACH (const PAIRTYPE(CScript, int64)& s, vecSend)
 {
                    CTxOut txout(s.second, s.first);
                    if (txout.IsDust())
                    {
                        strFailReason = _("Transaction amount too small");
                        return false;
                    }
                    wtxNew.vout.push_back(txout);
   }

2、填充交易的输出交易(vin)
先从钱包的交易信息中选择合适的比特币(SelectCoins函数),填充到交易的输入交易中。
3、签名(CTxIn.scriptSig)
对输入交易的scriptSig签名(SignSignature函数)。
由新的交易信息、私钥计算哈希值(SignatureHash函数),填充到输入交易的scriptSig中(Solver函数)。构造交易完毕,再提交交易,发送出去。

 // Choose coins to use
 set<pair<const CWalletTx*,unsigned int> > setCoins;
 int64 nValueIn = 0;
 if (!SelectCoins(nTotalValue, setCoins, nValueIn))
  {                   
                    strFailReason = _("Insufficient funds");
                    return false;
  }

BOOST_FOREACH(PAIRTYPE(const CWalletTx*, unsigned int) pcoin, setCoins)
{
                    int64 nCredit = pcoin.first->vout[pcoin.second].nValue;
                    //The priority after the next block (depth+1) is used instead of the current,
                    //reflecting an assumption the user would accept a bit more delay for
                    //a chance at a free transaction.
                    dPriority += (double)nCredit * (pcoin.first->GetDepthInMainChain()+1);
 }
// Fill vin
BOOST_FOREACH(const PAIRTYPE(const CWalletTx*,unsigned int)& coin, setCoins)
wtxNew.vin.push_back(CTxIn(coin.first->GetHash(),coin.second));

// Sign
int nIn = 0;
BOOST_FOREACH(const PAIRTYPE(const CWalletTx*,unsigned int)& coin, setCoins)
if (!SignSignature(*this, *coin.first, wtxNew, nIn++))
{
         strFailReason = _("Signing transaction failed");
         return false;
 }

二、发送交易
当构造完交易,则提交交易(钱包类CWallet的CommitTransaction函数),发送出去。
提交交易时,先把交易添加到钱包中,然后标记旧的比特币为已经花费,再添加到交易内存池中,最后把交易传播下去。

// Add tx to wallet, because if it has change it's also ours,
// otherwise just for transaction history.
AddToWallet(wtxNew);

// Mark old coins as spent
set<CWalletTx*> setCoins;
BOOST_FOREACH(const CTxIn& txin, wtxNew.vin)
{
                CWalletTx &coin = mapWallet[txin.prevout.hash];
                coin.BindWallet(this);
                coin.MarkSpent(txin.prevout.n);
                coin.WriteToDisk();
                NotifyTransactionChanged(this, coin.GetHash(), CT_UPDATED);
}
// Broadcast
if (!wtxNew.AcceptToMemoryPool(true, false))
{
            // This must not fail. The transaction has already been signed and recorded.
            printf("CommitTransaction() : Error: Transaction not valid");
            return false;
}
wtxNew.RelayWalletTransaction();

传播交易时(RelayTransaction函数),由交易信息的哈希值构造CInv,类型
为MSG_TX,添加到每个节点的发送清单(vInventoryToSend)中,发送消息
(SendMessages)时把节点中的发送清单中的交易信息以”inv”命令发送出去。

void CWalletTx::RelayWalletTransaction()
{
    BOOST_FOREACH(const CMerkleTx& tx, vtxPrev)
    {
        if (!tx.IsCoinBase())
            if (tx.GetDepthInMainChain() == 0)
                RelayTransaction((CTransaction)tx, tx.GetHash());
    }
    if (!IsCoinBase())
    {
        if (GetDepthInMainChain() == 0) {
            uint256 hash = GetHash();
            printf("Relaying wtx %s\n", hash.ToString().c_str());
            RelayTransaction((CTransaction)*this, hash);
        }
    }
}
void RelayTransaction(const CTransaction& tx, const uint256& hash, const CDataStream& ss)
{
    CInv inv(MSG_TX, hash);
    {
        LOCK(cs_mapRelay);
        // Expire old relay messages
        while (!vRelayExpiration.empty() && vRelayExpiration.front().first < GetTime())
        {
            mapRelay.erase(vRelayExpiration.front().second);
            vRelayExpiration.pop_front();
        }

        // Save original serialized message so newer versions are preserved
        mapRelay.insert(std::make_pair(inv, ss));
        vRelayExpiration.push_back(std::make_pair(GetTime() + 15 * 60, inv));
    }
    LOCK(cs_vNodes);
    BOOST_FOREACH(CNode* pnode, vNodes)
    {
        if(!pnode->fRelayTxes)
            continue;
        LOCK(pnode->cs_filter);
        if (pnode->pfilter)
        {
            if (pnode->pfilter->IsRelevantAndUpdate(tx, hash))
                pnode->PushInventory(inv);
        } else
            pnode->PushInventory(inv);
    }
}

三、接收交易
当节点接收到交易命令(”tx”)后,把交易信息添加到交易内存池中,且传播
下去,详见ProcessMessage函数。

else if (strCommand == "tx")
    {
        vector<uint256> vWorkQueue;
        vector<uint256> vEraseQueue;
        CDataStream vMsg(vRecv);
        CTransaction tx;
        vRecv >> tx;

        CInv inv(MSG_TX, tx.GetHash());
        pfrom->AddInventoryKnown(inv);

        bool fMissingInputs = false;
        CValidationState state;
        if (tx.AcceptToMemoryPool(state, true, true, &fMissingInputs))
        {
            RelayTransaction(tx, inv.hash, vMsg);
            mapAlreadyAskedFor.erase(inv);
            vWorkQueue.push_back(inv.hash);
            vEraseQueue.push_back(inv.hash);
            // Recursively process any orphan transactions that depended on this one
            for (unsigned int i = 0; i < vWorkQueue.size(); i++)
            {
                uint256 hashPrev = vWorkQueue[i];
                for (map<uint256, CDataStream*>::iterator mi = mapOrphanTransactionsByPrev[hashPrev].begin();
                     mi != mapOrphanTransactionsByPrev[hashPrev].end();
                     ++mi)
                {
                    const CDataStream& vMsg = *((*mi).second);
                    CTransaction tx;
                    CDataStream(vMsg) >> tx;
                    CInv inv(MSG_TX, tx.GetHash());
                    bool fMissingInputs2 = false;
                    // Use a dummy CValidationState so someone can't setup nodes to counter-DoS based on orphan resolution (that is, feeding people an invalid transaction based on LegitTxX in order to get anyone relaying LegitTxX banned)
                    CValidationState stateDummy;

                    if (tx.AcceptToMemoryPool(stateDummy, true, true, &fMissingInputs2))
                    {
                        printf("   accepted orphan tx %s\n", inv.hash.ToString().c_str());
                        RelayTransaction(tx, inv.hash, vMsg);
                        mapAlreadyAskedFor.erase(inv);
                        vWorkQueue.push_back(inv.hash);
                        vEraseQueue.push_back(inv.hash);
                    }
                    else if (!fMissingInputs2)
                    {
                        // invalid or too-little-fee orphan
                        vEraseQueue.push_back(inv.hash);
                        printf("   removed orphan tx %s\n", inv.hash.ToString().c_str());
                    }
                }
            }

            BOOST_FOREACH(uint256 hash, vEraseQueue)
                EraseOrphanTx(hash);
        }
        else if (fMissingInputs)
        {
            AddOrphanTx(vMsg);

            // DoS prevention: do not allow mapOrphanTransactions to grow unbounded
            unsigned int nEvicted = LimitOrphanTxSize(MAX_ORPHAN_TRANSACTIONS);
            if (nEvicted > 0)
                printf("mapOrphan overflow, removed %u tx\n", nEvicted);
        }
        int nDoS;
        if (state.IsInvalid(nDoS))
            pfrom->Misbehaving(nDoS);
    }

由于本人水平有限,文中难免有理解不透的地方,还请大家多多指教!

老生常谈new运算符和new函数

在 C++ 中,运算符(Operator)是一种特殊的符号,用于执行特定的操作。C++ 中的运算符可以用于操作数据,执行计算,或者进行一些特定的语言行为。运算符可以分为多种类型,例如算术运算符、赋值运算符、比较运算符、逻辑运算符等。

new 运算符是 C++ 中用于在堆上分配内存并创建对象的运算符。它的语法是 new Type,其中 Type 是要分配内存和构造的对象的类型。与普通的函数不同,new 运算符有以下几个特点:

内存分配和对象构造: new 运算符不仅会分配所需的内存空间,还会调用对象的构造函数来初始化对象。这样,你可以直接获得已构造的对象。

返回类型: new 运算符返回一个指向分配的内存空间的指针,该指针的类型与所创建的对象类型相同。

异常处理: 如果内存分配失败,new 运算符会抛出 std::bad_alloc 异常,你可以在代码中使用 try 和 catch 来处理这种情况。

与函数相比,new 运算符是一种特殊的操作符,它将在堆上分配内存并构造对象。

运算符不能重载,但是我们可以重载函数,也就是operator new,千万不要被前面的operator给带偏了,有一定的误导性。
它的原型是void* operator new(std::size_t size)。如果我们使用了函数new,也就是operator new来为对象生成空间的时候,还需要调用
placement new来手动的调用构造函数。如:

// 使用 new 运算符分配内存并构造对象
Obj* obj1 = new Obj();

// 手动调用构造函数,但不会分配新的内存
void* memory = operator new(sizeof(Obj));
// placement new
Obj* obj2 = new (memory) Obj();

用微服务的**来开发游戏

今天早上读了天美J3工作室服务器主程关于游戏服务器架构的点评,我略有一点体会,总结一下。

文中提到的优先考虑全区全服的模式,我比较赞同,如果一开始设计架构的时候就从这个高度入手,对于整个online团队就充满挑战,想想就很刺激。就算是有分区分服的需求,也可以从在全服上做虚拟分配,这样后面合服就非常方便。想想传统的分区分服的后期运营,每隔几天就合服,各种脚本跑,还不能保证是否有脏数据的情况。

另一个我比较有体会的就是解耦,拆分成多进程,大系统小做。这点从我开始做服务器到现在体会越来越深,如何解耦以及保证优雅的RPC,也就是最近各大厂商炒作中台服务的**。提高整套系统的容灾能力,加强熔断机制,提高系统的复用性,降低开发成本,这也是online团队一直孜孜不倦追求的目标。

对于解耦,我最近体会较深的是,如何剥离出更多的无状态服务。这些服务除了可以提供二进制的通讯协议,也可以提供REST的服务。比如登陆,匹配,聊天,甚至战斗也是可以的。这样这些无状态服务可以脚本化实现,单元测试也更加方便了。这也就是微服务的**吧。

说一说online程序员经常遇到的GameServer和CombatServer。GameServer在mmo游戏里扮演的角色比较重要,在其他的moba或是fps的房间类游戏中就没有那么重度。想一想mmo游戏和moba游戏,角色进入场景都携带哪些数据。Mmo中角色的属性,装备属性,其他乱七八糟的**式属性,这些数据都需要从db中load,而且这些数据要被角色一直携带在各个场景中穿梭,还要被经常update,并被落地到db。而在moba游戏中,角色携带的额外数据可能就是皮肤或是装扮之类的,这些数据被修改的频率是极低的,基本都是付费才能获得。其他的基本数据都是从配置文件中获取。所以mmo的GameServer是有状态的,而房间类的GameServer是无状态的,它甚至可以和Client共用一套代码,它更多的职责在于计算玩家的交互产生的数据,以及结果,所以它可能是CPU密集型。如果Client中有地形计算,碰撞计算的,那共用代码的优势就更加突出了,一份代码,两处运行,省心。mmo的GameServer业务逻辑复杂多变,让它变的简单容易就需要解耦,从它中间拆分出更多的无状态的服务,让瘦身后的GameServer充当转发数据的角色。这样我上面提到的CombatServer是完全可以从中间拆出来的,这样GameServer就变成了Host,CombatServer上都是Host的replication了,只要做好replication的同步就好了。

按照我们的思路进行解耦后,会发现代码清晰了很多,大家之间的分工协作更加轻松愉快了。但同时挑战也来了,就是大量的进程间交互以及数据传输。有几个点需要重视的:
1,在有状态的服务器上,要做好entity的状态迁移,要做到状态不依赖服务器的消息到达顺序,也就要求服务器启动顺序无关,所以消息的缓存以及重传机制也是必然的。
2,有状态服务和无状态服务的load balance算法。
3,容灾机制,要保证节点故障后,迅速切换到备用节点,降低受影响的用户。
4,多节点之间的数据一致性问题,如果出现了不一致,如何做出预警以及数据恢复。
5,自动化运维。

最后谈一谈压力测试以及性能监测。我的经验是至少承受指标120%的压力,同时在正式release的时候,只做80%的负载,这样两头都留有一定的空间,相当于上了双保险。性能监测这块,不说太多,我比较同意文中的不同类型进程搭配部署的想法,这样比较节省资源。也就是说可以把CPU密集型的进程和内存密集型的进程配对的部署在物理机上,这样可以充分发挥机器的性能。

以上。

网络游戏服务器开发杂记---登陆场景

上一小节讲述了玩家在选择角色界面所进行的操作,这一章节重点放在玩家选定角色后,进入游戏的流程。前文已经讲到各服务器之间的连接关系,如GateServer分别与CenterServer,GameServer,DBServer,MapServer相连接,这其中的数据库代理上次我们忘记提到了,这里特意补充,它的主要作用就是除了做数据库的CURD的封装操作外,还本地做了一些缓存数据。连接图如下:
服务器.png

当玩家选择角色进入游戏后,首先会在GateServer把该角色绑定,进入游戏后,我们一般会用状态机来做角色登陆状体的迁移。这里的Login的步骤具体如下:

1,GateServer向MapServer请求当前的场景信息是否可用,以及该场景的实例在哪个GameServer还有空余,如果没有空余就创建新的Copy。
2,相应的GameServer把场景准备好以后,通知MapServer已准备就绪。
3,MapServer通知GateServer,此时该角色可以进入场景了,场景所在的服务器GameServer为XXX。
4,客户端就会连接该GameServer上,触发GateServer的OnConnect的回调
5,在GateServer的OnConnect中,GateServer会通知GameServer做一些预准备的操作,比如检查场景是否存在,做角色绑定等
6,GameServer准备完毕后,通知客户端,客户端发起EnterScene的主动请求,注意这里并不是服务器发起,而是由客户端发起
7,当客户端重新进入场景时,GameServer需要从DBServer重新拉取一遍数据,拉取成功,DBServer会返回消息通知GameServer。
8,GameServer会该角色进行初始化,并把他添加到快速查找表中,接着是Role身上的各个组件进行初始化。比如移动组件中,要把该角色的上线通知给他的九宫格的Creature,同时通知这些Creature该玩家上线。
9,至此角色登陆游戏场景的流程基本结束。

还记得我们上个章节中的问题吗?玩家除了正常登陆游戏之外,还有小退,切换场景等。那么切换场景和正常登陆游戏的差别在哪里呢?切换场景第一步要做的是与当前的GameServer断开连接,然后再去连接新的场景的GameServer。所以它首先会触发一个GateServer的OnDisconnect的回调,在OnDisconnect中去查询新的场景实例,接下来的流程就是和登陆的流程类似了。我省略了一些细节的流程,关键的时序图如下:
时序图.png

到这里我们把角色完整的登陆流程梳理了一遍,其中小的知识点很多,只要踩过坑以后,以后就驾轻就熟了。

常用的算法

func binarySort(a []int, v int) int {
	l := len(a)
	begin := 0
	end := l - 1
	for {
		mid := (begin + end) / 2
		if a[mid] == v {
			return mid
		}

		if begin >= end {
			return -1
		}

		if a[begin] < a[mid] {
			if v < a[begin] {
				begin = mid + 1
				continue
			}

			if v < a[mid] {
				end = mid - 1
			} else {
				begin = mid + 1
			}
		} else {
			if v >= a[end] {
				end = mid - 1
				continue
			}

			if v > a[mid] {
				begin = mid + 1
			} else {
				end = mid - 1
			}
		}
	}

	return -1
}

func quickSort(arr []int) []int {
	if len(arr) <= 1 {
		return arr
	}
	splitdata := arr[0]          //第一个数据为基准
	low := make([]int, 0, 0)     //比我小的数据
	hight := make([]int, 0, 0)   //比我大的数据
	mid := make([]int, 0, 0)     //与我一样大的数据
	mid = append(mid, splitdata) //加入一个
	for i := 1; i < len(arr); i++ {
		if arr[i] < splitdata {
			low = append(low, arr[i])
		} else if arr[i] > splitdata {
			hight = append(hight, arr[i])
		} else {
			mid = append(mid, arr[i])
		}
	}
	low, hight = QuickSort(low), QuickSort(hight) //切割递归处理
	myarr := append(append(low, mid...), hight...)
	return myarr
}

// the longest substr
func maxSubstr(a string, b string) int {
	la := len(a)
	lb := len(b)
	matrix := make([][]int, lb)
	ret := 0
	for i := range matrix {
		matrix[i] = make([]int, la)
	}

	for i := 0; i < lb; i++ {
		for j := 0; j < la; j++ {
			if i > 0 && j > 0 {
				if a[j] == b[i] {
					matrix[i][j] = matrix[i-1][j-1] + 1
					if matrix[i][j] > ret {
						ret = matrix[i][j]
					}
				}
			} else {
				if a[j] == b[i] {
					matrix[i][j] = 1
					if matrix[i][j] > ret {
						ret = matrix[i][j]
					}
				}
			}

		}
	}

	return ret
}

比特币源码研读--交易细节

0x00 读码即挖矿

前两天看到群里还有人在讨论ETH和EOS的DAPP开发,看来区块链的落地还是一线希望,大家可以继续给自己的信仰充值。充值方式众多,比如加仓,Fomo,或是写DAPP,读代码。那我继续前两次的操作,继续阅读BTC的代码,版本0.8.2。上次粗读了一番交易的流程,大概的来龙去脉也略知一二,这次就按照关键的执行函数来扣一下具体的细节。

0x01 函数CreateTransaction

函数原型如下:

bool CWallet::CreateTransaction(const vector<pair<CScript, int64> >& vecSend,
                                CWalletTx& wtxNew, CReserveKey& reservekey, int64& nFeeRet, std::string& strFailReason)

我们先看一下该函数的具体引用,出现在rpcwallet.cpp中,具体的用途就是RPC调用,进行转账交易。

Value sendmany(const Array& params, bool fHelp)
{
    ...
    ...

    // Send
    CReserveKey keyChange(pwalletMain);
    int64 nFeeRequired = 0;
    string strFailReason;
    bool fCreated = pwalletMain->CreateTransaction(vecSend, wtx, keyChange, nFeeRequired, strFailReason);
    if (!fCreated)
        throw JSONRPCError(RPC_WALLET_INSUFFICIENT_FUNDS, strFailReason);
    if (!pwalletMain->CommitTransaction(wtx, keyChange))
        throw JSONRPCError(RPC_WALLET_ERROR, "Transaction commit failed");

    return wtx.GetHash().GetHex();
}

现在来看CreateTransaction函数的返回值和参数:

返回值:如果创建成功返回true,否则false
参数:
1,const vector<pair<CScript, int64> >& vecSend:目标地址和金额,数组,支持批量转账
2,CWalletTx& wtxNew:生成的钱包关联交易详情
3,int64& nFeeRet:gas费用
4,std::string& strFailReason:失败原因

整个代码的模块是一个loop,通过找到符合条件的utxo再break出来,看一看代码的内部实现:

1,判断发送目标地址和金额是否合法,没啥好说的

    int64 nValue = 0;
    BOOST_FOREACH (const PAIRTYPE(CScript, int64)& s, vecSend)
    {
        if (nValue < 0)
        {
            strFailReason = _("Transaction amounts must be positive");
            return false;
        }
        nValue += s.second;
    }
    if (vecSend.empty() || nValue < 0)
    {
        strFailReason = _("Transaction amounts must be positive");
        return false;
    }

2,构造交易输出,并丢弃粉尘交易,防止DDOS攻击,这块下文会详细描述

// vouts to the payees
BOOST_FOREACH (const PAIRTYPE(CScript, int64)& s, vecSend)
{
        CTxOut txout(s.second, s.first);
        if (txout.IsDust())
        {
            strFailReason = _("Transaction amount too small");
            return false;
        }
        wtxNew.vout.push_back(txout);
}

3,构造交易出入,也就是根据规则找到发送方最优的utxo集合,SelectCoins下文详细描述

// Choose coins to use
set<pair<const CWalletTx*,unsigned int> > setCoins;
int64 nValueIn = 0;
if (!SelectCoins(nTotalValue, setCoins, nValueIn))
{
    strFailReason = _("Insufficient funds");
    return false;
}

4,先计算优先级,再根据优先级推导出交易费用,下文详细描述

 BOOST_FOREACH(PAIRTYPE(const CWalletTx*, unsigned int) pcoin, setCoins)
{
    int64 nCredit = pcoin.first->vout[pcoin.second].nValue;
    //The priority after the next block (depth+1) is used instead of the current,
    //reflecting an assumption the user would accept a bit more delay for
    //a chance at a free transaction.
    dPriority += (double)nCredit * (pcoin.first->GetDepthInMainChain()+1)
}
...
dPriority /= nBytes;

5,计算找零,并创建新的找零钱包公钥,把找零加入到vOut中

 int64 nChange = nValueIn - nValue - nFeeRet;
 // if sub-cent change is required, the fee must be raised to at least nMinTxFee
 // or until nChange becomes zero
 // NOTE: this depends on the exact behaviour of GetMinFee
				
 if (nFeeRet < CTransaction::nMinTxFee && nChange > 0 && nChange < CENT)
 {
    int64 nMoveToFee = min(nChange, CTransaction::nMinTxFee - nFeeRet);
    nChange -= nMoveToFee;
    nFeeRet += nMoveToFee;
}

if (nChange > 0)
{
    // Note: We use a new key here to keep it from being obvious which side is the change.
    //  The drawback is that by not reusing a previous key, the change may be lost if a
    //  backup is restored, if the backup doesn't have the new private key for the change.
    //  If we reused the old key, it would be possible to add code to look for and
    //  rediscover unknown transactions that were written with keys of ours to recover
    //  post-backup change.

    // Reserve a new key pair from key pool
    CPubKey vchPubKey;
    assert(reservekey.GetReservedKey(vchPubKey)); // should never fail, as we just unlocked

    // Fill a vout to ourself
    // TODO: pass in scriptChange instead of reservekey so
    // change transaction isn't always pay-to-bitcoin-address
	// 构建找零的新的公钥
    CScript scriptChange;
    scriptChange.SetDestination(vchPubKey.GetID());

    CTxOut newTxOut(nChange, scriptChange);

    // Never create dust outputs; if we would, just
    // add the dust to the fee.
    if (newTxOut.IsDust())
    {
        nFeeRet += nChange;
        reservekey.ReturnKey();
    }
    else
    {
        // Insert change txn at random position:
        vector<CTxOut>::iterator position = wtxNew.vout.begin()+GetRandInt(wtxNew.vout.size()+1);
        wtxNew.vout.insert(position, newTxOut);
    }
}
else
    reservekey.ReturnKey();

6,签名,并计算交易费用,如果费用不满足条件,再重新进入循环,重复上述流程

// Fill vin
BOOST_FOREACH(const PAIRTYPE(const CWalletTx*,unsigned int)& coin, setCoins)
                    wtxNew.vin.push_back(CTxIn(coin.first->GetHash(),coin.second));

// Sign
int nIn = 0;
BOOST_FOREACH(const PAIRTYPE(const CWalletTx*,unsigned int)& coin, setCoins)
    if (!SignSignature(*this, *coin.first, wtxNew, nIn++))
    {
        strFailReason = _("Signing transaction failed");
        return false;
    }

// Limit size
unsigned int nBytes = ::GetSerializeSize(*(CTransaction*)&wtxNew, SER_NETWORK, PROTOCOL_VERSION);
if (nBytes >= MAX_STANDARD_TX_SIZE)
{
    strFailReason = _("Transaction too large");
    return false;
}
dPriority /= nBytes;

// Check that enough fee is included
int64 nPayFee = nTransactionFee * (1 + (int64)nBytes / 1000);
bool fAllowFree = CTransaction::AllowFree(dPriority);
int64 nMinFee = wtxNew.GetMinFee(1, fAllowFree, GMF_SEND);
if (nFeeRet < max(nPayFee, nMinFee))
{
    nFeeRet = max(nPayFee, nMinFee);
    continue;
}

7,填充好WalletTX的vIn和vOut后,用vIn的prevout来填充WalletTX的vtxPrev

// Fill vtxPrev by copying from previous transactions vtxPrev
wtxNew.AddSupportingTransactions();
wtxNew.fTimeReceivedIsTxTime = true;

0x02 粉尘交易Dust

前文我们已经讲过,比特币的交易并不是传统意义上的数字加减运算,它实质是utxo的转移。所以A向B转移1个BTC这个动作,产生的结果是未知的,比如就可以是1个完整的BTC转移,也有可能是1000个0.001个BTC的转移,也有可能是1.05个BTC转移(0.05个找零),当然用户不需要自己去手工配置,代码已经帮我们完成(下文的SelectCoin),但有时候还是无法避免有dust交易的产生。dust的存在会造成区块链网络上DDOS攻击,攻击者通过发送许多这样小金额的交易,来堵塞整个网络。在比特币的网络中,如果交易费(Fee)用高于1/3的交易价值(Value),则被视为粉尘交易。 常规的来说,一个P2PKH交易,由于其最小体积时为一个输入,一个输出,总共 '148 + 34 = 182' 字节,而交易手续费是由每个节点配置的minRelayTxFee(比特币中默认为'0.00001BTC/KB')决定的,故而手续费就是 '182 / 1000 * minRelayTxFee',那么其的3倍即 '546 / 1000 * minRelayTxFee',也即默认为 0.00000546 BTC,在BCH网络上也是一样的。

钱包在准备支付金额的时候有一个既定的规则,就是在众多输入(inputs)中筹备支付金额的时候尽量避免产生小于0.01BTC的金额变动(比如你要支付5.005BTC,钱包尽可能的选择3+2.005或者1+1+3.005,而不是5+0.005)。

Dust代码描述如下:

bool CTxOut::IsDust() const
{
    // "Dust" is defined in terms of CTransaction::nMinRelayTxFee,
    // which has units satoshis-per-kilobyte.
    // If you'd pay more than 1/3 in fees
    // to spend something, then we consider it dust.
    // A typical txout is 33 bytes big, and will
    // need a CTxIn of at least 148 bytes to spend,
    // so dust is a txout less than 54 uBTC
    // (5430 satoshis) with default nMinRelayTxFee
    return ((nValue*1000)/(3*((int)GetSerializeSize(SER_DISK,0)+148)) < CTransaction::nMinRelayTxFee);
}

0x03 交易费用计算Fee

数额越大、币龄(age)越高优先级越高

如果你发送金额太小或者是你的比特币刚开采出来不久,那么你的转账就不再免费之列。每一个交易都会分配一个优先级,这个优先级通过币的新旧程度、交易的字节数和交易的数量。具体来说,对于每一个输入(inputs)来讲,客户端会先将比特币的数量乘以这些币在块中存在的时间(币龄,age),然后将所有的乘积加起来除以此次交易的大小(以字节为单位),计算公式:priority = sum(input_value_in_base_units * input_age)/size_in_bytes,计算结果如果小于0.576,那么该交易就必须支付手续费。如果你确实大量的小额输入,又想免费转出,这时候你可以加一个数额大的、币龄大的比特币金额(多余的找零返回),就会将平均优先级提高,从而可以免费转出比特币。
代码如下:

static bool AllowFree(double dPriority)
    {
        // Large (in bytes) low-priority (new, small-coin) transactions
        // need a fee.
        return dPriority > COIN * 144 / 250;
    }

在转账的最后客户端会检测本次转账的大小(以字节为单位),大小一般取决于输入和输出的数额大小。
如果该次转账的大小超过10000字节但是优先级符合免费的标准,那么仍然可以享受免费转账,否则需要支付手续费。没1000字节的费用默认是0.0001BTC,但是你也可以在客户端里进行追加,依次打开选项卡“设置>选项>主要”进行手续费的调整。
相关代码如下:

int64 CTransaction::GetMinFee(unsigned int nBlockSize, bool fAllowFree,
                              enum GetMinFee_mode mode) const
{
    // Base fee is either nMinTxFee or nMinRelayTxFee
    int64 nBaseFee = (mode == GMF_RELAY) ? nMinRelayTxFee : nMinTxFee;

    unsigned int nBytes = ::GetSerializeSize(*this, SER_NETWORK, PROTOCOL_VERSION);
    unsigned int nNewBlockSize = nBlockSize + nBytes;
    int64 nMinFee = (1 + (int64)nBytes / 1000) * nBaseFee;

    if (fAllowFree)
    {
        if (nBlockSize == 1)
        {
            // Transactions under 10K are free
            // (about 4500 BTC if made of 50 BTC inputs)
            if (nBytes < 10000)
                nMinFee = 0;
        }
        else
        {
            // Free transaction area
            if (nNewBlockSize < 27000)
                nMinFee = 0;
        }
    }

    // To limit dust spam, require base fee if any output is less than 0.01
    if (nMinFee < nBaseFee)
    {
        BOOST_FOREACH(const CTxOut& txout, vout)
            if (txout.nValue < CENT)
                nMinFee = nBaseFee;
    }

    // Raise the price as the block approaches full
    if (nBlockSize != 1 && nNewBlockSize >= MAX_BLOCK_SIZE_GEN/2)
    {
        if (nNewBlockSize >= MAX_BLOCK_SIZE_GEN)
            return MAX_MONEY;
        nMinFee *= MAX_BLOCK_SIZE_GEN / (MAX_BLOCK_SIZE_GEN - nNewBlockSize);
    }

    if (!MoneyRange(nMinFee))
        nMinFee = MAX_MONEY;
    return nMinFee;
}

0x04 寻找最佳utxo集合策略SelectCoins

这个函数比较有意思,我们先看下函数实现

bool CWallet::SelectCoins(int64 nTargetValue, set<pair<const CWalletTx*,unsigned int> >& setCoinsRet, int64& nValueRet) const
{
    vector<COutput> vCoins;
    AvailableCoins(vCoins);

    return (SelectCoinsMinConf(nTargetValue, 1, 6, vCoins, setCoinsRet, nValueRet) ||
            SelectCoinsMinConf(nTargetValue, 1, 1, vCoins, setCoinsRet, nValueRet) ||
            SelectCoinsMinConf(nTargetValue, 0, 1, vCoins, setCoinsRet, nValueRet));
}

先说一说函数的返回值和参数

返回值:成功找到TxIn的集合返回true,否则返回false
nTargetValue:目标金额
setCoinsRet:要返回的TxIn集合
nValueRet:实际金额(有可能包含找零)

AvailabeCoins通过在当前钱包视图中预过滤筛选出COutput的集合,筛选策略主要是判断当前的引用输入是否经过确认等。
下一步再来看看SelectCoinsMinConf这个函数,看到(1,6),(1,1),(0,1)这样的硬编码顿时就有一种紧张感,还好参数的命名比较直观。这三次相同函数不同参数的函数调用,其实反映了一种选择策略,就是按照交易被区块链确认次数递减来扩大选择范围,转账给自己优先要用通过1次确认的,转给别人要优先用通过6次确认的。这就是为什么我们现在在交易所进行法币场外交易时,交易所作为第三方担保,一般会在交易经过6次甚至更多次确认后才会把法币打给转出方,这样实施双花恶意攻击就会难上加难。

再来说说SelectCoinsMinConf的策略:

1,如果存在某UTXO值正好等于发送金额nValue(已包含手续费nFee),则将该UTXO加入目标交易集并返回成功
2,找出账户中UTXO值小于发送金额nValue的UTXO集vValue,并将vValue中所有UTXO值求和为nTotalLower,并找出所有UTXO值大于nValue的最小值nLowestLarger,再分两种情况
2.1:nTotalLower小于nValue,如果nLowestLarger存在,则将该值对应的pcoinLowestLarger交易加入目标交易集并返回成功,如果nLowestLarger不存在,则说明“余额”不足,返回失败
2.2:nTotalLower大于nValue,则使用随进逼近法(最多1000次)找出UTXO值的和nBest最接近nValue的集合vfBest,看nBest和nLowestLarger(如果存在)谁更接近nValue,则选择谁为相应的目标UTXO集,并返回成功

代码如下:

bool CWallet::SelectCoinsMinConf(int64 nTargetValue, int nConfMine, int nConfTheirs, vector<COutput> vCoins,
                                 set<pair<const CWalletTx*,unsigned int> >& setCoinsRet, int64& nValueRet) const
{
    setCoinsRet.clear();
    nValueRet = 0;

    // List of values less than target
    pair<int64, pair<const CWalletTx*,unsigned int> > coinLowestLarger;
    coinLowestLarger.first = std::numeric_limits<int64>::max();
    coinLowestLarger.second.first = NULL;
    vector<pair<int64, pair<const CWalletTx*,unsigned int> > > vValue;
    int64 nTotalLower = 0;

    random_shuffle(vCoins.begin(), vCoins.end(), GetRandInt);

    BOOST_FOREACH(COutput output, vCoins)
    {
        const CWalletTx *pcoin = output.tx;

        if (output.nDepth < (pcoin->IsFromMe() ? nConfMine : nConfTheirs))
            continue;

        int i = output.i;
        int64 n = pcoin->vout[i].nValue;

        pair<int64,pair<const CWalletTx*,unsigned int> > coin = make_pair(n,make_pair(pcoin, i));

        if (n == nTargetValue)
        {
            setCoinsRet.insert(coin.second);
            nValueRet += coin.first;
            return true;
        }
        else if (n < nTargetValue + CENT)
        {
            vValue.push_back(coin);
            nTotalLower += n;
        }
        else if (n < coinLowestLarger.first)
        {
            coinLowestLarger = coin;
        }
    }

    if (nTotalLower == nTargetValue)
    {
        for (unsigned int i = 0; i < vValue.size(); ++i)
        {
            setCoinsRet.insert(vValue[i].second);
            nValueRet += vValue[i].first;
        }
        return true;
    }

    if (nTotalLower < nTargetValue)
    {
        if (coinLowestLarger.second.first == NULL)
            return false;
        setCoinsRet.insert(coinLowestLarger.second);
        nValueRet += coinLowestLarger.first;
        return true;
    }

    // Solve subset sum by stochastic approximation
    sort(vValue.rbegin(), vValue.rend(), CompareValueOnly());
    vector<char> vfBest;
    int64 nBest;

    ApproximateBestSubset(vValue, nTotalLower, nTargetValue, vfBest, nBest, 1000);
    if (nBest != nTargetValue && nTotalLower >= nTargetValue + CENT)
        ApproximateBestSubset(vValue, nTotalLower, nTargetValue + CENT, vfBest, nBest, 1000);

    // If we have a bigger coin and (either the stochastic approximation didn't find a good solution,
    //                                   or the next bigger coin is closer), return the bigger coin
    if (coinLowestLarger.second.first &&
        ((nBest != nTargetValue && nBest < nTargetValue + CENT) || coinLowestLarger.first <= nBest))
    {
        setCoinsRet.insert(coinLowestLarger.second);
        nValueRet += coinLowestLarger.first;
    }
    else {
        for (unsigned int i = 0; i < vValue.size(); i++)
            if (vfBest[i])
            {
                setCoinsRet.insert(vValue[i].second);
                nValueRet += vValue[i].first;
            }

        //// debug print
        printf("SelectCoins() best subset: ");
        for (unsigned int i = 0; i < vValue.size(); i++)
            if (vfBest[i])
                printf("%s ", FormatMoney(vValue[i].first).c_str());
        printf("total %s\n", FormatMoney(nBest).c_str());
    }

    return true;
}

0x05 总结

从上面分析来看,交易的流程主要就是创建交易输出vOut,以及寻找到交易输入vIn,然后配置上gas,广播到网络节点中去。比特币的交易模块是整个区块链技术体系中比较重要的部分,也是比较晦涩难懂的部分,值得花上一些时间细细品读。由于本人水平有限,文中难免有理解不透的地方,还请大家多多指教!

网友Black在我学习的过程中给出了大量的指点和帮助,特此感谢!

参考:
1,http://www.btc38.com/btc/btc_learning/220.html
2,https://zhuanlan.zhihu.com/p/43203734

闪亮的瞬间

2021/1/13 M

1,类的成员变量是否可以声明为引用?

A:可以的。

1,不能有默认构造函数,必须提供构造函数
2,构造函数的形参必须为引用类型
3,初始化必须在成员初始化链表内完成

2,类的析构函数可以为private吗?

A:可以的。但是它会出现一些问题,比如在栈上定义的临时类变量就无法析构,因为栈上的空间时系统自动回收的,那么就会有
运行时错误。也就是说这个类的成员只能在堆上构造,然后重新定义一个public的destory函数来回收类的空间,最后用placement delete回收4个字节的指针。

3,构造函数和析构函数调用虚函数的问题?

A:可以调用,但都是调用当前类的虚函数,因为在构造函数和析构函数中会先重置虚表地址,然后再做接下来的工作。
如下:
MyThreadBase()
{
// call virtual fun, but in dirvied class, it does not work
ThisIsVirtual();
}

virtual ~MyThreadBase()
{
// call virtual fun, but in dirvied class, it does not work
ThisIsVirtual();
}
MyThread()
{
this->ThreadBase();
this->vtbl_ptr = MyThread::vtbl;
RunUserCode();
}

virtual ~MyThread()
{
this->vtbl_ptr = MyThread::vtbl;
RunUserCode();
this->~ThreadBase();
}

4,举例说明weak_ptr是怎样打破shared_ptr的循环引用的?

class A { shared_ptr b; ... };
class B { shared_ptr a; ... };
shared_ptr x(new A); // +1
x->b = new B; // +1
x->b->a = x; // +1
// Ref count of 'x' is 2.
// Ref count of 'x->b' is 1.
// When 'x' leaves the scope, there will be a memory leak:
// 2 is decremented to 1, and so both ref counts will be 1.
// (Memory is deallocated only when ref count drops to 0)

class A { shared_ptr b; ... };
class B { weak_ptr
a; ... };
shared_ptr x(new A); // +1
x->b = new B; // +1
x->b->a = x; // No +1 here
// Ref count of 'x' is 1.
// Ref count of 'x->b' is 1.
// When 'x' leaves the scope, its ref count will drop to 0.
// While destroying it, ref count of 'x->b' will drop to 0.
// So both A and B will be deallocated.

5,代码说明unique_ptr, shared_ptr, weak_ptr 区别?

void unique_ptr_test()
{
    std::unique_ptr<int> up1(new int(11));
    //std::unique_ptr<int> up2 = up1;   // complie error
    std::cout << *up1 << endl;

    std::unique_ptr<int> up3 = std::move(up1);
    //std::cout << *up1 << endl;          // run error
    std::cout << *up3 << endl;

    up3.reset();    // release the memory
    up1.reset();    // no error
    //std::cout << *up3 << endl;      // run error, up3 memory has been released
    up3.reset(new int(22));
    std::cout << *up3 << endl;

    up3 = nullptr;
    up3.reset(new int(33));
    std::cout << *up3 << endl;

    std::unique_ptr<int> up4(new int(44));
    int* p = up4.release();           // move the ownership to p, NOT free the memory
    std::cout << *p << endl;
    //std::cout << *up4 <<endl;         // run error
    delete p;
}

void shared_ptr_test()
{
    std::shared_ptr<int> sp1(new int(11));
    std::shared_ptr<int> sp2 = sp1;
    std::cout << sp1.use_count() << " " << sp2.use_count() << endl;

    std::cout << *sp1 << endl;
    std::cout << *sp2 << endl;

    sp1.reset();
    std::cout << sp1.use_count() << endl;   // output 0
    std::cout << sp2.use_count() << endl;   // output 1

    std::cout << *sp1 << endl;  // run error
    std::cout << *sp2 << endl;
}

void check_weak_ptr(std::weak_ptr<int>& wp)
{
    std::shared_ptr<int> sp = wp.lock();
    if (sp != nullptr)
    {
        std::cout << *sp << endl;
    }
    else
    {
        std::cout << "invalid pointer" << endl;
    }
}

void weak_ptr_test()
{
    std::shared_ptr<int> sp1(new int(11));
    std::shared_ptr<int> sp2 = sp1;
    //std::weak_ptr<int> wp(new int(22));     // complie error
    std::weak_ptr<int> wp(sp1);

    std::cout << wp.use_count() << endl;      // 2, NOT 3
    std::cout << *sp1 << endl;                // 11
    std::cout << *sp2 << endl;                // 11
    check_weak_ptr(wp);                       // 11

    sp1.reset();
    std::cout << wp.use_count() << endl;      // 1
    std::cout << *sp2 << endl;
    check_weak_ptr(wp);                       // 11

    sp2.reset();
    std::cout << wp.use_count() << endl;      // 0
    check_weak_ptr(wp);                       // invalid
}

6,shared_ptr是线程安全的吗?

不是,因为shared_ptr有2个成员变量, ptr和cnt,修改这两个变量不是原子操作,有race_condition.
考虑一个简单的场景,有 3 个 shared_ptr 对象 x、g、n:

shared_ptr g(new Foo); // 线程之间共享的 shared_ptr
shared_ptr x; // 线程 A 的局部变量
shared_ptr n(new Foo); // 线程 B 的局部变量
一开始,各安其事。

image

线程 A 执行 x = g; (即 read g),以下完成了步骤 1,还没来及执行步骤 2。这时切换到了 B 线程。

image

同时编程 B 执行 g = n; (即 write g),两个步骤一起完成了。
先是步骤 1:

image

再是步骤 2:
image

这是 Foo1 对象已经销毁,x.ptr 成了空悬指针!

最后回到线程 A,完成步骤 2:

image

多线程无保护地读写 g,造成了“x 是空悬指针”的后果。这正是多线程读写同一个 shared_ptr 必须加锁的原因。

7,epoll的ET, LT触发时机

在代码中,针对fd回调的条件都是一样的,都是判断IN/OUT/DEL事件,所以要对需要被回调的fd打上对应的flag,但是被触发的条件不一样,在LT中,只要被打上IN的flag,在epoll_wait的read_callback中就会一直被回调,哪怕是数据已经读完,但是在ET中只有fd的内核状态由不可读变成可读,才会被触发,否则不会再次触发read_callback,这就要求在第一次的read_callback中要一次性把内核缓冲区的数据读完。同理OUT。

7.1,select与epoll

关于select和epoll网上科普的内容很多,大致说的都是epoll比select效率高,真的是这样吗?首先select和epoll都是同步的多路复用的网络模型,简单的点讲,他们都是通过一次系统调用来管理有信号的socket。通常我们知道的select的弱点就是它能管理的socket数量是有限制的,在Linux上通常是1024个。那么通俗点讲select和epoll分别维护了一个fd队列,select用的数据结构就是普通的数组,并且fd的值就是下标,所以可以通过max_fd可以确定数组的长度。select网络模型的特点是每次有网络IO的时候,都会产生中断,用户层维护的队列就需要轮询一遍看看哪个fd被is_read或是is_write了,然后做相应的处理,如果仅有个别的fd活跃,这种效率是较低的。epoll本质上来讲查找有信号的fd也是通过轮询,不过轮询由OS接管了,OS返回给用户态就是筛选后的fd列表,OS内部使用B+Tree来管理这些fd,进行快速的查询,删除操作。通过这些对比,发现epoll确实是比select 有优势的,但是在某些特定的情况下,他们俩也能打个平手,比如在大多数的fd都活跃的情况下,这个时候不管是epoll还是select都要在用户层进行轮询,时间复杂度为O(n)。
int epoll_create(int size);
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
int epoll_wait(int epfd, struct epoll_event *events,int maxevents, int timeout);

8,一致性Hash算法

8.1 先利用机器的ID或是IP:PORT做映射,把机器映射到环上

8.2 再把object根据规则,顺时针在环上找到最近的机器节点

8.3 即使在环上增加或删除新的机器节点,也只会影响它周围的很小一部份object的映射,代价很小

8.4 当机器节点较少的时候,可能会出现数据倾斜,比如只有A,B两台机器,那么可以增加A-1,A-2,B-1,B-2虚拟节点,来减少数据的倾斜,让object均匀的落在节点上。

9,网络部分

9.1 TCP的syn队列和accept的队列

image
syn队列,又称半连接队列,服务器在接收到客户端syn请求,给客户端回复ack+syn,并把这个连接信息放置到syn队列中,并等到客户端的ack。在不同的版本中影响syn队列的长度不同,但其中都会和tcp_max_syn_backlog相关,所以在防御syn攻击中,就可以修改这个值。
参考:https://www.ibanmen.com/posts/6214bc62.html
accept队列,又称全连接队列,服务器在收到客户端三次握手的最后ack时,会把syn队列中相应的连接转移到accept队列中,等待用户层代码调用Accept()函数来取出,并建立连接。全连接队列的长度为somaxconn 和 backlog的最小值,所以单独在listen中设置backlog的大小是不能完全改变accept队列的大小的。如果我们把tcp_abort_on_overflow设置为1,当accept队列满的时候,客户端会收到rst,显示为connection reset by peer。

9.2 如果TCP是两次握手即建立连接会怎么样?

一句话概括,TCP连接握手,握的是啥?
通信双方数据原点的序列号!
二次握手的过程:
2.1 A 发送同步信号SYN + A's Initial sequence number
2.2 B发送同步信号SYN + B's Initial sequence number+ B's ACK sequence number
这里有一个问题,A与B就A的初始序列号达成了一致,这里是1000。但是B无法知道A是否已经接收到自己的同步信号,如果这个同步信号丢失了,A和B就B的初始序列号将无法达成一致。
同时试想一下,如果两次握手就成功,那么syn攻击是不是更加简单了,这个时候连接已经建立了,服务器上就会有大量的僵尸连接,严重浪费服务器资源。

9.3 为什么分片在IP层?

MTU = 1500-sizeof(IP包头)
MSS = 1500-sizeof(IP包头)-sizeof(TCP包头),MSS是发送方和接收方不停协商而改变的。
在应用层因为有了TCP的分段,所以在该层是不可以的,MSS < MTU。
但是不管如何,就算是UDP,也要避免分片的产生,因为分片会产生一些额外的负面效果,所以UDP要控制包的大小,保证发送成功率。IPV6已经废弃分片。

9.4 如果在fin_wait_2的状态时候,服务器宕机会怎么样?

客户端启动fin_wait_2定时器,迁移到time_wait状态。

9.5 close和shutdown有什么区别?

TCP是全双工协议,shutdown可以选择关闭读或是写,close的话就是走tcp的四次挥手过程。shutdown就是关闭socket,但并不会释放资源,而close可以释放资源,同样close用于其他资源管理也一样,比如文件。

10 虚继承的内存布局

g++和clang不同编译器下关于虚继承的内存布局还不太一样,这里主要说的是VS的编译器。
类图如下所示:
image

菱形虚拟继承下,最派生类D类的对象模型又有不同的构成了。在D类对象的内存构成上,有以下几点:
• 在D类对象内存中,基类出现的顺序是:先是B1(最左父类),然后是B2(次左父类),最后是B(虚祖父类)
• D类对象的数据成员id放在B类前面,两部分数据依旧以0来分隔。
• 编译器没有为D类生成一个它自己的vptr,而是覆盖并扩展了最左父类的虚基类表,与简单继承的对象模型相同。
• 超类B的内容放到了D类对象内存布局的最后。
菱形虚拟继承下的C++对象模型为:

image

11,关于右值引用

右值引用必须要初始化为一个右值,初始化为左值是错误的,只能用std::move()把左值转化为右值。比如:
int i = 100;
int&& j = i; // 错误
int&& j = std::move(i); // 正确
移动构造函数通常意味着对象控制权的转移,我们常用的移动构造函数通常利用这种控制权的转移来避免内存的重新分配,比如常见的内存深拷贝。当一个对象被右值引用转移控制权之后,用户通常是无法再对其进行操作,等待它的只有是析构。在C++11之后的容器构造函数中都会有移动拷贝构造函数,这样也就意味着无需再次分配空间和删除原来的临时对象的空间,可以直接把临时对象的堆上空间做控制权转移即可。
void vector::push_back(T&& obj) noexcept; // 注意这里一定要用noexcpet

移动语义固然是个好方法,但是必须要保证的是被移动过的对象一定要手动将其置于可析构的状态,因为被调用的右值往往马上就要经历析构;如果一个以后源对象具有不确定的状态,对其调用std::move是危险的,当我们调用move时必须确认移动后源对象没有其他用户。

一般来说,有五个基本操作可以控制类的拷贝操作:拷贝构造函数、拷贝赋值运算符、移动构造函数、移动赋值运算符和析构函数,C++并不要求我们定义所有这些操作,可以只定义其中一个或者两个,而不必定义所有,但是这些操作通常应该被视为一个整体,只需要其中一个操作,而不需要定义所有操作的情况是及其少见的。

当我们在决定一个类是否需要自定义版本的拷贝成员时,一个基本原则是首先确定这个类是否需要一个析构函数,通常,对析构的需求要比对拷贝或移动的需求更为明显,如果这个类需要一个析构函数,我们几乎可以肯定它也需要一个拷贝构造函数和移动构造函数。

12,go for range的陷阱

具体参考 https://colobu.com/2015/09/07/gotchas-and-common-mistakes-in-go-golang/

13,go的安装

wget -c https://dl.google.com/go/go1.14.2.linux-amd64.tar.gz -O - | sudo tar -xz -C /usr/local
通过将 Go 目录添加到$PATH环境变量,系统将会知道在哪里可以找到 Go 可执行文件。
这个可以通过添加下面的行到/etc/profile文件(系统范围内安装)或者$HOME/.profile文件(当前用户安装):
export GOROOT=/usr/local/go
export GOPATH=~/workspace/me/go
export PATH=$PATH:$GOROOT/bin:$GOPATH/bin
保存文件,并且重新加载新的PATH 环境变量到当前的 shell 会话:
source ~/.profile

14, 我们在操作缓存的时候,到底应该删除缓存还是更新缓存呢?

image

  1. 线程A先发起一个写操作,第一步先更新数据库
  2. 线程B再发起一个写操作,第二步更新了数据库
  3. 由于网络等原因,线程B先更新了缓存
  4. 线程A更新缓存。
    所以要删除缓存。
    14.1 缓存雪崩
    缓存雪崩,即在某个时间点有大量的缓存失效,大量的流量打在数据库上,造成数据库热点。主要原因是在设置缓存的
    时候SetEx,设置过期时间相同,所以会有大量的缓存在同一时间失效,这样就造成了缓存雪崩。解决的方案是在设置缓存
    过期时间的时候加一个随机数,错峰过期。
    14.2 缓存击穿
    缓存击穿,即有大量数据在缓存中未命中,然后数据打在了DB上,造成数据库热点。造成这种情况的原因是请求被Hacker利用,
    发送大量无效的数据请求。解决的方案是使用布隆过滤,把合法的数据加在布隆过滤器中,布隆过滤器的原理是使用一个超长的
    bitset来存储有效数据源的Hash值,经过若干次的Hash变化后,即可判断出数据是否合法。比如布隆过滤器长度为1024,三次Hash的值分别为 18,266, 908,那么就将这三个位置分别置为1。那同样的道理即可判断出数据是否在缓存中,但是布隆过滤器存在一定误判率,可以通过调整长度和Hash次数来调整。

15,关于goroutine的理解

线程有自己的线程ID,独享CS:IP寄存器,独享栈寄存器,独享寄存器(哪些?),共享代码区,堆区。
goroutine不是通过共享内存来实现并发,而是通过通信即go和channel来实现并发,所以它是一种阻塞的模型,但它实现了goroutine阻塞,但线程不阻塞的特色。goroutine是用户态的并发模型,不涉及用户态和内核态的切换成本。
GO种的GMP模型:
M:指的是machine,一个M关联了一个内核线程,由操作系统管理。
P:指的是Processor,代表了M的上下文环境,也是处理用户级代码的逻辑处理器,负责M和G的调度。
G:指的是goroutine,包含了G当前的上下文。
image
蓝色的G为正在执行,灰色的为排队的队列。
1)work stealing 机制
​ 当本线程无可运行的 G 时,尝试从其他线程绑定的 P 偷取 G,而不是销毁线程。

2)hand off 机制
​ 当本线程因为 G 进行系统调用阻塞时,线程释放绑定的 P,把 P 转移给其他空闲的线程执行。这里的阻塞大部分是由于channel阻塞而产生的,也由可能是其他的system_call。
image
从上图我们可以分析出几个结论:

​ 1、我们通过 go func () 来创建一个 goroutine;

​ 2、有两个存储 G 的队列,一个是局部调度器 P 的本地队列、一个是全局 G 队列。新创建的 G 会先保存在 P 的本地队列中,如果 P 的本地队列已经满了就会保存在全局的队列中;

​ 3、G 只能运行在 M 中,一个 M 必须持有一个 P,M 与 P 是 1:1 的关系。M 会从 P 的本地队列弹出一个可执行状态的 G 来执行,如果 P 的本地队列为空,就会想其他的 MP 组合偷取一个可执行的 G 来执行;

​ 4、一个 M 调度 G 执行的过程是一个循环机制;

​ 5、当 M 执行某一个 G 时候如果发生了 syscall 或则其余阻塞操作,M 会阻塞,如果当前有一些 G 在执行,runtime 会把这个线程 M 从 P 中摘除 (detach),然后再创建一个新的操作系统的线程 (如果有空闲的线程可用就复用空闲线程) 来服务于这个 P;

​ 6、当 M 系统调用结束时候,这个 G 会尝试获取一个空闲的 P 执行,并放入到这个 P 的本地队列,这个P优先是考虑当时脱离的P,M上由记载这个信息。如果获取不到 P,那么这个线程 M 变成休眠状态, 加入到空闲线程中,然后这个 G 会被放入全局队列中。

特殊的 M0 和 G0

M0

M0 是启动程序后的编号为 0 的主线程,这个 M 对应的实例会在全局变量 runtime.m0 中,不需要在 heap 上分配,M0 负责执行初始化操作和启动第一个 G, 在之后 M0 就和其他的 M 一样了。

G0

G0 是每次启动一个 M 都会第一个创建的 gourtine,G0 仅用于负责调度的 G,G0 不指向任何可执行的函数,每个 M 都会有一个自己的 G0。在调度或系统调用时会使用 G0 的栈空间,全局变量的 G0 是 M0 的 G0。

参考https://learnku.com/articles/41728

16, 微服务中的雪崩效应

我们在发送请求的时候都会带上超时时间,这样如果某个节点因为程序原因停止相应,也有可能是缓存击穿,就会导致在上游节点堆积的请求越来越多,最后导致大量节点无法响应宕机。对应的解决方案有:
1,尽量去除单点,保证每个节点可以自动扩容和缩容,可以结合K8S使用。
2,在请求源限流,比如在做压测的时候,每个节点得出qps为100,那么可以设置一个阈值为100*80%,当超过这个值的时候,可以给与等待提示。还有一种成熟的解决方案就是设置一个token buff,定时的向buff中投递token,比如loadtest的qps是100,那么就可以每秒中向buff中投递80个,这样还可以利用token做安全校验。
3,可以做自动熔断和手动降级。
先说说简单的手动降级,在开发的时候可以给接口文件每个接口定义Level,并且可以利用配置可以热启和热关。可以关闭一些次要功能,把资源让出来给主要功能使用。
再说说自动熔断,自动熔断可以利用现有的框架实现,比如hystrix,它的原理是当下游的服务因为某种原因响应慢,或是无法响应,为了保证上游服务的可用性,直接返回,快速释放资源。如果下游服务转好则恢复调用。每当20个请求中,有50%失败时,熔断器就会打开,此时再调用此服务,将会直接返回失败,不再调远程服务。直到5s钟之后,重新检测该触发条件,判断是否把熔断器关闭,或者继续打开。

17,分布式系统中的脑裂

image

正常情况下,此集群中只会有一个Leader,那么如果机房之间的网络断了之后,两个机房内的zkServer还是可以通信的,如果不使用过半机制,那么就会出现每个机房内部都会选出来一个Leader。
image

对于这种情况,利用Quorum过半机制,机房2不会出现新的Leader,机房1的Leader继续服务,等机房2的网络恢复后,从机房1同步数据。

18,Promuthues的使用

1,定义metric,目前的metric有如下几种类型,Gauge计数器,可以向上增长,也可以较少。Counter计数器,只有Add操作。Histogram直方图。
2,可以通过以下例子
tasksRunning = promauto.NewGaugeVec(prometheus.GaugeOpts{
Name: "r6_transfer_friends_running_tasks_total",
Help: "The number of running friend transfer tasks",
}, []string{"state"})
定义tasksRunning变量,使用的时候只要定义:
func incMetricsTasksRunning(state string) {
tasksRunning.WithLabelValues(state).Inc()
}
这里的state就是这个TasksRunning的枚举类型,比如busy, idle
还可以通过组合的状态,比如:
tasksStateChanged = promauto.NewCounterVec(prometheus.CounterOpts{
Name: "r6_transfer_friends_task_state_changed",
Help: "The number indicate friend tasks transfer state changed",
}, []string{"prevState", "newState"})
使用的时候,只需要用组合的state即可
func incMetricsTasksStateChanged(prevState string, newState string) {
tasksStateChanged.WithLabelValues(prevState, newState).Inc()
}
直方图的使用
ubiServiceInvokeDuration = promauto.NewHistogram(prometheus.HistogramOpts{
Name: "r6_transfer_friends_ubi_service_invoke_seconds",
Help: "The duration of friend transfer tasks",
Buckets: []float64{.25, .5, 1, 1.5, 2, 2.5, 3, 5, 10, 20, 30},
})
这里的buckets,最后就会统计出落在这些buckets中的数据统计图,比如执行时间<0.25,执行时间在0.25和0.5之间的等等。
然后我们启动promuthes的服务器,利用go的客户端把数据push进去,然后再启动granfana,去拉取promuthues服务器中的数据,可以完成dashboard。

19, 内存分布

image

20,关于service

service分成两种类型,一种是有状态的session服务,使用UDP协议,另一种是无状态的RESTFUL服务,使用HTTP协议。多节点shard集群,通过一致性Hash确保每次的请求或是固定的session落到某个shard中。外层有balanceloader,进行负载均衡。
20.1 匹配
匹配可以选择正在进行中的比赛,也可以是新创建的比赛。对正在进行中的比赛按照create_time进行排序,选择加入。
对当前队列中所有的队伍按照特定的加权值进行排序,比如为
Agroup_size+Bgroup_mmr+C*wait_time,按照FIFO的算法依次取出队伍,根据mmr扩散算法,找出符合当前队伍的所有avilable_groups,并构建成小根堆,然后依次把根节点放入A,B的slots中,如果有一方超出人数限制,就尝试放在另一个中,如果都不满足,则删除此根节点,旋转堆,最后一直到构建出A,B两只队伍为止。匹配成功后,再由云服务商allocate出GS进行战场服务,战场结束后回收GS。

21,REDIS

https://www.wmyskxz.com/2020/02/28/redis-1-5-chong-ji-ben-shu-ju-jie-gou/
关于缓存穿透的行为可以用布隆过滤器,也可以在DB返回错误的时候,向redis中插入一条空数据,这样就避免了向DB中请求数据,同时逻辑上也直接做了若空返回。

22,ETCD

prometheus

1, 在service中埋点,然后在service中监听,prometheus的server去应用service中拉取这些metrics。
2,counter只增不减。比如服务的请求,已完成的任务。
3,gauge仪表盘,可增可减,比如内存或是cpu。

查询成功请求数,以endpoint区分
http_requests_total{job="http-simulator", status="200"}

查询总成功请求数
sum(http_requests_total{job="http-simulator", status="200"})

查询成功请求速率,以endpoint区分,在5min中内,每秒的请求数。
rate(http_requests_total{job="http-simulator", status="200"}[5m])

查询总成功请求率
sum(rate(http_requests_total{job="http-simulator", status="200"}[5m]))

延迟分布(Latency distribution)查询
查询http-simulator延迟分布
http_request_duration_milliseconds_bucket{job="http-simulator"}

查询成功login延迟分布
http_request_duration_milliseconds_bucket{job="http-simulator", status="200", endpoint="/login"}

不超过200ms延迟的成功login请求占比
sum(http_request_duration_milliseconds_bucket{job="http-simulator", status="200", endpoint="/login", le="200"}) / sum(http_request_duration_milliseconds_count{job="http-simulator", status="200", endpoint="/login"})

成功login请求延迟的99百分位
histogram_quantile(0.99, rate(http_request_duration_milliseconds_bucket{job="http-simulator", status="200", endpoint="/login"}[5m]))

Go中的Cron库

库的仓库地址为:https://github.com/robfig/cron
该库代码比较精炼,主要的文件也只有三个,而且test case覆盖的也非常好,值得一读。
主要的原理,就是cron管理了entry列表,entry中存储了每个job的下次执行时间,cron用快排对该事件进行排序,然后用
timer来触发job。
1,cron:管理entry
2,parse:解析job的事件规则
3,spec:用来计算下一次的job时间,其中用到了位运算来提高计算效率

用的过程中,大家肯定会有疑问,就是只有AddJob的接口,却没有RemoveJob的接口,这种业务场景是有的。我目前的
解决办法是,每个cron中只管理一个entry,也就是一个job。这样我就可以利用cron的stop来结束这个job。其实看了代码,
你就会发现,这样的效率其实比一个cron管理多个entry效率要高,为什么?

func (c *Cron) run() {
	// Figure out the next activation times for each entry.
	now := c.now()
	for _, entry := range c.entries {
		entry.Next = entry.Schedule.Next(now)
	}

	for {
		// Determine the next entry to run.
                // 多个entry,这里有可能会成为热点
		sort.Sort(byTime(c.entries))

		var timer *time.Timer
		if len(c.entries) == 0 || c.entries[0].Next.IsZero() {
			// If there are no entries yet, just sleep - it still handles new entries
			// and stop requests.
			timer = time.NewTimer(100000 * time.Hour)
		} else {
			timer = time.NewTimer(c.entries[0].Next.Sub(now))
		}

		for {
			select {
			case now = <-timer.C:
				now = now.In(c.location)
				// Run every entry whose next time was less than now
				for _, e := range c.entries {
					if e.Next.After(now) || e.Next.IsZero() {
						break
					}
					go c.runWithRecovery(e.Job)
					e.Prev = e.Next
					e.Next = e.Schedule.Next(now)
				}

			case newEntry := <-c.add:
				timer.Stop()
				now = c.now()
				newEntry.Next = newEntry.Schedule.Next(now)
				c.entries = append(c.entries, newEntry)

			case <-c.snapshot:
				c.snapshot <- c.entrySnapshot()
				continue

			case <-c.stop:
				timer.Stop()
				return
			}

			break
		}
	}
}

原因就是上面代码中的排序,如果entry长度越大,效率就会越低。用一个cron管理一个entry,实际就是以空间换时间的**。

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.