通信模型之“透明”

    今天是大牛走后的第一周,大牛走了,理所当然所有的代码变成我维护修改了。由于最开始写的是写一步算一步,现在0.0.1版release后发现代码结构有很多不合理,理所当然要重构。首先重构的当然是影响着整的前后台系统的socket通信。之前socket的设计是这样的:

     前后台系统共用一个socket.dll,使用socket.dll来封装socket的部分。但是今天看代码时发现其实bg(后台程序)与fg(前台程序)分别加载时socket时,socket.dll分别在两个进程中都有一分部没用的数据与代码拷贝,如后台只需要服务端的相关处理部分数据与代码,前台只需要客户端的处理部分与代码,放在一个DLL中逻辑上看起来很不优雅。而且在socket.dll的代码中定义了一套与外部接口相对应的内部通信数据结构,如socket.dll 导出一个s_aa(struct bb *p)样子的接口那么socket.dll中必有一个struct bb_private与之对应,在调用s_aa时其实p所指向的参数是会被转成struct bb_private后才会被发出去。个人感觉这样做,这个socket.dll仅仅是一个库,如果以后增加了一个功能,这个库可能就需要修改很大,不能实现高内聚低耦合的宗旨。

      个人认为优雅的实现应该是这样的:

    SocketClient.dll与SocketServer负责Socket通信的处理,但是这两个DLL并不知道所有Socket之上的东西,他们仅提供一种机制那就是“透明”,将前台(Client)与后台(Server)之间的网络透明掉,让程序的数据与处理在逻辑上如图中虚线那样执行,如FG调用BG的某个功能,Socketxx.DLL并不需要知道任何关于这个功能函数的数据结构,逻辑上看就像是前台直接调用了BG的函数然后得到了BG函数的返回值。这时Socketxx.DLL不需要知道任何调用的细节,这就是机制,如果要加一个FG调BG的功能函数,那么只需要修改FG与BG而不需要同时修改Socketxx.DLL这两个DLL,这才是透明机制。这样一看逻辑上就清晰多了,而且也优雅多了。

我的C编码风格

首先声明一点,我是一个不相信注释的人,所以我的代码不会有很多注释,我代码的注释越多说明我实现的越蹩脚。我的大部分风格与linux kernel code style保持一致,又增加了一点自己的个人习惯。

1. 所有变量函数名全为小写,单词之间用下画线隔开

2. 所有宏全用大写,我把枚举变量的值也视为宏

3. 将Tab键设为8个空格

4. 从不设置编辑器将tab变成空格,在我看来,这简直是强奸民意,比如我自己的编辑器tab是8个空格,但是别人也许更喜欢4个空格呢?

5. 所有关键字与括号之间有空格,双目运算符两边有空格

6. if while for struct 大括号在与if while for struct 在同一行

7. error code从0为成功1到无穷大为错误代码

8. 函数返回值对error code取负,因为可能函数的返回值是正值时是有其他意义的

9. 对于代码参数如果些函数不会修改这个指针指向的内容,那么加上const修饰

10. 不轻易使用typedef,代码是写给懂C语言的人看的,因此我不会去隐藏指针与结构体的存在。

11. 所有经过typedef重定义过的类型,都会在后面加上_t,如aa_t,对于众所周知的u8 u16 u32 s8 s16 s32除外

12. 如果理论上代码的switch绝对不会跑到default,那么我会加上default 并在default中加上类似下面这样一段代码,这样有朝一日脑子抽了加了一个case,那么这个宏就可以起到提醒的作用。

switch(cmd) {
case xxx:
xxx;
break;
default:
assert(!”Unsupport instruct”)
break;
}
13. 所有函数的指针参数在理论上应该不为空的情况下,我会使用assert而不时if (!pointer)这种形式,因为当他为空时本来就是成功的bug,本就应该修复,函数不应该处理这种理论上不存在的错误。

14. 如果一个变量特性是全局的(最简单的一个变量每调某一个函数这个变量会自增1),但是这个变量只在这个函数内被访问,那么在这个函数定义static变量是一个好主义。

15. 同样如果一个全局特性的变量但是只在某一个文件里被访问,那么同样加上static修饰。

16.永远不要使用真正的全局变量,一定有办法可以不用全局变量来优雅的实现。

