邓世龙的自留地

兼济天下则达 独善其身则穷

月度归档: 2013年12月

以后再也不参加暴饮暴食的活动


4月份的时候,因为混乱的生活,肠胃受到重创,阑尾炎和胃病接踵而至,吃了些药之后,慢慢调养才变好一些。工作之后,每天早上来不及去店里喝粥,于是重新习惯喝牛奶和面包的日子。每天早上,一瓶酸奶,一瓶鲜奶,两个面包,慢慢的,营养也跟上去了。通过锻炼身体,坚持跑步,肌肉更加结实,体能也有了质的飞跃,而肠胃的状况也变得相当健康。然而前几天公司的活动,让我知道,这点健康还是非常脆弱的。

圣诞节,于是公司搞了几个小游戏,其中一个游戏是暴饮暴食,也就是团队6个人要在最短的时间内吃完三个汉堡,喝完三听冰啤酒。经过分配,我义不容辞的担任起喝一听啤酒和半个汉堡的任务。老实说,自从那次胃病后,我一直都不敢喝冰的啤酒,然而这种时候还是得挺身而出。喝完啤酒后,等队友吃完半个汉堡,又接着往下吃汉堡,几乎没有咀嚼就吞下去了,这完全不是我的风格,要知道,我吃饭时都是细嚼慢咽的,这样我的胃才会消化。正因为这样,我的胃开始反抗了,腹泻,胃疼接继而来。而我花了5个多月建立起来的牛奶面包体系也付之一炬。直到今天,肠胃才舒服一些。

于是我开始怀疑参加这种活动的意义。也不知道是谁想出这样的活动,说是为了团队合作,可是我不禁要问,团队合作就是要靠摧残自己的身体来建立的?在我看来,团队合作的建立更应该是通过解决问题,大家集思广益,这样才会觉得有意义。而这种暴饮暴食的活动,除了伤害自己的身体,别无它用。也罢,以后自己不参加暴饮暴食的活动就是了。

用epoll提供telnet服务(续)


用epoll提供telnet服务中说到,使用epoll提供telnet服务,用telnet登录后,然后输入命令,一切都很正常。只是由于一些原因,只能用nc命令,如echo stats | nc 192.168.168.128 9999,这个命令如果在这台机器上运行,还是会返回正确的结果,然而当这个命令在其它机器上运行时,则有时候会返回结果,有时又不返回。于是用样的命令去测试memcache,不论是在本机上还是其它机器上,都可以返回结果。暂时想不到memcache是如何做到的,于是先用echo stats | nc -i 1 192.168.168.128 9999来应付这个问题。可是这还不是最佳的解决办法,既然memcache能够做到,那么一定有解决的办法。

可喜的是,memcache是开源的,可以去看看它是如何解决这个问题的。打开memcached.c,文件不是一般的长,竟然有5000多行,慢慢找才知道memcache使用libevent,于是去了解libevent相关的知识,按照libevent官网和自带的示例程序,自己写了一个小例子运行,发现还是存在相同的问题。于是自己写了一个非常简单的单连接程序来取代epoll那一部分,可是还是存在相同的问题。问过同学之后,说是通过打印日志和tcpdump来查看网络包就一定可以找到。自己之前也用tcpdump查看过,客户端的确是有发数据的,服务端也收到了,可是为什么不显示收到的命令呢?依然没有解决我的问题,看来只有去《UNIX网络编程》中寻找问题的解答。

说实话,还真没有认认真真的看过《UNIX网络编程》这本书,因为之前对于网络编程总觉得异常繁琐,要调用socket,bind,listen,accept等函数才能建立一个链接,还要去填写sockaddr_in这个结构体,可是现在是工作,而且c就是这么底层,而网络编程也是很底层的,所以只好从头开始看起。此时对于TCP状态转换图才逐渐有些了解,没有想到啊,毕业5个多月后才知道这些,似乎有些晚了。难怪当初面试腾讯时,会被人鄙视说,许多工作的人都还能记得住这些转换图,你一个在校生怎么会不知道这些呢?其实当时很想和他说,这些都是工程实践很强的东西,没经常在这方面打滚,肯定不知道的,工作的人经常要接触这方面的东西,自然会记得住了。也罢,至少现在知道还不算晚。

看过书后,才知道有shutdown这样的东西,虽然以前看《UNIX环境编程》时,也看到过shutdown这个函数,可是没有写过实践代码,看过之后就忘记了。shudown的函数原型如下 #include int shutdown(int sockfd, int hoot); 返回:0--成功, -1--失败 该函数的行为依赖于howto参数的值。其中SHUT_WR选项,这个的意思是关闭连接写的这一半,当前留在套接口发送缓冲区的数据将被发送掉,后跟TCP的正常连接终止序列。用上这个,还是不行。

继续往下看,才知道有SO_LINGER这个选项,它使用linger结构体,如下:

struct linger {
int l_onoff; /* 0 = off, nozero = on <em>/
int l_linger; /</em> linger time */
}lin;

当l_onoff是1,且l_linger是0时,close某个连接时,TCP将会丢弃保留在套接口发送缓冲区的任何数据并发送一个RST给对端。于是我回头看代码,建立连接这一段,我用的是公司封装过的函数,发现代码中将l_onoff设置为1,l_linger设置为0,于是我认为是找到了解决的办法,将l_onoff改为0就可以了。试过之后,发现客户端还是收不到数据,看来还存在其它问题。

