历史之2018

2018年已经成为历史,当我想总结一下过去一年的所得时,却发现什么都想不起来。借着过去一年的blog和github,总算可以粗略回溯一下历史。

去年的今天,我定下三个目标:

1. 阅读lua源码,并实现虚拟机
2. 阅读《计算机程序设计艺术》
3. 实现一个软件光栅器

到今天为止,lua源码只完整阅读了GC部分,计算机程序设计艺术几乎等于没看,只有软件光栅器做了个七七八八(但其实连光照都没做完)。

下面来说一说流水帐。

过完农历年后,我先挑了“软件光栅器”来做。

一方面是因为《计算机程序艺术》和阅读lua源码都需要比较大的时间片,而软件光栅器相对来讲可能更像是快程课。同时在三个目标中,我的渲染知识最弱,对于一直致力于全栈的我来讲,肯定也要先补齐短板。

从另一方面来讲,前两个更属于内功,软件光栅器更偏向招式,容易被人看出来,早点学可以防身。

在此过程中,主要的叁考书籍就是《3D游戏编程大师技巧》,这本书介绍了如何利用《3D数学基础》中的知识,将图形渲染出来。

但是这本书实在太老了,老到作者为了程序可以流畅跑起来,做了很多优化,比如这个坑其实最后的修正版,就是一个lerp而已。但是作者没有提示出这一关键,导致我最开始渲染出的图形一直有问题。甚至都搞了半个多月。

之后断断续续踩了几个坑,不带光照版的光栅器总算将就能看了。

然而,我这个人有个大毛病,一直想克服,但是至今没有成功。当我认为我会了,我就懒得进行下一步了。

刚好这时lua5.4 work1发布了。增加了分代GC支持,我一看这是个高级特性了,得跟上潮流,下载代码一看,其实lua5.4的分代GC是在5.3的增量三色GC算法上改进而来的,加上年初定的目标,就咬咬牙花了大概一周的时间,把lua5.3的GC源码大致看了一遍。

但是由于每次我看lua源码时,闭包上值的实现,总是让我有意无意的略过去了,因此其实算不上真正把lua GC看懂,应该说是基本看了个七七八八。

刚看完luaGC没两天,差不多五月份左右,我们项目中一个战斗系统刚好也算正式定下怎么做了。

这个系统有一个极特殊的地方,就是同屏人数可能上百,这就意味在数据同步量会非常大,那么如何尽可能的降低数据同步量,成了整个系统设计的重中之重。

这时我开始困难的回忆我所知道的所有知识,比如帧同步,状态同步。每一种都有自己的适应场景,但是都不太适合我们现在这种游戏模式。当然这里所说的帧同步,其实已经退化成行为同步了。

最终,我终于打破了自己的思维局限。

谁说一定只能用一种模式,以我们游戏的模式,其实是可以两者之间进行折中,每走到一片土地后,我们可以先用状态模式拉一下所有状态,然后只要视野不变,就采用行为同步方式进行同步。这样需要广播的数据量大大减少。

其实现在想一想,我以前做过的FPS游戏,中途加入游戏,其实跟我们切视野的思路差不多,这再次印证了太阳底下没有新鲜事。

解决完这个问题,我的周末终于又是我的了。

这时我自认能写软件光栅器了,就想去解决一个我心中的痛投影贴花,这个效果我在做FPS时,别人给我说过之后,我就开始研究了。然而一都没有办法去实现,即使找到文章都看不懂为何能达到这样的效果。幸运的是这次竟然真的看懂了。

解决了,又不知道要干啥了,刚好看到想到最近微服务这么火,那就研究微服务吧。即然都说docker好用,那就把我VPS上的进程全部Docker化吧。

在docker化时,发现了rssreader的一些bug。

在修改bug过程中,发现了一个奇怪的现象,在某些情况下杀死silly会进入死锁状态。其实之前也出现过几次,只是当时没有在意。这次打算认真查一下到底是什么原因。

最后发现,其实是因为lua中的__gc函数使用不当,造成了GC竞争, 这种情况只会以程序退出进行清理资源时才会出现。

具体情况是,由于GC竞争,导致在不同的GC函数中double free了同一个指针,而在进程退出时,jemalloc由于这个double free死锁了。

在做上面这些事的同时,我还有一条支线任务,就是阅读《计算机程序设计艺术》,由于这本书很多地方都会计算指令级开销,让我有种过早优化的倾向。

所以周末翻了翻软件工程神作《Unix编程艺术》,没想到这次解决了一个我当时工作中碰到了一个很棘手的问题。

这时,已经八月份了,时间已经溜走了将近三分之二了。也就是从八月份起,我需要处理很多生活中的问题。

因此后三分之一的时间,我其实并没有太多的精力去探索了。在这期间只是把以前的想法落实到代码上去。

比如我做了一个客户端差量更新工具,这个工具其实我早在三年前,就有打算做了,只是当时一来没需求,二来兴趣也不是很大。这次之所以动手做,还是因为我对他们引入的第三方框架的更新工具,很不满意。

