thinkcmf5(tp5.0) + workman实现简单客服聊天功能

配置过程中遇到的问题及说明:

1、首先需要对长连接相关概念有一定了解。

2、通过composer默认安装的workman版本不对,报错,使用命令composer require topthink/think-worker=1.0.1

3、通过1、发现composer包列表网址,https://packagist.org,可以通过该网址查找composer包,包括说明。

4、报错:stream_socket_server() has been disabled for security reasons … ,修改php.ini,disable_functions项,去掉stream_socket_server。

 

安装步骤说明:

1、composer安装命令,composer包安装倒了vendor/workerman/目录中。

1
composer require topthink/think-worker=1.0.1

2、在项目根目录中新建server文件,内容如下:

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
#!/usr/bin/env php
<?php
// +----------------------------------------------------------------------
// | Date:2019.1.15
// +----------------------------------------------------------------------
// | Author: asher.w
// +----------------------------------------------------------------------
// 调试模式开关
define("APP_DEBUG", true);
// 定义CMF根目录,可更改此目录
define('CMF_ROOT', __DIR__ . '/');
// 定义插件目录
define('PLUGINS_PATH', __DIR__ . '/public/plugins/');
// 定义应用目录
define('APP_PATH', __DIR__ . '/app/');
// 定义CMF核心包目录
define('CMF_PATH', CMF_ROOT . 'simplewind/cmf/');
// 定义扩展目录
define('EXTEND_PATH', CMF_ROOT . 'simplewind/extend/');
define('VENDOR_PATH', CMF_ROOT . 'simplewind/vendor/');
// 定义应用的运行时目录
define('RUNTIME_PATH', CMF_ROOT . 'data/runtime/');

define('BIND_MODULE','push/Chat');
// 加载框架引导文件
require CMF_ROOT.'simplewind/thinkphp/start.php';

3、服务器端控制器代码:

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
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
<!--?php /** * Created by PhpStorm. * User: asher * Date: 2019/1/13 * Time: 15:19 */ namespace app\push\controller; use think\worker\Server; class ChatController extends Server { protected $socket = 'websocket://0.0.0.0:2346'; private $_name = []; private $_uidConnections = []; /** * 收到信息 * @param $connection * @param $data */ public function onMessage($connection, $data) { if (!$this-&gt;is_json($data)){&lt;br ?--> $connection-&gt;send('非法请求');
return;
}

$data = json_decode($data,true);

if (!isset($data['type'])){
$connection-&gt;send('非法请求');
return;
}

switch ($data['type']){
case 'private_message':

// 假设消息格式为 uid:message 时是对 uid 发送 message
$p_msg = explode(':',$data['data']['message']);

$_msg = ['name'=&gt;$this-&gt;getName($connection-&gt;id),'message'=&gt;$p_msg[1]];

$this-&gt;sendMessageByUid($p_msg[0], $this-&gt;createData('private_message',$_msg));

break;
case 'message':
foreach ( $this-&gt;worker-&gt;connections as $_connection ){
$_connection-&gt;send($this-&gt;createData('message',['name'=&gt;$this-&gt;getName($connection-&gt;id),'message'=&gt;$data['data']['message']]));
}

break;
case 'setName':
$this-&gt;setName($connection-&gt;id, $data['data']['name']);

//测试 设置id
$this-&gt;_uidConnections[$data['data']['name']] = $connection;

$connection-&gt;send($this-&gt;createData('msg',['msg'=&gt;'设置昵称为:'.$data['data']['name']]));
break;
case 'push':
if ($data['id']){
if (isset($this-&gt;worker-&gt;connections[$data['id']])){
$this-&gt;worker-&gt;connections[$data['id']]-&gt;send($this-&gt;createData('message',['name'=&gt;'后台','message'=&gt;$data['data']]));
}else{
$connection-&gt;send($this-&gt;createData('result',['msg'=&gt;'连接不存在'],1));
}

}else{
foreach ( $this-&gt;worker-&gt;connections as $_connection ){
if ($connection-&gt;id != $_connection-&gt;id)
$_connection-&gt;send($this-&gt;createData('message',['name'=&gt;'后台','message'=&gt;$data['data']]));
}
}

$connection-&gt;send($this-&gt;createData('result',['msg'=&gt;'推送成功']));
break;
}

}

// 针对uid推送数据
function sendMessageByUid($uid, $message)
{
if(isset($this-&gt;_uidConnections[$uid]))
{
$connection = $this-&gt;_uidConnections[$uid];
$connection-&gt;send($message);
}
}

/**
* 当连接建立时触发的回调函数
* @param $connection
*/

public function onConnect($connection)
{
//设置默认昵称
$this-&gt;setName($connection-&gt;id,'匿名'.rand(1000,9999));
}

/**
* 当连接断开时触发的回调函数
* @param $connection
*/

public function onClose($connection)
{
unset($this-&gt;_name[$connection-&gt;id]);
}

/**
* 当客户端的连接上发生错误时触发
* @param $connection
* @param $code
* @param $msg
*/

public function onError($connection, $code, $msg)
{
echo "error $code $msg\n";
}

/**
* 每个进程启动
* @param $worker
*/

public function onWorkerStart($worker)
{

}

private function createData($type,$data,$code=0){
$_data = [
'code' =&gt; $code,
'type' =&gt; $type,
'data' =&gt; $data
];
return json_encode($_data);
}

private function setName($id,$name){
$this-&gt;_name[$id] = $name;
}

private function getName($id){
return $this-&gt;_name[$id];
}

private function is_json($str)
{
json_decode($str);
return(json_last_error() == JSON_ERROR_NONE) &amp;&amp; !is_numeric($str) ;
}

}

4、linux进入项目根目录,启用服务监听端口。

