最近在做业务项目迁移,把ec的代码迁移到laravel中,
为什么选用laravel,我也不知道(也不是我做选型)。其他的框架也没太多接触过,都是跑跑hello world,看的多一点的算是yaf了,不过个人理解,yaf没有orm,完全就是个vc框架,没有model层。需要自己去扩展,但是眼前项目很紧,明显不能这么用。以前用的框架是自己公司内部使用的,虽然思想的都不错,但是随便使用。
因为是公司内部物流系统,所以ui直接都是后端人员开发,ui框架选用的semantic,对于我这种不熟悉css,js的人来说也还不错,毕竟有文档,复制粘贴,大体的页面也就出来了。
但是开发过程中,还是想吐槽一下laravel。
1.最想吐槽的就是它的配置,放到.env里,每行其实就是个键值对,通过config目录下的配置读取,把这些配置替换过去,
但是我的项目配置不可能放在项目里啊,这是最基本的需求!因为不能对代码做入侵,所以可以在app初始化的时候,去set到外围目录里。为什么要set到外围目录?很简单,因为配置本就应该和代码分离,这样开发环境就能和线上环境区分出来了。但是这样远远不够,作为配置,很多项目是重复使用同一配置的,那就公用起来,做为每个项目不同的部分,也要区分出来,所以配置就成为了一个配置中心。以前在使用java开发时,比较简单,用的是superdiamond作为配置中心,公用的部分完全可以共用,独立的部分也不会被干扰。但是php显然不行,作为以进程为模型的语言,不可能每处理一个请求的时候就去读一遍配置,而应该是读取本地的某个固定目录的php文件,将配置载入。一旦配置发生改变(比如 redis迁移,mysql迁移,域名变更等)不需要一台一台地去更新配置,而是直接更新发布机的配置文件,直接发布到各机器上,这种方式可以使用ansible去做,Jenkins也可以做。也有自己去实现的,比如在每台机器上安装一个客户端,然后定时去和中心机器上的文件做对比,有不同就更新。想到的也就推模式和拉模式两种方式。
2.接下来就是blade模板,关于这个模板,文档实在是太少了,在页面上做变量嵌套的时候总是出问题,也无从查起,只能换种方式去做
3.log 这个就不多说了,实用的log并不多,php中的exception和fatal error是俩概念,无从和java的exception比拟。需要一个日志收集系统,去收集log,做分析。elk在这方面应该很强大,zabbix应该也可以。
4.分页,虽然可以自己实现,但是想短时间内做一个美观的无错误逻辑的,还是要花时间的。虽然这个在文档上有,但是用下来才发现,是给bootstrap模板使用的!一万个草泥马奔腾而过,然后github上找到了一份,还挺好用,虽然是个简单的实现。代码不贴了,在这里,感谢开源。因为想尽量保证少量的依赖包,所以直接拷贝到文件里了。
先吐槽这些吧,吐槽归吐槽,laravel也有很多优点,比如依赖注入,validation,orm等等,很方便,不多说了

在尝试写扩展时,阅读到了这样类似的代码

1
2
3
4
5
#define ZVAL_BOOL(z, b) do { \
zval *__z = (z); \
Z_LVAL_P(__z) = ((b) != 0); \
Z_TYPE_P(__z) = IS_BOOL; \
} while (0)

好奇怪,明明是只做一次运算,为何要使用do{}while(0)的方式
google一下,涨了很多姿势
接下来的大部分部分参考do{…}while(0)的意义和用法

linux内核和其他一些开源的代码中,经常会遇到这样的代码:

1
2
3
do{
...
}while(0)

总结起来这样写主要有以下几点好处:

1、辅助定义复杂的宏,避免引用的时候出错:

举例来说,假设你需要定义这样一个宏:

1
2
3
#define DOSOMETHING()\
foo1();\
foo2()

这个宏的本意是,当调用DOSOMETHING()时,函数foo1()和foo2()都会被调用。但是如果你在调用的时候这么写:

1
2
if(a>0)
DOSOMETHING();

因为宏在预处理的时候会直接被展开,你实际上写的代码是这个样子的:

1
2
3
if(a>0)
foo1();
foo2();

这就出现了问题,因为无论a是否大于0,foo2()都会被执行,导致程序出错。

那么仅仅使用{}将foo1()和foo2()包起来行么?

我们在写代码的时候都习惯在语句右面加上分号,如果在宏中使用{},代码里就相当于这样写了:“{…};”,展开后就是这个样子:

1
2
3
4
5
if(a>0)
{
foo1();
foo2();
};

所以碰到这种调用宏:

1
2
3
4
if (a>0)
DOSOMETHING();
else
DOOTHERTHING();

会被扩展成

1
2
3
4
5
6
if (a>0) {
foo1();
foo2();
};
else
DOOTHERTHING(wolf);

这样不会编译通过。所以,很多人才采用了do{…}while(0);

1
2
3
4
5
6
7
8
9
10
11
12
#define DOSOMETHING() \
do{ \
foo1();\
foo2();\
}while(0)\
...
if(a>0)
DOSOMETHING();
...

这样,宏被展开后,才会保留初始的语义。GCC提供了Statement-Expressions用以替代do{…}while(0); 所以你也可以这样定义宏:

1
2
3
4
#define DOSOMETHING() ({\
foo1(); \
foo2(); \
})

2、避免使用goto对程序流进行统一的控制:

有些函数中,在函数return之前我们经常会进行一些收尾的工作,比如free掉一块函数开始malloc的内存,goto一直都是一个比较简便的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
int foo()
{
somestruct* ptr = malloc(...);
dosomething...;
if(error)
{
goto END;
}
dosomething...;
if(error)
{
goto END;
}
dosomething...;
END:
free(ptr);
return 0;
}

由于goto不符合软件工程的结构化,而且有可能使得代码难懂,所以很多人都不倡导使用,那这个时候就可以用do{}while(0)来进行统一的管理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
int foo()
{
somestruct* ptr = malloc(...);
do{
dosomething...;
if(error)
{
break;
}
dosomething...;
if(error)
{
break;
}
dosomething...;
}while(0);
free(ptr);
return 0;
}

