2017年8月21日

記錄用 CentOS 7 安裝 Redmine 3.3.3

因機器搬遷,需要把舊的 server 的資料,移到新機器上。

用 docker 測試

docker run -d -p 10022:22 -p 10080:80 centosssh /usr/sbin/sshd -D

如果遇到 Failed to get D-Bus connection: Operation not permitted 的問題,就改用這個方式啟動。因為需要 ssh 及 web 的 port,啟動時先對應好。

ref [原创] 解决 CentOS7 容器 Failed to get D-Bus connection: Operation not permitted

docker run -d -p 10022:22 -p 10080:80 -e "container=docker" --privileged=true -v /sys/fs/cgroup:/sys/fs/cgroup --name centos7test centosssh /usr/sbin/init

docker exec -it centos7test /bin/bash

安裝Apache、MariaDB、PHP

yum install -y httpd php mariadb mariadb-server mariadb-devel systemd which wget

設定MariaDB的DB為utf8

vi /etc/my.cnf.d/server.cnf

[mysqld]
character-set-server=utf8

vi /etc/my.cnf.d/client.cnf

[client]
default-character-set=utf8

啟動 MariaDB

systemctl start mariadb
mysql_secure_installation

建立 redmine 資料庫及帳號

mysql -u root -p

CREATE DATABASE redmine CHARACTER SET utf8;
CREATE USER 'redmine'@'localhost' IDENTIFIED BY 'password';
GRANT ALL PRIVILEGES ON redmine.* TO 'redmine'@'localhost';
flush privileges;
exit;

修改 MariaDB 密碼

mysqladmin -u root password 'password'

安裝 rvm

安裝RVM (Ruby管理工具)、Ruby、Rubygem

# Install Key
gpg --keyserver hkp://keys.gnupg.net --recv-keys 409B6B1796C275462A1703113804BB82D39DC0E3
#Install RVM
\curl -sSL https://get.rvm.io | bash -s stable

測試過 ruby 2.4.0 有些問題,所以安裝 ruby 限制在 2.2.4 版。

source /etc/profile.d/rvm.sh
rvm requirements
rvm install ruby 2.2.4
#rvm install rubygem
#gem install rails --no-rdoc --no-ri

不使用 yum 安裝 ruby (現在是 2.0 版)

#yum list ruby 
yum install -y gcc libxml2-devel

# 包含ruby/gem/libyaml
#yum install -y ruby ruby-devel

gem install bundler

gem install rake --no-document
gem i nokogiri --no-document -v='1.6.8'
gem i mime-types --no-document

# ruby 2 與 rails 5 不相容
gem install rails --no-document -v='4.2.7'

gem install rbpdf --no-document
gem install rbpdf-font --no-document

安裝passenger

yum install -y libcurl-devel httpd-devel apr-devel apr-util-devel

gem install passenger
passenger-install-apache2-module

安裝完成後,會出現module passenger的設定檔文字

vi /etc/httpd/conf.d/passenger.conf

LoadModule passenger_module /usr/local/rvm/gems/ruby-2.2.4/gems/passenger-5.1.4/buildout/apache2/mod_passenger.so
<IfModule mod_passenger.c>
     PassengerRoot /usr/local/rvm/gems/ruby-2.2.4/gems/passenger-5.1.4
     PassengerDefaultRuby /usr/local/rvm/gems/ruby-2.2.4/wrappers/ruby
</IfModule>

設定 redmine httpd virtual host

vi /etc/httpd/conf.d/redmine.conf

RailsEnv production
RailsBaseURI /redmine

<Directory /home/redmine/redmine-3.3.3/public>
  Options FollowSymlinks
  AllowOverride none
  Require all granted
</Directory>

restart httpd

systemctl restart httpd

安裝 redmine

cd /home

mkdir redmine
cd redmine

wget http://www.redmine.org/releases/redmine-3.3.3.tar.gz

tar -xf redmine-3.3.3.tar.gz

ln -s /home/redmine/redmine-3.3.3/public /var/www/html/redmine

chown -R apache:apache redmine-3.3.3

設定 mysql

cd redmine-3.3.3/config
cp database.yml.example database.yml

# 設定資料庫使用者名稱、密碼。

vi database.yml

把
production:
  adapter: mysql2
  database: redmine
  host: localhost
  username: root
  password: ""
  encoding: utf8

改為

production:
  adapter: mysql2
  database: redmine
  host: localhost
  username: redmine
  password: "dbpassword"
  encoding: utf8
cd ..

bundle install --without development test rmagick

### 這個是生成redmine的什麼token,不生成的話瀏覽器會連接不上的,每次生成,以前的cookie內容就會失效
bundle exec rake generate_secret_token

如果有舊的 DB 要先 restore redmine db

mysql -uroot -p redmine < redmine_noplugins.sql

升級 DB schema

RAILS_ENV=production bundle exec rake db:migrate

有舊DB 就不需要 loaddefaultdata

# 生成數據庫對象

RAILS_ENV=production REDMINE_LANG=zh bundle exec rake redmine:load_default_data

可用 webrick 內建 web server 測試,也可以跳過不做

bundle exec rails server webrick -e production
wget http://localhost:3000
  > 測試安裝

為以後apache服務器對應(redmine/public目錄)做準備

cd public

cp htaccess.fcgi.example htaccess.fcgi
cp dispatch.fcgi.example dispatch.fcgi

設定 email

cd /home/redmine/redmine-3.3.3/config
cp configuration.yml.example configuration.yml
vi configuration.yml

修改 configuration.yml 前面的 email_delivery,要注意不能修改縮排的格式。

  email_delivery:
    delivery_method: :smtp
    smtp_settings:
      enable_starttls_auto: true
      address: "smtp.gmail.com"
      port: 587
      domain: "maxkit.com.tw"
      authentication: :login
      user_name: "maxkit@maxkit.com.tw"
      password: "youremailpassword"

references

Installing Redmine

CentOS 7 安裝 Redmine

Centos 7安裝 redmine 3.X

最小化安装centos7.3 redmine3.3.3 passenger


Upgrading redmine

Redmine 2.6.3 to 3.0.1 upgrade

redmine_bak to git repository

2017年8月14日

以 docker 安裝一個可以遠端 ssh 登入的 centos 7 image

以下紀錄如何產生一個基本的 docker image,安裝了 openssh-server 可以用 ssh 遠端登入。

設定 docker image 以及 openssh-server

docker run -it --name c1 centos:latest /bin/bash

安裝一些基本工具,以及 openssh-server

#yum provides ifconfig

yum install -y net-tools telnet iptables sudo initscripts
yum install -y passwd openssl openssh-server

測試 sshd

/usr/sbin/sshd -D
Could not load host key: /etc/ssh/ssh_host_rsa_key
Could not load host key: /etc/ssh/ssh_host_ecdsa_key
Could not load host key: /etc/ssh/ssh_host_ed25519_key

缺少了一些 key

ssh-keygen -t rsa -f /etc/ssh/ssh_host_rsa_key
#直接 enter 即可

ssh-keygen -t dsa -f /etc/ssh/ssh_host_dsa_key
#直接 enter 即可

ssh-keygen -t ecdsa -f /etc/ssh/ssh_host_ecdsa_key -N ""

ssh-keygen -t ed25519 -f /etc/ssh/ssh_host_ed25519_key -N ""

修改 UsePAM 設定

vi /etc/ssh/sshd_config
# UsePAM yes 改成 UsePAM no
UsePAM no

再測試看看 sshd

/usr/sbin/sshd -D

修改 root 密碼

passwd root

離開 docker

exit

以 docker ps -l 找到剛剛那個 container 的 id

$ docker ps -l
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS                     PORTS               NAMES
107fb9c3fc0d        centos:latest       "/bin/bash"         7 minutes ago       Exited (0) 2 seconds ago                       c1

將 container 存成另一個新的 image

docker commit 107fb9c3fc0d centosssh

以新的 image 啟動另一個 docker instance

docker run -d -p 10022:22 centosssh /usr/sbin/sshd -D

現在可以直接 ssh 登入新的 docker machine

ssh root@localhost -p 10022

如果遇到 Failed to get D-Bus connection: Operation not permitted 的問題:ref [原创] 解决 CentOS7 容器 Failed to get D-Bus connection: Operation not permitted

docker run -d -p 10022:22 -e "container=docker" --privileged=true -v /sys/fs/cgroup:/sys/fs/cgroup --name centos7test centosssh /usr/sbin/init

docker exec -it centos7test /bin/bash

gitolite 測試

在新的 docker 機器上安裝 gitolite 測試

yum install -y autoconf git

useradd git
passwd git

產生管理員的 key

ssh-keygen

Generating public/private rsa key pair.
Enter file in which to save the key (/root/.ssh/id_rsa):
Created directory '/root/.ssh'.
Enter passphrase (empty for no passphrase):
Enter same passphrase again:
Your identification has been saved in /root/.ssh/id_rsa.
Your public key has been saved in /root/.ssh/id_rsa.pub.
The key fingerprint is:
01:93:46:03:17:6e:e2:06:ec:d6:07:db:2e:13:a3:92 root@1f01b0c5ad69
The key's randomart image is:
+--[ RSA 2048]----+
|    .oBo         |
| .   oo+         |
|  o o.o .        |
| . + *   .       |
|  o B o S        |
| o o =           |
|E . o .          |
| .   o           |
|                 |
+-----------------+
cp /root/.ssh/id_rsa.pub /home/git/admin.pub

以 scp 遠端測試 key

sshpass -p "password" scp -p -P 10022 git@localhost:/home/git/admin.pub .

在本機上安裝 gitolite

su - git

mkdir ~/bin

git clone git://github.com/sitaramc/gitolite

gitolite/install -ln ~/bin

把 admin.pub 放入 gitolite

gitolite setup -pk admin.pub

Initialized empty Git repository in /home/git/repositories/gitolite-admin.git/
Initialized empty Git repository in /home/git/repositories/testing.git/
WARNING: /home/git/.ssh missing; creating a new one
    (this is normal on a brand new install)
WARNING: /home/git/.ssh/authorized_keys missing; creating a new one
    (this is normal on a brand new install)

回到 root 身份

exit

以 git clone gitolite-admin 進行 local git 測試

mkdir test
cd test
git config --global user.email "charley@maxkit.com.tw"
git config --global user.name "charley"

git clone ssh://git@localhost/gitolite-admin

現在就可以利用 gitolite-admin 進行 git 帳號及 repo 維護

放入新的 user key: test.pub 放到 keydir 目錄中

git add keydir/test.pub

修改 conf/gitolite.conf

repo gitolite-admin
    RW+     =   admin
    RW+     =   test

repo testing
    RW+     =   admin
    RW+     =   test

將新的 test 增加到 gitolite-admin 裡面

git add keydir/test.pub
git add conf/gitolite.conf
git commit -m 'add test key'
git push origin master

也可以用遠端的方式存取 git

git clone ssh://git@localhost:10022/gitolite-admin

How to install Gitolite in CentOS 7

Linux 使用 Gitolite 架設 Git Server

使用Gitolite搭建Git服務器

gitolite basic administration

References

centos7中安裝一個可以ssh登陸的docker容器

Docker安裝SSH【Ubuntu、CentOS】

2017年8月7日

python tornado websocket server and client

tornado 是一個用Python語言寫成的Web服務器兼Web應用框架,以下記錄如何用 tornado framework 撰寫 websocket Echo Server & Client。

安裝 tornado

在 debian 安裝 python library

wget https://bootstrap.pypa.io/get-pip.py
sudo python get-pip.py

sudo pip install tornado

在 mac 安裝 tornado

sudo port install py27-tornado

Echo Server

# -*- coding: utf-8 -*-

import datetime
import sys
import tornado.httpserver
import tornado.websocket
import tornado.ioloop
import tornado.web

class WSHandler(tornado.websocket.WebSocketHandler):
    clients = []

    def check_origin(self, origin):
        return True

    def open(self):
        print "New client connected"
        #self.write_message("You are connected")
        WSHandler.clients.append(self)

    def on_message(self, message):
        self.write_message(message)

    def on_close(self):
        print "Client disconnected"
        WSHandler.clients.remove(self)

    @classmethod
    def write_to_clients(cls):
        print "Writing to clients"
        for client in cls.clients:
            client.write_message("Hi there!")

application = tornado.web.Application([
    (r"/", WSHandler),
])

if __name__ == "__main__":
    try:
        http_server = tornado.httpserver.HTTPServer(application)
        http_server.listen(9000)
        main_loop = tornado.ioloop.IOLoop.instance()

        # Schedule event (5 seconds from now)
        #main_loop.add_timeout(datetime.timedelta(seconds=5), WSHandler.write_to_clients)

        # background update every x seconds
        # 固定每 5 秒鐘就呼叫一次 WSHandler.write_to_clients 廣播訊息
        task = tornado.ioloop.PeriodicCallback(
                WSHandler.write_to_clients,
                5 * 1000)
        task.start()

        # Start main loop
        #main_loop.start()
        main_loop.make_current()
    except KeyboardInterrupt:
        #print("KeyboardInterrupt")
        sys.exit()