再比如,我封装了一个lua框架forUnity,同样是因为我对现有的框架很不满意。

总结到现在,我发现一个规律,不管有没有生活上的其他事,我每年在10月分之后,基本代码和灵感都呈现断崖式下跌。

是时候定一下,2019年的目标了:

1. 断续阅读lua源码
2. 继续阅读《计算机程序设计艺术》
3. 为软件光栅器加上实时光照和阴影
4. 转向Unreal阵营,这次一定要先下手为强
5. 如果有精力的化,尽量涉及一下深度学习,以打码工具做为项目驱动

再见! 2018。

再见2017

2017年过的格外的快,似乎2016年元旦刚过去没多久,2017年的元旦又来了。

大致回溯了一下2017,好像什么也想不起来了。只好重新扒了一下Blog,才慢慢回想起这一年到底干了些什么。

仿佛是为了印证“计划赶不上变化”这句话。2016年定下的目标,依然没有全部完成。这几乎是一个惯例,每年初定下的目标,到了第二年去看时,一定没有100%完成。而我也已经习惯了这个规律。

2016年目标进度如下:

luaVM的源码并没有读完,只是其中断断续续的读了一部分。到目前为止,已读代码包括:string的实现,table的实现(不包括metatable table的实现), gc的实现,寄存器的分配及部分语法解析和OPCODE生成。

设计一个虚拟机更是一点都没有启动。

计算机程序设计艺术,仅仅读到了卷1的2.2.3节。

blog系统也只做了基本的主题展示和数据存储,还缺一个庞大的后台系统。

spark和zookeeper分布式源码更是一个也没有看,只研究了一下paxos协议。


在看lua语法解析部分期间,而刚好有同学跟我讨论起C语言为什么没有import机制。当时脑子短路,竟然花了大半天时间,炮制了个预处理器就这样把import给支持了,虽然很简陋,但至少可以证明可行性。我个人觉得这个思路非常值得一提。

之后很长一段时间都在研究如何让服务器具备高可伸缩性。并最终实现了一套与韦蝠王在文中提到的“类型4”服务器架构相似的架构。

在实现场景服务器时,接触到了Arena Of Interest的概念,并自己试着实现了一份

这个可伸缩服务器架构是基于一个demo做的,毕竟有需求,才会知道如何去设计。不过最终在框架成形后,demo由于缺少资源等原因没能继续下去。

后来微信小程序开始预热,加上我缺一个移动端好用的rss阅读器。就在小程序的基础上做了一个RssReader,虽然可能没有想象中的那么好用,但是对我自己来讲基本上够用了。之所以做个,除了我缺一个rss阅读器以外,也是因为当时有种陷入瓶颈的感觉。我需要跳出舒适区,来换个领域换个思路。

去年就说过,我的短板是数据库。在实际项目中虽然也使用数据库,但是并没有大幅度使sql数据库。因此接下来花了近一个月的时间学习了一下mysql的使用和sql语句的优化。同样为了跳出舒适区,最初计划学习完mysql之后,把rssreader的后台数据库从redis换成mysql。但犹豫过于懒惰,未能成行。

学习完mysql的过程中,就接触到了,一些分布式存储和一致的概念,比如二段式提交,Paxos协议。其中二段式提交还比较好理解,Paxos虽然描述都很简单,但是我琢磨了将近一个月才大致理解,为什么他会保持一致性。后根据理解实现了一个不考虑效率的Paxos协议的demo

回过头来看,2017年其实并没有干太多事,主要精力都花在了分布式上。如果为2017年取一个总结词,那应该就是”分布式”了。


在做游戏demo时,我顺便学习了Unity和Shader。虽然也能写,但是总觉得有种“浮沙筑高台”的感觉。

因此2018年,我的主要精力应该在图形引擎上,目标如下:

1. 继续阅读lua源码,并实现虚拟机
2. 继续阅读《计算机程序设计艺术》
3. 实现一个软件光栅器(要支持实时光照/阴影大部分图形引擎的功能)

回首2016

16年初我又辞职了,即使在满足我的薪资要求的情况下。主要原因就在于,我的技术路线图受阻。

原本按我的打算,在有了少许高并发和socket经验之后,我需要进一下积累数据库使用实战经验。

然而在上家公司中,数据库部分操作是单独剥离出来,由java实现的。
而我所维护的C++部分仅仅处理高并发和正常的战斗逻辑并不涉及任何数据库部分。

因此,为了顺利继续我的技术路线图,我违反了自己的约定。

如果要找一个词来概括我2016年的大部分内容,那就是lua。

是的,正是由于这门语言,让我接触到了不一样的世界,函数式编程、面向原型编程,动态语言,虚拟机,协程,垃圾回收等各种以前闻所未闻的概念。

