TCPサーバー・クライアントを作成する

動作確認用にTCPサーバーとクライアントを作ったので、まとめてみます。

サーバー

クライアントから受け取ったメッセージを返信するだけのサーバーです。

require 'socket'

port = if ARGV[0] then ARGV[0] else "echo" end
gate = TCPServer.open(port)

sock = gate.accept
gate.close

while msg = sock.gets
  sock.write(msg)
end
sock.close

クライアント

コンソールからの入力をサーバーに送るだけのクライアントです。

require 'socket'

host = if ARGV[0] then ARGV[0] else "localhost" end
port = if ARGV[1] then ARGV[1] else "echo" end
sock = TCPSocket.open(host, port)

while msg = STDIN.gets
  sock.write(msg)
  print sock.gets
end
sock.close

UDPサーバー・クライアントを作成する

これからTCPサーバーとクライアントを作るので、その前に以前作ったUDPサーバー・クライアントをまとめます。

サーバー

クライアントから受け取ったメッセージをプリントするだけのサーバーです。

require "socket"

port = if ARGV[0] then ARGV[0] else 10000 end
sock = UDPSocket.open
sock.bind("", port)

loop do
  msg, (afam, port, host, ip) = sock.recvfrom(1024)
  print "AFAM=" + afam + " PORT=" + port.to_s + " IP=" + ip
  print "MSG=" + msg
end

クライアント

コンソールからの入力をサーバーに送るだけのクライアントです。

require "socket"

host = if ARGV[0] then ARGV[0] else "localhost" end
port = if ARGV[1] then ARGV[1] else 10000 end
sock = UDPSocket.open

while msg = STDIN.gets
  begin
    sock.send(msg, 0, host, port)
  rescue
    puts "Send Error."
  end
end
sock.close

Armadilloでシリアル通信

Armadilloでシリアル通信の動作確認をするところまでをまとめてみます。

1.シリアルインターフェースの解放

出荷時のArmadilloのシリアルインターフェース(CON3)は、シリアルコンソールとして使用するよう設定されています。
まずは、これを解放します。


まず、ATDEでromfs以下にあるetc/inittabを編集してイメージファイルを更新します。

atmark@atde3:~/source/20100603/atmark-dist$ vi romfs/etc/inittab

::sysinit:/etc/init.d/rc
::respawn:/sbin/getty -L 115200 ttymxc1 vt102
::shutdown:/etc/init.d/reboot
::ctrlaltdel:/sbin/reboot


"::respawn:/sbin/getty -L 115200 ttymxc1 vt102"をコメントアウト

atmark@atde3:~/source/20100603/atmark-dist$ vi romfs/etc/inittab

::sysinit:/etc/init.d/rc
#::respawn:/sbin/getty -L 115200 ttymxc1 vt102
::shutdown:/etc/init.d/reboot
::ctrlaltdel:/sbin/reboot


保存したら、make imageを実行します。

atmark@atde3:~/source/20100603/atmark-dist$ make image


できあがったイメージファイルをArmadilloに転送します。
開発中はtftpブートが簡単なのでオススメです。

