MPEG-DASH video concept

在html5中影片播放常用方式大致上分為兩種:

  • Progress Download
  • Adaptive Streaming

Progress Download

這是一種最容易實踐的方式,主要是透過http Range的header,跟server要取需要的檔案區塊,然後存在暫存檔案。實作面只需要透過video.src或者source.src指定好來源檔案,再搭配有支援range header的server,如nginxapache等的。

Adaptive Streaming

Progress類似,最大不同會先將原先的影片切片,依照描述檔或當初切割的時間,去抓取所需要的片段。同樣可以透過http Range去針對單個檔案抓去區塊,或者直接分割成多個檔案,依照url去區分需要抓取的檔案。若在html5中,可透過MSE(Media Source Extensions),將片段資料寫入。


MPEG-DASH串流概念

MPEG-DASH (Dynamic Adaptive Streaming over HTTP)本身就是Adaptive Streaming的一種,與apple的HLS (HTTP Live Streaming)是同樣的作用。在MPEG-DASH規格中的MPD描述檔案,主要是用來描述檔案的mimeTypecodecssegment資訊等等的,MPD實際上是一個xml檔案。

在瀏覽器的實作方式,主要是透過MediaSource這個元件,建立一個sourceBuffer,接著透過MPD所描述的Initialization這個初始區塊,依照Range去向server端要取所需要的segment,最後透過sourceBuffer.appendBuffer將抓取的資料塞入,即可完成初始化。然後你只要依照時間推算出你所需要的SegmentURL區塊,用一樣的方式把資料塞回sourceBuffer.appendBuffer即可。

每一個segment的時間公式計算如下:
1
time = (size * 8) / bitrate

套用MPD中的SegmentURL

1
2
var ranges = mediaRange.split('-');
var time = (ranges[1] - ranges[0]) * 8 / bandwidth;

產生MPD格式

需要產生MPD檔,及對應的mp4,你可以透過MP4Box建立:

1
MP4Box -dash 10000 -frag 1000 -rap bunny.mp4

若是無法產出MPD檔案,代表你的mp4格式是需要經過處理,你可以透過mp4fragment去轉換:

1
mp4fragment ~/Movies/bunny.mp4 fragmented.mp4

如果你不想透過mp4fragment,你也能使用ffmpeg

1
ffmpeg -i bunny.mp4 -movflags frag_keyframe+empty_moov fragmented.mp4

在你執行完MP4Box -dash 10000 -frag 1000 -rap bunny.mp4後,應該會得到.mpdinit.mp4

1
2
├── bunny_dash.mpd
└── bunny_dashinit.mp4

產出來的xml格式會如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<?xml version="1.0"?>
<MPD xmlns="urn:mpeg:dash:schema:mpd:2011" minBufferTime="PT1.500S" type="static" mediaPresentationDuration="PT0H30M0.157S" maxSegmentDuration="PT0H0M10.000S" profiles="urn:mpeg:dash:profile:full:2011">
<ProgramInformation moreInformationURL="http://gpac.sourceforge.net">
<Title>bunny_dash.mpd generated by GPAC</Title>
</ProgramInformation>

<Period duration="PT0H30M0.157S">
<AdaptationSet segmentAlignment="true" maxWidth="1280" maxHeight="720" maxFrameRate="7" par="16:9" lang="und">
<ContentComponent id="1" contentType="video" />
<ContentComponent id="2" contentType="audio" />
<Representation id="1" mimeType="video/mp4" codecs="avc3.64001f,mp4a.40.2" width="1280" height="720" frameRate="7" sar="1:1" audioSamplingRate="44100" startWithSAP="1" bandwidth="400359">
<AudioChannelConfiguration schemeIdUri="urn:mpeg:dash:23003:3:audio_channel_configuration:2011" value="2"/>
<BaseURL>bunny_dashinit.mp4</BaseURL>
<SegmentList timescale="1000" duration="10000">
<Initialization range="0-3330"/>
<SegmentURL mediaRange="3331-826541" indexRange="3331-3482"/>
<SegmentURL mediaRange="826542-1664049" indexRange="826542-826693"/>
<SegmentURL mediaRange="1664050-2506385" indexRange="1664050-1664201"/>
<SegmentURL mediaRange="2506386-2936161" indexRange="2506386-2506489"/>
<SegmentURL mediaRange="2936162-3767192" indexRange="2936162-2936313"/>
...
</SegmentList>
</Representation>
</AdaptationSet>
</Period>
</MPD>

其中一定會使用到的有RepresentationmimeTypecodecs作為MediaSource.addSourceBuffer初始化用,而bandwidth屬性則會與SegmentURLmediaRange用來計算時間,Initialization則是剛開始初始化區需要。


在HTML5中使用MPD格式

在這邊以AJAX已經取得MPD檔案為前提來實作,主要都是片段程式碼,若你要直接看完整範例,可直接參考微軟的MPEG-DASH實作範例

首先需要先建立MediaSource元件:

1
var mediaSource = new MediaSource();

接著透過URL.createObjectURL,將URL指定給video.src

1
videoElement.src = URL.createObjectURL(mediaSource);

註冊sourceopen事件,對sourceBuffer做初始化:

1
2
3
4
5
6
7
8
9
mediaSource.addEventListener('sourceopen', function (e) {
try {
videoSource = mediaSource.addSourceBuffer(`${mimeType} codecs="${codecs}"`);
initVideo();
} catch (e) {
log('Exception calling addSourceBuffer for video', e);
return;
}
},false);

參照MPD檔案中,抓取Initialization裡面的range屬性,向server要取初始化的區塊,再塞入到buffer之中:

1
2
3
4
function initVideo() {
var range = Initialization.getAttribute('range');
fetchRange(range);
}

fetchRange的function中,主要是透過http的range header,向server取得所需要的區塊,再塞入到buffer之中,換句話說,你只要載入Initializationrange區塊後,即可載入你所需要的SegmentURL片段。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function fetchRange(range) {
return new Promise(function (resolve, reject) {
var xhr = new XMLHttpRequest();
var url = 'http://localhost/bunny_dashinit.mp4';

xhr.open('GET', url);
xhr.setRequestHeader("Range", "bytes=" + range);
xhr.send();
xhr.responseType = 'arraybuffer';

xhr.addEventListener("readystatechange", function () {
if (xhr.readyState == xhr.DONE) { //wait for video to load
try {
videoSource.appendBuffer(new Uint8Array(xhr.response));
resolve();
} catch (e) {
log('Exception while appending', e);
reject();
}
}
}, false);
});
}

若你不想透過range header,向server取得資料,你也可以直切分割成多個檔案,直接透過url的方式指定:

1
MP4Box -dash 4000 -frag 4000  -rap -segment-name output/segment_ bunny.mp4

透過上面指令,最後你應該會取得到多個分割檔案,及segment_init和bunny_dash.mpd檔:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
├── bunny.mp4
├── bunny_dash.mpd
└── output
├── segment_1.m4s
├── segment_10.m4s
├── segment_100.m4s
├── segment_101.m4s
├── segment_102.m4s
├── segment_103.m4s
├── segment_104.m4s
├── segment_105.m4s
├── segment_106.m4s
├── segment_107.m4s
├── segment_108.m4s
├── segment_109.m4s
├── segment_11.m4s
├── segment_110.m4s
├── segment_111.m4s
├── segment_112.m4s
├── segment_113.m4s
├── segment_114.m4s
...
└── segment_init.mp4

若是針對上面檔案沒做處理,或者沒依照SegmentURLrange,直接塞入MediaSource中的sourceBuffer,你會發現MediaSource將會被abort。按照原先mp4檔案,你仍然可透過range,抓取部分區塊,最後組成一個blob檔案,透過URL.createObjectURL,設定到video.src,這時你會發現,你有抓取的區塊是可以播放。


下載相關執行檔


相關文章

Build a docker image

建置一個可輸入參數的docker image,只需要有dockerfile和另一隻shell,就能決定在container執行前,輸入你所要的參數。

Dockerfile:

以下dockerfile透過ubuntu/ping的image,將ping.sh複製到container裡面,並且提供執行的權限,最後再經由ENTRYPOINT,決定執行container要呼叫的shell

1
2
3
4
5
6
7
8
FROM ubuntu/ping

MAINTAINER sparrow.jang <sparrow.jang@gmail.com>
ENV HOME=/root SERVICE_TAGS=ping
COPY ping.sh ${HOME}/ping.sh
RUN chmod +x ${HOME}/ping.sh

ENTRYPOINT ["/root/ping.sh"]

ping.sh:

如果有參數才執行ping,否則將返回一個提示文字。

1
2
3
4
5
6
#!bin/sh
if [ -z "$1" ]; then
echo "please give me a host"
else
ping $1
fi

Build a image

透過docker build就能建置一個image,最主要功用是將配置好的image,再透過dockerfile來加工成新的image

1
docker build --no-cache -t sparrow/run-ping .

Run image

執行剛建置好的docker image

1
docker run -it sparrow/run-ping www.google.com.tw

如果你不想透過剛剛的ENTRYPOINT,那你可以在執行docker run的時候,加入--entrypoint去指定。

1
docker run -it --entrypoint /bin/sh sparrow/run-ping

Push docker hub

只要在docker hub建立好repository,接著就可以執行docker push,然後在你的repository就可以看到。

1
docker push sparrow/run-ping

Mac OSX

如果你剛好開啟一個port要對外,此時你又是透過osx的系統,使用virutalbox來開啟docker,這時你就必須使用docker-machine ip default來取得真實的ip,例如拿到192.168.99.100,然後你的開啟4000port,真實port和ip就會是192.168.99.100:4000

1
docker-machine ip default

Titanium Android native module

titanium本身有提供建立native module的指令,可透過指令產出基本可以使用的template,然後再做修改。

可參考官方的quick start,或者直接參考以下教學:

建立一個android native module

建立一個android module
1
ti create -p android -t module -d . -n test -u http:// --id com.example.test
build一個native java project
1
2
cd test/android
ant
將build好的zip,解壓縮至titanium的module路徑
1
unzip -o com.example.test-android-1.0.0.zip -d ~/Library/Application\ Support/Titanium/

KrollModule與KrollProxy

透過titanium的指令產生的native module,會建立出Module和Proxy兩個class,分別繼承KrollModuleTiViewProxy。其中會看到Kroll.methodKroll.getPropertyKroll.setProperty這些annotation,主要是會將method或者參數讓js去使用或存取寫入用。