对于垃圾回收,我以前是有鄙视心理的,因为我曾一直有一个错觉,当你把这个对象置为nil,则此对象即会被释放,因此垃圾收集仅仅是屏蔽了malloc/free而已(就像是C++用引用来代替指针一样),在经过一段时间实践之后,才发现之前的短视。 并由此开始研究垃圾回收算法,如三色标记清除、引用计数,指针探测等各种不同的垃圾回收算法。

在luaVM源码中对于弱类型的实现,可以说是以C语言的方式来对ADT做出了完美诠释。其实说穿了,本质上抽象都是一样的,在C++的世界里我们还可以用OO的方式来实现, 这也再次说明其实大部分OO上的编程技巧并不是OO语言所专有的,重要的是在这背后的思想。

对于C++的特性,我一般是尽可能的少用C++提供的复杂的特性,对于模板我也是仅仅在极其必要是才会使用。因此模板我其实并不是非常熟。换言之,并不能写出很精秒的模板代码。但是新公司中将板模用于自动序列化的做法确实让我惊艳到了

在接触到数据库使用之后,由于我们使用的是内存数据库,我曾一度限入内存数据库和进程内cache之间取舍的纠结之中,最后终于幡然醒悟(当然也可能是误入歧途 :D)。

在不断重构silly的过程中,除了对lua及luaVM的了解不断加深,对于TCP/IP在linux实现的实现方式也有了更深入的了解。


不过遗憾的是,2015年我制定的目标,并没有完成。

一方面是因为时间没有想象中的那么充足。
另一方面则是因为初次接受这么多新奇的概念,在1年之内我很难做到完全消化。

因此,到目前为止, 我也仅仅做到对luaVM的源码有些熟悉而已,只阅读过部分luaVM中的源码(gc部分)。更谈不上脚本语言的设计和虚拟机的实现。

那么2017年,我的目标将是2016年的延续:
1. 阅读完luaVM的每一行源码
2. 设计一个虚拟机(如果有可能实现一个栈式虚拟机和一个寄存器虚拟机)
3. 阅读《计算机程序设计艺术》
4. 实现一个完整的blog系统(基于silly)
5. 研究一下主流的分布式系统源码如spark,zookeeper等

btw, 这一年的lua使用下来,我发现了一个事实。

动态语言并不简单,由于缺少类型系统等一些除错手段,因此编写动态语言代码往往需要更深厚的编码经验和准则。

当然这也是正常的,毕竟做任何事都是要付出代价的,动态语言简洁的代价便是,你需要有更清晰的头脑。

如果你不以为然,请试着用lua写一个1W行代码的的逻辑一试便知 🙂

2015

眨眼间2015年已经过去了, 也许是我最近记忆力变差了, 总感觉好像昨天才辞职的样子。

在2015年年初辞职时, 其实我的内心非常的纠结,一方面是安逸,一方面是未知, 很难下定决定到底何去何从。

终于还是求知欲战胜了懒惰。因为我发现如果再干着同样的工作,我的技术不太可能提高太多。所以最终我还是选择了辞职,由于上一份工作的原因,相比前端程序来讲,我对于后端的兴趣更大。因此在找工作方面,我更着重于找服务器编程方面的工作。因此后来就找了一份游戏服务器工作.

现在想想刚入职时闹的一个笑话都还脸红。才入职时,基于对新人的保守使用,都是先编写UI逻辑。而客户端的UI逻辑是采用lua+异步的方式来实现的。而我当时并不知道什么是异步,甚至还想在lua中使用sleep来保证代码的时序逻辑。直到后来我在silly中集成luaVM时,我才终于知道客户端lua+CPP中异步到底是如何工作的。

虽然当时我很想直接去写服务器代码,但是即然写了客户端,也就花了大约2周时间去学了一下《3D数学基础》。再说依我的性格虽然准备写服务器, 但一点都不了解客户端,那也不是我的性格 :), 不过在学习过程中,我发现游戏客户端其实就是在用各种自然学科的知识去模拟整个世界,也就是在这时候,我却突然对客户端代码也产生了兴趣。

这时分给我的UI工作刚好做完, 手头上暂时没什么事, 就硬着头皮研究了一下客户端的引擎代码部分。物理部分和声音部分都是不开源的,因此能研究的也就只有动画部分了。在研究了几W行动画相关代码之后,终于大致知道了一些动画渲染的原理。比如骨骼动画,动画融合等。

粗略研究了一下动画原理之后, 就顺便研究了一下客户端多国语言的实现。上一份工作中,我对于自己多国语言的设计非常不满意。看完这个多国语言的实现之后,我才明白原来当是自己是过度设计了,在简化了设计之后,我只花了几十行就重新实现了一个多国语言模块。

下一阶段的开发任务来了,我终于能够编写服务器代码了(虽然只是服务器中很简单的逻辑部分), 不过却见到了一些我从没想过的C++的用法, 由此又顺便学习了一下C++的一些高级用法(比如模板推导等)。与此同时我的个人服务器框架也正基本上正式开始编写了。