1
php server start
1
2
3
4
5
6
7
8
Workerman[server] start in DEBUG mode
------------------------------------------- WORKERMAN --------------------------------------------
Workerman version:3.5.17 PHP version:5.6.36
-------------------------------------------- WORKERS ---------------------------------------------
proto user worker listen processes status
tcp root none websocket://0.0.0.0:2346 1 [OK]
--------------------------------------------------------------------------------------------------
Press Ctrl+C to stop. Start success.

5、前端页面

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
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
<style>
        .content{<br />            width: 700px;<br />            margin: auto;<br />        }<br />        .box{<br />            padding-bottom: 3px;<br />            padding-top: 3px;<br />        }<br />        .box input{<br />            width: 70%;<br />        }<br />        .box button{<br />            width: 25%;<br />        }<br />    </style>

&nbsp;
<h3>CHAT TEST</h3>

<hr />

<div class="content">
<div class="box"><input name="name" type="text" />
<button id="setName">设置昵称</button></div>
<div class="box"><input name="message" type="text" />
<button id="send">发送</button></div>
<div id="content" class="box"></div>
</div>
<script>
    var ws;
    var connected = false;

    $(function () {

        ws = new WebSocket("ws://local.hillmatrix.com:2346");
        ws.onopen = function() {
            console.log('WebSocket 连接成功');
            connected = true;
        };
        ws.onmessage = function(e) {
            // e.data
            console.log(e.data);
            try {
                var data = JSON.parse(e.data);
                //非法数据,不处理
                if (!data.hasOwnProperty("code")) return;
                if (data.code != 0){
                    if (data.hasOwnProperty("msg")){
                        alert(data.msg);
                    }
                    return
                }

                if (!data.hasOwnProperty("data") || !data.hasOwnProperty("type") || !data.hasOwnProperty("data")) return;

                switch (data.type){
                    case 'private_message':
                        addChatItem(data.data.name + '(悄悄的...)',data.data.message)
                        break;
                    case 'message':
                        addChatItem(data.data.name,data.data.message)
                        break;
                    case 'msg':
                        alert(data.data.msg)
                        break;
                }

            }catch (ex) {

            }
        };

    });

    $("#setName").click(function () {
        var name = $('input[name=name]').val().trim();
        if (name==''){
            alert('昵称不能为空');
            return;
        }
        sendData('setName',{name:name});
    });

    $("#send").click(function () {
        var message = $('input[name=message]').val().trim();
        if (message==''){
            alert('内容不能为空');
            return;
        }

        if(message.indexOf(":") >= 0){
            sendData('private_message',{message:message});
        }else{
            sendData('message',{message:message});
        }

    })

    function addChatItem( name,message ) {
        var temp = '</p>
<p>
</p>
<div class="box"><p>
</p>
<p>__NAME__ 说:</p>
<p>
</p>
<p>__CONTENT__</p>
<p>
</div>
<p>
</p>
<p>';
        temp = temp.replace( "__NAME__",name );
        temp = temp.replace( "__CONTENT__",message );
        $("#content").append(temp);
    }

    function sendData(type,data) {
        if(ws.readyState != 1){
            alert(connected ? "连接已中断" : "连接不成功");
            return;
        }
        ws.send(JSON.stringify({type:type,data:data}));
    }
</script>

6、测试效果

参考代码:https://gitee.com/goto8848/build_a_simple_chat_room_with_workerman

官方文档:http://doc.workerman.net/faq/send-data-to-client.html

thinkphp5.1 + workerman 留着以后参考:https://blog.csdn.net/qq_27238185/article/details/81477303

PDF文档数字签名

fpdf官网:http://www.fpdf.org/?lang=zh

tcpdf官网:https://tcpdf.org/

1、对证书签名与验签逻辑优化,在linux系统中生成根证书CA,用CA为子证书签名,最后使用子证书为pdf做签名,防止pdf伪造自签证书。

1
2
3
4
5
6
7
8
#1、生成根证书 #a).生成根证书私钥(key文件)
openssl genrsa -aes256 -out ca.key 2048

#b).生成根证书签发申请文件(csr文件)
openssl req -new -key ca.key -out ca.csr -config /usr/lib/ssl/openssl.cnf

#c).自签发根证书(crt文件)
openssl x509 -req -days 3650 -sha1 -extensions v3_ca -signkey ca.key -in ca.csr -out ca.crt
1
2
3
4
5
6
7
8
9
10
11
#2、用根证书签发server端证书
#a).生成根证书私钥(key文件)
openssl genrsa -aes256 -out tcpdf.key 2048

#b).生成根证书签发申请文件(csr文件)
openssl req -new -key tcpdf.key -out tcpdf.csr -config /usr/lib/ssl/openssl.cnf

#c).使用根证书签发服务端证书
openssl ca -in tcpdf.csr -out tcpdf.crt -days 730 -cert ca.crt -keyfile ca.key -config /usr/lib/ssl/openssl.cnf

#The organizationName field needed to be the same in the CA certificate (Timeswealth Global Root CA)

若发生错误: I am unable to access the ./demoCA/newcerts directory ./demoCA/newcerts: No such file or directory

做如下处理

1
2
3
4
5
mkdir demoCA
mkdir demoCA/newcerts
mkdir demoCA/private
touch demoCA/index.txt
echo "01" &gt;&gt; demoCA/serial

备注:一下是tcpdf官方提供的生成自签名证书方法,测试用,如果用于实际会有逻辑漏洞,没有根证书约束。