书上有一个poll函数的例子,于是认真看看。先胡扯一下,网上都说,当epoll工作在水平触发模式时,则是一个高效的poll,可是他们都不给出一个epoll使用水平触发的例子,只是在那里将epoll工作在边沿触发模式的程序抄来抄去的,非常令人失望。而《UNIX环境编程》也只是给出poll的介绍,没有给出例子,看了之后还是很无厘头。还好俊杰同学留下这本书,真得感谢他。用上这个例子,测试之后,发现客户端是可以收到数据的,将这段代码去代替epoll那部分代码,发现依然可以收到,于是问题的范围就缩小了。

打印日志,终于找到问题的所在。首先说明一点,在边沿触发模式下,程序一直在while循环中read客户端发过来的命令,当返回-1,且errno设置为EAGAIN;而当读到结束符,也就是客户端发送FIN包过来时,read返回0。看代码中的处理,原来在代码中,是检查最后read的结果,而不是检查最后读到的字节总数,如果返回0的话,则关闭socket,不再去处理命令,那这里有什么问题呢?通过打印日志,发现如果将命令在与服务端同一台机器上执行,则会输出

n 6 nread -1

n 0 read 0

因为是在同一台机器上,所以两个包是单独处理的,这样就会给客户端返回命令后再关闭连接。而如果不是在同一台机器上,则输出就会发生变化。有时它会收到

n 6 read 0

此时已经从客户端读到命令的,同时也读到了结束符,在我的程序中就会直接关闭连接,这样就不会去处理命令,发送数据给客户端。而有时它会收到

n 6 nread -1

n 0 read 0

这时它就可以正常处理。这就可以解释为什么之前客户端有时可以收到反射,有时又收不到。而当在nc命令中加上-i 1后,客户端的FIN包将会在命令发送大约一秒钟后再发送,服务端就可以正常处理。

通过解决这个问题,了解许多网络的知识,受益良多。如果对于网络的知识不是很了解,调试的过程总不能得心应手,找不到合适的地方输出日志。如果当初能想到输出n和nread的值,也许就可以很快的找到问题的症结,只是没有想到这里。《UNIX网络编程》果然是本好书,在实践过程中来看这本书,才发现其中闪烁的真知见灼,都是来自于实践,有些怀念Richard Stevens了。另外tcpdump确实是个好东西,对于查看网络包,非常方便。

最终的代码见这里用epoll提供telnet服务的代码续

2013深圳盐田山地马拉松纪实


虽然从天气预报中得知15号这天会下雨,但还是来到了深圳。14号当天从广州南站坐高铁到深圳北站,之后坐地铁去皇冠体育中心领取衣物,之后就坐观光1号线去大梅沙。预订的酒店在中兴通讯学院,在山坡上,环境还不错。志容状态不好,要呆在酒店休息,于是我只好一个人在外面溜达。先去看看大梅沙,到了大梅沙,才知道冬天的海边是挺荒凉的,毫无生气。天空下起了小雨,于是回酒店,天色还早于是到酒店的后山走走,一个人在山上走,还是挺刺激的,太安静了,想起鬼故事,皮肤都起鸡皮疙瘩了。就这样漫无目的的往上走,想走到山顶,可是天空下起了中雨,考虑到明天还要跑马拉松,只好走到半路就会酒店了。晚上和志容吃过晚饭后,就呆在家里休息,准备明天的雨中奔跑。看志容的状态,估计明天是跑不了马拉松了。

定了5:50的闹钟,准点起床,打开窗,外面还下着雨。洗漱之后,开始吃早餐,昨天买了一袋红枣,一个士力架,还从广州带了几个苹果,吃过早餐后,换好衣服,做准备活动。志容也吃过早餐,但最终还是没有一起去。于是只好一个人带上装备,前往月亮广场。已经有很多人在海滨公园了,雨天也阻挡不了大家跑步的步伐。7:20放好包裹,到出发处集合。7:58分时开始跑步,提前两分钟跑了。刚开始时,大部分人都还穿着雨衣,到了一个长坡后,小雨停了,明显感觉到浑身发热,于是大家都脱了雨衣,有些人还将雨衣丢弃。

32分,到达第一个5公里,没有了广马时的羁绊,速度还行。一路上保安还是非常敬业的,冒着雨在那里站岗。此时已经到了山中,吹起了风,穿着长袖还觉得有些冷,而旁边有人却在说好爽。一个具备相当实力的女跑步爱好者说,好爽是真的,很冷是假的。怎么能这么说呢,每个人都是一个独立的个体,对于生活有自己的体会,怎么能随便否定别人的想法。于是我记住了她,心里暗下决心,一定要赶在她之前跑完马拉松。我知道,现在还不是时候,后面还有很长的路要跑。这个女的很快就跑到前面去了,知道3个小时候,我才再次见到她的身影。

1:02分,到达10公里处,考虑到这是山地跑,也算是正常速度了。此时偷懒一会,拍了一张海边的照片。天空中再次飘起了小雨,我只好把雨衣再次穿上,之后及不愿意脱了,因为太冷了。最冷的还是双手,一直迎着风。头发也是一个问题,一直淋湿着,头都感觉有些冷,于是把魔术头巾套在头上,很有型。

1:32分,到达15公里处,跟着主流队伍跑,速度还算是稳定,以这样的速度跑,4个半小时肯定可以跑完全程。