在接触到服务器编程之后, 由于是TPS游戏都是开房间的,因此我一直都是思考如何才能让所有玩家都可以在同一张地图上进行游戏。在此之前一直都想不通要怎么做。在网上搜了很多文章也都没有找到什么好的办法,我只好暂时暂停了此问题的思考,转而去研究了《redis的设计与实现》来换换脑筋。在《redis的设计与实现》的最后部分,作者讲了一下redis对于集群的设计,一下令我慌然大悟

在下一个开发阶段,由于业务需求,我终于要单独写一个服务器,想想都令人激动。在摆脱了以往框架限制的同时,我也需要从头来实现一些机制。 正是在实现这些机制的同时却让我对TCP协议有了更深的了解,比如tcp的端口绑定, TIME_WAIT等。就这样基本上都是上班写代码,然后发现自己有什么理解错误就下班回来改silly.

终于开发任务完成了, 又有一段时间可以搞自己的东西了.由于很多UI上的bug都不是100%必现的,而lua的调试手段一般就是加print来打印一些变量的值。当出现bug时我们需要打印一下当时一些变量的值就非常的不方便,于是就琢磨着写了一个简单的lua调试器.这样在出现bug时就可以拿lua调试器直接attach上去来打印当时的call stack以及以些变量的值等需要的调试信息,可以大大减少了bug重现的需要。

在很早以前就知道google protobuffer这个东西,而且也对编译原理有所眼馋。在silly中上层逻辑都是使用lua来编写,在socket传输序列化时很不方便,因此老早就想实现一个类似google protobuffer的东西了。趁还有时间就花了几周看了一下编译原理的词法和语法部分,然后实现了zproto, 用作silly的配套lua序列化库。这个库直到上周才终于完成了。


总得来说,如果2014年我得到的是新的技能的话,那么2015年我得到的其实就是经验。而获取经验的最大途径并不是来自于工作,而是对于silly的一次次的重构甚至重写。

所谓的经验其实就是各种取舍,在编码的过程中, 总会遇到各种情况,在这时就需要靠经验来进行取舍。

比如linux中的seqlock就是估算了index的递增速度以及linux调度时间来大大降低了实现的复杂度。

在silly中关于连接号的管理也同样借鉴了类似的方式大大降低了实现的复杂度。

因此在编码过程中,我们需要会的不仅仅是技能,还有对于问题的取舍,怎样做到功能和复杂度的平横,才是一个程序员迫切需要解决的问题。

在2016年,我打算阅读完luaVM的源码,一个脚本语言的设计,一个栈式虚拟机的实现,仅此而已。

lua编码风格

最近都是在看lua代码, 并在其基础上进行修改和增加功能. 在代码中看到了不少个人感觉很不好的现象, 就忍不住吐槽一下.

lua做为一门动态语言, 其弱类型及灵活性, 的确大大加快了开发和修改的效率. 但是这种自由有时不加以限制的使用, 有时候可能会造成很严重的后果.

先以我有限的理解说一下lua语言对于访问控制的有限支持.

定义变量或函数只有加上local才代表局部变量或函数, 否则只要这个模块被加载就可以被其他模块访问.
require代表要去加载某个模块, 如果两次调用require去调用同一模块并不会造成同一模块的多次加载.
lua5.1之后加入的module(…,seeall)函数可以自动导出lua函数中的非local函数及变量

在我所看到的代码中到处都是大文件(大的都有1W多), 魔数, 全局变量, 循环依赖, 如果某一模块已经加载并不会再去调用rquire函数等各种问题.

全局变量的存在使得看似将程序分了多个模块, 但是由于全局变量可以被所有模块相互访问和修改, 全无依赖层次可言. 最后不得不将这些模块化为一个整体模块来使用.
魔数让看代码的人搞不情作者意图, 还多了许多可能修改错误的机会.
并没有显式调用require会导致实在看不情模块之间的依赖关系, 对于把握整个程序的结果来看非常不利.
单个文件1W行的代码就像是一大滩稀泥放在那, 让你敢看不敢碰.


虽然lua给予的代码访问限制很少, 甚至于还提供了module(…,seeall)这样方便的函数. 但是如果我们不加限制的去使用只会使程序成为一滩无法维护的烂泥.

《C Programming Language》中有这样一句话 “…限制不仅提倡了经济性, 更在某种程序上提倡了设计了优雅”.

在参考了网上大神的lua源码和《lua程序设计》以后, 我觉得可以采用部分限制来提供代码的可读和可维护性.

require函数的作用就是去加载一个没有被加载过的模块, 并将此模块的返回值记录下来, 作为每次调用require函数的返回值来使用. 那么便可以在require函数上来做文章.

首先除非有必要, 应该舍弃module(…,seeall)函数的调用, 采用返回表的方式来返回某个模块的所有导出接口. 代码如下:

--bar.lua

local bar = {}

