Shell编程的独特之处

2017年9月30日 shell No comments

  1. 定义变量时,变量名和等号之间不能有空格
  2. 变量名定义时不加$符号,使用时要在变量名前面加$符号
  3. 用括号来表示数组,数组元素用”空格”符号分割开
  4. 读取数组元素值的一般格式是:${array_name[index]}
  5. 表达式和运算符之间要有空格,例如 2+2 是不对的,必须写成 2 + 2
  6. 条件表达式要放在方括号之间,并且要有空格,例如: [$a==$b] 是错误的,必须写成 [ $a == $b ]
  7. 乘号(*)前边必须加反斜杠()才能实现乘法运算,例如,val=expr $a \* $b
  8. 关系运算符(-eq/-ne/-gt/-lt/-ge/-le)只支持数字,不支持字符串,除非字符串的值是数字;
  9. 字符串的比较要使用字符串运算符(=/!=/-z/-n/str)
  10. sh的流程控制不可为空,,如果else分支没有语句执行,就不要写这个else
  11. case语句取值后面必须为单词in,每一模式必须以右括号结束。取值可以为变量或常数。匹配发现取值符合某一模式后,其间所有命令开始执行直至 ;;
  12. case语句需要一个esac(就是case反过来)作为结束标记,每个case分支用右圆括号,用两个分号表示break。
  13. Shell 函数参数返回,可以显示加:return 返回,如果不加,将以最后一条命令运行结果,作为返回值
  14. 所有函数在使用前必须定义。这意味着必须将函数放在脚本开始部分,直至shell解释器首次发现它时,才可以使用。调用函数仅使用其函数名即可。
  15. 在Shell中,调用函数时可以向其传递参数。在函数体内部,通过 $n 的形式来获取参数的值,例如,$1表示第一个参数,$2表示第二个参数,但是$10 不能获取第十个参数,获取第十个参数需要${10}。当n>=10时,需要使用${n}来获取参数。
  16. 函数返回值在调用该函数后通过 $? 来获得。
  17. Shell 文件包含的语法格式如下:. filename # 注意点号(.)和文件名中间有一空格 或source filename

nginx反向代理web socket

2017年9月23日 WebServer No comments

概述

