kakts-log

programming について調べたことを整理していきます

Golang listパッケージを使ったdoubly linked listの操作

Golang listという標準パッケージを使ってdoubly linked listの操作を簡単に行えるので、まとめます。

list - The Go Programming Language

使い方

listパッケージを使うためには、公式ドキュメントにある通り、"container/list"をインポートします。

list.New()で新たなlistインスタンスを生成する事ができます。

http://golang-jp.org/pkg/container/list/#New

func New() *List

List構造体とListがもつ要素を表すElement構造体は、以下のようなデータ構造となります。

// List represents a doubly linked list.
// The zero value for List is an empty list ready to use.
type List struct {
    root Element // sentinel list element, only &root, root.prev, and root.next are used
    len  int     // current list length excluding (this) sentinel element
}

// Element is an element of a linked list.
type Element struct {
    // Next and previous pointers in the doubly-linked list of elements.
    // To simplify the implementation, internally a list l is implemented
    // as a ring, such that &l.root is both the next element of the last
    // list element (l.Back()) and the previous element of the first list
    // element (l.Front()).
    next, prev *Element

    // The list to which this element belongs.
    list *List

    // The value stored with this element.
    Value interface{}
}

List構造体は、Element型のroot要素を一つもつのと、List自体の長さを表すlenを持ちます。
そしてElement構造体は、要素自身がもつ値Valueと、自分が属するListのポインタ Listと、さらには、自分の要素の前・後に位置する要素のポインタ next, prevを持ちます。

List Element構造体を使って、他の言語でもよく見られる一般的なdoubly linked listの構造をあらわしています。

package main

import (
    "container/list"
    "fmt"
)

func main() {
    // create a new list
    l := list.New()
    l.PushBack(1) // l {1}

    // list mに要素を2つ追加
    m := list.New()
    m.PushBack(2) // m {2}
    m.PushBack(3) // m {2, 3}
    fmt.Printf("Length of m is %d\n", m.Len())

    // lの後ろにmをつなげる
    l.PushBackList(m)

    // Iterate through list and print its contents.
    // 1 2 3と表示される
    for e := l.Front(); e != nil; e = e.Next() {
        fmt.Println(e.Value)
    }

    // l の先頭に要素追加
    l.PushFront(5) // l {5, 1, 2, 3}

    // l の先頭にmの要素をつなげる
    l.PushFrontList(m) // l {2, 3, 5, 1 , 2, 3}

    // Iterate through list and print its contents.
    // 2 3 5 1 2 3 と表示される
    for e := l.Front(); e != nil; e = e.Next() {
        fmt.Println(e.Value)
    }

    // 最後の要素を削除する
    l.Remove(l.Back())
    // Iterate through list and print its contents.
    // 2 3 5 1 2 と表示される
    for e := l.Front(); e != nil; e = e.Next() {
        fmt.Println(e.Value)
    }

}

非常に簡単に使えます。
Goに関しては勉強中で、サードパーティのコレクションライブラリなどもあるかどうかは調べれてないですが、
listパッケージを使って問題なくdoubly linked listを利用できます。

node.js へのコントリビュート

Merged. github.com

最近リリースされたhttp2コアモジュールのdocsの修正のみですがnode.js本家へのPRマージされました。

今後は実際のコードの改修とかどんどんやっていきたい。

node.js nightlyバージョンで使えるようになった http2モジュールについて

概要

先日 node.jsにおいて http/2の実装がマージされました。
experimentalな機能で、現在だとnode nightlyバージョン(9.x系)で実行時にオプションを付けることで http2 コアモジュールが利用可能です。
近々node.js 8.xに取り込まれるようで年内にはちゃんと使えるようです。
今回は、このhttp2コアモジュールについて簡単な使い方と内部実装について調べたことをまとめます。

ドキュメントはnodeのmasterブランチにマージされており、見ることができます。
- github.com

http2コアモジュールの実装について

http2の実装はnghttp2というCライブラリが採用されております。 このライブラリはhttp/2の実装のなかで人気であり、 RFC7540 HTTP/2 とRFC7541の仕様に基づいて実装されています。
- github.com

http2 コアモジュールのJS apiは下記で定義されています。
- node/http2.js at master · nodejs/node · GitHub