2:05分,到达20公里处,志愿者小美女们的声音好甜,加油加油的,加的人心里暖暖的。到了二十二公里处,就有深山马特有的鸡汤喝了,也许深山马最为人知的就是这个鸡汤了。喝了两杯后,肠胃顿时暖和了许多。这一个5公里的路程是极为困难的,坡度一个接着一个,有一个特别长的陡坡,很多人都只好走上去了。

2:41分,到达25公里。这5公里跑的不容易。在这里的时候,已经看到有人从折返点回来了。这些家伙还是挺厉害的。接下来的路程里下起了中雨,幸好雨衣加身,还没被淋着。不过全身已经湿透了,裤子一直往下掉,因为穿着运动长裤,好麻烦。到达28公里的折返点后,一路上就非常畅快了。跑在我前面的是一个和我一样穿雨衣的壮实中年人,因为是下坡,他跑得很快,于是我想,跑着他跑,速度一定可以保证。于是跟着他的速度,一路上超过了许多人。跑完广马的经历告诉我,25公里到30公里是一个转折点,在这里超过别人的话,他就很难再超过你,事实证明的确如此。

3:14分,到达31公里处,速度上比上一个5公里快了很多。继续跑着,到了3小时25分时,看到了那个相当实力的女跑步者,于是超过她就成了我的目标。很快我就超过她了,但不久又被她超过去了。事实上,跑到这里的人,大家水平都差不多,就看谁能坚持下去。因为这一段路很滑,而我一直都不是下坡能手。于是就一直去追她。

3:55分,到了39公里处,再次超过了她,一定要在她前面跑完马拉松,谁让她随便否定别人的意见。估计他也看出我的心思,还是把我超过去了。可我也是一个不服输的人,于是再次超过她,并且把目标指向前面一个年轻人。就这样一直跑在她前面。最后400米时,一个女的把我超过去了,可是我实在无力,已经到了担心抽筋的地步了。最后20米是,那个女的竟然就在我旁边了,于是使出最后的力气跟她拼了,最终还是在她前面跑完了全程。

时间4小时14分,可是后来官网上给的结果是4小时22分,差太多了。跑完之后,去领补给。然后去打印证书,没想到要1个小时后才可以打印,非常不解。也罢,回酒店洗个热水澡,相当痛快。

这次马拉松虽然是在雨中跑步,有些地方非常湿滑,但速度还是挺快的,比广马的还快,也算是有进步了。更快,更高,更强才是体育精神,而不是安于现状。

壹基金举办的马拉松还是不错的,参加这个比赛也算是为灾区儿童做点贡献,虽然最后没有完赛奖金。一路上有许多开心的是,有给你拿水的小朋友,有为你加油的小美女,有一起鼓励的跑步爱好者,相信明年如果再来深圳参加马拉松的话,还是要报名深山马。

这次跑步我才真正意识到,跑步是自己的事情。每个人都有自己的追求,也无需强求别人和你一样的价值观。对于我来说,只有在速度上不断提高才有意义,不断根据路况和自己的体能做出变化,否则只能是一个无思想的跑步者。

写在深圳山地马拉松之前


明天就要去深圳参加15号举行的深圳山地马拉松了,此刻显得异常的平静,一个人呆在家里发呆,无聊,也不想和朋友联系,只是等待明天的到来。看天气预报, 后天是要下雨的,而且下的不小,后天的气温也很低,这意味着后天的马拉松将异常艰难。而我还是愿意奔赴深圳,去参加这次雨中的马拉松。

不 去多想这么做有什么意义,跑步本身就是一件很无聊的事情,只有很无聊的人才会去参加这么无聊的运动,而我本身恰好是一个很无聊的人。马拉松距离之长,对于 平时缺少训练的人来说,要跑完全程是一件很困难的事,上次广马的经历也说明了这一点。即使准备充分了,意外也会发生。跑马拉松也是一件相当违背人的本性的 一件事,人天生害怕吃苦,而跑马拉松就是要让你吃苦。在我看来,跑马拉松以来,最有意义的一点是回头看看以前的自己,那个对一千米就无比畏惧的自己,发现 自己成长了,此时心情有种别样的喜悦。而这次冒雨跑马拉松,本身是毫无必要的,何苦这么折磨自己呢?志荣说,如果下中雨的话,就放弃跑了,可我不愿意,真 心不愿意。跑马拉松是一种挑战,在恶劣条件下跑马拉松更是一种挑战,而人生本来就是一个不断超越自我,挑战自我的过程,怎么能这么轻易放弃呢?

想 想那个去参加环勃朗峰极限耐力赛的杨家根,正不断的超越自我,向世界证明,中国人的耐力也是可以的。在和志容说到,明年的目标是跑的更快,将速度提高到4 个小时跑完全程,志容说还是不要了,没必要跑那么快。我不禁想,是我还年轻,还是志容已经老了。也罢,每个人都有自己的生活追求,有自己的人生观,也不强 求别人非得认同自己的观点。跑步是这么一件孤独的是,就让自己孤独的去完成。

如果可以,让风雨来得更猛烈些。

毕业旅行


临毕业前,做了一次短暂的毕业旅行,一直想以文字记录下来,可是一直都太懒了,今天正好有这个心情,于是记下它,免得以后忘记了。

