2017/08/28

Firebase Cloud Messaging FCM

GCM 已經被 Firebase Cloud Messaging FCM 取代,以下記錄測試 FCM 的過程。

註冊 Firebase

firebase 的頁面登入 google 帳號,登入後點擊右上方的 "Console" 即可進入控制台,第一件事是建立Firebase 專案,進入主控台後,請按下「CREATE NEW PROJECT」建立一個新專案,專案名稱填 alarm,國家/地區 填台灣。

接下來建立一個 android 應用程式,套件名稱的部分,要跟 Android APP 一樣,我們填 tw.com.maxkit.alarm

建立 Android project

用 Android Studio 建立一個新的 project: Alarm,package name 為 tw.com.maxkit.alarm。

將剛剛新增的 firebase android 應用程式裡面下載的 "google-services.json" 這個檔案,放到 Alarm/app 這個目錄裡面。

修改以下的設定檔

  • Alarm/build.gradle

增加一行 classpath 'com.google.gms:google-services:3.1.0'

buildscript {
    repositories {
        jcenter()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:2.3.2'

        classpath 'com.google.gms:google-services:3.1.0'
    }
}

allprojects {
    repositories {
        jcenter()
    }
}

task clean(type: Delete) {
    delete rootProject.buildDir
}
  • Alarm/app/build.gradle

增加這三個部分

compile 'com.google.firebase:firebase-messaging:10.2.6'
compile 'com.firebase:firebase-jobdispatcher:0.5.2'

apply plugin: 'com.google.gms.google-services'

完整的內容

apply plugin: 'com.android.application'

android {
    compileSdkVersion 24
    buildToolsVersion "25.0.3"
    defaultConfig {
        applicationId "tw.com.maxkit.alarm"
        minSdkVersion 14
        targetSdkVersion 24
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
}

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
        exclude group: 'com.android.support', module: 'support-annotations'
    })
    compile 'com.android.support:appcompat-v7:24.2.1'
    compile 'com.android.support.constraint:constraint-layout:1.0.2'
    testCompile 'junit:junit:4.12'

    compile 'com.google.firebase:firebase-messaging:10.2.6'
    compile 'com.firebase:firebase-jobdispatcher:0.5.2'
}

apply plugin: 'com.google.gms.google-services'
  • Alarm/app/src/main/AndroidManifest.xml

增加兩個 meta-data,以及 MyFirebaseMessagingService 這個 service

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="tw.com.maxkit.alarm">

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <!-- 收到通知時, 到狀態列要顯示的 icon -->
        <meta-data
            android:name="com.google.firebase.messaging.default_notification_icon"
            android:resource="@drawable/ic_stat_announcement" />
        <!-- 收到通知的背景色 -->
        <meta-data
            android:name="com.google.firebase.messaging.default_notification_color"
            android:resource="@color/black" />

        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>

        <service
            android:name=".MyFirebaseMessagingService">
            <intent-filter>
                <action android:name="com.google.firebase.MESSAGING_EVENT"/>
            </intent-filter>
        </service>
    </application>

</manifest>

這裡用到了自訂的 color 跟 icon

Alarm/app/src/main/res/values/colors.xml

要增加 black

<color name="black">#00000000</color>
  • MainActivity.java

處理 notification 的部分,由系統列點擊 notification 會進入 MainActivity,在這裡取得該 notification 的資訊。

package tw.com.maxkit.alarm;

import android.content.Intent;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;

import com.google.firebase.messaging.FirebaseMessaging;

public class MainActivity extends AppCompatActivity {

    private static final String TAG = "MainActivity";

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        //這一行是要註冊 alarms 這個 topic,如果不需要,就把這行刪除
        FirebaseMessaging.getInstance().subscribeToTopic("alarms");

        Log.d(TAG, "onCreate ..");

        Intent intent = getIntent();
        String msg = intent.getStringExtra("msg");

        if (msg!=null)
            Log.d(TAG, "msg:"+msg);

        if (getIntent().getExtras() != null) {
            for (String key : getIntent().getExtras().keySet()) {
                Object value = getIntent().getExtras().get(key);
                Log.d(TAG, "Key: " + key + " Value: " + value);
            }
        }
    }

}
  • MyFirebaseMessagingService.java

處理訊息通知的 code,主要是 onMessageReceived

package tw.com.maxkit.alarm;

import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.media.RingtoneManager;
import android.net.Uri;
import android.support.v4.app.NotificationCompat;
import android.util.Log;
import com.google.firebase.messaging.FirebaseMessagingService;
import com.google.firebase.messaging.RemoteMessage;

