言語ゲーム

とあるエンジニアが嘘ばかり書く日記

Twitter: @propella

Ruby で VoIP (IP 電話) を実装する

Squeak で説明しても WikiPhone の面白さがいまいちよく分からないと思うので、Ruby 1.8.5 で簡単な WikiPhone クライアントを実装してみます。今回出力する側と入力する側を別々に作ってみましたが、どちらも 40行ちょっとで書けます。

パイプとしての WikiPhone

WikiPhone は、ここでは単なるパイプのように振舞います。URL は、WikiPhone サーバ内であればなんでも使えます。メッセージの送受信はこのようにします。

送信側

$ echo "Hi there" | ./wpput.rb http://languagegame.org:9090/chat

受信側

$ ./wpget.rb http://languagegame.org:9090/chat
Hi there

Ruby + WikiPhone でファイル転送

WikiPhone をファイル転送に使ってみます。受信側から起動します。

$ ./wpget.rb http://languagegame.org:9090/chat > dst.html

次に送信側を起動します。

$ ./wpput.rb http://languagegame.org:9090/chat < src.html

ダサいですが、ファイルを受け取ったと思ったら受信側は自分で Ctrl + C します。

Ruby で IP 電話???

さて、いよいよ Ruby だけで VoIP の実験です。/dev/dsp をそのまま使います。送信側から。

$ ./wpput.rb http://languagegame.org:9090/voip < /dev/dsp

受信側です。

$ ./wpget.rb http://languagegame.org:9090/voip > /dev/dsp

RubySqueak WikiPhone の会話ログをとる。

/dev/dsp というのは、音声の入出力を扱うデバイスで、linuxcygwin 等で使えます。これは Squeak 純正の WikiPhone とは互換性が無いので、Squeak の WikiPhone の信号を扱いたければ変換する必要があります。gsm 22050 Hz を使っていますので、例えば会話のログをとるワンライナーは以下のようになります。

./wpget.rb http://languagegame.org:9090/phone | sox -t gsm -r22050 - -r22050 -c1 -sw -fs log.wav

以下に wpget.rb と wpput.rb のソースを載せます。サーバに興味のある方は http://www.squeaksource.com/WikiPhone.html と Squeak3.8 を使ってください。なお、languagegame.org は玄箱という小さなサーバで動かしていますので、遅くてよく止まります。

#!/bin/env ruby
# wpget.rb: WikiPhone GET client

require "socket"
require "uri"
require 'net/http'

def read_a_chunk sock
  dest = ""
  line = sock.readline
  hexlen = line.slice(/[0-9a-fA-F]+/) or
    raise Net::HTTPBadResponse, "wrong chunk size line: #{line.dump}"
  len = hexlen.hex
  break if len == 0
#  $stderr.print "length: #{len}\n"
  sock.read len, dest
  sock.read 2 # chop CRLF
  dest
end

def main
  if ARGV.size < 1 then
    $stderr.print "usage: wpget url\n"
    exit 1
  end
  
  uri = URI.parse ARGV.first
  socket = TCPSocket.new(uri.host, uri.port)
  socket.write "GET #{uri.path} HTTP/1.1\r\n\r\n"
  begin
    response = Net::HTTPResponse.read_new(Net::BufferedIO.new(socket))
  end while response.kind_of?(Net::HTTPContinue)
#  response.each { |name, value| $stderr.print "#{name} : #{value}\n" }

  while true
    print read_a_chunk(socket)
  end
end

$stdout.sync = true
main
#!/bin/env ruby
# wpput.rb: WikiPhone PUT client

require "socket"
require "uri"
require 'net/http'

def post_a_chunk (socket, data)
  len = data.length
  header = "#{len.to_s(16)};\r\n";
  socket.write(header)
  socket.write(data)
  socket.write("\r\n")
end

def main
  if ARGV.size < 1 then
    $stderr.print "usage: wpput.rb url < input\n"
    exit 1
  end
  
  uri = URI.parse ARGV.first
  socket = TCPSocket.new(uri.host, uri.port)
  socket.write "PUT #{uri.path} HTTP/1.1\r\n"
  socket.write "Transfer-Encoding: chunked\r\n\r\n"
  
  begin
    while true
      buffer = $stdin.readpartial 130
      post_a_chunk(socket, buffer)
    end
  rescue EOFError
  end

end

main