【Java】SocketChannelを利用したサーバと複数クライアント間のTCP通信

2018年6月9日

以前の記事で基本的なTCPソケット通信のサンプルを紹介しましたが、今回はノンブロッキングで処理できるSocketChannelによるTCP通信処理についてみていきます。

SocketChannelクラスはByteBuffer経由でデータのやり取りを行いますので、それに関してはこちらの記事を参考にしてください。ByteBufferはバイナリ操作が楽になるクラスなので覚えておいて損はないはずです。



SocketChannel

SocketChannelクラスはノンブロッキングで通信処理を行うことができるクラスです。通常のSocket通信では受信処理でスレッドが待機しますが、configureBlockingメソッドにfalseを設定すると、受信待機を行いません。受信ブロックを回避するためのスレッドを生成する必要がないわけですね。

SocketChannel channel = SocketChannel.open(new InetSocketAddress(ホスト名, ポート番号));

のようにプログラムを作ると指定したPCと通信ができるインスタンスを構築できます。

受信処理はread(ByteBuffer)メソッドを実行することで実装可能です。

送信処理はwrite(ByteBuffer)メソッドを実行することで実装可能です。

 

通信を切断する場合はcloseメソッドを呼び出しましょう。

ServerSocketChannel

ServerSocketChannelクラスはサーバ側実装に使用するSocketChannelクラスです。こちらも接続受付時のスレッド待機を無効化できます。接続待機をノンブロッキング化するにはconfigureBlockingメソッドでfalseに設定しましょう。

ServerSocketChannel channel = ServerSocketChannel.open();
channel.socket().bind(new InetSocketAddress(ポート番号));//受信ポートを指定

のように記述することで初期処理を行います。

acceptメソッドで接続を待ち受けます。

Socketクラスと同じ感じで、acceptメソッドの戻り値がSocketChannelとなり、このインスタンスでクライアントとデータをやり取りできます。

ServerSocketChannelを終了する場合はcloseメソッドを呼び出しましょう。

クライアントサーバー間をやり取りする簡単なサンプル

上記で説明したSocketChannelを使用し、クライアントサーバー通信をテストする簡単なプログラムを紹介します。とりあえず動作させたい場合は参考にしてみてください。

私の環境では同一PCでのテストも別のPC間でのテストも成功しました。

別のPCで接続テストを行いたい場合はまずIPアドレスを調べて、クライアントのホスト名指定で設定する必要があります。また、セキュリティの関係上ポート解放が必要なケースがあります。手っ取り早く試したい場合はファイアウォールを一時的に無効化し、試すのが簡単です。テストが終了した後は、必ずファイアウォールを有効化し直しておいてください。

IPアドレスを調べる方法

ファイアウォールの有効無効設定方法

クライアント側

Client.java
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
import java.util.Scanner;

public class Client {

	public static void main(String[] args) {
		try{
			//送信アドレスとポートを指定
			SocketChannel channel = SocketChannel.open(new InetSocketAddress("localhost", 10007));
			//SocketChannel channel = SocketChannel.open(new InetSocketAddress("192.168.11.21", 10007));//別PC間で通信したい場合IPやドメイン指定
			
			//送信バッファデータを構築(今回はint型をテストするので最低4バイトを確保)
			ByteBuffer bb = ByteBuffer.allocate(4);
			
			//操作説明
			System.out.println("送信する数値を入力してEnterで送信します。");
			
			//数値を入力させる(オーバーフローなどは考慮していない)
			bb.putInt(new Scanner(System.in).nextInt());
			
			//送信準備を行う
			bb.flip();
			
			//送信処理
			channel.write(bb);
			
			//切断
			channel.close();
			
			//送信データを表示します
			System.out.println("送信:"+bb.getInt(0));
		}catch(IOException e){
			e.printStackTrace();
		}
	}
}

サーバー側

Server.java
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
import java.nio.channels.ServerSocketChannel;

public class Server {