这里将函数主体使用do()while(0)包含起来,使用break来代替goto,后续的处理工作在while之后,就能够达到同样的效果。

3、避免空宏引起的warning

内核中由于不同架构的限制,很多时候会用到空宏,在编译的时候,空宏会给出warning,为了避免这样的warning,就可以使用do{}while(0)来定义空宏:

1
#define EMPTYMICRO do{}while(0)

4、定义一个单独的函数块来实现复杂的操作:

当你的功能很复杂,变量很多你又不愿意增加一个函数的时候,使用do{}while(0);,将你的代码写在里面,里面可以定义变量而不用考虑变量名会同函数之前或者之后的重复。

理解了这个do{…}while(0),一切豁然开朗。
发现ZVAL_RESOURCE,ZVAL_STRING,ZVAL_ZVAL等宏也都是使用的do{}while(0)的形式,ZVAL_DOUBLE,ZVAL_LONG等并没有使用,个人理解ZVAL_BOOL这样定义宏属于第一种情况。

最近深入学习写扩展,发现C功底太弱了,也印证了那句话,出来混迟早要还的。
大学里因为不喜欢指针,放弃了C转战java,结果到现在,也只有自己给自己填坑了。
所以找了些文章充实一下吧。
本文转自C指针

第一章 指针的概念

指针是一个特殊的变量,它里面存储的数值被解释成为内存里的一个地址。

要搞清一个指针需要搞清指针的四方面的内容:指针的类型,指针所指向的类型,指针的值或者叫指针所指向的内存区,还有指针本身所占据的内存区。让我们分别说明。
先声明几个指针放着做例子:
例一:
(1)int ptr;
(2)char
ptr;
(3)int *ptr;
(4)int (
ptr)[3];
(5)int (ptr)[4];

  1. 指针的类型。
    从语法的角度看,你只要把指针声明语句里的指针名字去掉,剩下的部分就是这个指针的类型。这是指针本身所具有的类型。让我们看看例一中各个指针的类型:
    (1)int ptr; //指针的类型是int
    (2)char ptr; //指针的类型是char
    (3)int ptr; //指针的类型是 int
    (4)int (ptr)[3]; //指针的类型是 int()[3]
    (5)int (ptr)[4]; //指针的类型是 int ()[4]
    怎么样?找出指针的类型的方法是不是很简单?

  2. 指针所指向的类型。
    当你通过指针来访问指针所指向的内存区时,指针所指向的类型决定了编译器将把那片内存区里的内容当做什么来看待。从语法上看,你只须把指针声明语句中的指针名字和名字左边的指针声明符去掉,剩下的就是指针所指向的类型。例如:
    (1)int
    ptr; //指针所指向的类型是int
    (2)char ptr; //指针所指向的的类型是char
    (3)int **ptr; //指针所指向的的类型是 int

    (4)int (ptr)[3]; //指针所指向的的类型是 int()[3]
    (5)int
    (ptr)[4]; //指针所指向的的类型是 int ()[4]
    在指针的算术运算中,指针所指向的类型有很大的作用。指针的类型(即指针本身的类型)和指针所指向的类型是两个概念。当你对C越
    来越熟悉时,你会发现,把与指针搅和在一起的”类型”这个概念分成”指针的类型”和”指针所指向的类型”两个概念,是精通指针的关键点之一。我看了不少书,发现有些写得差的书中,就把指针的这两个概念搅在一起了,所以看起书来前后矛盾,越看越糊涂。

  3. 指针的值,或者叫指针所指向的内存区或地址。
    指针的值是指针本身存储的数值,这个值将被编译器当作一个地址,而不是一个一般的数值。在32位程序里,所有类型的指针的值都是一个32位整数,因为32位程序里内存地址全都是32位长。指针所指向的内存区就是从指针的值所代表的那个内存地址开始,长度为si
    zeof(指针所指向的类型)的一片内存区。以后,我们说一个指针的值是XX,就相当于说该指针指向了以XX为首地址的一片内存区域;我们说一个指针指向了某块内存区域,就相当于说该指针的值是这块内存区域的首地址。指针所指向的内存区和指针所指向的类型是两个完全不同的概念。在例一中,指针所指向的类型已经有了,但由于指针还未初始化,所以它所指向的内存区是不存在的,或者说是无意义的。以后,每遇到一个指针,都应该问问:这个指针的类型是什么?指针指向的类型是什么?该指针指向了哪里?

  4. 指针本身所占据的内存区。
    指针本身占了多大的内存?你只要用函数sizeof(指针的类型)测一下就知道
    了。在32位平台里,指针本身占据了4个字节的长度。
    指针本身占据的内存这个概念在判断一个指针表达式是否是左值时很有用。

第二章 指针的算术运算

指针可以加上或减去一个整数。指针的这种运算的意义和通常的数值的加减运算的意义是不一样的。例如:
例二:

1
2
3
4
5
char a[20];
int *ptr=a;
...
...
ptr++;

在上例中,指针ptr的类型是int*,它指向的类型是int,它被初始化为指向整形变量a。接下来的第3句中,指针ptr被加了1,编译器是这样处理的:它把指针ptr的值加上了sizeof(int),在32位程序中,是被加上了4。由于地址是用字节做单位的,故ptr所指向的地址由原来的变量a的地址向高地址方向增加了4个字节。由于char类型的长度是一个字节,所以,原来ptr是指向数组a的第0号单元开始的四个字节,此时指向了数组a中从第4号单元开始的四个字节。我们可以用一个指针和一个循环来遍历一个数组,看例子:
例三:

1
2
3
4
5
6
7
8
9
10
int array[20];
int *ptr=array;
...
//此处略去为整型数组赋值的代码。
...
for(i=0;i<20;i++)
{
(*ptr)++;
ptr++;
}