WebSocket协议支持客户端与服务端之间进行全双工通信,得到了越来越多的浏览器的支持。在使用过程中,可以直接new WebSocket(“ws://domain:port”);的方式连接服务端,也可用通过Nginx的反向代理功能连接websocket服务器。使用Nginx反向代理主要有两点好处:

  1. Nginx可以作为负载均衡,保证WebSocket服务器的高性能和高可用;
  2. 可以直接使用Web服务的80/443端口,不需要额外开放websocket服务的外网端口。

Nginx配置

server {
    listen 80;
    server_name hungerhunger.cn;
    ......
    location ^~ /socketapi/ {
        proxy_connect_timeout 1s;
        proxy_next_upstream http_502 http_504 http_404 error timeout invalid_header;

        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_pass http://websocket;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
    }
}

upstream websocket {
    server 192.168.0.1:8080;
    server 192.168.0.2:8080;
    server 192.168.0.3:8080;
}

协议转换

WebSocket握手是通过HTTP来完成的,主要使用HTTP的Upgrade来升级连接从HTTP到WebSocket,Upgrade和Connection的头信息必须被显式的设置。

    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";

参数传递

使用nginx作为代理,nginx实际上充当了websocket服务器的客户端,所以在websocket服务端获取的客户端ip为nginx服务器的ip,并非实际的客户端ip。nginx可以将客户端真实ip通过http header传递进来。

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

负载均衡

负载均衡是通过Nginx的upstream实现的,默认使用轮询方式进行负载,每个请求按时间顺序逐一分配到不同的后端websocket服务器,保证服务高性能和可扩展。

   proxy_pass http://websocket;
    upstream websocket {
        server 192.168.0.1:8080;
        server 192.168.0.2:8080;
        server 192.168.0.3:8080;
    }

高可用

通过设置proxy_connect_timeout、proxy_next_upstream等指令可以实现后端服务器的健康检查,当某一台后端节点出现故障时,自动切换到健康节点来提供访问,保证服务的高可用。主要有这样以下几个指令: proxy_connect_timeout 指令设置与后端服务器建立连接的超时时间

语法: proxy_connect_timeout time;
默认值:proxy_connect_timeout 60s;
上下文:http, server, location

proxy_read_timeout指令定义从后端服务器读取响应的超时。此超时是指相邻两次读操作之间的最长时间间隔,而不是整个响应传输完成的最长时间。如果后端服务器在超时时间段内没有传输任何数据,连接将被关闭。

语法: proxy_read_timeout time;
默认值:    proxy_read_timeout 60s;
上下文:    http, server, location

proxy_next_upstream指令指定在何种情况下一个失败的请求应该被发送到下一台后端服务器

error      # 和后端服务器建立连接时,或者向后端服务器发送请求时,或者从后端服务器接收响应头时,出现错误
timeout    # 和后端服务器建立连接时,或者向后端服务器发送请求时,或者从后端服务器接收响应头时,出现超时
invalid_header  # 后端服务器返回空响应或者非法响应头
http_500   # 后端服务器返回的响应状态码为500
http_502   # 后端服务器返回的响应状态码为502
http_503   # 后端服务器返回的响应状态码为503
http_504   # 后端服务器返回的响应状态码为504
http_404   # 后端服务器返回的响应状态码为404
off        # 停止将请求发送给下一台后端服务器

upstream 中的server指令

upstream websocket {
    server 192.168.0.1:8080 max_fails=1 fail_timeout=10s; 
    server 192.168.0.2:8080 max_fails=1 fail_timeout=10s;
    server 192.168.0.3:8080 max_fails=1 fail_timeout=10s;
 }

max_fails设定Nginx与服务器通信的尝试失败的次数。在fail_timeout参数定义的时间段内,如果失败的次数达到此值,Nginx就认为服务器不可用。在下一个fail_timeout时间段,服务器不会再被尝试。 失败的尝试次数默认是1。设为0就会停止统计尝试次数,认为服务器是一直可用的。 可以通过指令proxy_next_upstream来配置什么是失败的尝试。 fail_timeout设定服务器被认为不可用的时间段以及统计失败尝试次数的时间段。在这段时间中,服务器失败次数达到指定的尝试次数,服务器就被认为不可用。默认情况下,该超时时间是10秒。

参考

  1. http://www.oschina.net/translate/websocket-nginx
  2. http://www.cnblogs.com/kevingrace/p/6685698.html
  3. http://saiyaren.iteye.com/blog/1914865

web服务器iptables配置

2017年9月20日 WebServer No comments

概述

iptables 是建立在 netfilter 架构基础上的一个包过滤管理工具,最主要的作用是用来做防火墙或透明代理。Iptables 提供以下三种功能:包过滤、NAT(网络地址转换)和通用的 pre-route packet mangling。对于web服务器来讲,主要利用的是防火墙功能。

原理

iptables实现防火墙功能的原理是:在数据包经过内核的过程中有五处关键地方,分别是PREROUTING、INPUT、OUTPUT、FORWARD、POSTROUTING,称为钩子函数。

  • INPUT链:当接收到防火墙本机地址的数据包(入站)时,应用此链中的规则。
  • OUTPUT链:当防火墙本机向外发送数据包(出站)时,应用此链中的规则。
  • FORWARD链:当接收到需要通过防火墙发送给其他地址的数据包(转发)时,应用此链中的规则。
  • PREROUTING链:在对数据包作路由选择之前,应用此链中的规则,如DNAT。
  • POSTROUTING链:在对数据包作路由选择之后,应用此链中的规则,如SNAT。
  • iptables这款用户空间的软件可以在这5处地方写规则,对经过的数据包进行处理,规则一般的定义为“如果数据包头符合这样的条件,就这样处理数据包”。

防火墙处理数据包的方式(规则):

  • ACCEPT:允许数据包通过
  • DROP:直接丢弃数据包,不给任何回应信息
  • REJECT:拒绝数据包通过,必要时会给数据发送端一个响应的信息。
  • SNAT:源地址转换。在进入路由层面的route之前,重新改写源地址,目标地址不变,并在本机建立NAT表项,当数据返回时,根据NAT表将目的地址数据改写为数据发送出去时候的源地址,并发送给主机。解决内网用户用同一个公网地址上网的问题。
  • MASQUERADE,是SNAT的一种特殊形式,适用于像adsl这种临时会变的ip上
  • DNAT:目标地址转换。和SNAT相反,IP包经过route之后、出本地的网络栈之前,重新修改目标地址,源地址不变,在本机建立NAT表项,当数据返回时,根据NAT表将源地址修改为数据发送过来时的目标地址,并发给远程主机。可以隐藏后端服务器的真实地址。
  • REDIRECT:是DNAT的一种特殊形式,将网络包转发到本地host上(不管IP头部指定的目标地址是啥),方便在本机做端口转发。
  • LOG:在/var/log/messages文件中记录日志信息,然后将数据包传递给下一条规则

iptables

数据包先经过PREOUTING,由该链确定数据包的走向:

  1. 目的地址是本地,则发送到INPUT,让INPUT决定是否接收下来送到用户空间,流程为①—>②;
  2. 若满足PREROUTING的nat表上的转发规则,则发送给FORWARD,然后再经过POSTROUTING发送出去,流程为: ①—>③—>④—>⑥
  3. 主机发送数据包时,流程则是⑤—>⑥

iptables编写规则

rule

  • [-t 表名]:该规则所操作的哪个表,可以使用filter、nat等,如果没有指定则默认为filter
  • -A:新增一条规则,到该规则链列表的最后一行
  • -I:插入一条规则,原本该位置上的规则会往后顺序移动,没有指定编号则为1
  • -D:从规则链中删除一条规则,要么输入完整的规则,或者指定规则编号加以删除
  • -R:替换某条规则,规则替换不会改变顺序,而且必须指定编号。
  • -P:设置某条规则链的默认动作 -nL:-L、-n,查看当前运行的防火墙规则列表
  • chain名:指定规则表的哪个链,如INPUT、OUPUT、FORWARD、PREROUTING等
  • [规则编号]:插入、删除、替换规则时用,–line-numbers显示号码
  • [-i|o 网卡名称]:i是指定数据包从哪块网卡进入,o是指定数据包从哪块网卡输出
  • [-p 协议类型]:可以指定规则应用的协议,包含tcp、udp和icmp等
  • [-s 源IP地址]:源主机的IP地址或子网地址
  • [–sport 源端口号]:数据包的IP的源端口号
  • [-d目标IP地址]:目标主机的IP地址或子网地址
  • [–dport目标端口号]:数据包的IP的目标端口号
  • -m:extend matches,这个选项用于提供更多的匹配参数,如:

    -m state –state ESTABLISHED,RELATED
    -m tcp –dport 22
    -m multiport –dports 80,8080
    -m icmp –icmp-type 8

  • <-j 动作>:处理数据包的动作,包括ACCEPT、DROP、REJECT等

iptables配置

1.查看关于IPTABLES设置

[root@iZwz96p7abo3ry2g07zyq9Z liyuhang]# iptables -L -n
Chain INPUT (policy ACCEPT)
target     prot opt source               destination         

Chain FORWARD (policy ACCEPT)
target     prot opt source               destination         

Chain OUTPUT (policy ACCEPT)
target     prot opt source               destination   

本机未设置任何防火墙策略

2.清除原有规则

 [root@hunger ~]# iptables -F 清除预设表filter中的所有规则链的规则
 [root@hunger ~]# iptables -X 清除预设表filter中使用者自定链中的规则

注意:一定要在 Chain INPUT (policy ACCEPT)的情况下,才能执行iptables -F,否则,如果是ssh登陆到主机上的话,会马上断开

3.添加默认规则

[root@hunger ~]# iptables -P INPUT ACCEPT 
[root@hunger ~]# iptables -P FORWARD ACCEPT 
[root@hunger ~]# iptables -P OUTPUT ACCEPT

4.添加规则

//ssh
[root@hunger ~]# iptables -A INPUT -p tcp --dport 22 -j ACCEPT 
[root@hunger ~]# iptables -A OUTPUT -p tcp --dport 22 -j ACCEPT 

// web访问
[root@hunger ~]# iptables -A INPUT -p tcp --dport 80 -j ACCEPT 
[root@hunger ~]# iptables -A INPUT -p tcp --dport 443 -j ACCEPT 
[root@hunger ~]# iptables -A OUTPUT -p tcp --dport 80 -j ACCEPT 
[root@hunger ~]# iptables -A OUTPUT -p tcp --dport 443 -j ACCEPT 

//允许ping
[root@hunger ~]# iptables -A INPUT -p icmp -j ACCEPT 
[root@hunger ~]# iptables -A OUTPUT -p icmp -j ACCEPT

//设置环路,使得 ping 127.0.0.1这样的包额可以通过
[root@hunger ~]# iptables -A INPUT -i lo -j ACCEPT 
[root@hunger ~]# iptables -A OUTPUT -o lo -j ACCEPT 

//开放DNS端口,不然无法访问外网
[root@hunger ~]# iptables -A INPUT -p udp --sport 53 -j ACCEPT
[root@hunger ~]# iptables -A INPUT -p udp --dport 53 -j ACCEPT
[root@hunger ~]# iptables -A OUTPUT -p udp --dport 53 -j ACCEPT
[root@hunger ~]# iptables -A OUTPUT -p udp --sport 53 -j ACCEPT

#allow exist connection
[root@hunger ~]# iptables -A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT 
[root@hunger ~]# iptables -A OUTPUT -m state --state ESTABLISHED,RELATED -j ACCEPT 

5.重置预设规则

[root@hunger ~]# iptables -P INPUT DROP 
[root@hunger ~]# iptables -P OUTPUT DROP

FORWARD 链配置同INPUT OUTPUT 只有当作为路由器时才需要配置

6.保存规则

[root@hunger ~]# /etc/rc.d/init.d/iptables save 
iptables: Saving firewall rules to /etc/sysconfig/iptables:[ OK ]

7.重启iptables

[root@hunger ~]# service iptables restart 

参考

  1. http://www.cnblogs.com/alimac/p/5848372.html
  2. https://www.nigesb.com/setting-up-standard-web-server-iptables.html
  3. http://www.linuxidc.com/Linux/2016-09/134832.htm
  4. https://segmentfault.com/a/1190000002540601

升级php版本

2017年9月13日 PHP, WebServer No comments

背景

新业务要使用workman做websocket长连接,wornman中使用了命名空间,开发机上的php版本为5.2,不支持命名空间,所以准备升级一下php,考虑到线上服务器版本,暂时将php版本升级为与线上业务相同的php5.5.25

原版本介绍

原版本php5.2.17采用源码编译安装,通过php -i 可以看到configure参数如下:

./configure  --with-apxs2=/usr/local/apache2/bin/apxs --enable-mbstring --with-mysql=/usr/local/mysql --with-pdo-mysql=/usr/local/mysql --with-libxml-dir --with-iconv --enable-sqlite-utf8 --with-zlib --with-curl --with-gd --with-freetype-dir=/usr/lib64/ --with-jpeg-dir=/usr/lib64 --with-libxml-dir=/usr/lib64/ --with-png-dir=/usr/lib64/ --with-xpm-dir=/usr/lib64

由于没有指定安装目录,安装文件bin/etc/include/lib/php默认安装到/usr/local的对应目录下, php –ini命令可以看到,php.ini的位置

Configuration File (php.ini) Path: /usr/local/lib
Loaded Configuration File:         /usr/local/lib/php.ini

php -m 可以看到安装的扩展模块

[PHP Modules]
 ctype curl date dom exif fastdfs_client filter ftp gd hash iconv ipquery json libxml mbstring mcrypt memcache memcached mongo mssql mysql openssl pcntl pcre PDO pdo_dblib pdo_mysql pdo_sqlite posix rar redis Reflection session SimpleXML sockets SPL SQLite ssh2 standard tokenizer xhprof xml xmlreader xmlwriter yaf zip zlib zookeeper

安装过程

源码编译

configure参数在原版本基础上,指定了安装目录和php.ini文件路径,并且增加了一些扩展的支持

./configure --prefix=/usr/local/php5.5.25  --with-config-file-path=/usr/local/php5.5.25/etc --enable-fpm  --enable-exif  --enable-pcntl --enable-ftp --with-openssl --enable-zip  --enable-sockets --with-mcrypt --enable-pcntl --enable-mssql  --enable-pdo_dblib  --with-apxs2=/usr/local/apache2/bin/apxs --enable-mbstring --with-mysql=/usr/local/mysql --with-pdo-mysql=/usr/local/mysql --with-libxml-dir --with-iconv --enable-sqlite-utf8 --with-zlib=/usr/local/zlib --with-curl --with-gd --with-freetype-dir=/usr/lib64/ --with-jpeg-dir=/usr/lib64 --with-libxml-dir=/usr/lib64/ --with-png-dir=/usr/lib64/ --with-xpm-dir=/usr/lib64
make 
sudo make install

说明: enable表示某个功能是否开启[yes/no],且这种功能往往是php内置的.with表示,是否需要添加某个功能,往往需要指定依赖的外部库在哪里.所以,enable的选项后面往往无参数,–disable-short-tags,–enable-safe-mode而with后的选项,往往带上参数,如:–with-config-file-path=PATH

安装扩展

老版本的php除了configure参数指定安装的的一些扩展,由于后期业务的需要,手动安装了一些扩展,所以新版本的php也要安装这些扩展。 这些扩展中一部分在php源码文件中的ext文件夹下已经包含,如openssl、pcntl、rar等,这一类扩展实际上是可以在编译的时候加configure参数直接编译的如–with-openssl‘ ’源码文件中没有的,就需要到http://pecl.php.net下载安装。 这两类扩展的安装方式都是一样的

  1. /usr/local/php5.5.25/bin/phpize phpize是用来扩展php扩展模块的,通过phpize可以建立php的外挂模块,运行完该命令后,当前目录下会生成configure文件
  2. ./configure –with-php-config=/usr/local/php5/bin/php-config 在编译扩展时,如果安装有多个 PHP 版本,可以在配置时用 –with-php-config 选项来指定使用哪一个版本编译,该选项指定了相对应的 php-config 脚本的路径 如果该扩展还依赖于外部安装库,就要加 –with指定对应安装目录,如memched的安装,需要先安装libmemcached ./configure –with-php-config=/usr/local/php5.5.25/bin/php-config –with-libmemcached-dir=/usr/local/libmemcached
  3. make & sudo make install 安装后,在/usr/local/php5.5.25/lib/php/extensions/no-debug-non-zts-20121212就会生成.so文件
  4. 修改php.ini 添加扩展 extension=memcached.so

命令行cli相关修改

安装新版本的php,执行php -v 还是显示老版本的信息,因为php可行性程序还是老版本的

 which php
 /usr/local/bin/php  

老版本的php未指定安装目录,php、phpize、php-config文件直接安装到了/usr/local/bin/目录下,先备份,再做软连接

cp /usr/local/bin/php /usr/local/bin/php52
cp /usr/local/bin/phpize /usr/local/bin/phpize52
cp /usr/local/bin/php-config /usr/local/bin/php-config52
ln -s /usr/local/php5.5.25/bin/php   /usr/local/bin/php 
ln -s /usr/local/php5.5.25/bin/phpize   /usr/local/bin/phpize 
ln -s /usr/local/php5.5.25/bin/php-config   /usr/local/bin/php-config 

再次执行php -v,正常

PHP 5.5.25 (cli) (built: Sep 12 2017 09:58:24) 
Copyright (c) 1997-2015 The PHP Group
Zend Engine v2.5.0, Copyright (c) 1998-2015 Zend Technologies

apache相关修改

apache与php有CGI/Module/FastCGI三种结合方式,php -i 可以看出测试环境呢采用了module的方式,

 Server APIApache 2.0 Handler 

意味着php是作为apache的一个模块来启动的,因此在apache启动的时候会加载php模块,httpd.conf文件中有一行

LoadModule php5_module modules/libphp5.so 

libphp5.so是如何产生的呢? php的configure参数中有个–with-apxs2=/usr/local/apache2/bin/apxs,apxs是一个为Apache HTTP服务器编译和安装扩展模块的工具,加了该参数,make install的时候,就会在apache安装目录的modules文件夹下生成libphp5.so,为了不把老版本的libphp5.so文件覆盖,安装之前备份一下

cp libphp5.so libphp5.so.52 

所以在安装新版本的php之后,原来的 libphp5.so已被覆盖,如果要切会老版本,就直接cp libphp5.so.52 libphp5.so,再重启apache就可以了

nginx相关修改

nginx只能通过php-fpm调用php Server APIFPM/FastCGI 而且项目中是使用tcp socket的方式连接php-fpm

      location ~ \.php$ {
         include fastcgi.conf;
          fastcgi_param  PROJECT_INCLUDE_PATH " ";
          fastcgi_pass 127.0.0.1:9000;
      }

只需要停掉之前的php-fpm,再启动新版本的php-fpm就可以了

php中类的自动加载机制

2017年9月1日 PHP No comments

问题

项目中使用了PHPExcel进行excel表格的处理,在new PHPExcel()之后,发现有些类找不到了,报错

PHP message: PHP Fatal error: Uncaught Error: Class 'XXX' not found in 

这里涉及到PHP类的自动加载机制

类的自动加载

php是脚本语言,不同于c++只需要编译一次,php每次执行过程中都需要编译,当我们需要在代码中引入一个外部文件定义的类时,需要使用require或include手工进行加载

<?php
include_once("./myClass.php");
include_once("./myFoo.php");
include_once("./myBar.php");

$obj = new myClass();
$foo = new Foo();
$bar = new Bar();
?>

这样会产生两个问题:
1. 每个文件前,都要把需要的文件逐行引入,非常繁琐;
2. 任何一个被包含的文件,无论是否使用,均会被php引擎编译,如果不使用,却被编译,会造成资源浪费

在php5之后,实现了类的自动机制机制,提供一种“lazy load”的机制,当第一次需要使用相关类时调用,这样就不会加载不必要的类,主要有以下两种方式

__autoload

它会在试图使用尚未被定义的类时自动调用

<?php
// we've writen this code where we need
function __autoload($classname) {
    $filename = "./". $classname .".php";
    include_once($filename);
}

// we've called a class ***
$obj = new myClass();
?>

__autoload有一个问题是,在一个项目中只能使用一次,当你的项目引用了别人的一个项目,你的项目中有一个__autoload,别人的项目也有一个__autoload,这样两个__autoload就冲突,所以这种方法在PHP7.2后被废弃了

spl_autoload_register()

spl_autoload_register() 提供了一种更加灵活的方式来实现类的自动加载,同一个应用中,可以支持任意数量的加载器

<?php   
function loader($class)   
{   
$file = $class . '.php';   
if (is_file($file)) {   
require_once($file);   
}   
}   
spl_autoload_register('loader');   

$a = new A();

spl_autoload_register 会将函数注册到SPL __autoload函数队列中。如果该队列中的函数尚未激活,则激活它们。
如果在你的程序中已经实现了__autoload()函数,它必须显式注册到__autoload()队列中。因为 spl_autoload_register()函数会将Zend Engine中的__autoload()函数取代为spl_autoload()或spl_autoload_call()

问题解决

PHPExcel作为一个第三方的库,它的的自动加载方式如下
class PHPExcel_Autoloader

{
    public static function Register() {
        return spl_autoload_register(array('PHPExcel_Autoloader', 'Load'));
    }   //  function Register()
    public static function Load($pObjectName){
        if ((class_exists($pObjectName)) || (strpos($pObjectName, 'PHPExcel') === False)) {
            return false;
        }
        $pObjectFilePath =  PHPEXCEL_ROOT.str_replace('_',DIRECTORY_SEPARATOR,$pObjectName).'.php';
        if ((file_exists($pObjectFilePath) === false) || (is_readable($pObjectFilePath) === false)) {
            return false;
        }
        require($pObjectFilePath);
    }   //  function Load()
}

然后看了一下项目中的自动加载方式

function __autoload($classname)
{
     $classpath = getClassPath();
     if (isset($classpath[$classname]))
     {
         include($classpath[$classname]);
     }
     else
     {
         @header("Location:/error/notfound");
     }
}

原因就显而易见了,当遇到PHPExcel中未加载的类时,代用spl_autoload_register的作用就是注册__autoload的回调,将原来项目中的_autoload()覆盖掉了,也就是说,当使用了spl_autoload_register之后,再遇到找不到的类的时候,就会去spl_autoload_register注册的加载类里去找了,就不会再去原项目中的去__autoload找啦,修改也很简单,将项目中的加载方式也改成spl_autoload_register的方式

spl_autoload_register("myAutoLoad");
function myAutoLoad($classname)
{
    $classpath = getClassPath();
    if (isset($classpath[$classname]))
    {
        include($classpath[$classname]);
    }
    else
    {
        @header("Location:/error/notfound");
    }  
}

《编写可读代码的艺术》笔记

2017年9月1日 读书笔记 No comments

命名

关键思想:把信息装入命名中

  1. 使用min max来表示(包含)极限
  2. 使用first last来表示包含的范围
  3. 使用begin(包含)和 end(排除)来表示包含/排除范围
  4. 布尔值 变量前 加 is has can should等
  5. 不要轻易用get

代码

关键思想:让相似的代码看上去相似,把相关的代码行分组,形成代码块,使用一致的布局,让读者很快就习惯这种风格

  1. 合理安排换行保持代码一致和紧凑
  2. 在需要时使用列对齐,让代码更容易浏览
  3. 将代码按块组织起来,使阅读者按照分组和层次接口思考
  4. 代码分段落,每段做不同的事, 用空行把大段代码逻辑分段
  5. 有多段相似的代码,考虑写到一个函数里

注释

关键思想:注释的目的是尽量帮助读者了解的和读者一样多

  1. 不要为那些从代码本身就能快速推断的事实写注释
  2. 不要给不好的名字写注释,应该把名字改好
  3. 好的注释,是在写代码的时候记录你的重要想法
  4. 注释保持紧凑
  5. 避免使用不明确的词
  6. 用输入输出例子来说明特别的情况

控制流

关键思想:把条件、循环以及其他对控制流的改变做的越自然越好。运用一种方式使读者不用停下来读你的代码

  1. 比较时,把改变的值写在左边,把更稳定的值写在右边更好一点
  2. 尽量避免使用 do/while goto语句的使用
  3. 减少嵌套,提早返回

组织代码

关键思想:积极地发现并抽取出不相关的子问题

  1. 把一般代码和项目专有代码分开
  2. 把代码组织得一次只做一件事情
  3. 减少重复代码,删除无用代码

php多进程

2017年8月31日 PHP No comments

前言

PHP多进程的实现,要借助于pcntl和posix两个扩展,利用pcntl_fork创建子进程,进行多进程处理。主要分两种角色,master进程和worker进程,master进程的主要作用是创建、管理并负责回收worker进程,并不参与具体的任务处理;而worker进程只负责任务处理。

实例代码

<?php
declare(ticks = 1);

$worker_arr = array();
$worker_num = N;

init_worker($worker_num, $worker_arr);
do_master();
destruct();

function init_worker($worker_num, &$worker_arr) {
    for($i = 0; $i < $worker_num; $i++){
        $pid = pcntl_fork();
        if($pid > 0) {
            $worker_arr[$i] = $pid;
            continue;
        } else if($pid == 0) {
            do_worker($i);
            exit;
        } else {
            echo "create worker fail";
        }
    }
}

function do_worker($num) {
    //do job
}

function do_master() {
    pcntl_signal(SIGTERM, 'kill_all');
}

function destruct_worker() {
    global $worker_arr;
    foreach ($worker_arr as $i=>$child_pid) {
        $pid = pcntl_waitpid($child_pid, $status);
        if (pcntl_wifexited($status)) {
            echo "work $i exit, pid: {$pid}";
        } else {
            echo "work $i exit exception, pid: {$pid}, status: {$status}";
        }
    }
}

function kill_all() {
    global $worker_arr;
    foreach ($worker_arr as $i=>$pid) {
        $ret = posix_kill($pid, SIGTERM);
        if ($ret) { 
            echo "kill worker: $pid";
        } else {
            echo "kill_fail worker: $pid";
        }
    }
    exit();
} 

创建子进程

init_worker的作用是创建子进程,利用pcntl_fork函数,该函数会在当前进程当前位置产生分支(子进程),父进程和子进程 都从fork的位置开始向下继续执行,不同的是父进程执行过程中,得到的fork返回值为子进程号,而子进程得到的是0。 如果判断是子进程,直接执行任务,执行后直接exit退出;如果判断是父进程,continue继续执行,直到创建足够的子进程,将所有子进程pid保存到全局变量worker_arr中。

回收子进程

destruct_worker的作用是回收子进程,如果子进程先于父进程退出, 同时父进程又没有调用wait/waitpid,则该子进程将成为僵尸进程。通过ps命令,我们可以看到该进程的状态为Z(表示僵死),造成系统资源浪费。所以为了避免子进程变成僵尸进程,父进程需要对子进程进程回收,这里用到pcntl_waitpid函数,该函数会等待或返回fork的子进程状态,如果指定的子进程在此函数调用时已经退出,此函数将立刻返回,并释放子进程的所有系统资源,同时pcntl_wifexited会检查状态代码是否正常退出。

管理子进程

如果代码运行过程中,出现问题,需要终止所有进程的处理怎么办? 命令行下可以通过Ctrl+C来关闭这个程序,或者使用kill命令,这都是向进程发送信号,Ctrl+C 是通过键盘向程序发送了一个中断的信号:SIGINT。kill [PID]命令,未加任何其他参数的话,程序会接收到一个SIGTERM信号,我们向master进程发送了终止信号,需要master进程对worker进程进行管理,逐个将子进程终止。pcntl_signal(SIGTERM, ‘kill_all’)函数就是安装一个信号处理器,当收到SIGTERM信号时,会调用回调函数kill_all, kill_all函数中又调用posix_kill向每一个子进程发送SIGTERM信号,达到终止所有进程的目的。代码开头的declare(ticks = 1)表示每执行一条低级指令,就检查一次信号,如果检测到注册的信号,就调用其信号处理器。

数据库锁机制–以Mysql数据库Innodb引擎为例

2017年8月21日 WebServer No comments

加锁作用


数据库通过锁机制来实现并发控制,确保在多个事务同时存取数据库中同一数据时不破坏事务的隔离性以及数据的一致性

事务

一组独立的执行单元,不可再分,要么全部成功执行,事务的正确执行要满足以下几个要 * 原子性(Atomicity) 一个事务像一个原子一样不可再分,要么全部提交成功,要么全部失败回滚,不可能只执行部分操作。

  • 一致性(Consistency) 事务执行前和执行后,要保证数据库从一个一致性状态转换都另一个一致性状态,保证数据的一致性约束。

  • 隔离性(Isolation) 一个事务对数据的修改,对其他事务来讲,通常都是不可见的,可见程度由数据库的隔离级别决定

  • 持久性(Durability) 事务执行完成,其所作的修改就会永久保存到数据库中。

锁分类

根据锁作用分为:

  • 共享锁S 又称读锁,允许多个事务共享,共享锁就是多个事务对于同一数据可以共享一把锁,都能访问到数据,但是只能读不能修改

  • 排他锁X(写锁): 又称写锁,不能与其他锁并存,如一个事务获取了一个数据行的排他锁,其他事务就不能再获取该行的其他锁,包括共享锁和排他锁,但是获取排他锁的事务是可以对数据进行读取和修改

根据锁的操作分为:

  • 悲观锁 很悲观,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会block直到它拿到锁
  • 乐观锁 很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,乐观锁适用于多读的应用类型,这样可以提高吞吐量

根据锁粒度分为:

  • 表锁:锁定整张表,当要进行写操作时(增删改)前必须获得写锁,释放写锁之前,其他的对该表的读写操作都会阻塞;读锁之间不不互相阻塞;锁开销较少

  • 行锁 锁定中的一行或多行, 最大程度地支持并发操作;锁开销较大;InnoDB采用这种策略

举例

select * from table where ?;                                         快照读  不加锁 
select * from table where ? lock in share mode;                      当前读  共享锁
select * from table where ? for update;                              当前读  排他锁
insert into table values (…);                                        当前读  排他锁              
update table set ? where ?;                                          当前读  排他锁
delete from table where ?;                                           当前读  排他锁

快照读读取的是记录的可见版本 (有可能是历史版本),不用加锁 当前读读取的是记录的最新版本,并且,当前读返回的记录,都会加上锁,保证其他事务不会再并发修改这条记录 for update查是显式的加排他锁,可以理解增删改在操作之前隐式地执行了for update查询操作,要保证对数据库所作的修改必须是最新版本,同时保证在对该数据修改的同时,其他事务不能对其修改。

加锁粒度


加锁是为了进行并发控制,在加锁的同时,要尽可能减少加锁数据范围,提高并发性能,在这一节之前,需要了解数据库索引和事务隔离级别的知识。

事务的隔离级别

  • READ-UNCOMMITTED 未提交读
    (1)所有事务都可以看到其他未提交事务的执行结果
    (2)本隔离级别很少用于实际应用,因为它的性能也不比其他级别好多少
    (3)该级别引发的问题是——脏读(Dirty Read):读取到了未提交的数据
  • READ-COMMITED 提交读
    (1)这是大多数数据库系统的默认隔离级别(但不是MySQL默认的)
    (2)它满足了隔离的简单定义:一个事务只能看见已经提交事务所做的改变
    (3)这种隔离级别出现的问题是——不可重复读(Nonrepeatable Read):不可重复读意味着我们在同一个事务中执行完全相同的select语句时可能看到不一样的结果。 导致这种情况的原因可能是一个交叉的事务有新的commit,导致了数据的改变
  • REPEATABLE-READ 可重复读
    (1)这是MySQL的默认事务隔离级别
    (2)它确保同一事务的多个实例在并发读取数据时,会看到同样的数据行
    (3)此级别可能出现的问题——幻读(Phantom Read):当用户读取某一范围的数据行时,另一个事务又在该范围内插入了新行,当用户再读取该范围的数据行时,会发现有新的“幻影” 行
    (4)InnoDB和Falcon存储引擎通过多版本并发控制(MVCC,Multiversion Concurrency Control)机制解决了该问题
  • SERIALIZABLE 可串行化
    (1)这是最高的隔离级别
    (2)它通过强制事务排序,使之不可能相互冲突,从而解决幻读问题。简言之,它是在每个读的数据行上加上共享锁。
    (3)在这个级别,可能导致大量的超时现象和锁竞争

可重复读隔离级别下的加锁范围

  • 主键索引
    (1)在主键索引上加锁
  • 唯一索引
    (1)在唯一索引上加锁
    (2)在唯一索引对应的主键索引上加锁
  • 非唯一索引
    (1)在非唯一索引上加锁
    (2)在非唯一索引对应的主键索引上加锁
    (3)在非唯一索引之间及其两侧加间隙锁
  • 无索引
    (1)在所有的聚簇索引上加锁
    (2)在所有的聚簇索引的间隙加上间隙锁

死锁

两个或两个以上的进程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去 死锁发生的关键在于:两个(或以上)的事务加锁的顺序不一致

优化建议

  1. 合理设计索引,让InnoDB在索引键上面加锁的时候尽可能准确,尽可能地缩小锁定范围,避免造成不必要的锁定
  2. 尽量控制事务的大小,减少锁定的资源量和锁定时间
  3. 尽量约定以相同的顺序锁定表,可以大大降低死锁机会

升级https

2017年8月16日 WebServer No comments

HTTPS(Secure Hypertext Transfer Protocol)安全超文本传输协议 它是一个安全通信通道,它基于HTTP开发,用于在客户计算机和服务器之间交换信息。它使用安全套接字层(SSL)进行信息交换,简单来说它是HTTP的安全版,是使用 TLS/SSL 加密的 HTTP 协议。

https作用

  • 使用非对称加密和对称加密,保证数据的保密性
  • 使用基于散列函数算法验证信息的完整性
  • 使用数字证书进行身份验证

https原理

HTTPS在传输数据之前需要客户端(浏览器)与服务端(网站)之前进行一次握手,在握手过程中将确立双方加密传输数据的密码信息。握手过程的简单描述如下:
1.浏览器将自己支持的一套加密规则发送给网站。
2.网站从中选出一组加密算法与HASH算法,并将自己的身份信息以证书的形式发回给浏览器。证书里面包含了网站地址,加密公钥,以及证书的颁发机构等信息。
3.获得网站证书之后浏览器要做以下工作:
a) 验证证书的合法性(颁发证书的机构是否合法,证书中包含的网站地址是否与正在访问的地址一致等),如果证书受信任,则浏览器栏里面会显示一个小锁头,否则会给出证书不受信的提示。
b) 如果证书受信任,或者是用户接受了不受信的证书,浏览器会生成一串随机数的密码,并用证书中提供的公钥加密。
c) 使用约定好的HASH计算握手消息,并使用生成的随机数对消息进行加密,最后将之前生成的所有信息发送给网站。
4.网站接收浏览器发来的数据之后要做以下的操作:
a) 使用自己的私钥将信息解密取出密码,使用密码解密浏览器发来的握手消息,并验证HASH是否与浏览器发来的一致。
b) 使用密码加密一段握手消息,发送给浏览器。
5.浏览器解密并计算握手消息的HASH,如果与服务端发来的HASH一致,此时握手过程结束,之后所有的通信数据将由之前浏览器生成的随机密码并利用对称加密算法进行加密。

