这篇文章本来是昨天要写的,不过昨天忙着写密码学作业,一直写到今天早上四点多,所以只能放在今天来完成。
这个星期一的23:59是我们项目的上交截止日期。在此之外,我们还要在一个预定时间把项目的运行情况给TA展示一下。昨天下午就是我们小组展示的时间段。老师的原话是做一个demo,我之前以为是要上台作报告演讲之类的,还稍微有些紧张。结果就是我们三个人私下给TA运行一下程序,让TA知道程序运行正常,实现了哪些功能就是了。展示的时间是15分钟,结果不知不觉就过去了。
在这个项目中,前期主要是我一个人在编码,在试验了集中可能性之后(多进程、多线程、select),在最后一周内是我和当地人做了绝大多数的工作。我负责实现了两个结构体数组保存用户的信息和程序的状态,然后写一些API函数。当地人把我之前做的局域网多人聊天程序改写成服务器程序,分析客户端发来的protocol信息并调用相应的我写的函数,并根据函数的返回值生成新的protocol并回发给客户端。然后当地人写了客户端的读取用户输入,生成protocol并发送的部分,我写了客户端的接收服务器的protocol并生成给用户的信息并输出的部分。到了最后的一天,我们和另外一个中国人写了文档,我用LaTeX排版后上交。
这次项目自然学到了很多东西。首先是Subversion的使用。项目开始的时候,我惊讶的发现另外两人没有使用版本管理的经验。开始我跟中国人讨论的时候,问他有没有对于使用哪种版本管理工具的想法,他问我有没有用过Dropbox,说我们的工作可以保存在Dropbox里然后分享。在之前的暑假学期里,他和我都修了《软件工程》这门课,他们小组就是这么做的。我当时虽然很惊讶,但因为只是项目的开始,没有实际设计到编码,而且我平时也用Dropbox,就建立了一个共享目录,把当时的protocol草稿放了进去。
到了第二次我和当地人又讨论这个问题时,他竟然也没有使用版本管理工具的经验。我用过Subversion和Git,在GitHub上有帐号,本来想让大家都用Git的。但当地人好像只听说过Subversion,而且之前我的印象是Git对Windows机器支持不好,而另外两人都是Windows机器,我们于是就定下了用Subversion来管理版本。
我在Google Code上建立了一个仓库。之前那个暑假学期我在修《软件工程》课时,我们的课程的小组项目也是用Subversion来管理的。当时的项目组长在xp-dev.org上建立了一个SVN仓库,我们又是用NetBeans来开发,因此只要在NetBeans设定好仓库地址和密码,其它的都由NetBeans来办就成了。而且当时为了方便,我们是建立了一个公用密码,因此从头到尾只能看到一个人在commit。除了上次的项目外,我之前对SVN的使用也仅限于checkout别人的代码而已,因此在Google Code上建立好仓库后竟然很长时间不知道怎么用。因此那几天就只是整理了一些开发文档,放到了Google Code仓库的wiki上。后来看了一些文档,但却很少有与Google Code相关的,因此在本地建立仓库之类的命令就怎么也用不着了。后来自己试验了一下,原来直接checkout那个加密的版本,就算是在本地设定好了仓库了。之后的开发就水到渠成了。
到了上个周末,我SVN的一些基本功能都比较熟练了。因为之前用过Git,因此又去YouTube上找了Linus on Git视频再看一遍。之前看的时候多两个系统都没有怎么用过,因此收获不多,这次听了之后,则对Linus一直强调的Git的分布式优势的理解更深了。不过我现在觉得我们这三人小组(实际写代码并使用SVN操作仓库的只有我和当地人两人)使用SVN正合适,使用Git也无法体现出分布式的好处。不同于开源项目,我们的编码是有计划而且固定的。我只负责游戏API和部分客户端,当地人只负责服务器和另一部分客户端。因此commit的时候很少有冲突的情况。当两个人同时编码时,如果我们如果不是坐在一起,就会一直用MSN交流,因此哪怕发现了对方代码里的bug,也是从MSN上通知对方,对方修改并提交后我们再更新本地代码。这其实并没有把版本控制发挥的“淋漓尽致”,但刚好满足了我们的情况。开始的时候我们分别在两个branch中开发,这也不是标准的分支,对方的分支只有服务器和客户端,我的分支只有游戏API,最后由我合并到trunk中,互不干扰。这样Git的优势也很难体现出来。
第二个收获是获得了编码的经验。我之前的文章说过,我几乎没有用C语言写过什么称得上“项目”的东西。之前虽然有用C语言写过实际程序,但属于算法方面的偏多,实际应用就很少了。算法的程序虽然理论技术含量高,但实现起来基本上就是数组或者链表就OK了。而实际应用就不同了,虽然算法上很简单,但在C语言中写起来却容易出错。我们这次写的游戏就是这样,逻辑上的部分我用脑子想象就有了伪码,很快就写好了,但中间的一些底层问题调试起来却要了老命。说起来还是C语言太低级的缘故。很早就知道C语言是一门低级的高级语言,但只限于语义上的认识,实际上没怎么用C语言写过程序,很难知道其中的含义。
比方说指针,我过去看过的许多教材(包括我的C/C++语言启蒙教材),都说指针是让C语言强力的原因,因此从很早之前我就对C语言的指针充满向往,希望有一天我学好指针后也能写出强力的程序。后来真正看到指针章节后,觉得指针的概念很简单啊,书上也没具体怎么说指针怎么让C语言强力了,只是让我知道了指针和数组之间的同性,以及两者之间可以互相转换表达形式的特点,在我看来只是语法糖而已,并没有什么很大的不同。当我实际写程序的时候才觉得,很难说指针让C语言强力,事实上是C语言没有了指针就没法办一些事。归根结底还是C语言太低级了,从语法上没有别的语言中的传址的概念,因此我们要让一个函数可以修改参数的值,就只有通过指针把地址传给函数,函数通过类似汇编语言中indirect address的东西来读取或修改的地址指向内存的内容。另外的例子是数组和字符串,往函数中传递的时候也是传的地址,没有了指针都很难办到。因为C语言低级,所以并没有帮助我们弄出高级一些的传址方式,所以我们要自己用指针实现。在这次的程序中,我用保存用户信息和游戏状态的结构体定义了两个数组。按照之前在Java中的经验,这些数组中的元素自然是一个个结构体类型,结果我却发现里面指向字符串的指针在运行时却指向了同一块内存,后来在初始化每个数组元素时,都用malloc先分配内存,才解决了问题。因为C语言低级,没有帮我们自动处理内存分配这些东西,所以我们要手动设定指针并分配内存。这给我们带来了很多灵活性,但也是出错的根源之一。(本段写的未必正确)
我之前没有正式写过网络程序,之前在一次实验要把老师给我们的socket程序改写成查询公交车到站时间的程序,实际上只是加上一个事务模块,并分析发过来的信息以及生成返回信息就行了。之前以为这次的程序也差不多,结果在11月初才知道我想法的幼稚,两个客户端怎么同时与服务器通过一个端口交流,客户端怎么在和服务器交互的同时读取用户的输入,这些问题我之前都没有考虑过。像之前的暑假里《操作系统》课程里C语言库函数中函数的blocking性质似乎早就还给了老师,一直到了项目后期我才惊觉。后来在某个凌晨,在网上找了一些socket交互的代码才知道了在服务器用select来选择与那个客户端交互(我上个学期学《分布式程序设计》的时候在Perl中学过select,但没有学扎实,因此在这个项目的前期就尽量逃避这个思路,多线程也是这样),在客户端用两个进程分别与服务器和用户交互,这样才自然的解决了问题。
这次的项目还让我用gdb调试的水平有了提高(因为之前是0,所以可以说提高了很多)。我最早调试程序的经验是从QBasic开始,后来用Turbo Pascal、FreePascal来写一些计算机奥赛程序,也经常用调试器。他们虽然古老,而且还是在字符界面下工作,但都有了“图形”界面的IDE。我过去都是用IDE里面带的调试器来调试程序的。但现在写C语言,只能用gdb调试,就有点不大习惯了。在Linux下有ddd,但我一直没有花功夫学,而且在Mac下运行的也不好。gdb我知道一些指令,但却没有实际调试的经验。结果下来却发现熟练了后也没有这么困难,只监视变量和堆栈在字符界面下不如图形界面下那么直观就是了。其实我用到的指令也不多,主要是发生seg fault的时候就在gdb里面执行一次,当发生错误的时候程序终止的位置通常就是有问题的代码。或者当程序的逻辑不对的时候,也就是找到调用那段代码的行号,并通过b来设定断点,程序暂停后用p来查看变量的值、用bt查看函数调用的堆栈、用n来单步执行、用s来进入一个函数跟踪。稍微有点耐心的分析变量的值有什么变化,很容易就能找到出了什么问题。
还有一个经验是自从一年前之后,我就没有自己写过Makefile了。这次项目的编译需要一个Makefile,我于是又找了文档来研究。通过实验与改进,我终于成功的弄出了一个可以根据代码是否改动来决定是否编译某一部分的Makefile。后来写Makefile写上了隐,我又给doc目录下写了一个Makefile,来自动从LaTeX生成PDF文档。
这个项目的前期我们都没怎么行动。同组的另一个中国人因为生病,一直没有实际参与项目,我又低估了项目的难度并高估了我的实力,一直在一个人写服务器端。一直到了项目截止日期前一个星期我才发现,老师给我们分的三部分计划,我连第一部分都没有实现。于是在那个晚上,我去网上找了一些代码来学习,并再第二天开始和当地人合作实际分工写代码。大概过了大半个星期后,我们的代码写的差不多了,于是那个周六我们就把代码合并,在周日主要测试并调试,因此到周一就让代码几乎没有问题了。但周一晚上当地人在写测试文档的时候,发现了程序有我们还没考虑的bug,于是我们就立刻紧张起来,调试并添加新的判断条件。结果到最后我再整理文档的时候,时间就挺紧急了,到了截止时间之前5分钟我才运行了提交程序。慌乱的导致了我露掉了最后加进来的测试文档,我当时把当地人传给我的文档放在了本地的目录中,但忘了在SVN里面注册,所以我最终commit的时候,测试文档并没有被包括在内。我到第二天中午的时候发现了问题,并给老师发邮件,幸好老师说没问题。
在演示的时候,我们本来运行的挺好的程序,却在用户名的部分有了点问题,其中一个用户名在正常的用户名之外还显式一些奇怪的东西,估计是哪个指针没有设定好。不过TA说这不是大问题,重点是网络交互这部分工作正常就行了。
之前一个星期的工作实际上挺紧张的,因为项目设计与别人相关,经常是我想放松一下的时候,当地人在MSN上又说了一个新问题,我又要继续编码。所以我现在Google Reader里的未读项目已经接近500了。当中很少有大块时间可以干别的,唯一的乐趣就是去看袁萌的博客以及留言。昨天本来觉得可以放松一下了,结果今天早上又是另外一项作业的截止日期。我当晚到12点多才回家,并一直弄到4点多才写完。结果今天早上在图书馆匆匆忙忙的打印出作业后,老师却说作业的截止日期延伸到周五,直接让我晕倒。