这个例子将整型数组中各个单元的值加1。由于每次循环都将指针ptr加1,所以每次循环都能访问数组的下一个单元。
再看例子:
例四:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
char a[20];
int *ptr=a;
...
...
ptr+=5;
```
在这个例子中,ptr被加上了5,编译器是这样处理的:将指针ptr的值加上5乘sizeof(int),在32位程序中就是加上了5乘4=20。由于地址的单位是字节,故现在的ptr所指向的地址比起加5后的ptr所指向的地址来说,向高地址方向移动了20个字节。在这个例子中,没加5前的ptr指向数组a的第0号单元开始的四个字节,加5后,ptr已经指向了数组a的合法范围之外了。虽然这种情况在应用上会出问题,但在语法上却是可以的。这也体现出了指针的灵活性。如果上例中,ptr是被减去5,那么处理过程大同小异,只不过ptr的值是被减去5乘sizeof(int),新的ptr指向的地址将比原来的ptr所指向的地址向低地址方向移动了20个字节。
总结一下,一个指针ptrold加上一个整数n后,结果是一个新的指针ptrnew,ptrnew的类型和ptrold的类型相同,ptrnew所指向的类型和ptrold所指向的类型也相同。ptrnew的值将比ptrold的值增加了n乘sizeof(ptrold所指向的类型)个字节。就是说,ptrnew所指向的内存区将比ptrold所指向的内存区向高地址方向移动了n乘sizeof(ptrold所指向的类型)个字节。一个指针ptrold减去一个整数n后,结果是一个新的指针ptrnew,ptrnew的类型和ptrold的类型相同,ptrnew所指向的类型和ptrold所指向的类型也相同。ptrnew的值将比ptrold的值减少了n乘sizeof(ptrold所指向的类型)个字节,就是说 ,ptrnew所指向的内存区将比ptrold所指向的内存区向低地址方向移动了n乘sizeof(ptrold所指向的类型)个字节。
第三章 运算符号&和*
-----------
这里&是取地址运算符,*是...书上叫做"间接运算符"。&a的运算结果是一个指针,指针的类型是a的类型加个*,指针所指向的类型
是a的类型,指针所指向的地址嘛,那就是a的地址。*p的运算结果就五花八门了。总之*p的结果是p所指向的东西,这个东西有这
些特点:它的类型是p指向的类型,它所占用的地址是p所指向的地址。
例五:

int a=12;
int b;
int p;
int **ptr;
p=&a;//&a的结果是一个指针,类型是int
,指向的类型是int,指向的地址是a的地址。
p=24;//p的结果,在这里它的类型是int,它所占用的地址是p所指向的地址,显然,p就是变量a。
ptr=&p;//&p的结果是个指针,该指针的类型是p的类型加个
,在这里是int。该指针所指向的类型是p的类型,这里是int。该指针所指向的地址就是指针p自己的地址。 ptr=&b;//ptr是个指针,&b的结果也是个指针,且这两个指针的类型和所指向的类型是一样的,所以用&b来给ptr赋值就是毫无 问题的了。 ptr=34;//ptr的结果是ptr所指向的东西,在这里是一个指针,对这个指针再做一次运算,结果就是一个int类型的变量。

1
2
3
4
5
6
第四章 指针表达式
---------
一个表达式的最后结果如果是一个指针,那么这个表达式就叫指针表达式。下面是一些指针表达式的例子:
例六:

int a,b;
int array[10];
int pa;
pa=&a;//&a是一个指针表达式。
int **ptr=&pa;//&pa也是一个指针表达式。
ptr=&b;//*ptr和&b都是指针表达式。
pa=array;
pa++;//这也是指针表达式。

1
2
3
4
5
6
7
8
9
例七:
```
char *arr[20];
char **parr=arr;//如果把arr看作指针的话,arr也是指针表达式
char *str;
str=*parr;//*parr是指针表达式
str=*(parr+1);//*(parr+1)是指针表达式
str=*(parr+2);//*(parr+2)是指针表达式

由于指针表达式的结果是一个指针,所以指针表达式也具有指针所具有的四个要素:指针的类型,指针所指向的类型,指针指向的内存区,指针自身占据的内存。好了,当一个指针表达式的结果指针已经明确地具有了指针自身占据的内存的话,这个指针表达式就是一个左值,否则就不是一个左值。
在例六中,&a不是一个左值,因为它还没有占据明确的内存。ptr是一个左值,因为ptr这个指针已经占据了内存,其实ptr就是指针pa,既然pa已经在内存中有了自己的位置,那么ptr当然也有了自己的位置。

第五章 数组和指针的关系

数组的数组名其实可以看作一个指针。看下例:
例八:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
int array[10]={0,1,2,3,4,5,6,7,8,9},value;
...
...
value=array[0];//也可写成:value=*array;
value=array[3];//也可写成:value=*(array+3);
value=array[4];//也可写成:value=*(array+4);
```
上例中,一般而言数组名array代表数组本身,类型是int [10],但如果把array看做指针的话,它指向数组的第0个单元,类型是int *,所指向的类型是数组单元的类型即int。因此*array等于0就一点也不奇怪了。同理,array+3是一个指向数组第3个单元的指针,所以*(array+3)等于3。其它依此类推。
例九:
```
char *str[3]={
"Hello,this is a sample!",
"Hi,good morning.",
"Hello world"
};
char s[80];
strcpy(s,str[0]);//也可写成strcpy(s,*str);
strcpy(s,str[1]);//也可写成strcpy(s,*(str+1));
strcpy(s,str[2]);//也可写成strcpy(s,*(str+2));

上例中,str是一个三单元的数组,该数组的每个单元都是一个指针,这些指针各指向一个字符串。把指针数组名str当作一个指针的话,它指向数组的第0号单元,它的类型是char,它指向的类型是char str也是一个指针,它的类型是char*,它所指向的类型是char,它指向的地址是字符串”Hello,this is a sample!”的第一个字符的地址,即’H’的地址。str+1也是一个指针,它指向数组的第1号单元,它的类型是char,它指向的类型是char (str+1)也是一个指针,它的类型是char*,它所指向的类型是char,它指向 “Hi,good morning.”的第一个字符’H’,等等。