EchoServer in aother Thread

將 Server 放在另一個 Thread 啟動,保留 main thread 用在其他的用途上。

# -*- coding: utf-8 -*-

import datetime
import sys
import tornado.httpserver
import tornado.websocket
import tornado.ioloop
import tornado.web
import os
from threading import Thread

class WSHandler(tornado.websocket.WebSocketHandler):
    clients = []

    def check_origin(self, origin):
        return True

    def open(self):
        print "New client connected"
        #self.write_message("You are connected")
        WSHandler.clients.append(self)

    def on_message(self, message):
        self.write_message(message)

    def on_close(self):
        print "Client disconnected"
        WSHandler.clients.remove(self)

    @classmethod
    def write_to_clients(cls):
        print "Writing to clients"
        for client in cls.clients:
            client.write_message("Hi there!")

class WebThread(Thread):
    def __init__(self):
        Thread.__init__(self, name='WebThread')

    def run(self):
        curdir = os.path.dirname(os.path.realpath(__file__))

        application = tornado.web.Application([
            (r"/", WSHandler),
        ])

        http_server = tornado.httpserver.HTTPServer(application)
        http_server.listen(9000)
        main_loop = tornado.ioloop.IOLoop.instance()

        # Schedule event (5 seconds from now)
        #main_loop.add_timeout(datetime.timedelta(seconds=5), WSHandler.write_to_clients)

        # background update every x seconds
        # 固定每 5 秒鐘就呼叫一次 WSHandler.write_to_clients 廣播訊息
        task = tornado.ioloop.PeriodicCallback(
                WSHandler.write_to_clients,
                5 * 1000)
        task.start()

        main_loop.start()


if __name__ == "__main__":
    try:
        webThread = WebThread()
        webThread.daemon = True
        webThread.start()

        while True:
            pass

    except KeyboardInterrupt:
        #print("KeyboardInterrupt")
        sys.exit()

EchoClient.html


<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.12.1/jquery.min.js"></script>
<script>

$(document).ready(function(){
    var socket = new WebSocket('ws://127.0.0.1:9000/');

    socket.onopen = function(event){
        socket.send('Hi');
    }

    socket.onmessage = function(event){
        console.log(event.data);
    };

    $(window).unload(function(event){
        socket.close();
    });
});

</script>

Echo Client with tornado framework

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import sys

from tornado.ioloop import IOLoop, PeriodicCallback
from tornado import gen
from tornado.websocket import websocket_connect


class Client(object):
    def __init__(self, url, timeout):
        self.url = url
        self.timeout = timeout
        self.ioloop = IOLoop.instance()
        self.ws = None
        self.connect()

        # 每 20 秒發送一次 ping
        PeriodicCallback(self.keep_alive, 20000, io_loop=self.ioloop).start()

        self.ioloop.start()

    @gen.coroutine
    def connect(self):
        print "trying to connect"
        try:
            self.ws = yield websocket_connect(self.url)
        except Exception, e:
            print "connection error"
        else:
            print "connected"
            self.run()

    @gen.coroutine
    def run(self):
        while True:
            msg = yield self.ws.read_message()
            if msg is None:
                print "connection closed"
                self.ws = None
                break
            else:
                print msg

    def keep_alive(self):
        if self.ws is None:
            self.connect()
        else:
            self.ws.write_message("ping")

if __name__ == "__main__":
    try:
        client = Client("ws://localhost:9000", 5)
    except KeyboardInterrupt:
        #print("KeyboardInterrupt")
        sys.exit()

References

SIMPLE WEB SOCKET CLIENT IMPLEMENTATION USING TORNADO FRAMEWORK.

tornado-websocket-client-example/client.py

2017年7月31日

用 socket 將 OpenCV 影像傳送到遠端 client

camera 影像以 socket 傳送到 client 的測試,目前是用 socket,將來還要改成用 websocket 處理,用以接受多個 client 連線的問題。

numpy

numpy 可以快速地將 bytearray 及 martix 進行轉換,透過這邊的程式碼,我們可以了解到,圖片就是一個二維陣列的矩陣,矩陣中每一個點,代表圖片中的一個像素點,而 OpenCV 是以 BGR 的形式儲存像素點的資料。

#!/usr/bin/python
# coding=utf-8

import cv2
import numpy
import os
import time

# 亂數產生 120000 個 bytes, 轉換為 numpy array
randomByteArray = bytearray(os.urandom(120000))
flatNumpyArray = numpy.array(randomByteArray)

# reshape 成 300x400 的矩陣並存成 gray scale 圖片
grayImage = flatNumpyArray.reshape(300,400)
cv2.imwrite('RandomGray.png', grayImage)

# reshape 成 400x100 的矩陣並存成 BGR 圖片
bgrImage = flatNumpyArray.reshape(100,400, 3)
cv2.imwrite('RandomBGR.png', bgrImage)

camera 影像以 socket 傳送到 client

  • 版本1

server.py 等待 client.py 連接,server 接收 client 的 camera 資料

server.py

import socket
import cv2
import numpy

def recvall(sock, count):
    buf = b''
    while count:
        newbuf = sock.recv(count)
        if not newbuf: return None
        buf += newbuf
        count -= len(newbuf)
    return buf

TCP_IP = "192.168.1.152"
TCP_PORT = 8002
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.bind((TCP_IP, TCP_PORT))
s.listen(True)
conn, addr = s.accept()
while 1:
    length = recvall(conn,16)
    stringData = recvall(conn, int(length))
    data = numpy.fromstring(stringData, dtype='uint8')
    decimg=cv2.imdecode(data,1)
    cv2.imshow('SERVER',decimg)
    cv2.waitKey(30)

s.close()
cv2.destroyAllWindows()

client.py

import socket
import cv2
import numpy

TCP_IP = "192.168.1.152"
TCP_PORT = 8002

sock = socket.socket()
capture = cv2.VideoCapture(0)
ret, frame = capture.read()
sock.connect((TCP_IP, TCP_PORT))
encode_param=[int(cv2.IMWRITE_JPEG_QUALITY),90]
while ret:
    result, imgencode = cv2.imencode('.jpg', frame, encode_param)
    data = numpy.array(imgencode)
    stringData = data.tostring()
    sock.send( str(len(stringData)).ljust(16));
    sock.send( stringData );

    ret, frame = capture.read()
    decimg=cv2.imdecode(data,1)
    cv2.imshow('CLIENT',decimg)
    cv2.waitKey(30)

sock.close()
cv2.destroyAllWindows()
  • 版本2

server.py 等待 client.py 連接,client 接收 server 的 camera 資料,顯示在畫面上

server2.py

import socket
import cv2
import numpy

capture = cv2.VideoCapture(0)

TCP_IP = "192.168.1.159"
TCP_PORT = 8002
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.bind((TCP_IP, TCP_PORT))
s.listen(True)

conn, addr = s.accept()

ret, frame = capture.read()
encode_param=[int(cv2.IMWRITE_JPEG_QUALITY),90]

while ret:
    result, imgencode = cv2.imencode('.jpg', frame, encode_param)
    data = numpy.array(imgencode)
    stringData = data.tostring()

    conn.send( str(len(stringData)).ljust(16));
    conn.send( stringData );

    ret, frame = capture.read()
    decimg=cv2.imdecode(data,1)
    cv2.imshow('SERVER2',decimg)
    cv2.waitKey(30)

conn.close()
cv2.destroyAllWindows()

client2.py

import socket
import cv2
import numpy

def recvall(sock, count):
    buf = b''
    while count:
        newbuf = sock.recv(count)
        if not newbuf: return None
        buf += newbuf
        count -= len(newbuf)
    return buf

TCP_IP = "192.168.1.159"
TCP_PORT = 8002

sock = socket.socket()
sock.connect((TCP_IP, TCP_PORT))

while 1:
    length = recvall(sock,16)
    stringData = recvall(sock, int(length))
    data = numpy.fromstring(stringData, dtype='uint8')
    decimg=cv2.imdecode(data,1)
    cv2.imshow('CLIENT2',decimg)
    cv2.waitKey(1)

sock.close()
cv2.destroyAllWindows()

改用 wxPython 作為 GUI

wxWidget 是一個開放原始碼且跨平台的物件工具集(widget toolkit),可用來建立基本的圖形使用者介面,先前測試時,都是以 cv2.imshow 進行畫面 preview,未來為了要製作更複雜的 GUI,所以先測試將畫面改為利用 wxPython 處理。

# mac 上要安裝 py27-wxpython
sudo port install py27-wxpython-3.0
# -*- coding: utf-8 -*-
import wx
import cv2
import time

class TestOpenCV ( wx.Frame ):
    windowWidth = 500
    windowHeight = 320

    def __init__( self, parent=None ):
        wx.Frame.__init__ ( self, parent, id = wx.ID_ANY, title = u"單一視訊畫面", pos = wx.DefaultPosition, size = wx.Size( self.windowWidth, self.windowHeight), style = wx.DEFAULT_FRAME_STYLE|wx.TAB_TRAVERSAL )

        self.SetSizeHintsSz( wx.DefaultSize, wx.DefaultSize )

        bSizer1 = wx.BoxSizer( wx.VERTICAL )

        self.stbmp1 = wx.StaticBitmap( self, wx.ID_ANY, wx.NullBitmap, wx.DefaultPosition, wx.DefaultSize, 0 )
        bSizer1.Add( self.stbmp1, 1, wx.ALL|wx.EXPAND, 5 )
##        self.stbmp1.SetBitmap(wx.Bitmap( u"../image/heats1-f1-02_gray_pressed.png", wx.BITMAP_TYPE_ANY ))

        self.SetSizer( bSizer1 )
        self.Layout()

        self.Centre( wx.BOTH )

    def __del__( self ):
        pass

    def scale_bitmap(self, bitmap, width, height):
        image = wx.ImageFromBitmap(bitmap)
        image = image.Scale(width, height, wx.IMAGE_QUALITY_NORMAL)
        newimg = wx.BitmapFromImage(image)
        return newimg

    def updateImage(self, bitmap):
        # 縮小圖片符合視窗的大小
        newbitmap = self.scale_bitmap(bitmap, self.windowWidth-10, self.windowHeight-30)
        self.stbmp1.SetBitmap(newbitmap)

class App(wx.App):
    """Application class."""

    def OnInit(self):
        self.frame = TestOpenCV()
        self.frame.Show()
        self.SetTopWindow(self.frame)
        self.run()
        return True

    def rot90(self, img, angle):
        if(angle == 270 or angle == -90):
            img = cv2.transpose(img)
            img = cv2.flip(img, 0)  # transpose+flip(0)=CCW
        elif (angle == 180 or angle == -180):
            img = cv2.flip(img, -1)  # transpose+flip(-1)=180
        elif (angle == 90 or angle == -270):
            img = cv2.transpose(img)
            img = cv2.flip(img, 1)  # transpose+flip(1)=CW
        elif (angle == 360 or angle == 0 or angle == -360):
            pass
        else :
            raise Exception("Unknown rotation angle({})".format(angle))
        return img

    def run(self):

        cap = cv2.VideoCapture(0);

        while True:
            ret, frame = cap.read()

            if ret == True:
                # 畫面旋轉 90度
                srcBGR = self.rot90(frame, -90)

                # wxPython 只能處理 RGB 的圖片,要從 BGR 轉 RGB
                srcRGB = cv2.cvtColor(srcBGR, cv2.COLOR_BGR2RGB)

                #print dst.shape  w=720, h=1280
                w, h = srcRGB.shape[:2]

                #dst = cv2.resize(srcRGB, (h/2,w/2), interpolation = cv2.INTER_AREA)
                #wxImage = wx.ImageFromBuffer(h/2, w/2, dst)
                wxImage = wx.ImageFromBuffer(h, w, srcRGB)
                bitmap = wx.BitmapFromImage(wxImage)

                # 更新 視窗上的圖片
                self.frame.updateImage(bitmap)

                #cv2.imshow('frame', dst)
                #if cv2.waitKey(30) & 0xFF == ord('q'):
                #    break

                # sleep 30ms
                time.sleep(0.03)

            else:
                break

        cap.release()
        cv2.destroyAllWindows()


def main():
    app = App()
    app.MainLoop()

if __name__ == '__main__':
    main()

2017年7月24日

安裝 OpenCV 3

OpenCV Open Source Computer Vision Library 是一個跨平台的電腦視覺庫,由 Intel 發起並參與開發,由於 Intel 為了推動需要更高速運算的應用,增加硬體的銷售,因此發展了這個機器視覺的運算函式庫,以BSD授權條款授權發行,可以在商業和研究領域中免費使用。OpenCV可用於開發 real time 的圖像處理、電腦視覺以及特徵識別程式。

OpenCV 雖然是以 C++ 寫成,但同時提供了 python 及 java 的 bindings,也因為 python,我們可以用更短的程式碼就完成一些基本的視覺應用,以下紀錄如何在 RPi 3 以及 MacOS 中安裝 OpenCV。

Raspberry Pi 3

RPi 的 camera 是使用 CSI 介面,參考 Raspberry Pi相機模組開箱文 將 RPi 的 camera 裝好。