安装Let’s Encrypt免费SSL证书

获取证书

1
2
3
git clone https://github.com/letsencrypt/letsencrypt
cd letsencrypt
./letsencrypt-auto certonly --standalone --email hungerhunger@sina.com -d errorlogs.cn -d www.errorlogs.cn

会出现以下提示信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
IMPORTANT NOTES:
 - Congratulations! Your certificate and chain have been saved at
   /etc/letsencrypt/live/errorlogs.cn/fullchain.pem. Your cert will
   expire on 2017-06-30. To obtain a new or tweaked version of this
   certificate in the future, simply run letsencrypt-auto again. To
   non-interactively renew *all* of your certificates, run
   "letsencrypt-auto renew"
 - Your account credentials have been saved in your Certbot
   configuration directory at /etc/letsencrypt. You should make a
   secure backup of this folder now. This configuration directory will
   also contain certificates and private keys obtained by Certbot so
   making regular backups of this folder is ideal.
 - If you like Certbot, please consider supporting our work by:
 
   Donating to ISRG / Let's Encrypt:   https://letsencrypt.org/donate
   Donating to EFF:                    https://eff.org/donate-le

发现在/etc/letsencrypt/live/errorlogs.cn/文件夹下多了4个文件 cert.pem – Apache服务器端证书 chain.pem – Apache根证书和中继证书 fullchain.pem – Nginx所需要ssl_certificate文件 privkey.pem – 安全证书KEY文件