这次旅 行的最主要目的地是福州,去看望初中时的数学老师陈治平。今年他已经71岁了,我担心现在不去看他,以后就没有机会在见到他。虽然陈老师只教过一年,但他 对我的影响还是很深远的。初一的时候,在上尺规作图时,陈老师给我们展示了如何画正五角星,正是这次作图,让我知道了数学的魅力。后来我也去计算过,正五 角星中到处充满着黄金分割比。因为他,数学一直以来是我喜欢的一门课,以至于后来高考填报志愿时,会选择了数学系。本来打算去看看饼干的,她说不想见我, 于是只好放弃。沿途,还要去看看泉州的堂姐,福清的舅舅。

6月21日,从广州东站出发,坐火车到龙岩。就要去福州了,心情有些激动,仔细 算算,已经13年没去福州了。于是在微博里写到“对于大多数沙县小吃从业者来说 福州是一座有特殊意义的城市 正是从这里出发 他们走向全国各地 13年后 再次踏上去往福州的火车 有种难以言状的心情”。虽然我不是沙县小吃从业者,但作为从业者的子女,每年暑假都会去与父母相聚,也算是走南闯北了,自然明白福州的意义。

6 月22日,到龙岩。在龙岩中转时,本打算先去福清的,看了地图才知道,要先到泉州,之后才到福清,于是先去泉州堂姐家。想不到的是,龙岩已经通动车了,极 为方便,可是当我在火车站上厕所时,才知道这里的配套设施如此差劲,于是又写到“龙岩火车站的洗手间真是烂的可以 这样怎么能提高城市的形象 吸引游客呢 哎 可惜了动车组”。做动车就是舒服,环境舒适,不拥挤,列车不晃动,速度快。于是狂发微博,“暂定计划 泉州 福清 福州 杭州”,“漳州的房子很有特色 来到闽南地区了”,“呀 到厦门了 这次时间不允许 下次再来逛逛”,“闽南地区果然富庶”,很快就到泉州了。之后就去堂姐小珠家里。吃过午饭后,就和小珠,还有大外甥去上乐高的培训课,之后又去上小外甥的 早教课。这时我才知道,小珠这些中产阶级们在小孩教育上真是不遗余力,可是我还是不赞同这中做法。儿童的天性就是玩,只要与同龄人在一起玩,玩玩泥巴都够 了,何必要上那么多的培训课。

6月23日,上午去黎明大学,找外甥女,只过了一年,变得成熟许多,大学还是没有白上。下午就坐动车去福清 了。15年前曾来过福清,于是又在微博里写到“15年后再到福清 风一直吹”。在福清时,爸妈还是受过不少苦,因为这里的人很不讲理,有些人还会来闹事。晚上和舅舅聊天,才发现舅舅变老了许多,主要还是因为要考虑的事情 太多了。沙县的房价贵的惊人,这样一个小县城竟然均价就要7、8千了,太离谱了。开小吃虽然赚了点钱,但那都是血汗钱啊。舅舅也当了房奴,每个月要交 4000。舅妈和外甥女、外甥去学校了,晚上学校有晚会,外甥女要去表演民族舞。老实说,还是不喜欢外甥女去上民族舞培训课,还是多读点书好一些。

6 月24日,外甥所在小学开家长后,去当了一次家长,发现小学老师还是极为负责的。反倒是许多家长虽然口中说很重视小孩教育,但在行为上却大相径庭。就拿我 舅舅来说,他一直说,自己没什么文化,三年级之后的内容他就没办法教外甥女了,现在外甥女成绩很差,自己也没有办法。其实在我看来,更重要的不是教会孩子 什么,而是要给孩子不断学习的理念。像舅舅现在,有空的时候不是看书,学习,而是看别人打牌,这对孩子的影响是极为不好的。反倒是舅妈非常勤劳,以前一直 认为像舅妈这种喜欢保养自己的女人,一般不会很勤劳,没想到我错了。于是写到”没有不勤劳的母亲“。

6月25日,上午告别二舅一家,去福 州。到了福州,心情更加激动了,再次狂发微博,“久违了 福州”,“福州的公交车司机这般服务态度 比广州的公交司机差了不知几条街了”,“福州果然在大兴土木建地铁 我觉得先提高公交的服务更重要”,“很早就知道福州别称榕城 因为榕树多 只是不知道有这么多榕树 现在才见识到”,“公交的广播能不能别那么无聊 用福州话就算了 还要教别人学福州话”,“没想到20路公交只需要1元钱 亏了”,“白天坐公交就是好 可以左看看 右看看 现在才发现坐公交有时也是一种旅行方式”。见到陈老师才知道,老家伙还非常有活力,一头黑发,他母亲97了,身体还依然健朗,于是写到"老师71岁 依然一头黑发 他母亲97岁 依然有许多黑发 真是特殊的一家人 据老师说他们家基因就是这样",在异地见到恩师,还是非常高兴的一件事。陈老退休之后,还是干了许多事,其实以他的状态,完全可以多教几年书的,只是政策 不允许。之后离开老师,写到“告别了初中的数学老师 没想到老师71岁了还如此健朗 不出意外 20年之后还可以见面”,虽说来过福州多次,但还是不熟悉,于是写到“对福州不熟悉 只好别了老师就奔向耗子窝 连师大都没看上几眼 可惜了”。到了大学城,耗子还没下班,于是逛了一下大学城的菜地,这里好多菜。晚上在耗子家住,耗子还混的不错,住的是教师公寓。