http/2のJS apiの各実装は lib/internal/http2配下のファイルにあります。内部の実装を見たい人はここで見ることができます。
- node/lib/internal/http2 at master · nodejs/node · GitHub

nghttp2ライブラリへのインターフェースはsrc/node_http2.ccと src/node_http2.hで定義されています
nghttp2とコアモジュールのやり取りはこのあたりを見ればよいかと思います。
- node/node_http2_core.cc at master · nodejs/node · GitHub
- node/node_http2.h at master · nodejs/node · GitHub

上記のhttp2コアモジュールを使うにあたり、nodeの実行時に –expose-http2 フラグを追加することで、 require(‘http2’) が使えるようになります。
これは、すでに http2というnpmモジュールが存在しているため、 –expose-http2フラグを付けないと下記のnpmモジュールをrequireするようになり、既存のnpm http2モジュールを使っているプロジェクトは注意が必要です。
- www.npmjs.com

node.js nightly版のインストール

http2のコアモジュールを使うために、node.jsの最も最新のnightly版(現時点で9.x系)を使えるようにします。
mac OSの環境が前提ですが、 node.js nightly版を簡単にインストールできる node-nightlyというnpmモジュールを使います。

node-nightlyモジュールインストール
$npm install --global node-nightly

nightly最新版のnodeインストール
$ node-nightly 
Downloading the nightly version, hang on...
node-nightly is available on your CLI!

nightlyバージョンの確認
$ node-nightly -v
 New nightly available. To upgrade: `node-nightly --upgrade` 
v9.0.0-nightly20170806e96ca62480

これでnode.js nightly版を簡単にインストールすることができました。

http2.createSecureServerを使ってhttp2サーバを作る

ここでさっそくhttp2のTLS/SSLを使ったセキュアなサーバを作ってみます。
http2.createSecureServer()というメソッドで、サーバのインスタンスを作成することができます。

http2.createSecureServer()について

http2.createSecureServer()の内部の実装は、主要なところだけ抜粋すると以下のようになっています。 - https://github.com/nodejs/node/blob/master/lib/internal/http2/core.js#L2427-L2434

function createSecureServer(options, handler) {
  if (typeof options === 'function') {
    handler = options;
    options = Object.create(null);
  }
  debug('creating http2secureserver');
  // Http2SecureServerのインスタンスを返す
  return new Http2SecureServer(options, handler);
}

・・・

// Http2SecureServerクラス
class Http2SecureServer extends TLSServer {
  // コンストラクタ
  constructor(options, requestListener) {
    options = initializeTLSOptions(options);
    super(options, connectionListener);
    this[kOptions] = options;
    this.timeout = kDefaultSocketTimeout;
    this.on('newListener', setupCompat);
    if (typeof requestListener === 'function')
      this.on('request', requestListener);
    this.on('tlsClientError', onErrorSecureServerSession);
    debug('http2secureserver created');
  }

  // setTImeoutメソッド
  setTimeout(msecs, callback) {
    this.timeout = msecs;
    if (callback !== undefined) {
      if (typeof callback !== 'function')
        throw new errors.TypeError('ERR_INVALID_CALLBACK');
      this.on('timeout', callback);
    }
    return this;
  }
}

このメソッドの戻り値として、Http2SecureServerクラスのインスタンスを返します。
Http2SecureServerクラスはTLSServerクラスを継承しており、接続においてSSL/TLSを使う事となります。

TLSServerクラスはtlsというコアモジュールで定義され、実装は下記になります。