以 raspi-config 指令 enable camera,然後 reboot。

sudo raspi-config

vcgencmd 是用來查詢一些系統參數的指令,可以用 vcgencmd 測試 camera 狀態。

$ vcgencmd get_camera
supported=1 detected=1
vcgencmd version
vcgencmd get_mem arm
vcgencmd get_mem gpu

安裝 OpenCV 3,必須先更新 RPi,安裝一些基本的工具及 library

sudo apt-get update
sudo apt-get upgrade
sudo apt-get -y install build-essential cmake pkg-config
sudo apt-get -y install cmake-curses-gui htop swig

# load various image file formats from disk
sudo apt-get -y install libjpeg-dev libtiff5-dev libjasper-dev libpng12-dev

#sudo apt-get -y install build-essential cmake cmake-curses-gui pkg-config libpng12-0 libpng12-dev libpng++-dev libpng3 libpnglite-dev zlib1g-dbg zlib1g zlib1g-dev pngtools libtiff5-dev libtiff5 libtiffxx0c2 libtiff-tools libeigen3-dev

#sudo apt-get -y install libjpeg8 libjpeg8-dev libjpeg8-dbg libjpeg-progs swig libv4l-0 libv4l-dev python-numpy

# read various video file formats from disk
sudo apt-get -y install libavcodec-dev libavformat-dev libswscale-dev libv4l-dev
sudo apt-get -y install libxvidcore-dev libx264-dev

# for module: highgui
sudo apt-get -y install libgtk2.0-dev

# matrix operations
sudo apt-get -y install libatlas-base-dev gfortran

# python
sudo apt-get -y install python2.7-dev python3-dev python-numpy python3-numpy

sudo apt-get -y install doxygen

如果需要 tesseract,不安裝也沒關係,可以直接用 pytesseract:

sudo apt-get install -y tesseract-ocr libtesseract-dev libleptonica-dev

安裝 python library

wget https://bootstrap.pypa.io/get-pip.py
sudo python get-pip.py

# for numerical processing
sudo pip install numpy

下載並安裝 OpenCV 3.1

wget -O opencv-3.1.0.zip https://github.com/Itseez/opencv/archive/3.1.0.zip

wget -O opencv_contrib-3.1.0.zip https://github.com/opencv/opencv_contrib/archive/3.1.0.zip

unzip opencv-3.1.0.zip
unzip opencv_contrib-3.1.0.zip

因為 Opencv + Tesseract 編譯會發生問題,所以要把 OpenCV 偵測 Tesseract library 的部分關閉,未來直接用 pytesseract 就可以做 OCR,不用透過 OpenCV 呼叫 tesseract。

#修改 /opt/opencv_contrib-3.1.0/modules/text/FindTesseract.cmake

#增加最後一行
set(Tesseract_FOUND 0)

編譯 opencv

cd opencv-3.1.0/
mkdir build
cd build

# setup build with cmake 或是以 ccmake ../ 用介面設定 build
cmake -D CMAKE_BUILD_TYPE=RELEASE \
    -D CMAKE_INSTALL_PREFIX=/usr/local \
    -D INSTALL_C_EXAMPLES=OFF \
    -D INSTALL_PYTHON_EXAMPLES=ON \
    -D OPENCV_EXTRA_MODULES_PATH=~/opencv/opencv_contrib-3.1.0/modules \
    -D BUILD_EXAMPLES=ON ..

如果不直接用 cmake,也可以用 ccmake gui 設定 cmake 的選項

ccmake ../

# 按 c 產生全新設定,決定細項設定後,修改完後,按 c 設定新的選項,然後再按下 g 即可產生編譯用的設定檔案。

如果 cmake 時會出現這個錯誤

CMake Error at samples/gpu/CMakeLists.txt:100 (list):
  list sub-command REMOVE_ITEM requires list to be present.

參考這裡的解法,要將 INSTALLCEXAMPLES=OFF 設定為 OFF。

# 這個步驟會要做很久,大概是 80 分鐘,如果一直發生問題,就改用 make 去掉 -j4,但會需要 3~4 hrs
# 如果 make -j4 出現 error,可以再用 make -j4 或是改用 make -j2 多試幾次看看,如果錯誤沒有出現在同一個地方,,可以這樣繼續編譯
make -j2

也可以用這樣的方式重複 10 次 make

for i in {1..10}; do make -j2; done

或是用 shell script

#!/bin/bash
for i in {1..10}
do
    make -j2
    if [ $? -ne 0 ]; then
        echo "Try again";
    else
        break;
    fi
done
sudo make install
sudo ldconfig

在 python 的 dist-packages 目錄中,看到 cv2.so 就成功了。

ls -l /usr/local/lib/python2.7/dist-packages/

以 python 測試 cv2

$ python
>>> import cv2
>>> cv2.__version__
'3.1.0'

測試拍照及錄影

raspistill -o t.jpg
raspivid -o t.h264

要讓 OpenCV 使用 camera 必須安裝V4L2套件,在 RPi 必須要先載入 video driver for cv video capture method,camera 的程式才會有作用。

可以直接編譯

cd ~/opencv/
wget http://linuxtv.org/downloads/v4l-utils/v4l-utils-1.6.2.tar.bz2
tar xfvj v4l-utils-1.6.2.tar.bz2

sudo apt-get -y install autoconf gettext libtool libjpeg-dev

cd v4l-utils-1.6.2
autoreconf -vfi
./configure
make
sudo make install

或是用這樣的方式安裝

# 增加sources.list
$ sudo vim /etc/apt/sources.list
於sources.list中寫入以下資訊
deb http://www.linux-projects.org/listing/uv4l_repo/raspbian/ wheezy main

# 加入GPG key
$ sudo wget http://www.linux-projects.org/listing/uv4l_repo/lrkey.asc ~/
$ sudo apt-key add ./lrkey.asc

# 再更新一次系統
$ sudo apt-get update && sudo apt-get upgrade

# 安裝V4L2套件
$ sudo apt-get install uv4l uv4l-raspicam
sudo modprobe bcm2835-v4l2

在modules文件中寫入以下資訊,下次開機就會自動載入這個 module

$ sudo vim /etc/modules
bcm2835-v4l2

測試 /dev/video0

v4l2-ctl --list-ctrls --device /dev/video0

Mac OS El Caption

用類似 RPi 的方式編譯,因為 OpenCV 3.2 會遇到 freetype 編譯錯誤,還不知道怎麼解決,目前只能使用 OpenCV 3.1。另外 python 整合的部分也沒有裝好。所以就不用這樣的方式,改用 macport 直接安裝 opencv +python27。

#安裝 macport, xcode

sudo xcodebuild -license
sudo port install cmake +gui

sudo port install libgphoto2
sudo port install jpeg libpng tiff openexr
sudo port install eigen tbb eigen3
sudo port install py27-numpy

unzip opencv-3.1.0.zip
unzip opencv_contrib-3.1.0.zip

cd opencv-3.1.0
mkdir build
cd build

cmake -D CMAKE_BUILD_TYPE=Release \
    -D CMAKE_INSTALL_PREFIX=/usr/local \
    -D OPENCV_EXTRA_MODULES_PATH=~/project/opencv/opencv_contrib-3.1.0/modules \
    -D BUILD_opencv_python2=ON \
    -D BUILD_opencv_python3=OFF \
    -D INSTALL_PYTHON_EXAMPLES=ON \
    -D INSTALL_C_EXAMPLES=OFF \
    -D BUILD_EXAMPLES=ON \
    -D WITH_EIGEN=OFF \
    -D PYTHON2_PACKAGES_PATH=/opt/local/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/site-packages \
    -D PYTHON2_LIBRARY=/opt/local/Library/Frameworks/Python.framework/Versions/2.7/bin \
    -D PYTHON2_INCLUDE_DIR=/opt/local/Library/Frameworks/Python.framework/Versions/2.7/include/python2.7/ \
    ..

make -j4

sudo make install

改用 macport 直接安裝的方式

# 安裝 xcode
xcode-select -install

sudo port selfupdate

sudo port install py27-tkinter
sudo port install cmake +gui
sudo port install py27-scipy

# 設定 python
sudo port install python_select
sudo port slect python python27

# 編譯 opencv
sudo port install opencv +python27 +openni

可以在編譯 opencv 的 Porfile 中查看一些資料,目前是用 3.1.0 版,也已經包含了 opencv_contrib modules,如果真的需要調整編譯的過程,可以設定 macport 的 local repository,複製這個 Portfile,修改編譯的過程。

/opt/local/var/macports/sources/rsync.macports.org/release/tarballs/ports/graphics/opencv/Portfile

測試 1: 打開 1.jpg 顯示在視窗畫面中

test.cpp: 打開 1.jpg 顯示在視窗畫面中

#include <opencv2/opencv.hpp>
using namespace cv;

int main()
{
        Mat img=imread("1.jpg");
        imshow("result",img);
        // 6s後視窗自動關閉
        waitKey(6000);
}

編譯與執行

# 在 mac 編譯
g++ -ggdb `pkg-config --cflags --libs opencv` -stdlib=libc++ test.cpp -o test

# 在 RPi 編譯
g++ -ggdb `pkg-config --cflags --libs opencv` test.cpp -o test

./test

一樣的程式碼,改用 python 寫:test.py

#!/usr/bin/python
# coding=utf-8

import cv2
import time

img = cv2.imread('1.jpg');
cv2.imshow("result",img);
cv2.waitKey()

直接用 python 就可以執行,在 Mac 及 RPi 都一樣。

python test.py

測試 2: 載入圖片, 並用 MORPH_RECT 腐蝕操作

用圖片中暗色的部分,腐蝕高亮的部分。

test2.cpp

#include <opencv2/highgui/highgui.hpp>
#include <opencv2/imgproc/imgproc.hpp>

using namespace cv;

int main(   )
{
    //載入原圖
    Mat srcImage = imread("1.jpg");
    //顯示原圖
    imshow("source", srcImage);
    //進行腐蝕操作
    Mat element = getStructuringElement(MORPH_RECT, Size(15, 15));
    Mat dstImage;
    erode(srcImage, dstImage, element);
    //顯示效果圖
    imshow("result", dstImage);
    waitKey(0);

    return 0;
}

編譯+執行

# 在 mac 編譯
g++ -ggdb `pkg-config --cflags --libs opencv` -stdlib=libc++  test2.cpp -o test2

# 在 RPi 編譯
g++ -ggdb `pkg-config --cflags --libs opencv` test2.cpp -o test2

./test2

test2.py

#!/usr/bin/python
# coding=utf-8

import cv2
import time

# 中文字型
from matplotlib.font_manager import FontProperties
font = FontProperties(fname=r"SimSun.ttc", size=14)

img = cv2.imread('1.jpg');
# grayscale image
#img = cv2.imread('1.jpg', 0);
#img = cv2.imread('1.jpg', cv2.IMREAD_GRAYSCALE)

kernel = cv2.getStructuringElement(cv2.MORPH_RECT,(5,5))
#腐蝕圖像  
eroded = cv2.erode(img,kernel)

#顯示腐蝕後的圖片
cv2.imshow("result", eroded);
cv2.waitKey(0)
cv2.destroyAllWindows()

編譯+執行

python test2.py

測試 3: blur 影像模糊

test3.cpp

#include "opencv2/highgui/highgui.hpp"
#include "opencv2/imgproc/imgproc.hpp"
using namespace cv;

int main( )
{
    Mat srcImage=imread("1.jpg");
    imshow( "source", srcImage );

    Mat dstImage;
    blur( srcImage, dstImage, Size(7, 7));

    imshow( "result" ,dstImage );

    waitKey( 0 );
}

編譯+執行

# 在 mac 編譯
g++ -ggdb `pkg-config --cflags --libs opencv` -stdlib=libc++ test3.cpp -o test3
./test3

# 在 RPi 編譯
g++ -ggdb `pkg-config --cflags --libs opencv` test3.cpp -o test3

./test3

tes3.py

#!/usr/bin/python
# coding=utf-8

import cv2
import time

img = cv2.imread('1.jpg');

blur = cv2.blur(img,(7,7))

cv2.imshow("result", blur);
cv2.waitKey(0)
cv2.destroyAllWindows()

測試 4: canny 邊緣檢測

test4.cpp

#include <opencv2/opencv.hpp>
#include<opencv2/imgproc/imgproc.hpp>
using namespace cv;

int main( )
{
    Mat srcImage = imread("1.jpg");
    imshow("source", srcImage);     //顯示原始圖
    Mat dstImage,edge,grayImage;    //參數定義

    //建立與src同類別型和大小的矩陣(dst)
    dstImage.create( srcImage.size(), srcImage.type() );

    //將原圖像轉換為灰度圖像
    //此句程式碼的OpenCV2版為:
    //cvtColor( srcImage, grayImage, CV_BGR2GRAY );
    //此句程式碼的OpenCV3版為:
    cvtColor( srcImage, grayImage, COLOR_BGR2GRAY );

    // 使用 3x3核心來降噪
    blur( grayImage, edge, Size(3,3) );

    // 執行Canny算子
    Canny( edge, edge, 3, 9,3 );

    imshow("result", edge);

    waitKey(0);

    return 0;
}