6月 26日,买了下午2点的高铁去杭州,时间还早,饼干又不让去找她,于是只好在福州城里溜达,"没有想到福州也会堵车 长见识了",“福州的公交司机都偏年轻 这可能是他们服务态度差的原因之一 再加上福州市民的忍耐力 司机更加肆无忌惮了”,想起以前去过的西禅寺,报恩塔,于是决定去看看,可是”冲着报恩塔来的 竟然不让进 这叫人情何以堪啊“,于是只好到旁边的罗汉塔看看,”500罗汉神态各异 与卢沟桥的狮子相比 毫不逊色 表情丰富 仿佛在诉说着众生百态“,”500罗汉中唯一的女罗汉原来就是观世音“,”买了两斤多的荔枝 一个人在车站啃 荔枝的味道比起寺里偷摘的要好一些 可是其中的滋味要逊色不少“,很快就离开福州,踏上开往杭州的动车,于是写下"别了 福州 对于福州 作为一个匆匆过客 并没有太多的发言权 只好从公交这一点来说 应该说 福州的公交线路很全 转一次车 基本上没有到不了的地方 价格便宜 不拥挤 空调很有力 只是司机的态度相对较差 令人不爽 广播很无聊 多用福州话 太狭隘了 希望在这方面能有所改变"。在高铁上,不喜欢坐对面的女生,可是对于饼干,虽然素未蒙面,却有种相见恨晚的感觉,于是写下”人就是很奇怪 有些人 你看第一眼就很讨厌 而有些人即使没见过也很喜欢“,高铁就是舒服,沿途景色亦不赖,于是狂发微博,”高铁就是牛 上个洗手间也有大珠小珠落玉盘的感觉“,”宁德果然有优秀的渔港 有机会一定要去“。”许多人将普通铁路的习惯带上高铁 一上车就睡觉 其实这是非常浪费的 高铁沿途的景色很不错 就像刚刚的宁德的水港“,”在福建坐火车 隧道就是多“,”坐高铁出行有旅行的感觉 期待高铁横贯全国那一天“。晚上7点到大舅的小吃店,没有想到大舅也当了房奴,同样每个月要交4000。

6月27日,舅妈做了土豆饺子,吃了好多之后,道别舅舅一家,去爸妈的小吃店,老妈已经累的不行,闪到腰了,都不能蹲下了,好无赖啊,自己现在又没办法养着她。

6月28日,道别爸妈,踏上开往广州的火车。

为期7天的毕业旅行就结束了,这样短暂的旅行还是有许多收获的,通过比较,可以发现每个城市的不足以及吸引人的地方,如果每年都有这么一次旅行那该多好。在 旅途中,还带了一本书,那就是林丹写的《世界尽头》,这让我看到一个真是的林丹,一个有想法的林丹,于是写下“没有想到林丹在对待奥运金牌上会有如此见地 佩服 许多人确实粗暴地把国家荣誉和运动员的价值绑在最后的那块金牌上”,“没想到林丹在爱情 生活 体育上都有如此见解 折服”。

跑步三周年


不知不觉中,已经开始坚持跑步三周年了,现在跑步已经成为了我生活中的一部分了,几天不跑步,腿就痒了,要的就是这种效果。

翻开日记,才 发现记录的很乱,已经不清楚哪一天有跑步了,跑的距离和时间,混乱的生活啊。研三上学期,正是找工作的混乱时期,没想到找一份工作就把我整的这么惨,连记 日志的方式都改变了,不再是那么密密麻麻的记录每天干了什么,而是每天就几句话,一笔带过。因为如此,已经不知跑步的里程,可是有一点可以肯定的是,我已 经离不开跑步了。研三下学期,生活更加混乱了,找完工作,还要写毕业论文,这更加痛苦,生活也更加混乱,导致我的胃一度犯病,胃病和阑尾炎一并来犯,把我 给吓坏了,还好扛过来了。

真正有计划的跑步,还是在参加工作之后。因为心中一直有参加马拉松的梦想,这个梦想是由三年前的一个小伙子点燃 的。三年前,那时我还在扬州。一个跑步爱好者对我说,看你双目无神,实则对生活充满了渴望,眉宇间散发着一股孤独的气息,再加上你体格健壮,极为适合跑马 拉松。瞬时感觉遇到知音,为报答知遇之恩,三年间从未间断跑马拉松的想法,参加工作后,终于有机会完成了。我住的地方正好在主江边上,而公司的时间很弹 性,所以工作之余,有足够的时间来实施我的跑步计划。

从7月8号入职,每个星期都会去跑上三次。刚开始的时候,因为体力已经退化,只好在 中大校园的操场上练习,后来由于操场是晚上9:30就关门了,而我的体力也逐渐增加,于是改道去珠江边跑。老实说,珠江边的空气还是极为不好的,时不时传 来一股鱼腥味,偶尔还有一股尿骚味,只是没有更加合适的地方。刚开始是往广州塔这个方向跑,发现这里人太多,于是改为往人民桥方向。毕竟还是有基础的,很 快速度和体力又回到了10KM/h,隔天跑上个10KM,呼吸非常自如。后来因为报名了深圳的山地马拉松,于是和公司的同事胡志容一起去跑白云山。志容已 经参加过三次马拉松了,在他的带领下和鼓舞下,跑了六次白云山,我的体力也有了明显的进步。

骑行也有助于提高耐力。用第一个月工资买了勇 士500后,到处骑行。在单骑走从化凤凰水库后,我的耐力更上一层楼了。可是马拉松的距离对于这平时的训练还是太远了,于是得跑更远的距离。周末的时候, 往更远的琶洲大桥进发,虽然很累,但还是坚持下来了。慢慢的20KM,25KM,30KM,已经具备了跑完马拉松的实力了。果然,顺利的完成了广州马拉 松。