1
2
3
4
5
6
7
8
9
10
11
/* NOTES:

- To create self-signed signature: openssl req -x509 -nodes -days 365000 -newkey rsa:1024 -keyout tcpdf.crt -out tcpdf.crt

- To export crt to p12: openssl pkcs12 -export -in tcpdf.crt -out tcpdf.p12

- To export p12 to pfx: openssl pkcs12 -export -inkey tcpdf.crt -in tcpdf.crt -out tcpdf.pfx

- To convert pfx certificate to pem: openssl pkcs12 -in tcpdf.pfx -out tcpdf.crt -nodes

*/

自己封装的tcpdf操作类

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
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
<pre>&lt;?php
require_once __DIR__."/tcpdf/tcpdf.php";

class TcpdfProxy
{
    //tcpdf对象
    private $pdf = null;
    //证书路径及密码
    private $_certificate = array(
        'signing_cert'          =&gt;  '',
        'private_key'           =&gt;  '',
        'private_key_password'  =&gt;  'tcpdfpass',
    );

    private $_signConf = array(
        'image'             =&gt;  '',
        'image_position'    =&gt;  array(
            'x' =&gt;  80,
            'y' =&gt;  25,
            'w' =&gt;  50,
            'h' =&gt;  50,
        ),
        'sign_info'         =&gt;  array(
            'Name'      =&gt; 'tcpdf',
            'Location'  =&gt; 'location',
            'Reason'    =&gt; 'Digital signature of PDF file',
            //'ContactInfo' =&gt; SITE_DOMAIN,
        ),

    );

    /***********  自签名证书配置  ***************/
    private $prikeyPass     =   '123456789';
    private $numberOfDays   =   '730';
    private $cerPath        =   "";
    private $cerConfig      =   array(
                                    "digest_alg"        =&gt; "sha256",
                                    "private_key_bits"  =&gt; 1024,                    //字节数    512 1024  2048   4096 等
                                    "private_key_type"  =&gt; OPENSSL_KEYTYPE_RSA,     //加密类型
                                    //"config"          =&gt; "/etc/ssl/openssl.cnf"
                                );
    private $dn             = array(
                                    "countryName"               =&gt; "CN",                            //所在国家
                                    "stateOrProvinceName"       =&gt; "LiaoNing",                      //所在省份
                                    "localityName"              =&gt; "FuShun",                        //所在城市
                                    "organizationName"          =&gt; "Richter59 Global Root CA",    //注册人姓名
                                    "organizationalUnitName"    =&gt; "www.richter59.cn",            //组织名称
                                    "commonName"                =&gt; "www.richter59.cn",            //公共名称
                                    "emailAddress"              =&gt; "postmaster@richter59.cn"      //邮箱
                                );
    /************  自签名证书配置END  **************/


    public function __construct()
    {
        $this-&gt;pdf = new TCPDF(PDF_PAGE_ORIENTATION, PDF_UNIT, PDF_PAGE_FORMAT, true, 'UTF-8', false);

        $this-&gt;setConfig();

        $this-&gt;cerPath = $_SERVER["DOCUMENT_ROOT"]."/public/";

        $this-&gt;_certificate['signing_cert'] = 'file://'.$this-&gt;cerPath.'tcpdf.crt';
        $this-&gt;_certificate['private_key']  = 'file://'.$this-&gt;cerPath.'tcpdf.crt';
    }

    private function setConfig()
    {
        //页面头部横线取消
        $this-&gt;pdf-&gt;setPrintHeader(false);
        //页面底部更显取消
        //$pdf-&gt;setPrintFooter(false);
        //自动分页
        $this-&gt;pdf-&gt;SetAutoPageBreak(TRUE, PDF_MARGIN_BOTTOM);
        //设置页面margin
        $this-&gt;pdf-&gt;SetMargins(PDF_MARGIN_LEFT, 15, PDF_MARGIN_RIGHT);
        //设置页码
        $this-&gt;pdf-&gt;setFooterFont(Array(PDF_FONT_NAME_DATA, '', PDF_FONT_SIZE_DATA));
        $this-&gt;pdf-&gt;SetFooterMargin(PDF_MARGIN_FOOTER);
        //设置字体,注意在循环里面一定要把new都一起放在循环里面,不然会报错,没有设置字体,因为这个需要上下文来读取配置
        $this-&gt;pdf-&gt;SetFont('stsongstdlight', '', 12);
    }

    public function setSignConf(array $conf){
        $this-&gt;_signConf['image'] = $conf['image'] ?: '';
        isset($conf['image_position']['x']) ? $this-&gt;_signConf['image_position']['x'] = $conf['image_position']['x'] : '';
        isset($conf['image_position']['y']) ? $this-&gt;_signConf['image_position']['y'] = $conf['image_position']['y'] : '';
        isset($conf['image_position']['w']) ? $this-&gt;_signConf['image_position']['w'] = $conf['image_position']['w'] : '';
        isset($conf['image_position']['h']) ? $this-&gt;_signConf['image_position']['h'] = $conf['image_position']['h'] : '';

        if(isset($conf['Name']) &amp;&amp; !empty($conf['Name'])){
            $this-&gt;_signConf['sign_info']['Name'] = $conf['sign_info']['Name'];
        }
        if(isset($conf['Location']) &amp;&amp; !empty($conf['Location'])){
            $this-&gt;_signConf['sign_info']['Location'] = $conf['sign_info']['Location'];
        }
        if(isset($conf['Reason']) &amp;&amp; !empty($conf['Reason'])){
            $this-&gt;_signConf['sign_info']['Reason'] = $conf['sign_info']['Reason'];
        }
        if(isset($conf['ContactInfo']) &amp;&amp; !empty($conf['ContactInfo'])){
            $this-&gt;_signConf['ContactInfo']['Name'] = $conf['sign_info']['ContactInfo'];
        }
    }