編譯+執行

# 在 mac 編譯
g++ -ggdb `pkg-config --cflags --libs opencv` -stdlib=libc++ test4.cpp -o test4
./test4

# 在 RPi 編譯
g++ -ggdb `pkg-config --cflags --libs opencv` test4.cpp -o test4

./test4

test4.py

#!/usr/bin/python
# coding=utf-8

import cv2
import time

# grayscale image
img = cv2.imread('1.jpg', 0);

# 用高斯平滑處理原圖像降噪
img = cv2.GaussianBlur(img,(3,3),0)
# Canny只能處理 grayscale image, 指定最大和最小閾值,其中apertureSize默認為3
canny = cv2.Canny(img, 3, 9)

cv2.imshow("result", canny);
cv2.waitKey(0)
cv2.destroyAllWindows()

test5: 播放 avi 影片

test5.cpp

#include <opencv2/opencv.hpp>
using namespace cv;

//-----------------------------------【main( )函數】--------------------------------------------
//      描述:控制臺應用程式的入口函數,我們的程式從這里開始
//-------------------------------------------------------------------------------------------------
int main( )
{
    VideoCapture capture("1.avi");

    while(1)
    {
        Mat frame;//定義一個Mat變數, 儲存現在的 frame
        capture>>frame;  //讀取現在的 frame
        if (!frame.empty()) {
            imshow("result",frame);
        } else {
            break;
        }
        waitKey(30);  // delay 30ms
    }
    return 0;
}

編譯+執行

# 在 mac 編譯
g++ -ggdb `pkg-config --cflags --libs opencv` -stdlib=libc++ test5.cpp -o test5
./test5

# 在 RPi 編譯
g++ -ggdb `pkg-config --cflags --libs opencv` test5.cpp -o test5

./test5

test5.py

#!/usr/bin/python
# coding=utf-8

import cv2
import time

cap = cv2.VideoCapture('1.avi');

while True:
    ret, frame = cap.read()

    if ret == True:
        ## grayscale avi
        #gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
        #cv2.imshow('frame', gray)

        ## normal avi
        cv2.imshow('frame', frame)

        if cv2.waitKey(30) & 0xFF == ord('q'):
            break

    else:
        break

cap.release()
cv2.destroyAllWindows()

test6: 使用 camera

test6.cpp,只有將 VideoCapture capture(0); 換成 camera index 就可以了。

#include <opencv2/opencv.hpp>
using namespace cv;

int main( )
{
    VideoCapture capture(0);
    while(1)
    {
        Mat frame, dst;
        capture>>frame;

        if (!frame.empty()) {
            imshow("result", frame);
        } else {
            break;
        }
        waitKey(30);  //delay 30ms
    }
    return 0;
}

編譯+執行

# 在 mac 編譯
g++ -ggdb `pkg-config --cflags --libs opencv` -stdlib=libc++ test6.cpp -o test6
./test6

# 在 RPi 編譯
g++ -ggdb `pkg-config --cflags --libs opencv` test6.cpp -o test6

./test6

test6.py

#!/usr/bin/python
# coding=utf-8

import cv2
import time

cap = cv2.VideoCapture(0);

while True:
    ret, frame = cap.read()

    if ret == True:
        ## grayscale avi
        #gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
        #cv2.imshow('frame', gray)

        ## normal avi
        cv2.imshow('frame', frame)

        if cv2.waitKey(30) & 0xFF == ord('q'):
            break

    else:
        break

cap.release()
cv2.destroyAllWindows()

test7: 使用 camera 加上畫面旋轉

上面的 test6 在 mac 的 camera 角度不對,畫面必須逆時針旋轉一下,才回變成正面。

#include <opencv2/opencv.hpp>
using namespace cv;

void rotate_90n(cv::Mat const &src, cv::Mat &dst, int angle)
{
     CV_Assert(angle % 90 == 0 && angle <= 360 && angle >= -360);
     if(angle == 270 || angle == -90){
        // Rotate clockwise 270 degrees
        cv::transpose(src, dst);
        cv::flip(dst, dst, 0);
    }else if(angle == 180 || angle == -180){
        // Rotate clockwise 180 degrees
        cv::flip(src, dst, -1);
    }else if(angle == 90 || angle == -270){
        // Rotate clockwise 90 degrees
        cv::transpose(src, dst);
        cv::flip(dst, dst, 1);
    }else if(angle == 360 || angle == 0 || angle == -360){
        if(src.data != dst.data){
            src.copyTo(dst);
        }
    }
}

int main( )
{
    VideoCapture capture(0);
    while(1)
    {
        Mat frame, dst;
        capture>>frame;

        if (!frame.empty()) {

            rotate_90n(frame, dst, -90);
            imshow("result", dst);
        } else {
            break;
        }
        waitKey(30);  //delay 30ms
    }
    return 0;
}

編譯+執行

# 在 mac 編譯
g++ -ggdb `pkg-config --cflags --libs opencv` -stdlib=libc++ test7.cpp -o test7
./test7

# 在 RPi 編譯
g++ -ggdb `pkg-config --cflags --libs opencv` test7.cpp -o test7

./test7

test7.py

#!/usr/bin/python
# coding=utf-8

import cv2
import time

def rot90(img, angle):
    if(angle == 270 or angle == -90):
        img = cv2.transpose(img)
        img = cv2.flip(img, 0)  # transpose+flip(0)=CCW
    elif (angle == 180 or angle == -180):
        img = cv2.flip(img, -1)  # transpose+flip(-1)=180
    elif (angle == 90 or angle == -270):
        img = cv2.transpose(img)
        img = cv2.flip(img, 1)  # transpose+flip(1)=CW
    elif (angle == 360 or angle == 0 or angle == -360):
        pass
    else :
        raise Exception("Unknown rotation angle({})".format(angle))
    return img

cap = cv2.VideoCapture(0);

while True:
    ret, frame = cap.read()

    if ret == True:
        ## grayscale avi
        #gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
        #cv2.imshow('frame', gray)

        ## normal avi
        dst = rot90(frame, -90)
        cv2.imshow('frame', dst)

        if cv2.waitKey(30) & 0xFF == ord('q'):
            break

    else:
        break

cap.release()
cv2.destroyAllWindows()

References

Install OpenCV-Python in Windows

Computer Vision with Raspberry Pi and the Camera Pi module

使用 vcgencmd 指令查看 Raspberry Pi 的 CPU 溫度、運行速度與電壓等資訊

在 Raspberry Pi 上面安裝 OpenCV 函式庫

安裝 OPENCV 紀錄

Install guide: Raspberry Pi 3 + Raspbian Jessie + OpenCV 3

[翻译]Python 2.7 和 Python 3+ 的OpenCV 3.0 安装教程

[Raspberry Pi] 解決 Raspberry Pi 找不到 /dev/video0

OpenCV on Raspberry Pi - Using Java(6)- 使用 OpenCV 拍攝照片(Camera Module)


Installing OpenCV in Mac OSx tutorial

macOS: Install OpenCV 3 and Python 2.7

Mac下安装OpenCV3.0—包含opencv_contrib模块

Undefined freetype symbols when building openCV 3.2.0


How to compile OpenCV sample code ?

【OpenCV】安裝在Mac及XCode筆記

VideoCapture.open(0) won't recognize pi cam

Rotate image by 90, 180 or 270 degrees

2017年7月17日

使用 Tsung 測試 websocket

tsung是erlang開發的多協議分佈式負載測試工具,它能用來壓力測試HTTP, WebDAV, SOAP, PostgreSQL, MySQL, LDAP, webscoket 和 Jabber/XMPP 的 server。

安裝 Tsung

在 mac 安裝 tsung 可使用 macport

sudo port install tsung

安裝成功後,以 -v 測試

$ tsung -v
Tsung version 1.6.0

在這個路徑下面,有一些 perl script,接下來可以利用這些 script 產生報表

$ $ ls -m /opt/local/lib/tsung/bin/
log2tsung.pl, tsung-rrd.pl, tsung_percentile.pl, tsung_stats.pl

範例的路徑

/opt/local/share/doc/tsung/examples

我們可以將 websocket.xml 範例複製出來修改。

cp /opt/local/share/doc/tsung/examples/websocket.xml .

撰寫 test scenario

修改從範例複製的 websocket.xml

<?xml version="1.0"?>
<!DOCTYPE tsung SYSTEM "/opt/local/share/tsung/tsung-1.0.dtd" []>
<tsung loglevel="notice" version="1.0">
  <clients>
    <client host="localhost" use_controller_vm="true" maxusers="1000" />
  </clients>

  <servers>
    <server host="127.0.0.1" port="9000" type="tcp" />
  </servers>

  <load>
    <!-- phase 1: 0.1 usr per second in 10 minutes, max: 1000 -->
    <arrivalphase phase="1" duration="10" unit="minute">
      <users maxnumber="1000" arrivalrate="0.1" unit="second" />
    </arrivalphase>

    <!-- phase 2: 1 new user every second in 10 minutes -->
    <!--
    <arrivalphase phase="2" duration="10" unit="minute">
      <users interarrival="1" unit="second"></users>
    </arrivalphase>
    -->

    <!-- phase 3: 1 new usr every 0.1 second in 10 minutes -->
    <!--
    <arrivalphase phase="3" duration="10" unit="minute">
      <users interarrival="0.1" unit="second"></users>
    </arrivalphase>
    -->

    <!-- phase 4: 10 usrs per second in 10 minutes, the same as phase 3, max: 100 -->
    <!--
    <arrivalphase phase="4" duration="10" unit="minute">
      <users maxnumber="100" arrivalrate="10" unit="second"></users>
    </arrivalphase>
    -->
  </load>

  <options>
    <option name="file_server" id='userdb' value="usr.csv"/>
  </options>

  <sessions>
    <session name="websocket" probability="100" type="ts_websocket">
        <setdynvars sourcetype="file" fileid="userdb" delimiter=";" order="iter">
          <var name="usrid"/>
          <var name="pwd"/>
        </setdynvars>

        <request subst="true">
             <websocket type="connect" path="/msgsrv/ws"></websocket>
        </request>

        <thinktime min="2" max="5" random="true"></thinktime>

        <request subst="true">

            <websocket type="message">
{
  "task":1,
  "action":1,
  "serial":"01-01-%%_usrid%%",
  "rdata":{
    "userid":"%%_usrid%%",
    "userpwd":"%%_pwd%%"
  }
}
            </websocket>
        </request>

        <thinktime min="3" max="10" random="true"></thinktime>

        <request>
            <websocket type="message" >
{
  "task":3,
  "action":2,
  "serial":"03-02-%%_usrid%%",
  "rdata":{
    "areaseq":1
  }
}
            </websocket>
        </request>

        <thinktime value="30"></thinktime>

        <request>
            <websocket type="close"></websocket>
        </request>
    </session>
  </sessions>
</tsung>

以下說明每個部分的設定:

load 的部分是新連線產生的速度,下面有四個 phases,分別有不同的連線產生的速度

<load>
    <!-- phase 1: 0.1 usr per second in 10 minutes, max: 1000 -->
    <arrivalphase phase="1" duration="10" unit="minute">
      <users maxnumber="1000" arrivalrate="0.1" unit="second" />
    </arrivalphase>

    <!-- phase 2: 1 new user every second in 10 minutes -->
    <arrivalphase phase="2" duration="10" unit="minute">
      <users interarrival="1" unit="second"></users>
    </arrivalphase>

    <!-- phase 3: 1 new usr every 0.1 second in 10 minutes -->
    <arrivalphase phase="3" duration="10" unit="minute">
      <users interarrival="0.1" unit="second"></users>
    </arrivalphase>

    <!-- phase 4: 10 usrs per second in 10 minutes, the same as phase 3, max: 100 -->
    <arrivalphase phase="4" duration="10" unit="minute">
      <users maxnumber="100" arrivalrate="10" unit="second"></users>
    </arrivalphase>
  </load>

option 是定義一個外部 csv 檔案的 id,給後面參考使用

  <options>
    <option name="file_server" id='userdb' value="usr.csv"/>
  </options>

session 的一開始,先取得 userdb 對應的 csv 檔案,並取得兩欄資料,對應到 usrid 及 pwd 兩個變數。後面在 request 的地方,可以用 %%_usrid%% 參考到 csv 裡面的變數。

<session name="websocket" probability="100" type="ts_websocket">
        <setdynvars sourcetype="file" fileid="userdb" delimiter=";" order="iter">
          <var name="usrid"/>
          <var name="pwd"/>
        </setdynvars>

        <request subst="true">
             <websocket type="connect" path="/msgsrv/ws"></websocket>
        </request>

        <thinktime min="2" max="5" random="true"></thinktime>

        <request subst="true">

            <websocket type="message">
{
  "task":1,
  "action":1,
  "serial":"01-01-%%_usrid%%",
  "rdata":{
    "userid":"%%_usrid%%",
    "userpwd":"%%_pwd%%"
  }
}
            </websocket>
        </request>

        <thinktime min="3" max="10" random="true"></thinktime>