下面总结一下数组的数组名的问题。声明了一个数组TYPE array[n],则数组名称array就有了两重含义:第一,它代表整个数组,它的类型是TYPE [n];第二,它是一个指针,该指针的类型是TYPE,该指针指向的类型是TYPE,也就是数组单元的类型,该指针指向的内存区就是数组第0号单元,该指针自己占有单独的内存区,注意它和数组第0号单元占据的内存区是不同的。该指针的值是不能修改的,即类似array++的表达式是错误的。
在不同的表达式中数组名array可以扮演不同的角色。在表达式sizeof(array)中,数组名array代表数组本身,故这时sizeof函数
测出的是整个数组的大小。在表达式
array中,array扮演的是指针,因此这个表达式的结果就是数组第 0号单元的值。sizeof(array)测出的是数组单元的大小。 表达式array+n(其中n=0,1,2,….。)中,array扮演的是指针,故arr
ay+n的结果是一个指针,它的类型是TYPE
,它指向的类型是TYPE,它指向数组第
n号单元。故sizeof(array+n)测出的是指针类型的大小。
例十:

1
2
3
int array[10];
int (*ptr)[10];
ptr=&array;

上例中ptr是一个指针,它的类型是int (*)[10],他指向的类型是int [10],我们用整个数组的首地址来初始化它。在语句ptr=&array中,array代表数组本身。

本节中提到了函数sizeof(),那么我来问一问,sizeof(指针名称)测出的究竟是指针自身类型的大小呢还是指针所指向的类型的大小?答案是前者。例如:

int (*ptr)[10];
则在32位程序中,有:

1
2
3
sizeof(int(*)[10])==4
sizeof(int [10])==40
sizeof(ptr)==4

实际上,sizeof(对象)测出的都是对象自身的类型的大小,而不是别的什么类型的大小。

第六章 指针和结构类型的关系

可以声明一个指向结构类型对象的指针。
例十一:

1
2
3
4
5
6
7
8
9
struct MyStruct
{
int a;
int b;
int c;
}
MyStruct ss={20,30,40};//声明了结构对象ss,并把ss的三个成员初始化为20,30和40。
MyStruct *ptr=&ss;//声明了一个指向结构对象ss的指针。它的类型是MyStruct*,它指向的类型是MyStruct。
int *pstr=(int*)&ss;//声明了一个指向结构对象ss的指针。但是它的类型和它指向的类型和ptr是不同的。

请问怎样通过指针ptr来访问ss的三个成员变量?
答案:

1
2
3
ptr->a;
ptr->b;
ptr->c;

又请问怎样通过指针pstr来访问ss的三个成员变量?
答案:
pstr;//访问了ss的成员a。 (pstr+1);//访问了ss的成员b。
(pstr+2)//访问了ss的成员c。
呵呵,虽然我在我的MSVC++6.0上调式过上述代码,但是要知道,这样使用pstr来访问结构成员是不正规的,为了说明为什么不正规,让我们看看怎样通过指针来访问数组的各个单元:
例十二:
int array[3]={35,56,37};
int
pa=array;
通过指针pa访问数组array的三个单元的方法是:
pa;//访问了第0号单元 (pa+1);//访问了第1号单元
(pa+2);//访问了第2号单元
从格式上看倒是与通过指针访问结构成员的不正规方法的格式一样。所有的C/C++编译器在排列数组的单元时,总是把各个数组单元存放在连续的存储区里,单元和单元之间没有空隙。但在存放结构对象的各个成员时,在某种编译环境下,可能会需要字对齐或双字对齐或者是别的什么对齐,需要在相邻两个成员之间加若干?quot;填充字节”,这就导致各个成员之间可能会有若干个字节的空隙。
所以,在例十二中,即使
pstr访问到了结构对象ss的第一个成员变量a,也不能保证(pstr+1)就一定能访问到结构成员b。因为成员a和成员b之间可能会有若干填充字节,说不定(pstr+1)就正好访问到了这些填充字节呢。这也证明了指针的灵活性。要是你的目的就是想看看各个结构成员之间到底有没有填充字节,嘿,这倒是个不错的方法。 通过指针访问结构成员的正确方法应该是象例十二中使用指针ptr的方法。

第七章 指针和函数的关系

可以把一个指针声明成为一个指向函数的指针。

1
2
3
4
5
6
int fun1(char*,int);
int (*pfun1)(char*,int);
pfun1=fun1;
....
....
int a=(*pfun1)("abcdefg",7);//通过函数指针调用函数。

可以把指针作为函数的形参。在函数调用语句中,可以用指针表达式来作为实参。
例十三:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int fun(char*);
int a;
char str[]="abcdefghijklmn";
a=fun(str);
...
...
int fun(char*s)
{
int num=0;
for(int i=0;i<strlen(s);i++)
{
num+=*s;s++;
}
return num;
)

这个例子中的函数fun统计一个字符串中各个字符的ASCII码值之和。前面说了,数组的名字也是一个指针。在函数调用中,当把str作为实参传递给形参s后,实际是把str的值传递给了s,s所指向的地址就和str所指向的地址一致,但是str和s各自占用各自的存储空间。在函数体内对s进行自加1运算,并不意味着同时对str进行了自加1运算。

第八章 指针类型转换

当我们初始化一个指针或给一个指针赋值时,赋值号的左边是一个指针,赋值号的右边是一个指针表达式。在我们前面所举的例子中,绝大多数情况下,指针的类型和指针表达式的类型是一样的,指针所指向的类型和指针表达式所指向的类型是一样的。
例十四:

1
2
3
float f=12.3;
float *fptr=&f;
int *p;

在上面的例子中,假如我们想让指针p指向实数f,应该怎么搞?是用下面的语句吗?
p=&f;
不对。因为指针p的类型是int,它指向的类型是int。表达式&f的结果是一个指针,指针的类型是float,它指向的类型是float。两者不一致,直接赋值的方法是不行的。至少在我的MSVC++6.0上,对指针的赋值语句要求赋值号两边的类型一致,所指向的类型也一致,其它的编译器上我没试过,大家可以试试。为了实现我们的目的,需要进行”强制类型转换”:
p=(int)&f;
如果有一个指针p,我们需要把它的类型和所指向的类型改为TYEP
和TYPE,那么语法格式是:
(TYPE)p;
这样强制类型转换的结果是一个新指针,该新指针的类型是TYPE
,它指向的类型是TYPE,它指向的地址就是原指针指向的地址。而原来的指针p的一切属性都没有被修改。