web服务器配置

  • nginx
1
2
3
4
5
6
7
8
9
10
listen 443 ssl;
server_name errorlogs.cn;
index index.html index.htm index.php default.html default.htm default.php;
root /home/wwwroot/errorlogs;
ssl_certificate /etc/letsencrypt/live/errorlogs.cn/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/errorlogs.cn/privkey.pem;
ssl_ciphers "EECDH+CHACHA20:EECDH+CHACHA20-draft:EECDH+AES128:RSA+AES128:EECDH+AES256:RSA+AES256:EECDH+3DES:RSA+3DES:!MD5";
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_prefer_server_ciphers on;
ssl_session_cache shared:SSL:10m;
  • apache
1
2
3
SSLEngine on
 SSLCertificateFile /etc/letsencrypt/live/hungerhunger.cn/fullchain.pem
 SSLCertificateKeyFile /etc/letsencrypt/live/hungerhunger.cn/privkey.pem

http重定向https

  • nginx
1
2
3
4
5
server {
  listen 80;
  server_name domain.com www.domain.com;
  return 301 https://domain.com$request_uri;
}
  • apache .htaccess文件
1
2
3
4
5
&lt;IfModule mod_rewrite.c>
RewriteEngine On
RewriteCond %{HTTPS} !=on
RewriteRule ^(.*) https://%{SERVER_NAME}/$1 [R,L]
&lt;/IfModule>