local v1 = 3
local v2 = 4

function bar.test()
print("----bar.lua---, v1, v2", v1, v2)
end

return bar

--foo.lua
local bar = require("bar")
bar.test()

虽然相比module(…,seeall)函数来讲, 代码中可以要多打几个字. 但是相对于这种方式提供的好处来讲却是巨大的.

因为采用require函数的返回值来调用相应的模块函数, 那么就限制了只要模块有依赖就必须去显式调用require.
就算不小心写漏了local, 只要不会有意在bar表中去赋值, 那么外部模块也并不能去访问bar模块中的v1和v2变量

由于lua中字符串管理是采用类似《C接口与实现》中的atom的管理方式, 因此字符串比较与整数比较的效率几乎是一样的.
那么其实可以通过直接用字符串传入参数或采用下面这种方式来避免魔数.

--foo.bar

local foo = {}
foo.state = {ONE = "the first step of state machine", TWO = "the step is do something"}

foo.test(step)
if(step == foo.state.ONE) then
dosomething()
elesif (step == foo.state.TWO) then
dosomething()
end
end

毫无疑问我更偏爱上面的方式, 即省了注释, 还能有与使用magic number一样的效率, 在调用此函数时, 还可以减少击键次数.

当然这些限制, 只能某种程序上解决一定的问题, 提高了代码的可读性. 但是像循环依赖, 刻意的全局变量等设计问题, 是无法靠变这些限制来解决的.

入职一周

经过了大半个月的折腾, 总算是换到纯软件公司了。 有时不能不感叹天意弄人, 这次依然没能如愿做纯逻辑的后端, 不小心做了客户端。

以前对于游戏开发领域几乎没了解, 所以其实这一周几乎都是在代码和百度中度过的。

在上一份工作中, 由于工作内容的特殊性,软件需求一直变化和增加, 所以一直通过研究软件工程来做到更合理的模块划分和接口定义, 以便代码对于未知需求有一定的应变能力。 虽然需求多变, 但是其实现难度变不高,几乎不会用到算法, 惟一一次用到的一个算法(快速排序)还在在离职交接过程中解bug时才用到的。

长时间的思维定式都差点成功的让我认为, 软件的编写其实就是软件工程, 那些传说中算法, 数学等几乎不可能用到。

然而在科普游戏开发的过程中,意外发现游戏制作其实就是去创造一个新的世界。 在这个世界中一样有人, 有光线, 有重力加速度等等。

因为需要去模拟真实世界的很多属性, 那么相应的也就会去会涉及到诸多领域的知识, 如物理, 数学, 算法, 人工智能等。

抛开其学习难度, 那么这种多姿多彩的程序世界无疑是我更想去做的。

想象一下。有一天, 可以亲手将学会的知识作为另一个世界的规则去运行,那该是多么令人激动和兴奋的事啊。


btw
周三时厚着脸皮去请大牛给推荐了本书<<3D数学基础>>.

今天挤了半天时间看了6章就忍不住感叹, 要是以前我上学时教材都这样, 我当年怎么会学不好啊。

就拿最简单的向量来说, 记忆中以前学数学时, 上去就是公式怎么样, 碰到这题怎么算。 结果算是会算了, 可是依然不知道什么时间以及什么情况下可以去使用。
结果就是公式背下来了, 考试也考过去了, 但是潜意识里就觉得, 这玩意我这辈子都不会用到他。

再看看<<3D数学基础>> 每一个公式都会去讲解其内在几何原理, 使用方法等, 看上去清晰明了, 简单易懂。

迟到的2014年总结

本来这篇总结早该写了, 只是最近一直在纠结网络编程问题没能静下心来去总结, 今天终于有时间就仔细总结一下以勉2015. 说是2014年总结, 其实应该算是毕业之后的总结, 因为13年才工作半年实在没什么好可以总结的, 因此拖到14年一并总结了.

13年7月毕业后, 我装着MFC 2K行的项目经验和几个玩具的MCU代码以及二千块钱便和同学一块来到了上海. 刚到没两天, 火车上的觉都没补够就被昂贵的消费打醒了. 房租隔断间都要700多(因为住隔断间我同学把笔记本也丢了), 每顿伙食费也要12块以上. 更要命的是找工作总是需要简历和坐地铁, 可是简历一张打印至少1块钱, 一趟地铁也至少要3块. 于是托师兄到他们公司偷偷打了两张简历, 后来又怕不够就又请还没来上海的同学帮忙打了一叠拿过来. 比较悲催的是我的简历才用了6章, 我的资金链就快断了. 在无奈的情况下, 只能在面试的六家公司中比较了一下, 挑了一家稍微正式点的公司, 也就是我现在所呆的公司.