	public static void main(String[] args) {
		try{
			//サーバーソケットの開始
			ServerSocketChannel channel = ServerSocketChannel.open();
			
			//受信ポートを指定
			channel.socket().bind(new InetSocketAddress(10007));
			
			//接続待機
			SocketChannel sc = channel.accept();
			
			//バッファデータ(バイト配列)を作成(今回は4バイトのint型のみをテスト)
			ByteBuffer bb = ByteBuffer.allocate(4);
			
			//バッファ(バイト配列)に受信データを読み込み
			sc.read(bb);
			
			//ソケットチャンネルクローズ
			sc.close();
			
			//サーバーチャンネルクローズ
			channel.close();
			
			//intデータを受信データの0バイト目から読み込み
			System.out.println("受信:"+bb.getInt(0));
		}catch(IOException e){
			e.printStackTrace();
		}
	}
}
クライアント実行結果

送信する数値を入力してEnterで送信します。
345
送信:345

サーバー実行結果

受信:345

サンプルはサーバーを実行してからクライアントを実行してください。

セレクタの利用

ノンブロッキングでの通信処理を実装するための手助けをしてくれるクラス群が存在します。これらは接続要求が来たかどうかや、メッセージが送信されたかどうかなどを、複数のSocketChannelの監視をすることにより、検出します。

何らかの通信イベントが発生するまで、スレッドをブロックしたり、スレッドブロックなしで、要求があるかを判断できる機能などがあり、用途によって使い分けられます。

この機能はSelectableChannelが実装されたクラスはすべて利用できます。SocketChannelやServerSocketChannelもSelectableChannelを実装しています。

SelectableChannelにはregisterメソッドがあり、コレで接続要求監視を登録したり、受信データ監視を登録できます。

registerにはSelectorと監視タイプを指定します。

登録後に要求を受けた場合、SelectorのselectメソッドやselectNowメソッドがイベント数を返します。

selectメソッドは要求があるまでスレッド待機を行い、selectNowメソッドは要求があろうがなかろうが待機しません。

ServerSocketChannel channel = ServerSocketChannel.open();
channel.socket().bind(new InetSocketAddress(10009));//受信ポートを指定
channel.configureBlocking(false);//ノンブロックの有効化
Selector sel = Selector.open();

//接続要求監視を登録
channel.register(sel, SelectionKey.OP_ACCEPT);

//接続要求があるまでスレッド待機
while(sel.select()>0){

	//発生したイベントを取得
	Iterator it = sel.selectedKeys().iterator();
	while(it.hasNext()) {
		SelectionKey key = it.next();
		it.remove();
		if ( key.isAcceptable() ) {
			//接続処理
		}
	}
}

次のサンプルで実際に使用する場合のテストプログラムを用意しておりますので、そちらも参考にしていただければと思います。



 

サーバと複数クライアントを接続して送信メッセージをアニメーションで表示させてみる

せっかくノンブロッキングでの通信処理の実装を勉強したので、今回紹介した内容を有効活用した通信プログラムを簡単に作成してみました。

クライアント側からメッセージを入力してEnterを押下してもらうと、JFrameで実装したサーバー側プログラムに受信したメッセージが流れ始めるというプログラムになります。

複数のクライアント端末と同時に接続できるようにしています。試すのが面倒な方は実行結果の動画も用意しているので、そちらをご覧ください。

クライアント側

Client.java
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
import java.util.Scanner;
import java.nio.charset.StandardCharsets;

public class Client {

	public static void main(String[] args) {
		try(
			SocketChannel channel = SocketChannel.open(new InetSocketAddress("localhost", 10009));//ローカルで試す場合こっち
			//SocketChannel channel = SocketChannel.open(new InetSocketAddress("192.168.111.26", 10009));//接続先指定
			Scanner sc = new Scanner(System.in);//標準入力
		){

			System.out.println("送信する文字列を入力してEnterで送信します。ENDで終了します。");
			String text = null;
			
			//ENDが入力されるまで入力文字をサーバーに送信する
			while(!"END".equalsIgnoreCase(text = sc.next())) {
				ByteBuffer bb = StandardCharsets.UTF_8.encode(text);
				channel.write(bb);//送信
				System.out.println("送信:"+text);
			}
		} catch (IOException e) {
			System.out.println("切断されました。");
			e.printStackTrace();
		}
	}
}