一个函数如果使用了指针作为形参,那么在函数调用语句的实参和形参的结合过程中,也会发生指针类型的转换。
例十五:

1
2
3
4
5
6
7
8
9
10
11
12
void fun(char*);
int a=125,b;
fun((char*)&a);
...
...
void fun(char*s)
{
char c;
c=*(s+3);*(s+3)=*(s+0);*(s+0)=c;
c=*(s+2);*(s+2)=*(s+1);*(s+1)=c;
}
}

注意这是一个32位程序,故int类型占了四个字节,char类型占一个字节。函数fun的作用是把一个整数的四个字节的顺序来个颠倒。注意到了吗?在函数调用语句中,实参&a的结果是一个指针,它的类型是int ,它指向的类型是int。形参这个指针的类型是char,它指向的类型是char。这样,在实参和形参的结合过程中,我们必须进行一次从int类型到char类型的转换。结合这个例子,我们可以这样来想象编译器进行转换的过程:编译器先构造一个临时指针 chartemp,然后执行temp=(char)&a,最后再把temp的值传递给s。所以最后的结果是:s的类型是char*,它指向的类型是char,它指向的地址就是a的首地址。

我们已经知道,指针的值就是指针指向的地址,在32位程序中,指针的值其实是一个32位整数。那可不可以把一个整数当作指针的值直接赋给指针呢?就象下面的语句:

1
2
3
4
5
6
7
8
unsigned int a;
TYPE *ptr;//TYPE是int,char或结构类型等等类型。
...
...
a=20345686;
ptr=20345686;//我们的目的是要使指针ptr指向地址20345686(十进制
ptr=a;//我们的目的是要使指针ptr指向地址20345686(十进制)

编译一下吧。结果发现后面两条语句全是错的。那么我们的目的就不能达到了吗?不,还有办法:

1
2
3
4
5
6
unsigned int a;
TYPE *ptr;//TYPE是int,char或结构类型等等类型。
...
...
a=某个数,这个数必须代表一个合法的地址;
ptr=(TYPE*)a;//呵呵,这就可以了。

严格说来这里的(TYPE)和指针类型转换中的(TYPE)还不一样。这里的(TYPE*)的意思是把无符号整数a的值当作一个地址来看待。
上面强调了a的值必须代表一个合法的地址,否则的话,在你使用ptr的时候,就会出现非法操作错误。

想想能不能反过来,把指针指向的地址即指针的值当作一个整数取出来。完全可以。下面的例子演示了把一个指针的值当作一个整数取出来,然后再把这个整数当作一个地址赋给一个指针:
例十六:

1
2
3
4
5
int a=123,b;
int *ptr=&a;
char *str;
b=(int)ptr;//把指针ptr的值当作一个整数取出来。
str=(char*)b;//把这个整数的值当作一个地址赋给指针str。

好了,现在我们已经知道了,可以把指针的值当作一个整数取出来,也可以把一个整数值当作地址赋给一个指针。

第九章 指针的安全问题

看下面的例子:
例十七:

1
2
3
4
char s=’a’;
int *ptr;
ptr=(int*)&s;
*ptr=1298;

指针ptr是一个int*类型的指针,它指向的类型是int。它指向的地址就是s的首地址。在32位程序中,s占一个字节,int类型占四个字节。最后一条语句不但改变了s所占的一个字节,还把和s相临的高地址方向的三个字节也改变了。这三个字节是干什么的?只有编译程序知道,而写程序的人是不太可能知道的。也许这三个字节里存储了非常重要的数据,也许这三个字节里正好是程序的一条代码,而由于你对指针的马虎应用,这三个字节的值被改变了!这会造成崩溃性的错误。
让我们再来看一例:
例十八:

1
2
3
4
5
6
char a;
int *ptr=&a;
...
...
ptr++;
*ptr=115;

该例子完全可以通过编译,并能执行。但是看到没有?第3句对指针ptr进行自加1运算后,ptr指向了和整形变量a相邻的高地址方向的一块存储区。这块存储区里是什么?我们不知道。有可能它是一个非常重要的数据,甚至可能是一条代码。而第4句竟然往这片存储区里写入一个数据!这是严重的错误。所以在使用指针时,程序员心里必须非常清楚:我的指针究竟指向了哪里。
在用指针访问数组的时候,也要注意不要超出数组的低端和高端界限,否则也会造成类似的错误。
在指针的强制类型转换:ptr1=(TYPE*)ptr2中,如果sizeof(ptr2的类型)大于sizeof(ptr1的类型),那么在使用指针ptr1来访问ptr2所指向的存储区时是安全的。如果sizeof(ptr2的类型)小于sizeof(ptr1的类型),那么在使用指针ptr1来访问ptr2所指向的存储区时是不安全的。至于为什么,读者结合例十七来想一想,应该会明白的。

新的一年 心的开始
过去一年,工作换了两次,一直都在试用期,过去一年的目标,也完成了不少。
参与设计开发了一些比较有规模的java项目,这个应该是完成的最好的了
php的框架,一直在学习,心中也有了一些简单框架的雏形
php扩展,一直以来,对c还是有些茫然,主要还是指针,不过最近通过书籍,基本熟悉了扩展开发流程,但是C的基础太差,也只能做最简单的开发
接下来,也为这一年,定下自己的目标吧
首先是C了,接下来的一年,一定要掌握这门语言,为以后打基础
接下来是扩展了,写一个基本的扩展
然后就是nginx了,了解nginx各模块,以及openrestry
就先这些吧,多说无益