在面试时说的是做Firmware然后偶尔可能需要用汇编开发(在毕业之初, 我其实是想做Firmware工程师的), 但是入职第一天, 给了我一份自定义汇编的英文文档、一个示波器和一个m25p16说这是试用期考核项目. 我一下子就傻眼了, MCU中C去驱动SPI_FLASH很随意, 甚至于说用FPGA都是小意思. 但是给我一个自定义汇编的文档, 然后就让我自己用示波器边写边调是我万万没想到的, 在上学时只用过电子束的示波器, 哪见过这么高端的数字示波器. 后来阿汤哥见到我一筹莫展的样子好心教了我示波器怎么用, 于是终于开始了我人生工作中的第一次代码经历. 经过一周多之后我终于可以用那自定义汇编操作M25P16了, 自以为万事大吉了. 原来还不算完, 还有要MFC dll, 还好有MFC功底, 搞定.

我以为原来这些工作熟悉之后是如此的随意, 等到正式分配给我的第一个芯片后才发现没我想的那么简单, 原来之前的考核项目中少了第一个步骤, 画原理图并layout. 原本画板对于我来说也不算什么, 但我只是会画数字电路PCB而已, 对于模拟电路和电源部分根本连了解都算不上, 于是我的第一关就卡死在电源上, 最后以学会一个新名词”纹波”(用于形容电压的稳定度)为代价解决了这个问题. 第二关就是协议,那颗芯片的调试接口几乎没有文档, 最后终于在某角落里找到一个中文文档后来还发现有错误, 过了这两关我总算是成功支持了一颗芯片, 再回头已经是一个月后的事了.

做完这颗芯片之后我心里很矛盾, 工作内容与我想的完全不一样, 这哪里是Fimrware工程师, 这其实是杀死脑细胞而又没收获的苦力. 之后就一直在想辞职的事, 恰好这时候又分给我一颗很变态的芯片, 半个月后发现自已依然如上一次一样处处碰坎, 别人一周做完的到我这就得一个月, 总觉得自己实在不适合做这个工作. 于是打定主意第二天找老板辞职. 每每回想起来, 都不得不感叹这是天意, 在回去路上碰到了一个熟人, 我与他在地铁上一直讨论到下地铁, 然后就决定熬到年底再说.

在第三芯片毫无起色时, 公司正在准备做新架构, 他们又出重新制定了一套汇编语言, 汇编器已经完成, 但是他们没有调试器UI. 这里要再次感谢阿汤哥的帮助, 他们当时都有人在忙, 然后看我芯片支持做的毫无起色,在阿汤哥的提议下这个任务终于放到我头上了, 当时高兴了老半天了. 他们让我估时间, 我不想失去这个难得的机会, 于是直接说一周内搞定. 后来才发现一周内搞定简直是天方夜谈, 就凭我那半调子的MFC基础, 周一到周日每天都加班, 最后也才出了个bug百出的简化版.

后来公司出了一件不大不小的事, 两位软件工程师先后离职, 公司正准备开始做新架构软件, 但是又找不出做软件的人. 于是我就如愿以偿的顶了过去, 当时想终于跟芯片支持说拜拜啦.

然后我就开始做新架构软件demo啦, 最初开始做时只是做了一个MFC程序, 然后多任务模拟进度条而已. 然而一切都没有我想象的那么简单, 到这时我才发现我的软件知识弱爆了. 第一步就碰到了一个很奇怪的问题, 我开了8个线程去跑8个进度条, 这8个假进度条函数是完全一样的, 但是他们的进度却完全不同步, 于是我第一次知道了在多线程中其实sleep(0)也是有用的, 虽然windows号称是抢占式操作系统, 但是还是需要我们在恰当的时机调用sleep(0)让出cpu的使用权来让程序跑的更均匀. 在后来总结多线程时总觉得这么多线程怕cpu会跑死, 于是就有了使用状态机去模拟多线芯程的做法, 当然其实也只是想想, 虽然可以做到但是很麻烦. 直到前不久才发现其实coroutine这种工作模式应该比状态机模拟多线程要好的多, 据说在windows下可以使用fiber来实现, 在linux下可以用swapcontext函数来实现, 具体还没有深入研究.

demo UI做完之后, 老板提出新的设想, 他觉得把数据处理部分和UI逻辑处理部分混合做在一起不好, 他觉得整个软件应该分成两块, 一个只是处理用户逻辑, 一个用于处理所有数据作业. 提到这种架构,我不得不说一句, 虽然老板是搞FPGA的, 但是他这一观点真的很不错, 与大名鼎鼎的MVC模式有不谋而合的味道, 后来我在做UI时采用了类似的方法. 然后我们就把逻辑处理部分定义为后台, 也就是TCP端的server端. 但是Server一词被引入后老板就开始有点胡思乱想了, 他觉得要把软件做成UI可远程控制的模式, 于是我们花大力气去做远程文件浏览器等很多不必要的复杂设计. 现在软件开发已经快结束了, 我可以很负责任的说这完全是过度设计, 为了所谓远程操作功能, 我们不得不在设计上牺牲很多其他功能以保证这个功能的实现, 而且至目前为止这种设计可能还残留着一个大坑.