</session>

usr.csv 裡面就放兩欄,分別是 usrid 及 pwd

u0001;u0001pwd
u0002;u0002pwd
u0003;u0003pwd

進行 tsung 測試

啟動 tsung

tsung -f websocket.xml -l log start

產生的報告會放在 log 目錄下面,一個日期 timestamp的目錄中

tail -f log/20170324-1531/tsung.log

可以用 tsung_stats.pl 產生 html 的 report

mkdir report

cd report

/opt/local/lib/tsung/bin/tsung_stats.pl --stats ../log/20170324-1531/tsung.log chromium graph.html

最後可以看到一些統計報表

References

Install tsung centos 7

installing tsung in centos

CentOS下安裝 Tsung-壓力測試工具

CentOS壓力測試工具Tsung安裝和圖形報表生成

詳解CentOS下Tsung環境的搭建到跑通第一個測試用例

Load Testing using Tsung

tsung的使用筆記

Tsung’s documentation

tsung測試openfire時從CSV文件讀取user信息

2017年7月3日

使用 vim Plugins 改造 vim 為開發的 IDE

在使用 linux 一定知道要用 vim,vim 提供了很多 plugins 讓我們可以調整 vim,而一個大型的 programming IDE,核心也是從文字編輯器開始,延伸其他的功能,合併成一個完整的 IDE。

這麼多 plugins,如果要瞭解內容跟細節,還是一個一個慢慢安裝,閱讀文件才會比較清楚該 plugin 能做什麼事情,不過也要花時間慢慢去調整出適合自己的工作環境。

利用 vundle 快速設定 vim

ref: Configuring Vim as an IDE

Vundle是vim plugin的管理工具,利用 vundle 可以快速將很多 plugins 一次安裝起來。

首先將以下文件內容寫入 ~/.vimrc

syntax on

set nocompatible
set smartindent
set shiftwidth=4
set backspace=indent,eol,start
set ruler
set number
set showcmd
set incsearch
set hlsearch
set hls
set ic
set pastetoggle=<F12>
set enc=utf8

set mouse=a

filetype off 

" set the runtime path to include Vundle and initialize
set rtp+=~/.vim/bundle/Vundle.vim



"-------------- PLUGINS STARTS -----------------
call vundle#begin()

Plugin 'VundleVim/Vundle.vim'
Plugin 'vim-airline/vim-airline'
Plugin 'vim-airline/vim-airline-themes'
Plugin 'altercation/vim-colors-solarized'
Plugin 'scrooloose/nerdtree'
Plugin 'jistr/vim-nerdtree-tabs'
Plugin 'scrooloose/syntastic'
Plugin 'xolox/vim-misc'
Plugin 'xolox/vim-easytags'
Plugin 'majutsushi/tagbar'
Plugin 'ctrlpvim/ctrlp.vim'
Plugin 'vim-scripts/a.vim'
Plugin 'airblade/vim-gitgutter'
Plugin 'tpope/vim-fugitive'
Plugin 'Raimondi/delimitMate'
Plugin 'christoomey/vim-tmux-navigator'
Plugin 'jez/vim-c0'
Plugin 'jez/vim-ispc'
Plugin 'kchmck/vim-coffee-script'
Plugin 'flazz/vim-colorschemes'

call vundle#end()  
"-------------- PLUGINS END --------------------
filetype plugin indent on



"----- GENERAL SETTINGS-------
set laststatus=2
let g:airline_powerline_fonts = 1
let g:airline_detect_paste=1
let g:airline#extensions#tabline#enabled = 1
let g:airline_theme='solarized'
set background=dark
let g:solarized_termcolors=256
colorscheme solarized


"---------NERD-TREE SETTINGS----------
nmap <silent> <leader>t :NERDTreeTabsToggle<CR>
let g:nerdtree_tabs_open_on_console_startup = 1


"-------- SYNTASTIC SETTINGS---------
let g:syntastic_error_symbol = '✘'
let g:syntastic_warning_symbol = "▲"

augroup mySyntastic
    au!
    au FileType tex let b:syntastic_mode = "passive"
augroup END


"-------- TAGS SETTINGS --------------------------------
let g:easytags_events = ['BufReadPost', 'BufWritePost']
let g:easytags_async = 1
let g:easytags_dynamic_files = 2
let g:easytags_resolve_links = 1
let g:easytags_suppress_ctags_warning = 1
let g:tagbar_autoclose=2

nmap <silent> <leader>b :TagbarToggle<CR>
"autocmd BufEnter * nested :call tagbar#autoopen(0)

"---------GIT SETTINGS--------------
hi clear SignColumn
let g:airline#extensions#hunks#non_zero_only = 1


"----------DELIMITEMATE SETTINGS-----------------
let delimitMate_expand_cr = 1
augroup mydelimitMate
    au!
    au FileType markdown let b:delimitMate_nesting_quotes = ["`"]
    au FileType tex let b:delimitMate_quotes = ""
    au FileType tex let b:delimitMate_matchpairs = "(:),[:],{:},`:'"
    au FileType python let b:delimitMate_nesting_quotes = ['"', "'"]
augroup END

"-----------TMUX SETTINGS--------------
let g:tmux_navigator_save_on_switch = 2

以 git 安裝 vundle

git clone https://github.com/VundleVim/Vundle.vim.git ~/.vim/bundle/Vundle.vim

在 CentOS 要安裝 ctags

yum install ctags

如果是 Debian 則是

sudo apt-get install exuberant-ctags

安裝字型 Menlo-for-Powerline

git clone https://github.com/abertsch/Menlo-for-Powerline.git

mkdir ~/.fonts
cd Menlo-for-Powerline/
cp "Menlo for Powerline.ttf" ~/.fonts/
fc-cache -vf ~/.fonts

安裝 color solarized

cd ~/.vim/bundle
git clone git://github.com/altercation/vim-colors-solarized.git

最後在 console 執行,花一點時間,就可以安裝 .vimrc 指定的所有 plugins

vim +PluginInstall +qall

透過以上方式,就安裝了

  1. Vundle
  2. vim-airline
  3. vim-airline-themes
  4. vim-colors-solarized
  5. nerdtree
  6. vim-nerdtree-tabs
  7. syntastic
  8. vim-misc
  9. vim-easytags
  10. ctrlp.vim
  11. a.vim
  12. vim-gitgutter
  13. vim-fugitive
  14. delimitMate
  15. vim-tmux-navigator
  16. vim-colorschemes

NERDTree

ref: 上古神器vim插件:你真的學會用NERDTree了嗎?

NERDTree 是安裝後進入 vim,馬上就會看到的介面,他是一個檔案管理員的功能。

使用 NERDTree 搭配 nerdtree-tabs,感覺就比較接近一般的 IDE。

NERDTree 的指令有這些,但其實只要用滑鼠,點擊目錄跟檔案,也可以打開檔案

?: 快速幫助文檔
o: 打開一個目錄或者打開文件,創建的是buffer,也可以用來打開書籤
go: 打開一個文件,但是光標仍然留在NERDTree,創建的是buffer
t: 打開一個文件,創建的是Tab,對書籤同樣生效
T: 打開一個文件,但是光標仍然留在NERDTree,創建的是Tab,對書籤同樣生效
i: 水平分割創建文件的窗口,創建的是buffer
gi: 水平分割創建文件的窗口,但是光標仍然留在NERDTree
s: 垂直分割創建文件的窗口,創建的是buffer
gs: 和gi,go類似
x: 收起當前打開的目錄
X: 收起所有打開的目錄
e: 以文件管理的方式打開選中的目錄
D: 刪除書籤
P: 大寫,跳轉到當前根路徑
p: 小寫,跳轉到光標所在的上一級路徑
K: 跳轉到第一個子路徑
J: 跳轉到最後一個子路徑
<C-j>和<C-k>: 在同級目錄和文件間移動,忽略子目錄和子文件
C: 將根路徑設置為光標所在的目錄
u: 設置上級目錄為根路徑
U: 設置上級目錄為跟路徑,但是維持原來目錄打開的狀態
r: 刷新光標所在的目錄
R: 刷新當前根路徑
I: 顯示或者不顯示隱藏文件
f: 打開和關閉文件過濾器
q: 關閉NERDTree
A: 全屏顯示NERDTree,或者關閉全屏

Java, Scala, Python...

以 java 開發為例,最基本就是要在編輯 .java 檔案時,填寫 System. 的時候,就要自動出現該 package 可以填寫的 package or class。

在 .vimrc plugins 區塊,call vundle#end() 的前面加上

" java import
Plugin 'javaimp.vim'
" java auto complete
Plugin 'javacomplete'

.vimrc 最後面加上 javacomplete 的設定