    private function signatureWithPdf($img,array $signInfo){
        // set certificate file
        if($this-&gt;_certificate){
            $certificate = $this-&gt;_certificate;
        }else{
            $certificate = $this-&gt;getCrtFile();
        }

        if($certificate){
            // set additional information
            if(!empty($signInfo)){
                isset($signInfo['Name']) &amp;&amp; !empty($signInfo['Name']) ? $this-&gt;sign_info['Name']=$signInfo['Name'] : "";
                isset($signInfo['Location']) &amp;&amp; !empty($signInfo['Location']) ? $this-&gt;sign_info['Location']=$signInfo['Location'] : "";
                isset($signInfo['Reason']) &amp;&amp; !empty($signInfo['Reason']) ? $this-&gt;sign_info['Reason']=$signInfo['Reason'] : "";
                isset($signInfo['ContactInfo']) &amp;&amp; !empty($signInfo['ContactInfo']) ? $this-&gt;sign_info['ContactInfo']=$signInfo['ContactInfo'] : "";
            }

            // set document signature
            $this-&gt;pdf-&gt;setSignature($certificate['signing_cert'], $certificate['private_key'], $certificate['private_key_password'], '', 1, $this-&gt;sign_info);

            // reset pointer to the last page
            //$pdf-&gt;lastPage();
            $this-&gt;pdf-&gt;setPage(1);

            // *** set signature appearance ***
            $x = $this-&gt;_signConf['image_position']['x'];
            $y = $this-&gt;_signConf['image_position']['y'];
            $w = $this-&gt;_signConf['image_position']['w'];
            $h = $this-&gt;_signConf['image_position']['h'];

            // create content for signature (image and/or text)
            $this-&gt;pdf-&gt;Image($img, $x, $y, $w, $h);

            // define active area for signature appearance
            $this-&gt;pdf-&gt;setSignatureAppearance($x, $y, $w, $h);

            // *** set an empty signature appearance ***
            //$this-&gt;pdf-&gt;addEmptySignatureAppearance(180, 80, 15, 15);
        }

    }

    public function outputPdf($html,$fileName)
    {

        $this-&gt;pdf-&gt;AddPage();

        // output the HTML content
        $this-&gt;pdf-&gt;writeHTML($html, true, false, true, false, '');

        if($this-&gt;_signConf['image'] != '' &amp;&amp; file_exists($this-&gt;_signConf['image'])){
            $this-&gt;signatureWithPdf($this-&gt;_signConf['image'],$this-&gt;_signConf['sign_info']);
        }

        //Close and output PDF document
        $this-&gt;pdf-&gt;Output($fileName, 'I');
    }

    //本地生成.cer自签名证书文件
    public function getCrtFile()
    {
        $prikeyPass     = $this-&gt;prikeyPass;    //私钥密码
        $numberOfDays   = $this-&gt;numberOfDays;  //有效时长
        $cerPath        = $this-&gt;cerPath.md5($prikeyPass.$numberOfDays.'cer').".cer"; //生成证书路径
        //$pfxpath = APP_ROOT_PATH."public/".md5($prikeyPass.$numberOfDays.'pfx').".pfx"; //密钥文件路径

        if(!file_exists($cerPath)){
            $config = $this-&gt;cerConfig;

            $dn = $this-&gt;dn;

            //生成证书文件
            $priKey = openssl_pkey_new($config);
            $csr = openssl_csr_new($dn, $priKey,$config);
            $x509 = openssl_csr_sign($csr, null, $priKey, $numberOfDays);

            openssl_x509_export($x509,$cerStr);
            openssl_pkey_export($priKey, $pkStr);

            $data = $cerStr.$pkStr;

            $writeResult = file_put_contents($cerPath,$data);

            if($writeResult === false){
                throw new Exception("证书或签名生成失败!");
            }
        }
        $data = array();
        $data['signing_cert']   = 'file://'.realpath($cerPath);
        $data['private_key']    = 'file://'.realpath($cerPath);
        $data['private_key_password'] = $prikeyPass;
        return $data;
    }
}</pre>

操作类的使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<pre>require_once APP_ROOT_PATH."/system/utils/tcpdf_proxy.php";

$tcpdf = new TcpdfProxy();

$html = '&lt;html&gt;content&lt;/html&gt;';

$singConf = array();

$singConf['image'] = APP_ROOT_PATH.app_conf('CONTRACT_SEAL');

$singConf['sign_info'] = array(
    'Name' =&gt; app_conf('SHOP_TITLE'),
);

$tcpdf-&gt;setSignConf($singConf);

$tcpdf-&gt;outputPdf($html,'contract.pdf');</pre>

Git服务器搭建与客户端简单操作

服务器端准备工作:

1. 安装git

1
apt-get install git

2. 为了安全,新建一个专门用于代码部署的无特权用户

1
2
3
useradd -m git
#设置该用户的密码
passwd git

3. 新建一个目录作为要部署代码的根目录,如:

1
mkdir /var/www/git/item.git

4. 将这个目录的属主和属组都改为上面新建的用户git

1
2
cd /var/www/git
chown git:git item.git

5. 切换到部署代码的专用用户

1
su git

6. 进入项目根目录,初始化为git仓库

1
2
cd item.git
git init Initialized empty Git repository in /var/www/git/item.git/.git/

7. 必要的配置

1
2
3
4
5
6
7
8
#让仓库接受代码提交
git config receive.denyCurrentBranch ignore

#可选
git config core.worktree ~/www

#可选,禁止强制推送
git config --bool receive.denyNonFastForwards false

注意!

我们可以在自己的服务器上创建git仓库,有两种方式:

git –bare init (裸仓库)

git init

两者区别:

1,普通git仓库的目录结构就和你的代码目录结构一致,只多了.git目录,.git目录中包含了git的一些配置等数据