闲了很多天,又不知道写点什么,看书看到了socket这块,还挺有意思,照着书上的模仿了一下,用socket模拟smtp协议发送邮件,邮件服务器是qq的。不过书有点老,qq的邮件服务器必须用https了,所以自己摸索着实现了一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
<?php
/**
* Created by PhpStorm.
* User: eric
* Date: 15/12/24
* Time: 上午10:54
*/
class Mail{
private $sock;
private $errorStr;
private $errorNo;
private $timeout=30;
private $host;
private $port;
private $user;
private $password;
function __construct($host,$port,$user,$pass,$format=1,$debug=0){
$this->host = $host;
$this->port = $port;
$this->user = base64_encode($user);
$this->password = base64_encode($pass);
$this->sock = fsockopen($this->host,$this->port,$this->errorNo,$this->errorStr,$this->timeout);
if(!$this->sock){
exit("oops");
}
$response = fgets($this->sock);
if(strstr($response,'220') === false){
exit("server error");
}
}
function showDebug($response,$commond){
echo $commond;
echo $response.PHP_EOL;
}
function doCommond($commond,$return_code){
fwrite($this->sock,$commond);
$response = fgets($this->sock);
if(stristr($response,"$return_code") == false){
$this->showDebug($response,$commond);
return false;
}
return true;
}
function sendMail($from,$to,$sub,$body){
$detail = "FROM:".$from."\r\n";
$detail.="To:".$to."\r\n";
$detail.="Subject:".$sub."\r\n";
$detail.="Content-type:text/plain;\r\n\r\n";
$detail.=$body;
$this->doCommond("HELO smtp.qq.com\r\n",250);
$this->doCommond("AUTH LOGIN\r\n",334);
$this->doCommond($this->user."\r\n",334);
$this->doCommond($this->password."\r\n",235);
$this->doCommond("MAIL FROM:".$from."\r\n",250);
$this->doCommond("RCPT TO:".$to."\r\n",250);
$this->doCommond("DATA\r\n",354);
$this->doCommond($detail."\r\n.\r\n",250);
$this->doCommond("QUIT\r\n",221);
return true;
}
}
$mail = new Mail('ssl://smtp.qq.com',465,'xxx@qq.com','特殊的密码,因为我有邮箱独立密码,所以只能使用qq邮箱额外提供的');
$mail->sendMail('xxx@qq.com','xxxx@qq.com','hi','嘿嘿嘿');

因为smtp可以在telnet中模拟,所以想着去模拟一个请求去访问redis也应该是可以的,在没有装redis扩展或者composer依赖的时候,简单的请求可以直接用socket实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php
/**
* Created by PhpStorm.
* User: eric
* Date: 15/12/24
* Time: 下午2:15
*/
$socket = fsockopen('127.0.0.1',6379);
fwrite($socket,"ping\r\n");
$response = fgets($socket);
echo "<pre>";print_r($response);
fwrite($socket,"set a aaa\r\n ");
$response = fgets($socket);
echo "<pre>";print_r($response);
fwrite($socket,"get a\r\n");
$response = fgets($socket);
echo "<pre>";print_r($response);

接下来尝试下用php socket搭建一个煎蛋代理服务(因为之前转过了一篇node)

看了并发编程网的这篇翻译
看了标题,首先想到的是如何实现一个单机锁,因为redis中大多数操作都是原子的,所以想法是:

  • 在redis中存在一个key,保证客户端在读到这个key的时候,阻塞
  • 如果一个客户端在读不到的这个key的时候,说明没有客户端持有锁,可以将其上锁
  • 获取到锁的客户端在执行逻辑完后,释放锁