浏览器缓存

2017年8月14日 WebServer No comments

浏览器缓存是为了节约网络的资源加速浏览,浏览器在用户磁盘上对最近请求过的文档进行存储,当访问者再次请求这个页面时,浏览器就可以从本地磁盘显示文档,这样就可以加速页面的阅览。

浏览器缓存机制

浏览器缓存机制,就是浏览器和服务器之间,通过http协议的请求头和响应头进行协商,使得浏览器尽可能少的从服务端获取资源,而且还要保证浏览器最终使用的资源是最新的。

服务器告诉浏览器资源过期时间

浏览器在第一次访问服务端资源时,在服务器的http响应头中会带上资源的有效期,在有效期内,浏览器对该资源的请求都会直接使用浏览器缓存而不用向服务器请求。

  • Expires(HTTP1.0),Expires 头部字段提供一个有效期的绝对时间。
  • Cache-Control(HTTP1.1),Cache-Control可以设置多个值,最常用的是max-age,这是一个相对时间,单位秒。 expires

当Expires和Cache-Control同时出现时,Cahce-Control优先级更高。

浏览器告诉服务器本地资源的新鲜度

当超过资源的有效期时,浏览器会放弃使用本地换粗,再次向服务器发起请求,同时会再请求头中带着本地缓存文件的新鲜度信息,服务端会根据该信息,与服务端资源文件进行比较,如果浏览器的缓存文件仍是最新的就返回304,否则就返回200,将最新的资源文件发送给浏览器。那么新鲜度信息从哪里来?当然也是服务端返回的,在第一次请求资源文件的响应头中除了Exprires和Cache-Control信息,还有Last-Modified和Etag信息。

  • Last-Modified/If-Modified-Since Last-Modified表示这个响应资源的最后修改时间, 当资源过期时,发现资源具有Last-Modified声明,则再次向web服务器请求时带上头 If-Modified-Since。

  • Etag/If-None-Match Etag是当前资源在服务器的唯一标识(生成规则由服务器觉得)。Apache中,ETag的值,默认是对文件的索引节(INode),大小(Size)和最后修改时间(MTime)进行Hash后得到的,当资源过期时,发现资源具有Last-Modified声明,就会将请求头If-None-Match设置为该值