2,裸仓库只保存了一些配置信息等,肉眼是找不到我们所上传的代码的

额外说明:用”git init”初始化的版本库用户也可以在该目录下执行所有git方面的操作。但别的用户在将更新push上来的时候容易出现冲突。比如有用户在该目录(就称为远端仓库)下执行git操作,且有两个分支(master 和 b1),当前在master分支下。另一个用户想把自己在本地仓库(就称为本地仓库)的master分支的更新提交到远端仓库的master分支,他就想当然的敲了git push origin master:master于是乎出现错误 ,因为远端仓库的用户正在master的分支上操作,而你又要把更新提交到这个master分支上,当然就出错了。

确定一个repository是否为bare库的关键在于core.bare属性(boolean型属性)。core.bare属性可以有git config命令设置,也可以通过修改config文件的方式设置,conf文件指的就是当前库下面的conf配置文件,如果这里 bare = true 那么即使有.git文件夹也还是不能进行相关操作。 如果使用了git init初始化,则远程仓库的目录下,也包含work tree,当本地仓库向远程仓库push时, 如果远程仓库正在push的分支上(如果当时不在push的分支,就没有问题), 那么push后的结果不会反应在work tree上, 也即在远程仓库的目录下对应的文件还是之前的内容,必须得使用git reset –hard才能看到push后的内容.

本地仓库准备工作:

1. 通过 git clone 或 git pull 从仓库上将代码获取到本地

1
2
3
4
Administrator@PC-20170526BRPS MINGW32 /d/www/gitgit clone git@t.024ea.com:/var/www/git/item.git

Cloning into 'item'...
git@t.024ea.com's password: warning: You appear to have cloned an empty repository.

接下来接个phpstrom编辑更改代码提交,就简单了。

git利用post-receive自动化部署:

1. 更新服务端 git 仓库状态并检出文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
cd /var/www/git/item.git
git update-server-info
git checkout -f

# OR:

# 如果push的不是master分支
# git checkout branch_name

ls
new.html
cat new.html
#已经能看到提交文件
hello new git!

2.设置服务器钩子

1
2
3
4
5
6
#仓库配置文件
cd /var/www/git/item.git/.git/hooks

#默认没有这文件,新建了一个
touch post_receive
vim post_receive

shell脚本

1
2
3
4
5
6
7
8
9
10
#!/bin/sh
chown www-data /var/www/git/item.git -R
DEPLOY_PATH=/var/www/git/item.git

unset  GIT_DIR #这条命令很重要
cd $DEPLOY_PATH
git reset --hard
git pull
chown www-data -R $DEPLOY_PATH
chgrp www-data -R $DEPLOY_PATH

关于unset GIT_DIR的说明:

经过查找资料,如过没有unset命令,在文件中添加 git pull 时会报错,提示:”fatal: Not a git repository: ‘.'”。hook脚本执行了cd之后,继续执行git语句拉取的时候还是在hooks文件夹下,而不是cd的文件路径。git的hooks里面默认有一些环境变量,会导致无论在哪个语句之后执行git命令都会有一个默认的环境路径,既然这样unset 掉默认的GIT环境变量就可以了。

参考:http://alfred-long.iteye.com/blog/1836347

补充

关于本地第一次使用clone命令无法使用git用户登陆,需要先使用命令ssh-keygen,参考:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
root@office-host-ubuntu:/var/www/git# ssh-keygen
Generating public/private rsa key pair.
Enter file in which to save the key (/root/.ssh/id_rsa)/home/git/.ssh/id_rsa
Enter passphrase (empty for no passphrase):
Enter same passphrase again:
Your identification has been saved in /home/git/.ssh/id_rsa.
Your public key has been saved in /home/git/.ssh/id_rsa.pub.
The key fingerprint is:
SHA256:eXBtsym+2BZfcbWIk3E7Xh6doWn7q1lzMotX1zXxsbY
root@office-host-ubuntu The key's randomart image is:
+---[RSA 2048]----+
|                 |
|           o . +.|
|        . . O = @|
|         + = @.O=|
|        S o * *o*|
|         o.. o.E+|
|          .o .++o|
|         o....+=o|
|        ..o .+o..|
+----[SHA256]-----+

在Ubuntu上安装Chrome浏览器和ChromeDriver

一.安装Chrome浏览器

1.安装依赖

1
sudo apt-get install libxss1 libappindicator1 libindicator7

2.下载Chrome安装包

1
wget https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb

3.安装

1
2
sudo dpkg -i google-chrome*.deb
sudo apt-get install -f

二.安装ChromeDriver

1.安装xvfb以便我们可以无头奔跑地运行Chrome

1
sudo apt-get install xvfb

2.安装依赖

1
sudo apt-get install unzip

3.下载安装包

1
wget -N http://chromedriver.storage.googleapis.com/对应版本/chromedriver_linux64.zip

 
chromedriver与chrome的对应关系表:
各个版本的下载地址:淘宝镜像

chromedriver版本 支持的Chrome版本
v2.41 v67-69
v2.40 v66-68
v2.39 v66-68
v2.38 v65-67
v2.37 v64-66
v2.36 v63-65
v2.35 v62-64
v2.34 v61-63
v2.33 v60-62
v2.32 v59-61
v2.31 v58-60
v2.30 v58-60
v2.29 v56-58
v2.28 v55-57
v2.27 v54-56
v2.26 v53-55
v2.25 v53-55
v2.24 v52-54
v2.23 v51-53
v2.22 v49-52
v2.21 v46-50
v2.20 v43-48
v2.19 v43-47
v2.18 v43-46
v2.17 v42-43
v2.13 v42-45
v2.15 v40-43
v2.14 v39-42
v2.13 v38-41
v2.12 v36-40
v2.11 v36-40
v2.10 v33-36
v2.9 v31-34
v2.8 v30-33
v2.7 v30-33
v2.6 v29-32
v2.5 v29-32
v2.4 v29-32