redis中的命令刚好有一个这样的命令 setnx。
继续看下面的翻译,发现自己的想法过于简单,任何设计都应该遵循design for failure 的原则

  • 如果一个客户端在获取到锁后,因为某些原因,down掉了,所有的客户端就永远不会持有锁了
  • 针对上面的情况,可以为这个key设置一个存活时间(但是setnx并不支持存在时间,如果使用expire,不能保证原子性,看了文档,在2.6后续版本中,set已经可以替代setnx了[set的文档]http://redis.io/commands/set)
  • 针对上一条的情况,由于某些因素导致某客户端A获取锁后执行的时间过长,一致于超过了锁的缓存时间,这时又有其他客户端B获取到锁,A执行完成之后,释放锁,这时其他客户端就会获取锁。所以需要为每一个锁设置唯一的value,保证只有持有锁的客户端才能删除它

在其他的blog里也看到了类似的想法谈谈Redis的SETNX

redis作者也给出了代码(php版本),这里不贴了,分布式版本中,考虑的问题就更多了。原文中,针对分布式中的情况作了很多解释,翻译也有了,就不贴了,不过翻译有的地方是错的,以后还是看原文吧,英语看的慢点,多看几遍,不至于理解错

今天看到有人问php中对象如何转化成数组,想到的第一个方式便是json encode和decode,不过这种方式在性能上会有些问题,因为序列化反序列化始终是个耗时的操作

1
json_decode(json_encode($object),true)

然后有人说

1
(array)$object

就可以转化了,试了一下很神奇,竟然可以。但是当一个对象属性是对象时,没法将该对象的属性也变成一个数组
所以还是需要写一个递归函数

1
2
3
4
5
6
7
8
9
function objectToArray($stobject){
$_array = is_object($stobject) ? get_object_vars($stobject) : $stobject;
if(is_array($_array)){
foreach ($_array as $key => $value) {
$value = (is_array($value) || is_object($value)) ? objectToArray($value) : $value;
$array[$key] = $value;
}}
return $array;
}

array2object也是同样的道理,如果比较懒,可以直接使用json的方式

虽然mac常以待机状态恢复基本不关机,不过有时候会死机。
现在常用nginx作为php的应用服务器,而且反向代理nginx使用起来比较方便。
所以需要关闭apache,释放80端口,使用nginx

mac os不像linux有/etc/init.d/rc.local以及service的方式可以设置程序随机启动,而是使用plist文件管理。

apache的plist文件在/System/Library/LaunchDaemons目录下,文件名是org.apache.httpd.plist

launchctl 管理OS X的启动脚本,控制启动计算机时需要开启的服务。也可以设置定时执行特定任务的脚本,就像Linux cron一样。

Launchd脚本存储在以下位置:

~/Library/LaunchAgents    
/Library/LaunchAgents          
/Library/LaunchDaemons
/System/Library/LaunchAgents
/System/Library/LaunchDaemons

可以使用 launchctl list |grep apache 查看是否开机启动apache
关闭开机启动

sudo launchctl unload  -w /System/Library/LaunchDaemons/org.apache.httpd.plist

然而这并没有完,使用/usr/sbin/apachectl启动apache的时候,apache依旧会重新开机启动,原因是/usr/sbin/apachectl这个脚本里,使用的是launchctl load来启动的,它会把它加入到启动项里,解决办法就是自己写一个httpd的启动关闭脚本,直接启动httpd

早起早到公司,安静地看会tech blog,很喜欢这种宁静的感觉。
以前稀里糊涂看了http权威指南,并没有领会什么深刻点的东西,大概是经验不足吧,早上看的这篇文章,感觉不错,转一下。
文章转自HTTP 请求头中的 X-Forwarded-For

我一直认为,对于从事 Web 前端开发的同学来说,HTTP 协议以及其他常见的网络知识属于必备项。一方面,前端很多工作如 Web 性能优化,大部分规则都跟 HTTP、HTTPS、SPDY 和 TCP 等协议的特点直接对应,如果不从协议本身出发而是一味地照办教条,很可能适得其反。另一方面,随着 Node 的发展壮大,越来越多的前端同学开始写服务端程序,甚至是框架(ThinkJS 就是这样由前端工程师开发,并有着众多前端工程师用户的 Node 框架),掌握必要的网络知识,对于服务端程序安全、部署、运维等工作来说至关重要。
我的博客有一个「HTTP 相关」专题,今后会陆续更新更多内容进去,欢迎关注。今天要说的是 HTTP 请求头中的 X-Forwarded-For(XFF)。

背景


通过名字就知道,X-Forwarded-For 是一个扩展头。HTTP/1.1(RFC 2616)协议并没有对它的定义,它最开始是由 Squid 这个缓存代理软件引入,用来表示 HTTP 请求端真实 IP,现在已经成为事实上的标准,被各大 HTTP 代理、负载均衡等转发服务广泛使用,并被写入 RFC 7239(Forwarded HTTP Extension)标准之中。
X-Forwarded-For 请求头格式非常简单,就这样:
X-Forwarded-For: client, proxy1, proxy2
可以看到,XFF 的内容由「英文逗号 + 空格」隔开的多个部分组成,最开始的是离服务端最远的设备 IP,然后是每一级代理设备的 IP。
如果一个 HTTP 请求到达服务器之前,经过了三个代理 Proxy1、Proxy2、Proxy3,IP 分别为 IP1、IP2、IP3,用户真实 IP 为 IP0,那么按照 XFF 标准,服务端最终会收到以下信息:
X-Forwarded-For: IP0, IP1, IP2
Proxy3 直连服务器,它会给 XFF 追加 IP2,表示它是在帮 Proxy2 转发请求。列表中并没有 IP3,IP3 可以通过服务端的 Remote Address 字段获得。我们知道 HTTP 连接基于 TCP 连接,HTTP 协议中没有 IP 的概念,Remote Address 来自 TCP 连接,表示与服务端建立 TCP 连接的设备 IP,在这个例子里就是 IP3。
Remote Address 无法伪造,因为建立 TCP 连接需要三次握手,如果伪造了源 IP,无法建立 TCP 连接,更不会有后面的 HTTP 请求。不同语言获取 Remote Address 的方式不一样,例如 php 是 $_SERVER[“REMOTE_ADDR”],Node 是 req.connection.remoteAddress,但原理都一样。

问题


有了上面的背景知识,开始说问题。我用 Node 写了一个最简单的 Web Server 用于测试。HTTP 协议跟语言无关,这里用 Node 只是为了方便演示,换成任何其他语言都可以得到相同结论。另外本文用 Nginx 也是一样的道理,如果有兴趣,换成 Apache 或其他 Web Server 也一样。
下面这段代码会监听 9009 端口,并在收到 HTTP 请求后,输出一些信息:
JS

var http = require('http');

http.createServer(function (req, res) {
    res.writeHead(200, {'Content-Type': 'text/plain'});
    res.write('remoteAddress: ' + req.connection.remoteAddress + '\n');
    res.write('x-forwarded-for: ' + req.headers['x-forwarded-for'] + '\n');
    res.write('x-real-ip: ' + req.headers['x-real-ip'] + '\n');
    res.end();
}).listen(9009, '0.0.0.0');

这段代码除了前面介绍过的 Remote Address 和 X-Forwarded-For,还有一个 X-Real-Ip,这又是一个自定义头。X-Real-Ip 通常被 HTTP 代理用来表示与它产生 TCP 连接的设备 IP,这个设备可能是其他代理,也可能是真正的请求端。需要注意的是,X-Real-Ip 目前并不属于任何标准,代理和 Web 应用之间可以约定用任何自定义头来传递这个信息。
现在可以用域名 + 端口号直接访问这个 Node 服务,再配一个 Nginx 反向代理:
NGINX

location / {
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header Host $http_host;
    proxy_set_header X-NginX-Proxy true;

    proxy_pass http://127.0.0.1:9009/;
    proxy_redirect off;
}

我的 Nginx 监听 80 端口,所以不带端口就可以访问 Nginx 转发过的服务。
测试直接访问 Node 服务:
SHELL

curl http://t1.imququ.com:9009/

remoteAddress: 114.248.238.236
x-forwarded-for: undefined
x-real-ip: undefined

由于我的电脑直接连接了 Node 服务,Remote Address 就是我的 IP。同时我并未指定额外的自定义头,所以后两个字段都是 undefined。
再来访问 Nginx 转发过的服务:
SHELL

curl http://t1.imququ.com/

remoteAddress: 127.0.0.1
x-forwarded-for: 114.248.238.236
x-real-ip: 114.248.238.236

这一次,我的电脑是通过 Nginx 访问 Node 服务,得到的 Remote Address 实际上是 Nginx 的本地 IP。而前面 Nginx 配置中的这两行起作用了,为请求额外增加了两个自定义头:

proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

实际上,在生产环境中部署 Web 应用,一般都采用上面第二种方式,好处多多,具体是哪些不是本文重点不写了。这就引入一个隐患:很多 Web 应用为了获取用户真正的 IP,从 HTTP 请求头中获取 IP。
HTTP 请求头可以随意构造,我们通过 curl 的 -H 参数构造 X-Forwarded-Fox 和 X-Real-Ip,再来测试一把。
直接访问 Node 服务:
SHELL

curl http://t1.imququ.com:9009/ -H 'X-Forwarded-For: 1.1.1.1' -H 'X-Real-Ip: 2.2.2.2'

remoteAddress: 114.248.238.236
x-forwarded-for: 1.1.1.1
x-real-ip: 2.2.2.2

对于 Web 应用来说,X-Forwarded-Fox 和 X-Real-Ip 就是两个普通的请求头,自然就不做任何处理原样输出了。这说明,对于直连部署方式,除了从 TCP 连接中得到的 Remote Address 之外,请求头中携带的 IP 信息都不能信。
访问 Nginx 转发过的服务:
SHELL

curl http://t1.imququ.com/ -H 'X-Forwarded-For: 1.1.1.1' -H 'X-Real-Ip: 2.2.2.2'

remoteAddress: 127.0.0.1
x-forwarded-for: 1.1.1.1, 114.248.238.236
x-real-ip: 114.248.238.236

这一次,Nginx 会在 X-Forwarded-For 后追加我的 IP;并用我的 IP 覆盖 X-Real-Ip 请求头。这说明,有了 Nginx 的加工,X-Forwarded-For 最后一节以及 X-Real-Ip 整个内容无法构造,可以用于获取用户 IP。
用户 IP 往往被使用在跟 Web 安全有关的场景上,例如检查用户登录地区,基于 IP 做访问频率控制等等。这种场景下,确保 IP 无法构造更重要。经过前面的测试和分析,对于直接面向用户部署的 Web 应用,必须使用从 TCP 连接中得到的 Remote Address;对于部署了 Nginx 这样反向代理的 Web 应用,在正确配置了 Set Header 行为后,可以使用 Nginx 传过来的 X-Real-Ip 或 X-Forwarded-Ip 最后一节(实际上它们一定等价)。
那么,Web 应用自身如何判断请求是直接过来,还是由可控的代理转发来的呢?在代理转发时增加额外的请求头是一个办法,但是不怎么保险,因为请求头太容易构造了。如果一定要这么用,这个自定义头要够长够罕见,还要保管好不能泄露出去。
判断 Remote Address 是不是本地 IP 也是一种办法,不过也不完善,因为在 Nginx 所处服务器上访问,无论直连还是走 Nginx 代理,Remote Address 都是 127.0.0.1。这个问题还好通常可以忽略,更麻烦的是,反向代理服务器和实际的 Web 应用不一定部署在同一台服务器上。所以更合理的做法是收集所有代理服务器 IP 列表,Web 应用拿到 Remote Address 后逐一比对来判断是以何种方式访问。
通常,为了简化逻辑,生产环境会封掉通过带端口直接访问 Web 应用的形式,只允许通过 Nginx 来访问。那是不是这样就没问题了呢?也不见得。
首先,如果用户真的是通过代理访问 Nginx,X-Forwarded-For 最后一节以及 X-Real-Ip 得到的是代理的 IP,安全相关的场景只能用这个,但有些场景如根据 IP 显示所在地天气,就需要尽可能获得用户真实 IP,这时候 X-Forwarded-For 中第一个 IP 就可以排上用场了。这时候需要注意一个问题,还是拿之前的例子做测试:
SHELL

curl http://t1.imququ.com/ -H 'X-Forwarded-For: unknown, <>"1.1.1.1'
remoteAddress: 127.0.0.1
x-forwarded-for: unknown, <>"1.1.1.1, 114.248.238.236
x-real-ip: 114.248.238.236

X-Forwarded-For 最后一节是 Nginx 追加上去的,但之前部分都来自于 Nginx 收到的请求头,这部分用户输入内容完全不可信。使用时需要格外小心,符合 IP 格式才能使用,不然容易引发 SQL 注入或 XSS 等安全漏洞。
结论
直接对外提供服务的 Web 应用,在进行与安全有关的操作时,只能通过 Remote Address 获取 IP,不能相信任何请求头;
使用 Nginx 等 Web Server 进行反向代理的 Web 应用,在配置正确的前提下,要用 X-Forwarded-For 最后一节 或 X-Real-Ip 来获取 IP(因为 Remote Address 得到的是 Nginx 所在服务器的内网 IP);同时还应该禁止 Web 应用直接对外提供服务;
在与安全无关的场景,例如通过 IP 显示所在地天气,可以从 X-Forwarded-For 靠前的位置获取 IP,但是需要校验 IP 格式合法性;
PS:网上有些文章建议这样配置 Nginx,其实并不合理:

proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $remote_addr;

这样配置之后,安全性确实提高了,但是也导致请求到达 Nginx 之前的所有代理信息都被抹掉,无法为真正使用代理的用户提供更好的服务。还是应该弄明白这中间的原理,具体场景具体分析。

工作时候常需要修改某些root权限的文件(如/etc/hosts),然而经常忘记打sudo命令,直接更改文件后,保存修改时才发现,白改了
退出后执行

1
sudo !!

可以用root权限执行上一次执行的命令,然后重新修改文件

最近发现了一个比较好的方法

1
w !sudo tee %

w的意思是把当前文件的所有行转到标准输入 !表示要执行cmd命令,tee是一个小工具,读取标准输入的数据,并将其内容输出成文件。%表示当前文件,所以很好理解了,把当前文件传入到标准输入流,并使用root权限读取标准输入的数据输出到当前文件。

当然并没有完,tee也会将缓存内容输出到标准输出,所以我们要忽略它,命令进化为

1
w !sudo tee > /dev/null %

但是这个在vim里使用 要敲打着么多 费劲不?
当然费劲,而且每次都是敲同样的
所以把它放到vimrc里

1
cmap w!! w !sudo tee > /dev/null %

这样再有什么需要root权限的文件,确定需要修改的时候 就可以 使用:w!!(很像 sudo !!)就完成了 省了很多时间

参考
How does the vim “write with sudo” trick work?