tls_wrapでTLSServerクラスの実装が見れます。
[https://github.com/nodejs/node/blob/master/lib/
tls_wrap.js#L784-L893]

かなり階層が深くなりますが、TLS/SSLを使ったセキュアなサーバを作る場合、http2.createSecureServerの引数にオプションを渡すことができ、 接続において使うTLS/SSLの証明書や秘密鍵を設定することができます。 optionとして、オブジェクトのkey, certにそれぞれ指定してあげます。
node/_tls_wrap.js at master · nodejs/node · GitHub

TLS/SSL証明書 秘密鍵の作成

今回はここで証明書と秘密鍵を指定するために、ローカルで自前で準備していきます。
ここではあくまでローカル環境でのhttp2サーバを作ることが目的のため、自前でローカルpcでopensslコマンドを使って作成します。
- stackoverflow.com

mac OSでopensslを使って以下のコマンドを実行することでカレントディレクトリに下記2つのファイルが生成されるので、 サーバインスタンス生成時にこれを指定します。
- server.crt: SSLサーバ証明書 - server.key: SSL公開鍵暗号用の秘密鍵

サーバを立ててみる

さっそく上記の説明を踏まえてhttp2サーバを立ててみます。
ここではあくまでhttp2コアモジュールのサンプルコードをそのまま使用します。

http2サーバインスタンス生成時に 証明書・秘密鍵を指定し、 3000番ポートで待ち受けます。
https://localhost:3000をブラウザでアクセスしてhello worldを表示させる簡単なものを作ります。

サーバを立てた後、アクセスがくると サーバインスタンスに対して"stream"イベントが発火されるので、 ハンドラ関数を設定します。
ここではresponse statusと hello worldを記述した簡単なhtmlをstreamで返します。

// http2コアモジュールをrequire
const http2 = require('http2');

// createSecureServerでのオプションを指定する
const options = {
  key: fs.readFileSync('server.crt'), // 公開鍵暗号の秘密鍵
  cert: fs.readFileSync('server.key') // TLS/SSL サーバ証明書
};

// Create a plain-text HTTP/2 server
const server = http2.createSecureServer(options);

// アクセスが来た場合、streamイベントが発火されう
server.on('stream', (stream, headers) => {
  stream.respond({
    'content-type': 'text/html',
    ':status': 200
  });
  stream.end('<h1>Hello World</h1>');
});

server.listen(3000);

chromeでアクセスする

ここで、サーバを立ち上げる事ができたので、chromeブラウザで https://localhost:3000へアクセスしてみます。
f:id:kakts:20170809005348p:plain
上述したように、今回はTLS/SSL証明書を自前で用意したため、chromeでwarning画面がでますが、無視して、「詳細設定」→「localhost にアクセスする(安全ではありません)」をクリックすることでアクセスが可能です。

f:id:kakts:20170809005555p:plain

まとめ

これでhttp/2のセキュアなサーバにアクセスできました。 今回は簡単な開設のため、http/2固有の機能を使ったサーバの作り方は解説せず、ここで終わります。
http/2固有の機能を使うことでよりパフォーマンスが向上するため、時間があるときに、http/2RFCをちゃんと読んで仕様を把握していきたいと思います。

参考

qiita.com

d.hatena.ne.jp

babel ES6からES5へのトランスパイル

概要

babel.jsをつかって ES6のシンタックスで書かれたjavascriptファイルをES5のシンタックスにトランスパイルする方法をまとめました。
実務で使ったことなかったので勉強がてらさらっとまとめます。

babel-cliのインストー

babelをコマンドラインで利用するに当たって、babelのコマンドラインクライアントである、npm モジュールのbabel-cliをインストールする必要があります。

www.npmjs.com

開発環境のみで使うので –save-devでpackage.jsonに対してdevDependenciesにモジュールの情報を追記させます。

$ npm install --save-dev babel-cli

今回はサンプルとして今回 hapiというhttpサーバモジュールを使うのでこちらもインストールしておきます

$ npm install --save hapi

.babelrcにプラグイン設定記述

babelでトランスパイルする際に、利用するプラグインを指定する必要があります。
今回はES6(ES2015とも言う)からES5へトランスパイルするため、ES2015 presetを指定します。
babeljs.io

babelコマンドの –presetsオプションでも指定できますが、毎回打つのは面倒なので、 babelの設定を記述するファイルである .babelrcを作成し、json形式で記述します。

.babelrc

{
  "presets": ["es2015"]
}

presets項目に使用するプラグイン名を指定します。

トランスパイル元のjsファイル作成

ここで、トランスパイル元のjsファイルである src/index.jsを作成します。
hapiモジュールを使って簡単なhttpサーバを作ります。
全てES6(ES2015)のシンタックスで記述します。

import Hapi from 'hapi';

const server = new Hapi.Server();
server.connection({
  host: 'localhost',
  port: 8000
});

// Add routing
server.route({
  method: 'GET',
  path: '/hello',
  handler: function(request, reply) {
    console.log('[/hello] requested');
    reply('hello world');
  }
});

server.start();

これを importや constを使わない、ES5形式にトランスパイルさせます。

babelコマンドをつかったトランスパイル

トランスパイルした後のファイルは dist/index.jsに作成させるとします。

さっそくコマンドラインでbabelコマンドを使ってトランスパイルさせます。
babelコマンドの –out-fileオプションをつかって、トランスパイル後のファイルの出力先を指定します。

$ babel src/index.js --out-file dist/index.js

これによって、 dist/index.jsにES5形式にトランスパイルされたjsファイルができあがります。

'use strict';

var _hapi = require('hapi');

var _hapi2 = _interopRequireDefault(_hapi);

function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }

var server = new _hapi2.default.Server();
server.connection({
  host: 'localhost',
  port: 8000
});

// Add routing
server.route({
  method: 'GET',
  path: '/hello',
  handler: function handler(request, reply) {
    console.log('[/hello] requested');
    reply('hello world');
  }
});

server.start();

元のjsファイルで使っていた import, const が使われずにそれぞれ require varを使うようにトランスパイルされているのが確認できます。

babelをつかったトランスパイルの手順は以上となります。 まだ試していませんが、最新のES2017形式のファイルをES5にトランスパイルする場合は ES2017 presetを使えばできます。
babeljs.io

node.js stringからbufferへの変換

node.jsにおいて、文字列をbufferへ変換させたいときの方法をまとめます

環境
node.js 8.1.3

Bufferインスタンスをどう生成するか

node.js 8.x系におけるBufferインスタンスの作成において、 new Bufferは既にdepricatedになっている*1ため、ここではbufferインスタンスの作成はBuffer.allocを利用します。

Buffer | Node.js v8.1.3 Documentation

Buffer.alloc と Buffer.allocUnsafe

Buffer.alloc(size)の他に、インスタンス生成時にデータを初期化させないBuffer.allocUnsafe(size)があります。
Buffer.allocは、インスタンス生成時にデータをzero-filledする初期化コストがかかるため、Buffer.allocUnsafeよりパフォーマンスは劣りますが、sensitiveなデータを含まないことが保証されるので、速度を最優先させる場面以外において、基本的にBuffer.allocを使う方が良いかと思います。
https://nodejs.org/dist/latest-v8.x/docs/api/buffer.html#buffer_class_method_buffer_allocunsafe_size

文字列をbufferに変換する場合において、Buffer.allocを利用時にBufferサイズ指定する必要があります。 下記に文字列をBufferに変換するスクリプトにあるように、ここで毎回文字列のサイズを渡してBufferインスタンスを生成します。

function bufferFromString(str) {
  // Allocates a new Buffer of size bytes.
  // If fill is undefined, the Buffer will be zero filled;
  // set size of string length
  var buffer = Buffer.alloc(str.length)
  buffer.write(str)
  return buffer
}

const text1 = bufferFromString('hello');
const text2 = bufferFromString('wor');

console.log('----text1', text1);
console.log('----text2', text2);

// text1のbufferにworを書き込む
text1.write('wor');

// 先頭の3文字分 のみ更新させるため、4,5文字目は変わらない
// hello → worlo
console.log(text1.toString('utf8'));

ただ、欠点なのは毎回Buffer.allock(size)でインスタンスを生成した後、Buffer.write()でデータを書き込む必要があるため、若干冗長です。

Buffer.from

Buffer.fromの引数に文字列を渡すと、一発で文字列をBufferに変換できます。

https://nodejs.org/dist/latest-v8.x/docs/api/buffer.html#buffer_class_method_buffer_from_string_encoding

> Buffer.from("hello world")
<Buffer 68 65 6c 6c 6f 20 77 6f 72 6c 64>

まとめ

文字列からBufferへの変換方法をざっとまとめました。 Buffer.alloc Buffer.allocUnsafe Buffer.fromはそれぞれ挙動に違いがあるので、公式ドキュメントをちゃんと読んで、状況に応じて使い分けるとよいかと思います。

参考

Uncaught TypeError: Cannot read property 'split' of undefined · Issue #185 · Keyang/node-csvtojson · GitHub

Contributing to Fawn module.