4.解压缩+添加执行权限

1
unzip chromedriver_linux64.zip

5.移动

1
sudo mv -f chromedriver /usr/local/share/chromedriver

6.建立软连接

1
2
sudo ln -s /usr/local/share/chromedriver /usr/local/bin/chromedriver
sudo ln -s /usr/local/share/chromedriver /usr/bin/chromedriver

三.无头运行Chrome

1.安装Python依赖

1
2
pip3 install selenium
pip3 install pyvirtualdisplay

2.开整

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
from selenium import webdriver
from selenium.webdriver.chrome.options import Options 
chrome_options = Options()
chrome_options.add_argument('--no-sandbox')
chrome_options.add_argument('--headless')
chrome_options.add_argument('--disable-gpu')
driver = webdriver.Chrome(chrome_options=chrome_options)
driver.get(url)
iframe=driver.find_elements_by_tag_name('iframe')
    if len(iframe)>0:
        iframe = iframe[0]        
        driver.switch_to.frame(iframe)        
        ibody = driver.find_elements_by_tag_name('body')        
        if len(ibody)>0:            
            ibody=ibody[0]            
            video =ibody.find_element_by_tag_name('video')            
            if video is not None:                
                src = video.get_attribute("src")                
                if src !"" and src is not None:                    
                    res = {'src':src}                    
                    return str(res)

 

Linux禁止root帐号直接登录

Linux的默认管理员名即是root,只需要知道ROOT密码即可直接登录SSH。禁止Root从SSH直接登录可以提高服务器安全性。经过以下操作后即可实现。

本文适用于CentOS、Debian等Linux系统。
一、新建帐户

1
useradd kwxgd

SSH执行以上命令,可以创建名为“kwxgd”的帐号,可以自定义。
二、设置帐户密码

1
passwd kwxgd

使用passwd命令即可给相应帐户设置或修改密码。
根据图示,设置或修改密码需要填写两次,第二次为效验密码,输入完毕后请回车确认。
三、不允许root直接登陆1、修改相关文件

1
vi /etc/ssh/sshd_config

SSH执行以上命令,修改sshd_config文件
2、禁止root登陆查找“#PermitRootLogin yes”,将前面的“#”去掉,短尾“Yes”改为“No”,并保存文件。

四、下次登陆
1、先使用新建账号“kwxgd”以普通用户登陆。
2、若要获得ROOT权限,在SSH中执行以下命令

1
su root

执行以上命令并输入root密码后即可获得root权限。

Linux简单性能测试命令

1.top
Top命令显示了实际CPU使用情况,默认情况下,它显示了服务器上占用CPU的任务信息并且每5秒钟刷新一次。你可以通过多种方式分类它们,包括PID、时间和内存使用情况。
第一行的load average即为系统负载,就是说整个VPS资源占用情况,如果正常建站,一般很少有超过5的时候;
第三行的,这个是CPU占用资源。还有后面的??%wa这个是硬盘状态,正常情况下CPU最好不要超过30%占用.wa指数长期30%以上,基本上硬盘就是不给力状态。
第四行是内存,总内存,已使用内存,空闲内存。我这里是W2的VPS,内存为1GB,大家可以参考下。

2.查看CPU,硬盘和内存信息

1
2
3
cat /proc/cpuinfo(CPU信息)
cat /proc/meminfo(内存信息)
df –lh(查看硬盘信息)

3.下载测试

1
wget http://cachefly.cachefly.net/100mb.test

4.磁盘I/O测试

1
dd if=/dev/zero of=test bs=64k count=16k oflag=dsync

这个命令,是测试磁盘I/O性能的,图中有磁盘写入速率,可以作为参考。
或者使用此命令:

1
dd if=/dev/zero of=test bs=64k count=4k oflag=dsync

SSD磁盘用这个命令:

1
hdparm -t /dev/xvda

经过上面两步测试,磁盘多出了2个文件:100mb.test,test,我们用命令删除它们。

1
2
rm 100mb.test
rm test

5.UBI跑分综合性能测试

1
wget http://www.CTOHome.com/linux-vps-pack/unixbench.sh;sh ./unixbench.sh;

一般高于400分就算正常水准,如果高于1000的话,就是非常给力。

6.其他测试命令

1
2
3
iostat  (磁盘和内存使用率)
Vmstat  (进程、内存、页面I/O块和CPU等信息的监控)
wget -qO- bench.sh | bash

linux 系统中 /etc/passwd 和 /etc/shadow文件详解

在linux操作系统中, /etc/passwd文件中的每个用户都有一个对应的记录行,记录着这个用户的一下基本属性。该文件对所有用户可读。

而/etc/shadow文件正如他的名字一样,他是passwd文件的一个影子,/etc/shadow文件中的记录行与/etc/passwd中的一一对应,它由pwconv命令根据/etc/passwd中的数据自动产生。但是/etc/shadow文件只有系统管理员才能够进行修改和查看。

/etc/passwd文件介绍
首先,我们通过命令行

1
cat /etc/passwd