取得剛建立好的native module
1
var test = require('com.example.test');
若是在KrollModule宣告的Kroll.method這類annotation,將可直接讓js module存取
java code
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Kroll.module(name="Test", id="com.example.test")
public class TestModule extends KrollModule {

@Kroll.method
public String example() {

return "hello world";
}

@Kroll.getProperty
public String getExampleProp() {

return "hello world";
}

@Kroll.setProperty
public void setExampleProp(String value) {

Log.d(LCAT, "set example property: " + value)
}

//....
}
js code
1
2
3
Ti.API.info( test.example() );
Ti.API.info("module exampleProp is => " + test.exampleProp);
test.exampleProp = "This is a test value";
若透過KrollProxy宣告的Kroll.method這類annotation,會先建立一個instance,在讓js使用
java code
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
@Kroll.proxy(creatableInModule=TestModule.class)
public class ExampleProxy extends TiViewProxy {

// ....

@Override
public TiUIView createView(Activity activity)
{

TiUIView view = new ExampleView(this);
view.getLayoutParams().autoFillsHeight = true;
view.getLayoutParams().autoFillsWidth = true;
return view;
}

// Methods
@Kroll.method
public void printMessage(String message)
{

Log.d(LCAT, "printing message: " + message);
}

@Kroll.getProperty @Kroll.method
public String getMessage()
{

return "Hello World from my module";
}

@Kroll.setProperty @Kroll.method
public void setMessage(String message)
{

Log.d(LCAT, "Tried setting module message to: " + message);
}

}
js code
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
if (Ti.Platform.name == "android") {
var proxy = test.createExample({
message: "Creating an example Proxy",
backgroundColor: "red",
width: 100,
height: 100,
top: 100,
left: 150
});

proxy.printMessage("Hello world!");
proxy.message = "Hi world!. It's me again.";
proxy.printMessage("Hello world!");
win.add(proxy);
}
若是想傳入function可透過KrollFunction
java code
1
2
3
4
5
6
@Kroll.method 
public void testCallback(KrollFunction cb) {

KrollDict dict = new KrollDict();
dict.put("test", 123);
cb.callAsync( getKrollObject(), dict );
}
js code
1
2
3
4
5
6
7
8
9
test.testCallback(function( evt ){
//1
Ti.API.info(arguments.length);
Ti.API.info(evt);
//{"test":123}
Ti.API.info(JSON.stringify( evt ));
//123
Ti.API.info(evt.test);
});

以上的程式碼皆為片段。

Connect Arduino Uno via Android Bluetooth

[Arduino端]

需要東西:

  • Arduino Uno
  • hc06 (藍芽模組,可自己挑一種自己需要)

Arduino只需要與藍芽模組對接即可:

  • VCC -> 5v
  • GND -> GND
  • TXD -> 8 (依照code setting)
  • RXC -> 9 (依照code setting)
載入需要函式庫
1
#include <SoftwareSerial.h>
定義連接藍牙模組的序列埠
1
SoftwareSerial BT(8, 9); // 接收腳, 傳送腳
配置監控視窗和藍芽模組鮑率
1
2
3
4
5
6
7
8
9
10
11
char val;  // 儲存接收資料的變數

void setup() {
Serial.begin(9600); // 與電腦序列埠連線
Serial.println("BT is ready!");

// 設定藍牙模組的連線速率
// 如果是HC-05,請改成38400
// HC-06 預設是9600
BT.begin(9600);
}
將監控視窗輸入的資料藍芽模組,並且列印至監控視窗
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void loop() {
// 若收到「序列埠監控視窗」的資料,則送到藍牙模組
if (Serial.available()) {
val = Serial.read();
BT.print(val);
}

// 若收到藍牙模組的資料,則送到「序列埠監控視窗」
if (BT.available()) {
val = BT.read();
// 此行為了測試android發送資料,回傳相同資訊回去
BT.write(val);
Serial.print(val);
}
}

HC-06的命令設定

監控視窗向藍芽模組下指令

  • AT : 測試,回應「OK」
  • AT+VERSION: 回應韌體版本。
  • AT+NAME: 設定名稱,中間不得有空白。(ex:AT+NAME123, 名稱設為123)
  • AT+PIN: 設定連線密碼。(ex:AT+PIN1234, 密把設為1234)
  • AT+BAUD: 設定鮑率。(ex:AT+BAUD4, 設為9600bps)
鮑率數值如下:
1
2
3
4
5
6
7
8
1 set to 1200bps
2 set to 2400bps
3 set to 4800bps
4 set to 9600bps (Default)
5 set to 19200bps
6 set to 38400bps
7 set to 57600bps
8 set to 115200bps

[Android端]

android部分只需要先開藍芽匹配好arduino端的藍芽,接著就可開啟自己寫的code,去發送資訊。

