テキストチャット (J)
簡単な Chat プログラム
さっきは、一方向でデータ通信してみましたが、今度は、双方向に通信してみましょう。
作成するのは、簡単なチャットです。
- ChatServer が、指定ポートで待っている
- ChatClient が、ホスト名とポート番号を指定して接続を行う
- 接続完了後は、クライアント、サーバが交互に一行ずつメッセージを交換する。
- どちらかが exit という入力を入れたら終了
動作イメージは、こんな感じ
server 側 client 側
[起動]
Open a server socket and wait on it.
[起動]
Connecting to localhost/127.0.0.1:50011
Connection established Connection established
Client Input> clientMsg0 [入力]
Client says: clientMsg0 [受信]
Server Input> serverMsg0 [入力]
Server says: serverMsg0 [受信]
Client Input> client3 [入力]
Client says: client3 [受信]
Server Input> exit [入力]
normal termination Server says: exit [受信]
normal termination
まずは、動かしてみて、イメージを掴んでください。
実行の際に、ターミナルを二つ同時に見れないと困りますよね。
- InteliJ の場合は、実行タブが二つあるので、一方を右か下の領域に drag すると、画面分割できるようになります。
- eclipse の場合は、先にコンソールをもう一つ作成してから、コンソールタブを drag することでコンソールを同時に二つ出せます。各コンソールで、適切なプログラム実行を選んでください。(下図参照)
- 表示対象を固定しないと、コンソールが表示対象を切り替えてきます。
接続
動きを把握したところで、プログラムを眺めていきましょう。
connection を張るまでは、先ほどの例と差はありません。 違うのは、今回は双方向通信なので、Server, Client 両側で、Input/OutputStream を両方開いている点ですね。
サーバ側
System.out.println("Open a server socket and wait on it.");
try (ServerSocket serverSocket = new ServerSocket(portNum);
Socket socket = serverSocket.accept() /* 接続待ち */) {
System.out.println("Connection established"); /* 接続完了 */
receiveAndSend(socket);
}
- 接続開始〜確立は同じ
- socket を用いて
receiveAndSend(socket)
を実行
クライアント側
/* IP address の取得 */
InetAddress host = InetAddress.getByName(hostName);
System.out.println("Connecting to " + host + ":" + portNum);
try (Socket socket = new Socket(host, portNum)){
System.out.println("Connection established");
sendAndReceive(socket);
}
- 接続開始〜確立は同じ
- socket を用いて
sendAndReceive(socket)
を実行
交互に送受信
サーバ(受信・送信を交互に)
Scanner userIn = new Scanner(System.in);
try (Scanner netIn = new Scanner(socket.getInputStream());
PrintWriter netOut = new PrintWriter(socket.getOutputStream())) {
while (true) {
String recvMsg = netIn.nextLine();
System.out.println("Client says: " + recvMsg);
if (recvMsg.equals("exit"))
break;
System.out.print("Server Input> ");
System.out.flush();
String userMsg = userIn.nextLine();
netOut.println(userMsg);
netOut.flush();
if (userMsg.equals("exit"))
break;
}
System.out.println("normal termination");
}
主なI/O
- userIn: Scanner, ユーザ入力の取り込み用
- java.util.Scanner: InputStreamから一行単位読み込む(nextLine())のに利用
- netIn: Scanner, クライアントからの受信用
- netOut: PrintWriter, クライアントへの送信用
- java.io.PrintWriter: OutputStream に出力 (println()など)するのに利用
プログラム見てもらえば、交互に送受信しているだけですね。
注意点
としては、送信時に flush()
している点です。これは、送信したつもりでバッファに残っていたりすると、
- 送信側:送ったつもり(実はバッファに溜まっている)
- 受信側:受け取り待ち
とかなって、処理が進まなくなってしまうからです。(java.io.PrintWriter の自動フラッシュ機能をON にしている場合は、println()
がflush()
もしてくれます。自動フラッシュを ON にするコンストラクタもあるので、そっちを使ってもらっても構いません。)
クライアント(受信・送信を交互に)
受信と送信の順序が逆になっている以外は、処理の流れはまったく同じです。
Scanner userIn = new Scanner(System.in);
try (Scanner netIn = new Scanner(socket.getInputStream());
PrintWriter netOut = new PrintWriter(socket.getOutputStream())){
while (true) {
System.out.print("Client Input> ");
System.out.flush();
String userMsg = userIn.nextLine();
netOut.println(userMsg);
netOut.flush();
if (userMsg.equals("exit"))
break;
String recvMsg = netIn.nextLine();
System.out.println("Server says: " + recvMsg);
if (recvMsg.equals("exit"))
break;
}
System.out.println("normal termination");
}
まとめ
交互に通信
実は、今回のプログラム互い違いに通信してました。逆にいうと、双方のユーザが好きなタイミングでメッセージを送ることができていません。 それは、プログラムが、「標準入力を待つか」「ネット入力を待つか」のどちらかしかできないからです。一度待ち始めると、途中で止めづらい。 で、このあたりはマルチスレッドをつかうと簡単に解消できるのですが、それはまた後日。
TCP 接続
今回あつかったのは TCP 接続です。 2点間の通信を担当し、パケット落ちなどがあると再送手続きなどしてくれます。 一方で、ネットワークの勉強をした人なら、 UDP 接続というのも聞いたことがあるかと思います。 パケットを直接おくるためのもので、マルチキャストやブロードキャストも可能ですが、パケット落ちなどがあっても、利用者側で対応しなくてはいけません。
今回は、再送制御などは TCP にお任せしています。