public class MyFirebaseMessagingService extends FirebaseMessagingService{

    private static final String TAG = "MyFirebaseMsgService";

    @Override
    public void onMessageReceived(RemoteMessage remoteMessage) {
        Log.d(TAG, "onMessageReceived:"+remoteMessage+", from:"+remoteMessage.getFrom()+", data="+remoteMessage.getData());

        if (remoteMessage.getData().size() > 0) {
            Log.d(TAG, "Message data notifytitle: "+ remoteMessage.getData().get("notifytitle"));
            Log.d(TAG, "Message data notifybody: "+ remoteMessage.getData().get("notifybody"));
            Log.d(TAG, "Message data payload: " + remoteMessage.getData());
        }

        if (remoteMessage.getNotification() != null) {
            Log.d(TAG, "Message Notification title: "+ remoteMessage.getData().get("title"));
            Log.d(TAG, "Message Notification Body: " + remoteMessage.getNotification().getBody());
        }

        //Display notification in notification bar
        sendNotification(remoteMessage.getData().get("notifytitle"), remoteMessage.getData().get("notifybody"));
    }

    private void sendNotification(String notifytitle, String notifybody) {
        Intent intent = new Intent(this, MainActivity.class);

        intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
        PendingIntent pendingIntent = PendingIntent.getActivity(this, 0 /* Request code */, intent,
                PendingIntent.FLAG_ONE_SHOT);

        Uri defaultSoundUri=
                RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION);
        NotificationCompat.Builder notificationBuilder = new NotificationCompat.Builder(this)
                .setSmallIcon(R.drawable.ic_stat_access_alarms)
                .setContentTitle( notifytitle )
                .setContentText( notifybody )
                //.setAutoCancel(true)
                .setSound(defaultSoundUri)
                .setContentIntent(pendingIntent);

        NotificationManager notificationManager =
                (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
        notificationManager.notify(0 /* ID of notification */, notificationBuilder.build());
    }
}

用 Firebase 網頁測試

在左側點擊 Notifications 的部分,填寫以下的訊息,就會以 notification 的方式,收到訊息。意思就是說,當 APP 在背景時,會直接在系統列裡面收到 notification。

用 curl 測試

根據 Firebase Cloud Messaging HTTP Protocol 的說明,可以直接用 http 的方式發送訊息。

在專案設定 -> Cloud Messaging 的地方,可以找到 Authorization Key 伺服器金鑰

以下是 curl 的測試,"Authorization: key=" 後面要換成剛剛取得的 伺服器金鑰。

data 的部分可以參考 Firebase Cloud Messaging HTTP Protocol 的說明,以下我們設定了 to alarms 這個 topic,另外只傳送了 data 區塊,在 android APP 不管是前景或背景,都會在 onMessageReceived 收到訊息,我們可在那邊發送 local notification 把資料顯示在系統列上。

curl -X POST -H "Content-Type: application/json" \
    -H "Authorization: key=yourkey" \
    https://fcm.googleapis.com/fcm/send \
    -d '{"to":"/topics/alarms", "priority":"high", "data":{"notifytitle":"測試title", "notifybody":"測試body", "訊息 key": "訊息 Topic!", "key2": "value2"}}'

也可以加上 notification 的區塊,這部分就會跟在 Firebase 頁面測試的結果一樣,Android APP 會直接收到 notification。

curl -X POST -H "Content-Type: application/json" \
    -H "Authorization: key= yourkey" \
    https://fcm.googleapis.com/fcm/send \
    -d '{"to":"/topics/alarms", "priority":"high", "notification":{"title" : "title","icon" : "new","body" : "訊息 body"}, "data":{"訊息 key": "訊息 Topic!", "key2": "value2"}}'

References

Firebase 心得(Cloud Messaging)

在Android中使用2016新版Firebase加快開發過程(一)

Firebase雲端訊息-發送測試通知至Android APP中

Firebase Cloud Messaging (FCM) 入門到進階應用(1) --- 開發環境建立

【Android Studio】從 GCM 移植到 FCM

Android: 利用Firebase實作使用者間的產品分享機制

Send Messages to Multiple Devices

Firebase push Notification using curl command — Devoid Backend


for ios

Push Notification教學:如何使用Firebase在iOS實現推播功能

[教學] 實作Google Firebase的Notification 使用Objective-C

2017/08/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/08/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/08/07

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