取得藍芽裝置, 如果無法找到藍芽裝置,將會回傳空值
1
BluetoothAdapter myBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
取得arduino的device
1
2
// arduino的mac address
BluetoothDevice device = mBluetoothAdapter.getRemoteDevice(address);
連接arduino端的藍芽
1
2
3
// MY_UUID 可自訂一組
BluetoothSocketBluetoothSocket btSocket = device.createRfcommSocketToServiceRecord(MY_UUID);
btSocket.connect();
寫入訊息到arduino端
1
2
3
4
String message = "Hello world";
byte[] msgBuffer = message.getBytes();
outStream = btSocket.getOutputStream();
outStream.write(msgBuffer);
接收來至於arduino端的訊息
1
2
3
4
5
6
7
inStream = btSocket.getInputStream();
int bytesAvailable = inStream.available();
if(bytesAvailable > 0)
{
byte[] packetBytes = new byte[bytesAvailable];
inStream.read(packetBytes);
}
在Activity class中找尋device
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
final BroadcastReceiver bReceiver = new BroadcastReceiver() {
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
// 探索到的device
if (BluetoothDevice.ACTION_FOUND.equals(action)) {
// 取得一個藍芽device
BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
// 顯示device的名稱和mac address
Log.d(device.getName() + "\n" + device.getAddress());
}
}
};

public void find(View view) {
if (myBluetoothAdapter.isDiscovering()) {
// 取消藍芽探索
myBluetoothAdapter.cancelDiscovery();
}
else {
// 開始探索藍芽裝置
myBluetoothAdapter.startDiscovery();
// 向activity註冊一個接收者
registerReceiver(bReceiver, new IntentFilter(BluetoothDevice.ACTION_FOUND));
}
}

My github

angularjs

javascript

jquery

polymer

  • spScrollView
    scroll裡面裝多個view,一次只顯示一個,可以互相切換,與android的scrollablevie功能相同。

python

docker

Image converter

在很多情況要顯示圖片清單時,都會需要固定的圖片size,如果想保留原始圖片,又想要有固定size的大小,大部份都會採用overflow的方式,將不需要的地方隱藏起來,或者是直接使用瀑布流的方式,這樣就不需要理會圖片高度,只要設定一個固定寬度即可,但是有時候的圖片,可能會過於龐大,所以需要裁剪適當的小,這時候可能就需要自動截取適當的大小。

Quick Start

imageConverter

LiveDemo

imageConverter

Install

使用bower安裝。

1
bower install imageConverter

Usage

截取圖片200x150,如果圖片比這size還要小,同樣也能截取出來,只是圖片可能會變成模糊不清。

1
2
3
4
5
6
7
8
var im = new Image();
im.src = "pig_head.png";

imageConverter.resizeAndOptimize( im, 200 ,150, function( data ){
var outIm = new Image();
outIm.src = data;
document.body.appendChild( outIm );
});

以上範例為js版的imageConverter,若要使用後端處理,也可透過python版本的imageConverter

The unsafe resource URL and cross origin on angular

在angular很常遇到compile template時出錯的情況,因為在angular中,使用比較嚴謹的判斷方式,只要看到不符合規範,會直接拋出exception

##Cross origin

以下是一個video的範例,設定影片的source:

1
2
3
<video width="320" height="240" controls>
<source src="{{source}}" type="video/mp4">
</video>

設定source的影片來源:

1
$scope.source = "http://other.sparrowhome.twbbs.org/movie.mp4"

這時就會發現angular拋出exception,出現cross domain的問題。

但是若是直接寫在html上呢?

1
2
3
<video width="320" height="240" controls>
<source src="http://other.sparrowhome.twbbs.org/movie.mp4" type="video/mp4">
</video>

此時你可以發現能正常的播放,但是在angular就是會拋錯,這時你可以透過$sceDelegateProvider,來設定黑名單白名單,決定哪個domain可以讀取這隻影片。

假設你要讓所有domain都能使用,在白名單可以這樣寫:

1
2
3
4
5
angular
.module('MyApp')
.config(['$sceDelegateProvider', function($sceDelegateProvider) {
$sceDelegateProvider.resourceUrlWhitelist(['**']);
})

若是你只要目前網域底下的子網域都可以讀取的話,可以這樣寫:

1
$sceDelegateProvider.resourceUrlWhitelist(['http://*.sparrowhome.twbbs.org'])

如果要透過黑名單可以直接使用resourceUrlBlacklist

##Unsafe resource URL

如果是讀取img,不管是cross domain都能正常讀取,不過還是會有不讀取的狀況。

例如你透過一個<input type="file">,去取得顯示image,像是<img ng-src="{{img}}" />

1
$scope.img = URL.createObjectURL(imgFile);

此時就會拋出unsafe:blobexception,依據URL.createObjectURL所取得url,可以很容易發現,最前方協定是不同的。例如URL.createObjectURL回傳一個blob:http%3A%2F%2Fsparrowhome.twbbs.org%2Fimage.png,此時不在預設的允許名單中,這時就要透過$compileProvider.imgSrcSanitizationWhitelist去改寫。

像是讓所有協定都通過:

1
2
3
4
angular.module('MyApp').config(['$sceDelegateProvider', function( $compileProvider ) {

$compileProvider.imgSrcSanitizationWhitelist(/^.*/);
}

也可自訂要通過特定的協定,像是/^(https?|ftp|file|blob|data):/,同樣的href也有aHrefSanitizationWhitelist可以去設定。在angular中使用要嚴謹的判斷方式,當然也可透過jquery或原生javascript繞過這問題,不過這樣就有點失去用angular的意義了,畢竟用template的方式,比起寫code跟為簡潔。

Neo4j - Transactional HTTP endpoint

要讀取、更新、刪除和建立neo4j的node或relationship,可以透過embedded或http的方式,去針對database去操作。

使用http的方式,基本上就是呼叫rest api,db server會依照不同的狀況回傳response status code,同樣也會依照get、post、put、delete去告訴db server,目前要處理方式。

然而neo4j中,提供了很多http的api,可以針對db操作,像是rest api transactionalCypher這類的neo4j rest api

Embedded graph database

透過embedded的方式,會直接在嵌入在你所撰寫的project裡,指定好要儲存的db的path,換句話說,檔案會儲存在server的local端,與以往的mysql、mongodb透過socket的方式不同,有點類似sqlite。

使用embedded的方式,大概會如下方(參考官方):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
GraphDatabaseService graphDb = new GraphDatabaseFactory().newEmbeddedDatabase( "/your_db_path" );

try {
Transaction tx = graphDb.beginTx();

Node firstNode = graphDb.createNode();
firstNode.setProperty( "message", "Hello world" );
System.out.print( firstNode.getProperty( "message" ) );

// Database operations go here
tx.success();

}catch( Exception e ){

//do something
}

Transactional HTTP endpoint

在這簡單介紹Transactional HTTP endpoint,首先neo4j的每個query都會經由transaction機制去處理,所以可以透過已知的transactionid,下一整串的query,最後在決定要rollbackcommit,如果太久沒有commit,neo4j會有個timeout時間(預設60 sec),就會取消先前的query結果。

Begin a transaction

建立一個transaction,之後可以沿用這個transactionid,去執行一連串的query。

官方Example:

Request headers:

1
2
3
POST http://localhost:7474/db/data/transaction
Accept: application/json; charset=UTF-8
Content-Type: application/json

Request json data:

1
2
3
4
5
6
7
8
9
10
{
"statements" : [ {
"statement" : "CREATE (n {props}) RETURN n",
"parameters" : {
"props" : {
"name" : "My Node"
}
}
} ]
}

statements可以一次下多筆query,statement會參照parameters的參數。

Response headers:

1
2
3
201: Created
Content-Type: application/json
Location: http://localhost:7474/db/data/transaction/7

Response json data:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
"commit" : "http://localhost:7474/db/data/transaction/7/commit",
"results" : [ {
"columns" : [ "n" ],
"data" : [ {
"row" : [ {
"name" : "My Node"
} ]
} ]
} ],
"transaction" : {
"expires" : "Mon, 03 Feb 2014 13:26:48 +0000"
},
"errors" : [ ]
}

每個response都會回傳一個transactionexpires,這個expires的指的就是transaction的存活時間,而commit這個field包含transactionid,這上面這範例id為7。

Execute statements in an open transaction

使用先前建立transaction,依照transactionid,繼續執行query,若是transaction失效,最後會取得到error messsage。

官方Example:

Request headers:

1
2
3
POST http://localhost:7474/db/data/transaction/7
Accept: application/json; charset=UTF-8
Content-Type: application/json

Request json data:

1
2
3
4
5
{
"statements" : [ {
"statement" : "CREATE n RETURN n"
} ]
}

Response headers:

1
2
200: OK
Content-Type: application/json

Response json data:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
"commit" : "http://localhost:7474/db/data/transaction/7/commit",
"results" : [ {
"columns" : [ "n" ],
"data" : [ {
"row" : [ {
} ]
} ]
} ],
"transaction" : {
"expires" : "Mon, 03 Feb 2014 13:26:48 +0000"
},
"errors" : [ ]
}

Execute statements in an open transaction in REST format for the return

在還沒commit之前,只有node會被建立,而建立的propertiesrelationship等的…,一直到commit之後,在database中才有辦法query的到。而REST這個參數,可以幫助你取得目前node間的狀況,response會回傳各種想要查詢的rest api。

官方Example:

Request headers:

1
2
3
POST http://localhost:7474/db/data/transaction/1
Accept: application/json; charset=UTF-8
Content-Type: application/json

Request json data:

1
2
3
4
5
6
{
"statements" : [ {
"statement" : "CREATE n RETURN n",
"resultDataContents" : [ "REST" ]
} ]
}

Response headers:

1
2
200: OK
Content-Type: application/json

Response json data:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
{
"commit" : "http://localhost:7474/db/data/transaction/1/commit",
"results" : [ {
"columns" : [ "n" ],
"data" : [ {
"rest" : [ {
"paged_traverse" : "http://localhost:7474/db/data/node/12/paged/traverse/{returnType}{?pageSize,leaseTime}",
"labels" : "http://localhost:7474/db/data/node/12/labels",
"outgoing_relationships" : "http://localhost:7474/db/data/node/12/relationships/out",
"traverse" : "http://localhost:7474/db/data/node/12/traverse/{returnType}",
"all_typed_relationships" : "http://localhost:7474/db/data/node/12/relationships/all/{-list|&|types}",
"property" : "http://localhost:7474/db/data/node/12/properties/{key}",
"all_relationships" : "http://localhost:7474/db/data/node/12/relationships/all",
"self" : "http://localhost:7474/db/data/node/12",
"properties" : "http://localhost:7474/db/data/node/12/properties",
"outgoing_typed_relationships" : "http://localhost:7474/db/data/node/12/relationships/out/{-list|&|types}",
"incoming_relationships" : "http://localhost:7474/db/data/node/12/relationships/in",
"incoming_typed_relationships" : "http://localhost:7474/db/data/node/12/relationships/in/{-list|&|types}",
"create_relationship" : "http://localhost:7474/db/data/node/12/relationships",
"data" : {
}
} ]
} ]
} ],
"transaction" : {
"expires" : "Mon, 03 Feb 2014 13:26:44 +0000"
},
"errors" : [ ]
}

Reset transaction timeout of an open transaction

transaction是會過期的,要解決這問題,只要向http://localhost:7474/db/data/transaction/1發送,時效性就會自動延長,若不想下query,只想延長時效。可以發送一個不帶statements內容的的query。

Request headers:

1
2
3
POST http://localhost:7474/db/data/transaction/1
Accept: application/json; charset=UTF-8
Content-Type: application/json

Request json data:

1
2
3
{
"statements" : [ ]
}

Commit an open transaction

要確定存入database,只要commit目前的transaction即可。

Request:

1
2
3
POST http://localhost:7474/db/data/transaction/4/commit
Accept: application/json; charset=UTF-8
Content-Type: application/json

Rollback an open transaction

要取消之前transaction中執行過的query,只要使用rollback即可。

Request:

1
2
DELETE http://localhost:7474/db/data/transaction/3
Accept: application/json; charset=UTF-8

Begin and commit a transaction in one request

如果要執行query後,馬上commit,可以直接透過/db/data/transaction/commit

Request headers:

1
2
3
POST http://localhost:7474/db/data/transaction/commit
Accept: application/json; charset=UTF-8
Content-Type: application/json

Request json data:

1
2
3
4
5
{
"statements" : [ {
"statement" : "CREATE n RETURN id(n)"
} ]
}

Return results in graph format

如果想要查詢節點的圖形結構關係,可以在resultDataContents加入graph這個參數。

Request headers:

1
2
3
POST http://localhost:7474/db/data/transaction/commit
Accept: application/json; charset=UTF-8
Content-Type: application/json

Request json data:

1
2
3
4
5
6
{
"statements" : [ {
"statement" : "CREATE ( bike:Bike { weight: 10 } )CREATE ( frontWheel:Wheel { spokes: 3 } )CREATE ( backWheel:Wheel { spokes: 32 } )CREATE p1 = bike -[:HAS { position: 1 } ]-> frontWheel CREATE p2 = bike -[:HAS { position: 2 } ]-> backWheel RETURN bike, p1, p2",
"resultDataContents" : [ "row", "graph" ]
} ]
}

Response headers:

1
2
200: OK
Content-Type: application/json

Response json data:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
{
"results" : [ {
"columns" : [ "bike", "p1", "p2" ],
"data" : [ {
"row" : [ {
"weight" : 10
}, [ {
"weight" : 10
}, {
"position" : 1
}, {
"spokes" : 3
} ], [ {
"weight" : 10
}, {
"position" : 2
}, {
"spokes" : 32
} ] ],
"graph" : {
"nodes" : [ {
"id" : "17",
"labels" : [ "Wheel" ],
"properties" : {
"spokes" : 3
}
}, {
"id" : "16",
"labels" : [ "Bike" ],
"properties" : {
"weight" : 10
}
}, {
"id" : "18",
"labels" : [ "Wheel" ],
"properties" : {
"spokes" : 32
}
} ],
"relationships" : [ {
"id" : "9",
"type" : "HAS",
"startNode" : "16",
"endNode" : "17",
"properties" : {
"position" : 1
}
}, {
"id" : "10",
"type" : "HAS",
"startNode" : "16",
"endNode" : "18",
"properties" : {
"position" : 2
}
} ]
}
} ]
} ],
"errors" : [ ]
}


Python example

上述對照的request json datarequest headers可參考底下的範例,每個語言使用方式差不多。

dependency

install

1
sudo pip install requests

code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import requests, json       

headers = {
"Accept": "application/json; charset=UTF-8",
"Content-Type": "application/json"
}

data = {
"statements" : [ {
"statement" : "MATCH (n {name: {name}}) RETURN n",
"parameters" : {
"name":"Sparrow"
}
} ]
}

url = "http://localhost:7474/db/data/transaction"

req = requests.post( url , data = json.dumps( data ), headers = headers )

print req.text

可參照官方的Transactional HTTP endpoint,和rest api回傳的status code

Neo4j - Getting started

neo4j relationship pic

neo4j是一種圖形資料庫,同時也是nosql的一種,每筆資料為一個node,在node上可以定義label,類似於其他資料庫的table/collection。假設有個Person的collection,要定義PersonPerson之間的關係,勢必得額外定義一個collection或者field,去描述PersonPerson之間的關係,但是neo4j並不需要額外的collection或field,只要定義好Person這個label,當關係產生時,在額外描述PersonPerson之間關係(relationship)為何,換句話說一開始建立Person的node時,並不需要定義PersonPerson的關係。

舉例來說,如果今天Tom和Jack就讀同一個學校同一個班級,現在的關係是classmate,當熟識之後,這時關係可能是friend,這時就必須建立friend的關聯(朋友本身是雙向的,這邊以單向為例)。

tom-->jack

在neo4j建立Persontypefriend關聯的語法會如下:

1
CREATE (Tom {name:"tom"})-[:friend]->(Jack {name:"jack"});

如果單純的建立relationship,需先抓取tom和jack兩個人:

1
2
3
4
MATCH (tom), (jack)
WHERE tom.name = "tom" and jack.name = "jack"
CREATE (tom)-[:friend]->(jack)
RETURN tom,jack;

Neo4j中的Nodes

在neo4j中的node,可以寫成以下幾種方式:

  • (a)
  • (a:Person) 指定label為Person
  • () 以匿名的方式

括號中的a,如同(),唯一不同之處,可以在MATCH時,透過a來做比對,而透過:Person這種指定label的方式,可以明確要查詢的label名稱,同樣也會增加query的效率。

Neo4j中的Relationships

在neo4j中的關係(relationship)表示方式,以中括弧[]作為表示,可以參考以下幾種:

  • (a)-[r]->(m) 定義a點到m點之間的關係
  • (a)-[:ACTED_IN]->(m) 定義a點到m點之間的關係,其中定義:ACTED_IN的label,為描述a與m之間的關係

Labels

不管是noderelationship都可以定義一個甚至多個label。下方以定義一個label為例:

  • (a:Person)
  • (a:Person {name:"Keanu Reeves"})
  • (a:Person)-[:ACTED_IN]->(m:Movie)

CREATE

建立一個node,label為Person{name:"tom"}為node被建立的資料。最前面的tom只是一個變數,在這個語法中並不會被使用到。

1
CREATE (tom:Person {name:"tom"});

建立classmaterelationship,其中classmaterelationship中,被稱為type

1
CREATE (Tom {name:"tom"})-[:classmate]->(Jack {name:"jack"});

如果要描述關係的其他屬性,例如什麼時間點成為classmate,可在關係中附加資料。

1
CREATE (Tom {name:"tom"})-[:classmate {start_date:"2012/01/05"}]->(Jack {name:"jack"});

CREATE INDEX

依照name去建立index,在search中效率會提昇許多。

1
CREATE INDEX ON :Person(name);

MATCH

如果要回傳所有node筆數的資料如下。

1
MATCH (p) RETURN p;

找出有建立relationship的node,可以這樣寫。

1
MATCH (t)-->(j) RETURN t,j;

若是要依照type去search,例如要找是friend關係的,列出Person的name。

1
MATCH (t)-[:friend]->(j) RETURN t.name,j.name;

查詢PersonPerson之間的關係,可以透過type這個function。

1
MATCH (t)-[r]->(j) RETURN t.name, type( r ),j.name;

WHERE

要search某個屬性,可透過where,例如找name是tom的人。

1
2
3
MATCH (t)
WHERE t.name = "tom"
RETURN t;

同樣的查詢條件,也可直接透過下面這種方式,寫法更為簡短。

1
2
MATCH (t {name:"tom"})
RETURN t;

Order

依照出生日期作排序。

1
2
3
MATCH (a:Person)
RETURN a.name, a.born
ORDER BY a.born

Limit and Skip

limit指的是query後的資料要抓幾筆,而skip則是從第幾筆開始抓取。

從第10筆開始抓取,最多抓10筆。

1
2
3
4
MATCH (a)
RETURN a.name
SKIP 10
LIMIT 10;

nodes

在path中,回傳所有節點。

1
MATCH p = (t)-[:friend]->(j) RETURN nodes(p);

rels

在path中,回傳節點對應的關係。

1
MATCH p = (t)-[:friend]->(j) RETURN rels(p);

DISTINCT

假設有兩個Person的名稱為相同,此時要列出所有Person的name,這時可透過DISTINCT把相同name的都過濾掉,

1
2
MATCH (p:Person)
RETURN DISTINCT p;

SET

SET可用於noderelationshiplabel的屬性修改。

例如例如例如例如設定movie釋出時間。

1
2
3
4
MATCH (movie:Movie)
WHERE movie.title="Mystic River"
SET movie.released = 2003
RETURN movie;

如果要加入一個label,可直接使用SET

1
2
3
4
MATCH (movie:Movie)
WHERE movie.title="Mystic River"
SET movie:Movie
RETURN movie

REMOVE

移除label,直接使用REMOVE即可。

1
2
3
4
MATCH (movie:Movie)
WHERE movie.title="Mystic River"
REMOVE movie:Movie
RETURN movie

DELETE

刪除noderelationship

刪除一個節點。

1
2
MATCH (n { name: 'Peter' })
DELETE n

MATCH Depth Relationship

a到b所包含多個relationship

1
(a)-[*]->(b)

a到b包含1~3個relationship,也就是中間含有0~3個node。

1
(a)-[*1..4]->(b)

shortestPath

會自動找出最短路徑,也就是假設a到b點,可能有10個路徑,只會傳一個最短路徑。

1
2
3
4
MATCH (keanu:Person {name:"Keanu Reeves"}), 
(kevin:Person {name:"Kevin Bacon"})
MATCH p=shortestPath((keanu)-[:KNOWS*]->(kevin))
RETURN p;

extract

可在extract執行運算,之後的結果會回傳一個list。寫法包括以下2種:

  • extract(n in nodes(path) | n.name)
  • [ x in coll | expr ]
1
extract(x in [1,2,3] | x*x)

取得keanu知道(relationship)kevin的最短路徑,其中[1..-1]則捨棄list的第一個和最後一個值。

1
2
3
4
MATCH (keanu:Person {name:"Keanu Reeves"}), 
(kevin:Person {name:"Kevin Bacon"})
MATCH p=shortestPath((keanu)-[:KNOWS*]->(kevin))
RETURN [ n in nodes(p)[1..-1] | n.name ];

neo4j可用的function可以參考官方文件。另外可以參考官方的語法對照表官方的online tutorial,對於neo4j學習會有很大幫助。

The docker basic operation

What is the docker?

docker是一個能讓你的server中,擁有乾淨的環境,以及運行各種的linux平台,例如你開啟很多web service,可能有python、php、tomcat…,這時你可透過docker,讓這些web service,都擁有各自的環境,最後透過server的apache,proxy到各項服務,至於docker跟vm有什麼不同,可以參考這篇

Pull images

docker中要使用哪個作業系統,可以透過pull這個指令,直接抓取你所需要的image來源。

假設要抓取ubuntu:

1
sudo docker pull ubuntu

之後在用images這指令去查詢目前以抓下來的source:

1
sudo docker images

會顯示以下載的image列表:

1
2
3
ubuntu                latest              8dbd9e392a96        4 months ago        131.5 MB (virtual 131.5 MB)
learn/tutorial latest 8dbd9e392a96 2 months ago 131.5 MB (virtual 131.5 MB)
learn/ping latest effb66b31edb 10 minutes ago 11.57 MB (virtual 143.1 MB)

Run

這個指令主要用來執行image裡面的指令,換句話說image已經安裝ubuntu,就可以選擇目前已經安裝的指令去執行。

像是執行linux中,拿來下指令的bash

1
sudo docker run -i -t ubuntu /bin/bash

執行之後就可以隨意在ubuntu下任何指令,像是常用的apt-get,安裝一些web server,如apache、nginx等的。

若是要開一個常駐的程式,只要在run加上-d,此時docker執行的指令就會變成常駐程式。

例如執行一個無窮迴圈的shell:

1
$JOB = $(sudo docker run -d ubuntu /bin/sh -c "while true; do echo Hello world; sleep 1; done")

$JOB會取得到一個實際執行的container id

如果要看到目前的執行狀況,可透過logs這個指令:

1
sudo docker logs $JOB

要刪除這個只要透過剛剛取得的container id即可:

1
sudo docker kill $JOB

假設要開啟一個tcp的服務,可以在開啟的時候就輸入對內port號(docker開啟的ubuntu port):

1
$JOB=$(sudo docker run -d -p 4444 ubuntu:12.10 /bin/nc -l 4444)

docker會將正在執行的port號,對應實體機器目前的port,例如docker中的4444 port,對到目前機器的127.0.0.1:49011(port範圍落在49000..49900)。

取得目前機器正在運行的port:

1
PORT=$(sudo docker port $JOB 4444 | awk -F: '{ print $2 }')

利用實際的port將訊息丟到docker的container中:

1
echo hello world | nc 127.0.0.1 $PORT

在透過logs查詢即可:

1
echo "Daemon received: $(sudo docker logs $JOB)"

Commit

docker中運行到一半的container,如果已經安裝或配置完畢,要保留目前的狀態,此時必須要把他存成一個image

先查詢目前的正在執行的container id

1
sudo docker ps

commit成image檔案:

1
sudo docker commit fb66b3 nginx/ubuntu

若是忘記就離開container也沒關係,可以透過ps -a的指令找到:

1
sudo docker ps -a

Exec

當你已經運行了一個docker image,此時可透過docker exec進入到運行中的container

1
docker exec nginx/ubuntu /bin/sh

Ps

查詢目前在正運行的container:

1
sudo docker ps

輸入參數-a,會將停止運行的container一起顯示出來:

1
sudo docker ps -a

Images

查詢目前擁有的images檔案,像是透過pull和自己commit

1
sudo docker images

Inspect

如果要查詢正在運行的container資訊,可以透過inspect,可以將目前container正在運行的資訊顯示出來,例如docker提供的虛擬的ip。

可以先透過sudo docker ps查詢container id,在執行inspect

1
sudo docker inspect a5e78640ece4

Dockerfile

如果是要下一連串的指令,而且每次都必須去執行的話,可透過dockerfile這個檔案,把你要執行的動作寫在上面,類似的案例像是,要佈署新版project時,那這時要做的可能是抓取project,執行一些build的動作,這時透過dockerfile就可以一鍵執行完,而不需要每次登到container中去修改。

首先要先建立一個,dockerfile

1
2
3
4
5
6
7
8
9
10
11
# Nginx
#
# VERSION 0.0.1

FROM ubuntu

# make sure the package repository is up to date
RUN echo "deb http://archive.ubuntu.com/ubuntu precise main universe" > /etc/apt/sources.list
RUN apt-get update

RUN apt-get install -y inotify-tools nginx apache2 openssh-server

透過FROM去決定執行哪個image來源,然後可使用RUNCMD,去對目前的image去執行目前的bash指令。

執行dockerfile裡面內容,其中-t是要存的image目標的名稱:

1
sudo docker build -t shykes/myapp .

Export and import

如果要存成檔案可直接透過export,要匯入則是import

將目前正在運行的container id匯出成檔案:

1
docker export a1bcbabsdhb323h2b > myfile.tar

import目前的檔案,存成image來源:

1
cat myfile.tar | docker import - newimagelocal

還有滿多沒介紹的,例如push可以發布自己的image供其他人下載等的,有興趣可以深入研究。