" ========== omnifunc:javacomplete 自動補全功能 =========
" 設定此行在 java 檔案中,就可按(ctrl + x) + (ctrl + o) 自動補全
setlocal omnifunc=javacomplete#Complete
" 當檔案為副檔名為 java 動作
" mode的狀態下,按"."會替換成以下指令,換言之,與ide相同當按"."會自動補全
autocmd Filetype java,jsp set omnifunc=javacomplete#Complete
autocmd Filetype java,jsp set completefunc=javacomplete#CompleteParamsInf
autocmd Filetype java,jsp inoremap <buffer> . .<C-X><C-O><C-P><DOWN>
" 設定額外 include 的 classpath
" let b:classpath="/opt/apache-tomcat-8.0.30/lib/*"

" ========== omnifunc:javacomplete2 自動補全功能 =========
autocmd FileType java setlocal omnifunc=javacomplete#Complete
let g:JavaComplete_MavenRepositoryDisable = 1
let g:JavaComplete_UseFQN = 1
let g:JavaComplete_ClosingBrace = 1
let g:JavaComplete_JavaviDebug = 1
let g:JavaComplete_ImportDefault = 0

再安裝一次

vim +PluginInstall +qall

編輯 .java 就可以出現這樣的功能

References

Scala development in Vim

Coding Scala with Vim

vim plugin 推薦 (For python and java )

用 Vim 寫 JAVA - 環境建立 與 Eclim

Coding Java with Vim, 打造自己的工作環境

vim 使用 javacomplete2 自動補齊功能,只有 root 權限能完全工作問題

vim plugin 推薦 (For python and java )

2017年6月28日

解決 homebrew 修正成不用 root 身份執行後,mysql 無法啟動的問題

最近手邊要管的機器太多了,想試試用 Ansible 來處理,查了一下如何安裝,網路上是推薦用 homebrew 來裝最簡單,但是在使用很久以前安裝的 homebrew 時,出現了以下錯誤訊息:

$ sudo brew update
Error: Running Homebrew as root is extremely dangerous and no longer supported.
As Homebrew does not drop privileges on installation you would be giving all
build scripts full access to your system.

上網查了一下相關討論,大意就是 homebrew 不要讓你用 root 執行了,網路上找到的解決辦法就是,更改 /usr/local 的權限,如下:

sudo chown -R $(whoami) /usr/local

這樣一來,homebrew 就能正常運作了,執行以下指令就不會出錯,再也不需要加上 sudo 指令了:

brew update

但是問題來了,如果原本透過 .pkg 檔案來安裝 MySQL 的話,這時 MySQL 就會無法使用了!

$ mysql -uroot -p
Enter password: 
mysql> show databases;
ERROR 1018 (HY000): Can't read dir of '.' (errno: 13)

問題確認:

先查看一下 /usr/local 底下的目錄:

$ ls -al /usr/local/
total 80
drwxr-xr-x   29 mayer  wheel   986  6 28 09:55 .
drwxr-xr-x@  13 root   wheel   442  2 24 15:07 ..
drwxr-xr-x   16 mayer  admin   544  6 28 10:19 .git
......
lrwxr-xr-x    1 mayer  wheel    27  1 28  2014 mysql -> mysql-5.1.72-osx10.6-x86_64
drwxr-xr-x   16 mayer  wheel   544 12 17  2014 mysql-5.1.72-osx10.6-x86_64
drwxr-xr-x   10 mayer  admin   340 12  8  2015 opt
......

可以看到,/usr/local/ 底下有 MySQL 的目錄,由於剛剛我們改了 /usr/local 目錄底下所有目錄 owner 的關係,很可能是因為這樣出了問題。

先進入 Mac 的系統偏好設定 -> MySQL,看到正常運作中:

然後點擊 Stop MySQL Server 按鈕,會看到 MySQL 服務停止了,並且出現警告訊息:

/usr/local/mysql/data 目錄的 owner 不是 mysql 或 _mysql,可能會出問題。

問題解決:

先查看機器上,系統上有什麼樣的 mysql 使用者:

grep mysql /etc/passwd
_mysql:*:74:74:MySQL Server:/var/empty:/usr/bin/false

這邊發現到,只有一個 _mysql 這個使用者,很顯然的是在安裝 mysql 套件時一併建立的,因此我們將 /usr/local/mysql 相關的目錄更改目錄的 owner:

sudo chown -R _mysql /usr/local/mysql*

之後,在點 Start MySQL Server 按鈕,就能看到 MySQL 正常啟動了。

最後,透過 mysql command line 確認是否能正常讀取 database 了:

$ mysql -uroot -p   
mysql> show databases;
+---------------------+
| Database            |
+---------------------+
| information_schema  |
| ..................  |
| ..................  |
| test                |
+---------------------+
27 rows in set (0.01 sec)

一切又恢復正常了!

2017年6月26日

The Ten Commandments of Egoless Programming

無我編程的十條戒律,最早出現在由Gerald Weinberg於1971年出版的經典著作《程序開發心理學》裡。這十條準則,除了告訴 programmer 要鍛鍊自己的人格修養之外,更多的是提醒大家,要擺脫 geek的形象,走入社會及人群。

  1. Understand and accept that you will make mistakes. The point is to find them early, before they make it into production. Fortunately, except for the few of us developing rocket guidance software at JPL, mistakes are rarely fatal in our industry, so we can, and should, learn, laugh, and move on.

    接受自己會犯錯的事實。關鍵是要在錯誤進入到生產環境之前把它們找出來。所幸的是,除了小部分在噴氣推進實驗室裡開發火箭制導系統的程序員,大部分錯誤都不會造成致命的後果。所以,我們一定能夠而且也應該要學會嫣然一笑,然後繼續。

  2. You are not your code. Remember that the entire point of a review is to find problems, and problems will be found. Don't take it personally when one is uncovered.

    不要使用代碼來針對個人。要記住,代碼評審的目的是為了找出問題,而且總歸會找到問題。如果真的找到了問題,請不要把它作為針對個人的藉口。

  3. No matter how much "karate" you know, someone else will always know more. Such an individual can teach you some new moves if you ask. Seek and accept input from others, especially when you think it's not needed.

    不管你知道多少“秘籍”,總有人比你知道得更多。如果你開口,他們就會教你更多的東西。在你認為沒有必要的時候,學會接受他人的建議。

  4. Don't rewrite code without consultation. There's a fine line between "fixing code" and "rewriting code." Know the difference, and pursue stylistic changes within the framework of a code review, not as a lone enforcer.

    不要不經討論地重寫代碼。“修復代碼”與“重寫代碼”是有明顯的區別的。瞭解這些區別,並在代碼評審的框架之內進行程式化的變更,而不是單獨作戰。

  5. Treat people who know less than you with respect, deference, and patience. Nontechnical people who deal with developers on a regular basis almost universally hold the opinion that we are prima donnas at best and crybabies at worst. Don't reinforce this stereotype with anger and impatience.

    尊重比你懂得少的人,並對他們抱以耐心。與技術人員打交道的非技術人員認為技術人員要麼是妄自尊大的討厭鬼,要麼是愛撂挑子的倔驢。所以,我們不要用我們的憤怒和不耐煩去加深他們對我們的這種印象。

  6. The only constant in the world is change. Be open to it and accept it with a smile. Look at each change to your requirements, platform, or tool as a new challenge, not as some serious inconvenience to be fought.

    這個世界唯一不變的就是變化。敞開胸懷,面帶微笑地去擁抱變化。把每一個需求變更、平台變更或工具變更都看成是一個新的挑戰,而不是令人厭惡的麻煩。

  7. The only true authority stems from knowledge, not from position. Knowledge engenders authority, and authority engenders respect – so if you want respect in an egoless environment, cultivate knowledge.

    真正的權威來自於知識,而不是職位。知識造就了權威,而權威會迎來尊重。如果你想要在一個無我的環境裡得到尊重,那麼充實你的知識吧。

  8. Fight for what you believe, but gracefully accept defeat. Understand that sometimes your ideas will be overruled. Even if you do turn out to be right, don't take revenge or say, "I told you so" more than a few times at most, and don't make your dearly departed idea a martyr or rallying cry.

    堅定你的立場,優雅地接受挑戰。要知道,你的想法有時候會遭到反對。你可以證明自己是對的,但不要試圖報復,不要總是叫嚷著“我早就說過”,不要把被否定的想法看成是一個犧牲品或者某種戰鬥口號。

  9. Don't be "the guy in the room." Don't be the guy coding in the dark office emerging only to buy cola. The guy in the room is out of touch, out of sight, and out of control and has no place in an open, collaborative environment.

    不要成為“小黑屋裡的人”。不要躲在小黑屋裡寫代碼,就算偶爾露個面,也只是為了買一杯可樂。躲在小黑屋裡只會讓你與其他人失去聯繫,淡出他們的視野,失去控制。在一個開放的協作環境裡,你會找不到自己的位置。

  10. Critique code instead of people – be kind to the coder, not to the code. As much as possible, make all of your comments positive and oriented to improving the code. Relate comments to local standards, program specs, increased performance, etc.

    批評代碼,而不是人。對人好一點,而不是代碼。讓你所有的評審為代碼帶來積極的改進,把你的評審與局部標準、程序規範和更好的性能結合在一起。

References

無我編程的十條戒律

The Ten Commandments of Egoless Programming

2017年6月19日

Volumio2

Volumio 是支援類似 Raspberry Pi 等多款小電腦主機的音樂播放器,只要有一台 Raspberry Pi,就能夠接上網路上的串流、區域網路內的音樂庫、YouTube,進行音樂的播放。

內建的 Audio Out 就能夠播放音樂,但如果要得到更高語音品質的音樂輸出,則需要再購買 USB Speaker 或是 USB/I2S DAC。

安裝 volumio2 只要到 DOWNLOAD VOLUMIO FOR YOUR PLATFORM 網頁下載 Raspberry Pi 的 image,燒錄到 micro sdcard,直接用 RPi 開機就可以運作。

網路設定

如果直接接上 RPi console,可以用

帳號: volumio
密碼: volumio

登入進入 RPi,root 的密碼也是 volumio。

但基本上是 console mode。

volumio 必須要用 browser 進去設定以及使用,主畫面就像下面這樣。

我們除了設定連接到 NAS 的音樂庫,另外還下載了 YouTube Plugin。

設定「播放選項」

因為我沒買 DAC,在設定播放的輸出裝置時,一直發現一些問題,最後就是參考這個網頁

LIBRARY UPDATE: SEEMS TO CRASH VOLUMIO ON RASPI

把 /boot/cmdline.txt 裡面

由
"smsc95xx.turbo_mode=N"
改為
"smsc95xx.turbo_mode=Y"

另外把音量選項的 Mixer Type 改為 Software,就能運作了。

輸出裝置

RPi 內建的 Audio Out 聲音效果很差,會聽到很多雜音。

後來把一個 USB Speaker 接上 RPi,也沒有裝 Driver,就能在輸出設備中選到該 USB Speaker,聲音的品質改善很多。

APP

Volumio iOS 是個非常好用的工具,在 LAN 裡面把 app 打開,馬上就自動連接到 LAN 的 volumio。

References

Raspberry Pi 的應用 - 連音響發燒友也愛用的音樂播放器:Volumio

Raspberry Pi 的應用 - 數位音樂播放器的再進化:Volumio 2

2017年6月13日

Google Computer Engine 價錢相關

每次有人問到價錢相關問題時,都要上官網去查,官方資料雖然詳細豐富,但有時候還是覺得太多了,不需要看到這麼細,不如就自己整理一份堪用的吧。

基本

instance

最貴費用會在 instance 的費用,instance 會影響到 CPU 與記憶體。根據你選的機器類型,會得到相對應的費用。以 zone asia-east1-a 為例:

  • n1-standard-1 (1 個 vCPU、3.75 GB 記憶體) 費用一個月為 28.50 美金
  • n1-standard-2 (2 個 vCPU、7.5 GB 記憶體) 費用會變為一個月 56.61 美金。

region

region 也就是你 instance 的所在區域,比如說台灣的話就是 asia-east1,而東京的話就是 asia-northeast1。

所選的 region 與 zone 會影響價錢,比如若使用 n1-standard-1 的機器類型,於 asia-east1-a 一個月要 28.50 美金,位於 us-west1-a 一個月要 24.6 美金,位於 asia-northeast1-a 則要 31.69 美金。

images

所選 image 使用的作業系統會影響價錢,用一般 Linux 如 Debian、CentOS、Ubuntu 等等的是免費的,但是若是使用 Windows Server 或是 Red Hat 則要付授權費用。詳細看這邊 Premium images

網路

網路的收費有點複雜,有以下幾點:

  1. 外部網路傳進 GCE 機器的流量 (Ingress) 是不用收費的。
  2. 在相同 zone 內兩台 GCE 機器透過內部 ip 互傳的網路流量是不用收費的。
  3. GCE 機器傳至相同 region 的服務或是產品是不用收費的。
  4. GCE 機器若是透過外部 ip 傳出去的流量,則要根據地區與用量算錢,大多區域的價錢都一樣,只有到中國(香港的流量價錢和其他區域一樣)和澳洲的流量價錢會不一樣。每月使用流量 1TB內,則大多區域的價錢會是每 GB 0.12 美元,到中國會是每 GB 0.23 美元,到澳洲會是每 GB 0.19 美元。若你使用 1TB 以上,會便宜一點,若你使用到 10T 以上,價格會在便宜一些。

在白話一點就是,從外部送給 GCE instance 的流量都不用錢;GCE instance 之間若是透過內網 ip 互傳的流量也不用錢;若是透過外網 ip 從 GCE instance 傳出的流量就要收錢。

負載平衡

啟用則每小時 0.025 美金 (5 條規則內統一都是一樣價錢),之後額外加一條規則加 0.010 美金。而負載平衡處理的流量每 GB 為 0.008 美金。

硬碟

以台灣所屬的 zone,asia-east1 為範例:

  1. 標準硬碟:1 個月 1 GB,0.04 美元。
  2. SSD:1 個月 1 GB,0.170 美元。
  3. Snapshot:1 個月 1 GB,0.026 美元。
  4. Local SSD:1 個月 1 GB,0.218 美元。
  5. 自製 Image:1 個月 1 GB,0.085 美元。

IP

你可以跟 google 要固定 ip,但是若你要了 ip 卻沒將它使用在任何 instance 上,則 google 要跟你收費,每小時為 0.010 美金,若是該 ip 有在正常使用中,則不會被收取任何費用。

價錢範例

以下價錢範例,會將美金 * 30 當成美金轉換成台幣的匯率。

規格與用量相關

規格或用量 zone 每月花費美金 每月花費台幣
1台 n1-standard (1 vCPU、3.75 GB 記憶體),使用 centOS 7 image asia-east1 28.51 855.3
1台 f1-micro (0.2 vCPU、0.60 GB 記憶體),使用 centOS 7 image asia-east1 5 150
1台 g1-small (0.5 vCPU、1.70 GB 記憶體),使用 centOS 7 image asia-east1 15.33 459.9
HDD 10 GB asia-east1 0.40 12
HDD 100 GB asia-east1 4.00 120
snapshop 10 GB asia-east1 0.26 7.8
snapshop 100 GB asia-east1 2.60 78
負載平衡,5 條規則,一個月處理 1 GB 流量 asia-east1 18.26 547.8
負載平衡,5 條規則,一個月處理 100 GB 流量 asia-east1 19.05 571.5
負載平衡額外增加 1 條規則 asia-east1 7.3 219
負載平衡額外多處理 1 GB 流量 asia-east1 0.01 0.3

網路相關

GCE 傳出流量 每月花費美金 每月花費台幣
GCE 機器傳出 1 GB 網路流量到中國(香港不屬於此價位) 0.23 6.9
GCE 機器傳出 100 GB 網路流量到中國(香港不屬於此價位) 23 690
GCE 機器傳出 1 GB 網路流量到澳洲 0.19 5.7
GCE 機器傳出 100 GB 網路流量到澳洲 19 570
GCE 機器傳出 1 GB 網路流量到各其他各地(香港屬於此價位) 0.12 3.6
GCE 機器傳出 100 GB 網路流量到各其他各地(香港屬於此價位) 12 360

免費試用

GCP 服務現在提供一年 300 美金的免費試用服務,所以可以透過你的 google 帳號啟用 GCP 服務(需要填入信用卡),則可使用 GCP 服務。預設你的帳號是不會被收取任何費用的,當裡面的 300 元花完了只會讓帳號被暫停,而不會從信用卡扣款。

若你點了升級或是升級帳戶的按鈕將你的帳號升級成付費帳號,此時信用卡才會被拿來扣款。

免費方案

若你只是需要一台機器,跑跑 wordpress,或是練習 linux,則你可以考慮使用 GCP 免費方案,GCE 提供你在 US regions 開一台 f1-micro instance,一個月可以使用 30 GB 的 HDD,與 5GB 的 snapshot。網路部份一個月 1 GB 的網路對外流量從北美至世界各地(但不包括中國和澳洲)。所以基本上可以拿到一台免費的機器使用。

reference

Google Cloud Platform Pricing Calculator

Always Free Usage Limits

Regions and Zones

Google Compute Engine Pricing

2017年6月12日

如何備份、還原處理 RPi 的 sdcard image

Raspberry Pi 使用的 sdcard 雖然同樣是 16GB,但因為不同廠牌還是有些大小的差異,備份跟還原 sdcard 有些問題,畢竟大檔案的 image 不能還原到稍小的 sdcard 中。

以下紀錄如何備份、還原及縮小 RPi 的 sdcard。

在 MAC 處理 image

  • 使用 [SD Formatter] 可格式化 SD Card
sudo newfs_msdos -F 16 /dev/disk2
  • RASPBIAN 下載 RASPBIAN JESSIE WITH PIXEL,取得 2016-09-23-raspbian-jessie.zip 解壓縮得到2016-09-23-raspbian-jessie.img

  • 重新插入 SD Card,用以下指令的結果,得知 SD Card 在 /dev/disk2 或是 /dev/disk1

diskutil list
/dev/disk0 (internal, physical):
   #:                       TYPE NAME                    SIZE       IDENTIFIER
   0:      GUID_partition_scheme                        *480.1 GB   disk0
   1:                        EFI EFI                     209.7 MB   disk0s1
   2:                  Apple_HFS Macintosh HD            479.2 GB   disk0s2
   3:                 Apple_Boot Recovery HD             650.0 MB   disk0s3
/dev/disk2 (internal, physical):
   #:                       TYPE NAME                    SIZE       IDENTIFIER
   0:     FDisk_partition_scheme                        *15.9 GB    disk2
   1:             Windows_FAT_32 NO NAME                 15.9 GB    disk2s1
  • umount disk2
sudo diskutil unmountDisk /dev/disk2
  • 將 img 寫入 sdcard

如果 rdisk2 有問題,就改成 disk2

sudo dd bs=1m if=~/Downloads/2016-09-23-raspbian-jessie.img of=/dev/rdisk2
  • 將 sdcard dump 到 image
sudo dd bs=1m if=/dev/rdisk2 of=~/Downloads/messenger_20170405.img

在 RPi 開機狀態直接備份 sdcard

在 RPi 上,將新的 sdcard 放入 USB sdcard Reader 接上 RPi

sudo fdisk -l

如果是 Disk /dev/mmcblk0 的設備,就是 RPi 上原始的 sdcard,以 USB sdcard Reader 接上 RPi 後,會看到增加的 /dev/sda 設備,這是新的 sdcard

下載 rpi-clone

git clone https://github.com/billw2/rpi-clone.git
sudo cp rpi-clone/rpi-clone /usr/local/sbin
rm -rf rpi-clone

用以下指令就可以將 sdcard 複製到 新的 sdcard 上

sudo rpi-clone sda -v -x

在 Windows 處理 sdcard image

使用 Win32 Disk Imager 軟體就可以備份及還原 image

縮小 RPi image

使用 PiShrink 可以在 linux 縮小 RPi image

wget https://raw.githubusercontent.com/Drewsif/PiShrink/master/pishrink.sh

chmod +x ./pishrink.sh
sudo ./pishrink.sh ./raspbian_backup.img

以下是執行結果,可看到檔案縮小了 2G

Creating new /etc/rc.local
e2fsck 1.41.12 (17-May-2010)
Pass 1: Checking inodes, blocks, and sizes
Pass 2: Checking directory structure
Pass 3: Checking directory connectivity
Pass 4: Checking reference counts
Pass 5: Checking group summary information
/dev/loop0: 301607/960992 files (0.2% non-contiguous), 3200117/3841024 blocks
resize2fs 1.41.12 (17-May-2010)
resize2fs 1.41.12 (17-May-2010)
Resizing the filesystem on /dev/loop0 to 3208948 (4k) blocks.
Begin pass 2 (max = 228392)
Relocating blocks             XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
Begin pass 3 (max = 118)
Scanning inode table          XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
Begin pass 4 (max = 25082)
Updating inode references     XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
The filesystem on /dev/loop0 is now 3208948 blocks long.

Shrunk ./raspbian_backup.img from 15G to 13G

References

How to Clone Raspberry Pi SD Card on Windows, Linux and macOS

Backup (or Clone) a Raspberry Pi SD Card

用 Win32 Disk Imager 備份和燒錄 Raspberry Pi 的 SD 卡

2017年6月5日

倖存者偏差 survivorship bias

倖存者偏差(Survivorship Bias),就是只有存活下來的人,才會被後人看到,因此我們在分析資訊的時候,非常容易忽略掉那些被淘汰的競爭者,造成對局勢的評估過分樂觀,甚至導出完全相反的結論!

在觀察不熟悉的產業時,往往被這種倖存者偏差所蒙蔽,報章雜誌更是時常針對那些所謂成功的企業案例。實際上,這些結果論的成功故事所歸納出的成功要訣往往只是其中一小塊拼圖,這些1%的倖存者因為大量曝光的資訊,讓我們以為前景一片看好,我們卻沒有看到其他99%的競爭者早就默默地消失了!由於成功者的能見度壓倒性的高過失敗者,因此,人們總是系統性的高估了獲得成功的希望。

類似的名詞有:隱藏失敗者、沉默的數據、死人不會說話

統計的抽樣方法

抽樣 是一種推論統計方法,從目標總體(Population,母體)中抽取一部分個體作為樣本(Sample),通過觀察樣本的某一或某些屬性,依據所獲得的數據對總體的數量特徵得出具有一定可靠性的估計判斷,從而達到對總體的認識。

學問就在於,如何抽樣,抽樣的個體如何能代表整個母體?統計上有另一個名詞:信賴區間,信賴區間是樣本對某個總體參數的區間估計。信賴區間展現的是,這個總體參數的真實值有一定機率落在與該測量結果有關的某對應區間。

信賴區間會聲稱總體參數的真實值在測量值的區間所具有的可信程度,也就是前面所說的「一定機率」。這個機率被稱為信心水準。

舉例來說,如果在一次大選中某人的支持率為55%,而信心水準0.95上的信賴區間是(50%,60%),那麼他的真實支持率落在50%和60%之區間的機率為95%,因此他的真實支持率不足50%的可能性小於2.5%(假設分布是對稱的)。

在倖存者偏差中,由於抽樣的對象都是整個母體中的倖存者,因此對這些抽樣對象的分析結果,並不能代表整個母體,如果誤信這些倖存者的分析結果,忽略了原本母體中沈默的失敗者,就會造成對局勢過分樂觀的評估結果。

第二次世界大戰

1941年,第二次世界大戰中,美國哥倫比亞大學統計學亞伯拉罕.沃爾德(Abraham Wald)應軍方要求,利用其在統計方面的專業知識來提供關於《飛機應該如何加強防護,才能降低被炮火擊落的機率》的相關建議。

教授針對聯軍的轟炸機遭受攻擊後的數據,進行研究後發現:機翼是最容易被擊中的位置,機尾則是最少被擊中的位置。沃德教授的結論是「我們應該強化機尾的防護」,而軍方指揮官認為「應該加強機翼的防護,因為這是最容易被擊中的位置」。

沃德教授的堅持有以下幾點

  1. 統計的樣本,只涵蓋平安返回的轟炸機
  2. 被多次擊中機翼的轟炸機,似乎還是能夠安全返航
  3. 在機尾的位置,很少發現彈孔的原因並非真的不會中彈,而是一旦中彈,其安全返航的機率就微乎其微

軍方採用了教授的建議,並且後來證實該決策是正確的,看不見的彈痕卻最致命!

這個故事有兩個啟示:一是戰死或被俘的飛行員無法發表意見,所以彈痕數據的來源本身就有嚴重的偏誤;二是作戰經驗豐富的飛行員的專業意見也不一定能提升決策的質量,因為這些飛行員大多是機翼中彈機尾未中彈的倖存者。

基金及股票市場

基金行業會對外宣布,過去10年,基金行業的整體收益率超過100%,你是不是覺得買基金肯定賺翻了?

基金市場中,計算基金類股未來多年走勢時,通常會排除下檔基金的虧損,因此會高估了基金的整體報酬率,因為已經消失在市場的基金,沒有必要列在其中。

如果長期觀察新發行證券的股價發展,並排除已經破產的公司時,也會發現類似的效應。這種「排除」模式導致投資人對未來發展過度樂觀,因為破產公司無法拉低整體的統計數據。

股票指數的表現也會遭遇這類失真問題,因為指數表現較差的股票並不會納入計算,股市指數永遠只會統計表現最亮眼的那些股票。

讀書無用論

很多人在說,某某人當初沒好好上學如今照樣掙大錢,而好多用功讀書的人,畢業後反而不如那些沒好好學習的人混得好。並且因為這樣的例子有很多,所以很多人得出「讀書無用」的結論。

因為高學歷成功者的新聞性不會比低學歷成功者來得高,在報導成功案例時,如果有低學歷,或是輟學的過程,會讓整個故事更具有傳奇性,因此我們常常會聽到這樣的案例,相反的,如果是高學歷的成功者,報導中就不會強調這個部分。

生活中的 倖存者偏差

我親戚的病就是中醫看好的,所以中醫很靈驗。

喝葡萄酒的人比較長壽。

每個成功者都很努力,所以只要努力就能成功。

References

倖存者偏差

可怕的邏輯陷阱 -- 存活者偏差

彈孔最多的地方不需要保護──倖存者偏差

無所不在的生存者誤差 (Survivorship bias)

林斯諺/國外的月亮比較圓?《返校》熱潮的「倖存者偏差」

考研,一定要注意倖存者偏差

倖存者偏差是什麼意思?

3 - 6 信賴區間與信心水準的解讀

2017年5月22日

監督式機器學習方法 Supervised Machine Learning: 生成模型 (Generative Model) 與判別模型 (Discriminative Model)

機器學習 是人工智慧的一個分支,讓機器透過某種學習方法,實現並解決人工智慧中的一些問題。機器學習演算法是從資料中自動分析獲得規律,並利用規律對未知資料進行預測的演算法。學習演算法需要用到大量的統計學理論,也被稱為統計學習理論。

Machine Learning 的分類

  1. 監督學習:從給定的訓練資料集中學習出一個函式,當新的資料到來時,可以根據這個函式預測結果,訓練資料中的目標是人工標註的。

  2. 無監督學習:與監督學習相比,訓練資料沒有人為標註的結果。常見的無監督學習演算法有聚類分析 Cluster Analysis

  3. 半監督學習:介於監督學習與無監督學習之間。

  4. 增強學習:通過觀察來學習做成如何的動作。每個動作都會對環境有所影響,學習物件根據觀察到的周圍環境的反饋來做出判斷。

監督式機器學習方法 Supervised Learning

監督式機器學習方法 Supervised Learning 是 Machine Learning 中的一個方法,可以由訓練資料中學到或建立一個模式(函數 / learning model),並依此模式推測新的實例,換句話說,任務就是在觀察完一些訓練範例(輸入和預期輸出)後,利用這個函數去對任何可能出現的輸入的值,預測輸出的結果。

訓練資料是由輸入物件(通常是向量)和期待的輸出所組成。函數的輸出可以是一個連續的值(稱為迴歸分析),或是預測一個分類標籤(稱作分類)。

監督式機器學習方法可以分為生成方法和判別方法兩類,常見的生成方法有混合高斯模型、樸素貝葉斯法和隱形馬爾科夫模型等,常見的判別方法有SVM、LR等,生成方法學習出的是生成模型,判別方法學習出的是判別模型。

生成模型就是能夠隨機生成觀測數據的模型,也就是對影像、聲音以及對現實世界的一切其他實物(representations)進行創造的系統。如果 AI 學會了建立現實世界中的種種細節,例如現實中的圖像和聲音,這將幫助 AI 更好地理解現實世界的結構。

生成模型與判別模型的比較

生成式模型和判別式模型的區別很像人和機器的區別:機器採取的是完美主義,因為它可以不斷優化,追求極致。而人是把事情做得夠好就滿足了。

因為人類的構造天生跟機器不同,所以人類不需要跟 Alpha Go 比賽圍棋分出高下,Alpha Go 只專注在做圍棋這件事,並把它做到極限。

生成模型是所有變量的全機率模型,而判別模型是在給定觀測變量值前提下,觀測目標變量條件機率模型。

生成模型能夠用於模擬(即生成)模型中任意變量的分布情況,而判別模型只能根據觀測變量得到目標變量的樣本。

判別模型不提供觀測變量的分布建模,因此它不能夠表達觀測變量與目標變量之間更複雜的關係。因此,生成模型適用於無監督的任務,如分類和聚類。

由生成模型可以得到判別模型,但由判別模型得不到生成模型。

生成模型收斂速度比較快,如果樣本數量較多時,生成模型能更快地收斂於真實模型。生成模型中的聯合分佈能提供更多的資訊,但也需要更多的樣本和更多計算。


例如我們有一個輸入數據x,然後我們想將它分類為標籤y。最自然的做法就是條件概率分佈p(y|x),這就是為什麼我們對其直接求p(y|x)方法叫做判別模型演算法。生成模型學習聯合概率分佈p(x,y),p(x,y)可以通過貝葉斯方法轉化為p(y|x),然後再用其分類。但是p(x,y)還有其他作用,例如,你可以用它去生成(x,y)。

假設我們有以下(x,y)形式的數據:(1,0), (1,0), (2,0), (2, 1)

那麼p(x,y)是:y 共有四種結果,0,0,0,1,產生的機率分別是 1/4,其中前面兩個 0,0,它的 x 都是 1,所以當 x 為 1,產生出 y 為 0 的機率是 1/4+1/4 = 1/2,不可能產生 y=1 的狀況,所以機率為0。而當 x 為 2,產生出 y 為 0 的機率是 1/4,產生出 y 為 1 的機率也是 1/4。目標是要生成 y,所以生成 y 的所有機率總和為 1。

            y=0   y=1

           -----------

   x=1 | 1/2   0

   x=2 | 1/4   1/4

而p(y|x) 是:因為當 x =1,y 一定為 0,所以在 x 為 1 的基本條件下,y 為 0 的機率為 1,如果 x = 2 的條件下,y 有 0 或 1 兩種可能,發生的機率分別是 1/2。目標是先以 x 為基本條件,在給定 x 之後,得到 y 的機率分佈。

           y=0   y=1

           -----------

    x=1| 1     0

    x=2| 1/2   1/2

如果有某項工作是要識別一個語音屬於哪種語言,有兩種方法達到這個目的:

1、把所有語言先都學會,然後就能識別任何一段新的語音了。

2、不去學習每一種語言,只學習這些語言模型之間的特徵及差別,然後再分類。只要學會了漢語和英語等語言的發音的差別,直接用這樣的差異去分類就好了。

第一種方法就是生成方法,第二種方法是判別方法。

生成算法嘗試去找到底這個數據是怎麼產生的,然後再對一個信號進行分類。基於你的生成假設,那個類別的資料最有可能產生這個信號,這個信號就屬於那個類別。判別模型不關心數據是怎麼生成的,它只關心信號之間的差異,然後用差異來簡單對給定的一個信號進行分類。


生成模型:一般是學習一個代表目標的模型,然後通過它去搜索圖像區域,然後最小化重構誤差。類似於生成模型描述一個目標,然後就是模式匹配了,在圖像中找到和這個模型最匹配的區域,就是目標了。

判別模型:將跟蹤問題看成一個二分類問題,找到目標和背景的決策邊界。它不管目標是怎麼描述的,只知道目標和背景的差異,然後你給一個新的圖像,看它屬於那一邊,就歸為哪一類。

生成式對抗網絡 GenerativeAdverserial Network GAN

有兩個系統,在互相對抗,兩個系統都試圖優化自己的目標函數。第一個系統對應判別式模型D:判別式模型D在試圖識別到來的樣本是否是自然真實的;它在儘量增大對真實樣本的識別率,同時減少對模擬生成的樣本的誤判率。

另一個系統則對應著生成式模型G:G希望它生成的模擬樣本可以在D那裡魚目混珠。 所以G試圖最大可能地產生真實的樣本。判別器D從判別角度來說,判別的越好,D的目標實現的就越強大。

但對於生成器G來說,它要最小化(minimize)對方的優化函數,這就相當於最大化(maximize)它自己的優化函數。這個過程就像G和D在下棋一樣。

用一個AI對現實世界的圖像進行創造,再用另一個AI去分析結果並對圖像的真偽進行識別。兩個系統在競爭中不斷成長,最後兩個都達到最佳化。

將二者的關係想像成一個藝術家和一個文藝批評家。作為藝術家,生成模型希望愚弄藝術批評家,讓後者認為它畫出來的東西是真實的。因為藝術批評家努力地將這些畫辨認為假畫,藝術家慢慢學會了如何摹擬那些真的東西,而這原本只靠它自己是做不到的。

References

楊強教授漫談《西部世界》、生成式對抗網絡及遷移學習

機器學習中的貝氏定理:生成模型 (Generative Model) 與判別模型 (Discriminative Model)

生成模型與判別模型

機器學習“判定模型”和“生成模型‘有什麼區別?

生成模型和判別模型

生成模型 wiki

判别模型 wiki

機器學習常用算法梳理

GAN誕生記:最火的AI模型,來自一群博士的酒後爭吵

2017年5月15日

以 docker 測試網站

docker container 本身並沒有 persistence 的機制,但可以透過共享 valume 的方式,將本地機器的某個實體的路徑,綁定到 container 中,由於 container 疊加式的文件檔案系統,我們還是會覺得只有一個檔案系統。

靜態網站

在 sample 目錄中,製作一個 Dockerfile 檔案

FROM ubuntu:latest
MAINTAINER yaocl
ENV REFRESHED_AT 2016-12-23

# 安裝 ngnix
RUN apt-get -yqq update && apt-get -yqq install nginx

# 建立 website 目錄
RUN mkdir -p /var/www/html/website

# 調整 nginx 設定
ADD nginx/global.conf /etc/nginx/conf.d/
ADD nginx/nginx.conf /etc/nginx/

# TCP Port 80
EXPOSE 80

另外準備兩個 nginx 設定檔:

nginx/global.conf

server {
        listen          0.0.0.0:80;
        server_name     _;

        root            /var/www/html/website;
        index           index.html index.htm;

        access_log      /var/log/nginx/default_access.log;
        error_log       /var/log/nginx/default_error.log;
}

nginx/nginx.conf

user www-data;
# process 數量
worker_processes 4;
# 紀錄 process id
pid /run/nginx.pid;

# 不讓 nginx 進入 daemon 狀態,以前景執行,否則會讓 container 啟動後直接停掉
daemon off;

events {  }

http {
  sendfile on;
  tcp_nopush on;
  tcp_nodelay on;
  keepalive_timeout 65;
  types_hash_max_size 2048;
  include /etc/nginx/mime.types;
  default_type application/octet-stream;
  access_log /var/log/nginx/access.log;
  error_log /var/log/nginx/error.log;
  gzip on;
  gzip_disable "msie6";
  include /etc/nginx/conf.d/*.conf;
}

在 website 目錄,放一個 index.html 網頁。

所有檔案的目錄結構為

sample\
    Dockerfile
    nginx\
        global.conf
        nginx.conf
    website\
        index.html

以 build 指令產生 docker image

docker build -t yaocl/nginx .

可透過 history 查看 image 的過程

docker history yaocl/nginx

啟動 container,以 -v 將 website 目錄綁定為 container 的 /var/www/html/website 目錄,覆蓋掉原本在 image 中的那個目錄,我們就可以修改 website 的網頁,並即時由 browser 看到網頁的結果。

$ docker run -d -p 80:80 --name website \
    -v $PWD/website:/var/www/html/website \
    yaocl/nginx nginx
b64855fbfc6bb313c9190eeead2f1d433d28b9d759dba85d06399841e0ef9f78

如果加上 :ro ,就變成 readonly,rw 則是可讀寫

docker run -d -p 80:80 --name website \
    -v $PWD/website:/var/www/html/website:ro \
    yaocl/nginx nginx

連結兩個 container

如果我們需要用到兩個 containers,其中一個用來執行 application server,另一個執行 redis或 db,這時候有兩種方式,可以讓兩個 containers 可以互相連結使用服務。

我們建立一個 redis image 跟 container,另外產生一個只有 os 的 container,讓測試讓後面那個 container 可以用 redis-cli 連結到 redis server。

產生 redis db server 的 Dockerfile

FROM ubuntu:latest
MAINTAINER yaocl
ENV REFRESHED_AT 2016-12-23

RUN apt-get -yqq update && apt-get -yqq install redis-server redis-tools

EXPOSE 6379

ENTRYPOINT ["/usr/bin/redis-server"]
CMD []

產生 redis image,並啟動 container

docker build -t yaocl/redis .

docker run -d -p 6379:6379 --name redis yaocl/redis

如果剛剛沒有指定主機的 port 可以用 port 指令查詢 port

$ docker port redis 6379
0.0.0.0:6379

以本機的 redis-cli 測試 redis:6379

$ redis-cli -h 127.0.0.1 -p 6379
127.0.0.1:6379>

要從 container: redis-cli 連接到 redis,有兩種方式,一種是使用 docker 的內部網路 network stack,安裝 docker 時就會建立一個 docker0 的網路介面,每一個 docker container 都會在這個網路上分配到獨立的 172.16~172.30 這個範圍的 ip。

預設這些網路之間不能互相連接,如果搭配修改 iptables(DNAT),就可以讓 container 互相溝通,但 docker for mac 找不到 docker0 這個 network,如果要測試,可以參考 Docker container networking 的說明。

我們用另一個比較常見的方式,直接讓 container 互相連接。

在 mac 先將 lo0 綁定一個新的 ip

sudo ifconfig lo0 alias 10.200.10.1/24

首先刪除掉剛剛的 redis container

docker stop redis

docker rm redis

重新執行一個 redis container,但不指定 -p 6379

docker run -d --name redis yaocl/redis

redis-cli 的 Dockerfile

FROM ubuntu:latest
MAINTAINER yaocl
ENV REFRESHED_AT 2016-12-23

RUN apt-get update
RUN apt-get -y install inetutils-ping redis-tools

ENTRYPOINT ["/bin/bash"]
CMD []

產生 redis-cli image

docker build -t yaocl/redis-cli .
docker run --name webapp -t -i yaocl/redis-cli

如果不是用 docker for mac,可以用 link 的方式直接將 container 連接起來。

docker run --name webapp --link redis:db -t -i yaocl/redis-cli

啟動 redis-cli container webapp 後,就能直接用 db 這個 hostname 連接到 redis server

root@79bb961c2b78:/# redis-cli -h db
db:6379>

root@79bb961c2b78:/# env
HOSTNAME=79bb961c2b78
DB_NAME=/webapp/db
DB_PORT_6379_TCP_PORT=6379
TERM=xterm
DB_PORT=tcp://172.17.0.2:6379
DB_PORT_6379_TCP=tcp://172.17.0.2:6379
LS_COLORS=rs=0:di=01;34:ln=01;36:mh=00:pi=40;33:so=01;35:do=01;35:bd=40;33;01:cd=40;33;01:or=40;31;01:mi=00:su=37;41:sg=30;43:ca=30;41:tw=30;42:ow=34;42:st=37;44:ex=01;32:*.tar=01;31:*.tgz=01;31:*.arc=01;31:*.arj=01;31:*.taz=01;31:*.lha=01;31:*.lz4=01;31:*.lzh=01;31:*.lzma=01;31:*.tlz=01;31:*.txz=01;31:*.tzo=01;31:*.t7z=01;31:*.zip=01;31:*.z=01;31:*.Z=01;31:*.dz=01;31:*.gz=01;31:*.lrz=01;31:*.lz=01;31:*.lzo=01;31:*.xz=01;31:*.bz2=01;31:*.bz=01;31:*.tbz=01;31:*.tbz2=01;31:*.tz=01;31:*.deb=01;31:*.rpm=01;31:*.jar=01;31:*.war=01;31:*.ear=01;31:*.sar=01;31:*.rar=01;31:*.alz=01;31:*.ace=01;31:*.zoo=01;31:*.cpio=01;31:*.7z=01;31:*.rz=01;31:*.cab=01;31:*.jpg=01;35:*.jpeg=01;35:*.gif=01;35:*.bmp=01;35:*.pbm=01;35:*.pgm=01;35:*.ppm=01;35:*.tga=01;35:*.xbm=01;35:*.xpm=01;35:*.tif=01;35:*.tiff=01;35:*.png=01;35:*.svg=01;35:*.svgz=01;35:*.mng=01;35:*.pcx=01;35:*.mov=01;35:*.mpg=01;35:*.mpeg=01;35:*.m2v=01;35:*.mkv=01;35:*.webm=01;35:*.ogm=01;35:*.mp4=01;35:*.m4v=01;35:*.mp4v=01;35:*.vob=01;35:*.qt=01;35:*.nuv=01;35:*.wmv=01;35:*.asf=01;35:*.rm=01;35:*.rmvb=01;35:*.flc=01;35:*.avi=01;35:*.fli=01;35:*.flv=01;35:*.gl=01;35:*.dl=01;35:*.xcf=01;35:*.xwd=01;35:*.yuv=01;35:*.cgm=01;35:*.emf=01;35:*.ogv=01;35:*.ogx=01;35:*.aac=00;36:*.au=00;36:*.flac=00;36:*.m4a=00;36:*.mid=00;36:*.midi=00;36:*.mka=00;36:*.mp3=00;36:*.mpc=00;36:*.ogg=00;36:*.ra=00;36:*.wav=00;36:*.oga=00;36:*.opus=00;36:*.spx=00;36:*.xspf=00;36:
DB_ENV_REFRESHED_AT=2016-12-23
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
REFRESHED_AT=2016-12-23
PWD=/
DB_PORT_6379_TCP_ADDR=172.17.0.2
DB_PORT_6379_TCP_PROTO=tcp
SHLVL=1
HOME=/root
no_proxy=*.local, 169.254/16
DB_ENV_no_proxy=*.local, 169.254/16
_=/usr/bin/env

root@79bb961c2b78:/# ping db
PING db (172.17.0.2): 56 data bytes
64 bytes from 172.17.0.2: icmp_seq=0 ttl=64 time=0.223 ms
64 bytes from 172.17.0.2: icmp_seq=1 ttl=64 time=0.139 ms

References

The Docker Book

Networking your docker containers using docker0 bridge

How to create a bidirectional link between containers?