在demo做完之后, server刚做了个开头, 公司招来了一位有多年经验的软件工程师, 所以最初socket通信模式是他来实现的, 但是他也只实现了socket通信那一块就离职了. 他走之后我来维护他那部分socket代码时总是很容易搞错, 于是把socket部分给重写了. 在实现socket命令处理之初我在把socket命令放在每一个模块的顶层去调用, 但是没多久我就发现这种实现的坏处了, socket相关的操作到处都是, 完全不便于移值和维护. 于是花了一周时间将所有socket命令集中实现为一个模块,也可以称为中间件, 这样以后不管是socket, CLI还是PIPE, 甚至于将TLV的通信协议换成文本通信都不会产生太大的问题, 我要做的只是换掉这个中间件而已, 这也是我第一次在软件开发中引用了分层的概念.

当我将socket单独实现为一个模块之后, 其实之前就存在但是不明显的问题现在就很明显了. 一些structure, Server要用, Client要用, socket通信要用, 为了满足高内聚低耦合, 在实现之初我都是定义三个structure, struct server, struct protocol, struct client, 然后在在模块与模块交互处一个字段一个字段的去转换, 这个问题一直困扰到我今年下半年, 后来才被我想通. 其实软件开发除了设计上的智慧外可能还需要一个度的取舍. 比如如上需求, 有些structure几乎是与整个软件相关的, 使用这些structure的模块其实是为这些structure来服务的, 那么这些structure就应该单独抽出来时做成全局头文件, 让各模块包含. 当然这也是需要自己去衡量哪些structure定义为全局最好,哪些structure定义为局部分有利于高内聚低耦合.

软件的重中之重就是数据解析部分, 设计之初只有一个数据解析模块, 后来随着需求的增加我发现不同的模式下其实数据解析除了接口一样之外, 其UI逻辑和解析逻辑完全不同, 而且不能保证对于数据解析模块的需求总是固定不变的. 在一次讨论上我提议根据open-close原则, 将数据解析模块拆分成三个具有相同接口的DLL, 然后根据ini文件来配置何时使用哪个数据解析模块, 以减少不断变更需求时更改代码对于其他数据解析模块误修改的风险. 由于数据解析模块也需要UI, 所以每一个数据解析模块被拆分成两部分, UI逻辑部分和数据解析部分. UI逻辑部分和数据解析部分中间采用tcp协议进行通信. 由于不同的数据解析模块的UI完全不一样, 所以更不要想定义一个结构体去通用所有数据解析模块的UI配置信息了, 更何况有可能还有第四个数据解析模块的出现. 于是我们在软件中首次采用了透明的概念, 即在UI的DLL接口中出来的是unsigned char数据和实际size, 在server端的DLL接口处进去的是unsigned char数据和实际的size, 然后在各自的DLL中转为相应的结构体.

这个软件其实严格来说算是一个平台, 中间还需要一些工程师写DLL插件. 当基础功能实现差不多时, 平台也总算初具雏形, 终于迎来了换架构热潮. 在一波又一波的问题之下, 最终发现其实那些DLL插件原本可以更简单, 于是我用实现了一个DLL导出基类, 然后给写DLL的插件的工程师用. 这个DLL插件和插件基类DLL是在Client下使用的, 正如前面所说由于不得不考虑Client和Server可能不运行于同一台PC上, 最终我提出使用将DLL通过socket复制到Client当前目录, 然后再使用LoadLibrary进行本地加载.

平台的基本功能该有的都差不多了, 随着写DLL插件的工程师编写插件的增加, 每天依然会有很多问题, 但是有些问题其实只是关于代码的实现问题. 当时下班后在观摩nignx, 恰好看到服务端的所有程序都是有log的, 而且log都分级的. 为了不影响量产使用时程序的执行效率, 我改写了log模块, 为log分级, 然后在debug级别打印了许多代码的执行过程以便其他工程师可以方便debug, 如果需要很底层的log信息, 则去Server的配置文件去定义log级别为DEBUG即可.

在软件基本功能还没完成时就提出了uniquekey功能, 结果这个功能反而是最后一个实现的. 这个模块的功能就是用来生成一个惟一序号, 供其他线程使用, 典型的生产者消费者模式. 最坏情况是1个线程生产, 64个线程消费. 以前对锁的效率测试过, 所以对于这种线程极多的情况下我是很忌讳用锁的. 在研究过DisruptorDisruptor框架中的队列实现方式之后, 仿写了一无锁队列出来, 使生产者消费者通过队列来消费, 由于无锁队列的数据拷贝上是可以并行的, 因此会快上不少.

在软件使用过程中, 开始有人报怨server启动慢的问题. 最后发现有一部分原因是软件使用xml来充当数据库的角色(大概几十M), 而且采用了非常规的定义方式(xml中几乎全是属性, 没有值的存在)导至了普通的xml解析器速度不是非常理想. 为此我专门针对这种特殊xml的用法重写了个xml解析器.