用了三年的时间,完成了人生的第一个马拉松,也算是对那个小伙子的一个回报。老实说,还真得感谢他,要不然我不知道还要多久才能发现自己跑马拉松能力。

现在跑步已经融入我的生活了,也难怪在日记不再会记录跑到哪里,不再记录时间和距离。因为跑步对于我来说已经像吃饭一样了,试问,有谁会在日记里几下今天吃了什么饭,吃了多少,用多少时间呢?

用Sphinx提供的klist处理文章更新和删除


在前面的介绍中,都没有处理更新和删除问题,这里有必要说说。在关于sphinx引擎的一些想法中说过公司所用的引擎中,处理更新和删除的办法是在索引中增加一个属性来标志这条记录是否失效,每次做增量时,就要去主索引和增量索引中更改相应id的属性值,这确实可以解决问题。不过并不是一个很好的解决办法,Sphinx的作者也说过这种方法既麻烦又容易出错。既然有更新和删除这个需求,必然会提供解决的办法,这个办法就是kilst。所谓的klist,就是kill list,按照字面理解,就是删除列表。我们只需要在增量索引中保存一个id列表,搜索时,如果在主索引中搜到相关文档,而文档的id存在于增量索引的id列表中,则这个文档将被丢弃。

这里有一个需要注意的是,当文章被删除时,仅仅通过增量抓取,在增量索引中并不能知道主索引中哪一个文档被删除了,所以这就必须在表中文档被删除时,能够记录下被删除的id,这就需要用到触发器,也需要建立一个辅助表来保存这些id。辅助表的建立如下:

create table sphinxklist(
        id integer not null,
        ts timestamp not null
);

触发器的建立如下:

DELIMITER //
CREATE TRIGGER sphinx_kill
AFTER DELETE ON wp_posts
FOR EACH ROW
BEGIN
        INSERT INTO sphinxklist VALUES (OLD.ID, NOW());
END
//

有了这些准备工作后,我们就可以使用klist了,事实上在之前的配置文件的基础上,只需要修改一点点内容就好了。首先修改主索引

source srcmain : base{
        sql_query_pre = SET NAMES utf8
        sql_query_pre = SET SESSION query_cache_type=OFF
        sql_query_pre = UPDATE sphinx_helper SET main_tmp_maxts=NOW() WHERE appid='blog_search';
        sql_query = \
                SELECT ID, post_title, post_content, UNIX_TIMESTAMP(post_modified) AS post_modified FROM wp_posts WHERE \
                        post_status='publish' AND post_modified &lt; (SELECT main_tmp_maxts FROM sphinx_helper WHERE appid='blog_search');
        sql_query_post_index = UPDATE sphinx_helper SET main_maxts=main_tmp_maxts WHERE appid='blog_search';
        sql_query_post_index = DELETE FROM sphinxklist WHERE ts &lt; (SELECT main_maxts FROM sphinx_helper WHERE appid='blog_search');
        sql_attr_timestamp = post_modified
        sql_field_string = post_title
}

可以看到,相对于之前的配置,这里只添加了一行 sql_query_post_index = DELETE FROM sphinxklist WHERE ts < (SELECT main_maxts FROM sphinx_helper WHERE appid='blog_search'); 添加这行是为了防止之前运行引擎时留下的id再次被使用。 之后修改临时索引:

source srcdelta_temp : srcmain {
        sql_query_pre = SET NAMES utf8
        sql_query_pre = SET SESSION query_cache_type=OFF
        sql_query_pre = SET @maxtsdelta:=NOW();
        sql_query_pre = UPDATE sphinx_helper SET delta_tmp_maxts=@maxtsdelta WHERE appid='blog_search';
        sql_query = SELECT ID, post_title, post_content, UNIX_TIMESTAMP(post_modified) AS post_modified FROM wp_posts WHERE \
                post_status='publish' AND post_modified &gt;= (SELECT main_maxts FROM sphinx_helper WHERE appid='blog_search')\
                AND post_modified &lt; @maxtsdelta;
        sql_query_killlist = SELECT ID FROM wp_posts WHERE post_modified &gt;= (SELECT main_maxts FROM sphinx_helper WHERE \
                appid='blog_search') AND post_modified &lt; @maxtsdelta UNION SELECT id FROM sphinxklist;
        sql_query_post_index = UPDATE sphinx_helper SET delta_maxts=delta_tmp_maxts WHERE appid='blog_search';
}

也只是添加了一行,也就是将这次抓取的id与sphinxlist中的id合并。 之后还需要修改Shell脚本

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
#!/bin/bash
baseDir=/home/long/sphinxforchinese/blog_search
conf=$baseDir/etc/blog_search.conf
binDir=$baseDir/bin
cd $binDir
while [ true ]
do
        #./indexer -c $conf --merge-klists --rotate --merge delta deltaTemp
        ./indexer -c $conf  --merge-klists --rotate --merge delta delta_temp
        if [ "$?" -eq "0" ]; then
                cat $baseDir/script/post_merge.sql | mysql -u root --password=123456 blog
                ./indexer -c $conf --rotate delta_temp
        fi
        sleep 60
done

这个脚本相对于原来的只增加了--merge-klists这个参数,这个参数的意义是,将delta_temp合并到delta时,并不会删除delta的klist,而是将delta_temp的klist和delta的klist合并,这正是我们想要的。经过这样的变化,一个可以处理更新和删除的main+delta索引就建好了。