浏览器缓存文件在哪

以chrome浏览器为例,在地址栏输入 chrome://cache, 回车 chrome-cache

点击任意文件名

cache-file

服务端配置

  • Nginx
1
2
3
4
5
6
location ~ .*.(gif|jpg|jpeg|png|bmp|swf)$ {
    expires 30d; 
}
location ~.*.(js|css)?$ { 
    expires 1h; 
}
  • Apache

http.conf中打开LoadModule expires_module modules/mod_expires.so
.htaccess中加入

1
2
3
4
5
6
7
8
9
10
11
12
<IfModule mod_expires.c>  
    # 打开缓存   
    ExpiresActive On   
    # 默认对所有资源缓存600秒    
    ExpiresDefault A600    
    # png格式的资源缓存5秒  
    ExpiresByType image/png A5
    # jpg格式的资源缓存50秒    
    ExpiresByType image/jpg A50   
    # 好含这些后缀的资源,都缓存100秒   
    <FilesMatch "\.(jpg|jpeg|png|gif|swf)$"> ExpiresDefault A100 </FilesMatch>  
</IfModule>

注意:经测试,如果服务器端不做进行配置,即响应头中不包含资源的有效期,浏览器任然会缓存该文件,再次请求资源时,还会优先使用缓存资源

服务端在资源有效期内修改了文件

一般情况下,服务端会设置资源的过期时间为一个很长的日期,如果在有效期内修改了文件,而浏览器端仍然已第一次接收时的为准,不会与浏览器进行协商,这时资源文件就不是最新的了,怎么解决这个问题呢?
大部分的做法时,对于需要缓冲的资源文件的uri上加一个版本号,当资源修改上线时,同步修改版本号,那么对于浏览器来讲,这是一个新的文件,自然会直接向服务器请求,获取最最信资源。在测试环境,为了调试方便,通常已当前时间戳作为版本号。
version