17. 对于goto,我只想说如果你的goto是只向一个方向走的话,就尽情的用吧!

18. 不要把所有头文件放成include.h里面, 然后在所有头文件包含这个头文件, 试想一下你看到一个includes.h你一眼会看出来这个C文件到底引用了哪些模块了,他与哪些模块耦合了呢?(或许你可以说用source insight, 啊哈或许这是一个好主意,但是一个好的代码不应该只能依靠工具来分析,不是么?),而且当我们包含的头太多时,可以提醒我们这个C文件足够复杂,是否需要分拆了.

19. 如果是纯操作数据,那么类型与数据长度要尽可能的相匹配,而且这种情况要杜绝使用void *,因为读你代码的人不知道你的length是怎么定义的,所以这里延用指针的自增习惯.如:

  int download(unsigned char *buff, unsigned long n);  //n应该是 数据字节数 / sizeof(char)

  int download(unsigned short *buff, unsigned long n);  //n应该是 数据字节数 / sizeof(unsigned short)

  int downlaod(void *buff, unsigned long n);  // 容易使人误解的写法: n ?= 数据字节数 / sizeof(void)

20. 如果有些地方不想让别人操作,而且无关类型的,可以使用void *来躲避类型检查,慎用之,如:http://www.cnblogs.com/findstr/p/3525865.html里面的回调函数的参数。

21. 模块文名最多两个单词如rw_lock.c, 模块结构体采用与文件名相同的命名方式如struct rw_lock, 模块函数名前缀则省略掉下划线如rwlock_get_lock.

状态机

  从毕业到现在都没有写过博客了,一是没什么大的收获,二是这段时间发生了太多事,从入职到这段时间,思想经历了各种斗争。今天终于有了点值得写的东西,就暂时放下思想斗争记录一下新发现吧。

  以前写代码时从来只知道有状态机,但是从来没有用过,这段时间因为代码开的线程太多了,程序吃不消,经高人指点才发现其实可以用状态机来用一个任务来模拟多个相似任务。下面就来分析一下状态机实现多任务的原理。

  一个完整的代码是由数据和代码组成,由同一个函数创建的多个线程在线程切换时仅仅是切换其中的数据共用其中代码。

  可以实现这样一个纯代码函数,这个纯代码函数只根据参数的内容来执行与代码上下文无关,在模拟切换线程时仅切换一下函数参数即可。线程最重要的特点就是没执行完就可以由时钟中断进行切换,归根到底就是每次每个线程只执行一段程序,因此把一个函数分成几个块,每次每个模拟线程执行一块,这样就可以模拟出一个类似时间片轮转的多线程调度算法, 状态机恰好满足上面要求。语言描述总归是无力的,下面请看代码。

 1 #include <stdio.h>
 2 #include <assert.h>
 3 
 4 struct fsm_param {
 5         int next;
 6         char *param;
 7 };
 8 
 9 int fsm(struct fsm_param *param)
10 {
11         switch (param->next) {
12         case 0:
13                 printf("%c", param->param[0]);
14                 param->next = 1;
15                 break;
16         case 1:
17                 printf("%c", param->param[1]);
18                 param->next = 2;
19                 break;
20         case 2:
21                 printf("%c", param->param[2]);
22                 param->next = 3;
23                 break;
24         case 3:
25                 printf("%c", param->param[3]);
26                 param->next = 4;
27                 break;
28         case 4:
29                 printf("%c", param->param[4]);
30                 param->next = 5;
31                 break;
32         case 5:
33                 printf("n");
34                 param->next = 6;
35                 break;
36         default:
37                 assert(!"Never arrive here");
38         }
39         return 1;
40 }
41 
42 int main()
43 {
44         int i;
45         struct fsm_param thread[2];
46 
47         thread[0].next = 0;
48         thread[0].param = "AAAA";
49         thread[1].next = 0;
50         thread[1].param = "BBBB";
51 
52         do {
53                 for (i = 0; i < 2; i++)
54                         fsm(&thread[i]);
55 
56         } while (thread[0].next != 6 && thread[1].next != 6);
57         return 0;
58 }

  啥也不说了上图看效果,是不是和时间片轮转调度算法写出来的多线程一致呢?甚至我们可以更改调度算法哟!再次感叹一句,C语言博大精深啊!