感谢Sphinx团队,感谢Sphinx-for-chinese团队,给我们提供了一个这么好用的开源引擎。

用Sphinx建立main+delta索引(下篇)


上篇中,我们介绍了一种建立主索引和增量索引的方法,这种方法有一种不足之处就是会改变主索引,因为每次增量索引都会与主索引合并成新的主索引。为此,我们可以想出另一种解决的办法,每次只改变增量索引,这就需要另外再建立一个临时索引。

这里只需要改变少量地方,一个是增量索引,另外还需新增一个临时索引,具体配置如下:

source srcdelta : srcmain{
        sql_query_pre = SET NAMES utf8
        sql_query_pre = SET SESSION query_cache_type=OFF
        sql_query = SELECT ID, post_title, post_content, UNIX_TIMESTAMP(post_modified) AS post_modified FROM wp_posts WHERE \
                post_status='publish' limit 0;
        sql_query_post_index =
}
source srcdelta_temp : srcmain {
        sql_query_pre = SET NAMES utf8
        sql_query_pre = SET SESSION query_cache_type=OFF
        sql_query_pre = SET @maxtsdelta:=NOW();
        sql_query_pre = UPDATE sphinx_helper SET delta_tmp_maxts=@maxtsdelta WHERE appid='blog_search';
        sql_query = SELECT ID, post_title, post_content, UNIX_TIMESTAMP(post_modified) AS post_modified FROM wp_posts WHERE \
                post_status='publish' AND post_modified &gt;= (SELECT main_maxts FROM sphinx_helper WHERE appid='blog_search')\
                AND post_modified &lt; @maxtsdelta;
        sql_query_post_index = UPDATE sphinx_helper SET delta_maxts=delta_tmp_maxts WHERE appid='blog_search';
}
index delta_temp : main{
        source = srcdelta_temp
        path = /home/long/sphinxforchinese/blog_search/var/data/delta_temp
}

实际上,我们是先建立一个空的增量索引,之后临时索引中的数据慢慢合并到增量索引中。在这里,增量索引很像上篇中的主索引,而临时索引则像上篇中的增量索引。 此时我们需要修改dist_blog_search,即增加临时索引

index dist_blog_search {
    type = distributed
    local = main
    local = delta
    local = delta_temp
    agent_connect_timeout = 1000
    agent_query_timeout = 3000

}

此后还需改变Shell脚本的内容

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
#!/bin/bash
baseDir=/home/long/sphinxforchinese/blog_search
conf=$baseDir/etc/main_delta_temp.conf
binDir=$baseDir/bin
cd $binDir
while [ true ]
do
        ./indexer -c $conf  --rotate --merge delta delta_temp
        if [ "$?" -eq "0" ]; then
                cat $baseDir/script/post_merge.sql | mysql -u root --password=123456 blog
                ./indexer -c $conf --rotate delta_temp
        fi
        sleep 60
done

事实上,改变的内容还是很少的。经过这样的改变,我们就无需再改变主索引了。第一次建立主索引后,就一直保持不变,变化的是增量索引。

用Sphinx建立main+delta索引(上篇)


虽然只建立主索引就可以满足许多应用,但当数据非常多时,每次都重建索引是一件非常耗时的事情,而且每次重建都会浪费CPU,这也是极为不好的。考虑这样一种情况,在数据库中一共有1千万个文档,而每天只新增一万个文档,如果每次都要重建索引,则第一次重建时,是1001万个文档,第二次时是1002万个文档,这都非常耗时的。如果建好主索引后,只对这些新增的一万个数据建一个增量索引,之后把它合并到主索引中,所需的时间将缩短。所以建立main+delta索引是一个不错的选择。

这里依然以之前的博客搜索为例。为了便于做增量,我们需要记录每次抓取的时间,而为了持久保存这个时间,我们需要在数据中建立一个辅助表,建表语句如下

create table sphinx_helper(
        appid varchar(300) not null primary key,
        main_maxts datetime,
        main_tmp_maxts datetime,
        delta_maxts datetime,
        delta_tmp_maxts datetime
);
insert into sphinx_helper (appid) values ('blog_search');

在wp_posts表中, post_modified这个时间字段是随着每次文章的更新而自动变化的,所以可以使用它来做增量。主要思路就是用一个值来保存上次增量索引的时间,当需要再做增量索引时,则只需索引从这个保存的时间到现在这段时间里的数据。在sphinx_helper中,这个值用main_maxts来标示。对于主索引,写成配置文件如下,

source base{
        type = mysql
        sql_host = localhost
        sql_user = root
        sql_pass = 123456
        sql_db = blog
        sql_port = 3306
}
source srcmain : base{
        sql_query_pre = SET NAMES utf8
        sql_query_pre = SET SESSION query_cache_type=OFF
        sql_query_pre = UPDATE sphinx_helper SET main_tmp_maxts=NOW() WHERE appid='blog_search';
        sql_query = \
                SELECT ID, post_title, post_content, UNIX_TIMESTAMP(post_modified) AS post_modified FROM wp_posts WHERE \
                        post_status='publish' AND post_modified &lt; (SELECT main_tmp_maxts FROM sphinx_helper WHERE appid='blog_search');
        sql_query_post_index = UPDATE sphinx_helper SET main_maxts=main_tmp_maxts WHERE appid='blog_search';
        sql_attr_timestamp = post_modified
        sql_field_string = post_title

}