在软件不断添加小功能和改bug的过程中, 我一直在反思软件的模块化到底应该怎么进行. 虽我尽了最大努力, 但是在不断维护过程中, 发现我的代码组织方式依然不够理想. 为此我先是使用代码模块化(一)的方式去重构代码, 使代码依赖关系变得清晰, 最后却发现我的代码模块成图状完全没有层次的概念. 经过一个月的再次反思, 我借用了少许面向对象的思想, 采用代码模块化(二)的方式重构了代码. 重构完成之后发现代码像是树形了, 而且只要接口定义好之后结构就会非常清晰. 遗憾的是在重构完之后由于其他非技术原因没有使用上.

总得来说这一年半, 从无到有的开发一款软件还是感触颇多, 长了很多在书本上不能体会的知识. 有些东西只有做过再去看书上写的, 才会有共鸣. 不去经历永远也不会知道, 度的权衡永远也不会提高. 最近越来越觉得并发和网络是两大流行趋势, 可惜windows在此都不占优势, 于是准备转向linux平台, 在1月1日格了win8装上Debian 7.7也算有象征意义吧.

关于配置文件协议制定

今天关于某些UI设置怎么保存以便下次加载时重现上次UI设置开会做了讨论(一共就3个人^-^!),我提出使用文本化协议将UI设置存储为文本协议,当下次点开UI时首先去解析设置文本,然后重现用户的设置。
没想到的是,我理由都没提出就被两人集体反对。反对理由很简单,他们想用二进制方式来存储,这样一个fread就可以将一个structure读出来,连解析都省了。
那么我就在这里陈述一下理由好了。
其实理由也很简单:
1. 因为设置不多, 就算用C来解析字符串,也费不了多大功夫,效率不是问题
2. 如果以后UI设置增加操作项,那么这个structure势必会被增加字段,那么对旧的配置文件将是灾难性的.下面举例说明:
——————————————————————————————————————-
使用二进制结构配置形式如下:
假设有这样一个结构体:

struct a {
wchar_t a[32];
int b;
}

然后在配置文件里面存了struct a aa[5];这么一个数组。乍一看,存成二进制挺好, 一下就全读出来了,不用解析效率还高。
那么假设这么一种情况,因为UI变动或其他原因,必须要在 struct a;增加一个字段int c;那么别人用旧版软件保存的设置都将作费,因为所有的变量都会依次偏掉sizeof(int)个byte.
————————————-
如果使用文本将会是如下方式:
txt文档中以 a, b n的形式存储, 以n作为一项的结尾。
如果UI变动或其他原因, 必须要加一项c, 那么只需要将每一行改为a, b, c n即可, 如果旧版软件保存的配置文件中没有c字段,那么置默认值就好,这样最起码不会说造成解析错误之类的缺陷。
另外保存为txt档,对其他编辑工具是开放的,用户设置可以手动编辑这个文档来减少UI操作,有类似脚本功能。

程序员之恋

美国的贝尔实验室设计了最初的C语言

刻在UNIX操作系统距今已有三四十年

你在屏幕前凝视数据的缱绻

我却在旁轻轻敲打键盘把你的梦想展现

循环 递归 贪心 动规 是谁的从前

喜欢在匈牙利算法中你我牵手的画面

经过MSRA门前我以大牛之名许愿

思念像斐波那契数列般漫延

当软工沦落在设计的文档间

算法依旧是永垂不朽的诗篇

我给你的爱写在程序间

深藏在最长不下降子序列里面

几万组数据流过后发现

我的心依然不变

我给你的爱写在程序间

深藏在最长不下降子序列里面

用无尽的代码刻下了永远

那已保存千年的誓言

一切又重演我算了很多遍

时间复杂度还是趋于无限

我只想要这样永远链接在你的身边

 

注:不知出自何出,无法标明转载地址

Win7中IE8中二级链接打不开

我朋友最近老是吵着IE坏了,今天抽空去看了一下,发现症状如下,打开IE后可以打开百度,但搜索后打不开二级链接,上网搜索发现网上有很多和这种情况一样的朋友,也有很多网友提供了很多方法,但大都不太管用!在这里贴出自己的经验,希望给有和我一样情况的朋友一点帮助!

 


思路是这样的,既然IE坏了,肯定是某个DLL文件失效或是某些文件损坏最省事的方法就是重装一下IE。能基本解决大部分IE问题。

 

1.打开控制面板 —> 程序

出现如下界面

然后单击“打开或关闭Windows功能

出现如下界面

2.将其中“Internet Explorer 8″勾选掉,然后单击确定。

然后根据提示重启电脑,在重启过程中会出现类似更新补丁的界面。

3.重启后再按前面步骤把



中的”Internet Explorer 8″勾中,然后再根据提示重启电脑,重启后发现IE的问题已经消失了。这种方法可以解决绝大部分IE问题,缺点是稍嫌麻烦!