Done Refactored lib scripts. by kakts · Pull Request #11 · e-oj/Fawn · GitHub

AWS LambdaからDynamo DBにデータを追加する

概要

AWS lambda functionが呼び出された際に、AWS dynamoDBのテーブルにデータを追加していく方法についてまとめます。

今回は、API Gateway と lambdaで構築した line botに対して送信されたデータを dynamoDBに追加していくところまで説明します。

IAMユーザの設定

AWSの IAM manageページから、新規にIAMユーザを作成します。
このユーザは、Dynamo DBに対する書き込み、読み込み権限そして、lambda関数からdynamoを使う権限を付与します。

「ユーザを追加ボタン」でユーザ名を入力し、「アクセスの種類」で、プログラムによるアクセスと、AWSマネジメントコンソールへのアクセスを許可します。

ユーザの作成を完了すると、 「アクセスキーID」と、「シークレットアクセスキー」が取得できます。 この2つをlambdaからdynamoDBへのアクセス時に必要なので、メモしておきます。

作成後、IAMユーザの設定画面に移り、「アクセス権限を追加」ボタンをおし、アクセス権限を追加していきます。
予めグループを作成し、そのグループにアクセス権限を追加する方法もありますが、今回はユーザに対してアクセス権限を追加します。

「アクセス許可の付与」画面で、「既存のポリシーを直接アタッチ」項目を選択し、もともとAWS側で用意されているポリシーをユーザに追加します。 DynamoDB用のアクセス権限を追加するため、「AmazonDynamoDBFullAccess」ポリシーを探し、選択します。

f:id:kakts:20170625154758p:plain

これでIAMユーザを作成し、そのユーザに対してlambdaからDynamoDBへのデータの読み書きする権限を付与できました。

DynamoDBの設定

DynamoDBにデータを追加するためのテーブルを作成します。

DynamoDBのマネジメントコンソールにアクセスし、「テーブルの作成」ボタンからテーブル作成画面に遷移します テーブルの設定はデフォルトにし、テーブル名、プライマリーキーを設定します。
プライマリーキーはDynamoDBのテーブル内のアイテム毎にユニークなIDのことです。 本稿では、テーブル名はmessage プライマリーキーは_id と設定していきます

f:id:kakts:20170625162946p:plain

これでDynamoDBのテーブルを作成することができました。

lambda関数実装

IAMユーザの作成、DynamoDBのテーブルも作成できたので、次にlambda関数で、linebotに対して送られたメッセージをDynamoDBに保存する関数を実装していきます。
前回の 「aws lambdaとapi gatewayで linebotを作成する」で作成したlambda関数をベースに、DynamoDBにデータを追加する処理を実装します。

AWSの各サービスに対するクライアントsdkは公式で用意されており、

kakts-tec.hatenablog.com

npm の aws-sdkモジュールがあるので、コレを利用します。
www.npmjs.com

DynamoDBへのアクセス時に、先程IAMユーザの作成時に生成されたAPIアクセスキーとシークレットアクセスキーを利用します。
コードに直接埋め込むのは管理上よくないので、lambdaの環境変数に指定します。

  • DYNAMO_API_ACCESS_KEY : DynamoDBアクセス用IAMユーザのアクセスキー
  • DYNAMO_USER_SECRET_ACCESS_KEY : DynamoDBアクセス用IAMユーザのシークレットアクセスキー

DynamoDB用クライアントインスタンスの作成は下記の用な感じで行います。
exports.handlerの内部で記述すると、関数が読み込まれるたびに初期化が走るため、exports.handlerの外部で初期化させます。

const https = require('https');
const AWS = require('aws-sdk');

// dynamoDBクライアントインスタンス初期化
// IAMユーザのAPIアクセスキー、シークレットアクセスキーを指定する。  
// region apiVersionも指定する。  
const dynamo = new AWS.DynamoDB({
    region: 'ap-northeast-1',
    apiVersion: '2012-08-10',
    accessKeyId: process.env.DYNAMO_API_ACCESS_KEY,
    secretAccessKey: process.env.DYNAMO_USER_SECRET_ACCESS_KEY
});

登録するデータは、linebotから来るリクエストデータにはいっている、ユーザID、メッセージタイプ(text や sticker[スタンプ]など)、ユーザが入力したテキスト を登録していきます。