进行查看/etc/passwd文件内容:

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
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync
games:x:5:60:games:/usr/games:/usr/sbin/nologin
man:x:6:12:man:/var/cache/man:/usr/sbin/nologin
lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin
mail:x:8:8:mail:/var/mail:/usr/sbin/nologin
news:x:9:9:news:/var/spool/news:/usr/sbin/nologin
uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin
proxy:x:13:13:proxy:/bin:/usr/sbin/nologin
www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin
backup:x:34:34:backup:/var/backups:/usr/sbin/nologin
list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin
irc:x:39:39:ircd:/var/run/ircd:/usr/sbin/nologin
gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/usr/sbin/nologin
nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin
libuuid:x:100:101::/var/lib/libuuid:
syslog:x:101:104::/home/syslog:/bin/false
messagebus:x:102:106::/var/run/dbus:/bin/false
usbmux:x:103:46:usbmux daemon,,,:/home/usbmux:/bin/false
dnsmasq:x:104:65534:dnsmasq,,,:/var/lib/misc:/bin/false
avahi-autoipd:x:105:113:Avahi autoip daemon,,,:/var/lib/avahi-autoipd:/bin/false
kernoops:x:106:65534:Kernel Oops Tracking Daemon,,,:/:/bin/false
rtkit:x:107:114:RealtimeKit,,,:/proc:/bin/false
saned:x:108:115::/home/saned:/bin/false
whoopsie:x:109:116::/nonexistent:/bin/false
speech-dispatcher:x:110:29:Speech Dispatcher,,,:/var/run/speech-dispatcher:/bin/sh
avahi:x:111:117:Avahi mDNS daemon,,,:/var/run/avahi-daemon:/bin/false
lightdm:x:112:118:Light Display Manager:/var/lib/lightdm:/bin/false
colord:x:113:121:colord colour management daemon,,,:/var/lib/colord:/bin/false
hplip:x:114:7:HPLIP system user,,,:/var/run/hplip:/bin/false
pulse:x:115:122:PulseAudio daemon,,,:/var/run/pulse:/bin/false
yaofei:x:1000:1000:ubuntu14.04,,,:/home/yaofei:/bin/bash
sshd:x:116:65534::/var/run/sshd:/usr/sbin/nologin
mysql:x:117:125:MySQL Server,,,:/nonexistent:/bin/false

从文件中我们可以看到,/etc/passwd中一行记录对应着一个用户,每行记录又被冒号(:)分隔为7个字段,其格式和具体含义如下:
用户名:口令:用户标识号:组标识号:注释性描述:主目录:登录Shell

用户名(login_name):是代表用户账号的字符串。通常长度不超过8个字符,并且由大小写字母和/或数字组成。登录名中不能有冒号(:),因为冒号在这里是分隔符。为了兼容起见,登录名中最好不要包含点字符(.),并且不使用连字符(-)和加号(+)打头。
口令(passwd):一些系统中,存放着加密后的用户口令字。虽然这个字段存放的只是用户口令的加密串,不是明文,但是由于/etc/passwd文件对所有用户都可读,所以这仍是一个安全隐患。因此,现在许多Linux系统(如SVR4)都使用了shadow技术,把真正的加密后的用户口令字存放到/etc/shadow文件中,而在/etc/passwd文件的口令字段中只存放一个特殊的字符,例如“x”或者“*”。
用户标识号(UID):是一个整数,系统内部用它来标识用户。一般情况下它与用户名是一一对应的。如果几个用户名对应的用户标识号是一样的,系统内部将把它们视为同一个用户,但是它们可以有不同的口令、不同的主目录以及不同的登录Shell等。取值范围是0-65535。0是超级用户root的标识号,1-99由系统保留,作为管理账号,普通用户的标识号从100开始。在Linux系统中,这个界限是500。
组标识号(GID):字段记录的是用户所属的用户组。它对应着/etc/group文件中的一条记录。
注释性描述(users):字段记录着用户的一些个人情况,例如用户的真实姓名、电话、地址等,这个字段并没有什么实际的用途。在不同的Linux系统中,这个字段的格式并没有统一。在许多Linux系统中,这个字段存放的是一段任意的注释性描述文字,用做finger命令的输出。
主目录(home_directory):也就是用户的起始工作目录,它是用户在登录到系统之后所处的目录。在大多数系统中,各用户的主目录都被组织在同一个特定的目录下,而用户主目录的名称就是该用户的登录名。各用户对自己的主目录有读、写、执行(搜索)权限,其他用户对此目录的访问权限则根据具体情况设置。
登录Shell(Shell):用户登录后,要启动一个进程,负责将用户的操作传给内核,这个进程是用户登录到系统后运行的命令解释器或某个特定的程序,即Shell。Shell是用户与Linux系统之间的接口。Linux的Shell有许多种,每种都有不同的特点。常用的有

1
2
3
4
5
sh(BourneShell)
csh(CShell)
ksh(KornShell)
tcsh(TENEX/TOPS-20typeCShell)
bash(BourneAgainShell)

等。系统管理员可以根据系统情况和用户习惯为用户指定某个Shell。如果不指定Shell,那么系统使用sh为默认的登录Shell,即这个字段的值为/bin/sh。
/etc/shadow文件介绍
/etc/shadow文件格式与/etc/passwd文件格式类似,同样由若干个字段组成,字段之间用“:”隔开。
通过命令行输入

1
sudo cat /etc/shadow