atmark@atde3:~/source/20100603/atmark-dist$ sudo cp images/*gz /var/lib/tftpboot/


Armadilloを再起動すると、シリアルコンソールにはログイン画面が出てこなくなります。

2.シリアル通信のテスト準備

シリアルポート(外側)とコンソール(内側)の間で文字をやりとりしてみます。

telnetでArmadilloにログインします。
atmark@atde3:~/source/20100603/atmark-dist$ telnet 192.168.1.119
Trying 192.168.1.119...
Connected to 192.168.1.119.
Escape character is '^]'.

atmark-dist v1.26.1 (AtmarkTechno/Armadillo-420)
Linux 2.6.26-at9 [armv5tejl arch]

armadillo420-0 login: guest
[guest@armadillo420-0 (ttyp0) ~]$ 

rootに変更します。

[guest@armadillo420-0 (ttyp0) ~]$ su -
Password: 
[root@armadillo420-0 (ttyp0) ~]# 
通信設定を確認します。

CON3には/dev/ttymxc1が割り当てられています。

[root@armadillo420-0 (ttyp0) ~]# stty -F /dev/ttymxc1
speed 115200 baud;
-brkint ixoff -imaxbel
TeraTermでシリアルポートに接続します。

シリアルポートの設定は以下の通りです。

  • ボーレート:115200
  • データ:8bit
  • パリティ:none
  • ストップ:1bit
  • フロー制御:none

3.シリアルポートから入力された文字を受信してみる

[root@armadillo420-0 (ttyp0) ~]# cat /dev/ttymxc1

これで、シリアルポートからの入力待機中になります。

teraterm側に文字を入力します。

maido
ookini

telnet側には以下のように表示されます。

[root@armadillo420-0 (ttyp0) ~]# cat /dev/ttymxc1
maido
ookini

4.シリアルポートに文字を送信してみる

telnet

[root@armadillo420-0 (ttyp0) ~]# echo "moukari macca?" > /dev/ttymxc1


TeraTerm

moukari macca?

ポイント

  • inittabを編集してシリアルコンソールを解放する
  • 初期状態では/dev/ttymxc1へのアクセス権がないのでrootにsuする
  • シリアルポートから受信するには"cat /dev/ttymxc1"
  • シリアルポートへ送信するには"echo "hogehoge" > /dev/ttymxc1"

ArmadilloでRubyを動かす

組み込みLinuxで開発を行う事になったんだけど、Cってやった事がないんです。
なので、最近メインで使っているRubyをまずは組み込んでみます。

前提条件

ATDE3にて、Atmark Dist、Linuxカーネルソースがビルド済みであるとします。

Rubyの含まれるパッケージ名を調べる

atmark@atde3:~$ dpkg -S /usr/bin/ruby1.8 
ruby1.8: /usr/bin/ruby1.8

Debianのサイトでパッケージを取得する

http://www.jp.debian.org/distrib/packagesの"パッケージディレクトリを検索"で"ruby1.8"を検索します。

するとDebian -- Errorで取得できる事が分かります。

Armadilloのアーキテクチャはarmelなので、Debian -- Errorをダウンロードします。

atmark@atde3:~$ mkdir ruby
atmark@atde3:~$ cd ruby
atmark@atde3:~/ruby$ wget http://ftp.jp.debian.org/debian/pool/main/r/ruby1.8/ruby1.8_1.8.7.72-3lenny1_armel.deb
--2010-07-12 16:30:40--  http://ftp.jp.debian.org/debian/pool/main/r/ruby1.8/ruby1.8_1.8.7.72-3lenny1_armel.deb
ftp.jp.debian.org をDNSに問いあわせています... 150.65.7.130, 202.229.186.27, 203.141.150.38, ...
ftp.jp.debian.org|150.65.7.130|:80 に接続しています... 接続しました。
HTTP による接続要求を送信しました、応答を待っています... 200 OK
長さ: 283162 (277K) [application/x-debian-package]
`ruby1.8_1.8.7.72-3lenny1_armel.deb' に保存中

100%[======================================>] 283,162     --.-K/s 時間 0.1s    

2010-07-12 16:30:41 (1.88 MB/s) - `ruby1.8_1.8.7.72-3lenny1_armel.deb' へ保存完了 [283162/283162]

ruby1.8の依存しているパッケージも取得する

Debian -- Errorにはruby1.8が依存しているパッケージも記載されているので、libc6、libruby1.8もダウンロードしておきます。

atmark@atde3:~/ruby$ wget http://ftp.br.debian.org/debian/pool/main/g/glibc/libc6_2.7-18lenny4_armel.deb
atmark@atde3:~/ruby$ wget http://ftp.jp.debian.org/debian/pool/main/r/ruby1.8/libruby1.8_1.8.7.72-3lenny1_armel.deb

以下のコマンドで依存しているライブラリを調べる事も出来ます。

atmark@atde3:~/ruby$ ldd /usr/bin/ruby1.8 
	linux-gate.so.1 =>  (0xb7f72000)
	libruby1.8.so.1.8 => /usr/lib/libruby1.8.so.1.8 (0xb7e83000)
	libpthread.so.0 => /lib/i686/cmov/libpthread.so.0 (0xb7e6a000)
	libdl.so.2 => /lib/i686/cmov/libdl.so.2 (0xb7e65000)
	libcrypt.so.1 => /lib/i686/cmov/libcrypt.so.1 (0xb7e33000)
	libm.so.6 => /lib/i686/cmov/libm.so.6 (0xb7e0d000)
	libc.so.6 => /lib/i686/cmov/libc.so.6 (0xb7cb2000)
	/lib/ld-linux.so.2 (0xb7f73000)

取得したパッケージを展開する

パッケージからバイナリを取り出します。

atmark@atde3:~/ruby$ dpkg -x ruby1.8_1.8.7.72-3lenny1_armel.deb temp-ruby
atmark@atde3:~/ruby$ dpkg -x libc6_2.7-18lenny4_armel.deb temp-libc
atmark@atde3:~/ruby$ dpkg -x libruby1.8_1.8.7.72-3lenny1_armel.deb temp-libruby

全部同じディレクトリに展開しても良いんですが、どれがどれなのか把握しておいた方が良さそうです。

展開されたバイナリをatmark-distのromfs以下へコピーする

lsすれば分かるとおり、romfs以下にはユーザーランドが格納されています。
該当するディレクトリに展開されたバイナリをコピーして、必要に応じてシンボリックリンクを作成します。

atmark@atde3:~/ruby$ ls ../source/20100603/atmark-dist/romfs/
bin  dev  etc  home  lib  linuxrc  mnt  proc  root  sbin  sys  tmp  usr  var
a
ln -s ruby1.8 ruby

make imageの実行

追加したバイナリを含んだイメージファイルを作成します。

atmark@atde3:~/source/20100603/atmark-dist$ make image

イメージファイルをArmadilloに転送する

imagesディレクトリ作成されたイメージファイルのうち、圧縮された方(gzの方)をArmadilloに転送します。

atmark@atde3:~/source/20100603/atmark-dist$ ll images/
合計 31832
-rwxr-xr-x 1 atmark atmark  3610340 2010-07-12 17:00 linux.bin
-rw-r--r-- 1 atmark atmark  1762122 2010-07-12 17:00 linux.bin.gz
-rw-r--r-- 1 atmark atmark 19852325 2010-07-12 17:00 romfs.img
-rw-r--r-- 1 atmark atmark  7316516 2010-07-12 17:00 romfs.img.gz

ポイント

  • 取得したバイナリ全てを含めたイメージファイルを作ると、メモリ容量をオーバーする

Armadilloのメモリ容量が小さいため、必要なバイナリ、ライブラリだけに絞ってイメージファイルを作成しないと入りきらないみたいです。
組み込みの面倒なところですね。

自分の場合は、まず必須のruby1.8を転送したところ、libruby1.8.so.1.8が無いというエラーが出たのでlibruby1.8をイメージに含めました。
librubyの中にも必須ではないライブラリがたくさんあるので、必要に応じて削除しなくちゃならないと思います。
libcはエラーが出たら転送したらいいかなと考えています。

dblinkで他のデータベースのテーブルを更新する

PostgreSQLでは、トリガを使うと、テーブルが更新されたら、別のテーブルを自動的に更新する事が出来ます。
では、データベースが異なる場合はどうするか?
トリガの中でdblinkを実行する事で対応可能です。

環境

インストール

インストール後、データベースへの登録SQLを実行する事で利用可能になります。

CentOSの場合は、Yumでインストール出来ます。

sudo yum install postgresql-contrib

postgresにsuしてから登録用SQLを実行します。

$ su -
# su postgres
bash-3.2$ psql pro9_production -f /usr/share/pgsql/contrib/dblink.sql

dblinkの実行方法

基本は

  1. コネクションを開いて(dblink_connect | dblink_connect_u)
  2. SQLを実行して(dblink | dblink_exec)
  3. コネクションを閉じる(dblink_disconnect)

です。

dblink関数に引き渡すSQLは文字列として渡す必要がある所が、この関数の面倒なところです。
何が面倒かって、シングルコーテーション(')を適切にエスケープしなきゃならない所です。

トリガーでdblinkを実行する例

CREATE OR REPLACE FUNCTION トリガー関数名()
  RETURNS trigger AS
$BODY$DECLARE
  remote_query text;
  local_query text;
BEGIN
  /* create connection */
  perform dblink_connect('dbname=<データベース名> user=<ユーザー名> password=<パスワード>');

  /* create query text */
  IF (TG_OP = 'UPDATE') THEN
    remote_query := 'UPDATE daily_machine_datas SET ('
      || 'company_id,center_id,model_id,machine_id,rec_date,rec_time,'
      || 'machine_no,created_at,updated_at) = ('
      || NEW.company_id || ','
      || NEW.center_id || ','
      || NEW.model_id || ','
      || NEW.machine_id || ','
      || '''''' || NEW.rec_date || ''''','
      || '''''' || NEW.rec_time || ''''','
      || NEW.machine_no || ','
      || '''''' || NEW.created_at || ''''','
      || '''''' || NEW.updated_at || ''''')'
      || ' WHERE id = ' || NEW.id;
  ELSIF (TG_OP = 'INSERT') THEN
    remote_query := 'INSERT INTO daily_machine_datas(' 
      || 'id,company_id,center_id,model_id,machine_id,rec_date,rec_time,'
      || 'machine_no,created_at,updated_at) VALUES ('
      || NEW.id || ','
      || NEW.company_id || ','
      || NEW.center_id || ','
      || NEW.model_id || ','
      || NEW.machine_id || ','
      || '''''' || NEW.rec_date || ''''','
      || '''''' || NEW.rec_time || ''''','
      || NEW.machine_no || ','
      || '''''' || NEW.created_at || ''''','
      || '''''' || NEW.updated_at || ''''')';
  END IF;
  
  local_query := 'select dblink_exec(''' || remote_query || ''')';

  /* execute query */
  EXECUTE local_query;
  
  /* delete connection */
  perform dblink_disconnect();

  RETURN NULL;

END;$BODY$
  LANGUAGE 'plpgsql' VOLATILE
  COST 100;

ポイント

日付、時刻はシングルコーテーションで囲む必要があり、エスケープする必要がある

dblink関数に渡すSQL文は文字列なので、シングルコーテーションはエスケープする必要があります。
日付、時刻はシングルコーテーションで囲む必要があるため、'が6回も連続する事になります。

リモートのテーブルをSELECTした結果を、ローカルに引っ張ってくる。逆は出来ない

取ってくる事は出来ても、送り出す事が出来ません。
どうしてもリモートにデータを送りたい場合は、回りくどいですが、リモート側にdblinkを導入して、
ローカル側のテーブルを読み取る関数を用意し、それをローカルからdblinkで実行すればOKです。

エラーへの対処

実行例は、開発マシンでは動くのに、実行マシンでは動きませんでした。
ログを見てみると認証エラーが発生していました。

ERROR:  password is required
DETAIL:  Non-superuser cannot connect if the server does not request a password.
HINT:  Target server's authentication method must be changed.

pg_hba.confでローカル接続をtrustにしているせいで、パスワード無しで接続しようとしてはねられているようです。

今回は、稼働中のシステムの認証方法を変更するのは気乗りがしなかったので、dblink_connectを使わず、dblink_connect_uを使う事で、対処しました。

dblink_connect_uは、初期値ではpotgres以外は実行できないようになっているので、potgresでログインし、実行権限をユーザーに付与する必要があります。

GRANT EXECUTE ON FUNCTION dblink_connect_u(text) TO <ユーザー名>;

コードは以下のように書き直します。

  /* create connection */
  perform dblink_connect_u('dbname=<接続先データベース名>');

以上で、無事に動くようになりました。

CSVファイルのインポート、エクスポート

PostgreSQLへのテストデータのロードにはcopyコマンドが使えます。

環境

ポイント

  • Windows環境では"\"を二回続ける
  • ファイルの一行目がヘッダーの場合は、"header"オプションを付ける
  • 文字コードの設定を行う(行わないと文字化けします)

文字コードの設定

SET client_encoding TO 'SJIS';

Excelで保存したファイルは文字コードがShift-JISなので、SQL実行までに変更してしまいます。

全カラム出力

copy books to E'c:\\csv\\books.csv' csv header ;

指定カラムのみ出力

copy books(name,price) to E'c:\\csv\\books2.csv' csv header ;

指定カラムのみ入力

copy books(name,price) from E'c:\\csv\\books4.csv' csv header ;

文字コードの設定

SET client_encoding TO 'UTF8';
select * from books;

サーバーの文字コードに合わせて、元に戻します。

複数レコードの一括更新

複数レコードの一括更新を実装しました。

最初はインプレース編集で実装したんですが、一件ごとの更新に意外と時間がかかるので、一括更新で再実装しました。

環境

View

ポイントは"select"の引数"machine_mode[]"です。

<td><%= select "machine_mode[]", "mode_no", [1, 2, 3, 4, 5, 6] %></td>

このようにすると、パラメータは、

"machine_mode"=>{"37"=>{"mode_no"=>"2", "reset_flag"=>"false"}, "38"=>{"mode_no"=>"1", "reset_flag"=>"false"}}

と言う、ハッシュになります。

上記の通り、自動的にidがハッシュのキーになります。

<% form_remote_tag :url => {:controller => "machine_modes", :action => "update" } do -%>
<p><%= submit_tag t('button.Update')  %><%= hidden_field_tag "model_id", @model_id %></p>
<table class="table">
  <tr>
    <th><%= t('activerecord.attributes.machine.machine_no') %></th>
    <th><%= t('activerecord.attributes.machine_mode.mode_no') %></th>
    <th><%= t('activerecord.attributes.machine_mode.reset_flag') %></th>
    <th><%= t('activerecord.attributes.machine_mode.elapsed_days') %></th>
  </tr>
  <% for @machine_mode in @machine_modes %>
  <tr>
    <td><%= @machine_mode.machine.machine_no %></td>
    <td><%= select "machine_mode[]", "mode_no", [1, 2, 3, 4, 5, 6] %></td>
    <td><%= select "machine_mode[]", "reset_flag", [["はい", true],["いいえ",false]] %></td>
    <td><%= @machine_mode.elapsed_days %></td>
  </tr>
  <% end %>
</table>
<% end -%>

Controller

updateメソッドは、最初の引数にidの配列を、2番目の引数に属性の配列を指定すると、指定したidの数だけ更新処理を行ってくれます。

ids = params[:machine_mode].keys
attributes = params[:machine_mode].values
MachineMode.update(ids, attributes)

参考にしたページ

参考にした書籍

P.254-255を参考にしました。

Ruby on Rails 逆引きクイックリファレンス Rails 2.0対応
大場 寧子 大場 光一郎 久保 優子
毎日コミュニケーションズ
売り上げランキング: 251455