メッセージが送信される毎にデータを登録していくため、ユニークキーであるidには一意なものを指定します。
ここでは、とりあえず $USERID
$DATETIME を指定します。

データの登録は、DynamoDBインスタンスのputItemメソッドを使って行います。

http://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/DynamoDB.html#putItem-property

上記ドキュメントに書かれているサンプルコードですが、パラメータにテーブル名、そして 追加するアイテムを指定してきます。
アイテム内の各アトリビュートは、文字列(S)や数値(N) など、それぞれ登録するデータの型をキーに指定します。 このあたりの仕様はドキュメントを参照してください。

/* This example adds a new item to the Music table. */

 var params = {
  Item: {
   "AlbumTitle": {
     S: "Somewhat Famous"
    }, 
   "Artist": {
     S: "No One You Know"
    }, 
   "SongTitle": {
     S: "Call Me Today"
    }
  }, 
  ReturnConsumedCapacity: "TOTAL", 
  TableName: "Music"
 };
 dynamodb.putItem(params, function(err, data) {
   if (err) console.log(err, err.stack); // an error occurred
   else     console.log(data);           // successful response
   /*
   data = {
    ConsumedCapacity: {
     CapacityUnits: 1, 
     TableName: "Music"
    }
   }
   */
 });

実際に書いたlambda関数は以下になります。

const https = require('https');
const AWS = require('aws-sdk');

// dynamoDBクライアントインスタンス初期化
// IAMユーザのAPIアクセスキー、シークレットアクセスキーを指定する。  
// region apiVersionも指定する。  
const dynamo = new AWS.DynamoDB({
    region: 'ap-northeast-1',
    apiVersion: '2012-08-10',
    accessKeyId: process.env.DYNAMO_API_ACCESS_KEY,
    secretAccessKey: process.env.DYNAMO_USER_SECRET_ACCESS_KEY
});

const tableName = 'message';

// linebotから送信されたメッセージデータから返信用メッセージデータを作成する
// 本稿では、来たメッセージをそのまま返す
function createResponseMessage(messageData) {
  const message = messageData.message;
  let resMessage;
  if (message.type === 'text') {
    // text
    resMessage = {
      type: message.type,
      text: message.text
    }
  } else if (message.type === 'sticker') {
    // stamp
    resMessage = {
      type: message.type,
      packageId: message.packageId,
      stickerId: message.stickerId
    };
  }

  return resMessage;
}

exports.handler = (event, context, callback) => {
    const messageData = event.events && event.events[0];
    const replyToken = messageData.replyToken;
    const message = messageData.message;
    const text = message.text;
    const source = messageData.source;
    const resMessage = createResponseMessage(messageData);
    const data = JSON.stringify({
       replyToken: replyToken,
       messages: [resMessage]
    });

    // LINE reply apiのレスポンス設定
    const Authorization = 'Bearer ' + process.env.ENTER_ACCESS_TOKEN;
    opts = {
        hostname: 'api.line.me',
        path: '/v2/bot/message/reply',
        headers: {
            "Content-type": "application/json; charset=UTF-8",
            "Authorization": Authorization
        },
        method: 'POST',
    };

    const req = https.request(opts, function(res) {
        res.on('data', function(res) {
            console.log(res.toString());
        }).on('error', function(e) {
            console.log('ERROR: ' + e.stack);
        });
    });
    req.write(data);
    req.end();

    // DynamoDBへのメッセージデータ追加
    const userId = source.userId;
    const params = {
        TableName: tableName,
        Item: {
            _id: {
                S: userId + '_' + Date.now(),
            },
            userId: {
                S: userId
            },
            messageType: {
                S: message.type
            }
        }
    };
    // スタンプでなく文字列が投稿されたとき、textアトリビュートを追加する
    if (message.type === 'text') {
        params.Item['text'] = {
            S: message.text
        };
    }

    dynamo.putItem(params, function(err, data) {
        if (err) {
            console.error("Error occured", err);
        }
        console.error(data);
    });
};

最後に、linebotに対してメッセージを送信したあと、DynamoDBコンソールでデータが追加されていることを確認します。 f:id:kakts:20170625165955p:plain