以上就是主索引的配置,之所以需要将NOW()得到的时间保存到数据库中,之后在sql_query_post_index中取出来用,是因为sql_query_post_index和sql_query不是用一个数据连接。而之所以在sql_query_post_index里才更新main_maxts,是为了保证只有在索引成功建立后才更新这个值。而对于增量索引的配置,则如下:

source srcdelta : srcmain {
        sql_query_pre = SET NAMES utf8
        sql_query_pre = SET SESSION query_cache_type=OFF
        sql_query_pre = SET @maxtsdelta:=NOW();
        sql_query_pre = UPDATE sphinx_helper SET delta_tmp_maxts=@maxtsdelta WHERE appid='blog_search';
        sql_query = SELECT ID, post_title, post_content, UNIX_TIMESTAMP(post_modified) AS post_modified FROM wp_posts WHERE \
                post_status='publish' AND post_modified &gt;= (SELECT main_maxts FROM sphinx_helper WHERE appid='blog_search')\
                AND post_modified &lt; @maxtsdelta;
        sql_query_post_index = UPDATE sphinx_helper SET delta_maxts=delta_tmp_maxts WHERE appid='blog_search';
}

在sql_query中可以看到,每次增量索引的数据都是在[max_maxts, NOW()]之间,而只在sql_query_post_index中更新delta_maxts也是基于上述理由。剩下的配置如下:

index main {
        source = srcmain
        path = /home/long/sphinxforchinese/blog_search/var/data/main
        docinfo = extern
        charset_type = utf-8
        chinese_dictionary = /home/long/sphinxforchinese/blog_search/etc/xdict
}
index delta : main {
        source = srcdelta
        path = /home/long/sphinxforchinese/blog_search/var/data/delta
}

index dist_blog_search {
        type = distributed
        local = main
        local = delta
        agent_connect_timeout = 1000
        agent_query_timeout = 3000
}

这里我们多了一个dist_blog_search,它是结合main和delta的搜索结果,在客户端中搜索时,我们使用dist_blog_search这个索引的结果。剩下的配置与只有主索引时相同,这里就不累述了。

写好配置文件后,还需要有一个步骤。因为我们的策略是每隔一段时间将增量索引与主索引合并,当合并之后,我们需要更新main_maxts这个值。如果我们是每个60秒做一次增量索引,这需要写一个shell脚本,脚本如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
#!/bin/bash
baseDir=/home/long/sphinxforchinese/blog_search
conf=$baseDir/etc/main_delta.conf
binDir=$baseDir/bin
cd $binDir
while [ true ]
do
        ./indexer -c $conf --rotate --merge main delta
        if [ "$?" -eq "0" ]; then
                cat $baseDir/script/post_merge.sql | mysql -u root --password=123456 blog
                ./indexer -c $conf --rotate delta
        fi
        sleep 60
done

先执行./indexer -c $conf --rotate --merge main delta,这句是将主索引和增量索引合并,当合并成功时,则需要到数据库中修改main_maxts这个值,这个句子在post_merge.sql中,post_merge.sql的内容如下:

UPDATE sphinx_helper SET main_maxts=delta_maxts\
        WHERE appid='blog_search';

之后进行增量抓取 ./indexer -c $conf --rotate delta, 这里说说--rotate这个选项,这个选项非常有用。在主索引和增量索引合并时,indexer程序会将这两个索引合并成一个索引,当合并成功后,程序会发送一个SIGHUP信号给searchd,之后searchd就好去加载这个新的索引。

到这里,一个main+delta的索引就完成了。

谈谈编码


记得一年前找工作时,阿里巴巴的面试官问了这么一道题,汉字在C语言中是怎么存储的,或者说C语言是如何处理汉字的,对于那时的我是一脸茫然,因为实战经验太少了。前不久,在处理中文标题时出现了乱码问题,组长只在一段代码中改了一句话就解决了问题,觉得非常神奇,于是看这段代码,原来就是关于编码处理的,于是认真的看了这段代码。

通过努力,算是明白了这段代码,其实也就是用了iconv函数族来进行编码之间的转化,再进行特殊处理。例如如果最终目标语言是GBK,可能还需要进行全角和半角之间的转化,繁体到简体的转换。事实上,我们处理时出现了乱码问题正是在繁体到简体的转换时写错了,导致出现乱码。

这里只想写写我自己的理解,在我看来,任何编码保存的文件最后都是字节流,在读取这些字节流时,你只有采用保存文件时采用的编码才不会出现乱码。这就好像加密与解密一样,例如如果一个文件是用utf-8编码的,但你是用GBK打开,此时就会出现乱码了,因为两种编码的编码方式不同。当然还是有一些例外情况,例如如果你是用GB2312保存的文件,用GBK打开是不会出现乱码,因为GBK是兼容GB2312的,GB2312的所有编码的值在GBK中是一样的,只是GBK还利用了GB2312没有用的一些空间来增加了一些编码。而GB18030则又扩充了GBK,包含了更多的字符。

在C语言中,用字符串来保存文本,这些文本都是字节流,只是每种编码占用的空间不同。例如“我是中国人”这5个字,如果是utf-8编码的,因为每个汉字占用3个字节,则要占用15个字节,用strlen可以测出来。如果是GBK编码的,因为每个汉字占用2个字节,则要占用10个字节。对于这样的回答,不知道那个面试官会不会满意呢?

所以这里有一个问题是,给你一个字符串,如果没告诉你用什么编码的,那么要怎么判断它是什么编码的。