进行文件内容查看:

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
root:!:17043:0:99999:7:::
daemon:*:16652:0:99999:7:::
bin:*:16652:0:99999:7:::
sys:*:16652:0:99999:7:::
sync:*:16652:0:99999:7:::
games:*:16652:0:99999:7:::
man:*:16652:0:99999:7:::
lp:*:16652:0:99999:7:::
mail:*:16652:0:99999:7:::
news:*:16652:0:99999:7:::
uucp:*:16652:0:99999:7:::
proxy:*:16652:0:99999:7:::
www-data:*:16652:0:99999:7:::
backup:*:16652:0:99999:7:::
list:*:16652:0:99999:7:::
irc:*:16652:0:99999:7:::
gnats:*:16652:0:99999:7:::
nobody:*:16652:0:99999:7:::
libuuid:!:16652:0:99999:7:::
syslog:*:16652:0:99999:7:::
messagebus:*:16652:0:99999:7:::
usbmux:*:16652:0:99999:7:::
dnsmasq:*:16652:0:99999:7:::
avahi-autoipd:*:16652:0:99999:7:::
kernoops:*:16652:0:99999:7:::
rtkit:*:16652:0:99999:7:::
saned:*:16652:0:99999:7:::
whoopsie:*:16652:0:99999:7:::
speech-dispatcher:!:16652:0:99999:7:::
avahi:*:16652:0:99999:7:::
lightdm:*:16652:0:99999:7:::
colord:*:16652:0:99999:7:::
hplip:*:16652:0:99999:7:::
pulse:*:16652:0:99999:7:::
yaofei:$1$5M0Rbozg$1fWsJaQB.TFAL24b96xi41:17043:0:99999:7:::
sshd:*:17043:0:99999:7:::
mysql:!:17048:0:99999:7:::

文件中字段主要含义为:登录名:加密口令:最后一次修改时间:最小时间间隔:最大时间间隔:警告时间:不活动时间:失效时间:标志

“登录名”是与/etc/passwd文件中的登录名相一致的用户账号
“口令”字段存放的是加密后的用户口令字:
如果为空,则对应用户没有口令,登录时不需要口令;
星号代表帐号被锁定;
双叹号表示这个密码已经过期了;
$6$ 开头的,表明是用SHA-512加密;
$1$ 表明是用MD5加密;
$2$ 是用Blowfish加密;
$5$ 是用 SHA-256加密;
“最后一次修改时间”表示的是从某个时刻起,到用户最后一次修改口令时的天数。时间起点对不同的系统可能不一样。例如在SCOLinux中,这个时间起点是1970年1月1日。
“最小时间间隔”指的是两次修改口令之间所需的最小天数。
“最大时间间隔”指的是口令保持有效的最大天数。
“警告时间”字段表示的是从系统开始警告用户到用户密码正式失效之间的天数。
“不活动时间”表示的是用户没有登录活动但账号仍能保持有效的最大天数。
“失效时间”字段给出的是一个绝对的天数,如果使用了这个字段,那么就给出相应账号的生存期。期满后,该账号就不再是一个合法的账号,也就不能再用来登录了。

The following packages have been kept back 的解决方法

ubuntu 16.04

1
2
3
4
5
6
7
8
9
10
sudo apt-get upgrade
Reading package lists… Done
Building dependency tree
Reading state information… Done
The following packages have been kept back:
ginn hplip hplip-data libgrip0 libhpmud0 libsane-hpaio libunity-2d-private0
libunity-core-5.0-5 linux-generic linux-headers-generic linux-image-generic
printer-driver-hpcups printer-driver-hpijs unity unity-2d-common
unity-2d-panel unity-2d-shell unity-2d-spread unity-common unity-services
0 upgraded, 0 newly installed, 0 to remove and 20 not upgraded.

是因为有部份packages的安装版比release版新而出现”The following packages have been kept back”这个问题,解决方法是使用

1
apt-get -u dist-upgrade

统一更新到发布的版本。这条命令会强制更新软件包到最新版本,并自动解决缺少的依赖包。

Ubuntu Server配置SVN

1.安装SVN

1
apt-get install subversion

2.创建项目目录

1
2
3
4
mkdir svn //创建一个文件夹,用来存放一个或多个项目
cd ./svn //进入这个目录
mkdir project//创建一个新的项目目录
svnadmin create ./svn/project //创建svn文件仓库(这一步是让这个新的文件夹变成svn的版本库)

3.权限设置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
cd ./svn/project/conf
vim passwd //添加用户设置密码
root = 123456
user1 = 123456
user2 = 123456
vim authz //编辑用户权限
admin = user1,user2,root
@admin=rw
*=r
vim svnserve.conf //控制访问权限
anon-access = none #匿名用户(anonymous users)的访问权限
auth-access = write #授权用户(authenticated users)的访问权限
password-db = passwd #密码数据库文件的位置,这里指向同级目录下的passwd文件
authz-db = authz #用户授权规则文件的位置,这里指向同级目录下的authz文件

4.启动SVN
启动svn

1
svnserve -d -r /svn 

这里的/svn 的目录就是你第一步创建用来存放项目的那个目录

关闭svn
在配置过程中如果需要关闭svn则kill

1
2
3
4
ps aux | grep svn
root 21618 0.0 0.0 69596 616 ? Ss 19:07 0:00 svnserve -d -r /svn
root 21927 0.0 0.2 11748 2156 pts/0 S+ 19:10 0:00 grep –color=auto svn
kill 21618

5.配置钩子
在这里svn提供了好的钩子模板,你可以根据自己的情况选择模板,一般都是选择post-commit.tmpl(提交后更新)

1
2
cd ./svn/project/hooks
vim post-commit //新建文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
#!/bin/bash
EPOS="$1"
REV="$2"
TXN_NAME="$3"
export.UTF-8
SVN=/usr/bin/svn
WEB=/home/wwwroot/projectDIR
LOG=/tmp/www.log
$SVN update $WEB –username svnuser–password yourpasswd > $LOG || exit 1
chown wwwroot:www $WEB -R
exit 0
ifthen
echo "ok" >> /tmp/www.out
fi

6.设置模板执行权限

1
chmod 755 post-commit

7.上传代码纳入版本管理

1
svn import -m "import" /home/project svn://192.168.213.31/project

8.检出到WEB目录

1
/usr/bin/svn checkout svn://serverIP/project/ /home/wwwroot/project