サーバー側

Server.java
import java.awt.Graphics;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.nio.charset.StandardCharsets;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.Random;

import javax.swing.JFrame;
import javax.swing.JPanel;

public class Server {

	public static void main(String[] args) throws IOException {
		new Thread(new TestWindow()).start();
	}

	static class TestWindow extends JFrame implements Runnable{
		LinkedList comments = new LinkedList<>();
		ServerSocketChannel channel;
		Selector sel;
		ByteBuffer bb = ByteBuffer.allocate(1024);
		Random rand = new Random();
		public TestWindow() {
			setSize(640,480);
			add(new DrawCanvas());
			setVisible(true);
			initServer();
		}

		//サーバーソケットの開始
		void initServer(){
			try{
				channel = ServerSocketChannel.open();
				channel.socket().bind(new InetSocketAddress(10009));//受信ポートを指定
				channel.configureBlocking(false);
				sel = Selector.open();

				//接続要求を監視
				channel.register(sel, SelectionKey.OP_ACCEPT);
			} catch (IOException e) {
				System.out.println("サーバー開始エラー");
				e.printStackTrace();
			}
		}

		//接続受け付けメソッド
		void accept() {
			try {
				SocketChannel sc = channel.accept();
				if ( sc == null ) return;

				//非ブロックモードで接続リストに追加
				sc.configureBlocking(false);

				//受信を監視
				sc.register(sel, SelectionKey.OP_READ);
			} catch (IOException e) {
				e.printStackTrace();
			}
		}

		//データ受信メソッド
		void recv(SocketChannel sc) {
			try {
				//接続していないなら閉じる
				if ( !sc.isConnected() ) {
					System.out.println("接続終了");
					sc.close();
				}
				//バイナリ受信領域初期化
				bb.clear();
				
				//受信
				sc.read(bb);
				
				//ロード準備
				bb.flip();
				
				//文字列に変換
				String result = StandardCharsets.UTF_8.decode(bb).toString();
				if ( result.length() > 0 ) {
					System.out.println("受信:"+result);

					//ランダムな位置でメッセージを出現させる
					comments.add(new Comment(640, rand.nextInt(380)+50, result));
				}
			}catch(IOException e) {
				try {
					System.out.println("接続終了");
					sc.close();
				} catch (IOException e1) {}
			}
		}

		//通信処理
		void networkLogic() {
			try {
				//イベントが発生している場合は処理させます
				while(sel.selectNow()>0){
					Iterator it = sel.selectedKeys().iterator();
					while(it.hasNext()) {
						SelectionKey key = it.next();
						it.remove();
						if ( key.isAcceptable() ) {
							accept();
						} else if ( key.isReadable() ) {
							recv((SocketChannel) key.channel());
						}
					}
				}
			} catch (IOException e) {
				e.printStackTrace();
			}
		}

		//アニメーションループ
		@Override
		public void run() {
			while(true) {
				try {
					Thread.sleep(40);
					networkLogic();
					repaint();
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			}
		}

		//描画キャンバス
		class DrawCanvas extends JPanel{

			//描画
			public void paintComponent(Graphics g) {
				super.paintComponent(g);
				Iterator it = comments.iterator();
				while(it.hasNext()) {
					Comment comment = it.next();
					g.drawString(comment.comment, comment.x, comment.y);
					if ( comment.x + g.getFontMetrics().stringWidth(comment.comment) < 0 ) {
						it.remove();
					} else {
						comment.x--;
					}
				}
			}
		}

		//メッセージ情報
		class Comment{
			int x;
			int y;
			String comment;
			Comment(int x, int y, String comment){
				this.x = x;
				this.y = y;
				this.comment = comment;
			}
		}
	}
}
実行結果

実行結果の動画では右上、右下、左下のウィンドウがクライアントで左上がサーバープログラムです。3台のクライアントと1台のサーバーを想定したテスト動画となっております。

画質悪いのはごめんなさいm(_ _)m

Java

Posted by nompor