チャットプログラムのテスト

とあることがキッカケで、Rubyでシンプルなチャットプログラムを書いてみました。
チャットプログラムのザックリと要件を考えると以下の2点になる。

  • 受信したメッセージを適宜表示
  • 入力したメッセージを送信

ThreadとSTDIN/STDOUTの相性

要は「受信しつつ、送信」なので、Threadを使ったほうがよさそうな感じ。そこで慣れていないThread関係のテストプログラムを書いてたら、いきなり躓いた。そのコードは次のようなもの。

def recvLoop
  count = 0
  while true
    p "RECV:#{count}"   # とりあえず.
    count += 1
  end
end

def sendLoop
  while true
    p "SEND:"+gets  # 入力待ち.
  end
end

Thread.start{
    recvLoop
}
sendLoop

上記プログラムのダメなところは、sendLoop内のgetsでコンソール(標準入力)が待ち状態になり、recvLoopからの出力が行えなくなってしまうのです。
ウィンドウアプリなら入出力は別にするだろうし、コンソールアプリでも受信部と送信部を別々のプログラムにして別ウィンドウで処理したら問題ないのですが、このまま引き下がるのも悔しいのでなんとかならないか考えてみた。
「キー入力があったら、getsに遷移する」という方法でなんとかなるかもしれないと思って書いたコードが次のコード。(※参考サイト:小ネタいろいろ - Ruby Tips

require 'Win32API'
$kbhit = Win32API.new( 'msvcrt', '_kbhit', [], 'l')

def recvLoop
  count = 0
  while true
    p "RECV:#{count}"   # とりあえず.
    count += 1
  end
end

def sendLoop
  while true
    if $kbhit.call != 0 # キー入力があったら..
      p "SEND:"+gets    # getsを行う.
    end
  end
end

Thread.start{
    recvLoop
}
sendLoop

これでなんとか、コンソールアプリの問題点もクリアできた。
自分の入力中は受信出来てもすぐに表示できないけど、今回はしょうがないということで目をつむり、以前のサーバクライアントプログラムを参考にしつつ、とりあえず完成させてみた。

ちなみに、TCPServer#acceptのタイミングには注意する必要がある。メインスレッドでやるとそこで処理が止まってしまうので、別スレッドで行うようにした。

チャットテストスクリプトの完成

# testChat.rb
# チャットテスト

#---------- 変数定義 ----------#
require 'Win32API'
$kbhit = Win32API.new( 'msvcrt', '_kbhit', [], 'l')

$recvPort = "7700"    # 受信ポート
$recvSocket;
$friendIP = "192.168.0.27" # 接続先のIP
$friendSocket;

#---------- 関数定義 ----------#
def setup
  # 受信ポートを準備.
  $recvSocket = TCPServer.open($recvPort)
  
  # 送信ポートの準備.
  while true
    p "type friend IP >"
    inputIP = gets.chomp
    if inputIP == ""
      next
    end
    p "[#{inputIP}]"
    
    $friendIP = inputIP
    $friendSocket = TCPSocket.open($friendIP, $recvPort)
    break
  end
  
  print "=== chat with [#{$friendIP}] ===\n"
  return true
end

def recvLoop
  recv = $recvSocket.accept
  
  count = 0
  # 受信したら出力.
  while recv.gets
    print "RECV[#{count+=1}]:"+$_.chomp+"\n"
  end
end

def sendLoop
  while true
    if $kbhit.call != 0
      $friendSocket.write(gets)
    end
  end
end

#---------- 処理本編 ----------#
if setup
  Thread.start{
    recvLoop
  }
  sendLoop
end

「1対1でしか対応できていない」という大きな問題が残っていますが、とりあえず、最初のステップはここまで。

後で考えた

1対1以上の対応はどうしようか、と悩んで、後から気づいたのですが、UDPのブロードキャストを使えばよさそうですね。IP Messengerもメッセージのやり取りはUDPを使っているようですし。(ファイル添付時はTCP
と思ってテストしたら家のルータのパケットフィルタリングにはじかれました。設定しなおせばいいのですが、今はアレなので、UDPテストはまた後日。